restructure: move el compiler content into lang/
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
target/
|
||||
*.elc
|
||||
*.sealed
|
||||
*.map.json
|
||||
.el/
|
||||
.claude/
|
||||
engram-data/
|
||||
engram-data-tx-log/
|
||||
+131
@@ -0,0 +1,131 @@
|
||||
# El Language — Agent Guide
|
||||
|
||||
El is a self-hosting, statically-typed language that compiles to C. This file orients agents that work on El itself or on programs written in El.
|
||||
|
||||
---
|
||||
|
||||
## What El Is
|
||||
|
||||
El compiles `.el` source → C → native binary. Every El value is `el_val_t` (int64_t). Strings are heap pointers cast through int64_t. The compiler is written in El (self-hosting).
|
||||
|
||||
**The compiler pipeline:**
|
||||
```
|
||||
elc-cli.el
|
||||
└─ imports: compiler.el
|
||||
└─ imports: lexer.el, parser.el, codegen.el, codegen-js.el
|
||||
```
|
||||
|
||||
The canonical compiler binary is `dist/platform/elc`. It was produced by running an earlier version of itself on `elc-cli.el`.
|
||||
|
||||
---
|
||||
|
||||
## The Two Layers — Know Which One You're In
|
||||
|
||||
### Layer 1: El programs (`.el` files)
|
||||
|
||||
This is where almost all work belongs. El programs are source files that get compiled by `elc`. New library functions, application logic, and language-level utilities all go here as `.el` files.
|
||||
|
||||
**Do not add C code when El can express it.** If functionality can be built from existing El primitives (string ops, `exec`, `fs_read/write`, `http_post`, etc.), write it in El.
|
||||
|
||||
### Layer 2: The C seed (`el-compiler/runtime/el_seed.c`)
|
||||
|
||||
This is the self-contained C OS-boundary layer. It provides the `__`-prefixed primitives that compiled El programs call: libcurl HTTP, pthreads, filesystem I/O, arena allocation, etc. It is **not generated** — it is maintained by hand.
|
||||
|
||||
The old `el_runtime.c` has been archived to `el-compiler/runtime/legacy/`. The runtime is now native El (`runtime/*.el`). `el_seed.c` replaces `el_runtime.c` as the sole C compilation dependency.
|
||||
|
||||
**Only edit `el_seed.c` when you genuinely need OS-level access** (raw sockets, GPU calls, new libcurl features). For everything else, write El.
|
||||
|
||||
When you do add a C builtin:
|
||||
1. Add the C function to `el_seed.c`
|
||||
2. Declare it in `el_seed.h`
|
||||
3. Add it to the `builtin_arity` table in `el-compiler/src/codegen.el` (so the compiler knows the arg count)
|
||||
4. Rebuild the elc binary (see below)
|
||||
|
||||
---
|
||||
|
||||
## Rebuilding the Compiler
|
||||
|
||||
After changing any `.el` source in `el-compiler/src/`:
|
||||
|
||||
```bash
|
||||
cd /Users/will/Development/neuron-technologies/foundation/el
|
||||
./dist/platform/elc elc-cli.el > elc-new.c
|
||||
cc -std=c11 -I el-compiler/runtime -lcurl -lpthread \
|
||||
-o dist/platform/elc-new \
|
||||
elc-new.c el-compiler/runtime/el_seed.c
|
||||
# Verify self-hosting:
|
||||
./dist/platform/elc-new elc-cli.el > elc-verify.c
|
||||
diff elc-new.c elc-verify.c # should be identical
|
||||
mv dist/platform/elc-new dist/platform/elc
|
||||
```
|
||||
|
||||
After changing `el_seed.c` only (no El source changes), rebuild downstream programs but do NOT need to rebuild the compiler binary itself — the seed is linked at the application level, not the compiler level.
|
||||
|
||||
---
|
||||
|
||||
## How El Programs Are Built
|
||||
|
||||
Each El application has a `build.sh` that:
|
||||
1. Concatenates all `.el` source files (stripping `import` lines)
|
||||
2. Runs `elc` to produce a `.c` file
|
||||
3. Runs `cc` linking against `el_seed.c`
|
||||
|
||||
Example (cgi-studio daemon):
|
||||
```bash
|
||||
cd products/cgi-studio/el-daemon
|
||||
./build.sh
|
||||
```
|
||||
|
||||
When you add a new `.el` file to an application, add it to that application's `build.sh` concat list.
|
||||
|
||||
---
|
||||
|
||||
## Parallelism in El
|
||||
|
||||
El is single-threaded at the application level. Parallelism is achieved through subprocess fan-out:
|
||||
|
||||
```el
|
||||
// Pattern: write payloads to temp files, exec bash script with & and wait,
|
||||
// read results back from temp files.
|
||||
fn http_post_parallel(urls: [String], bodies: [String]) -> [String] {
|
||||
// ... bash fan-out via exec() ...
|
||||
}
|
||||
```
|
||||
|
||||
Use `exec()` (blocking) or `exec_bg()` (fire-and-forget) with shell scripts to run concurrent work. There is no goroutine or async/await — parallelism goes through the OS process layer.
|
||||
|
||||
---
|
||||
|
||||
## Key Files
|
||||
|
||||
| Path | What it is |
|
||||
|------|-----------|
|
||||
| `dist/platform/elc` | Canonical compiler binary (arm64 Mac) |
|
||||
| `el-compiler/src/codegen.el` | Code generator — builtin arity table lives here |
|
||||
| `el-compiler/src/lexer.el` | Lexer |
|
||||
| `el-compiler/src/parser.el` | Parser |
|
||||
| `el-compiler/runtime/el_seed.c` | Self-contained C OS-boundary layer (replaces el_runtime.c) |
|
||||
| `el-compiler/runtime/el_seed.h` | Seed header (C function declarations) |
|
||||
| `spec/language.md` | Language specification |
|
||||
| `BOOTSTRAP.md` | How to recover the compiler from scratch |
|
||||
| `elc-cli.el` | Compiler entry point |
|
||||
| `elc-combined.el` | Pre-merged single-file compiler (used during early bootstrap) |
|
||||
|
||||
---
|
||||
|
||||
## HTTP Timeout
|
||||
|
||||
The El HTTP client (libcurl) defaults to **60 seconds**. Override per-process via `EL_HTTP_TIMEOUT_MS` env var. Set it before spawning any subprocess that makes long API calls:
|
||||
|
||||
```el
|
||||
exec("EL_HTTP_TIMEOUT_MS=300000 " + SOME_BIN + " " + args + " 2>&1")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Rules
|
||||
|
||||
- New library functions → write in El
|
||||
- New OS/hardware primitives → write in C and register in `codegen.el` arity table
|
||||
- Never edit `dist/platform/elc` directly — always rebuild from source
|
||||
- Never modify `el_seed.c` to add functionality that El can express
|
||||
@@ -0,0 +1,979 @@
|
||||
# El Language Bootstrap Guide
|
||||
|
||||
This document is the authoritative guide for reconstructing the El compiler toolchain from scratch. If the bootstrap binary at `dist/platform/elc` is ever lost, this document is the path back.
|
||||
|
||||
---
|
||||
|
||||
## 1. The Bootstrap Chain (Current State)
|
||||
|
||||
### The Trust Chain
|
||||
|
||||
El is a self-hosting language. The compiler is written in El. This creates a circular dependency: you need an El compiler to compile the El compiler. The chain is resolved by a seed binary:
|
||||
|
||||
```
|
||||
dist/platform/elc (Mach-O arm64 native binary)
|
||||
↓
|
||||
compiles elc-cli.el
|
||||
↓
|
||||
new self-hosted elc binary
|
||||
↓
|
||||
compiles itself again (identity check)
|
||||
↓
|
||||
stable self-hosted compiler
|
||||
```
|
||||
|
||||
The binary at `dist/platform/elc` is a **Mach-O 64-bit arm64 executable**. The `elc.preselfhost` and `elc.legacy` files in the same directory are older snapshots kept as fallback checkpoints.
|
||||
|
||||
The key property: every binary in `dist/platform/` was produced by compiling the El source in `el-compiler/src/` using a previous version of that same binary. The chain is auditable: the source is the ground truth, not the binary.
|
||||
|
||||
### The Self-Hosting Pipeline
|
||||
|
||||
```
|
||||
elc-cli.el
|
||||
imports → el-compiler/src/compiler.el
|
||||
imports → el-compiler/src/lexer.el
|
||||
imports → el-compiler/src/parser.el
|
||||
imports → el-compiler/src/codegen.el
|
||||
imports → el-compiler/src/codegen-js.el
|
||||
```
|
||||
|
||||
Import resolution is textual. `compiler.el` recursively inlines all imported `.el` files before lex/parse. The result is one large unified source string that the compiler then processes in a single pass.
|
||||
|
||||
`elc-combined.el` in the repo root is a pre-merged single-file edition used during early bootstrap iterations.
|
||||
|
||||
### What the Bootstrap Binary Actually Is
|
||||
|
||||
The `dist/platform/elc` binary is a compiled El program that was produced by running an earlier version of itself on `elc-cli.el`. It is not a Rust binary. The `elc.legacy` and `elc.preselfhost` checkpoints suggest the chain has been continuously self-hosting and re-stamped. The original genesis compiler (referenced in the language spec as a "Rust genesis compiler") was used to produce the first self-hosted binary; that Rust binary is not present in this repo.
|
||||
|
||||
To rebuild the current binary from source using the current binary:
|
||||
|
||||
```bash
|
||||
cd /path/to/el
|
||||
./dist/platform/elc elc-cli.el elc-new.c
|
||||
cc -std=c11 -I el-compiler/runtime -lcurl -lpthread \
|
||||
-o dist/platform/elc-new \
|
||||
elc-new.c el-compiler/runtime/el_runtime.c
|
||||
```
|
||||
|
||||
Verify self-hosting by using `elc-new` to recompile itself and diffing the outputs.
|
||||
|
||||
---
|
||||
|
||||
## 2. The Language
|
||||
|
||||
### 2.1 Lexical Structure
|
||||
|
||||
El source is UTF-8. File extension `.el`. Comments are single-line only: `//` to end of line.
|
||||
|
||||
**Token representation:** every token is a map `{ "kind": String, "value": String }`.
|
||||
|
||||
**Keywords** — from `keyword_kind()` in `lexer.el`:
|
||||
|
||||
| Keyword | Token Kind | Notes |
|
||||
|---------|-----------|-------|
|
||||
| `let` | `Let` | variable binding |
|
||||
| `fn` | `Fn` | function definition |
|
||||
| `type` | `Type` | struct definition |
|
||||
| `enum` | `Enum` | enum definition |
|
||||
| `match` | `Match` | pattern match |
|
||||
| `return` | `Return` | function return |
|
||||
| `if` | `If` | conditional |
|
||||
| `else` | `Else` | |
|
||||
| `for` | `For` | iteration |
|
||||
| `in` | `In` | used in `for x in list` |
|
||||
| `while` | `While` | loop |
|
||||
| `import` | `Import` | module import |
|
||||
| `from` | `From` | `from mod import { Name }` |
|
||||
| `as` | `As` | (reserved, no parse form) |
|
||||
| `with` | `With` | (reserved) |
|
||||
| `sealed` | `Sealed` | (reserved) |
|
||||
| `activate` | `Activate` | (reserved) |
|
||||
| `where` | `Where` | (reserved) |
|
||||
| `test` | `Test` | (reserved) |
|
||||
| `seed` | `Seed` | (reserved) |
|
||||
| `assert` | `Assert` | (reserved) |
|
||||
| `protocol` | `Protocol` | (reserved) |
|
||||
| `impl` | `Impl` | (reserved) |
|
||||
| `retry` | `Retry` | reserved / soft keyword in expr position |
|
||||
| `times` | `Times` | reserved / soft keyword |
|
||||
| `fallback` | `Fallback` | reserved / soft keyword |
|
||||
| `reason` | `Reason` | reserved / soft keyword |
|
||||
| `parallel` | `Parallel` | reserved / soft keyword |
|
||||
| `trace` | `Trace` | reserved / soft keyword |
|
||||
| `requires` | `Requires` | reserved / soft keyword |
|
||||
| `deploy` | `Deploy` | reserved / soft keyword |
|
||||
| `to` | `To` | reserved / soft keyword |
|
||||
| `via` | `Via` | reserved / soft keyword |
|
||||
| `target` | `Target` | **RESERVED — cannot use as identifier** |
|
||||
| `true` | `Bool` | literal value `true` |
|
||||
| `false` | `Bool` | literal value `false` |
|
||||
| `cgi` | `Cgi` | CGI identity block |
|
||||
| `service` | `Service` | service declaration block |
|
||||
| `manager` | `Manager` | VBD role decorator / soft keyword |
|
||||
| `engine` | `Engine` | VBD role decorator / soft keyword |
|
||||
| `accessor` | `Accessor` | VBD role decorator / soft keyword |
|
||||
| `vessel` | `Vessel` | soft keyword |
|
||||
| `extern` | `Extern` | `extern fn` forward declaration |
|
||||
|
||||
**Soft keywords** (`target`, `to`, `via`, `deploy`, `reason`, `times`, `fallback`, `retry`, `parallel`, `trace`, `requires`, `where`, `as`, `with`, `manager`, `engine`, `accessor`, `vessel`): these have dedicated token kinds but the parser re-interprets them as `Ident` nodes when they appear in expression position (e.g., as parameter names or local variable names).
|
||||
|
||||
**All token kinds:**
|
||||
|
||||
| Kind | Pattern |
|
||||
|------|---------|
|
||||
| `Int` | `[0-9]+` |
|
||||
| `Float` | `[0-9]+ '.' [0-9]+` |
|
||||
| `Str` | `"…"` with `\"`, `\n`, `\t`, `\r`, `\\` escapes |
|
||||
| `Bool` | `true` or `false` |
|
||||
| `Ident` | `[a-zA-Z_][a-zA-Z0-9_]*` (not a keyword) |
|
||||
| keyword tokens | one per keyword above |
|
||||
| `Eq` | `=` |
|
||||
| `EqEq` | `==` |
|
||||
| `NotEq` | `!=` |
|
||||
| `Not` | `!` |
|
||||
| `Lt` / `LtEq` / `Gt` / `GtEq` | `<` `<=` `>` `>=` |
|
||||
| `And` | `&&` (single `&` is consumed and discarded) |
|
||||
| `Or` | `\|\|` |
|
||||
| `Pipe` | `\|` |
|
||||
| `PipeOp` | `\|>` |
|
||||
| `Plus` / `Minus` / `Star` / `Slash` | `+` `-` `*` `/` |
|
||||
| `Percent` | `%` |
|
||||
| `Arrow` | `->` |
|
||||
| `FatArrow` | `=>` |
|
||||
| `Colon` / `ColonColon` | `:` `::` |
|
||||
| `LParen` / `RParen` | `(` `)` |
|
||||
| `LBrace` / `RBrace` | `{` `}` |
|
||||
| `LBracket` / `RBracket` | `[` `]` |
|
||||
| `Comma` / `Dot` / `Semicolon` | `,` `.` `;` |
|
||||
| `At` | `@` |
|
||||
| `QuestionMark` | `?` |
|
||||
| `Eof` | end-of-input sentinel |
|
||||
|
||||
**String comment stripping:** the lexer contains a special heuristic for string literals that embed JavaScript or CSS (`looks_like_code`). If a string contains `<script`, `<style`, or `function` + `;`, the lexer strips `//` and `/* */` comments from the string value before producing the `Str` token. This is a compile-time content sanitization pass.
|
||||
|
||||
### 2.2 AST Node Types
|
||||
|
||||
Every AST node is a `Map<String, Any>`. The `"expr"` or `"stmt"` key names the node type.
|
||||
|
||||
**Expression nodes:**
|
||||
|
||||
| `expr` value | Fields | Meaning |
|
||||
|-------------|--------|---------|
|
||||
| `Int` | `value: String` | integer literal |
|
||||
| `Float` | `value: String` | float literal |
|
||||
| `Str` | `value: String` | string literal |
|
||||
| `Bool` | `value: String` | `"true"` or `"false"` |
|
||||
| `Nil` | — | null / missing |
|
||||
| `Ident` | `name: String` | identifier reference |
|
||||
| `BinOp` | `op: String`, `left`, `right` | binary operation |
|
||||
| `Not` | `inner` | unary `!` |
|
||||
| `Neg` | `inner` | unary `-` |
|
||||
| `Call` | `func`, `args: [expr]` | function call |
|
||||
| `Field` | `object`, `field: String` | `obj.field` |
|
||||
| `Index` | `object`, `index` | `obj[idx]` |
|
||||
| `Array` | `elems: [expr]` | `[e1, e2, …]` |
|
||||
| `Map` | `pairs: [{ key: String, value: expr }]` | `{ "k": v, … }` |
|
||||
| `If` | `cond`, `then: [stmt]`, `else: [stmt]`, `has_else: Bool` | conditional expression |
|
||||
| `For` | `item: String`, `list`, `body: [stmt]` | for-in expression |
|
||||
| `Match` | `subject`, `arms: [{ pattern, body }]` | pattern match |
|
||||
| `DurationLit` | `count: String`, `unit: String` | `30.seconds`, `1.hour` |
|
||||
| `Try` | `inner` | postfix `?` (no-op passthrough today) |
|
||||
|
||||
**Binary operators** (`op` field values): `Plus`, `Minus`, `Star`, `Slash`, `EqEq`, `NotEq`, `Lt`, `Gt`, `LtEq`, `GtEq`, `And`, `Or`.
|
||||
|
||||
**Operator precedence** (higher = tighter binding):
|
||||
|
||||
| Level | Operators |
|
||||
|-------|-----------|
|
||||
| 6 | `Star`, `Slash` |
|
||||
| 5 | `Plus`, `Minus` |
|
||||
| 4 | `Lt`, `Gt`, `LtEq`, `GtEq` |
|
||||
| 3 | `EqEq`, `NotEq` |
|
||||
| 2 | `And` |
|
||||
| 1 | `Or` |
|
||||
|
||||
**Pattern nodes** (used inside `Match` arms):
|
||||
|
||||
| `pattern` value | Fields | Meaning |
|
||||
|----------------|--------|---------|
|
||||
| `Wildcard` | — | `_` — always matches |
|
||||
| `Binding` | `name: String` | binds subject to name |
|
||||
| `LitInt` | `value: String` | integer literal pattern |
|
||||
| `LitStr` | `value: String` | string literal pattern |
|
||||
| `LitBool` | `value: String` | boolean literal pattern |
|
||||
|
||||
**Statement nodes:**
|
||||
|
||||
| `stmt` value | Fields | Meaning |
|
||||
|-------------|--------|---------|
|
||||
| `Let` | `name: String`, `value: expr`, `type: String` | variable binding |
|
||||
| `Assign` | `name: String`, `value: expr` | bare reassignment `name = expr` |
|
||||
| `Return` | `value: expr` | return statement |
|
||||
| `While` | `cond: expr`, `body: [stmt]` | while loop |
|
||||
| `For` | `item: String`, `list: expr`, `body: [stmt]` | for-in loop |
|
||||
| `FnDef` | `name: String`, `params: [param]`, `body: [stmt]`, `ret_type: String`, `decorator?: String` | function definition |
|
||||
| `ExternFn` | `name: String`, `params: [param]`, `ret_type: String` | forward declaration |
|
||||
| `TypeDef` | `name: String`, `fields: [{ name: String }]` | struct type definition |
|
||||
| `EnumDef` | `name: String`, `variants: [{ name: String }]` | enum definition |
|
||||
| `Import` | `path: String` | `import "file.el"` or `from mod import { … }` |
|
||||
| `CgiBlock` | `name`, `dharma_id`, `principal`, `network`, `engram`, `has_*: Bool` | CGI identity declaration |
|
||||
| `ServiceBlock` | `name`, `sponsor`, `domain` | service declaration |
|
||||
| `Expr` | `value: expr` | bare expression statement |
|
||||
|
||||
**Param nodes:** `{ "name": String, "type": String }` where `type` is the leading identifier of the type annotation (e.g., `"Int"`, `"String"`, `"Map"`) or `""` if unannotated.
|
||||
|
||||
### 2.3 The Type System
|
||||
|
||||
Type annotations are parsed and stored but not type-checked at compile time. They serve as documentation and as hints to the codegen for arithmetic dispatch.
|
||||
|
||||
**Built-in types:**
|
||||
|
||||
| Type | C representation | Notes |
|
||||
|------|-----------------|-------|
|
||||
| `String` | `const char*` cast to `el_val_t` | via `EL_STR()` macro |
|
||||
| `Int` | `int64_t` | direct |
|
||||
| `Bool` | `int64_t` | `0` = false, nonzero = true |
|
||||
| `Float` | `int64_t` | bit-cast double via `el_from_float()` |
|
||||
| `Void` | `void` | functions returning nothing |
|
||||
| `Any` | `void*` cast to `el_val_t` | generic containers |
|
||||
| `[T]` | `el_val_t` | pointer to ElList struct |
|
||||
| `Map<K,V>` | `el_val_t` | pointer to ElMap struct |
|
||||
|
||||
**Temporal types** (first-class in codegen):
|
||||
|
||||
| Type | Representation | Notes |
|
||||
|------|---------------|-------|
|
||||
| `Instant` | nanoseconds since Unix epoch as `int64_t` | `now()` returns this |
|
||||
| `Duration` | signed nanoseconds as `int64_t` | `30.seconds` = `30 * 1000000000` |
|
||||
| `Calendar` | pointer to heap-allocated struct | `earth_calendar(zone)` |
|
||||
| `CalendarTime` | pointer to heap-allocated struct | `now_in(cal)` |
|
||||
| `LocalDate` | pointer to heap-allocated struct | `local_date(y, m, d)` |
|
||||
| `LocalTime` | nanoseconds since midnight, direct `int64_t` | `local_time(h, m, s, ns)` |
|
||||
| `Zone` | pointer to heap-allocated struct | `zone("America/New_York")` |
|
||||
| `Rhythm` | pointer to heap-allocated struct | recurrence pattern |
|
||||
|
||||
The codegen tracks type-annotated variable names in per-function process state (`__int_names`, `__instant_names`, `__duration_names`, etc.) to dispatch arithmetic and comparisons through the correct runtime wrappers. Type-mismatched operations (e.g., `Instant + Instant`) are emitted as `#error` directives.
|
||||
|
||||
**Duration postfix literals:** `30.seconds`, `1.hour`, `500.millis`, `30.nanos` are parsed as `DurationLit` AST nodes and compiled to `el_duration_from_nanos(count * multiplier)`. The multipliers:
|
||||
|
||||
| Unit | Nanoseconds |
|
||||
|------|------------|
|
||||
| `nano` / `nanos` | 1 |
|
||||
| `milli` / `millis` / `millisecond` / `milliseconds` | 1,000,000 |
|
||||
| `second` / `seconds` | 1,000,000,000 |
|
||||
| `minute` / `minutes` | 60,000,000,000 |
|
||||
| `hour` / `hours` | 3,600,000,000,000 |
|
||||
| `day` / `days` | 86,400,000,000,000 |
|
||||
|
||||
### 2.4 Key Language Semantics
|
||||
|
||||
**Implicit return.** The final expression in a function body becomes the return value if it is not a control-flow construct (`If`, `For`). The codegen's `transform_implicit_return` rewrites the last `Expr` statement into a `Return` statement before emitting.
|
||||
|
||||
**Let-rebinding, not mutation.** El uses `let` for both initial binding and rebinding:
|
||||
```el
|
||||
let count = 0
|
||||
let count = count + 1 // NOT mutation — creates a new binding in the same scope
|
||||
```
|
||||
The codegen tracks declared names per C scope. When `count` is already in `declared`, it emits `count = count + 1;` (plain assignment). When it is new, it emits `el_val_t count = 0;`. This means **El does not have mutable variables in the traditional sense** — every `let` is a potential redeclaration. The practical effect is that shadowing and in-place update use identical syntax.
|
||||
|
||||
**Bare reassignment.** The parser also handles `name = expr` (without `let`) when an `Ident` is immediately followed by `Eq`. This emits a plain C assignment.
|
||||
|
||||
**`target` is reserved.** The word `target` is lexed as the `Target` token kind — it cannot be used as a variable or parameter name. Use `tgt` or another name instead. This is a live gotcha in `compiler.el` itself, which uses `tgt` for exactly this reason.
|
||||
|
||||
**`__no_block_expr` guard.** The parser uses process state key `__no_block_expr` to suppress Map-literal parsing when parsing the condition of `if`, `while`, `for`, and `match`. This prevents a stray `{` (the start of the then-block) from being parsed as a Map literal.
|
||||
|
||||
**Arena memory model.** The runtime includes an arena allocator that is activated in server/long-running contexts. In CLI mode (`elc`, `elb`) the arena is inactive. Memory is managed via ARC (reference counting): `el_retain()` and `el_release()` on Lists and Maps. Strings and ints are not refcounted — the retain/release functions are safe no-ops on non-tagged values.
|
||||
|
||||
---
|
||||
|
||||
## 3. The Runtime API
|
||||
|
||||
All runtime functions are declared in `el-compiler/runtime/el_runtime.h`. Every compiled El program links against `el-compiler/runtime/el_runtime.c`.
|
||||
|
||||
All values are `el_val_t` (`int64_t`). Strings are pointers cast through `int64_t` using `EL_STR(s)` / `EL_CSTR(v)` macros.
|
||||
|
||||
Canonical compile command:
|
||||
```bash
|
||||
cc -std=c11 -I el-compiler/runtime -lcurl -lpthread \
|
||||
-o <out> <prog>.c el-compiler/runtime/el_runtime.c
|
||||
```
|
||||
|
||||
### I/O
|
||||
|
||||
| Function | Signature | Description |
|
||||
|----------|-----------|-------------|
|
||||
| `println` | `(s) -> Void` | print string + newline to stdout |
|
||||
| `print` | `(s) -> Void` | print string without newline |
|
||||
| `readline` | `() -> String` | read one line from stdin |
|
||||
|
||||
### String Operations
|
||||
|
||||
| Function | Signature | Description |
|
||||
|----------|-----------|-------------|
|
||||
| `el_str_concat` | `(a, b) -> String` | concatenate two strings |
|
||||
| `str_concat` | `(a, b) -> String` | alias for `el_str_concat` |
|
||||
| `str_eq` | `(a, b) -> Bool` | string equality comparison |
|
||||
| `str_starts_with` | `(s, prefix) -> Bool` | prefix test |
|
||||
| `str_ends_with` | `(s, suffix) -> Bool` | suffix test |
|
||||
| `str_contains` | `(s, sub) -> Bool` | substring test |
|
||||
| `str_len` | `(s) -> Int` | byte length |
|
||||
| `str_slice` | `(s, start, end) -> String` | substring (byte offsets) |
|
||||
| `str_replace` | `(s, from, to) -> String` | replace all occurrences |
|
||||
| `str_to_upper` / `str_upper` | `(s) -> String` | uppercase |
|
||||
| `str_to_lower` / `str_lower` | `(s) -> String` | lowercase |
|
||||
| `str_trim` | `(s) -> String` | strip leading/trailing whitespace |
|
||||
| `str_lstrip` / `str_rstrip` | `(s) -> String` | one-sided strip |
|
||||
| `str_index_of` | `(s, sub) -> Int` | position of substring; `-1` if absent |
|
||||
| `str_last_index_of` | `(s, sub) -> Int` | last position |
|
||||
| `str_index_of_all` | `(s, sub) -> [Int]` | all byte offsets (non-overlapping) |
|
||||
| `str_find_chars` | `(s, any_of) -> Int` | first index of any char in set |
|
||||
| `str_split` | `(s, sep) -> [String]` | split on separator |
|
||||
| `str_split_lines` | `(s) -> [String]` | split on newlines |
|
||||
| `str_split_chars` | `(s) -> [String]` | split into individual characters |
|
||||
| `str_split_n` | `(s, sep, n) -> [String]` | split at most `n` times |
|
||||
| `str_join` | `(list, sep) -> String` | join list with separator |
|
||||
| `str_char_at` | `(s, i) -> String` | character at byte index |
|
||||
| `str_char_code` | `(s, i) -> Int` | Unicode code point at index |
|
||||
| `str_pad_left` | `(s, width, pad) -> String` | left-pad to width |
|
||||
| `str_pad_right` | `(s, width, pad) -> String` | right-pad to width |
|
||||
| `str_format` | `(fmt, data) -> String` | `{key}` interpolation |
|
||||
| `str_repeat` | `(s, n) -> String` | repeat string n times |
|
||||
| `str_reverse` | `(s) -> String` | reverse by codepoint |
|
||||
| `str_strip_prefix` | `(s, prefix) -> String` | remove prefix if present |
|
||||
| `str_strip_suffix` | `(s, suffix) -> String` | remove suffix if present |
|
||||
| `str_strip_chars` | `(s, chars) -> String` | strip characters from both ends |
|
||||
| `str_count` | `(s, sub) -> Int` | count non-overlapping occurrences |
|
||||
| `str_count_chars` | `(s) -> Int` | codepoint count |
|
||||
| `str_count_bytes` | `(s) -> Int` | alias for `str_len` |
|
||||
| `str_count_lines` | `(s) -> Int` | line count |
|
||||
| `str_count_words` | `(s) -> Int` | word count |
|
||||
| `str_count_letters` | `(s) -> Int` | ASCII letter count |
|
||||
| `str_count_digits` | `(s) -> Int` | ASCII digit count |
|
||||
| `is_letter` / `is_digit` / `is_alphanumeric` | `(s) -> Bool` | ASCII char classification |
|
||||
| `is_whitespace` / `is_punctuation` | `(s) -> Bool` | |
|
||||
| `is_uppercase` / `is_lowercase` | `(s) -> Bool` | |
|
||||
| `int_to_str` | `(n) -> String` | format integer |
|
||||
| `str_to_int` | `(s) -> Int` | parse integer |
|
||||
| `str_to_float` | `(s) -> Float` | parse float |
|
||||
| `parse_int` | `(s, default) -> Int` | parse with fallback |
|
||||
| `bool_to_str` | `(b) -> String` | format bool |
|
||||
|
||||
### Integer/Float Math
|
||||
|
||||
| Function | Description |
|
||||
|----------|-------------|
|
||||
| `el_abs(n)` | absolute value |
|
||||
| `el_max(a, b)` | maximum |
|
||||
| `el_min(a, b)` | minimum |
|
||||
| `float_to_str(f)` | format float as string |
|
||||
| `int_to_float(n)` | widen Int to Float |
|
||||
| `float_to_int(f)` | truncate Float to Int |
|
||||
| `format_float(f, decimals)` | format with N decimal places |
|
||||
| `decimal_round(f, decimals)` | round to N decimals |
|
||||
| `math_sqrt(f)` | square root |
|
||||
| `math_log(f)` / `math_ln(f)` | logarithms |
|
||||
| `math_sin(f)` / `math_cos(f)` / `math_pi()` | trigonometry |
|
||||
|
||||
### List Operations
|
||||
|
||||
| Function | Description |
|
||||
|----------|-------------|
|
||||
| `el_list_empty()` | create empty list |
|
||||
| `el_list_new(count, …)` | create list from N values (varargs) |
|
||||
| `el_list_len(list)` | length |
|
||||
| `el_list_get(list, i)` | element at index; `0` on out-of-bounds |
|
||||
| `el_list_append(list, e)` | append; returns updated list |
|
||||
| `el_list_clone(list)` | shallow copy |
|
||||
| `list_push(list, e)` | alias for `el_list_append` |
|
||||
| `list_push_front(list, e)` | prepend |
|
||||
| `list_join(list, sep)` | join to string |
|
||||
| `list_range(start, end)` | integer range `[start, end)` |
|
||||
| `native_list_empty()` | alias for `el_list_empty` (used in compiler source) |
|
||||
| `native_list_append(l, v)` | alias for `el_list_append` |
|
||||
| `native_list_get(l, idx)` | alias for `el_list_get` |
|
||||
| `native_list_len(l)` | alias for `el_list_len` |
|
||||
| `native_list_clone(l)` | alias for `el_list_clone` |
|
||||
| `append(l, e)` | method-call alias: `list.append(e)` |
|
||||
| `len(l)` | method-call alias: `list.len()` |
|
||||
| `get(l, i)` | method-call alias: `list.get(i)` |
|
||||
|
||||
### Map Operations
|
||||
|
||||
| Function | Description |
|
||||
|----------|-------------|
|
||||
| `el_map_new(count, …)` | create map from key/value pairs (varargs) |
|
||||
| `el_map_get(map, key)` | get value by key |
|
||||
| `el_map_set(map, key, value)` | set key; returns map |
|
||||
| `el_get_field(map, key)` | alias; emitted for `.field` access |
|
||||
| `map_get(map, key)` | method-call alias |
|
||||
| `map_set(map, key, value)` | method-call alias |
|
||||
|
||||
### ARC (Reference Counting)
|
||||
|
||||
| Function | Description |
|
||||
|----------|-------------|
|
||||
| `el_retain(v)` | increment refcount; no-op for non-heap values |
|
||||
| `el_release(v)` | decrement refcount; free when zero |
|
||||
|
||||
### In-Process State
|
||||
|
||||
| Function | Description |
|
||||
|----------|-------------|
|
||||
| `state_set(key, value)` | store in process-global key/value table |
|
||||
| `state_get(key)` | retrieve; `""` if absent |
|
||||
| `state_del(key)` | delete key |
|
||||
| `state_keys()` | all keys as `[String]` |
|
||||
|
||||
### Filesystem
|
||||
|
||||
| Function | Description |
|
||||
|----------|-------------|
|
||||
| `fs_read(path)` | read file to string; `""` on error |
|
||||
| `fs_write(path, content)` | write string; returns `1` on success |
|
||||
| `fs_write_bytes(path, bytes, length)` | write raw bytes of known length |
|
||||
| `fs_list(path)` | list directory entries |
|
||||
| `fs_exists(path)` | check if path exists |
|
||||
| `fs_mkdir(path)` | mkdir -p |
|
||||
|
||||
### HTTP Client
|
||||
|
||||
| Function | Description |
|
||||
|----------|-------------|
|
||||
| `http_get(url)` | GET; returns body string |
|
||||
| `http_post(url, body)` | POST; returns body string |
|
||||
| `http_post_json(url, json_body)` | POST with Content-Type: application/json |
|
||||
| `http_get_with_headers(url, headers_map)` | GET with custom headers |
|
||||
| `http_post_with_headers(url, body, headers_map)` | POST with custom headers |
|
||||
| `http_post_form_auth(url, form_body, auth_header)` | POST with auth |
|
||||
| `http_delete(url)` | DELETE |
|
||||
| `http_get_to_file(url, headers_map, output_path)` | stream response to file |
|
||||
| `http_post_to_file(url, body, headers_map, output_path)` | stream POST response to file |
|
||||
| `http_response(status, headers_json, body)` | build response envelope |
|
||||
| `url_encode(s)` | RFC 3986 percent-encoding |
|
||||
| `url_decode(s)` | URL decode |
|
||||
| `el_html_sanitize(html, allowlist_json)` | allowlist HTML sanitizer |
|
||||
|
||||
### HTTP Server
|
||||
|
||||
| Function | Description |
|
||||
|----------|-------------|
|
||||
| `http_serve(port, handler)` | start server; handler: `(method, path, body) -> String` |
|
||||
| `http_serve_v2(port, handler)` | start server; handler: `(method, path, headers_map, body) -> String` |
|
||||
| `http_set_handler(name)` | set handler by symbol name |
|
||||
| `http_set_handler_v2(name)` | v2 variant |
|
||||
|
||||
### JSON
|
||||
|
||||
| Function | Description |
|
||||
|----------|-------------|
|
||||
| `json_get(json, key)` | substring lookup of `"key": value` |
|
||||
| `json_parse(s)` | parse JSON string to List/Map |
|
||||
| `json_stringify(v)` | serialize Any to JSON string |
|
||||
| `json_get_string(j, key)` | typed extract: String |
|
||||
| `json_get_int(j, key)` | typed extract: Int |
|
||||
| `json_get_float(j, key)` | typed extract: Float |
|
||||
| `json_get_bool(j, key)` | typed extract: Bool |
|
||||
| `json_get_raw(j, key)` | extract nested object/array as JSON string |
|
||||
| `json_set(j, key, value)` | update field, return new JSON string |
|
||||
| `json_array_len(j)` | length of JSON array string |
|
||||
| `json_array_get(j, index)` | element at index |
|
||||
| `json_array_get_string(j, index)` | string element at index |
|
||||
|
||||
### Time (Epoch-Based)
|
||||
|
||||
| Function | Description |
|
||||
|----------|-------------|
|
||||
| `time_now()` | Unix epoch milliseconds |
|
||||
| `time_now_utc()` | same, explicit UTC |
|
||||
| `time_format(ts, fmt)` | format timestamp |
|
||||
| `time_to_parts(ts)` | decompose to Map of fields |
|
||||
| `time_from_parts(secs, ns, tz)` | construct timestamp |
|
||||
| `time_add(ts, n, unit)` | add duration |
|
||||
| `time_diff(ts1, ts2, unit)` | difference |
|
||||
| `unix_timestamp()` | Unix seconds as Int |
|
||||
| `sleep_secs(secs)` | sleep N seconds |
|
||||
| `sleep_ms(ms)` | sleep N milliseconds |
|
||||
|
||||
### Time (First-Class Instant/Duration)
|
||||
|
||||
| Function | Description |
|
||||
|----------|-------------|
|
||||
| `now()` / `el_now_instant()` | current time as Instant (nanoseconds) |
|
||||
| `unix_seconds(n)` | construct Instant from Unix seconds |
|
||||
| `unix_millis(n)` | construct Instant from Unix milliseconds |
|
||||
| `instant_from_iso8601(s)` | parse ISO 8601 string |
|
||||
| `instant_to_unix_seconds(i)` | extract Unix seconds |
|
||||
| `instant_to_unix_millis(i)` | extract Unix milliseconds |
|
||||
| `instant_to_iso8601(i)` | format as ISO 8601 |
|
||||
| `el_duration_from_nanos(ns)` | construct Duration from nanoseconds |
|
||||
| `duration_seconds(n)` | Duration from seconds |
|
||||
| `duration_millis(n)` | Duration from milliseconds |
|
||||
| `duration_nanos(n)` | Duration from nanoseconds |
|
||||
| `duration_to_seconds(d)` | extract seconds |
|
||||
| `duration_to_millis(d)` | extract milliseconds |
|
||||
| `duration_to_nanos(d)` | extract nanoseconds |
|
||||
| `el_instant_add_dur(inst, dur)` | Instant + Duration |
|
||||
| `el_instant_sub_dur(inst, dur)` | Instant - Duration |
|
||||
| `el_instant_diff(a, b)` | Instant - Instant = Duration |
|
||||
| `el_duration_add/sub/scale/div` | Duration arithmetic |
|
||||
| `el_instant_lt/le/gt/ge/eq/ne` | Instant comparison |
|
||||
| `el_duration_lt/le/gt/ge/eq/ne` | Duration comparison |
|
||||
| `el_sleep_duration(dur)` | sleep for a Duration |
|
||||
| `ttl_cache_set(key, value)` | store with TTL |
|
||||
| `ttl_cache_get(key, max_age)` | retrieve if within max_age |
|
||||
| `ttl_cache_age(key)` | age of cached value as Duration |
|
||||
|
||||
### Calendar System
|
||||
|
||||
| Function | Description |
|
||||
|----------|-------------|
|
||||
| `zone(id)` | IANA zone or fixed offset |
|
||||
| `zone_utc()` / `zone_local()` | UTC and local zone |
|
||||
| `zone_offset(hours, minutes)` | fixed offset zone |
|
||||
| `earth_calendar(z)` | Gregorian calendar in zone |
|
||||
| `earth_calendar_default()` | system default |
|
||||
| `mars_calendar()` / `cycle_calendar(period)` | non-Earth calendars |
|
||||
| `no_cycle_calendar()` / `relative_calendar(epoch)` | abstract calendars |
|
||||
| `now_in(cal)` | current time as CalendarTime |
|
||||
| `in_calendar(inst, cal)` | project Instant into Calendar |
|
||||
| `cal_format(ct, pattern)` | format CalendarTime |
|
||||
| `cal_to_instant(ct)` | extract underlying Instant |
|
||||
| `cal_cycle_phase(ct)` / `cal_in(ct, cal)` | calendar ops |
|
||||
| `local_date(y, m, d)` | construct LocalDate |
|
||||
| `local_time(h, m, s, ns)` | construct LocalTime |
|
||||
| `local_datetime(date, time)` | construct LocalDateTime |
|
||||
| `zoned(date, time, cal)` | zoned datetime |
|
||||
| `local_date_year/month/day` | LocalDate accessors |
|
||||
| `local_time_hour/minute/second/nanos` | LocalTime accessors |
|
||||
| `el_local_date_add_dur` / `el_local_time_add_dur` | date/time arithmetic |
|
||||
| `el_local_date_lt` / `el_local_date_eq` | date comparison |
|
||||
| `rhythm_*` | recurrence patterns (cycle_start, weekday, weekly_at, next_after, matches, …) |
|
||||
|
||||
### Process / Execution
|
||||
|
||||
| Function | Description |
|
||||
|----------|-------------|
|
||||
| `args()` | command-line arguments as `[String]` (excludes argv[0]) |
|
||||
| `env(key)` | read environment variable; `""` if unset |
|
||||
| `exit(code)` | exit process with code |
|
||||
| `exit_program(code)` | alias for `exit` |
|
||||
| `getpid_now()` | current process ID |
|
||||
| `exec_command(cmd)` | run shell command; return exit code |
|
||||
| `exec_capture(cmd)` | run shell command; capture and return stdout |
|
||||
| `uuid_new()` / `uuid_v4()` | generate UUID v4 |
|
||||
| `native_int_to_str(n)` | format integer (alias, used in compiler source) |
|
||||
| `native_string_chars(s)` | split string into `[String]` of single characters |
|
||||
|
||||
### Crypto
|
||||
|
||||
| Function | Description |
|
||||
|----------|-------------|
|
||||
| `sha256_hex(input)` | SHA-256, hex output |
|
||||
| `sha256_bytes(input)` | SHA-256, raw bytes |
|
||||
| `hmac_sha256_hex(key, msg)` | HMAC-SHA-256, hex |
|
||||
| `hmac_sha256_bytes(key, msg)` | HMAC-SHA-256, raw bytes |
|
||||
| `base64_encode(input)` / `base64_decode(input)` | standard base64 |
|
||||
| `base64url_encode(input)` / `base64url_decode(input)` | URL-safe base64 |
|
||||
| `sha3_256_hex(input)` | SHA3-256 (Keccak) |
|
||||
| `pq_keygen_signature()` | Dilithium-3 key pair |
|
||||
| `pq_sign(sk_hex, msg)` / `pq_verify(pk_hex, msg, sig_hex)` | PQ signatures |
|
||||
| `pq_kem_keygen()` / `pq_kem_encaps(pk)` / `pq_kem_decaps(sk, ct)` | Kyber-768 KEM |
|
||||
| `pq_hybrid_keygen()` / `pq_hybrid_handshake(remote_pub)` | X25519 + Kyber hybrid |
|
||||
| `aead_encrypt(key_hex, plaintext)` | AES-256-GCM encrypt |
|
||||
| `aead_decrypt(key_hex, nonce_hex, ct_hex)` | AES-256-GCM decrypt |
|
||||
|
||||
### DHARMA Network (CGI programs only)
|
||||
|
||||
| Function | Description |
|
||||
|----------|-------------|
|
||||
| `el_cgi_init(name, dharma_id, principal, network, engram)` | initialize CGI identity (called by generated `main()`) |
|
||||
| `dharma_connect(cgi_id)` | open channel to peer |
|
||||
| `dharma_send(channel, content)` | send message; blocks for response |
|
||||
| `dharma_activate(query)` | spreading activation across DHARMA network |
|
||||
| `dharma_emit(event_type, payload)` | emit network event (@manager only) |
|
||||
| `dharma_field(event_type)` | wait for event (@manager only) |
|
||||
| `dharma_strengthen(cgi_id, weight)` | Hebbian potentiation |
|
||||
| `dharma_relationship(cgi_id)` | current relationship weight |
|
||||
| `dharma_peers()` | all connected peers sorted by weight |
|
||||
|
||||
### Engram Knowledge Graph
|
||||
|
||||
| Function | Description |
|
||||
|----------|-------------|
|
||||
| `engram_node(content, type, salience)` | create node; returns ID |
|
||||
| `engram_node_full(content, type, label, salience, importance, confidence, tier, tags)` | full node creation |
|
||||
| `engram_node_layered(…, layer_id)` | create node in specific layer |
|
||||
| `engram_get_node(id)` | retrieve node by ID |
|
||||
| `engram_strengthen(node_id)` | Hebbian potentiation |
|
||||
| `engram_forget(node_id)` | delete node and edges |
|
||||
| `engram_node_count()` | total node count |
|
||||
| `engram_edge_count()` | total edge count |
|
||||
| `engram_search(query, limit)` | full-text search |
|
||||
| `engram_scan_nodes(limit, offset)` | paginated node scan |
|
||||
| `engram_connect(from, to, weight, relation)` | create directed edge |
|
||||
| `engram_edge_between(from, to)` | get edge |
|
||||
| `engram_neighbors(node_id)` | BFS neighbors |
|
||||
| `engram_neighbors_filtered(node_id, max_depth, direction)` | filtered BFS |
|
||||
| `engram_activate(query, depth)` | spreading activation |
|
||||
| `engram_save(path)` / `engram_load(path)` | snapshot to/from disk |
|
||||
| `engram_add_layer(name, priority, suppressible, transparent, injectable)` | add consciousness layer |
|
||||
| `engram_remove_layer(layer_id)` / `engram_list_layers()` | layer management |
|
||||
| `engram_*_json` variants | JSON-string versions of search/scan/activate |
|
||||
| `engram_compile_layered_json(intent, depth)` | prompt-ready context block |
|
||||
|
||||
### LLM (Anthropic API)
|
||||
|
||||
| Function | Description |
|
||||
|----------|-------------|
|
||||
| `llm_call(model, prompt)` | single-turn call |
|
||||
| `llm_call_system(model, system, user)` | call with system prompt |
|
||||
| `llm_call_agentic(model, system, user, tools)` | agentic call with tools (CGI only) |
|
||||
| `llm_vision(model, system, prompt, image)` | vision call |
|
||||
| `llm_models()` | list available models |
|
||||
| `llm_register_tool(name, handler_fn_name)` | register tool handler (CGI only) |
|
||||
|
||||
### Observability
|
||||
|
||||
| Function | Description |
|
||||
|----------|-------------|
|
||||
| `emit_log(level, msg, fields_json)` | emit OTLP log |
|
||||
| `emit_metric(name, value, tags_json)` | emit OTLP metric |
|
||||
| `trace_span_start(name)` | start trace span |
|
||||
| `trace_span_end(span_handle)` | end trace span |
|
||||
| `emit_event(name, duration_ms)` | emit event |
|
||||
|
||||
---
|
||||
|
||||
## 4. How to Re-Bootstrap from Zero
|
||||
|
||||
This section assumes the bootstrap binary is gone. Everything else (source files, runtime) is intact.
|
||||
|
||||
### What You Need to Implement
|
||||
|
||||
A minimal El compiler has three parts: lexer, parser, codegen. Each can be written in any language. The goal is to compile `elc-cli.el` into a working `elc` binary, after which El is self-hosting again.
|
||||
|
||||
### Step 1: Write a Minimal Lexer
|
||||
|
||||
The lexer must produce a list of `{ "kind": String, "value": String }` maps (or equivalent structures). Required token kinds: `Int`, `Float`, `Str`, `Bool`, `Ident`, `Eof`, and all keywords and operators listed in section 2.1.
|
||||
|
||||
The minimal subset needed to compile the compiler itself:
|
||||
- Keywords: `let`, `fn`, `return`, `if`, `else`, `while`, `for`, `in`, `import`, `from`, `true`, `false`, `extern`
|
||||
- Literals: `Int`, `Str`, `Bool`, `Ident`
|
||||
- Operators: `=`, `==`, `!=`, `!`, `<`, `>`, `<=`, `>=`, `&&`, `||`, `+`, `-`, `*`, `/`, `->`, `=>`, `:`, `,`, `.`, `(`, `)`, `{`, `}`, `[`, `]`, `@`, `?`
|
||||
- Special: `Eof`
|
||||
|
||||
The lexer in `lexer.el` walks a char array using `native_list_get` to avoid O(n²) string slicing. A Python implementation can use a simple index into a string. Escapes to handle: `\"`, `\n`, `\t`, `\r`, `\\`.
|
||||
|
||||
### Step 2: Write a Minimal Parser
|
||||
|
||||
The parser is a standard recursive descent parser. It produces AST maps as described in section 2.2.
|
||||
|
||||
The minimal statement forms needed to compile the compiler:
|
||||
- `let name [: Type] = expr`
|
||||
- `fn name(params) [-> Type] { body }`
|
||||
- `extern fn name(params) [-> Type]`
|
||||
- `return expr`
|
||||
- `while cond { body }`
|
||||
- `for item in list { body }`
|
||||
- `if cond { body } [else [if] { body }]`
|
||||
- `import "path"`
|
||||
- `from module import { … }`
|
||||
- `@decorator stmt`
|
||||
- `name = expr` (bare assignment)
|
||||
- bare expression statement
|
||||
|
||||
The minimal expression forms:
|
||||
- Integer, float, string, bool literals
|
||||
- Identifier
|
||||
- Binary operations with the precedence table from section 2.2
|
||||
- Unary `!` and `-`
|
||||
- Function call: `f(a, b, …)`
|
||||
- Method call: `obj.method(args)` (parsed as Call with Field func)
|
||||
- Field access: `obj.field`
|
||||
- Index access: `obj[i]`
|
||||
- Array literal: `[e1, e2, …]`
|
||||
- Map literal: `{ "key": value, … }`
|
||||
- `if` as expression
|
||||
- `match` expression
|
||||
- Postfix `?` (can be a no-op)
|
||||
- Duration literal: `N.unit`
|
||||
|
||||
The `__no_block_expr` guard (section 2.4) is important: without it, `if a || b { ... }` will incorrectly parse `{` as a Map literal.
|
||||
|
||||
### Step 3: Write a Minimal Codegen
|
||||
|
||||
The codegen emits C11 source. Required output structure:
|
||||
|
||||
```c
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
#include "el_runtime.h"
|
||||
|
||||
// Forward declarations for all non-main functions
|
||||
el_val_t fn_name(el_val_t p1, el_val_t p2);
|
||||
...
|
||||
|
||||
// File-scope let bindings (if any)
|
||||
el_val_t GLOBAL_NAME;
|
||||
|
||||
// Function bodies
|
||||
el_val_t fn_name(el_val_t p1, el_val_t p2) {
|
||||
...
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Entry point
|
||||
int main(int _argc, char** _argv) {
|
||||
el_runtime_init_args(_argc, _argv);
|
||||
...
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
Critical codegen rules:
|
||||
|
||||
1. **All values are `el_val_t`**. Every parameter, local variable, and return type is `el_val_t` unless the function has `ret_type == "Void"` (use `void`).
|
||||
|
||||
2. **Let-rebinding**: track declared names per C scope. Emit `el_val_t name = val;` on first occurrence; emit `name = val;` on subsequent occurrences of the same name in the same scope.
|
||||
|
||||
3. **`+` dispatch**: if either operand is a string literal → `el_str_concat(a, b)`. If both are provably integers → `(a + b)`. Default fallback → `el_str_concat`.
|
||||
|
||||
4. **`==` dispatch**: if either operand is a string or identifier → `str_eq(a, b)`. If both are integer literals or provably Int → `(a == b)`.
|
||||
|
||||
5. **String literals**: wrap in `EL_STR("…")` and escape: `\"` → `\\\"`, `\n` → `\\n`, `\t` → `\\t`, `\\` → `\\\\`.
|
||||
|
||||
6. **Map literals**: `el_map_new(N, "k1", v1, "k2", v2, …)`. Empty map: `el_map_new(0)`.
|
||||
|
||||
7. **Array literals**: `el_list_new(N, e1, e2, …)`. Empty: `el_list_empty()`.
|
||||
|
||||
8. **Index access**: string-literal index → `el_get_field(obj, EL_STR("key"))`. Integer index → `el_list_get(obj, idx)`.
|
||||
|
||||
9. **Field access** `obj.field` → `el_get_field(obj, EL_STR("field"))`.
|
||||
|
||||
10. **Method call** `obj.method(args)` → `method(obj, args)`.
|
||||
|
||||
11. **`for item in list`** → emit:
|
||||
```c
|
||||
{ el_val_t _el_lst = <list>; el_val_t _el_len = el_list_len(_el_lst);
|
||||
for (el_val_t _el_i = 0; _el_i < _el_len; _el_i++) {
|
||||
el_val_t item = el_list_get(_el_lst, _el_i);
|
||||
<body>
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
12. **`match`** → GCC/Clang statement expression with `goto`:
|
||||
```c
|
||||
({ el_val_t _s = <subject>; el_val_t _r = 0;
|
||||
if (_s == 42) { _r = <arm_body>; goto _done; }
|
||||
if (str_eq(_s, EL_STR("str"))) { _r = <arm_body>; goto _done; }
|
||||
{ _r = <wildcard_body>; goto _done; }
|
||||
_done:; _r; })
|
||||
```
|
||||
|
||||
13. **`if` as expression** → similarly wrapped in a GCC/Clang statement expression.
|
||||
|
||||
14. **Implicit return**: if the last statement in a function body is a bare `Expr` (not `If` or `For`), emit it as `return <expr>;` instead of `<expr>;`.
|
||||
|
||||
15. **Float literals**: emit as `el_from_float(<value>)`.
|
||||
|
||||
16. **Bool literals**: `true` → `1`, `false` → `0`.
|
||||
|
||||
17. **`fn main()`**: do not emit as a regular `el_val_t` function. Instead, fold its body into C's `int main()` after any top-level statements.
|
||||
|
||||
18. **`extern fn`**: emit only a forward declaration (no body).
|
||||
|
||||
19. **Forward declarations**: scan for all `FnDef` nodes before emitting bodies. This enables mutual recursion.
|
||||
|
||||
### Step 4: Compile the El Compiler
|
||||
|
||||
Using your minimal implementation, compile `elc-cli.el` (which imports the entire compiler chain):
|
||||
|
||||
```bash
|
||||
# Your minimal compiler
|
||||
python3 minimal_elc.py elc-cli.el > elc-new.c
|
||||
|
||||
# Build with the runtime
|
||||
cc -std=c11 -I el-compiler/runtime -lcurl -lpthread \
|
||||
-o elc-new elc-new.c el-compiler/runtime/el_runtime.c
|
||||
```
|
||||
|
||||
### Step 5: Verify Self-Hosting
|
||||
|
||||
```bash
|
||||
# Compile elc-cli.el with the new compiler
|
||||
./elc-new elc-cli.el elc-v2.c
|
||||
cc -std=c11 -I el-compiler/runtime -lcurl -lpthread \
|
||||
-o elc-v2 elc-v2.c el-compiler/runtime/el_runtime.c
|
||||
|
||||
# Compile again with the second-generation compiler
|
||||
./elc-v2 elc-cli.el elc-v3.c
|
||||
|
||||
# The outputs should be identical
|
||||
diff elc-v2.c elc-v3.c
|
||||
```
|
||||
|
||||
A clean diff confirms you have a stable fixed point: the compiler reproduces itself exactly.
|
||||
|
||||
### Step 6: Replace the Bootstrap Binary
|
||||
|
||||
```bash
|
||||
cp elc-v2 dist/platform/elc
|
||||
```
|
||||
|
||||
You are bootstrapped.
|
||||
|
||||
### Minimal El Subset for the Compiler Itself
|
||||
|
||||
The El compiler source (`lexer.el`, `parser.el`, `codegen.el`, `compiler.el`) uses:
|
||||
- `fn`, `let`, `while`, `if`/`else`, `return`, `for`/`in`, `import`
|
||||
- `extern fn` (for `.elh` headers)
|
||||
- `String`, `Int`, `Bool`, `Void`, `Any`, `Map<String, Any>`, `[String]`, `[Map<String, Any>]`
|
||||
- Map literals `{ "key": val }`
|
||||
- Array literals `[...]` (and `native_list_empty()`)
|
||||
- List operations: `native_list_empty()`, `native_list_append()`, `native_list_get()`, `native_list_len()`, `native_list_clone()`
|
||||
- String operations: `str_join()`, `str_eq()`, `str_contains()`, `str_starts_with()`, `str_slice()`, `str_trim()`, `str_split()`, `str_index_of()`, `str_len()`, `str_to_int()`, `native_string_chars()`, `native_int_to_str()`
|
||||
- `state_get()`, `state_set()`
|
||||
- `println()`, `fs_read()`, `fs_write()`, `exit()`
|
||||
- `el_release()` (ARC cleanup)
|
||||
|
||||
The compiler does not use: HTTP, engram, dharma, LLM, crypto, UUID, float arithmetic.
|
||||
|
||||
---
|
||||
|
||||
## 5. The Long-Term Solution: elvm
|
||||
|
||||
### Why a VM Makes Bootstrapping More Auditable
|
||||
|
||||
The current bootstrap chain relies on trusting a binary whose source we cannot fully audit by inspection alone. This is the classic "trusting trust" problem (Ken Thompson, 1984). A virtual machine breaks the chain:
|
||||
|
||||
- `elc` targets `elvm` bytecode (instead of C)
|
||||
- `elvm` is a minimal interpreter hand-written in ~500 lines of C
|
||||
- The hand-written C is small enough to audit completely
|
||||
- Anyone can compile `elvm.c` with any C compiler
|
||||
- From there: `elvm` interprets `elc.elvm` → `elc` compiles El → `cc` builds native binaries
|
||||
|
||||
The benefit: the trusted base shrinks from "a Mach-O binary" to "500 lines of straightforward C code that anyone can read in an afternoon."
|
||||
|
||||
### The elvm Design
|
||||
|
||||
A minimal elvm needs:
|
||||
- A stack or register machine (stack is simpler)
|
||||
- Instructions: push, pop, add, sub, mul, div, cmp, jump, call, return, load, store
|
||||
- A string table (El strings are mostly literals)
|
||||
- A heap for ElList and ElMap
|
||||
- An FFI table mapping El runtime builtins to C functions
|
||||
|
||||
The El compiler would gain a `--target=elvm` flag in `compile_dispatch()`. Codegen would emit bytecode instead of C text. The runtime interface stays the same — builtins map to FFI slots by name.
|
||||
|
||||
This is the planned path. It does not exist yet.
|
||||
|
||||
---
|
||||
|
||||
## 6. Compiler Source Map
|
||||
|
||||
| File | Role | Lines |
|
||||
|------|------|-------|
|
||||
| `elc-cli.el` | Entry point; imports compiler.el | 7 |
|
||||
| `el-compiler/src/compiler.el` | Pipeline wiring: lex → parse → codegen. Import resolution, `--emit-header`, `fn main()`. Defines `compile()`, `compile_js()`, `compile_dispatch()`, `resolve_imports()` | 298 |
|
||||
| `el-compiler/src/lexer.el` | Tokenizer. `lex(source)` → token list. Char helpers, keyword lookup, scan_digits, scan_ident, scan_string, strip_code_comments | 747 |
|
||||
| `el-compiler/src/parser.el` | Recursive descent parser. `parse(tokens)` → AST. All statement and expression forms | 1071 |
|
||||
| `el-compiler/src/codegen.el` | C code emitter. `codegen(stmts, source)` → (streams to stdout). Expression codegen, statement codegen, function codegen, type tracking, capability enforcement, temporal type dispatch | 2721 |
|
||||
| `el-compiler/src/codegen-js.el` | JavaScript backend. `codegen_js(stmts, source)` → JS source | ~500 |
|
||||
| `el-compiler/runtime/el_runtime.h` | Full runtime API declaration | 755 |
|
||||
| `el-compiler/runtime/el_runtime.c` | Full runtime implementation | large |
|
||||
| `el-compiler/runtime/el_runtime.js` | JS runtime | — |
|
||||
| `elb.el` | Build coordinator. Reads `manifest.el`, walks import graph, compiles modules, links binary. The `.NET`-style incremental build model | 367 |
|
||||
| `elc-combined.el` | Pre-merged single-file bootstrap edition (for early bootstrap iterations) | large |
|
||||
| `spec/language.md` | Language specification v1.2.0 | — |
|
||||
| `dist/platform/elc` | Current bootstrap binary (Mach-O arm64) | — |
|
||||
|
||||
---
|
||||
|
||||
## 7. Key Decisions and Gotchas
|
||||
|
||||
### `target` is a Reserved Keyword
|
||||
|
||||
`target` is lexed as the `Target` token kind. It cannot be used as a variable or parameter name anywhere in El source. If you write `fn compile(target: String)`, the parameter name will be tokenized as `Target`, which the parser does not recognize as an `Ident` in parameter position.
|
||||
|
||||
**Workaround:** use `tgt`, `dest`, `backend`, or any other name. The compiler source uses `tgt` specifically for this reason. This comes up whenever writing code that handles compilation targets.
|
||||
|
||||
### `let x = x + 1` is Let-Rebinding, Not Mutation
|
||||
|
||||
El has no mutable variables. `let count = count + 1` re-introduces `count` into the current scope, shadowing the previous binding. At the C level, the codegen tracks declared names and emits plain assignment for subsequent bindings of the same name:
|
||||
|
||||
- First `let count = 0` → `el_val_t count = 0;`
|
||||
- Second `let count = count + 1` → `count = count + 1;`
|
||||
|
||||
This means you cannot have two different values named `count` in the same C scope — the second binding overwrites the first. This is by design. Scoped shadowing works correctly because each block (if body, while body, for body) gets its own copy of the `declared` list.
|
||||
|
||||
### Arena is Inactive in CLI Mode
|
||||
|
||||
The runtime includes an arena allocator designed for long-running server processes. In CLI mode (`elc`, `elb`) the arena is not activated. Memory is managed by ARC (reference counting via `el_retain`/`el_release`). The compiler source explicitly calls `el_release(tokens)` after parsing and `el_release(stmt)` after codegen to prevent memory exhaustion on large source files.
|
||||
|
||||
If you are implementing a new runtime or embedding El, be aware that the ARC model expects callers to release values they are done with.
|
||||
|
||||
### The `extern fn` / `.elh` Separate Compilation Model
|
||||
|
||||
`elb` (the build coordinator) supports separate compilation. When a module changes:
|
||||
1. `elc --emit-header module.el module.c` compiles the module and writes `module.elh`
|
||||
2. `module.elh` contains `extern fn` declarations for all public functions
|
||||
3. Other modules that import `module.el` use the `.elh` header instead of re-parsing the source
|
||||
|
||||
The `resolve_imports` function in `compiler.el` checks for a `.elh` file before recursively inlining the `.el` source. If the header exists, it is used (and the `.el` is marked as seen to prevent double-inclusion).
|
||||
|
||||
This is important for bootstrap: if you have pre-compiled headers lying around from a broken build, they may shadow updated source. Delete `.elh` files (or use `elb --clean`) when debugging unexpected compilation behavior.
|
||||
|
||||
### Import Resolution: Depth-First with Deduplication
|
||||
|
||||
`resolve_imports` in `compiler.el`:
|
||||
|
||||
1. Walks imports depth-first (dependencies before dependents)
|
||||
2. Uses `state_set("__elc_imp__:" + path, "1")` to deduplicate: each file is included exactly once
|
||||
3. Builds the combined source string by concatenating import bodies ahead of the entry file's body
|
||||
4. If a `.elh` header exists for an import, uses that instead of recursing into the `.el`
|
||||
|
||||
The result is one large string that gets passed through `lex` → `parse` → `codegen` as a single unit. The codegen emits forward declarations for all functions before any body, so declaration order within the combined source does not matter.
|
||||
|
||||
### `+` Operator Dispatch is Heuristic
|
||||
|
||||
El's `+` operator serves double duty: integer addition and string concatenation. The codegen dispatches based on static analysis of the AST:
|
||||
|
||||
- If either operand is a `Str` literal → `el_str_concat`
|
||||
- If both operands are provably `Int` (via `is_int_expr`) → `(a + b)`
|
||||
- If either operand is a `Call` or `Ident` → `el_str_concat` (conservative fallback)
|
||||
|
||||
The `is_int_expr` predicate recurses through the AST: literal `Int`, names in `__int_names` (from `: Int` annotations), known Int-returning builtins, and arithmetic BinOps over Int operands all count as "provably Int."
|
||||
|
||||
If you write `let result = some_int_var + 1` and `some_int_var` is not annotated `: Int`, the codegen may emit `el_str_concat` instead of integer addition. Fix by adding `: Int` to the variable declaration.
|
||||
|
||||
### `==` Operator Dispatch is Also Heuristic
|
||||
|
||||
Similarly, `==` dispatches between `str_eq(a, b)` (string comparison) and `(a == b)` (integer comparison) based on operand types. The codegen tracks Int-typed names in `__int_names`. Two `Ident` operands where both are known Int-typed use `==`; all other Ident-Ident comparisons use `str_eq`.
|
||||
|
||||
This means comparing two integer variables that were not annotated `: Int` can silently produce `str_eq` on what are actually integer values — and `str_eq` treats them as `const char*` pointers, producing incorrect results or segfaults.
|
||||
|
||||
**Rule:** always annotate variables `: Int` when they will participate in `==` comparisons or `+` arithmetic.
|
||||
|
||||
### Capability Kind Enforcement
|
||||
|
||||
The codegen classifies programs into three capability tiers based on top-level declarations:
|
||||
- `cgi` block present → full capability (all primitives allowed)
|
||||
- `service` block present → restricted (no `llm_call_agentic`, `llm_register_tool`, `dharma_emit`, `dharma_field`)
|
||||
- Neither → `utility` (no DHARMA, no LLM)
|
||||
|
||||
Violations are collected during codegen and emitted as `#error` directives at the bottom of the generated C. The downstream `cc` step then fails with a clear message naming the forbidden call.
|
||||
|
||||
### The `__no_block_expr` Parse Guard
|
||||
|
||||
When parsing the condition of `if`, `while`, `for`, and `match`, the parser sets `state_set("__no_block_expr", "1")`. This prevents `parse_primary` from treating a `{` as the start of a Map literal — instead it returns `{ "expr": "Nil" }` and the caller sees the `{` and treats it as the block delimiter.
|
||||
|
||||
Without this guard, `if a || b { ... }` would recurse into `parse_expr` for `b`, hit `{`, try to parse it as a Map literal, fail to find string keys, loop in error-recovery mode, and hang.
|
||||
|
||||
### Codegen Streams Output via `println`
|
||||
|
||||
The codegen does not build the output as a string — it calls `println()` for each line as it is emitted. The `compile()` / `compile_js()` / `codegen()` functions return `""`. Output goes to stdout.
|
||||
|
||||
This design avoids O(n²) string concatenation for large programs. It also means you cannot capture the compiler's output in a variable within El itself — you must redirect stdout at the OS level (`elc source.el > output.c`).
|
||||
|
||||
When writing to a file, `elc` detects the output path argument, redirects C's `stdout` to the file (via `freopen` in the runtime), and the `println` calls go there instead.
|
||||
Vendored
+4793
File diff suppressed because it is too large
Load Diff
Vendored
+6699
File diff suppressed because it is too large
Load Diff
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>English</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>com.apple.xcode.dsym.elc-asan</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>dSYM</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
</dict>
|
||||
</plist>
|
||||
Binary file not shown.
+5
@@ -0,0 +1,5 @@
|
||||
---
|
||||
triple: 'arm64-apple-darwin'
|
||||
binary-path: '/Users/will/Development/neuron-technologies/foundation/el/dist/platform/elc-asan'
|
||||
relocations: []
|
||||
...
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Vendored
+6450
File diff suppressed because it is too large
Load Diff
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -0,0 +1,9 @@
|
||||
package "el-compiler" {
|
||||
version "0.1.0"
|
||||
description "el self-hosting compiler — lexer, parser, codegen"
|
||||
edition "2026"
|
||||
}
|
||||
|
||||
build {
|
||||
entry "src/compiler.el"
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,764 @@
|
||||
/*
|
||||
* el_runtime.h — El language C runtime header
|
||||
*
|
||||
* Declares all built-in functions available to compiled El programs.
|
||||
* Include this in every generated .c file.
|
||||
*
|
||||
* Value model:
|
||||
* All El values are represented as el_val_t (= int64_t).
|
||||
* On 64-bit systems a pointer fits in int64_t.
|
||||
* String values are cast: (el_val_t)(uintptr_t)"hello"
|
||||
* Integer values are stored directly.
|
||||
* This lets arithmetic work naturally while still passing strings around.
|
||||
*
|
||||
* Type conventions (El -> C):
|
||||
* String -> el_val_t (holds const char* via uintptr_t cast)
|
||||
* Int -> el_val_t
|
||||
* Bool -> el_val_t (0 = false, nonzero = true)
|
||||
* Any -> el_val_t
|
||||
* Void -> void
|
||||
*
|
||||
* Macros for convenience:
|
||||
* EL_STR(s) cast string literal to el_val_t
|
||||
* EL_CSTR(v) cast el_val_t back to const char*
|
||||
* EL_INT(v) identity — el_val_t is already int64_t
|
||||
*
|
||||
* Link requirements:
|
||||
* -lcurl — required for the HTTP client (http_get, http_post, llm_*).
|
||||
* -lpthread — required for the HTTP server (one detached thread per
|
||||
* connection, capped at 64 concurrent).
|
||||
* -loqs — optional; required only when liboqs is installed and the
|
||||
* pq_* / sha3_256_hex entry points are needed. Detected at
|
||||
* compile time via __has_include(<oqs/oqs.h>).
|
||||
* -lcrypto — optional; pulled in alongside -loqs. Used for X25519 in
|
||||
* pq_hybrid_* and HKDF-SHA256 derivation.
|
||||
*
|
||||
* Canonical compile command:
|
||||
* cc -std=c11 -I el-compiler/runtime -lcurl -lpthread \
|
||||
* -o <out> <prog>.c el-compiler/runtime/el_runtime.c
|
||||
*
|
||||
* With liboqs (post-quantum stack):
|
||||
* cc -std=c11 -I el-compiler/runtime -lcurl -lpthread -loqs -lcrypto \
|
||||
* -o <out> <prog>.c el-compiler/runtime/el_runtime.c
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
typedef int64_t el_val_t;
|
||||
|
||||
#define EL_STR(s) ((el_val_t)(uintptr_t)(s))
|
||||
#define EL_CSTR(v) ((const char*)(uintptr_t)(v))
|
||||
#define EL_INT(v) (v)
|
||||
#define EL_NULL ((el_val_t)0)
|
||||
|
||||
/* Float values share the el_val_t (int64) slot via a bit-cast.
|
||||
* The codegen emits Float literals as `el_from_float(<dbl>)` so the
|
||||
* underlying bits represent the IEEE 754 double. Float-aware builtins
|
||||
* (math, format, json) round-trip via these helpers. */
|
||||
static inline double el_to_float(el_val_t v) {
|
||||
union { int64_t i; double f; } u;
|
||||
u.i = (int64_t)v;
|
||||
return u.f;
|
||||
}
|
||||
|
||||
static inline el_val_t el_from_float(double f) {
|
||||
union { double f; int64_t i; } u;
|
||||
u.f = f;
|
||||
return (el_val_t)u.i;
|
||||
}
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* ── I/O ──────────────────────────────────────────────────────────────────── */
|
||||
|
||||
void println(el_val_t s);
|
||||
void print(el_val_t s);
|
||||
el_val_t readline(void);
|
||||
|
||||
/* ── String builtins ─────────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t el_str_concat(el_val_t a, el_val_t b);
|
||||
el_val_t str_eq(el_val_t a, el_val_t b);
|
||||
el_val_t str_starts_with(el_val_t s, el_val_t prefix);
|
||||
el_val_t str_ends_with(el_val_t s, el_val_t suffix);
|
||||
el_val_t str_len(el_val_t s);
|
||||
el_val_t str_concat(el_val_t a, el_val_t b);
|
||||
el_val_t int_to_str(el_val_t n);
|
||||
el_val_t str_to_int(el_val_t s);
|
||||
el_val_t str_slice(el_val_t s, el_val_t start, el_val_t end);
|
||||
el_val_t str_contains(el_val_t s, el_val_t sub);
|
||||
el_val_t str_replace(el_val_t s, el_val_t from, el_val_t to);
|
||||
el_val_t str_to_upper(el_val_t s);
|
||||
el_val_t str_to_lower(el_val_t s);
|
||||
el_val_t str_trim(el_val_t s);
|
||||
|
||||
/* ── Math ────────────────────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t el_abs(el_val_t n);
|
||||
el_val_t el_max(el_val_t a, el_val_t b);
|
||||
el_val_t el_min(el_val_t a, el_val_t b);
|
||||
|
||||
/* ── Refcount (ARC) ──────────────────────────────────────────────────────────
|
||||
* Lists and Maps carry a refcount. Strings and ints do not — el_retain and
|
||||
* el_release are safe no-ops on non-refcounted values (they sniff a magic
|
||||
* header at offset 0 and only act if the magic matches).
|
||||
*
|
||||
* Codegen emits these at let-binding shadowing, function entry (params), and
|
||||
* function exit (locals other than the returned value). The refcount lets
|
||||
* el_list_append and el_map_set mutate in place when uniquely owned (cheap)
|
||||
* and copy-on-write when shared (preserves persistent semantics across
|
||||
* accumulator patterns in the compiler itself). */
|
||||
|
||||
void el_retain(el_val_t v);
|
||||
void el_release(el_val_t v);
|
||||
|
||||
/* ── List ────────────────────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t el_list_new(el_val_t count, ...);
|
||||
el_val_t el_list_len(el_val_t list);
|
||||
el_val_t el_list_get(el_val_t list, el_val_t index);
|
||||
el_val_t el_list_append(el_val_t list, el_val_t elem);
|
||||
el_val_t el_list_empty(void);
|
||||
el_val_t el_list_clone(el_val_t list);
|
||||
|
||||
/* ── Map ─────────────────────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t el_map_new(el_val_t pair_count, ...);
|
||||
el_val_t el_get_field(el_val_t map, el_val_t key);
|
||||
el_val_t el_map_get(el_val_t map, el_val_t key);
|
||||
el_val_t el_map_set(el_val_t map, el_val_t key, el_val_t value);
|
||||
|
||||
/* ── HTTP ─────────────────────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t http_get(el_val_t url);
|
||||
el_val_t http_post(el_val_t url, el_val_t body);
|
||||
el_val_t http_post_json(el_val_t url, el_val_t json_body);
|
||||
el_val_t http_get_with_headers(el_val_t url, el_val_t headers_map);
|
||||
el_val_t http_post_with_headers(el_val_t url, el_val_t body, el_val_t headers_map);
|
||||
el_val_t http_post_form_auth(el_val_t url, el_val_t form_body, el_val_t auth_header);
|
||||
el_val_t http_delete(el_val_t url);
|
||||
void http_serve(el_val_t port, el_val_t handler);
|
||||
void http_set_handler(el_val_t name);
|
||||
|
||||
/* HTTP server v2 ─────────────────────────────────────────────────────────────
|
||||
* Same dispatch model as http_serve, but the handler signature is widened:
|
||||
*
|
||||
* el_val_t handler(method, path, headers_map, body)
|
||||
*
|
||||
* `headers_map` is an ElMap from lowercased header name → header value (both
|
||||
* Strings). Repeated headers are joined with ", " per RFC 7230.
|
||||
*
|
||||
* Response value: the handler may return either
|
||||
* (a) a plain body string — same auto-content-type / 200-OK behaviour as
|
||||
* http_serve (3-arg) — or
|
||||
* (b) a response envelope built with `http_response(status, headers_json,
|
||||
* body)`. The runtime detects the envelope discriminator
|
||||
* `"el_http_response":1` at the start of the returned string and
|
||||
* unpacks status / headers / body before sending.
|
||||
*
|
||||
* The 3-arg http_serve(port, handler) remains supported unchanged for
|
||||
* existing handlers (e.g. products/web/server.el): it dispatches with
|
||||
* (method, path, body), hardcodes 200 OK, and auto-detects content type. */
|
||||
void http_serve_v2(el_val_t port, el_val_t handler);
|
||||
void http_set_handler_v2(el_val_t name);
|
||||
|
||||
/* Build an HTTP response envelope. `headers_json` should be a JSON object
|
||||
* literal like `{"WWW-Authenticate":"Basic"}` (or "" / "{}" for none). The
|
||||
* returned string carries the discriminator `{"el_http_response":1,...}`
|
||||
* which the runtime's send-path detects and unpacks. Detection happens
|
||||
* uniformly inside http_send_response, so a 3-arg handler may also return
|
||||
* an envelope. The 3-arg variant remains documented as a fixed 200-OK
|
||||
* auto-content-type contract for legacy handlers that return plain bodies. */
|
||||
el_val_t http_response(el_val_t status, el_val_t headers_json, el_val_t body);
|
||||
|
||||
/* SSE connection fd — set by http_worker_v2 before calling the El handler,
|
||||
* cleared afterwards. Defined in el_seed.c; called from el_runtime.c.
|
||||
* The getter is exposed as __http_conn_fd() to El programs. */
|
||||
void el_seed_set_http_conn_fd(int fd);
|
||||
|
||||
/* HTTP timeout — every libcurl request honors EL_HTTP_TIMEOUT_MS (default
|
||||
* 60000ms). Read lazily on first use, so setting the env var any time before
|
||||
* the first http_* call is sufficient. */
|
||||
|
||||
/* Streaming variants — write the response body straight to a file via
|
||||
* libcurl's CURLOPT_WRITEFUNCTION = fwrite. These bypass the el_val_t string
|
||||
* wrapper entirely, so binary payloads (audio/mpeg, image/png, etc.) survive
|
||||
* embedded NUL bytes that would truncate a strlen()-based code path.
|
||||
*
|
||||
* Both honor EL_HTTP_TIMEOUT_MS, follow redirects, and accept the same
|
||||
* `headers_map` shape as http_post_with_headers (ElMap of String→String).
|
||||
*
|
||||
* Return value: 1 on success (file fully written), 0 on any failure
|
||||
* (network, file open, partial write). On failure the output file is removed
|
||||
* so callers cannot mistake a partially-written file for a valid one. */
|
||||
el_val_t http_post_to_file(el_val_t url, el_val_t body, el_val_t headers_map, el_val_t output_path);
|
||||
el_val_t http_get_to_file(el_val_t url, el_val_t headers_map, el_val_t output_path);
|
||||
|
||||
/* ── URL encoding ────────────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t url_encode(el_val_t s); /* RFC 3986 unreserved set */
|
||||
el_val_t url_decode(el_val_t s); /* '+' → space, %XX → byte */
|
||||
|
||||
/* ── HTML allowlist sanitizer ────────────────────────────────────────────────
|
||||
* el_html_sanitize(input_html, allowlist_json) — strict allowlist HTML
|
||||
* cleaner. State-machine parser; tag/attribute names compared case-
|
||||
* insensitively against the allowlist; `<a href>` / `<… src>` URL schemes
|
||||
* validated (http, https, mailto, fragment-only, or relative); whole-
|
||||
* subtree drop for script / style / iframe / object / embed / form; HTML-
|
||||
* escapes free text outside dropped subtrees.
|
||||
*
|
||||
* The allowlist is JSON of the form
|
||||
* {"p":[],"a":["href","title"],"strong":[],...}
|
||||
* where each value is the array of attribute names allowed for that tag. */
|
||||
el_val_t el_html_sanitize(el_val_t input_html, el_val_t allowlist_json);
|
||||
|
||||
/* ── Filesystem ──────────────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t fs_read(el_val_t path);
|
||||
el_val_t fs_write(el_val_t path, el_val_t content);
|
||||
el_val_t fs_list(el_val_t path);
|
||||
el_val_t fs_exists(el_val_t path);
|
||||
el_val_t fs_mkdir(el_val_t path); /* mkdir -p, mode 0755 */
|
||||
|
||||
/* Length-explicit binary write. `length` is an Int (el_val_t holding the
|
||||
* byte count). The caller knows the length from context — typically because
|
||||
* `bytes` came from base64_decode (which produces a magic-tagged binary
|
||||
* buffer with embedded NULs possible) and the caller already tracks the
|
||||
* decoded length, OR because the bytes came from a fixed-size source
|
||||
* (sha256_bytes = 32, hmac_sha256_bytes = 32). Bypasses strlen entirely.
|
||||
*
|
||||
* Returns 1 on success, 0 on failure (invalid path, can't open, partial
|
||||
* write, negative length). On partial-write failure, the file is removed
|
||||
* so callers cannot read back a truncated artefact. */
|
||||
el_val_t fs_write_bytes(el_val_t path, el_val_t bytes, el_val_t length);
|
||||
|
||||
/* ── JSON ────────────────────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t json_get(el_val_t json, el_val_t key);
|
||||
el_val_t json_parse(el_val_t s);
|
||||
el_val_t json_stringify(el_val_t v);
|
||||
el_val_t json_get_string(el_val_t json_str, el_val_t key);
|
||||
el_val_t json_get_int(el_val_t json_str, el_val_t key);
|
||||
el_val_t json_get_float(el_val_t json_str, el_val_t key);
|
||||
el_val_t json_get_bool(el_val_t json_str, el_val_t key);
|
||||
el_val_t json_get_raw(el_val_t json_str, el_val_t key);
|
||||
el_val_t json_set(el_val_t json_str, el_val_t key, el_val_t value);
|
||||
el_val_t json_array_len(el_val_t json_str);
|
||||
el_val_t json_array_get(el_val_t json_str, el_val_t index);
|
||||
el_val_t json_array_get_string(el_val_t json_str, el_val_t index);
|
||||
|
||||
/* ── Time ────────────────────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t time_now(void);
|
||||
el_val_t time_now_utc(void);
|
||||
el_val_t sleep_secs(el_val_t secs);
|
||||
el_val_t sleep_ms(el_val_t ms);
|
||||
el_val_t time_format(el_val_t ts, el_val_t fmt);
|
||||
el_val_t time_to_parts(el_val_t ts);
|
||||
el_val_t time_from_parts(el_val_t secs, el_val_t ns, el_val_t tz);
|
||||
el_val_t time_add(el_val_t ts, el_val_t n, el_val_t unit);
|
||||
el_val_t time_diff(el_val_t ts1, el_val_t ts2, el_val_t unit);
|
||||
|
||||
/* ── Instant + Duration: first-class temporal types ──────────────────────────
|
||||
* Both types share the el_val_t (int64) slot. Instants are nanoseconds
|
||||
* since the Unix epoch; Durations are signed nanoseconds. Type discipline
|
||||
* is enforced at codegen-time: BinOps on names registered as Instant or
|
||||
* Duration route through the typed wrappers below; mismatches like
|
||||
* Instant+Instant become #error at the C compiler.
|
||||
*
|
||||
* Postfix literals — `30.seconds`, `1.hour`, `500.millis`, `30.nanos` — are
|
||||
* recognised by the parser as DurationLit AST nodes and lowered to literal
|
||||
* int64 nanoseconds at codegen time. The runtime never sees the units. */
|
||||
|
||||
el_val_t el_now_instant(void);
|
||||
el_val_t now(void);
|
||||
el_val_t unix_seconds(el_val_t n);
|
||||
el_val_t unix_millis(el_val_t n);
|
||||
el_val_t instant_from_iso8601(el_val_t s);
|
||||
|
||||
el_val_t el_duration_from_nanos(el_val_t ns);
|
||||
el_val_t duration_seconds(el_val_t n);
|
||||
el_val_t duration_millis(el_val_t n);
|
||||
el_val_t duration_nanos(el_val_t n);
|
||||
|
||||
el_val_t el_instant_add_dur(el_val_t inst, el_val_t dur);
|
||||
el_val_t el_instant_sub_dur(el_val_t inst, el_val_t dur);
|
||||
el_val_t el_instant_diff(el_val_t a, el_val_t b);
|
||||
el_val_t el_duration_add(el_val_t a, el_val_t b);
|
||||
el_val_t el_duration_sub(el_val_t a, el_val_t b);
|
||||
el_val_t el_duration_scale(el_val_t dur, el_val_t scalar);
|
||||
el_val_t el_duration_div(el_val_t dur, el_val_t scalar);
|
||||
|
||||
el_val_t el_instant_lt(el_val_t a, el_val_t b);
|
||||
el_val_t el_instant_le(el_val_t a, el_val_t b);
|
||||
el_val_t el_instant_gt(el_val_t a, el_val_t b);
|
||||
el_val_t el_instant_ge(el_val_t a, el_val_t b);
|
||||
el_val_t el_instant_eq(el_val_t a, el_val_t b);
|
||||
el_val_t el_instant_ne(el_val_t a, el_val_t b);
|
||||
el_val_t el_duration_lt(el_val_t a, el_val_t b);
|
||||
el_val_t el_duration_le(el_val_t a, el_val_t b);
|
||||
el_val_t el_duration_gt(el_val_t a, el_val_t b);
|
||||
el_val_t el_duration_ge(el_val_t a, el_val_t b);
|
||||
el_val_t el_duration_eq(el_val_t a, el_val_t b);
|
||||
el_val_t el_duration_ne(el_val_t a, el_val_t b);
|
||||
|
||||
el_val_t instant_to_unix_seconds(el_val_t i);
|
||||
el_val_t instant_to_unix_millis(el_val_t i);
|
||||
el_val_t instant_to_iso8601(el_val_t i);
|
||||
el_val_t duration_to_seconds(el_val_t d);
|
||||
el_val_t duration_to_millis(el_val_t d);
|
||||
el_val_t duration_to_nanos(el_val_t d);
|
||||
|
||||
el_val_t el_sleep_duration(el_val_t dur);
|
||||
el_val_t unix_timestamp(void);
|
||||
|
||||
el_val_t ttl_cache_set(el_val_t key, el_val_t value);
|
||||
el_val_t ttl_cache_get(el_val_t key, el_val_t max_age);
|
||||
el_val_t ttl_cache_age(el_val_t key);
|
||||
|
||||
/* ── Calendar + CalendarTime + Rhythm + LocalDate/Time/DateTime ─────────────
|
||||
* Phase 1.5 of the time system. Calendar is pluggable: EarthCalendar (IANA
|
||||
* zones, Gregorian, DST) is the user-facing default; MarsCalendar,
|
||||
* CycleCalendar(period), NoCycleCalendar, RelativeCalendar handle non-Earth
|
||||
* domains.
|
||||
*
|
||||
* A Calendar interprets an Instant under a particular cycle convention and
|
||||
* produces a CalendarTime. CalendarTime carries the underlying Instant and
|
||||
* a back-pointer to its Calendar; arithmetic and formatting consult the
|
||||
* Calendar to convert ns since epoch into year/month/day/hour/minute/second
|
||||
* (or sol/phase, or cycle/phase, depending on kind).
|
||||
*
|
||||
* Storage convention: Calendar / CalendarTime / Rhythm / LocalDate /
|
||||
* LocalDateTime are heap-allocated structs whose pointers are cast into
|
||||
* el_val_t. A 24-bit magic header at offset 0 lets the runtime identify
|
||||
* the kind safely. LocalTime is small enough to live in the int64 slot
|
||||
* directly (nanos since midnight, signed). */
|
||||
|
||||
/* Zone — opaque IANA zone or fixed offset, used by EarthCalendar.
|
||||
* `zone_id` is either an IANA name ("America/New_York", "UTC") or a fixed
|
||||
* offset string ("+05:30", "-08:00"). The runtime resolves it via tzset()
|
||||
* on first use of the owning EarthCalendar. */
|
||||
el_val_t zone(el_val_t id);
|
||||
el_val_t zone_utc(void);
|
||||
el_val_t zone_local(void);
|
||||
el_val_t zone_offset(el_val_t hours, el_val_t minutes);
|
||||
|
||||
/* Calendar constructors. Each returns an el_val_t pointer to a heap-
|
||||
* allocated, magic-tagged Calendar struct. Calendars are interned by
|
||||
* (kind, zone_id, period_ns, epoch_ns) so identical constructors return
|
||||
* the same pointer — equality is reference equality. */
|
||||
el_val_t earth_calendar(el_val_t z);
|
||||
el_val_t earth_calendar_default(void);
|
||||
el_val_t mars_calendar(void);
|
||||
el_val_t cycle_calendar(el_val_t period_dur);
|
||||
el_val_t no_cycle_calendar(void);
|
||||
el_val_t relative_calendar(el_val_t epoch_inst);
|
||||
|
||||
/* CalendarTime constructors and methods. Returns a heap-allocated struct
|
||||
* whose pointer fits in el_val_t. */
|
||||
el_val_t now_in(el_val_t cal);
|
||||
el_val_t in_calendar(el_val_t inst, el_val_t cal);
|
||||
el_val_t cal_format(el_val_t ct, el_val_t pattern);
|
||||
el_val_t cal_to_instant(el_val_t ct);
|
||||
el_val_t cal_cycle_phase(el_val_t ct);
|
||||
el_val_t cal_in(el_val_t ct, el_val_t cal);
|
||||
|
||||
/* LocalDate / LocalTime / LocalDateTime — calendar-agnostic value types.
|
||||
* LocalTime carries nanoseconds since midnight as a signed int64 directly
|
||||
* in the el_val_t slot (no allocation). LocalDate / LocalDateTime are
|
||||
* heap-allocated structs with magic headers. */
|
||||
el_val_t local_date(el_val_t y, el_val_t m, el_val_t d);
|
||||
el_val_t local_time(el_val_t h, el_val_t m, el_val_t s, el_val_t ns);
|
||||
el_val_t local_datetime(el_val_t date, el_val_t time);
|
||||
el_val_t zoned(el_val_t date, el_val_t time, el_val_t cal);
|
||||
|
||||
el_val_t local_date_year(el_val_t ld);
|
||||
el_val_t local_date_month(el_val_t ld);
|
||||
el_val_t local_date_day(el_val_t ld);
|
||||
el_val_t local_time_hour(el_val_t lt);
|
||||
el_val_t local_time_minute(el_val_t lt);
|
||||
el_val_t local_time_second(el_val_t lt);
|
||||
el_val_t local_time_nanos(el_val_t lt);
|
||||
|
||||
el_val_t el_local_date_add_dur(el_val_t ld, el_val_t dur);
|
||||
el_val_t el_local_time_add_dur(el_val_t lt, el_val_t dur);
|
||||
el_val_t el_local_date_lt(el_val_t a, el_val_t b);
|
||||
el_val_t el_local_date_eq(el_val_t a, el_val_t b);
|
||||
|
||||
/* Rhythm — pluggable recurrence AST. Returns a heap-allocated struct
|
||||
* pointer in el_val_t; rhythms are immutable so callers may share them. */
|
||||
el_val_t rhythm_cycle_start(void);
|
||||
el_val_t rhythm_cycle_phase(el_val_t phase);
|
||||
el_val_t rhythm_duration(el_val_t d);
|
||||
el_val_t rhythm_session_start(void);
|
||||
el_val_t rhythm_event(el_val_t name);
|
||||
el_val_t rhythm_and(el_val_t a, el_val_t b);
|
||||
el_val_t rhythm_or(el_val_t a, el_val_t b);
|
||||
el_val_t rhythm_weekday(el_val_t day);
|
||||
el_val_t rhythm_weekly_at(el_val_t day, el_val_t hour, el_val_t minute);
|
||||
el_val_t rhythm_next_after(el_val_t r, el_val_t after, el_val_t cal);
|
||||
el_val_t rhythm_matches(el_val_t r, el_val_t ct);
|
||||
|
||||
/* ── UUID ────────────────────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t uuid_new(void);
|
||||
el_val_t uuid_v4(void);
|
||||
|
||||
/* ── Environment ─────────────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t env(el_val_t key);
|
||||
|
||||
/* ── In-process state K/V ────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t state_set(el_val_t key, el_val_t value);
|
||||
el_val_t state_get(el_val_t key);
|
||||
el_val_t state_del(el_val_t key);
|
||||
el_val_t state_keys(void);
|
||||
|
||||
/* ── Float formatting ────────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t float_to_str(el_val_t f);
|
||||
el_val_t int_to_float(el_val_t n);
|
||||
el_val_t float_to_int(el_val_t f);
|
||||
el_val_t format_float(el_val_t f, el_val_t decimals);
|
||||
el_val_t decimal_round(el_val_t f, el_val_t decimals);
|
||||
el_val_t str_to_float(el_val_t s);
|
||||
|
||||
/* ── Math (Float-aware) ──────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t math_sqrt(el_val_t f);
|
||||
el_val_t math_log(el_val_t f);
|
||||
el_val_t math_ln(el_val_t f);
|
||||
el_val_t math_sin(el_val_t f);
|
||||
el_val_t math_cos(el_val_t f);
|
||||
el_val_t math_pi(void);
|
||||
|
||||
/* ── String additions ────────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t str_index_of(el_val_t s, el_val_t sub);
|
||||
el_val_t str_split(el_val_t s, el_val_t sep);
|
||||
el_val_t str_char_at(el_val_t s, el_val_t i);
|
||||
el_val_t str_char_code(el_val_t s, el_val_t i);
|
||||
el_val_t str_pad_left(el_val_t s, el_val_t width, el_val_t pad);
|
||||
el_val_t str_pad_right(el_val_t s, el_val_t width, el_val_t pad);
|
||||
el_val_t str_format(el_val_t fmt, el_val_t data);
|
||||
el_val_t str_lower(el_val_t s);
|
||||
el_val_t str_upper(el_val_t s);
|
||||
|
||||
/* ── Text-processing primitives (Phase 1: byte/codepoint, ASCII char classes)
|
||||
* Phase 2 (filed): Unicode-grapheme awareness, NFC/NFD normalization, regex.
|
||||
* is_* predicates: empty input returns false; multi-char requires ALL bytes
|
||||
* to match. ASCII ranges only in Phase 1. */
|
||||
|
||||
/* Counting */
|
||||
el_val_t str_count(el_val_t s, el_val_t sub); /* non-overlapping */
|
||||
el_val_t str_count_chars(el_val_t s); /* codepoint count */
|
||||
el_val_t str_count_bytes(el_val_t s); /* alias of str_len */
|
||||
el_val_t str_count_lines(el_val_t s);
|
||||
el_val_t str_count_words(el_val_t s);
|
||||
el_val_t str_count_letters(el_val_t s); /* ASCII [A-Za-z] */
|
||||
el_val_t str_count_digits(el_val_t s); /* ASCII [0-9] */
|
||||
|
||||
/* Find / position */
|
||||
el_val_t str_index_of_all(el_val_t s, el_val_t sub); /* [Int] of byte offsets */
|
||||
el_val_t str_last_index_of(el_val_t s, el_val_t sub);
|
||||
el_val_t str_find_chars(el_val_t s, el_val_t any_of); /* first idx of any ch */
|
||||
|
||||
/* Transform */
|
||||
el_val_t str_repeat(el_val_t s, el_val_t n);
|
||||
el_val_t str_reverse(el_val_t s); /* by codepoint */
|
||||
el_val_t str_strip_prefix(el_val_t s, el_val_t prefix);
|
||||
el_val_t str_strip_suffix(el_val_t s, el_val_t suffix);
|
||||
el_val_t str_strip_chars(el_val_t s, el_val_t chars);
|
||||
el_val_t str_lstrip(el_val_t s);
|
||||
el_val_t str_rstrip(el_val_t s);
|
||||
|
||||
/* Char classification (Bool) */
|
||||
el_val_t is_letter(el_val_t s);
|
||||
el_val_t is_digit(el_val_t s);
|
||||
el_val_t is_alphanumeric(el_val_t s);
|
||||
el_val_t is_whitespace(el_val_t s);
|
||||
el_val_t is_punctuation(el_val_t s);
|
||||
el_val_t is_uppercase(el_val_t s);
|
||||
el_val_t is_lowercase(el_val_t s);
|
||||
|
||||
/* Split / join */
|
||||
el_val_t str_split_lines(el_val_t s);
|
||||
el_val_t str_split_chars(el_val_t s); /* alias of native_string_chars */
|
||||
el_val_t str_split_n(el_val_t s, el_val_t sep, el_val_t n);
|
||||
el_val_t str_join(el_val_t list, el_val_t sep); /* alias of list_join */
|
||||
|
||||
/* ── List additions ──────────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t list_push(el_val_t list, el_val_t elem);
|
||||
el_val_t list_push_front(el_val_t list, el_val_t elem);
|
||||
el_val_t list_join(el_val_t list, el_val_t sep);
|
||||
el_val_t list_range(el_val_t start, el_val_t end);
|
||||
|
||||
/* ── Bool helpers ────────────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t bool_to_str(el_val_t b);
|
||||
|
||||
/* ── Numeric parsing ─────────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t parse_int(el_val_t s, el_val_t default_val);
|
||||
|
||||
/* ── Process ─────────────────────────────────────────────────────────────── */
|
||||
|
||||
void exit_program(el_val_t code);
|
||||
el_val_t getpid_now(void);
|
||||
|
||||
/* ── CGI identity ─────────────────────────────────────────────────────────────
|
||||
* Called at the start of main() in CGI programs (those with a `cgi {}` block).
|
||||
* Records the program's DHARMA identity before any other code executes. */
|
||||
|
||||
void el_cgi_init(el_val_t name, el_val_t dharma_id, el_val_t principal,
|
||||
el_val_t network, el_val_t engram);
|
||||
|
||||
/* ── DHARMA network builtins ─────────────────────────────────────────────────
|
||||
* Available to CGI programs (declared with a `cgi {}` block).
|
||||
*
|
||||
* Peers are addressed by `dharma_id` of the form
|
||||
* "<registry-id>@<transport-url>" e.g. "ntn-genesis@http://localhost:7770"
|
||||
* If the @<url> portion is omitted, transport defaults to
|
||||
* "http://localhost:7770" (the local CGI daemon assumption).
|
||||
*
|
||||
* Wire protocol (all peers expose):
|
||||
* POST <url>/dharma/recv { channel, from, content } → response body
|
||||
* POST <url>/dharma/event { type, payload, source, timestamp }
|
||||
* POST <url>/api/activate { query } → list of nodes
|
||||
*
|
||||
* Hosting application's responsibility: an El program with a `cgi {}` block
|
||||
* runs http_serve() with its own request handler; that handler should route
|
||||
* "/dharma/event" requests by calling el_runtime_dharma_event_arrive() so
|
||||
* incoming events feed dharma_field() queues. The runtime itself does not
|
||||
* intercept any /dharma path. */
|
||||
|
||||
el_val_t dharma_connect(el_val_t cgi_id);
|
||||
el_val_t dharma_send(el_val_t channel, el_val_t content);
|
||||
el_val_t dharma_activate(el_val_t query);
|
||||
void dharma_emit(el_val_t event_type, el_val_t payload);
|
||||
el_val_t dharma_field(el_val_t event_type);
|
||||
void dharma_strengthen(el_val_t cgi_id, el_val_t weight);
|
||||
el_val_t dharma_relationship(el_val_t cgi_id);
|
||||
el_val_t dharma_peers(void);
|
||||
|
||||
/* Public C API: called by an El program's HTTP handler when a /dharma/event
|
||||
* request arrives. Pushes onto the per-event-type queue and signals any
|
||||
* pending dharma_field() blockers. All three arguments must be NUL-terminated
|
||||
* C strings (or NULL — then treated as empty). */
|
||||
void el_runtime_dharma_event_arrive(const char* event_type,
|
||||
const char* payload,
|
||||
const char* source);
|
||||
|
||||
/* ── Engram local graph primitives ───────────────────────────────────────────
|
||||
* Operate on the CGI's local Engram knowledge graph.
|
||||
* `engram_activate` queries the local graph only; `dharma_activate` is
|
||||
* network-wide across all connected CGI graphs. */
|
||||
|
||||
el_val_t engram_node(el_val_t content, el_val_t node_type, el_val_t salience);
|
||||
el_val_t engram_node_full(el_val_t content, el_val_t node_type, el_val_t label,
|
||||
el_val_t salience, el_val_t importance, el_val_t confidence,
|
||||
el_val_t tier, el_val_t tags);
|
||||
/* Layered consciousness — see el_runtime.c for the layered architecture
|
||||
* design notes (search "Layered consciousness architecture"). The five
|
||||
* canonical layers (safety / core-identity / domain-knowledge / imprint /
|
||||
* suit) are seeded automatically; engram_add_layer extends the registry
|
||||
* with imprint or suit overlays at runtime. Nodes default to layer 1
|
||||
* (core-identity) when created via engram_node / engram_node_full. */
|
||||
el_val_t engram_node_layered(el_val_t content, el_val_t node_type, el_val_t label,
|
||||
el_val_t salience, el_val_t certainty, el_val_t confidence,
|
||||
el_val_t status, el_val_t tags, el_val_t layer_id);
|
||||
el_val_t engram_add_layer(el_val_t name, el_val_t priority, el_val_t suppressible,
|
||||
el_val_t transparent, el_val_t injectable);
|
||||
el_val_t engram_remove_layer(el_val_t layer_id);
|
||||
el_val_t engram_list_layers(void);
|
||||
el_val_t engram_get_node(el_val_t id);
|
||||
void engram_strengthen(el_val_t node_id);
|
||||
void engram_forget(el_val_t node_id);
|
||||
el_val_t engram_node_count(void);
|
||||
el_val_t engram_search(el_val_t query, el_val_t limit);
|
||||
el_val_t engram_scan_nodes(el_val_t limit, el_val_t offset);
|
||||
void engram_connect(el_val_t from_id, el_val_t to_id, el_val_t weight, el_val_t relation);
|
||||
el_val_t engram_edge_between(el_val_t from_id, el_val_t to_id);
|
||||
el_val_t engram_neighbors(el_val_t node_id);
|
||||
el_val_t engram_neighbors_filtered(el_val_t node_id, el_val_t max_depth, el_val_t direction);
|
||||
el_val_t engram_edge_count(void);
|
||||
/* Three-pass activation: background fan-out → working-memory promotion →
|
||||
* Layer 0 override. See "Three-pass activation" in el_runtime.c. */
|
||||
el_val_t engram_activate(el_val_t query, el_val_t depth);
|
||||
el_val_t engram_save(el_val_t path);
|
||||
el_val_t engram_load(el_val_t path);
|
||||
|
||||
/* JSON-string accessors — return pre-serialized JSON so HTTP handlers
|
||||
* can pass results straight through without round-tripping ElList/ElMap
|
||||
* through json_stringify. */
|
||||
el_val_t engram_get_node_json(el_val_t id);
|
||||
el_val_t engram_search_json(el_val_t query, el_val_t limit);
|
||||
el_val_t engram_scan_nodes_json(el_val_t limit, el_val_t offset);
|
||||
el_val_t engram_scan_nodes_by_type_json(el_val_t node_type, el_val_t limit, el_val_t offset);
|
||||
el_val_t engram_neighbors_json(el_val_t node_id, el_val_t max_depth, el_val_t direction);
|
||||
el_val_t engram_activate_json(el_val_t query, el_val_t depth);
|
||||
el_val_t engram_stats_json(void);
|
||||
el_val_t engram_list_layers_json(void);
|
||||
/* engram_compile_layered_json — produce a prompt-ready text block split
|
||||
* into "[LAYER 0 — STRUCTURAL]" (non-suppressible layers, sacred fire)
|
||||
* and "[ENGRAM CONTEXT]" (standard suppressible layers). Returns "" if
|
||||
* no nodes promoted to working memory. */
|
||||
el_val_t engram_compile_layered_json(el_val_t intent, el_val_t depth);
|
||||
|
||||
/* ── LLM (Anthropic API client) ─────────────────────────────────────────────
|
||||
* All functions call https://api.anthropic.com/v1/messages with the API key
|
||||
* from env ANTHROPIC_API_KEY. Default model when empty: claude-sonnet-4-5. */
|
||||
|
||||
el_val_t llm_call(el_val_t model, el_val_t prompt);
|
||||
el_val_t llm_call_system(el_val_t model, el_val_t system_prompt, el_val_t user_prompt);
|
||||
el_val_t llm_call_agentic(el_val_t model, el_val_t system, el_val_t user, el_val_t tools);
|
||||
el_val_t llm_vision(el_val_t model, el_val_t system, el_val_t prompt, el_val_t image_url_or_b64);
|
||||
el_val_t llm_models(void);
|
||||
|
||||
/* Register a tool handler by name. The handler is looked up via dlsym
|
||||
* (mirroring http_set_handler), so any El `fn <name>(input)` compiles to
|
||||
* a global C symbol that this function can locate at runtime.
|
||||
* Handler signature: `el_val_t handler(el_val_t input_json)` — receives
|
||||
* the tool input as a JSON-string el_val_t and returns a JSON-string
|
||||
* el_val_t result. Used by llm_call_agentic. */
|
||||
void llm_register_tool(el_val_t name, el_val_t handler_fn_name);
|
||||
|
||||
/* ── args() ─────────────────────────────────────────────────────────────────
|
||||
* Provides access to command-line arguments passed to the program.
|
||||
* Populated by el_runtime_init_args() before main() runs. */
|
||||
|
||||
el_val_t args(void);
|
||||
void el_runtime_init_args(int argc, char** argv);
|
||||
|
||||
/* ── Crypto primitives ─────────────────────────────────────────────────────
|
||||
* SHA-256, HMAC-SHA-256, and base64 (standard + URL-safe).
|
||||
* Self-contained — no OpenSSL/libcrypto dependency. The implementations are
|
||||
* adapted from public-domain reference code (Brad Conte / RFC 4648).
|
||||
*
|
||||
* Bytes-returning variants (sha256_bytes, hmac_sha256_bytes) return a string
|
||||
* value whose contents are raw binary; callers usually feed these into
|
||||
* base64_encode. Note that el_val_t strings are NUL-terminated by convention,
|
||||
* so the binary payload may contain embedded NULs — pass it directly into
|
||||
* base64_encode (which uses an explicit length) rather than treating it as
|
||||
* a printable C string.
|
||||
*
|
||||
* The "base64" variants emit/accept RFC 4648 standard alphabet with padding.
|
||||
* The "base64url" variants use URL-safe alphabet (`-`/`_`) with no padding,
|
||||
* as used in JWTs. */
|
||||
|
||||
el_val_t sha256_hex(el_val_t input);
|
||||
el_val_t sha256_bytes(el_val_t input);
|
||||
el_val_t hmac_sha256_hex(el_val_t key, el_val_t message);
|
||||
el_val_t hmac_sha256_bytes(el_val_t key, el_val_t message);
|
||||
el_val_t base64_encode(el_val_t input);
|
||||
el_val_t base64_decode(el_val_t input);
|
||||
el_val_t base64url_encode(el_val_t input);
|
||||
el_val_t base64url_decode(el_val_t input);
|
||||
|
||||
/* Length-aware variants (internal — exposed for the rare caller that already
|
||||
* has a known-length binary buffer and doesn't want to round-trip through
|
||||
* a NUL-terminated el_val_t string). Sha256_bytes and hmac_sha256_bytes feed
|
||||
* these implicitly. */
|
||||
el_val_t el_sha256_bytes_n(const unsigned char* data, size_t len);
|
||||
el_val_t el_base64_encode_n(const unsigned char* data, size_t len, int url_safe);
|
||||
|
||||
/* ── Post-quantum primitives (liboqs-backed) ────────────────────────────────
|
||||
* All inputs/outputs hex-encoded. Algorithm choices:
|
||||
* Signature: CRYSTALS-Dilithium-3 (NIST level 3, balanced)
|
||||
* KEM: CRYSTALS-Kyber-768 (NIST level 3)
|
||||
* Hash: SHA3-256 (Keccak) (PQ-aware protocols favour SHA3 over SHA2)
|
||||
*
|
||||
* If liboqs is not linked (detected via __has_include(<oqs/oqs.h>) at compile
|
||||
* time), the pq_* entry points return a JSON-shaped error string so callers
|
||||
* fail loudly rather than silently fall back to classical schemes:
|
||||
* {"error":"liboqs not linked, post-quantum primitives unavailable"}
|
||||
*
|
||||
* The hybrid handshake pairs X25519 with Kyber-768 per NIST PQ guidance and
|
||||
* CNSA 2.0. Combined shared secret is HKDF-SHA256(x25519_ss || kyber_ss).
|
||||
* Even if Kyber falls, X25519 holds; if X25519 falls under quantum attack,
|
||||
* Kyber holds. SHA3-256 also remains usable independent of liboqs (the
|
||||
* Keccak permutation is PQ-OK as a primitive). */
|
||||
|
||||
el_val_t pq_keygen_signature(void);
|
||||
el_val_t pq_sign(el_val_t secret_key_hex, el_val_t message);
|
||||
el_val_t pq_verify(el_val_t public_key_hex, el_val_t message, el_val_t signature_hex);
|
||||
|
||||
el_val_t pq_kem_keygen(void);
|
||||
el_val_t pq_kem_encaps(el_val_t public_key_hex);
|
||||
el_val_t pq_kem_decaps(el_val_t secret_key_hex, el_val_t ciphertext_hex);
|
||||
|
||||
el_val_t pq_hybrid_keygen(void);
|
||||
el_val_t pq_hybrid_handshake(el_val_t remote_pub_combined);
|
||||
|
||||
el_val_t sha3_256_hex(el_val_t input);
|
||||
|
||||
/* ── AEAD: AES-256-GCM (libcrypto-backed) ───────────────────────────────────
|
||||
* Symmetric authenticated encryption used to wrap envelopes after a KEM
|
||||
* handshake. Caller MUST supply a 32-byte key (64 hex chars) — typically the
|
||||
* Kyber-768 / hybrid shared_secret, optionally normalized via SHA3-256.
|
||||
*
|
||||
* aead_encrypt returns a JSON map {"nonce":"...","ciphertext":"..."} where
|
||||
* ciphertext is the AES-256-GCM output with the 16-byte auth tag appended.
|
||||
* Nonce is a fresh 12-byte CSPRNG draw — callers never pick the nonce, which
|
||||
* structurally rules out the GCM nonce-reuse footgun.
|
||||
*
|
||||
* aead_decrypt returns the plaintext String, or "" on any failure (including
|
||||
* auth-tag mismatch). Callers MUST check for "" before trusting the result. */
|
||||
el_val_t aead_encrypt(el_val_t key_hex, el_val_t plaintext);
|
||||
el_val_t aead_decrypt(el_val_t key_hex, el_val_t nonce_hex, el_val_t ciphertext_hex);
|
||||
|
||||
/* ── Native VM builtin aliases (for compiled El source) ─────────────────────
|
||||
* These match the El VM's native_* builtins so that El source compiled
|
||||
* to C can call the same names without modification. */
|
||||
|
||||
el_val_t native_list_get(el_val_t list, el_val_t index);
|
||||
el_val_t native_list_len(el_val_t list);
|
||||
el_val_t native_list_append(el_val_t list, el_val_t elem);
|
||||
el_val_t native_list_empty(void);
|
||||
el_val_t native_list_clone(el_val_t list);
|
||||
el_val_t native_string_chars(el_val_t s);
|
||||
el_val_t native_int_to_str(el_val_t n);
|
||||
|
||||
/* ── Method-call shorthand aliases ──────────────────────────────────────────
|
||||
* The El method-call convention `obj.method(args)` compiles to
|
||||
* `method(obj, args)`. These aliases expose the runtime functions under
|
||||
* the short names that result from method calls in El source.
|
||||
*
|
||||
* Example: `myList.append(x)` → `append(myList, x)` (calls this alias)
|
||||
* `myList.len()` → `len(myList)` (calls this alias) */
|
||||
|
||||
el_val_t append(el_val_t list, el_val_t elem); /* el_list_append */
|
||||
el_val_t len(el_val_t list); /* el_list_len */
|
||||
el_val_t get(el_val_t list, el_val_t index); /* el_list_get */
|
||||
el_val_t map_get(el_val_t map, el_val_t key); /* el_map_get */
|
||||
el_val_t map_set(el_val_t map, el_val_t key, el_val_t value); /* el_map_set */
|
||||
|
||||
/* ── OTLP/HTTP Observability ─────────────────────────────────────────────── */
|
||||
/* See bottom of el_runtime.c for the implementation.
|
||||
* Configured by env vars OTLP_ENDPOINT, OTEL_SERVICE_NAME, OTEL_SERVICE_VERSION.
|
||||
* No-op when OTLP_ENDPOINT is unset. Drop-on-failure semantics. */
|
||||
/* ── Subprocess execution ────────────────────────────────────────────────── */
|
||||
el_val_t exec_command(el_val_t cmd); /* run shell command, return exit code */
|
||||
el_val_t exec_capture(el_val_t cmd); /* run shell command, capture stdout */
|
||||
el_val_t exec(el_val_t cmd); /* exec(cmd) → stdout String (30s timeout) */
|
||||
el_val_t exec_bg(el_val_t cmd); /* exec_bg(cmd) → PID String (non-blocking) */
|
||||
|
||||
el_val_t emit_log(el_val_t level, el_val_t msg, el_val_t fields_json);
|
||||
el_val_t emit_metric(el_val_t name, el_val_t value, el_val_t tags_json);
|
||||
el_val_t trace_span_start(el_val_t name);
|
||||
el_val_t trace_span_end(el_val_t span_handle);
|
||||
el_val_t emit_event(el_val_t name, el_val_t duration_ms);
|
||||
|
||||
el_val_t __thread_create(el_val_t fn_name_v, el_val_t arg_v);
|
||||
el_val_t __thread_join(el_val_t tid_v);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,248 @@
|
||||
/*
|
||||
* el_seed.h — El language seed runtime header
|
||||
*
|
||||
* Declares all OS-boundary primitives available to compiled El programs.
|
||||
* All functions use the __ prefix convention. Signatures use el_val_t (= int64_t)
|
||||
* as the universal value type.
|
||||
*
|
||||
* el_seed.c is the complete C boundary for the El runtime. The heavy runtime
|
||||
* (el_runtime.c) has been retired — everything lives in el_seed.c plus the
|
||||
* native El runtime (runtime/ *.el files).
|
||||
*
|
||||
* Link requirements:
|
||||
* -lcurl — HTTP client (__http_do, __http_do_to_file)
|
||||
* -lpthread — threading (__thread_create, __thread_join, __mutex_new, ...)
|
||||
*
|
||||
* Canonical compile (via elb):
|
||||
* elb builds and links el_seed.c automatically.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
/* ── Value model ─────────────────────────────────────────────────────────────
|
||||
* All El values are el_val_t (int64_t). On 64-bit systems a pointer fits.
|
||||
* String -> el_val_t (holds const char* via uintptr_t cast)
|
||||
* Int -> el_val_t (stored directly)
|
||||
* Bool -> el_val_t (0 = false, nonzero = true)
|
||||
* Void -> void
|
||||
*/
|
||||
typedef int64_t el_val_t;
|
||||
|
||||
#define EL_STR(s) ((el_val_t)(uintptr_t)(s))
|
||||
#define EL_CSTR(v) ((const char*)(uintptr_t)(v))
|
||||
#define EL_INT(v) (v)
|
||||
#define EL_NULL ((el_val_t)0)
|
||||
|
||||
/* Float values share the el_val_t slot via bit-cast. */
|
||||
static inline double el_to_float(el_val_t v) {
|
||||
union { int64_t i; double f; } u; u.i = (int64_t)v; return u.f;
|
||||
}
|
||||
static inline el_val_t el_from_float(double f) {
|
||||
union { double f; int64_t i; } u; u.f = f; return (el_val_t)u.i;
|
||||
}
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* ── String primitives ───────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t __str_len(el_val_t s);
|
||||
el_val_t __str_char_at(el_val_t s, el_val_t i); /* returns Int (byte value) */
|
||||
el_val_t __str_alloc(el_val_t n); /* malloc(n+1), zero-init, return String */
|
||||
el_val_t __str_set_char(el_val_t s, el_val_t i, el_val_t c); /* s[i]=c, return s */
|
||||
el_val_t __str_cmp(el_val_t a, el_val_t b); /* strcmp result as Int */
|
||||
el_val_t __str_ncmp(el_val_t a, el_val_t b, el_val_t n); /* strncmp */
|
||||
el_val_t __str_concat_raw(el_val_t a, el_val_t b); /* malloc+strcpy concat */
|
||||
el_val_t __str_slice_raw(el_val_t s, el_val_t start, el_val_t end); /* substring copy */
|
||||
el_val_t __int_to_str(el_val_t n);
|
||||
el_val_t __str_to_int(el_val_t s);
|
||||
el_val_t __float_to_str(el_val_t f); /* f is bit-cast double */
|
||||
el_val_t __str_to_float(el_val_t s); /* strtod, bit-cast result */
|
||||
|
||||
/* ── I/O ─────────────────────────────────────────────────────────────────── */
|
||||
|
||||
void __println(el_val_t s);
|
||||
void __print(el_val_t s);
|
||||
el_val_t __readline(void);
|
||||
|
||||
/* ── Filesystem ──────────────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t __fs_read(el_val_t path);
|
||||
el_val_t __fs_write(el_val_t path, el_val_t content);
|
||||
el_val_t __fs_exists(el_val_t path);
|
||||
el_val_t __fs_list_raw(el_val_t path); /* newline-separated filenames */
|
||||
el_val_t __fs_mkdir(el_val_t path);
|
||||
el_val_t __fs_write_bytes(el_val_t path, el_val_t bytes, el_val_t n);
|
||||
|
||||
/* ── HTTP client ─────────────────────────────────────────────────────────── */
|
||||
|
||||
/* Unified HTTP call. headers_json is a JSON object of header name->value pairs
|
||||
* (e.g. {"Authorization":"Bearer ...","Content-Type":"application/json"}).
|
||||
* Use "" or "{}" for no extra headers. timeout_ms <= 0 uses the default. */
|
||||
el_val_t __http_do(el_val_t method, el_val_t url, el_val_t body,
|
||||
el_val_t headers_json, el_val_t timeout_ms);
|
||||
|
||||
/* Stream response body directly to a file. Returns 1 on success, 0 on failure. */
|
||||
el_val_t __http_do_to_file(el_val_t method, el_val_t url, el_val_t body,
|
||||
el_val_t headers_json, el_val_t out_path);
|
||||
|
||||
/* ── HTTP server ─────────────────────────────────────────────────────────── */
|
||||
|
||||
/* Blocking HTTP server. handler_name is the El function name to dispatch to.
|
||||
* v1 handler: (method, path, body) -> String
|
||||
* v2 handler: (method, path, headers_map, body) -> String or envelope */
|
||||
void __http_serve(el_val_t port, el_val_t handler_name);
|
||||
void __http_serve_v2(el_val_t port, el_val_t handler_name);
|
||||
|
||||
/* Build a structured HTTP response envelope.
|
||||
* headers_json: JSON object literal like {"Content-Type":"text/plain"} or "{}" */
|
||||
el_val_t __http_response(el_val_t status, el_val_t headers_json, el_val_t body);
|
||||
|
||||
/* ── HTTP SSE — Server-Sent Events streaming ─────────────────────────────── */
|
||||
|
||||
/* Returns the raw file descriptor for the current HTTP connection.
|
||||
* Valid only inside an http_serve_v2 handler before it returns.
|
||||
* Returns -1 if called outside a handler context. */
|
||||
el_val_t __http_conn_fd(void);
|
||||
|
||||
/* Sends SSE response headers on conn_id (the fd from __http_conn_fd),
|
||||
* keeping the connection open for streaming. Returns 1 on success, 0 on
|
||||
* write failure. Call once at the start of an SSE handler. */
|
||||
el_val_t __http_sse_open(el_val_t conn_id);
|
||||
|
||||
/* Writes one SSE event frame: "data: <data>\n\n". data must not contain
|
||||
* newlines. Returns 1 on success, 0 if the client disconnected. */
|
||||
el_val_t __http_sse_send(el_val_t conn_id, el_val_t data);
|
||||
|
||||
/* Closes the SSE connection. The handler must return http_sse_sentinel()
|
||||
* so the HTTP worker does not double-close the fd. */
|
||||
el_val_t __http_sse_close(el_val_t conn_id);
|
||||
|
||||
/* ── Threading ───────────────────────────────────────────────────────────── */
|
||||
|
||||
/* Create a thread that calls the named El function with a String argument.
|
||||
* fn_name is resolved via dlsym(RTLD_DEFAULT, fn_name). Returns a thread
|
||||
* handle Int that can be passed to __thread_join. Returns -1 on failure. */
|
||||
el_val_t __thread_create(el_val_t fn_name, el_val_t arg);
|
||||
|
||||
/* Wait for thread tid (returned by __thread_create) to finish.
|
||||
* Returns the thread's return value as a String. */
|
||||
el_val_t __thread_join(el_val_t tid);
|
||||
|
||||
/* Allocate a new mutex. Returns a handle Int (index into internal table). */
|
||||
el_val_t __mutex_new(void);
|
||||
|
||||
void __mutex_lock(el_val_t m);
|
||||
void __mutex_unlock(el_val_t m);
|
||||
|
||||
/* ── Subprocess ──────────────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t __exec(el_val_t cmd); /* popen, capture all stdout, return String */
|
||||
void __exec_bg(el_val_t cmd); /* fire and forget */
|
||||
|
||||
/* ── Environment and process ─────────────────────────────────────────────── */
|
||||
|
||||
el_val_t __env_get(el_val_t key); /* getenv, return "" if not set */
|
||||
void __exit_program(el_val_t code);
|
||||
el_val_t __args_json(void); /* CLI args as JSON array string */
|
||||
|
||||
/* ── Time ────────────────────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t __time_now_ns(void); /* clock_gettime REALTIME, nanoseconds */
|
||||
void __sleep_ms(el_val_t ms);
|
||||
|
||||
/* ── UUID ────────────────────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t __uuid_v4(void);
|
||||
|
||||
/* ── Math ────────────────────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t __sqrt_f(el_val_t f);
|
||||
el_val_t __log_f(el_val_t f);
|
||||
el_val_t __ln_f(el_val_t f);
|
||||
el_val_t __sin_f(el_val_t f);
|
||||
el_val_t __cos_f(el_val_t f);
|
||||
el_val_t __pi_f(void);
|
||||
|
||||
/* ── JSON ────────────────────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t __json_get(el_val_t json, el_val_t key);
|
||||
el_val_t __json_get_raw(el_val_t json_str, el_val_t key);
|
||||
el_val_t __json_parse(el_val_t s);
|
||||
el_val_t __json_stringify(el_val_t v);
|
||||
el_val_t __json_array_len(el_val_t json_str);
|
||||
el_val_t __json_array_get(el_val_t json_str, el_val_t index);
|
||||
el_val_t __json_array_get_string(el_val_t json_str, el_val_t index);
|
||||
el_val_t __json_get_string(el_val_t json_str, el_val_t key);
|
||||
el_val_t __json_get_int(el_val_t json_str, el_val_t key);
|
||||
el_val_t __json_get_float(el_val_t json_str, el_val_t key);
|
||||
el_val_t __json_get_bool(el_val_t json_str, el_val_t key);
|
||||
el_val_t __json_set(el_val_t json_str, el_val_t key, el_val_t value);
|
||||
|
||||
/* ── State K/V ───────────────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t __state_set(el_val_t key, el_val_t value);
|
||||
el_val_t __state_get(el_val_t key);
|
||||
el_val_t __state_del(el_val_t key);
|
||||
el_val_t __state_keys(void);
|
||||
|
||||
/* ── HTML/URL ────────────────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t __html_sanitize(el_val_t input_html, el_val_t allowlist_json);
|
||||
el_val_t __url_encode(el_val_t s);
|
||||
el_val_t __url_decode(el_val_t s);
|
||||
|
||||
/* ── Engram ──────────────────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t __engram_node(el_val_t content, el_val_t node_type, el_val_t salience);
|
||||
el_val_t __engram_node_full(el_val_t content, el_val_t node_type, el_val_t label,
|
||||
el_val_t salience, el_val_t importance, el_val_t confidence,
|
||||
el_val_t tier, el_val_t tags);
|
||||
el_val_t __engram_node_layered(el_val_t content, el_val_t node_type, el_val_t label,
|
||||
el_val_t salience, el_val_t certainty, el_val_t confidence,
|
||||
el_val_t status, el_val_t tags, el_val_t layer_id);
|
||||
el_val_t __engram_add_layer(el_val_t name, el_val_t priority, el_val_t suppressible,
|
||||
el_val_t transparent, el_val_t injectable);
|
||||
el_val_t __engram_remove_layer(el_val_t layer_id);
|
||||
el_val_t __engram_list_layers(void);
|
||||
el_val_t __engram_get_node(el_val_t id);
|
||||
void __engram_strengthen(el_val_t node_id);
|
||||
void __engram_forget(el_val_t node_id);
|
||||
el_val_t __engram_node_count(void);
|
||||
el_val_t __engram_search(el_val_t query, el_val_t limit);
|
||||
el_val_t __engram_scan_nodes(el_val_t limit, el_val_t offset);
|
||||
void __engram_connect(el_val_t from_id, el_val_t to_id, el_val_t weight, el_val_t relation);
|
||||
el_val_t __engram_edge_between(el_val_t from_id, el_val_t to_id);
|
||||
el_val_t __engram_neighbors(el_val_t node_id);
|
||||
el_val_t __engram_neighbors_filtered(el_val_t node_id, el_val_t max_depth, el_val_t direction);
|
||||
el_val_t __engram_edge_count(void);
|
||||
el_val_t __engram_activate(el_val_t query, el_val_t depth);
|
||||
el_val_t __engram_save(el_val_t path);
|
||||
el_val_t __engram_load(el_val_t path);
|
||||
el_val_t __engram_get_node_json(el_val_t id);
|
||||
el_val_t __engram_search_json(el_val_t query, el_val_t limit);
|
||||
el_val_t __engram_scan_nodes_json(el_val_t limit, el_val_t offset);
|
||||
el_val_t __engram_scan_nodes_by_type_json(el_val_t node_type, el_val_t limit, el_val_t offset);
|
||||
el_val_t __engram_neighbors_json(el_val_t node_id, el_val_t max_depth, el_val_t direction);
|
||||
el_val_t __engram_activate_json(el_val_t query, el_val_t depth);
|
||||
el_val_t __engram_stats_json(void);
|
||||
el_val_t __engram_list_layers_json(void);
|
||||
el_val_t __engram_compile_layered_json(el_val_t intent, el_val_t depth);
|
||||
|
||||
/* ── Cryptographic hashing ────────────────────────────────────────────────── */
|
||||
|
||||
/* __sha256_hex — return the SHA-256 hex digest of a string.
|
||||
* The returned string is 64 hex characters (lowercase). */
|
||||
el_val_t __sha256_hex(el_val_t s);
|
||||
|
||||
/* ── args init (called from main) ────────────────────────────────────────── */
|
||||
/* Store argc/argv for __args_json. Call once at the start of main(). */
|
||||
void el_seed_init_args(int argc, char** argv);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,761 @@
|
||||
/*
|
||||
* el_runtime.h — El language C runtime header
|
||||
*
|
||||
* Declares all built-in functions available to compiled El programs.
|
||||
* Include this in every generated .c file.
|
||||
*
|
||||
* Value model:
|
||||
* All El values are represented as el_val_t (= int64_t).
|
||||
* On 64-bit systems a pointer fits in int64_t.
|
||||
* String values are cast: (el_val_t)(uintptr_t)"hello"
|
||||
* Integer values are stored directly.
|
||||
* This lets arithmetic work naturally while still passing strings around.
|
||||
*
|
||||
* Type conventions (El -> C):
|
||||
* String -> el_val_t (holds const char* via uintptr_t cast)
|
||||
* Int -> el_val_t
|
||||
* Bool -> el_val_t (0 = false, nonzero = true)
|
||||
* Any -> el_val_t
|
||||
* Void -> void
|
||||
*
|
||||
* Macros for convenience:
|
||||
* EL_STR(s) cast string literal to el_val_t
|
||||
* EL_CSTR(v) cast el_val_t back to const char*
|
||||
* EL_INT(v) identity — el_val_t is already int64_t
|
||||
*
|
||||
* Link requirements:
|
||||
* -lcurl — required for the HTTP client (http_get, http_post, llm_*).
|
||||
* -lpthread — required for the HTTP server (one detached thread per
|
||||
* connection, capped at 64 concurrent).
|
||||
* -loqs — optional; required only when liboqs is installed and the
|
||||
* pq_* / sha3_256_hex entry points are needed. Detected at
|
||||
* compile time via __has_include(<oqs/oqs.h>).
|
||||
* -lcrypto — optional; pulled in alongside -loqs. Used for X25519 in
|
||||
* pq_hybrid_* and HKDF-SHA256 derivation.
|
||||
*
|
||||
* Canonical compile command:
|
||||
* cc -std=c11 -I el-compiler/runtime -lcurl -lpthread \
|
||||
* -o <out> <prog>.c el-compiler/runtime/el_runtime.c
|
||||
*
|
||||
* With liboqs (post-quantum stack):
|
||||
* cc -std=c11 -I el-compiler/runtime -lcurl -lpthread -loqs -lcrypto \
|
||||
* -o <out> <prog>.c el-compiler/runtime/el_runtime.c
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
typedef int64_t el_val_t;
|
||||
|
||||
#define EL_STR(s) ((el_val_t)(uintptr_t)(s))
|
||||
#define EL_CSTR(v) ((const char*)(uintptr_t)(v))
|
||||
#define EL_INT(v) (v)
|
||||
#define EL_NULL ((el_val_t)0)
|
||||
|
||||
/* Float values share the el_val_t (int64) slot via a bit-cast.
|
||||
* The codegen emits Float literals as `el_from_float(<dbl>)` so the
|
||||
* underlying bits represent the IEEE 754 double. Float-aware builtins
|
||||
* (math, format, json) round-trip via these helpers. */
|
||||
static inline double el_to_float(el_val_t v) {
|
||||
union { int64_t i; double f; } u;
|
||||
u.i = (int64_t)v;
|
||||
return u.f;
|
||||
}
|
||||
|
||||
static inline el_val_t el_from_float(double f) {
|
||||
union { double f; int64_t i; } u;
|
||||
u.f = f;
|
||||
return (el_val_t)u.i;
|
||||
}
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* ── I/O ──────────────────────────────────────────────────────────────────── */
|
||||
|
||||
void println(el_val_t s);
|
||||
void print(el_val_t s);
|
||||
el_val_t readline(void);
|
||||
|
||||
/* ── String builtins ─────────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t el_str_concat(el_val_t a, el_val_t b);
|
||||
el_val_t str_eq(el_val_t a, el_val_t b);
|
||||
el_val_t str_starts_with(el_val_t s, el_val_t prefix);
|
||||
el_val_t str_ends_with(el_val_t s, el_val_t suffix);
|
||||
el_val_t str_len(el_val_t s);
|
||||
el_val_t str_concat(el_val_t a, el_val_t b);
|
||||
el_val_t int_to_str(el_val_t n);
|
||||
el_val_t str_to_int(el_val_t s);
|
||||
el_val_t str_slice(el_val_t s, el_val_t start, el_val_t end);
|
||||
el_val_t str_contains(el_val_t s, el_val_t sub);
|
||||
el_val_t str_replace(el_val_t s, el_val_t from, el_val_t to);
|
||||
el_val_t str_to_upper(el_val_t s);
|
||||
el_val_t str_to_lower(el_val_t s);
|
||||
el_val_t str_trim(el_val_t s);
|
||||
|
||||
/* ── Math ────────────────────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t el_abs(el_val_t n);
|
||||
el_val_t el_max(el_val_t a, el_val_t b);
|
||||
el_val_t el_min(el_val_t a, el_val_t b);
|
||||
|
||||
/* ── Refcount (ARC) ──────────────────────────────────────────────────────────
|
||||
* Lists and Maps carry a refcount. Strings and ints do not — el_retain and
|
||||
* el_release are safe no-ops on non-refcounted values (they sniff a magic
|
||||
* header at offset 0 and only act if the magic matches).
|
||||
*
|
||||
* Codegen emits these at let-binding shadowing, function entry (params), and
|
||||
* function exit (locals other than the returned value). The refcount lets
|
||||
* el_list_append and el_map_set mutate in place when uniquely owned (cheap)
|
||||
* and copy-on-write when shared (preserves persistent semantics across
|
||||
* accumulator patterns in the compiler itself). */
|
||||
|
||||
void el_retain(el_val_t v);
|
||||
void el_release(el_val_t v);
|
||||
|
||||
/* ── List ────────────────────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t el_list_new(el_val_t count, ...);
|
||||
el_val_t el_list_len(el_val_t list);
|
||||
el_val_t el_list_get(el_val_t list, el_val_t index);
|
||||
el_val_t el_list_append(el_val_t list, el_val_t elem);
|
||||
el_val_t el_list_empty(void);
|
||||
el_val_t el_list_clone(el_val_t list);
|
||||
|
||||
/* ── Map ─────────────────────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t el_map_new(el_val_t pair_count, ...);
|
||||
el_val_t el_get_field(el_val_t map, el_val_t key);
|
||||
el_val_t el_map_get(el_val_t map, el_val_t key);
|
||||
el_val_t el_map_set(el_val_t map, el_val_t key, el_val_t value);
|
||||
|
||||
/* ── HTTP ─────────────────────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t http_get(el_val_t url);
|
||||
el_val_t http_post(el_val_t url, el_val_t body);
|
||||
el_val_t http_post_json(el_val_t url, el_val_t json_body);
|
||||
el_val_t http_get_with_headers(el_val_t url, el_val_t headers_map);
|
||||
el_val_t http_post_with_headers(el_val_t url, el_val_t body, el_val_t headers_map);
|
||||
el_val_t http_post_form_auth(el_val_t url, el_val_t form_body, el_val_t auth_header);
|
||||
el_val_t http_delete(el_val_t url);
|
||||
void http_serve(el_val_t port, el_val_t handler);
|
||||
void http_set_handler(el_val_t name);
|
||||
|
||||
/* HTTP server v2 ─────────────────────────────────────────────────────────────
|
||||
* Same dispatch model as http_serve, but the handler signature is widened:
|
||||
*
|
||||
* el_val_t handler(method, path, headers_map, body)
|
||||
*
|
||||
* `headers_map` is an ElMap from lowercased header name → header value (both
|
||||
* Strings). Repeated headers are joined with ", " per RFC 7230.
|
||||
*
|
||||
* Response value: the handler may return either
|
||||
* (a) a plain body string — same auto-content-type / 200-OK behaviour as
|
||||
* http_serve (3-arg) — or
|
||||
* (b) a response envelope built with `http_response(status, headers_json,
|
||||
* body)`. The runtime detects the envelope discriminator
|
||||
* `"el_http_response":1` at the start of the returned string and
|
||||
* unpacks status / headers / body before sending.
|
||||
*
|
||||
* The 3-arg http_serve(port, handler) remains supported unchanged for
|
||||
* existing handlers (e.g. products/web/server.el): it dispatches with
|
||||
* (method, path, body), hardcodes 200 OK, and auto-detects content type. */
|
||||
void http_serve_v2(el_val_t port, el_val_t handler);
|
||||
void http_set_handler_v2(el_val_t name);
|
||||
|
||||
/* Build an HTTP response envelope. `headers_json` should be a JSON object
|
||||
* literal like `{"WWW-Authenticate":"Basic"}` (or "" / "{}" for none). The
|
||||
* returned string carries the discriminator `{"el_http_response":1,...}`
|
||||
* which the runtime's send-path detects and unpacks. Detection happens
|
||||
* uniformly inside http_send_response, so a 3-arg handler may also return
|
||||
* an envelope. The 3-arg variant remains documented as a fixed 200-OK
|
||||
* auto-content-type contract for legacy handlers that return plain bodies. */
|
||||
el_val_t http_response(el_val_t status, el_val_t headers_json, el_val_t body);
|
||||
|
||||
/* SSE connection fd — set by http_worker_v2 before calling the El handler,
|
||||
* cleared afterwards. Defined in el_seed.c; called from el_runtime.c.
|
||||
* The getter is exposed as __http_conn_fd() to El programs. */
|
||||
void el_seed_set_http_conn_fd(int fd);
|
||||
|
||||
/* HTTP timeout — every libcurl request honors EL_HTTP_TIMEOUT_MS (default
|
||||
* 60000ms). Read lazily on first use, so setting the env var any time before
|
||||
* the first http_* call is sufficient. */
|
||||
|
||||
/* Streaming variants — write the response body straight to a file via
|
||||
* libcurl's CURLOPT_WRITEFUNCTION = fwrite. These bypass the el_val_t string
|
||||
* wrapper entirely, so binary payloads (audio/mpeg, image/png, etc.) survive
|
||||
* embedded NUL bytes that would truncate a strlen()-based code path.
|
||||
*
|
||||
* Both honor EL_HTTP_TIMEOUT_MS, follow redirects, and accept the same
|
||||
* `headers_map` shape as http_post_with_headers (ElMap of String→String).
|
||||
*
|
||||
* Return value: 1 on success (file fully written), 0 on any failure
|
||||
* (network, file open, partial write). On failure the output file is removed
|
||||
* so callers cannot mistake a partially-written file for a valid one. */
|
||||
el_val_t http_post_to_file(el_val_t url, el_val_t body, el_val_t headers_map, el_val_t output_path);
|
||||
el_val_t http_get_to_file(el_val_t url, el_val_t headers_map, el_val_t output_path);
|
||||
|
||||
/* ── URL encoding ────────────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t url_encode(el_val_t s); /* RFC 3986 unreserved set */
|
||||
el_val_t url_decode(el_val_t s); /* '+' → space, %XX → byte */
|
||||
|
||||
/* ── HTML allowlist sanitizer ────────────────────────────────────────────────
|
||||
* el_html_sanitize(input_html, allowlist_json) — strict allowlist HTML
|
||||
* cleaner. State-machine parser; tag/attribute names compared case-
|
||||
* insensitively against the allowlist; `<a href>` / `<… src>` URL schemes
|
||||
* validated (http, https, mailto, fragment-only, or relative); whole-
|
||||
* subtree drop for script / style / iframe / object / embed / form; HTML-
|
||||
* escapes free text outside dropped subtrees.
|
||||
*
|
||||
* The allowlist is JSON of the form
|
||||
* {"p":[],"a":["href","title"],"strong":[],...}
|
||||
* where each value is the array of attribute names allowed for that tag. */
|
||||
el_val_t el_html_sanitize(el_val_t input_html, el_val_t allowlist_json);
|
||||
|
||||
/* ── Filesystem ──────────────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t fs_read(el_val_t path);
|
||||
el_val_t fs_write(el_val_t path, el_val_t content);
|
||||
el_val_t fs_list(el_val_t path);
|
||||
el_val_t fs_exists(el_val_t path);
|
||||
el_val_t fs_mkdir(el_val_t path); /* mkdir -p, mode 0755 */
|
||||
|
||||
/* Length-explicit binary write. `length` is an Int (el_val_t holding the
|
||||
* byte count). The caller knows the length from context — typically because
|
||||
* `bytes` came from base64_decode (which produces a magic-tagged binary
|
||||
* buffer with embedded NULs possible) and the caller already tracks the
|
||||
* decoded length, OR because the bytes came from a fixed-size source
|
||||
* (sha256_bytes = 32, hmac_sha256_bytes = 32). Bypasses strlen entirely.
|
||||
*
|
||||
* Returns 1 on success, 0 on failure (invalid path, can't open, partial
|
||||
* write, negative length). On partial-write failure, the file is removed
|
||||
* so callers cannot read back a truncated artefact. */
|
||||
el_val_t fs_write_bytes(el_val_t path, el_val_t bytes, el_val_t length);
|
||||
|
||||
/* ── JSON ────────────────────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t json_get(el_val_t json, el_val_t key);
|
||||
el_val_t json_parse(el_val_t s);
|
||||
el_val_t json_stringify(el_val_t v);
|
||||
el_val_t json_get_string(el_val_t json_str, el_val_t key);
|
||||
el_val_t json_get_int(el_val_t json_str, el_val_t key);
|
||||
el_val_t json_get_float(el_val_t json_str, el_val_t key);
|
||||
el_val_t json_get_bool(el_val_t json_str, el_val_t key);
|
||||
el_val_t json_get_raw(el_val_t json_str, el_val_t key);
|
||||
el_val_t json_set(el_val_t json_str, el_val_t key, el_val_t value);
|
||||
el_val_t json_array_len(el_val_t json_str);
|
||||
el_val_t json_array_get(el_val_t json_str, el_val_t index);
|
||||
el_val_t json_array_get_string(el_val_t json_str, el_val_t index);
|
||||
|
||||
/* ── Time ────────────────────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t time_now(void);
|
||||
el_val_t time_now_utc(void);
|
||||
el_val_t sleep_secs(el_val_t secs);
|
||||
el_val_t sleep_ms(el_val_t ms);
|
||||
el_val_t time_format(el_val_t ts, el_val_t fmt);
|
||||
el_val_t time_to_parts(el_val_t ts);
|
||||
el_val_t time_from_parts(el_val_t secs, el_val_t ns, el_val_t tz);
|
||||
el_val_t time_add(el_val_t ts, el_val_t n, el_val_t unit);
|
||||
el_val_t time_diff(el_val_t ts1, el_val_t ts2, el_val_t unit);
|
||||
|
||||
/* ── Instant + Duration: first-class temporal types ──────────────────────────
|
||||
* Both types share the el_val_t (int64) slot. Instants are nanoseconds
|
||||
* since the Unix epoch; Durations are signed nanoseconds. Type discipline
|
||||
* is enforced at codegen-time: BinOps on names registered as Instant or
|
||||
* Duration route through the typed wrappers below; mismatches like
|
||||
* Instant+Instant become #error at the C compiler.
|
||||
*
|
||||
* Postfix literals — `30.seconds`, `1.hour`, `500.millis`, `30.nanos` — are
|
||||
* recognised by the parser as DurationLit AST nodes and lowered to literal
|
||||
* int64 nanoseconds at codegen time. The runtime never sees the units. */
|
||||
|
||||
el_val_t el_now_instant(void);
|
||||
el_val_t now(void);
|
||||
el_val_t unix_seconds(el_val_t n);
|
||||
el_val_t unix_millis(el_val_t n);
|
||||
el_val_t instant_from_iso8601(el_val_t s);
|
||||
|
||||
el_val_t el_duration_from_nanos(el_val_t ns);
|
||||
el_val_t duration_seconds(el_val_t n);
|
||||
el_val_t duration_millis(el_val_t n);
|
||||
el_val_t duration_nanos(el_val_t n);
|
||||
|
||||
el_val_t el_instant_add_dur(el_val_t inst, el_val_t dur);
|
||||
el_val_t el_instant_sub_dur(el_val_t inst, el_val_t dur);
|
||||
el_val_t el_instant_diff(el_val_t a, el_val_t b);
|
||||
el_val_t el_duration_add(el_val_t a, el_val_t b);
|
||||
el_val_t el_duration_sub(el_val_t a, el_val_t b);
|
||||
el_val_t el_duration_scale(el_val_t dur, el_val_t scalar);
|
||||
el_val_t el_duration_div(el_val_t dur, el_val_t scalar);
|
||||
|
||||
el_val_t el_instant_lt(el_val_t a, el_val_t b);
|
||||
el_val_t el_instant_le(el_val_t a, el_val_t b);
|
||||
el_val_t el_instant_gt(el_val_t a, el_val_t b);
|
||||
el_val_t el_instant_ge(el_val_t a, el_val_t b);
|
||||
el_val_t el_instant_eq(el_val_t a, el_val_t b);
|
||||
el_val_t el_instant_ne(el_val_t a, el_val_t b);
|
||||
el_val_t el_duration_lt(el_val_t a, el_val_t b);
|
||||
el_val_t el_duration_le(el_val_t a, el_val_t b);
|
||||
el_val_t el_duration_gt(el_val_t a, el_val_t b);
|
||||
el_val_t el_duration_ge(el_val_t a, el_val_t b);
|
||||
el_val_t el_duration_eq(el_val_t a, el_val_t b);
|
||||
el_val_t el_duration_ne(el_val_t a, el_val_t b);
|
||||
|
||||
el_val_t instant_to_unix_seconds(el_val_t i);
|
||||
el_val_t instant_to_unix_millis(el_val_t i);
|
||||
el_val_t instant_to_iso8601(el_val_t i);
|
||||
el_val_t duration_to_seconds(el_val_t d);
|
||||
el_val_t duration_to_millis(el_val_t d);
|
||||
el_val_t duration_to_nanos(el_val_t d);
|
||||
|
||||
el_val_t el_sleep_duration(el_val_t dur);
|
||||
el_val_t unix_timestamp(void);
|
||||
|
||||
el_val_t ttl_cache_set(el_val_t key, el_val_t value);
|
||||
el_val_t ttl_cache_get(el_val_t key, el_val_t max_age);
|
||||
el_val_t ttl_cache_age(el_val_t key);
|
||||
|
||||
/* ── Calendar + CalendarTime + Rhythm + LocalDate/Time/DateTime ─────────────
|
||||
* Phase 1.5 of the time system. Calendar is pluggable: EarthCalendar (IANA
|
||||
* zones, Gregorian, DST) is the user-facing default; MarsCalendar,
|
||||
* CycleCalendar(period), NoCycleCalendar, RelativeCalendar handle non-Earth
|
||||
* domains.
|
||||
*
|
||||
* A Calendar interprets an Instant under a particular cycle convention and
|
||||
* produces a CalendarTime. CalendarTime carries the underlying Instant and
|
||||
* a back-pointer to its Calendar; arithmetic and formatting consult the
|
||||
* Calendar to convert ns since epoch into year/month/day/hour/minute/second
|
||||
* (or sol/phase, or cycle/phase, depending on kind).
|
||||
*
|
||||
* Storage convention: Calendar / CalendarTime / Rhythm / LocalDate /
|
||||
* LocalDateTime are heap-allocated structs whose pointers are cast into
|
||||
* el_val_t. A 24-bit magic header at offset 0 lets the runtime identify
|
||||
* the kind safely. LocalTime is small enough to live in the int64 slot
|
||||
* directly (nanos since midnight, signed). */
|
||||
|
||||
/* Zone — opaque IANA zone or fixed offset, used by EarthCalendar.
|
||||
* `zone_id` is either an IANA name ("America/New_York", "UTC") or a fixed
|
||||
* offset string ("+05:30", "-08:00"). The runtime resolves it via tzset()
|
||||
* on first use of the owning EarthCalendar. */
|
||||
el_val_t zone(el_val_t id);
|
||||
el_val_t zone_utc(void);
|
||||
el_val_t zone_local(void);
|
||||
el_val_t zone_offset(el_val_t hours, el_val_t minutes);
|
||||
|
||||
/* Calendar constructors. Each returns an el_val_t pointer to a heap-
|
||||
* allocated, magic-tagged Calendar struct. Calendars are interned by
|
||||
* (kind, zone_id, period_ns, epoch_ns) so identical constructors return
|
||||
* the same pointer — equality is reference equality. */
|
||||
el_val_t earth_calendar(el_val_t z);
|
||||
el_val_t earth_calendar_default(void);
|
||||
el_val_t mars_calendar(void);
|
||||
el_val_t cycle_calendar(el_val_t period_dur);
|
||||
el_val_t no_cycle_calendar(void);
|
||||
el_val_t relative_calendar(el_val_t epoch_inst);
|
||||
|
||||
/* CalendarTime constructors and methods. Returns a heap-allocated struct
|
||||
* whose pointer fits in el_val_t. */
|
||||
el_val_t now_in(el_val_t cal);
|
||||
el_val_t in_calendar(el_val_t inst, el_val_t cal);
|
||||
el_val_t cal_format(el_val_t ct, el_val_t pattern);
|
||||
el_val_t cal_to_instant(el_val_t ct);
|
||||
el_val_t cal_cycle_phase(el_val_t ct);
|
||||
el_val_t cal_in(el_val_t ct, el_val_t cal);
|
||||
|
||||
/* LocalDate / LocalTime / LocalDateTime — calendar-agnostic value types.
|
||||
* LocalTime carries nanoseconds since midnight as a signed int64 directly
|
||||
* in the el_val_t slot (no allocation). LocalDate / LocalDateTime are
|
||||
* heap-allocated structs with magic headers. */
|
||||
el_val_t local_date(el_val_t y, el_val_t m, el_val_t d);
|
||||
el_val_t local_time(el_val_t h, el_val_t m, el_val_t s, el_val_t ns);
|
||||
el_val_t local_datetime(el_val_t date, el_val_t time);
|
||||
el_val_t zoned(el_val_t date, el_val_t time, el_val_t cal);
|
||||
|
||||
el_val_t local_date_year(el_val_t ld);
|
||||
el_val_t local_date_month(el_val_t ld);
|
||||
el_val_t local_date_day(el_val_t ld);
|
||||
el_val_t local_time_hour(el_val_t lt);
|
||||
el_val_t local_time_minute(el_val_t lt);
|
||||
el_val_t local_time_second(el_val_t lt);
|
||||
el_val_t local_time_nanos(el_val_t lt);
|
||||
|
||||
el_val_t el_local_date_add_dur(el_val_t ld, el_val_t dur);
|
||||
el_val_t el_local_time_add_dur(el_val_t lt, el_val_t dur);
|
||||
el_val_t el_local_date_lt(el_val_t a, el_val_t b);
|
||||
el_val_t el_local_date_eq(el_val_t a, el_val_t b);
|
||||
|
||||
/* Rhythm — pluggable recurrence AST. Returns a heap-allocated struct
|
||||
* pointer in el_val_t; rhythms are immutable so callers may share them. */
|
||||
el_val_t rhythm_cycle_start(void);
|
||||
el_val_t rhythm_cycle_phase(el_val_t phase);
|
||||
el_val_t rhythm_duration(el_val_t d);
|
||||
el_val_t rhythm_session_start(void);
|
||||
el_val_t rhythm_event(el_val_t name);
|
||||
el_val_t rhythm_and(el_val_t a, el_val_t b);
|
||||
el_val_t rhythm_or(el_val_t a, el_val_t b);
|
||||
el_val_t rhythm_weekday(el_val_t day);
|
||||
el_val_t rhythm_weekly_at(el_val_t day, el_val_t hour, el_val_t minute);
|
||||
el_val_t rhythm_next_after(el_val_t r, el_val_t after, el_val_t cal);
|
||||
el_val_t rhythm_matches(el_val_t r, el_val_t ct);
|
||||
|
||||
/* ── UUID ────────────────────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t uuid_new(void);
|
||||
el_val_t uuid_v4(void);
|
||||
|
||||
/* ── Environment ─────────────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t env(el_val_t key);
|
||||
|
||||
/* ── In-process state K/V ────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t state_set(el_val_t key, el_val_t value);
|
||||
el_val_t state_get(el_val_t key);
|
||||
el_val_t state_del(el_val_t key);
|
||||
el_val_t state_keys(void);
|
||||
|
||||
/* ── Float formatting ────────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t float_to_str(el_val_t f);
|
||||
el_val_t int_to_float(el_val_t n);
|
||||
el_val_t float_to_int(el_val_t f);
|
||||
el_val_t format_float(el_val_t f, el_val_t decimals);
|
||||
el_val_t decimal_round(el_val_t f, el_val_t decimals);
|
||||
el_val_t str_to_float(el_val_t s);
|
||||
|
||||
/* ── Math (Float-aware) ──────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t math_sqrt(el_val_t f);
|
||||
el_val_t math_log(el_val_t f);
|
||||
el_val_t math_ln(el_val_t f);
|
||||
el_val_t math_sin(el_val_t f);
|
||||
el_val_t math_cos(el_val_t f);
|
||||
el_val_t math_pi(void);
|
||||
|
||||
/* ── String additions ────────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t str_index_of(el_val_t s, el_val_t sub);
|
||||
el_val_t str_split(el_val_t s, el_val_t sep);
|
||||
el_val_t str_char_at(el_val_t s, el_val_t i);
|
||||
el_val_t str_char_code(el_val_t s, el_val_t i);
|
||||
el_val_t str_pad_left(el_val_t s, el_val_t width, el_val_t pad);
|
||||
el_val_t str_pad_right(el_val_t s, el_val_t width, el_val_t pad);
|
||||
el_val_t str_format(el_val_t fmt, el_val_t data);
|
||||
el_val_t str_lower(el_val_t s);
|
||||
el_val_t str_upper(el_val_t s);
|
||||
|
||||
/* ── Text-processing primitives (Phase 1: byte/codepoint, ASCII char classes)
|
||||
* Phase 2 (filed): Unicode-grapheme awareness, NFC/NFD normalization, regex.
|
||||
* is_* predicates: empty input returns false; multi-char requires ALL bytes
|
||||
* to match. ASCII ranges only in Phase 1. */
|
||||
|
||||
/* Counting */
|
||||
el_val_t str_count(el_val_t s, el_val_t sub); /* non-overlapping */
|
||||
el_val_t str_count_chars(el_val_t s); /* codepoint count */
|
||||
el_val_t str_count_bytes(el_val_t s); /* alias of str_len */
|
||||
el_val_t str_count_lines(el_val_t s);
|
||||
el_val_t str_count_words(el_val_t s);
|
||||
el_val_t str_count_letters(el_val_t s); /* ASCII [A-Za-z] */
|
||||
el_val_t str_count_digits(el_val_t s); /* ASCII [0-9] */
|
||||
|
||||
/* Find / position */
|
||||
el_val_t str_index_of_all(el_val_t s, el_val_t sub); /* [Int] of byte offsets */
|
||||
el_val_t str_last_index_of(el_val_t s, el_val_t sub);
|
||||
el_val_t str_find_chars(el_val_t s, el_val_t any_of); /* first idx of any ch */
|
||||
|
||||
/* Transform */
|
||||
el_val_t str_repeat(el_val_t s, el_val_t n);
|
||||
el_val_t str_reverse(el_val_t s); /* by codepoint */
|
||||
el_val_t str_strip_prefix(el_val_t s, el_val_t prefix);
|
||||
el_val_t str_strip_suffix(el_val_t s, el_val_t suffix);
|
||||
el_val_t str_strip_chars(el_val_t s, el_val_t chars);
|
||||
el_val_t str_lstrip(el_val_t s);
|
||||
el_val_t str_rstrip(el_val_t s);
|
||||
|
||||
/* Char classification (Bool) */
|
||||
el_val_t is_letter(el_val_t s);
|
||||
el_val_t is_digit(el_val_t s);
|
||||
el_val_t is_alphanumeric(el_val_t s);
|
||||
el_val_t is_whitespace(el_val_t s);
|
||||
el_val_t is_punctuation(el_val_t s);
|
||||
el_val_t is_uppercase(el_val_t s);
|
||||
el_val_t is_lowercase(el_val_t s);
|
||||
|
||||
/* Split / join */
|
||||
el_val_t str_split_lines(el_val_t s);
|
||||
el_val_t str_split_chars(el_val_t s); /* alias of native_string_chars */
|
||||
el_val_t str_split_n(el_val_t s, el_val_t sep, el_val_t n);
|
||||
el_val_t str_join(el_val_t list, el_val_t sep); /* alias of list_join */
|
||||
|
||||
/* ── List additions ──────────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t list_push(el_val_t list, el_val_t elem);
|
||||
el_val_t list_push_front(el_val_t list, el_val_t elem);
|
||||
el_val_t list_join(el_val_t list, el_val_t sep);
|
||||
el_val_t list_range(el_val_t start, el_val_t end);
|
||||
|
||||
/* ── Bool helpers ────────────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t bool_to_str(el_val_t b);
|
||||
|
||||
/* ── Numeric parsing ─────────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t parse_int(el_val_t s, el_val_t default_val);
|
||||
|
||||
/* ── Process ─────────────────────────────────────────────────────────────── */
|
||||
|
||||
void exit_program(el_val_t code);
|
||||
el_val_t getpid_now(void);
|
||||
|
||||
/* ── CGI identity ─────────────────────────────────────────────────────────────
|
||||
* Called at the start of main() in CGI programs (those with a `cgi {}` block).
|
||||
* Records the program's DHARMA identity before any other code executes. */
|
||||
|
||||
void el_cgi_init(el_val_t name, el_val_t dharma_id, el_val_t principal,
|
||||
el_val_t network, el_val_t engram);
|
||||
|
||||
/* ── DHARMA network builtins ─────────────────────────────────────────────────
|
||||
* Available to CGI programs (declared with a `cgi {}` block).
|
||||
*
|
||||
* Peers are addressed by `dharma_id` of the form
|
||||
* "<registry-id>@<transport-url>" e.g. "ntn-genesis@http://localhost:7770"
|
||||
* If the @<url> portion is omitted, transport defaults to
|
||||
* "http://localhost:7770" (the local CGI daemon assumption).
|
||||
*
|
||||
* Wire protocol (all peers expose):
|
||||
* POST <url>/dharma/recv { channel, from, content } → response body
|
||||
* POST <url>/dharma/event { type, payload, source, timestamp }
|
||||
* POST <url>/api/activate { query } → list of nodes
|
||||
*
|
||||
* Hosting application's responsibility: an El program with a `cgi {}` block
|
||||
* runs http_serve() with its own request handler; that handler should route
|
||||
* "/dharma/event" requests by calling el_runtime_dharma_event_arrive() so
|
||||
* incoming events feed dharma_field() queues. The runtime itself does not
|
||||
* intercept any /dharma path. */
|
||||
|
||||
el_val_t dharma_connect(el_val_t cgi_id);
|
||||
el_val_t dharma_send(el_val_t channel, el_val_t content);
|
||||
el_val_t dharma_activate(el_val_t query);
|
||||
void dharma_emit(el_val_t event_type, el_val_t payload);
|
||||
el_val_t dharma_field(el_val_t event_type);
|
||||
void dharma_strengthen(el_val_t cgi_id, el_val_t weight);
|
||||
el_val_t dharma_relationship(el_val_t cgi_id);
|
||||
el_val_t dharma_peers(void);
|
||||
|
||||
/* Public C API: called by an El program's HTTP handler when a /dharma/event
|
||||
* request arrives. Pushes onto the per-event-type queue and signals any
|
||||
* pending dharma_field() blockers. All three arguments must be NUL-terminated
|
||||
* C strings (or NULL — then treated as empty). */
|
||||
void el_runtime_dharma_event_arrive(const char* event_type,
|
||||
const char* payload,
|
||||
const char* source);
|
||||
|
||||
/* ── Engram local graph primitives ───────────────────────────────────────────
|
||||
* Operate on the CGI's local Engram knowledge graph.
|
||||
* `engram_activate` queries the local graph only; `dharma_activate` is
|
||||
* network-wide across all connected CGI graphs. */
|
||||
|
||||
el_val_t engram_node(el_val_t content, el_val_t node_type, el_val_t salience);
|
||||
el_val_t engram_node_full(el_val_t content, el_val_t node_type, el_val_t label,
|
||||
el_val_t salience, el_val_t importance, el_val_t confidence,
|
||||
el_val_t tier, el_val_t tags);
|
||||
/* Layered consciousness — see el_runtime.c for the layered architecture
|
||||
* design notes (search "Layered consciousness architecture"). The five
|
||||
* canonical layers (safety / core-identity / domain-knowledge / imprint /
|
||||
* suit) are seeded automatically; engram_add_layer extends the registry
|
||||
* with imprint or suit overlays at runtime. Nodes default to layer 1
|
||||
* (core-identity) when created via engram_node / engram_node_full. */
|
||||
el_val_t engram_node_layered(el_val_t content, el_val_t node_type, el_val_t label,
|
||||
el_val_t salience, el_val_t certainty, el_val_t confidence,
|
||||
el_val_t status, el_val_t tags, el_val_t layer_id);
|
||||
el_val_t engram_add_layer(el_val_t name, el_val_t priority, el_val_t suppressible,
|
||||
el_val_t transparent, el_val_t injectable);
|
||||
el_val_t engram_remove_layer(el_val_t layer_id);
|
||||
el_val_t engram_list_layers(void);
|
||||
el_val_t engram_get_node(el_val_t id);
|
||||
void engram_strengthen(el_val_t node_id);
|
||||
void engram_forget(el_val_t node_id);
|
||||
el_val_t engram_node_count(void);
|
||||
el_val_t engram_search(el_val_t query, el_val_t limit);
|
||||
el_val_t engram_scan_nodes(el_val_t limit, el_val_t offset);
|
||||
void engram_connect(el_val_t from_id, el_val_t to_id, el_val_t weight, el_val_t relation);
|
||||
el_val_t engram_edge_between(el_val_t from_id, el_val_t to_id);
|
||||
el_val_t engram_neighbors(el_val_t node_id);
|
||||
el_val_t engram_neighbors_filtered(el_val_t node_id, el_val_t max_depth, el_val_t direction);
|
||||
el_val_t engram_edge_count(void);
|
||||
/* Three-pass activation: background fan-out → working-memory promotion →
|
||||
* Layer 0 override. See "Three-pass activation" in el_runtime.c. */
|
||||
el_val_t engram_activate(el_val_t query, el_val_t depth);
|
||||
el_val_t engram_save(el_val_t path);
|
||||
el_val_t engram_load(el_val_t path);
|
||||
|
||||
/* JSON-string accessors — return pre-serialized JSON so HTTP handlers
|
||||
* can pass results straight through without round-tripping ElList/ElMap
|
||||
* through json_stringify. */
|
||||
el_val_t engram_get_node_json(el_val_t id);
|
||||
el_val_t engram_search_json(el_val_t query, el_val_t limit);
|
||||
el_val_t engram_scan_nodes_json(el_val_t limit, el_val_t offset);
|
||||
el_val_t engram_scan_nodes_by_type_json(el_val_t node_type, el_val_t limit, el_val_t offset);
|
||||
el_val_t engram_neighbors_json(el_val_t node_id, el_val_t max_depth, el_val_t direction);
|
||||
el_val_t engram_activate_json(el_val_t query, el_val_t depth);
|
||||
el_val_t engram_stats_json(void);
|
||||
el_val_t engram_list_layers_json(void);
|
||||
/* engram_compile_layered_json — produce a prompt-ready text block split
|
||||
* into "[LAYER 0 — STRUCTURAL]" (non-suppressible layers, sacred fire)
|
||||
* and "[ENGRAM CONTEXT]" (standard suppressible layers). Returns "" if
|
||||
* no nodes promoted to working memory. */
|
||||
el_val_t engram_compile_layered_json(el_val_t intent, el_val_t depth);
|
||||
|
||||
/* ── LLM (Anthropic API client) ─────────────────────────────────────────────
|
||||
* All functions call https://api.anthropic.com/v1/messages with the API key
|
||||
* from env ANTHROPIC_API_KEY. Default model when empty: claude-sonnet-4-5. */
|
||||
|
||||
el_val_t llm_call(el_val_t model, el_val_t prompt);
|
||||
el_val_t llm_call_system(el_val_t model, el_val_t system_prompt, el_val_t user_prompt);
|
||||
el_val_t llm_call_agentic(el_val_t model, el_val_t system, el_val_t user, el_val_t tools);
|
||||
el_val_t llm_vision(el_val_t model, el_val_t system, el_val_t prompt, el_val_t image_url_or_b64);
|
||||
el_val_t llm_models(void);
|
||||
|
||||
/* Register a tool handler by name. The handler is looked up via dlsym
|
||||
* (mirroring http_set_handler), so any El `fn <name>(input)` compiles to
|
||||
* a global C symbol that this function can locate at runtime.
|
||||
* Handler signature: `el_val_t handler(el_val_t input_json)` — receives
|
||||
* the tool input as a JSON-string el_val_t and returns a JSON-string
|
||||
* el_val_t result. Used by llm_call_agentic. */
|
||||
void llm_register_tool(el_val_t name, el_val_t handler_fn_name);
|
||||
|
||||
/* ── args() ─────────────────────────────────────────────────────────────────
|
||||
* Provides access to command-line arguments passed to the program.
|
||||
* Populated by el_runtime_init_args() before main() runs. */
|
||||
|
||||
el_val_t args(void);
|
||||
void el_runtime_init_args(int argc, char** argv);
|
||||
|
||||
/* ── Crypto primitives ─────────────────────────────────────────────────────
|
||||
* SHA-256, HMAC-SHA-256, and base64 (standard + URL-safe).
|
||||
* Self-contained — no OpenSSL/libcrypto dependency. The implementations are
|
||||
* adapted from public-domain reference code (Brad Conte / RFC 4648).
|
||||
*
|
||||
* Bytes-returning variants (sha256_bytes, hmac_sha256_bytes) return a string
|
||||
* value whose contents are raw binary; callers usually feed these into
|
||||
* base64_encode. Note that el_val_t strings are NUL-terminated by convention,
|
||||
* so the binary payload may contain embedded NULs — pass it directly into
|
||||
* base64_encode (which uses an explicit length) rather than treating it as
|
||||
* a printable C string.
|
||||
*
|
||||
* The "base64" variants emit/accept RFC 4648 standard alphabet with padding.
|
||||
* The "base64url" variants use URL-safe alphabet (`-`/`_`) with no padding,
|
||||
* as used in JWTs. */
|
||||
|
||||
el_val_t sha256_hex(el_val_t input);
|
||||
el_val_t sha256_bytes(el_val_t input);
|
||||
el_val_t hmac_sha256_hex(el_val_t key, el_val_t message);
|
||||
el_val_t hmac_sha256_bytes(el_val_t key, el_val_t message);
|
||||
el_val_t base64_encode(el_val_t input);
|
||||
el_val_t base64_decode(el_val_t input);
|
||||
el_val_t base64url_encode(el_val_t input);
|
||||
el_val_t base64url_decode(el_val_t input);
|
||||
|
||||
/* Length-aware variants (internal — exposed for the rare caller that already
|
||||
* has a known-length binary buffer and doesn't want to round-trip through
|
||||
* a NUL-terminated el_val_t string). Sha256_bytes and hmac_sha256_bytes feed
|
||||
* these implicitly. */
|
||||
el_val_t el_sha256_bytes_n(const unsigned char* data, size_t len);
|
||||
el_val_t el_base64_encode_n(const unsigned char* data, size_t len, int url_safe);
|
||||
|
||||
/* ── Post-quantum primitives (liboqs-backed) ────────────────────────────────
|
||||
* All inputs/outputs hex-encoded. Algorithm choices:
|
||||
* Signature: CRYSTALS-Dilithium-3 (NIST level 3, balanced)
|
||||
* KEM: CRYSTALS-Kyber-768 (NIST level 3)
|
||||
* Hash: SHA3-256 (Keccak) (PQ-aware protocols favour SHA3 over SHA2)
|
||||
*
|
||||
* If liboqs is not linked (detected via __has_include(<oqs/oqs.h>) at compile
|
||||
* time), the pq_* entry points return a JSON-shaped error string so callers
|
||||
* fail loudly rather than silently fall back to classical schemes:
|
||||
* {"error":"liboqs not linked, post-quantum primitives unavailable"}
|
||||
*
|
||||
* The hybrid handshake pairs X25519 with Kyber-768 per NIST PQ guidance and
|
||||
* CNSA 2.0. Combined shared secret is HKDF-SHA256(x25519_ss || kyber_ss).
|
||||
* Even if Kyber falls, X25519 holds; if X25519 falls under quantum attack,
|
||||
* Kyber holds. SHA3-256 also remains usable independent of liboqs (the
|
||||
* Keccak permutation is PQ-OK as a primitive). */
|
||||
|
||||
el_val_t pq_keygen_signature(void);
|
||||
el_val_t pq_sign(el_val_t secret_key_hex, el_val_t message);
|
||||
el_val_t pq_verify(el_val_t public_key_hex, el_val_t message, el_val_t signature_hex);
|
||||
|
||||
el_val_t pq_kem_keygen(void);
|
||||
el_val_t pq_kem_encaps(el_val_t public_key_hex);
|
||||
el_val_t pq_kem_decaps(el_val_t secret_key_hex, el_val_t ciphertext_hex);
|
||||
|
||||
el_val_t pq_hybrid_keygen(void);
|
||||
el_val_t pq_hybrid_handshake(el_val_t remote_pub_combined);
|
||||
|
||||
el_val_t sha3_256_hex(el_val_t input);
|
||||
|
||||
/* ── AEAD: AES-256-GCM (libcrypto-backed) ───────────────────────────────────
|
||||
* Symmetric authenticated encryption used to wrap envelopes after a KEM
|
||||
* handshake. Caller MUST supply a 32-byte key (64 hex chars) — typically the
|
||||
* Kyber-768 / hybrid shared_secret, optionally normalized via SHA3-256.
|
||||
*
|
||||
* aead_encrypt returns a JSON map {"nonce":"...","ciphertext":"..."} where
|
||||
* ciphertext is the AES-256-GCM output with the 16-byte auth tag appended.
|
||||
* Nonce is a fresh 12-byte CSPRNG draw — callers never pick the nonce, which
|
||||
* structurally rules out the GCM nonce-reuse footgun.
|
||||
*
|
||||
* aead_decrypt returns the plaintext String, or "" on any failure (including
|
||||
* auth-tag mismatch). Callers MUST check for "" before trusting the result. */
|
||||
el_val_t aead_encrypt(el_val_t key_hex, el_val_t plaintext);
|
||||
el_val_t aead_decrypt(el_val_t key_hex, el_val_t nonce_hex, el_val_t ciphertext_hex);
|
||||
|
||||
/* ── Native VM builtin aliases (for compiled El source) ─────────────────────
|
||||
* These match the El VM's native_* builtins so that El source compiled
|
||||
* to C can call the same names without modification. */
|
||||
|
||||
el_val_t native_list_get(el_val_t list, el_val_t index);
|
||||
el_val_t native_list_len(el_val_t list);
|
||||
el_val_t native_list_append(el_val_t list, el_val_t elem);
|
||||
el_val_t native_list_empty(void);
|
||||
el_val_t native_list_clone(el_val_t list);
|
||||
el_val_t native_string_chars(el_val_t s);
|
||||
el_val_t native_int_to_str(el_val_t n);
|
||||
|
||||
/* ── Method-call shorthand aliases ──────────────────────────────────────────
|
||||
* The El method-call convention `obj.method(args)` compiles to
|
||||
* `method(obj, args)`. These aliases expose the runtime functions under
|
||||
* the short names that result from method calls in El source.
|
||||
*
|
||||
* Example: `myList.append(x)` → `append(myList, x)` (calls this alias)
|
||||
* `myList.len()` → `len(myList)` (calls this alias) */
|
||||
|
||||
el_val_t append(el_val_t list, el_val_t elem); /* el_list_append */
|
||||
el_val_t len(el_val_t list); /* el_list_len */
|
||||
el_val_t get(el_val_t list, el_val_t index); /* el_list_get */
|
||||
el_val_t map_get(el_val_t map, el_val_t key); /* el_map_get */
|
||||
el_val_t map_set(el_val_t map, el_val_t key, el_val_t value); /* el_map_set */
|
||||
|
||||
/* ── OTLP/HTTP Observability ─────────────────────────────────────────────── */
|
||||
/* See bottom of el_runtime.c for the implementation.
|
||||
* Configured by env vars OTLP_ENDPOINT, OTEL_SERVICE_NAME, OTEL_SERVICE_VERSION.
|
||||
* No-op when OTLP_ENDPOINT is unset. Drop-on-failure semantics. */
|
||||
/* ── Subprocess execution ────────────────────────────────────────────────── */
|
||||
el_val_t exec_command(el_val_t cmd); /* run shell command, return exit code */
|
||||
el_val_t exec_capture(el_val_t cmd); /* run shell command, capture stdout */
|
||||
el_val_t exec(el_val_t cmd); /* exec(cmd) → stdout String (30s timeout) */
|
||||
el_val_t exec_bg(el_val_t cmd); /* exec_bg(cmd) → PID String (non-blocking) */
|
||||
|
||||
el_val_t emit_log(el_val_t level, el_val_t msg, el_val_t fields_json);
|
||||
el_val_t emit_metric(el_val_t name, el_val_t value, el_val_t tags_json);
|
||||
el_val_t trace_span_start(el_val_t name);
|
||||
el_val_t trace_span_end(el_val_t span_handle);
|
||||
el_val_t emit_event(el_val_t name, el_val_t duration_ms);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,542 @@
|
||||
// compiler.el — el self-hosting compiler pipeline
|
||||
//
|
||||
// Wires lexer -> parser -> codegen into a single compile() function.
|
||||
// This is the bootstrap entry point: compiled once by the Rust el-compiler,
|
||||
// then self-hosted from that point forward.
|
||||
//
|
||||
// Two backends:
|
||||
// - C (default) compile() -> emits C source linked against el_runtime.c
|
||||
// - JS (--target=js) compile_js() -> emits JS source linked against el_runtime.js
|
||||
//
|
||||
// Compile the C output with:
|
||||
// cc -o <prog> <prog>.c el_runtime.c
|
||||
//
|
||||
// Run the JS output with:
|
||||
// node <prog>.js (after copying el_runtime.js next to it)
|
||||
|
||||
import "lexer.el"
|
||||
import "parser.el"
|
||||
import "codegen.el"
|
||||
import "codegen-js.el"
|
||||
|
||||
// compile — full pipeline (C target): source string -> C source string
|
||||
fn compile(source: String) -> String {
|
||||
let tokens: [Map<String, Any>] = lex(source)
|
||||
let stmts: [Map<String, Any>] = parse(tokens)
|
||||
// Token list is no longer needed after parsing — release it to free memory
|
||||
// before codegen allocates its own working data on large source files.
|
||||
el_release(tokens)
|
||||
codegen(stmts, source)
|
||||
}
|
||||
|
||||
// compile_js — full pipeline (JS target, module mode): source string -> JS source string
|
||||
fn compile_js(source: String) -> String {
|
||||
let tokens: [Map<String, Any>] = lex(source)
|
||||
let stmts: [Map<String, Any>] = parse(tokens)
|
||||
// Token list is no longer needed after parsing — release it to free memory.
|
||||
el_release(tokens)
|
||||
codegen_js(stmts, source)
|
||||
}
|
||||
|
||||
// compile_js_with_bundle — JS target in bundle mode.
|
||||
// Reads el_runtime.js from runtime_path and inlines it inside an IIFE.
|
||||
fn compile_js_with_bundle(source: String, runtime_path: String) -> String {
|
||||
let tokens: [Map<String, Any>] = lex(source)
|
||||
let stmts: [Map<String, Any>] = parse(tokens)
|
||||
el_release(tokens)
|
||||
let runtime_content: String = fs_read(runtime_path)
|
||||
if str_eq(runtime_content, "") {
|
||||
println("el-compiler: warning: --bundle: could not read runtime at " + runtime_path)
|
||||
println("el-compiler: warning: bundle output will be incomplete")
|
||||
}
|
||||
codegen_js_bundle(stmts, source, runtime_content)
|
||||
}
|
||||
|
||||
// compile_dispatch — pick a backend based on the requested target.
|
||||
// tgt = "c" | "js"
|
||||
// (The parameter is named `tgt` because `target` is a reserved keyword
|
||||
// in El's lexer — it would be tokenised as `Target`, breaking the
|
||||
// parser's identifier resolution.)
|
||||
fn compile_dispatch(tgt: String, source: String) -> String {
|
||||
if str_eq(tgt, "js") { return compile_js(source) }
|
||||
compile(source)
|
||||
}
|
||||
|
||||
// compile_dispatch_bundle — like compile_dispatch but bundle mode for JS.
|
||||
fn compile_dispatch_bundle(tgt: String, source: String, runtime_path: String) -> String {
|
||||
if str_eq(tgt, "js") { return compile_js_with_bundle(source, runtime_path) }
|
||||
compile(source)
|
||||
}
|
||||
|
||||
// Detect a `--target=<lang>` flag in argv and return the target.
|
||||
// Returns "c" if none specified or unrecognized.
|
||||
fn detect_target(argv: [String]) -> String {
|
||||
let n: Int = native_list_len(argv)
|
||||
let i = 0
|
||||
while i < n {
|
||||
let a: String = native_list_get(argv, i)
|
||||
if str_starts_with(a, "--target=") {
|
||||
let v: String = str_slice(a, 9, str_len(a))
|
||||
return v
|
||||
}
|
||||
let i = i + 1
|
||||
}
|
||||
return "c"
|
||||
}
|
||||
|
||||
// Strip flags from argv, leaving only positional arguments.
|
||||
fn strip_flags(argv: [String]) -> [String] {
|
||||
let out: [String] = native_list_empty()
|
||||
let n: Int = native_list_len(argv)
|
||||
let i = 0
|
||||
while i < n {
|
||||
let a: String = native_list_get(argv, i)
|
||||
if !str_starts_with(a, "--") {
|
||||
let out = native_list_append(out, a)
|
||||
}
|
||||
let i = i + 1
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// Detect --emit-header flag in argv.
|
||||
fn detect_emit_header(argv: [String]) -> Bool {
|
||||
let n: Int = native_list_len(argv)
|
||||
let i = 0
|
||||
while i < n {
|
||||
let a: String = native_list_get(argv, i)
|
||||
if str_eq(a, "--emit-header") { return true }
|
||||
let i = i + 1
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Detect --bundle flag in argv.
|
||||
fn detect_bundle(argv: [String]) -> Bool {
|
||||
let n: Int = native_list_len(argv)
|
||||
let i = 0
|
||||
while i < n {
|
||||
let a: String = native_list_get(argv, i)
|
||||
if str_eq(a, "--bundle") { return true }
|
||||
let i = i + 1
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Detect --minify flag in argv.
|
||||
fn detect_minify(argv: [String]) -> Bool {
|
||||
let n: Int = native_list_len(argv)
|
||||
let i = 0
|
||||
while i < n {
|
||||
let a: String = native_list_get(argv, i)
|
||||
if str_eq(a, "--minify") { return true }
|
||||
let i = i + 1
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Detect --obfuscate flag in argv.
|
||||
fn detect_obfuscate(argv: [String]) -> Bool {
|
||||
let n: Int = native_list_len(argv)
|
||||
let i = 0
|
||||
while i < n {
|
||||
let a: String = native_list_get(argv, i)
|
||||
if str_eq(a, "--obfuscate") { return true }
|
||||
let i = i + 1
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Build a unique temp file path: /tmp/elc-<pid>-<timestamp>.<suffix>
|
||||
fn make_temp_path(suffix: String) -> String {
|
||||
let pid: Int = getpid_now()
|
||||
let ts: Int = time_now()
|
||||
"/tmp/elc-" + native_int_to_str(pid) + "-" + native_int_to_str(ts) + "." + suffix
|
||||
}
|
||||
|
||||
// Reserved globals that terser and javascript-obfuscator must not mangle.
|
||||
// These are referenced from HTML onclick= attributes and other direct window usage.
|
||||
fn js_reserved_names() -> String {
|
||||
"neuronDemoToggle,neuronDemoSend,neuronDemoReset,signInWith,signInWithEmail,signUpWithEmail,sendMagicLink,signOut,resetPassword,sendResetEmail,updatePassword,showSignIn,showSignUp,hideReset,setSort,addFamilyMember,removeFamilyMember,copyForPlatform,entHeadcountChange,NEURON_CFG"
|
||||
}
|
||||
|
||||
// Find a CLI tool by checking node_modules paths first, then falling back to npx.
|
||||
// src_dir is the directory of the source file being compiled.
|
||||
// Returns the command string to invoke the tool, or "" if not found.
|
||||
fn find_node_tool(tool_name: String, src_dir: String) -> String {
|
||||
// 1. Check ./node_modules/.bin/<tool> relative to source file
|
||||
let cand1: String = src_dir + "/node_modules/.bin/" + tool_name
|
||||
let check1: String = str_trim(exec_capture("test -x " + cand1 + " && echo yes 2>/dev/null"))
|
||||
if str_eq(check1, "yes") { return cand1 }
|
||||
// 2. Check ../node_modules/.bin/<tool> (monorepo layout)
|
||||
let parent_dir: String = dirname_of(src_dir)
|
||||
let cand2: String = parent_dir + "/node_modules/.bin/" + tool_name
|
||||
let check2: String = str_trim(exec_capture("test -x " + cand2 + " && echo yes 2>/dev/null"))
|
||||
if str_eq(check2, "yes") { return cand2 }
|
||||
// 3. Fall back to npx if it is on PATH. npx will use the globally cached
|
||||
// package or download on first use. Use --no to avoid auto-install if
|
||||
// the package is not already cached; if that fails, try with --yes.
|
||||
let npx_path: String = str_trim(exec_capture("which npx 2>/dev/null"))
|
||||
if !str_eq(npx_path, "") { return "npx --yes " + tool_name }
|
||||
return ""
|
||||
}
|
||||
|
||||
// apply_minify — run terser on js_path, write result to out_path.
|
||||
// Returns true on success, false on failure.
|
||||
fn apply_minify(js_path: String, out_path: String, src_dir: String) -> Bool {
|
||||
let terser: String = find_node_tool("terser", src_dir)
|
||||
if str_eq(terser, "") {
|
||||
println("el-compiler: error: terser not found. Run 'npm install terser' in your project directory.")
|
||||
return false
|
||||
}
|
||||
let names: String = js_reserved_names()
|
||||
// Single-quote the mangle reserved list so the shell does not glob-expand
|
||||
// the bracket expression. The compress options are safe without quoting.
|
||||
let compress_opts: String = "passes=2,drop_console=false,drop_debugger=true"
|
||||
let mangle_reserved: String = "'reserved=[" + names + "]'"
|
||||
let cmd: String = terser + " " + js_path + " --compress " + compress_opts + " --mangle " + mangle_reserved + " --output " + out_path
|
||||
let ret: Int = exec_command(cmd)
|
||||
if ret == 0 { return true }
|
||||
println("el-compiler: error: terser failed (exit " + native_int_to_str(ret) + ")")
|
||||
return false
|
||||
}
|
||||
|
||||
// apply_obfuscate — run javascript-obfuscator on js_path, write result to out_path.
|
||||
// Returns true on success, false on failure.
|
||||
fn apply_obfuscate(js_path: String, out_path: String, src_dir: String) -> Bool {
|
||||
let obfuscator: String = find_node_tool("javascript-obfuscator", src_dir)
|
||||
if str_eq(obfuscator, "") {
|
||||
println("el-compiler: error: javascript-obfuscator not found. Run 'npm install javascript-obfuscator' in your project directory.")
|
||||
return false
|
||||
}
|
||||
let names: String = js_reserved_names()
|
||||
let cmd: String = obfuscator + " " + js_path + " --output " + out_path + " --compact true --simplify true --string-array true --string-array-encoding base64 --string-array-threshold 0.75 --identifier-names-generator hexadecimal --rename-globals false --self-defending false --reserved-names " + names
|
||||
let ret: Int = exec_command(cmd)
|
||||
if ret == 0 { return true }
|
||||
println("el-compiler: error: javascript-obfuscator failed (exit " + native_int_to_str(ret) + ")")
|
||||
return false
|
||||
}
|
||||
|
||||
// Resolve the runtime path for --bundle mode.
|
||||
// Looks for el_runtime.js next to the source file first;
|
||||
// if not found there, looks next to the elc binary itself.
|
||||
// Returns "" if not found anywhere (caller emits a warning).
|
||||
fn resolve_runtime_path(src_path: String) -> String {
|
||||
let src_dir: String = dirname_of(src_path)
|
||||
let candidate: String = src_dir + "/el_runtime.js"
|
||||
let existing: String = fs_read(candidate)
|
||||
if !str_eq(existing, "") {
|
||||
return candidate
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// Reconstruct an El type annotation string from a parsed type node.
|
||||
fn type_node_to_el(t: Map<String, Any>) -> String {
|
||||
let k: String = t["kind"]
|
||||
if str_eq(k, "Simple") { return t["name"] }
|
||||
if str_eq(k, "List") {
|
||||
let inner: String = type_node_to_el(t["inner"])
|
||||
return "[" + inner + "]"
|
||||
}
|
||||
if str_eq(k, "Map") {
|
||||
let kt: String = type_node_to_el(t["key"])
|
||||
let vt: String = type_node_to_el(t["val"])
|
||||
return "Map<" + kt + ", " + vt + ">"
|
||||
}
|
||||
"Any"
|
||||
}
|
||||
|
||||
// emit_header — write a .elh file from parsed statements.
|
||||
// Scans for FnDef nodes and emits 'extern fn' declarations.
|
||||
fn emit_header(stmts: [Map<String, Any>], hdr_path: String) -> Void {
|
||||
let n: Int = native_list_len(stmts)
|
||||
let i = 0
|
||||
let parts: [String] = native_list_empty()
|
||||
let parts = native_list_append(parts, "// auto-generated by elc --emit-header — do not edit\n")
|
||||
while i < n {
|
||||
let stmt = native_list_get(stmts, i)
|
||||
let kind: String = stmt["stmt"]
|
||||
if str_eq(kind, "FnDef") {
|
||||
let name: String = stmt["name"]
|
||||
if !str_eq(name, "main") {
|
||||
let params = stmt["params"]
|
||||
let ret_type: String = stmt["ret_type"]
|
||||
// build param list
|
||||
let np: Int = native_list_len(params)
|
||||
let pi = 0
|
||||
let param_parts: [String] = native_list_empty()
|
||||
while pi < np {
|
||||
let param = native_list_get(params, pi)
|
||||
let pname: String = param["name"]
|
||||
let ptype: String = param["type"]
|
||||
if str_eq(ptype, "") { let ptype = "Any" }
|
||||
let param_parts = native_list_append(param_parts, pname + ": " + ptype)
|
||||
let pi = pi + 1
|
||||
}
|
||||
let params_str: String = str_join(param_parts, ", ")
|
||||
let ret_str: String = ret_type
|
||||
if str_eq(ret_str, "") { let ret_str = "Any" }
|
||||
let sig: String = "extern fn " + name + "(" + params_str + ") -> " + ret_str
|
||||
let parts = native_list_append(parts, sig + "\n")
|
||||
}
|
||||
}
|
||||
let i = i + 1
|
||||
}
|
||||
let content: String = str_join(parts, "")
|
||||
let ok: Bool = fs_write(hdr_path, content)
|
||||
}
|
||||
|
||||
// ── Import resolution ────────────────────────────────────────────────────────
|
||||
//
|
||||
// elc supports two forms of import:
|
||||
// import "path/to/file.el" — quoted relative path
|
||||
// from module import { Name } — bare module name resolves to module.el
|
||||
// in the entry source's directory
|
||||
//
|
||||
// Codegen treats Import statements as no-ops (declarations only), so to
|
||||
// actually link bodies across files we textually concatenate every imported
|
||||
// source ahead of the entry source before lex/parse. resolve_imports does a
|
||||
// depth-first traversal with deduplication so any module that gets pulled in
|
||||
// transitively is included exactly once.
|
||||
|
||||
fn dirname_of(path: String) -> String {
|
||||
let n: Int = str_len(path)
|
||||
let i: Int = n - 1
|
||||
while i >= 0 {
|
||||
let c: String = str_slice(path, i, i + 1)
|
||||
if str_eq(c, "/") {
|
||||
return str_slice(path, 0, i)
|
||||
}
|
||||
let i = i - 1
|
||||
}
|
||||
return "."
|
||||
}
|
||||
|
||||
// Extract the resolved file path from a single trimmed source line. Returns
|
||||
// "" if the line is not an import.
|
||||
fn parse_import_line(trimmed: String, dir: String) -> String {
|
||||
if str_starts_with(trimmed, "import \"") {
|
||||
let after: String = str_slice(trimmed, 8, str_len(trimmed))
|
||||
let q: Int = str_index_of(after, "\"")
|
||||
if q > 0 {
|
||||
let mod: String = str_slice(after, 0, q)
|
||||
return dir + "/" + mod
|
||||
}
|
||||
}
|
||||
if str_starts_with(trimmed, "from ") {
|
||||
let after: String = str_slice(trimmed, 5, str_len(trimmed))
|
||||
// module name is the first whitespace-delimited token
|
||||
let sp: Int = str_index_of(after, " ")
|
||||
if sp > 0 {
|
||||
let mod_raw: String = str_slice(after, 0, sp)
|
||||
let mod: String = str_trim(mod_raw)
|
||||
if !str_eq(mod, "") {
|
||||
return dir + "/" + mod + ".el"
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// Recursively resolve imports starting from src_path. Returns the combined
|
||||
// source text with every imported module's body inlined ahead of the entry
|
||||
// source, deduplicated by absolute path. Uses state_set to track which paths
|
||||
// have already been pulled in for this run.
|
||||
//
|
||||
// Accumulates chunks into lists and joins once at the end to avoid the O(n²)
|
||||
// memory growth caused by repeated `prefix = prefix + chunk` concatenation.
|
||||
fn resolve_imports(src_path: String) -> String {
|
||||
let seen_key: String = "__elc_imp__:" + src_path
|
||||
let already: String = state_get(seen_key)
|
||||
if !str_eq(already, "") { return "" }
|
||||
state_set(seen_key, "1")
|
||||
|
||||
let source: String = fs_read(src_path)
|
||||
let dir: String = dirname_of(src_path)
|
||||
let lines: [String] = str_split(source, "\n")
|
||||
let n: Int = native_list_len(lines)
|
||||
|
||||
// Collect chunks into lists — O(1) amortized per append.
|
||||
// Join once at the end — O(n) single pass.
|
||||
let prefix_chunks: [String] = native_list_empty()
|
||||
let body_chunks: [String] = native_list_empty()
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
let line: String = native_list_get(lines, i)
|
||||
let trimmed: String = str_trim(line)
|
||||
let imp_path: String = parse_import_line(trimmed, dir)
|
||||
if !str_eq(imp_path, "") {
|
||||
// Use pre-compiled header if available (separate compilation).
|
||||
// Only check .elh for imported files — never for the entry file itself.
|
||||
let imp_elh_path: String = str_slice(imp_path, 0, str_len(imp_path) - 3) + ".elh"
|
||||
let imp_elh: String = fs_read(imp_elh_path)
|
||||
if !str_eq(imp_elh, "") {
|
||||
// Header exists: mark the .el as seen (so it won't be re-inlined
|
||||
// if something else also imports it) and use the header text.
|
||||
let seen_imp_key: String = "__elc_imp__:" + imp_path
|
||||
state_set(seen_imp_key, "1")
|
||||
let prefix_chunks = native_list_append(prefix_chunks, imp_elh)
|
||||
} else {
|
||||
let imp_body: String = resolve_imports(imp_path)
|
||||
let prefix_chunks = native_list_append(prefix_chunks, imp_body)
|
||||
}
|
||||
} else {
|
||||
let body_chunks = native_list_append(body_chunks, line + "\n")
|
||||
}
|
||||
let i = i + 1
|
||||
}
|
||||
return str_join(prefix_chunks, "") + str_join(body_chunks, "")
|
||||
}
|
||||
|
||||
// run_with_postprocess — codegen + minify + optional obfuscate pipeline.
|
||||
//
|
||||
// Called from main() when --minify or --obfuscate is active. Redirects stdout
|
||||
// to a temp file during codegen so the output can be passed through the
|
||||
// external tools (terser, javascript-obfuscator) before final emission.
|
||||
//
|
||||
// Pipeline: codegen -> terser -> (javascript-obfuscator) -> stdout or file
|
||||
fn run_with_postprocess(tgt: String, source: String, src_path: String, do_bundle: Bool, do_obfuscate: Bool, argc: Int, positional: [String]) -> Void {
|
||||
let src_dir: String = dirname_of(src_path)
|
||||
let tmp_gen: String = make_temp_path("js")
|
||||
let tmp_min: String = make_temp_path("min.js")
|
||||
|
||||
// Redirect stdout to tmp_gen so codegen println output is captured.
|
||||
stdout_to_file(tmp_gen)
|
||||
if do_bundle {
|
||||
let runtime_path: String = resolve_runtime_path(src_path)
|
||||
compile_dispatch_bundle(tgt, source, runtime_path)
|
||||
} else {
|
||||
compile_dispatch(tgt, source)
|
||||
}
|
||||
stdout_restore()
|
||||
|
||||
// Run terser: tmp_gen -> tmp_min
|
||||
let ok_min: Bool = apply_minify(tmp_gen, tmp_min, src_dir)
|
||||
if !ok_min {
|
||||
exec_command("rm -f " + tmp_gen + " " + tmp_min)
|
||||
exit(1)
|
||||
}
|
||||
|
||||
// Determine final result path (either tmp_min or post-obfuscation file).
|
||||
// Use state to pass the final path out of the optional obfuscation branch.
|
||||
state_set("__elc_final_js", tmp_min)
|
||||
|
||||
if do_obfuscate {
|
||||
let tmp_obf: String = make_temp_path("obf.js")
|
||||
let ok_obf: Bool = apply_obfuscate(tmp_min, tmp_obf, src_dir)
|
||||
if !ok_obf {
|
||||
exec_command("rm -f " + tmp_gen + " " + tmp_min + " " + tmp_obf)
|
||||
exit(1)
|
||||
}
|
||||
state_set("__elc_final_js", tmp_obf)
|
||||
}
|
||||
|
||||
let final_path: String = state_get("__elc_final_js")
|
||||
let final_js: String = fs_read(final_path)
|
||||
|
||||
// Clean up all temp files.
|
||||
exec_command("rm -f " + tmp_gen + " " + tmp_min)
|
||||
if do_obfuscate {
|
||||
exec_command("rm -f " + final_path)
|
||||
}
|
||||
|
||||
if argc >= 2 {
|
||||
let out_path: String = native_list_get(positional, 1)
|
||||
let ok: Bool = fs_write(out_path, final_js)
|
||||
if ok {
|
||||
return
|
||||
} else {
|
||||
println("el-compiler: failed to write output")
|
||||
exit(1)
|
||||
}
|
||||
}
|
||||
// No output file: print final JS to stdout.
|
||||
print(final_js)
|
||||
}
|
||||
|
||||
// main — CLI entry point.
|
||||
//
|
||||
// elc <source.el> # emit C to stdout
|
||||
// elc --target=js <source.el> # emit JS (module) to stdout
|
||||
// elc --target=js --bundle <source.el> # emit self-contained JS (IIFE) to stdout
|
||||
// elc --target=js --bundle --minify <source.el> # emit minified IIFE to stdout
|
||||
// elc --target=js --bundle --obfuscate <source.el> # emit minified+obfuscated IIFE to stdout
|
||||
// elc --target=c <source.el> <out.c> # write C to file
|
||||
// elc --target=js <source.el> <out.js> # write JS to file
|
||||
// elc --target=js --bundle <source.el> <out.js> # write bundled JS to file
|
||||
// elc --target=js --bundle --minify <source.el> <out.min.js> # write minified JS to file
|
||||
fn main() -> Void {
|
||||
let argv: [String] = args()
|
||||
// Use `tgt` not `target`: `target` is a reserved keyword in the lexer
|
||||
// (Section 1.5 of the language spec). detect_target itself is fine
|
||||
// because the function-name position has no token-class restriction.
|
||||
let tgt: String = detect_target(argv)
|
||||
let do_emit_header: Bool = detect_emit_header(argv)
|
||||
let do_bundle: Bool = detect_bundle(argv)
|
||||
let do_minify: Bool = detect_minify(argv)
|
||||
let do_obfuscate: Bool = detect_obfuscate(argv)
|
||||
// --obfuscate implies --minify: obfuscating unminified code is pointless.
|
||||
if do_obfuscate {
|
||||
let do_minify = true
|
||||
}
|
||||
let positional: [String] = strip_flags(argv)
|
||||
let argc: Int = native_list_len(positional)
|
||||
if argc < 1 {
|
||||
println("el-compiler: usage: elc [--target=c|js] [--bundle] [--minify] [--obfuscate] [--emit-header] <source.el> [<output>]")
|
||||
exit(1)
|
||||
}
|
||||
|
||||
// --minify and --obfuscate require --target=js
|
||||
if do_minify {
|
||||
if !str_eq(tgt, "js") {
|
||||
println("el-compiler: error: --minify and --obfuscate require --target=js")
|
||||
exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
let src_path: String = native_list_get(positional, 0)
|
||||
|
||||
// When --emit-header is requested, parse the source file directly
|
||||
// (without inlining imports) and write out a .elh file alongside the .c.
|
||||
if do_emit_header {
|
||||
let raw_source: String = fs_read(src_path)
|
||||
let hdr_tokens: [Map<String, Any>] = lex(raw_source)
|
||||
let hdr_stmts: [Map<String, Any>] = parse(hdr_tokens)
|
||||
el_release(hdr_tokens)
|
||||
let hdr_path: String = str_slice(src_path, 0, str_len(src_path) - 3) + ".elh"
|
||||
emit_header(hdr_stmts, hdr_path)
|
||||
el_release(hdr_stmts)
|
||||
}
|
||||
|
||||
let source: String = resolve_imports(src_path)
|
||||
|
||||
// When post-processing (--minify or --obfuscate) is requested, redirect
|
||||
// stdout to a temp file so codegen output can be captured and piped through
|
||||
// the external tools. After codegen, restore stdout before emitting the
|
||||
// final result.
|
||||
if do_minify {
|
||||
run_with_postprocess(tgt, source, src_path, do_bundle, do_obfuscate, argc, positional)
|
||||
exit(0)
|
||||
}
|
||||
|
||||
// Standard path (no post-processing).
|
||||
let out: String = ""
|
||||
if do_bundle {
|
||||
let runtime_path: String = resolve_runtime_path(src_path)
|
||||
let out = compile_dispatch_bundle(tgt, source, runtime_path)
|
||||
} else {
|
||||
let out = compile_dispatch(tgt, source)
|
||||
}
|
||||
if argc >= 2 {
|
||||
let out_path: String = native_list_get(positional, 1)
|
||||
let ok: Bool = fs_write(out_path, out)
|
||||
if ok {
|
||||
exit(0)
|
||||
} else {
|
||||
println("el-compiler: failed to write output")
|
||||
exit(1)
|
||||
}
|
||||
}
|
||||
// No output path: codegen streamed to stdout already; out is "".
|
||||
}
|
||||
@@ -0,0 +1,993 @@
|
||||
// lexer.el - el self-hosting lexer
|
||||
//
|
||||
// Tokenises an el source string into a list of token maps.
|
||||
// Each token is a Map<String, Any> with keys:
|
||||
// "kind" -> String (e.g. "Int", "Ident", "Plus")
|
||||
// "value" -> String (the raw text of the token)
|
||||
//
|
||||
// Entry point: fn lex(source: String) -> [Map<String, Any>]
|
||||
//
|
||||
// Uses native_string_chars to split the source into a chars list,
|
||||
// then indexes it with native_list_get - avoids O(N-) string cloning.
|
||||
|
||||
// -- Character helpers ---------------------------------------------------------
|
||||
|
||||
fn lex_is_digit(ch: String) -> Bool {
|
||||
if ch == "0" { return true }
|
||||
if ch == "1" { return true }
|
||||
if ch == "2" { return true }
|
||||
if ch == "3" { return true }
|
||||
if ch == "4" { return true }
|
||||
if ch == "5" { return true }
|
||||
if ch == "6" { return true }
|
||||
if ch == "7" { return true }
|
||||
if ch == "8" { return true }
|
||||
if ch == "9" { return true }
|
||||
false
|
||||
}
|
||||
|
||||
fn lex_is_alpha(ch: String) -> Bool {
|
||||
if ch == "a" { return true }
|
||||
if ch == "b" { return true }
|
||||
if ch == "c" { return true }
|
||||
if ch == "d" { return true }
|
||||
if ch == "e" { return true }
|
||||
if ch == "f" { return true }
|
||||
if ch == "g" { return true }
|
||||
if ch == "h" { return true }
|
||||
if ch == "i" { return true }
|
||||
if ch == "j" { return true }
|
||||
if ch == "k" { return true }
|
||||
if ch == "l" { return true }
|
||||
if ch == "m" { return true }
|
||||
if ch == "n" { return true }
|
||||
if ch == "o" { return true }
|
||||
if ch == "p" { return true }
|
||||
if ch == "q" { return true }
|
||||
if ch == "r" { return true }
|
||||
if ch == "s" { return true }
|
||||
if ch == "t" { return true }
|
||||
if ch == "u" { return true }
|
||||
if ch == "v" { return true }
|
||||
if ch == "w" { return true }
|
||||
if ch == "x" { return true }
|
||||
if ch == "y" { return true }
|
||||
if ch == "z" { return true }
|
||||
if ch == "A" { return true }
|
||||
if ch == "B" { return true }
|
||||
if ch == "C" { return true }
|
||||
if ch == "D" { return true }
|
||||
if ch == "E" { return true }
|
||||
if ch == "F" { return true }
|
||||
if ch == "G" { return true }
|
||||
if ch == "H" { return true }
|
||||
if ch == "I" { return true }
|
||||
if ch == "J" { return true }
|
||||
if ch == "K" { return true }
|
||||
if ch == "L" { return true }
|
||||
if ch == "M" { return true }
|
||||
if ch == "N" { return true }
|
||||
if ch == "O" { return true }
|
||||
if ch == "P" { return true }
|
||||
if ch == "Q" { return true }
|
||||
if ch == "R" { return true }
|
||||
if ch == "S" { return true }
|
||||
if ch == "T" { return true }
|
||||
if ch == "U" { return true }
|
||||
if ch == "V" { return true }
|
||||
if ch == "W" { return true }
|
||||
if ch == "X" { return true }
|
||||
if ch == "Y" { return true }
|
||||
if ch == "Z" { return true }
|
||||
false
|
||||
}
|
||||
|
||||
fn is_alnum_or_underscore(ch: String) -> Bool {
|
||||
if lex_is_digit(ch) { return true }
|
||||
if lex_is_alpha(ch) { return true }
|
||||
if ch == "_" { return true }
|
||||
false
|
||||
}
|
||||
|
||||
fn lex_is_whitespace(ch: String) -> Bool {
|
||||
if ch == " " { return true }
|
||||
if ch == "\t" { return true }
|
||||
if ch == "\n" { return true }
|
||||
if ch == "\r" { return true }
|
||||
false
|
||||
}
|
||||
|
||||
fn make_tok(kind: String, value: String) -> Map<String, Any> {
|
||||
{ "kind": kind, "value": value }
|
||||
}
|
||||
|
||||
// -- Keyword lookup ------------------------------------------------------------
|
||||
|
||||
fn keyword_kind(word: String) -> String {
|
||||
if word == "let" { return "Let" }
|
||||
if word == "fn" { return "Fn" }
|
||||
if word == "type" { return "Type" }
|
||||
if word == "enum" { return "Enum" }
|
||||
if word == "match" { return "Match" }
|
||||
if word == "return" { return "Return" }
|
||||
if word == "if" { return "If" }
|
||||
if word == "else" { return "Else" }
|
||||
if word == "for" { return "For" }
|
||||
if word == "in" { return "In" }
|
||||
if word == "while" { return "While" }
|
||||
if word == "import" { return "Import" }
|
||||
if word == "from" { return "From" }
|
||||
if word == "as" { return "As" }
|
||||
if word == "with" { return "With" }
|
||||
if word == "sealed" { return "Sealed" }
|
||||
if word == "activate" { return "Activate" }
|
||||
if word == "where" { return "Where" }
|
||||
if word == "test" { return "Test" }
|
||||
if word == "seed" { return "Seed" }
|
||||
if word == "assert" { return "Assert" }
|
||||
if word == "protocol" { return "Protocol" }
|
||||
if word == "impl" { return "Impl" }
|
||||
if word == "retry" { return "Retry" }
|
||||
if word == "times" { return "Times" }
|
||||
if word == "fallback" { return "Fallback" }
|
||||
if word == "reason" { return "Reason" }
|
||||
if word == "parallel" { return "Parallel" }
|
||||
if word == "trace" { return "Trace" }
|
||||
if word == "requires" { return "Requires" }
|
||||
if word == "deploy" { return "Deploy" }
|
||||
if word == "to" { return "To" }
|
||||
if word == "via" { return "Via" }
|
||||
if word == "target" { return "Target" }
|
||||
if word == "true" { return "Bool" }
|
||||
if word == "false" { return "Bool" }
|
||||
if word == "cgi" { return "Cgi" }
|
||||
if word == "service" { return "Service" }
|
||||
if word == "manager" { return "Manager" }
|
||||
if word == "engine" { return "Engine" }
|
||||
if word == "accessor" { return "Accessor" }
|
||||
if word == "vessel" { return "Vessel" }
|
||||
if word == "extern" { return "Extern" }
|
||||
if word == "break" { return "Break" }
|
||||
if word == "continue" { return "Continue" }
|
||||
""
|
||||
}
|
||||
|
||||
// -- Scan helpers --------------------------------------------------------------
|
||||
// All scan helpers receive the chars list and total length.
|
||||
|
||||
// scan_digits - advance i while chars[i] is a digit
|
||||
// Returns { "text": ..., "pos": i }
|
||||
fn scan_digits(chars: [String], start: Int, total: Int) -> Map<String, Any> {
|
||||
let i = start
|
||||
let parts: [String] = native_list_empty()
|
||||
let running = true
|
||||
while running {
|
||||
if i >= total {
|
||||
let running = false
|
||||
} else {
|
||||
let ch: String = native_list_get(chars, i)
|
||||
if lex_is_digit(ch) {
|
||||
let parts = native_list_append(parts, ch)
|
||||
let i = i + 1
|
||||
} else {
|
||||
let running = false
|
||||
}
|
||||
}
|
||||
}
|
||||
{ "text": str_join(parts, ""), "pos": i }
|
||||
}
|
||||
|
||||
// scan_ident - advance i while chars[i] is alphanumeric or underscore
|
||||
fn scan_ident(chars: [String], start: Int, total: Int) -> Map<String, Any> {
|
||||
let i = start
|
||||
let parts: [String] = native_list_empty()
|
||||
let running = true
|
||||
while running {
|
||||
if i >= total {
|
||||
let running = false
|
||||
} else {
|
||||
let ch: String = native_list_get(chars, i)
|
||||
if is_alnum_or_underscore(ch) {
|
||||
let parts = native_list_append(parts, ch)
|
||||
let i = i + 1
|
||||
} else {
|
||||
let running = false
|
||||
}
|
||||
}
|
||||
}
|
||||
{ "text": str_join(parts, ""), "pos": i }
|
||||
}
|
||||
|
||||
// -- Code-bearing string detection + comment strip ----------------------------
|
||||
// Inline JS/CSS literals embedded in El source (e.g. <script>-</script> blobs
|
||||
// or stylesheet payloads inside string literals) carry their own line and
|
||||
// block comments. Those comments leak into the served HTML and reveal build
|
||||
// notes the visitor should never see. We strip them at the lexer so every
|
||||
// downstream consumer (codegen-c, codegen-js, parser) gets the cleaned form.
|
||||
//
|
||||
// looks_like_code - heuristic gate so we only strip strings that actually
|
||||
// embed JS or CSS. Plain prose, hex blobs, JSON, etc. pass through verbatim.
|
||||
|
||||
fn substr_at(chars: [String], start: Int, total: Int, needle: String) -> Bool {
|
||||
let nchars: [String] = native_string_chars(needle)
|
||||
let nlen: Int = native_list_len(nchars)
|
||||
if start + nlen > total { return false }
|
||||
let i = 0
|
||||
let matched = true
|
||||
while i < nlen {
|
||||
let a: String = native_list_get(chars, start + i)
|
||||
let b: String = native_list_get(nchars, i)
|
||||
if a == b { let i = i + 1 } else { let matched = false; let i = nlen }
|
||||
}
|
||||
matched
|
||||
}
|
||||
|
||||
fn str_has(s: String, needle: String) -> Bool {
|
||||
let chars: [String] = native_string_chars(s)
|
||||
let total: Int = native_list_len(chars)
|
||||
let i = 0
|
||||
let found = false
|
||||
while i < total {
|
||||
if substr_at(chars, i, total, needle) {
|
||||
let found = true
|
||||
let i = total
|
||||
} else {
|
||||
let i = i + 1
|
||||
}
|
||||
}
|
||||
found
|
||||
}
|
||||
|
||||
fn looks_like_code(s: String) -> Bool {
|
||||
if str_has(s, "<script") { return true }
|
||||
if str_has(s, "<style") { return true }
|
||||
if str_has(s, "function") {
|
||||
if str_has(s, ";") { return true }
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
// strip_code_comments - character-by-character walk. Tracks JS string state
|
||||
// (single, double, backtick) and never strips inside one. Backslash escapes
|
||||
// inside JS strings consume the next char verbatim. URLs like https:// are
|
||||
// preserved by checking the previous char before treating // as a line
|
||||
// comment opener: if the char immediately before '/' is ':', emit the '/'
|
||||
// literally and advance one position.
|
||||
fn strip_code_comments(s: String) -> String {
|
||||
let chars: [String] = native_string_chars(s)
|
||||
let total: Int = native_list_len(chars)
|
||||
let out_parts: [String] = native_list_empty()
|
||||
let i = 0
|
||||
let in_squote = false
|
||||
let in_dquote = false
|
||||
let in_btick = false
|
||||
let prev = ""
|
||||
while i < total {
|
||||
let ch: String = native_list_get(chars, i)
|
||||
let in_js_string = false
|
||||
if in_squote { let in_js_string = true }
|
||||
if in_dquote { let in_js_string = true }
|
||||
if in_btick { let in_js_string = true }
|
||||
|
||||
if in_js_string {
|
||||
// Backslash escape: consume next char verbatim regardless of which.
|
||||
if ch == "\\" {
|
||||
let out_parts = native_list_append(out_parts, ch)
|
||||
let next_i = i + 1
|
||||
if next_i < total {
|
||||
let nc: String = native_list_get(chars, next_i)
|
||||
let out_parts = native_list_append(out_parts, nc)
|
||||
let prev = nc
|
||||
let i = next_i + 1
|
||||
} else {
|
||||
let prev = ch
|
||||
let i = next_i
|
||||
}
|
||||
} else {
|
||||
if in_squote {
|
||||
if ch == "'" { let in_squote = false }
|
||||
} else {
|
||||
if in_dquote {
|
||||
if ch == "\"" { let in_dquote = false }
|
||||
} else {
|
||||
if in_btick {
|
||||
if ch == "`" { let in_btick = false }
|
||||
}
|
||||
}
|
||||
}
|
||||
let out_parts = native_list_append(out_parts, ch)
|
||||
let prev = ch
|
||||
let i = i + 1
|
||||
}
|
||||
} else {
|
||||
// Not in a JS string. Check for comment openers.
|
||||
let next_i = i + 1
|
||||
let next_ch = ""
|
||||
if next_i < total {
|
||||
let next_ch: String = native_list_get(chars, next_i)
|
||||
}
|
||||
|
||||
if ch == "/" {
|
||||
if next_ch == "/" {
|
||||
// URL guard: prev char ':' means this is "://", not a comment.
|
||||
if prev == ":" {
|
||||
let out_parts = native_list_append(out_parts, ch)
|
||||
let prev = ch
|
||||
let i = i + 1
|
||||
} else {
|
||||
// Skip until newline (newline itself is preserved so
|
||||
// surrounding line counts/structure stay sane).
|
||||
let i = i + 2
|
||||
let scanning = true
|
||||
while scanning {
|
||||
if i >= total {
|
||||
let scanning = false
|
||||
} else {
|
||||
let lc: String = native_list_get(chars, i)
|
||||
if lc == "\n" {
|
||||
let scanning = false
|
||||
} else {
|
||||
let i = i + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
let prev = ""
|
||||
}
|
||||
} else {
|
||||
if next_ch == "*" {
|
||||
// Skip until matching "*/".
|
||||
let i = i + 2
|
||||
let scanning2 = true
|
||||
while scanning2 {
|
||||
if i >= total {
|
||||
let scanning2 = false
|
||||
} else {
|
||||
let bc: String = native_list_get(chars, i)
|
||||
if bc == "*" {
|
||||
let after = i + 1
|
||||
if after < total {
|
||||
let nc2: String = native_list_get(chars, after)
|
||||
if nc2 == "/" {
|
||||
let i = after + 1
|
||||
let scanning2 = false
|
||||
} else {
|
||||
let i = i + 1
|
||||
}
|
||||
} else {
|
||||
let i = i + 1
|
||||
}
|
||||
} else {
|
||||
let i = i + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
let prev = ""
|
||||
} else {
|
||||
let out_parts = native_list_append(out_parts, ch)
|
||||
let prev = ch
|
||||
let i = i + 1
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Open a JS string?
|
||||
if ch == "'" {
|
||||
let in_squote = true
|
||||
let out_parts = native_list_append(out_parts, ch)
|
||||
let prev = ch
|
||||
let i = i + 1
|
||||
} else {
|
||||
if ch == "\"" {
|
||||
let in_dquote = true
|
||||
let out_parts = native_list_append(out_parts, ch)
|
||||
let prev = ch
|
||||
let i = i + 1
|
||||
} else {
|
||||
if ch == "`" {
|
||||
let in_btick = true
|
||||
let out_parts = native_list_append(out_parts, ch)
|
||||
let prev = ch
|
||||
let i = i + 1
|
||||
} else {
|
||||
let out_parts = native_list_append(out_parts, ch)
|
||||
let prev = ch
|
||||
let i = i + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
str_join(out_parts, "")
|
||||
}
|
||||
|
||||
// scan_string - scan a quoted string literal, handling \" escapes.
|
||||
// Starts AFTER the opening quote. Returns { "text": content, "pos": i_after_close }
|
||||
fn scan_string(chars: [String], start: Int, total: Int) -> Map<String, Any> {
|
||||
let i = start
|
||||
let parts: [String] = native_list_empty()
|
||||
let running = true
|
||||
while running {
|
||||
if i >= total {
|
||||
let running = false
|
||||
} else {
|
||||
let ch: String = native_list_get(chars, i)
|
||||
if ch == "\\" {
|
||||
// escape: peek next char
|
||||
let next_i = i + 1
|
||||
if next_i < total {
|
||||
let next_ch: String = native_list_get(chars, next_i)
|
||||
if next_ch == "\"" {
|
||||
let parts = native_list_append(parts, "\"")
|
||||
let i = next_i + 1
|
||||
} else {
|
||||
if next_ch == "n" {
|
||||
let parts = native_list_append(parts, "\n")
|
||||
let i = next_i + 1
|
||||
} else {
|
||||
if next_ch == "t" {
|
||||
let parts = native_list_append(parts, "\t")
|
||||
let i = next_i + 1
|
||||
} else {
|
||||
if next_ch == "r" {
|
||||
let parts = native_list_append(parts, "\r")
|
||||
let i = next_i + 1
|
||||
} else {
|
||||
if next_ch == "\\" {
|
||||
let parts = native_list_append(parts, "\\")
|
||||
let i = next_i + 1
|
||||
} else {
|
||||
let parts = native_list_append(parts, next_ch)
|
||||
let i = next_i + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let i = i + 1
|
||||
}
|
||||
} else {
|
||||
if ch == "\"" {
|
||||
let i = i + 1
|
||||
let running = false
|
||||
} else {
|
||||
let parts = native_list_append(parts, ch)
|
||||
let i = i + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
{ "text": str_join(parts, ""), "pos": i }
|
||||
}
|
||||
|
||||
// -- String interpolation ------------------------------------------------------
|
||||
//
|
||||
// scan_interp_brace - scan from `start` (the char after `${`) to the matching
|
||||
// `}`, tracking brace depth so inner braces (e.g. fn calls, map literals) are
|
||||
// handled correctly. Returns { "text": inner_source, "pos": i_after_close }.
|
||||
fn scan_interp_brace(chars: [String], start: Int, total: Int) -> Map<String, Any> {
|
||||
let i = start
|
||||
let parts: [String] = native_list_empty()
|
||||
let depth = 1
|
||||
let running = true
|
||||
while running {
|
||||
if i >= total {
|
||||
let running = false
|
||||
} else {
|
||||
let ch: String = native_list_get(chars, i)
|
||||
if ch == "{" {
|
||||
let depth = depth + 1
|
||||
let parts = native_list_append(parts, ch)
|
||||
let i = i + 1
|
||||
} else {
|
||||
if ch == "}" {
|
||||
let depth = depth - 1
|
||||
if depth <= 0 {
|
||||
// Closing brace of the interpolation - stop, do not include it
|
||||
let i = i + 1
|
||||
let running = false
|
||||
} else {
|
||||
let parts = native_list_append(parts, ch)
|
||||
let i = i + 1
|
||||
}
|
||||
} else {
|
||||
let parts = native_list_append(parts, ch)
|
||||
let i = i + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
{ "text": str_join(parts, ""), "pos": i }
|
||||
}
|
||||
|
||||
// interp_tokens_append_all - copy every token from src into dst, skipping the
|
||||
// trailing Eof sentinel that lex() always appends. Returns the updated dst list.
|
||||
fn interp_tokens_append_all(dst: [Map<String, Any>], src: [Map<String, Any>]) -> [Map<String, Any>] {
|
||||
let src_len: Int = native_list_len(src)
|
||||
let j = 0
|
||||
let result = dst
|
||||
while j < src_len {
|
||||
let tok: Map<String, Any> = native_list_get(src, j)
|
||||
let tk: String = tok["kind"]
|
||||
if tk == "Eof" {
|
||||
let j = src_len
|
||||
} else {
|
||||
let result = native_list_append(result, tok)
|
||||
let j = j + 1
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
// scan_interp_string - scan a string literal that may contain ${expr}
|
||||
// interpolations. Starts AFTER the opening `"`.
|
||||
// Returns { "tokens": [token list to inject], "pos": i_after_close_quote }.
|
||||
//
|
||||
// For a plain string (no ${}) this emits a single Str token, identical to the
|
||||
// old scan_string path. For an interpolated string it emits a flat sequence
|
||||
// of tokens equivalent to the string-concat expression, for example:
|
||||
//
|
||||
// "hello ${name}!"
|
||||
// => Str("hello ") Plus <tokens for name> Plus Str("!")
|
||||
//
|
||||
// Empty literal segments between adjacent ${ } blocks are omitted. The
|
||||
// resulting token stream is consumed by the existing parse_binop / parse_primary
|
||||
// path in the parser with zero parser changes required.
|
||||
//
|
||||
// Supported escape sequences: \" \n \t \r \\ \$ (literal dollar sign).
|
||||
// Nested quotes inside ${} are not supported; use a variable instead.
|
||||
fn scan_interp_string(chars: [String], start: Int, total: Int) -> Map<String, Any> {
|
||||
let i = start
|
||||
let out_tokens: [Map<String, Any>] = native_list_empty()
|
||||
let cur_part: [String] = native_list_empty()
|
||||
let has_interp = false
|
||||
let need_plus = false
|
||||
let running = true
|
||||
|
||||
while running {
|
||||
if i >= total {
|
||||
let running = false
|
||||
} else {
|
||||
let ch: String = native_list_get(chars, i)
|
||||
|
||||
if ch == "\\" {
|
||||
// Escape sequence
|
||||
let next_i = i + 1
|
||||
if next_i < total {
|
||||
let next_ch: String = native_list_get(chars, next_i)
|
||||
if next_ch == "$" {
|
||||
// \$ => literal '$' (escape for interpolation syntax)
|
||||
let cur_part = native_list_append(cur_part, "$")
|
||||
let i = next_i + 1
|
||||
} else {
|
||||
if next_ch == "\"" {
|
||||
let cur_part = native_list_append(cur_part, "\"")
|
||||
let i = next_i + 1
|
||||
} else {
|
||||
if next_ch == "n" {
|
||||
let cur_part = native_list_append(cur_part, "\n")
|
||||
let i = next_i + 1
|
||||
} else {
|
||||
if next_ch == "t" {
|
||||
let cur_part = native_list_append(cur_part, "\t")
|
||||
let i = next_i + 1
|
||||
} else {
|
||||
if next_ch == "r" {
|
||||
let cur_part = native_list_append(cur_part, "\r")
|
||||
let i = next_i + 1
|
||||
} else {
|
||||
if next_ch == "\\" {
|
||||
let cur_part = native_list_append(cur_part, "\\")
|
||||
let i = next_i + 1
|
||||
} else {
|
||||
let cur_part = native_list_append(cur_part, next_ch)
|
||||
let i = next_i + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let i = i + 1
|
||||
}
|
||||
} else {
|
||||
if ch == "\"" {
|
||||
// Closing quote - stop scanning
|
||||
let i = i + 1
|
||||
let running = false
|
||||
} else {
|
||||
if ch == "$" {
|
||||
// Check for ${ (start of interpolation)
|
||||
let next_i = i + 1
|
||||
let is_interp = false
|
||||
if next_i < total {
|
||||
let next_ch: String = native_list_get(chars, next_i)
|
||||
if next_ch == "{" {
|
||||
let is_interp = true
|
||||
}
|
||||
}
|
||||
if is_interp {
|
||||
// Flush the accumulated literal part (if non-empty)
|
||||
let part_len: Int = native_list_len(cur_part)
|
||||
if part_len > 0 {
|
||||
let part_text = str_join(cur_part, "")
|
||||
if need_plus {
|
||||
let out_tokens = native_list_append(out_tokens, make_tok("Plus", "+"))
|
||||
}
|
||||
let clean_part = part_text
|
||||
if looks_like_code(part_text) {
|
||||
let clean_part = strip_code_comments(part_text)
|
||||
}
|
||||
let out_tokens = native_list_append(out_tokens, make_tok("Str", clean_part))
|
||||
let need_plus = true
|
||||
}
|
||||
let cur_part = native_list_empty()
|
||||
let has_interp = true
|
||||
|
||||
// Scan brace-balanced expression source
|
||||
let brace_result = scan_interp_brace(chars, next_i + 1, total)
|
||||
let expr_src: String = brace_result["text"]
|
||||
let new_i: Int = brace_result["pos"]
|
||||
let i = new_i
|
||||
|
||||
// Re-lex the expression and inline the tokens.
|
||||
// Wrap in ( ) so that operators inside ${} (e.g.
|
||||
// age + 1) are parsed as a grouped sub-expression
|
||||
// rather than merging with the surrounding concat
|
||||
// Plus tokens at the wrong precedence level.
|
||||
let inner_toks: [Map<String, Any>] = lex(expr_src)
|
||||
let inner_len: Int = native_list_len(inner_toks)
|
||||
|
||||
if need_plus {
|
||||
let out_tokens = native_list_append(out_tokens, make_tok("Plus", "+"))
|
||||
}
|
||||
// Empty interpolation ${} => empty string segment
|
||||
if inner_len <= 1 {
|
||||
let out_tokens = native_list_append(out_tokens, make_tok("Str", ""))
|
||||
} else {
|
||||
let out_tokens = native_list_append(out_tokens, make_tok("LParen", "("))
|
||||
let out_tokens = interp_tokens_append_all(out_tokens, inner_toks)
|
||||
let out_tokens = native_list_append(out_tokens, make_tok("RParen", ")"))
|
||||
}
|
||||
let need_plus = true
|
||||
} else {
|
||||
// Plain '$' not followed by '{' - treat as literal
|
||||
let cur_part = native_list_append(cur_part, "$")
|
||||
let i = i + 1
|
||||
}
|
||||
} else {
|
||||
let cur_part = native_list_append(cur_part, ch)
|
||||
let i = i + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Flush remaining literal segment and build final token list
|
||||
let part_text = str_join(cur_part, "")
|
||||
let part_len: Int = native_list_len(cur_part)
|
||||
if has_interp {
|
||||
// Interpolated string: only emit trailing segment if non-empty
|
||||
if part_len > 0 {
|
||||
let clean_part = part_text
|
||||
if looks_like_code(part_text) {
|
||||
let clean_part = strip_code_comments(part_text)
|
||||
}
|
||||
if need_plus {
|
||||
let out_tokens = native_list_append(out_tokens, make_tok("Plus", "+"))
|
||||
}
|
||||
let out_tokens = native_list_append(out_tokens, make_tok("Str", clean_part))
|
||||
}
|
||||
} else {
|
||||
// Plain string with no interpolation - same behaviour as old scan_string
|
||||
let clean_text = part_text
|
||||
if looks_like_code(part_text) {
|
||||
let clean_text = strip_code_comments(part_text)
|
||||
}
|
||||
let out_tokens = native_list_append(out_tokens, make_tok("Str", clean_text))
|
||||
}
|
||||
|
||||
{ "tokens": out_tokens, "pos": i }
|
||||
}
|
||||
|
||||
// -- Main lexer ----------------------------------------------------------------
|
||||
|
||||
fn lex(source: String) -> [Map<String, Any>] {
|
||||
let chars: [String] = native_string_chars(source)
|
||||
let total: Int = native_list_len(chars)
|
||||
let tokens: [Map<String, Any>] = native_list_empty()
|
||||
let i: Int = 0
|
||||
|
||||
while i < total {
|
||||
let ch: String = native_list_get(chars, i)
|
||||
|
||||
// Skip whitespace
|
||||
if lex_is_whitespace(ch) {
|
||||
let i = i + 1
|
||||
} else {
|
||||
// Line comments: //
|
||||
if ch == "/" {
|
||||
let next_i = i + 1
|
||||
if next_i < total {
|
||||
let next_ch: String = native_list_get(chars, next_i)
|
||||
if next_ch == "/" {
|
||||
// skip to end of line
|
||||
let i = i + 2
|
||||
let running2 = true
|
||||
while running2 {
|
||||
if i >= total {
|
||||
let running2 = false
|
||||
} else {
|
||||
let lch: String = native_list_get(chars, i)
|
||||
if lch == "\n" {
|
||||
let running2 = false
|
||||
} else {
|
||||
let i = i + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let tokens = native_list_append(tokens, make_tok("Slash", "/"))
|
||||
let i = i + 1
|
||||
}
|
||||
} else {
|
||||
let tokens = native_list_append(tokens, make_tok("Slash", "/"))
|
||||
let i = i + 1
|
||||
}
|
||||
} else {
|
||||
// String literal (plain or interpolated with ${expr} syntax).
|
||||
// scan_interp_string handles both cases: plain strings emit a
|
||||
// single Str token; interpolated strings emit a flat token
|
||||
// sequence (Str Plus expr-tokens Plus Str ...) that the parser
|
||||
// naturally assembles into a BinOp concat tree.
|
||||
if ch == "\"" {
|
||||
let interp_result = scan_interp_string(chars, i + 1, total)
|
||||
let interp_toks: [Map<String, Any>] = interp_result["tokens"]
|
||||
let new_pos: Int = interp_result["pos"]
|
||||
let tokens = interp_tokens_append_all(tokens, interp_toks)
|
||||
let i = new_pos
|
||||
} else {
|
||||
// Number literal
|
||||
if lex_is_digit(ch) {
|
||||
let result = scan_digits(chars, i, total)
|
||||
let num_text: String = result["text"]
|
||||
let new_pos: Int = result["pos"]
|
||||
// check for float (dot followed by digit)
|
||||
if new_pos < total {
|
||||
let dot_ch: String = native_list_get(chars, new_pos)
|
||||
if dot_ch == "." {
|
||||
let after_dot = new_pos + 1
|
||||
if after_dot < total {
|
||||
let after_dot_ch: String = native_list_get(chars, after_dot)
|
||||
if lex_is_digit(after_dot_ch) {
|
||||
let frac_result = scan_digits(chars, after_dot, total)
|
||||
let frac_text: String = frac_result["text"]
|
||||
let frac_pos: Int = frac_result["pos"]
|
||||
let tokens = native_list_append(tokens, make_tok("Float", num_text + "." + frac_text))
|
||||
let i = frac_pos
|
||||
} else {
|
||||
let tokens = native_list_append(tokens, make_tok("Int", num_text))
|
||||
let i = new_pos
|
||||
}
|
||||
} else {
|
||||
let tokens = native_list_append(tokens, make_tok("Int", num_text))
|
||||
let i = new_pos
|
||||
}
|
||||
} else {
|
||||
let tokens = native_list_append(tokens, make_tok("Int", num_text))
|
||||
let i = new_pos
|
||||
}
|
||||
} else {
|
||||
let tokens = native_list_append(tokens, make_tok("Int", num_text))
|
||||
let i = new_pos
|
||||
}
|
||||
} else {
|
||||
// Identifier or keyword
|
||||
if lex_is_alpha(ch) || ch == "_" {
|
||||
let result = scan_ident(chars, i, total)
|
||||
let word: String = result["text"]
|
||||
let new_pos: Int = result["pos"]
|
||||
let kw = keyword_kind(word)
|
||||
if kw == "" {
|
||||
let tokens = native_list_append(tokens, make_tok("Ident", word))
|
||||
} else {
|
||||
let tokens = native_list_append(tokens, make_tok(kw, word))
|
||||
}
|
||||
let i = new_pos
|
||||
} else {
|
||||
// Multi-char and single-char operators/delimiters
|
||||
let peek_i = i + 1
|
||||
let peek_ch = ""
|
||||
if peek_i < total {
|
||||
let peek_ch: String = native_list_get(chars, peek_i)
|
||||
}
|
||||
|
||||
if ch == "=" {
|
||||
if peek_ch == "=" {
|
||||
let tokens = native_list_append(tokens, make_tok("EqEq", "=="))
|
||||
let i = i + 2
|
||||
} else {
|
||||
if peek_ch == ">" {
|
||||
let tokens = native_list_append(tokens, make_tok("FatArrow", "=>"))
|
||||
let i = i + 2
|
||||
} else {
|
||||
let tokens = native_list_append(tokens, make_tok("Eq", "="))
|
||||
let i = i + 1
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if ch == "!" {
|
||||
if peek_ch == "=" {
|
||||
let tokens = native_list_append(tokens, make_tok("NotEq", "!="))
|
||||
let i = i + 2
|
||||
} else {
|
||||
let tokens = native_list_append(tokens, make_tok("Not", "!"))
|
||||
let i = i + 1
|
||||
}
|
||||
} else {
|
||||
if ch == "<" {
|
||||
if peek_ch == "=" {
|
||||
let tokens = native_list_append(tokens, make_tok("LtEq", "<="))
|
||||
let i = i + 2
|
||||
} else {
|
||||
let tokens = native_list_append(tokens, make_tok("Lt", "<"))
|
||||
let i = i + 1
|
||||
}
|
||||
} else {
|
||||
if ch == ">" {
|
||||
if peek_ch == "=" {
|
||||
let tokens = native_list_append(tokens, make_tok("GtEq", ">="))
|
||||
let i = i + 2
|
||||
} else {
|
||||
let tokens = native_list_append(tokens, make_tok("Gt", ">"))
|
||||
let i = i + 1
|
||||
}
|
||||
} else {
|
||||
if ch == "&" {
|
||||
if peek_ch == "&" {
|
||||
let tokens = native_list_append(tokens, make_tok("And", "&&"))
|
||||
let i = i + 2
|
||||
} else {
|
||||
let i = i + 1
|
||||
}
|
||||
} else {
|
||||
if ch == "|" {
|
||||
if peek_ch == "|" {
|
||||
let tokens = native_list_append(tokens, make_tok("Or", "||"))
|
||||
let i = i + 2
|
||||
} else {
|
||||
if peek_ch == ">" {
|
||||
let tokens = native_list_append(tokens, make_tok("PipeOp", "|>"))
|
||||
let i = i + 2
|
||||
} else {
|
||||
let tokens = native_list_append(tokens, make_tok("Pipe", "|"))
|
||||
let i = i + 1
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if ch == "-" {
|
||||
if peek_ch == ">" {
|
||||
let tokens = native_list_append(tokens, make_tok("Arrow", "->"))
|
||||
let i = i + 2
|
||||
} else {
|
||||
let tokens = native_list_append(tokens, make_tok("Minus", "-"))
|
||||
let i = i + 1
|
||||
}
|
||||
} else {
|
||||
if ch == ":" {
|
||||
if peek_ch == ":" {
|
||||
let tokens = native_list_append(tokens, make_tok("ColonColon", "::"))
|
||||
let i = i + 2
|
||||
} else {
|
||||
let tokens = native_list_append(tokens, make_tok("Colon", ":"))
|
||||
let i = i + 1
|
||||
}
|
||||
} else {
|
||||
if ch == "+" {
|
||||
let tokens = native_list_append(tokens, make_tok("Plus", "+"))
|
||||
let i = i + 1
|
||||
} else {
|
||||
if ch == "*" {
|
||||
let tokens = native_list_append(tokens, make_tok("Star", "*"))
|
||||
let i = i + 1
|
||||
} else {
|
||||
if ch == "%" {
|
||||
let tokens = native_list_append(tokens, make_tok("Percent", "%"))
|
||||
let i = i + 1
|
||||
} else {
|
||||
if ch == "(" {
|
||||
let tokens = native_list_append(tokens, make_tok("LParen", "("))
|
||||
let i = i + 1
|
||||
} else {
|
||||
if ch == ")" {
|
||||
let tokens = native_list_append(tokens, make_tok("RParen", ")"))
|
||||
let i = i + 1
|
||||
} else {
|
||||
if ch == "{" {
|
||||
let tokens = native_list_append(tokens, make_tok("LBrace", "{"))
|
||||
let i = i + 1
|
||||
} else {
|
||||
if ch == "}" {
|
||||
let tokens = native_list_append(tokens, make_tok("RBrace", "}"))
|
||||
let i = i + 1
|
||||
} else {
|
||||
if ch == "[" {
|
||||
let tokens = native_list_append(tokens, make_tok("LBracket", "["))
|
||||
let i = i + 1
|
||||
} else {
|
||||
if ch == "]" {
|
||||
let tokens = native_list_append(tokens, make_tok("RBracket", "]"))
|
||||
let i = i + 1
|
||||
} else {
|
||||
if ch == "," {
|
||||
let tokens = native_list_append(tokens, make_tok("Comma", ","))
|
||||
let i = i + 1
|
||||
} else {
|
||||
if ch == "." {
|
||||
// Check for ..= (inclusive range) before .. (exclusive range) before single .
|
||||
let peek2_i = i + 2
|
||||
let peek2_ch = ""
|
||||
if peek2_i < total {
|
||||
let peek2_ch: String = native_list_get(chars, peek2_i)
|
||||
}
|
||||
if peek_ch == "." {
|
||||
if peek2_ch == "=" {
|
||||
let tokens = native_list_append(tokens, make_tok("DotDotEq", "..="))
|
||||
let i = i + 3
|
||||
} else {
|
||||
let tokens = native_list_append(tokens, make_tok("DotDot", ".."))
|
||||
let i = i + 2
|
||||
}
|
||||
} else {
|
||||
let tokens = native_list_append(tokens, make_tok("Dot", "."))
|
||||
let i = i + 1
|
||||
}
|
||||
} else {
|
||||
if ch == ";" {
|
||||
let tokens = native_list_append(tokens, make_tok("Semicolon", ";"))
|
||||
let i = i + 1
|
||||
} else {
|
||||
if ch == "@" {
|
||||
let tokens = native_list_append(tokens, make_tok("At", "@"))
|
||||
let i = i + 1
|
||||
} else {
|
||||
if ch == "?" {
|
||||
let tokens = native_list_append(tokens, make_tok("QuestionMark", "?"))
|
||||
let i = i + 1
|
||||
} else {
|
||||
// unknown char - skip
|
||||
let i = i + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let tokens = native_list_append(tokens, make_tok("Eof", ""))
|
||||
tokens
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
+379
@@ -0,0 +1,379 @@
|
||||
// elb.el - El Build Coordinator
|
||||
//
|
||||
// The build system for El programs. Written in El. Builds El.
|
||||
//
|
||||
// Usage:
|
||||
// elb # build from manifest.el in current dir
|
||||
// elb --clean # remove generated artifacts and rebuild
|
||||
// elb --dry-run # print actions without executing
|
||||
// elb --jobs=N # parallel compile jobs (default: 4)
|
||||
// elb --out=DIR # output directory (default: dist)
|
||||
// elb --runtime=PATH # path to el_runtime.c
|
||||
//
|
||||
// How it works (the .NET model):
|
||||
// 1. Read manifest.el to find the entry file
|
||||
// 2. Walk the import graph depth-first, build topological order
|
||||
// 3. For each file: if .el is newer than .elh/.c, compile with elc --emit-header
|
||||
// 4. Link all .c files + el_runtime.c into the final binary
|
||||
//
|
||||
// Each module compiles independently - no 128K-line blobs.
|
||||
// Downstream compilations read .elh headers (function signatures only),
|
||||
// not source. Incremental: only recompile what changed.
|
||||
|
||||
// -- Flags ---------------------------------------------------------------------
|
||||
|
||||
fn flag_bool(argv: [String], name: String) -> Bool {
|
||||
let n: Int = native_list_len(argv)
|
||||
let i = 0
|
||||
while i < n {
|
||||
let a: String = native_list_get(argv, i)
|
||||
if str_eq(a, name) { return true }
|
||||
let i = i + 1
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
fn flag_val(argv: [String], name: String, default_val: String) -> String {
|
||||
let n: Int = native_list_len(argv)
|
||||
let prefix: String = name + "="
|
||||
let i = 0
|
||||
while i < n {
|
||||
let a: String = native_list_get(argv, i)
|
||||
if str_starts_with(a, prefix) {
|
||||
return str_slice(a, str_len(prefix), str_len(a))
|
||||
}
|
||||
let i = i + 1
|
||||
}
|
||||
return default_val
|
||||
}
|
||||
|
||||
// -- Manifest parsing ----------------------------------------------------------
|
||||
//
|
||||
// Read the entry file from manifest.el:
|
||||
// build { entry "soul.el" }
|
||||
|
||||
fn parse_manifest_entry(src: String) -> String {
|
||||
let lines: [String] = str_split(src, "\n")
|
||||
let n: Int = native_list_len(lines)
|
||||
let i = 0
|
||||
while i < n {
|
||||
let line: String = native_list_get(lines, i)
|
||||
let t: String = str_trim(line)
|
||||
if str_starts_with(t, "entry ") {
|
||||
// entry "soul.el"
|
||||
let after: String = str_slice(t, 6, str_len(t))
|
||||
let trimmed: String = str_trim(after)
|
||||
// strip surrounding quotes
|
||||
if str_starts_with(trimmed, "\"") {
|
||||
let inner: String = str_slice(trimmed, 1, str_len(trimmed))
|
||||
let q: Int = str_index_of(inner, "\"")
|
||||
if q >= 0 {
|
||||
return str_slice(inner, 0, q)
|
||||
}
|
||||
}
|
||||
}
|
||||
let i = i + 1
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
fn parse_manifest_name(src: String) -> String {
|
||||
let lines: [String] = str_split(src, "\n")
|
||||
let n: Int = native_list_len(lines)
|
||||
let i = 0
|
||||
while i < n {
|
||||
let line: String = native_list_get(lines, i)
|
||||
let t: String = str_trim(line)
|
||||
if str_starts_with(t, "package ") {
|
||||
let after: String = str_slice(t, 8, str_len(t))
|
||||
let trimmed: String = str_trim(after)
|
||||
if str_starts_with(trimmed, "\"") {
|
||||
let inner: String = str_slice(trimmed, 1, str_len(trimmed))
|
||||
let q: Int = str_index_of(inner, "\"")
|
||||
if q >= 0 {
|
||||
return str_slice(inner, 0, q)
|
||||
}
|
||||
}
|
||||
}
|
||||
let i = i + 1
|
||||
}
|
||||
return "out"
|
||||
}
|
||||
|
||||
// -- Path helpers ---------------------------------------------------------------
|
||||
|
||||
fn dirname_of(path: String) -> String {
|
||||
let n: Int = str_len(path)
|
||||
let i: Int = n - 1
|
||||
while i >= 0 {
|
||||
let c: String = str_slice(path, i, i + 1)
|
||||
if str_eq(c, "/") {
|
||||
return str_slice(path, 0, i)
|
||||
}
|
||||
let i = i - 1
|
||||
}
|
||||
return "."
|
||||
}
|
||||
|
||||
fn basename_noext(path: String) -> String {
|
||||
// strip directory
|
||||
let n: Int = str_len(path)
|
||||
let last_slash: Int = -1
|
||||
let i = 0
|
||||
while i < n {
|
||||
let c: String = str_slice(path, i, i + 1)
|
||||
if str_eq(c, "/") { let last_slash = i }
|
||||
let i = i + 1
|
||||
}
|
||||
let base: String = str_slice(path, last_slash + 1, n)
|
||||
// strip .el extension
|
||||
let bn: Int = str_len(base)
|
||||
if str_ends_with(base, ".el") {
|
||||
return str_slice(base, 0, bn - 3)
|
||||
}
|
||||
return base
|
||||
}
|
||||
|
||||
fn path_with_ext(path: String, ext: String) -> String {
|
||||
let n: Int = str_len(path)
|
||||
if str_ends_with(path, ".el") {
|
||||
return str_slice(path, 0, n - 3) + ext
|
||||
}
|
||||
return path + ext
|
||||
}
|
||||
|
||||
fn file_is_newer(a: String, b: String) -> Bool {
|
||||
// Returns true if file a is newer than file b, or if b doesn't exist.
|
||||
// Uses exec_capture with stat to compare modification times.
|
||||
let cmd: String = "test -f " + b + " && test " + a + " -nt " + b + " && echo yes || echo no"
|
||||
let result: String = str_trim(exec_capture(cmd))
|
||||
if str_eq(result, "yes") { return true }
|
||||
// b doesn't exist - check with test -f
|
||||
let exist_cmd: String = "test -f " + b + " && echo exists || echo missing"
|
||||
let exist: String = str_trim(exec_capture(exist_cmd))
|
||||
if str_eq(exist, "missing") { return true }
|
||||
return false
|
||||
}
|
||||
|
||||
// -- Import graph walker --------------------------------------------------------
|
||||
//
|
||||
// Walk import statements in each .el file to build the dependency graph.
|
||||
// Returns a list of absolute paths in topological order (deps before dependents).
|
||||
|
||||
fn parse_import_path(line: String, dir: String) -> String {
|
||||
let t: String = str_trim(line)
|
||||
if str_starts_with(t, "import \"") {
|
||||
let after: String = str_slice(t, 8, str_len(t))
|
||||
let q: Int = str_index_of(after, "\"")
|
||||
if q > 0 {
|
||||
let mod: String = str_slice(after, 0, q)
|
||||
return dir + "/" + mod
|
||||
}
|
||||
}
|
||||
if str_starts_with(t, "from ") {
|
||||
let after: String = str_slice(t, 5, str_len(t))
|
||||
let sp: Int = str_index_of(after, " ")
|
||||
if sp > 0 {
|
||||
let mod: String = str_trim(str_slice(after, 0, sp))
|
||||
if !str_eq(mod, "") {
|
||||
return dir + "/" + mod + ".el"
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
fn walk_imports(src_path: String, visited: [String], order: [String]) -> Map<String, Any> {
|
||||
// Dedup check
|
||||
let n: Int = native_list_len(visited)
|
||||
let i = 0
|
||||
while i < n {
|
||||
let v: String = native_list_get(visited, i)
|
||||
if str_eq(v, src_path) {
|
||||
return { "visited": visited, "order": order }
|
||||
}
|
||||
let i = i + 1
|
||||
}
|
||||
let visited = native_list_append(visited, src_path)
|
||||
|
||||
let source: String = fs_read(src_path)
|
||||
if str_eq(source, "") {
|
||||
return { "visited": visited, "order": order }
|
||||
}
|
||||
let dir: String = dirname_of(src_path)
|
||||
let lines: [String] = str_split(source, "\n")
|
||||
let ln: Int = native_list_len(lines)
|
||||
let j = 0
|
||||
while j < ln {
|
||||
let line: String = native_list_get(lines, j)
|
||||
let imp: String = parse_import_path(line, dir)
|
||||
if !str_eq(imp, "") {
|
||||
let r = walk_imports(imp, visited, order)
|
||||
let visited = r["visited"]
|
||||
let order = r["order"]
|
||||
}
|
||||
let j = j + 1
|
||||
}
|
||||
// Add self after all deps
|
||||
let order = native_list_append(order, src_path)
|
||||
return { "visited": visited, "order": order }
|
||||
}
|
||||
|
||||
// -- Build ----------------------------------------------------------------------
|
||||
|
||||
fn compile_module(src_path: String, out_dir: String, elc_bin: String, dry_run: Bool, verbose: Bool) -> Bool {
|
||||
let bname: String = basename_noext(src_path)
|
||||
let c_out: String = out_dir + "/" + bname + ".c"
|
||||
let elh_out: String = out_dir + "/" + bname + ".elh"
|
||||
|
||||
// Check if recompile needed
|
||||
if !file_is_newer(src_path, c_out) {
|
||||
if verbose {
|
||||
println(" skip " + bname + ".el (up to date)")
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// elc streams C to stdout (collect mode not yet implemented); use
|
||||
// shell redirection so the output lands in the file, not the terminal.
|
||||
let cmd: String = elc_bin + " --emit-header " + src_path + " > " + c_out + " 2>&1"
|
||||
println(" compile " + src_path)
|
||||
|
||||
if dry_run { return true }
|
||||
|
||||
let ret: Int = exec_command(cmd)
|
||||
if ret != 0 {
|
||||
println("elb: compile failed: " + src_path)
|
||||
return false
|
||||
}
|
||||
|
||||
// Move the generated .elh (written next to the source by elc) into
|
||||
// out_dir so that #include "module.elh" lines in the generated .c
|
||||
// files resolve correctly when cc is invoked with -I <out_dir>.
|
||||
let src_elh: String = path_with_ext(src_path, ".elh")
|
||||
let mv_cmd: String = "cp " + src_elh + " " + elh_out + " 2>/dev/null || true"
|
||||
exec_command(mv_cmd)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
fn link_binary(c_files: [String], out_bin: String, runtime_path: String, out_dir: String, dry_run: Bool) -> Bool {
|
||||
let n: Int = native_list_len(c_files)
|
||||
let parts: [String] = native_list_empty()
|
||||
// Include both the runtime dir (for el_runtime.h) and the output dir
|
||||
// (for module.elh cross-module forward declarations).
|
||||
let parts = native_list_append(parts, "cc -O2 -fbracket-depth=1024 -I " + dirname_of(runtime_path) + " -I " + out_dir)
|
||||
let i = 0
|
||||
while i < n {
|
||||
let f: String = native_list_get(c_files, i)
|
||||
let parts = native_list_append(parts, f)
|
||||
let i = i + 1
|
||||
}
|
||||
let parts = native_list_append(parts, runtime_path)
|
||||
let parts = native_list_append(parts, "-lcurl -lpthread -lm")
|
||||
let parts = native_list_append(parts, "-o " + out_bin)
|
||||
let cmd: String = str_join(parts, " ")
|
||||
println(" link " + out_bin)
|
||||
if dry_run { return true }
|
||||
let ret: Int = exec_command(cmd)
|
||||
if ret != 0 {
|
||||
println("elb: link failed")
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// -- Main -----------------------------------------------------------------------
|
||||
|
||||
fn main() -> Void {
|
||||
let argv: [String] = args()
|
||||
let clean: Bool = flag_bool(argv, "--clean")
|
||||
let dry_run: Bool = flag_bool(argv, "--dry-run")
|
||||
let verbose: Bool = flag_bool(argv, "--verbose")
|
||||
let out_dir: String = flag_val(argv, "--out", "dist")
|
||||
let elc_bin: String = flag_val(argv, "--elc", "elc")
|
||||
let runtime: String = flag_val(argv, "--runtime", "")
|
||||
|
||||
// Find manifest
|
||||
let manifest_src: String = fs_read("manifest.el")
|
||||
if str_eq(manifest_src, "") {
|
||||
println("elb: no manifest.el found in current directory")
|
||||
exit(1)
|
||||
}
|
||||
|
||||
let pkg_name: String = parse_manifest_name(manifest_src)
|
||||
let entry: String = parse_manifest_entry(manifest_src)
|
||||
if str_eq(entry, "") {
|
||||
println("elb: manifest.el has no 'entry' declaration")
|
||||
exit(1)
|
||||
}
|
||||
|
||||
println("elb: building " + pkg_name + " (entry: " + entry + ")")
|
||||
|
||||
// Locate runtime
|
||||
let runtime_path: String = runtime
|
||||
if str_eq(runtime_path, "") {
|
||||
// Try to find el_runtime.c relative to elc binary
|
||||
let which_out: String = str_trim(exec_capture("which " + elc_bin + " 2>/dev/null"))
|
||||
if !str_eq(which_out, "") {
|
||||
let elc_dir: String = dirname_of(which_out)
|
||||
runtime_path = elc_dir + "/../el-compiler/runtime/el_runtime.c"
|
||||
}
|
||||
}
|
||||
if str_eq(runtime_path, "") {
|
||||
println("elb: cannot locate el_runtime.c - use --runtime=PATH")
|
||||
exit(1)
|
||||
}
|
||||
|
||||
// Ensure output directory
|
||||
let mkdir_ret: Int = exec_command("mkdir -p " + out_dir)
|
||||
|
||||
// Clean if requested
|
||||
if clean {
|
||||
println("elb: cleaning " + out_dir)
|
||||
if !dry_run {
|
||||
let rm_ret: Int = exec_command("rm -f " + out_dir + "/*.c " + out_dir + "/*.elh")
|
||||
}
|
||||
}
|
||||
|
||||
// Walk import graph from entry file
|
||||
let empty_visited: [String] = native_list_empty()
|
||||
let empty_order: [String] = native_list_empty()
|
||||
let r = walk_imports(entry, empty_visited, empty_order)
|
||||
let order: [String] = r["order"]
|
||||
let total: Int = native_list_len(order)
|
||||
println("elb: " + native_int_to_str(total) + " modules in build graph")
|
||||
|
||||
// Compile each module
|
||||
let c_files: [String] = native_list_empty()
|
||||
let i = 0
|
||||
let ok = true
|
||||
while i < total {
|
||||
let src: String = native_list_get(order, i)
|
||||
let bname: String = basename_noext(src)
|
||||
let c_out: String = out_dir + "/" + bname + ".c"
|
||||
let compiled: Bool = compile_module(src, out_dir, elc_bin, dry_run, verbose)
|
||||
if !compiled {
|
||||
let ok = false
|
||||
let i = total
|
||||
} else {
|
||||
let c_files = native_list_append(c_files, c_out)
|
||||
}
|
||||
let i = i + 1
|
||||
}
|
||||
|
||||
if !ok {
|
||||
println("elb: build failed")
|
||||
exit(1)
|
||||
}
|
||||
|
||||
// Link
|
||||
let out_bin: String = out_dir + "/" + pkg_name
|
||||
let linked: Bool = link_binary(c_files, out_bin, runtime_path, out_dir, dry_run)
|
||||
if !linked {
|
||||
println("elb: link failed")
|
||||
exit(1)
|
||||
}
|
||||
|
||||
println("elb: done -> " + out_bin)
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
// elc-cli.el — entry point for the self-hosted el compiler.
|
||||
//
|
||||
// All logic lives in el-compiler/src/compiler.el (which defines fn main()).
|
||||
// Importing it pulls in compiler.el + parser + lexer + codegen transitively.
|
||||
// The native compiler resolves imports textually and folds fn main() into
|
||||
// C's int main(), so this file does not declare any top-level work itself.
|
||||
import "el-compiler/src/compiler.el"
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,54 @@
|
||||
// activate.el — The `activate` construct: spreading activation over the type graph.
|
||||
//
|
||||
// The `activate` keyword is the most novel feature of the Engram language.
|
||||
// Instead of querying a database with SQL or a key lookup, you query the
|
||||
// *type graph* using a natural language description. The result is
|
||||
// statically typed — the compiler knows you get [User] back, not Any.
|
||||
//
|
||||
// How it works:
|
||||
//
|
||||
// 1. At compile time, the type checker verifies that `User` is a registered
|
||||
// type with a corresponding Engram node mapping.
|
||||
//
|
||||
// 2. At runtime, the Engram runtime performs spreading activation over
|
||||
// the knowledge graph, starting from the User node, propagating to
|
||||
// semantically related nodes with co-activation energy proportional
|
||||
// to edge weights and node salience.
|
||||
//
|
||||
// 3. The query string "customer who purchased recently" is embedded
|
||||
// and used as a semantic filter: only nodes whose embeddings are
|
||||
// within cosine-similarity threshold of the query embedding are returned.
|
||||
//
|
||||
// 4. The result is type-safe: [User]. You don't get back Any or a JSON blob.
|
||||
// The type checker enforces this at compile time.
|
||||
|
||||
type User {
|
||||
id: Uuid
|
||||
name: String
|
||||
email: String
|
||||
}
|
||||
|
||||
type Order {
|
||||
id: Uuid
|
||||
user_id: Uuid
|
||||
amount: Float
|
||||
status: String
|
||||
}
|
||||
|
||||
// Query: find all users who are semantically related to "customer who purchased recently"
|
||||
// The result type is [User] — an array of User nodes from the knowledge graph.
|
||||
let recent_buyers: [User] = activate User where "customer who purchased recently"
|
||||
|
||||
// You can also activate on other types:
|
||||
let pending_orders: [Order] = activate Order where "order awaiting fulfillment"
|
||||
|
||||
// Spreading activation respects the type graph edges.
|
||||
// If User and Customer share an Engram node type (e.g. "Entity"),
|
||||
// they are semantically compatible — the type checker allows
|
||||
// treating one as the other when they map to the same Engram node class.
|
||||
|
||||
fn process_buyers(users: [User]) -> String {
|
||||
return "processing users"
|
||||
}
|
||||
|
||||
let result: String = process_buyers(recent_buyers)
|
||||
@@ -0,0 +1,200 @@
|
||||
// browser-auth.el -- El-compiled auth flow using Supabase
|
||||
//
|
||||
// Compile: elc --target=js --bundle examples/browser-auth.el > auth.js
|
||||
// (requires el_runtime.js in the same directory as browser-auth.el)
|
||||
//
|
||||
// Demonstrates:
|
||||
// - extern fn for declaring Supabase client constructor
|
||||
// - anonymous function literals for callbacks
|
||||
// - method call syntax on Any-typed values (client.auth.signInWithOtp)
|
||||
// - try/catch for error handling
|
||||
// - @async functions with DOM interaction
|
||||
// - DOM bridge: dom_get_element, dom_get_value, dom_set_text, dom_add_class
|
||||
// dom_remove_class, dom_show, dom_hide, dom_is_null
|
||||
// - window_set to expose El functions to the browser global scope
|
||||
// - local_storage_set/get for session hints
|
||||
// - set_timeout for transient UI state
|
||||
// - state_set/get for component state
|
||||
//
|
||||
// Expected HTML elements:
|
||||
// #acct-email-input -- email text input
|
||||
// #send-link-btn -- submit button
|
||||
// #auth-message -- status message container
|
||||
// #auth-form -- the form to hide after success
|
||||
//
|
||||
// The Supabase JS SDK is loaded from CDN via a <script> tag before auth.js.
|
||||
// supabase_create_client is declared extern: the runtime provides it via
|
||||
// the global supabase.createClient function exposed by the CDN bundle.
|
||||
|
||||
// ── External declarations ─────────────────────────────────────────────────
|
||||
//
|
||||
// These functions are provided by the JS environment (CDN script tags).
|
||||
// No body is emitted -- the compiler just records the names.
|
||||
|
||||
extern fn supabase_create_client(url: String, key: String) -> Any
|
||||
|
||||
// ── UI helpers ─────────────────────────────────────────────────────────────
|
||||
|
||||
fn show_message(text: String, is_error: Bool) -> Void {
|
||||
let msg_el = dom_get_element("auth-message")
|
||||
if !dom_is_null(msg_el) {
|
||||
dom_set_text(msg_el, text)
|
||||
dom_remove_class(msg_el, "hidden")
|
||||
if is_error {
|
||||
dom_add_class(msg_el, "error")
|
||||
dom_remove_class(msg_el, "success")
|
||||
} else {
|
||||
dom_add_class(msg_el, "success")
|
||||
dom_remove_class(msg_el, "error")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn set_button_loading(loading: Bool) -> Void {
|
||||
let btn = dom_get_element("send-link-btn")
|
||||
if !dom_is_null(btn) {
|
||||
if loading {
|
||||
dom_set_text(btn, "Sending...")
|
||||
dom_set_attr(btn, "disabled", "true")
|
||||
} else {
|
||||
dom_set_text(btn, "Send Magic Link")
|
||||
dom_remove_attr(btn, "disabled")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn clear_message() -> Void {
|
||||
let msg_el = dom_get_element("auth-message")
|
||||
if !dom_is_null(msg_el) {
|
||||
dom_add_class(msg_el, "hidden")
|
||||
dom_set_text(msg_el, "")
|
||||
}
|
||||
}
|
||||
|
||||
// ── Email validation ───────────────────────────────────────────────────────
|
||||
|
||||
fn is_valid_email(email: String) -> Bool {
|
||||
let trimmed: String = str_trim(email)
|
||||
if str_len(trimmed) < 5 { return false }
|
||||
let at_pos: Int = str_index_of(trimmed, "@")
|
||||
if at_pos < 1 { return false }
|
||||
let dot_pos: Int = str_index_of(trimmed, ".")
|
||||
if dot_pos < at_pos + 2 { return false }
|
||||
return true
|
||||
}
|
||||
|
||||
// ── Supabase client construction ──────────────────────────────────────────
|
||||
//
|
||||
// Build a Supabase client from config injected into the page as NEURON_CFG.
|
||||
// The extern fn supabase_create_client maps to supabase.createClient on
|
||||
// the global object exposed by the CDN bundle.
|
||||
|
||||
fn get_supabase_client() -> Any {
|
||||
let cfg = window_get("NEURON_CFG")
|
||||
if dom_is_null(cfg) {
|
||||
return null
|
||||
}
|
||||
let url: String = cfg["supabaseUrl"]
|
||||
let key: String = cfg["supabaseAnonKey"]
|
||||
supabase_create_client(url, key)
|
||||
}
|
||||
|
||||
// ── Auth flow ──────────────────────────────────────────────────────────────
|
||||
|
||||
@async
|
||||
fn send_magic_link() -> Void {
|
||||
let email_el = dom_get_element("acct-email-input")
|
||||
if dom_is_null(email_el) {
|
||||
show_message("Could not find email input", true)
|
||||
return null
|
||||
}
|
||||
|
||||
let email: String = str_trim(dom_get_value(email_el))
|
||||
|
||||
if !is_valid_email(email) {
|
||||
show_message("Please enter a valid email address", true)
|
||||
return null
|
||||
}
|
||||
|
||||
clear_message()
|
||||
set_button_loading(true)
|
||||
state_set("auth_email", email)
|
||||
|
||||
// Build the Supabase client and call auth.signInWithOtp directly.
|
||||
// Method call syntax on Any-typed values: client.auth.signInWithOtp(opts)
|
||||
// No native_js_call required.
|
||||
let client = get_supabase_client()
|
||||
if dom_is_null(client) {
|
||||
show_message("Auth service not configured", true)
|
||||
set_button_loading(false)
|
||||
return null
|
||||
}
|
||||
|
||||
try {
|
||||
let opts: Map<String, Any> = { "email": email }
|
||||
// client is Any-typed; .auth returns the auth sub-client (also Any).
|
||||
// .signInWithOtp(opts) returns a Promise. @async + await handles it.
|
||||
let resp = client.auth.signInWithOtp(opts)
|
||||
let err = resp["error"]
|
||||
if !dom_is_null(err) {
|
||||
let msg: String = err["message"]
|
||||
show_message("Error: " + msg, true)
|
||||
} else {
|
||||
local_storage_set("auth_pending_email", email)
|
||||
show_message("Magic link sent! Check your inbox for " + email, false)
|
||||
let form = dom_get_element("auth-form")
|
||||
if !dom_is_null(form) {
|
||||
dom_hide(form)
|
||||
}
|
||||
}
|
||||
} catch (err: Any) {
|
||||
show_message("Unexpected error. Please try again.", true)
|
||||
}
|
||||
|
||||
set_button_loading(false)
|
||||
}
|
||||
|
||||
// ── Keyboard support ───────────────────────────────────────────────────────
|
||||
|
||||
fn handle_email_keydown(event: Any) -> Void {
|
||||
let key: String = dom_get_prop(event, "key")
|
||||
if str_eq(key, "Enter") {
|
||||
send_magic_link()
|
||||
}
|
||||
}
|
||||
|
||||
// ── Initialization ─────────────────────────────────────────────────────────
|
||||
|
||||
fn init_auth() -> Void {
|
||||
let email_el = dom_get_element("acct-email-input")
|
||||
if !dom_is_null(email_el) {
|
||||
// Pre-fill from local storage if a pending send was interrupted.
|
||||
let pending: String = local_storage_get("auth_pending_email")
|
||||
if !str_eq(pending, "") {
|
||||
dom_set_value(email_el, pending)
|
||||
}
|
||||
// Anonymous function literal for inline event handler.
|
||||
dom_listen(email_el, "keydown", fn(event: Any) -> Void {
|
||||
let key: String = dom_get_prop(event, "key")
|
||||
if str_eq(key, "Enter") {
|
||||
send_magic_link()
|
||||
}
|
||||
})
|
||||
}
|
||||
let btn = dom_get_element("send-link-btn")
|
||||
if !dom_is_null(btn) {
|
||||
dom_listen(btn, "click", fn(event: Any) -> Void {
|
||||
send_magic_link()
|
||||
})
|
||||
}
|
||||
state_set("auth_initialized", "true")
|
||||
}
|
||||
|
||||
fn main() -> Void {
|
||||
// Expose send_magic_link globally so inline event handlers can call it.
|
||||
window_set("sendMagicLink", send_magic_link)
|
||||
window_set("initAuth", init_auth)
|
||||
|
||||
// Run init when DOM is ready.
|
||||
window_on_load(init_auth)
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
// browser-counter.el — canonical browser DOM bridge example
|
||||
//
|
||||
// Compile with: elc --target=js examples/browser-counter.el > counter.js
|
||||
//
|
||||
// Then include in an HTML page that has a <span id="count-display"> element.
|
||||
// The page can call window.increment() from any onclick handler, e.g.:
|
||||
// <button onclick="increment()">+1</button>
|
||||
//
|
||||
// On load the display is initialised to "0". Each call to increment()
|
||||
// adds 1 and updates the display text.
|
||||
//
|
||||
// Demonstrates:
|
||||
// - dom_get_element to locate a DOM node by id
|
||||
// - dom_set_text to update visible text content
|
||||
// - dom_is_null to guard against missing elements
|
||||
// - window_set to expose an El function for inline event handlers
|
||||
// - state_set/get for in-memory counter state (survives calls, resets
|
||||
// on page reload — same semantics as the C state_* API)
|
||||
|
||||
fn init() -> Void {
|
||||
state_set("counter", 0)
|
||||
let display = dom_get_element("count-display")
|
||||
if !dom_is_null(display) {
|
||||
dom_set_text(display, "0")
|
||||
}
|
||||
}
|
||||
|
||||
fn increment() -> Void {
|
||||
let current = str_to_int(state_get("counter"))
|
||||
let next = current + 1
|
||||
state_set("counter", next)
|
||||
let display = dom_get_element("count-display")
|
||||
if !dom_is_null(display) {
|
||||
dom_set_text(display, int_to_str(next))
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> Void {
|
||||
init()
|
||||
window_set("increment", increment)
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
// Engram language — example test file
|
||||
// Run with: el test-file examples/hello-project/src/tests.el
|
||||
|
||||
test "basic arithmetic" {
|
||||
let x: Int = 6
|
||||
let y: Int = 7
|
||||
let result: Int = x * y
|
||||
assert result == 42
|
||||
}
|
||||
|
||||
test "string operations" {
|
||||
let greeting: String = "Hello"
|
||||
let name: String = "Engram"
|
||||
let full: String = greeting + ", " + name
|
||||
assert full == "Hello, Engram"
|
||||
}
|
||||
|
||||
test "type graph has Point type" {
|
||||
seed Node {
|
||||
type: "Point"
|
||||
content: "A 2D coordinate"
|
||||
importance: 0.8
|
||||
tier: Semantic
|
||||
}
|
||||
|
||||
let results = activate Point where "coordinate"
|
||||
assert results.len() > 0
|
||||
}
|
||||
|
||||
test "empty graph" {
|
||||
let results = activate Customer where "anything"
|
||||
assert results.len() == 0
|
||||
}
|
||||
|
||||
test "empty graph returns insufficient" {
|
||||
let result = reason("Is there a customer named Will?")
|
||||
assert result.verdict == "Insufficient"
|
||||
}
|
||||
|
||||
test "seeded graph reasoning" {
|
||||
seed Node {
|
||||
type: "Customer"
|
||||
content: "Will Anderson, founding member"
|
||||
importance: 0.9
|
||||
tier: Semantic
|
||||
}
|
||||
|
||||
let result = reason("founding member")
|
||||
assert result.verdict == "Supported"
|
||||
}
|
||||
|
||||
test "production customer lookup" target: e2e {
|
||||
// No seeds — uses the real Engram database
|
||||
// (skipped when ENGRAM_URL is not set)
|
||||
let results = activate Customer where "founding member"
|
||||
assert results.len() > 0
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
// hello.el — The simplest Engram program.
|
||||
// Run with: el run hello.el
|
||||
|
||||
fn greet(name: String) -> String {
|
||||
return "Hello, " + name
|
||||
}
|
||||
|
||||
let message: String = greet("World")
|
||||
@@ -0,0 +1,40 @@
|
||||
// html-page.el — Example of native HTML template syntax in El.
|
||||
//
|
||||
// El HTML templates let you write HTML directly in expression position.
|
||||
// Interpolated values are automatically HTML-escaped.
|
||||
// Use raw(expr) to bypass escaping when you know the content is safe.
|
||||
//
|
||||
// Compile and run:
|
||||
// ./dist/platform/elc examples/html-page.el > /tmp/html-page.c
|
||||
// cc -std=c11 -I el-compiler/runtime -lcurl -lpthread \
|
||||
// -o /tmp/html-page /tmp/html-page.c el-compiler/runtime/el_runtime.c
|
||||
// /tmp/html-page
|
||||
|
||||
fn render_item(item: String) -> String {
|
||||
return <li class="item">{item}</li>
|
||||
}
|
||||
|
||||
fn render_page(title: String, items: [String]) -> String {
|
||||
return <!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>{title}</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>{title}</h1>
|
||||
<ul>
|
||||
{#each items as item}
|
||||
<li class="item">{item}</li>
|
||||
{/each}
|
||||
</ul>
|
||||
<p>Built with El HTML templates</p>
|
||||
</body>
|
||||
</html>
|
||||
}
|
||||
|
||||
fn main() -> Void {
|
||||
let items: [String] = ["Lexer", "Parser", "Codegen", "Runtime"]
|
||||
let page: String = render_page("El Compiler Stages", items)
|
||||
println(page)
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
// http-status-envelope.el — acceptance test for the __status__ HTTP envelope.
|
||||
//
|
||||
// Before fix: a handler returning {"__status__":401,"error":"unauthorized"}
|
||||
// went out as an HTTP 200 with the JSON body verbatim, so Cloud Run logs were
|
||||
// full of 200s for what should have been 4xx/5xx.
|
||||
//
|
||||
// After fix: when the response body's FIRST key is __status__, the runtime
|
||||
// reads the integer value as the HTTP status code and strips the marker from
|
||||
// the body before sending it to the client.
|
||||
//
|
||||
// Verify with curl:
|
||||
// curl -i http://localhost:8081/auth -> HTTP/1.1 401 Unauthorized
|
||||
// curl -i http://localhost:8081/health -> HTTP/1.1 200 OK
|
||||
// curl -i http://localhost:8081/oops -> HTTP/1.1 503 Service Unavailable
|
||||
|
||||
fn handle(method: String, path: String, body: String) -> String {
|
||||
if path == "/auth" {
|
||||
return "{\"__status__\":401,\"error\":\"unauthorized\"}"
|
||||
}
|
||||
if path == "/oops" {
|
||||
return "{\"__status__\":503,\"error\":\"degraded\"}"
|
||||
}
|
||||
if path == "/health" {
|
||||
return "{\"ok\":true}"
|
||||
}
|
||||
return "{\"__status__\":404,\"error\":\"not found\"}"
|
||||
}
|
||||
|
||||
fn main() -> Int {
|
||||
http_set_handler("handle")
|
||||
http_serve(8081, "handle")
|
||||
return 0
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
// json-array-traversal.el — acceptance test for json_get dot-path with array
|
||||
// indices.
|
||||
//
|
||||
// Before fix: json_get("...", "0.field") would substring-search for a literal
|
||||
// key named `"0.field"` and find nothing, returning "".
|
||||
//
|
||||
// After fix: dot-path segments that are all digits are treated as array
|
||||
// indices and the walker descends into the array.
|
||||
|
||||
fn test_array_traversal() -> String {
|
||||
let s: String = "[{\"name\":\"alice\"},{\"name\":\"bob\"}]"
|
||||
let a: String = json_get(s, "0.name")
|
||||
let b: String = json_get(s, "1.name")
|
||||
return a + "," + b
|
||||
}
|
||||
|
||||
fn main() -> Int {
|
||||
let r: String = test_array_traversal()
|
||||
print(r)
|
||||
return 0
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
// reassign-in-if.el — acceptance test for bare reassignment inside if-body.
|
||||
//
|
||||
// Before fix: parser dropped `x = "override"` on the floor and codegen emitted
|
||||
// three orphan expressions (`x; EL_NULL; EL_STR("override");`). Effective store
|
||||
// was lost, so the function returned "default".
|
||||
//
|
||||
// After fix: parse_stmt recognises `Ident "=" Expr` as an Assign statement and
|
||||
// codegen emits a real C assignment, so the function returns "override".
|
||||
|
||||
fn test_reassign() -> String {
|
||||
let x: String = "default"
|
||||
if true {
|
||||
x = "override"
|
||||
}
|
||||
return x
|
||||
}
|
||||
|
||||
fn main() -> Int {
|
||||
let r: String = test_reassign()
|
||||
print(r)
|
||||
return 0
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
// types.el — Type definitions and pattern matching.
|
||||
//
|
||||
// Demonstrates:
|
||||
// - type (struct) definitions
|
||||
// - enum definitions with payloads
|
||||
// - match expressions with enum patterns
|
||||
|
||||
// A user-defined struct type.
|
||||
// Every field is typed; the type name maps to an Engram graph node.
|
||||
type User {
|
||||
id: Uuid
|
||||
name: String
|
||||
email: String
|
||||
}
|
||||
|
||||
// An enum with a payload variant.
|
||||
enum Status {
|
||||
Active
|
||||
Inactive
|
||||
Pending(String)
|
||||
}
|
||||
|
||||
// A function that takes a Status and returns a human-readable string.
|
||||
fn describe_status(s: Status) -> String {
|
||||
return match s {
|
||||
Status::Active => "user is active"
|
||||
Status::Inactive => "user is inactive"
|
||||
Status::Pending(reason) => "pending: " + reason
|
||||
}
|
||||
}
|
||||
|
||||
// Sealed blocks protect sensitive values even in debug builds.
|
||||
// The runtime enforces that values in sealed blocks are never
|
||||
// observable through a debugger or memory dump.
|
||||
sealed {
|
||||
let api_key: String = "sk-prod-12345"
|
||||
}
|
||||
|
||||
let status: Status = Status::Pending("email not verified")
|
||||
let description: String = describe_status(status)
|
||||
@@ -0,0 +1,80 @@
|
||||
#!/usr/bin/env bash
|
||||
# install.sh — Install the El SDK from the latest Gitea release.
|
||||
#
|
||||
# Usage:
|
||||
# bash install.sh
|
||||
# EL_VERSION=v1.0.0 bash install.sh # pin a specific release tag
|
||||
# EL_PREFIX=/opt/el bash install.sh # custom install prefix
|
||||
#
|
||||
# Environment variables:
|
||||
# EL_VERSION Release tag to download (default: latest)
|
||||
# EL_PREFIX Install prefix (default: /usr/local)
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
REPO_BASE="https://git.neuralplatform.ai/neuron-technologies/el"
|
||||
VERSION="${EL_VERSION:-latest}"
|
||||
PREFIX="${EL_PREFIX:-/usr/local}"
|
||||
|
||||
BIN_DIR="${PREFIX}/bin"
|
||||
LIB_DIR="${PREFIX}/lib/el"
|
||||
|
||||
RELEASE_BASE="${REPO_BASE}/releases/download/${VERSION}"
|
||||
|
||||
echo "==> Installing El SDK ${VERSION}"
|
||||
echo " prefix : ${PREFIX}"
|
||||
echo " bin : ${BIN_DIR}"
|
||||
echo " lib : ${LIB_DIR}"
|
||||
echo
|
||||
|
||||
# Create directories
|
||||
mkdir -p "${BIN_DIR}" "${LIB_DIR}"
|
||||
|
||||
# Download helper
|
||||
download() {
|
||||
local url="$1"
|
||||
local dest="$2"
|
||||
echo " Downloading $(basename "${dest}")..."
|
||||
if command -v curl >/dev/null 2>&1; then
|
||||
curl -fsSL "${url}" -o "${dest}"
|
||||
elif command -v wget >/dev/null 2>&1; then
|
||||
wget -q "${url}" -O "${dest}"
|
||||
else
|
||||
echo "Error: neither curl nor wget found" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Download assets
|
||||
TMP_DIR="$(mktemp -d)"
|
||||
trap 'rm -rf "${TMP_DIR}"' EXIT
|
||||
|
||||
download "${RELEASE_BASE}/elc" "${TMP_DIR}/elc"
|
||||
download "${RELEASE_BASE}/el_runtime.c" "${TMP_DIR}/el_runtime.c"
|
||||
download "${RELEASE_BASE}/el_runtime.h" "${TMP_DIR}/el_runtime.h"
|
||||
|
||||
# Install
|
||||
install -m 755 "${TMP_DIR}/elc" "${BIN_DIR}/elc"
|
||||
install -m 644 "${TMP_DIR}/el_runtime.c" "${LIB_DIR}/el_runtime.c"
|
||||
install -m 644 "${TMP_DIR}/el_runtime.h" "${LIB_DIR}/el_runtime.h"
|
||||
|
||||
echo
|
||||
echo "==> El SDK installed successfully"
|
||||
echo
|
||||
echo " elc binary : ${BIN_DIR}/elc"
|
||||
echo " runtime : ${LIB_DIR}/el_runtime.c"
|
||||
echo " header : ${LIB_DIR}/el_runtime.h"
|
||||
echo
|
||||
echo "Add the following to your Makefile to build El programs:"
|
||||
echo
|
||||
echo " EL_LIB := ${LIB_DIR}"
|
||||
echo " ELC := elc"
|
||||
echo " CC := cc"
|
||||
echo " CFLAGS := -std=c11 -O2 -I\$(EL_LIB)"
|
||||
echo
|
||||
echo " dist/myapp.c: src/myapp.el"
|
||||
echo " \t\$(ELC) src/myapp.el > dist/myapp.c"
|
||||
echo
|
||||
echo " dist/myapp: dist/myapp.c"
|
||||
echo " \t\$(CC) \$(CFLAGS) -o dist/myapp dist/myapp.c \$(EL_LIB)/el_runtime.c -lcurl -lpthread"
|
||||
echo
|
||||
@@ -0,0 +1,28 @@
|
||||
# El Compiler Release v1.0.0 — 2026-05-02
|
||||
|
||||
## Components
|
||||
- `bootstrap.py` — El language compiler (Python, recursive descent parser, emits C)
|
||||
- `el_runtime.c` — El runtime (C, HTTP server, engram, DHARMA, LLM chain)
|
||||
- `el_runtime.h` — Runtime public API header
|
||||
|
||||
## Changes in this release
|
||||
|
||||
### Critical bug fixes
|
||||
- `state_set`/`state_get` are now thread-safe (pthread_mutex). Was racing across 64 worker threads.
|
||||
- `looks_like_string` threshold raised from 1,000,000 to 4GB. Unix timestamps were being dereferenced as heap pointers.
|
||||
- `fs_read` guards against negative `ftell` result (pipe/special file overflow).
|
||||
|
||||
### Engram architecture (major)
|
||||
- Two-layer activation: `background_activation` (Layer 1, broad fan-out) + `working_memory_weight` (Layer 2, executive filter)
|
||||
- Inhibitory edges: `EngramEdge.inhibitory` flag suppresses working memory promotion without affecting background activation
|
||||
- Suppression memory: `suppression_count` — nodes activated-but-suppressed accumulate pressure toward breakthrough
|
||||
- Temporal decay: `temporal_decay_rate`, `created_at`, `last_activated_at`, `activation_count` on EngramNode
|
||||
- Per-type activation thresholds (Safety: 0.05, Canonical: 0.15, Lesson: 0.25, Note: 0.40)
|
||||
- Temporal range query: `engram_query_range(start_ms, end_ms)`
|
||||
- Layered consciousness: `EngramLayer` struct, `layer_id` on nodes and edges, `EngramStore.layers[]`
|
||||
- Layer 0 override pass: safety layer fires last and cannot be suppressed
|
||||
|
||||
## SHA256
|
||||
bootstrap.py
|
||||
el_runtime.c
|
||||
el_runtime.h
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,756 @@
|
||||
/*
|
||||
* el_runtime.h — El language C runtime header
|
||||
*
|
||||
* Declares all built-in functions available to compiled El programs.
|
||||
* Include this in every generated .c file.
|
||||
*
|
||||
* Value model:
|
||||
* All El values are represented as el_val_t (= int64_t).
|
||||
* On 64-bit systems a pointer fits in int64_t.
|
||||
* String values are cast: (el_val_t)(uintptr_t)"hello"
|
||||
* Integer values are stored directly.
|
||||
* This lets arithmetic work naturally while still passing strings around.
|
||||
*
|
||||
* Type conventions (El -> C):
|
||||
* String -> el_val_t (holds const char* via uintptr_t cast)
|
||||
* Int -> el_val_t
|
||||
* Bool -> el_val_t (0 = false, nonzero = true)
|
||||
* Any -> el_val_t
|
||||
* Void -> void
|
||||
*
|
||||
* Macros for convenience:
|
||||
* EL_STR(s) cast string literal to el_val_t
|
||||
* EL_CSTR(v) cast el_val_t back to const char*
|
||||
* EL_INT(v) identity — el_val_t is already int64_t
|
||||
*
|
||||
* Link requirements:
|
||||
* -lcurl — required for the HTTP client (http_get, http_post, llm_*).
|
||||
* -lpthread — required for the HTTP server (one detached thread per
|
||||
* connection, capped at 64 concurrent).
|
||||
* -loqs — optional; required only when liboqs is installed and the
|
||||
* pq_* / sha3_256_hex entry points are needed. Detected at
|
||||
* compile time via __has_include(<oqs/oqs.h>).
|
||||
* -lcrypto — optional; pulled in alongside -loqs. Used for X25519 in
|
||||
* pq_hybrid_* and HKDF-SHA256 derivation.
|
||||
*
|
||||
* Canonical compile command:
|
||||
* cc -std=c11 -I el-compiler/runtime -lcurl -lpthread \
|
||||
* -o <out> <prog>.c el-compiler/runtime/el_runtime.c
|
||||
*
|
||||
* With liboqs (post-quantum stack):
|
||||
* cc -std=c11 -I el-compiler/runtime -lcurl -lpthread -loqs -lcrypto \
|
||||
* -o <out> <prog>.c el-compiler/runtime/el_runtime.c
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
typedef int64_t el_val_t;
|
||||
|
||||
#define EL_STR(s) ((el_val_t)(uintptr_t)(s))
|
||||
#define EL_CSTR(v) ((const char*)(uintptr_t)(v))
|
||||
#define EL_INT(v) (v)
|
||||
#define EL_NULL ((el_val_t)0)
|
||||
|
||||
/* Float values share the el_val_t (int64) slot via a bit-cast.
|
||||
* The codegen emits Float literals as `el_from_float(<dbl>)` so the
|
||||
* underlying bits represent the IEEE 754 double. Float-aware builtins
|
||||
* (math, format, json) round-trip via these helpers. */
|
||||
static inline double el_to_float(el_val_t v) {
|
||||
union { int64_t i; double f; } u;
|
||||
u.i = (int64_t)v;
|
||||
return u.f;
|
||||
}
|
||||
|
||||
static inline el_val_t el_from_float(double f) {
|
||||
union { double f; int64_t i; } u;
|
||||
u.f = f;
|
||||
return (el_val_t)u.i;
|
||||
}
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* ── I/O ──────────────────────────────────────────────────────────────────── */
|
||||
|
||||
void println(el_val_t s);
|
||||
void print(el_val_t s);
|
||||
el_val_t readline(void);
|
||||
|
||||
/* ── String builtins ─────────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t el_str_concat(el_val_t a, el_val_t b);
|
||||
el_val_t str_eq(el_val_t a, el_val_t b);
|
||||
el_val_t str_starts_with(el_val_t s, el_val_t prefix);
|
||||
el_val_t str_ends_with(el_val_t s, el_val_t suffix);
|
||||
el_val_t str_len(el_val_t s);
|
||||
el_val_t str_concat(el_val_t a, el_val_t b);
|
||||
el_val_t int_to_str(el_val_t n);
|
||||
el_val_t str_to_int(el_val_t s);
|
||||
el_val_t str_slice(el_val_t s, el_val_t start, el_val_t end);
|
||||
el_val_t str_contains(el_val_t s, el_val_t sub);
|
||||
el_val_t str_replace(el_val_t s, el_val_t from, el_val_t to);
|
||||
el_val_t str_to_upper(el_val_t s);
|
||||
el_val_t str_to_lower(el_val_t s);
|
||||
el_val_t str_trim(el_val_t s);
|
||||
|
||||
/* ── Math ────────────────────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t el_abs(el_val_t n);
|
||||
el_val_t el_max(el_val_t a, el_val_t b);
|
||||
el_val_t el_min(el_val_t a, el_val_t b);
|
||||
|
||||
/* ── Refcount (ARC) ──────────────────────────────────────────────────────────
|
||||
* Lists and Maps carry a refcount. Strings and ints do not — el_retain and
|
||||
* el_release are safe no-ops on non-refcounted values (they sniff a magic
|
||||
* header at offset 0 and only act if the magic matches).
|
||||
*
|
||||
* Codegen emits these at let-binding shadowing, function entry (params), and
|
||||
* function exit (locals other than the returned value). The refcount lets
|
||||
* el_list_append and el_map_set mutate in place when uniquely owned (cheap)
|
||||
* and copy-on-write when shared (preserves persistent semantics across
|
||||
* accumulator patterns in the compiler itself). */
|
||||
|
||||
void el_retain(el_val_t v);
|
||||
void el_release(el_val_t v);
|
||||
|
||||
/* ── List ────────────────────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t el_list_new(el_val_t count, ...);
|
||||
el_val_t el_list_len(el_val_t list);
|
||||
el_val_t el_list_get(el_val_t list, el_val_t index);
|
||||
el_val_t el_list_append(el_val_t list, el_val_t elem);
|
||||
el_val_t el_list_empty(void);
|
||||
el_val_t el_list_clone(el_val_t list);
|
||||
|
||||
/* ── Map ─────────────────────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t el_map_new(el_val_t pair_count, ...);
|
||||
el_val_t el_get_field(el_val_t map, el_val_t key);
|
||||
el_val_t el_map_get(el_val_t map, el_val_t key);
|
||||
el_val_t el_map_set(el_val_t map, el_val_t key, el_val_t value);
|
||||
|
||||
/* ── HTTP ─────────────────────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t http_get(el_val_t url);
|
||||
el_val_t http_post(el_val_t url, el_val_t body);
|
||||
el_val_t http_post_json(el_val_t url, el_val_t json_body);
|
||||
el_val_t http_get_with_headers(el_val_t url, el_val_t headers_map);
|
||||
el_val_t http_post_with_headers(el_val_t url, el_val_t body, el_val_t headers_map);
|
||||
el_val_t http_post_form_auth(el_val_t url, el_val_t form_body, el_val_t auth_header);
|
||||
el_val_t http_delete(el_val_t url);
|
||||
void http_serve(el_val_t port, el_val_t handler);
|
||||
void http_set_handler(el_val_t name);
|
||||
|
||||
/* HTTP server v2 ─────────────────────────────────────────────────────────────
|
||||
* Same dispatch model as http_serve, but the handler signature is widened:
|
||||
*
|
||||
* el_val_t handler(method, path, headers_map, body)
|
||||
*
|
||||
* `headers_map` is an ElMap from lowercased header name → header value (both
|
||||
* Strings). Repeated headers are joined with ", " per RFC 7230.
|
||||
*
|
||||
* Response value: the handler may return either
|
||||
* (a) a plain body string — same auto-content-type / 200-OK behaviour as
|
||||
* http_serve (3-arg) — or
|
||||
* (b) a response envelope built with `http_response(status, headers_json,
|
||||
* body)`. The runtime detects the envelope discriminator
|
||||
* `"el_http_response":1` at the start of the returned string and
|
||||
* unpacks status / headers / body before sending.
|
||||
*
|
||||
* The 3-arg http_serve(port, handler) remains supported unchanged for
|
||||
* existing handlers (e.g. products/web/server.el): it dispatches with
|
||||
* (method, path, body), hardcodes 200 OK, and auto-detects content type. */
|
||||
void http_serve_v2(el_val_t port, el_val_t handler);
|
||||
void http_set_handler_v2(el_val_t name);
|
||||
|
||||
/* Build an HTTP response envelope. `headers_json` should be a JSON object
|
||||
* literal like `{"WWW-Authenticate":"Basic"}` (or "" / "{}" for none). The
|
||||
* returned string carries the discriminator `{"el_http_response":1,...}`
|
||||
* which the runtime's send-path detects and unpacks. Detection happens
|
||||
* uniformly inside http_send_response, so a 3-arg handler may also return
|
||||
* an envelope. The 3-arg variant remains documented as a fixed 200-OK
|
||||
* auto-content-type contract for legacy handlers that return plain bodies. */
|
||||
el_val_t http_response(el_val_t status, el_val_t headers_json, el_val_t body);
|
||||
|
||||
/* HTTP timeout — every libcurl request honors EL_HTTP_TIMEOUT_MS (default
|
||||
* 60000ms). Read lazily on first use, so setting the env var any time before
|
||||
* the first http_* call is sufficient. */
|
||||
|
||||
/* Streaming variants — write the response body straight to a file via
|
||||
* libcurl's CURLOPT_WRITEFUNCTION = fwrite. These bypass the el_val_t string
|
||||
* wrapper entirely, so binary payloads (audio/mpeg, image/png, etc.) survive
|
||||
* embedded NUL bytes that would truncate a strlen()-based code path.
|
||||
*
|
||||
* Both honor EL_HTTP_TIMEOUT_MS, follow redirects, and accept the same
|
||||
* `headers_map` shape as http_post_with_headers (ElMap of String→String).
|
||||
*
|
||||
* Return value: 1 on success (file fully written), 0 on any failure
|
||||
* (network, file open, partial write). On failure the output file is removed
|
||||
* so callers cannot mistake a partially-written file for a valid one. */
|
||||
el_val_t http_post_to_file(el_val_t url, el_val_t body, el_val_t headers_map, el_val_t output_path);
|
||||
el_val_t http_get_to_file(el_val_t url, el_val_t headers_map, el_val_t output_path);
|
||||
|
||||
/* ── URL encoding ────────────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t url_encode(el_val_t s); /* RFC 3986 unreserved set */
|
||||
el_val_t url_decode(el_val_t s); /* '+' → space, %XX → byte */
|
||||
|
||||
/* ── HTML allowlist sanitizer ────────────────────────────────────────────────
|
||||
* el_html_sanitize(input_html, allowlist_json) — strict allowlist HTML
|
||||
* cleaner. State-machine parser; tag/attribute names compared case-
|
||||
* insensitively against the allowlist; `<a href>` / `<… src>` URL schemes
|
||||
* validated (http, https, mailto, fragment-only, or relative); whole-
|
||||
* subtree drop for script / style / iframe / object / embed / form; HTML-
|
||||
* escapes free text outside dropped subtrees.
|
||||
*
|
||||
* The allowlist is JSON of the form
|
||||
* {"p":[],"a":["href","title"],"strong":[],...}
|
||||
* where each value is the array of attribute names allowed for that tag. */
|
||||
el_val_t el_html_sanitize(el_val_t input_html, el_val_t allowlist_json);
|
||||
|
||||
/* ── Filesystem ──────────────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t fs_read(el_val_t path);
|
||||
el_val_t fs_write(el_val_t path, el_val_t content);
|
||||
el_val_t fs_list(el_val_t path);
|
||||
el_val_t fs_exists(el_val_t path);
|
||||
el_val_t fs_mkdir(el_val_t path); /* mkdir -p, mode 0755 */
|
||||
|
||||
/* Length-explicit binary write. `length` is an Int (el_val_t holding the
|
||||
* byte count). The caller knows the length from context — typically because
|
||||
* `bytes` came from base64_decode (which produces a magic-tagged binary
|
||||
* buffer with embedded NULs possible) and the caller already tracks the
|
||||
* decoded length, OR because the bytes came from a fixed-size source
|
||||
* (sha256_bytes = 32, hmac_sha256_bytes = 32). Bypasses strlen entirely.
|
||||
*
|
||||
* Returns 1 on success, 0 on failure (invalid path, can't open, partial
|
||||
* write, negative length). On partial-write failure, the file is removed
|
||||
* so callers cannot read back a truncated artefact. */
|
||||
el_val_t fs_write_bytes(el_val_t path, el_val_t bytes, el_val_t length);
|
||||
|
||||
/* ── JSON ────────────────────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t json_get(el_val_t json, el_val_t key);
|
||||
el_val_t json_parse(el_val_t s);
|
||||
el_val_t json_stringify(el_val_t v);
|
||||
el_val_t json_get_string(el_val_t json_str, el_val_t key);
|
||||
el_val_t json_get_int(el_val_t json_str, el_val_t key);
|
||||
el_val_t json_get_float(el_val_t json_str, el_val_t key);
|
||||
el_val_t json_get_bool(el_val_t json_str, el_val_t key);
|
||||
el_val_t json_get_raw(el_val_t json_str, el_val_t key);
|
||||
el_val_t json_set(el_val_t json_str, el_val_t key, el_val_t value);
|
||||
el_val_t json_array_len(el_val_t json_str);
|
||||
el_val_t json_array_get(el_val_t json_str, el_val_t index);
|
||||
el_val_t json_array_get_string(el_val_t json_str, el_val_t index);
|
||||
|
||||
/* ── Time ────────────────────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t time_now(void);
|
||||
el_val_t time_now_utc(void);
|
||||
el_val_t sleep_secs(el_val_t secs);
|
||||
el_val_t sleep_ms(el_val_t ms);
|
||||
el_val_t time_format(el_val_t ts, el_val_t fmt);
|
||||
el_val_t time_to_parts(el_val_t ts);
|
||||
el_val_t time_from_parts(el_val_t secs, el_val_t ns, el_val_t tz);
|
||||
el_val_t time_add(el_val_t ts, el_val_t n, el_val_t unit);
|
||||
el_val_t time_diff(el_val_t ts1, el_val_t ts2, el_val_t unit);
|
||||
|
||||
/* ── Instant + Duration: first-class temporal types ──────────────────────────
|
||||
* Both types share the el_val_t (int64) slot. Instants are nanoseconds
|
||||
* since the Unix epoch; Durations are signed nanoseconds. Type discipline
|
||||
* is enforced at codegen-time: BinOps on names registered as Instant or
|
||||
* Duration route through the typed wrappers below; mismatches like
|
||||
* Instant+Instant become #error at the C compiler.
|
||||
*
|
||||
* Postfix literals — `30.seconds`, `1.hour`, `500.millis`, `30.nanos` — are
|
||||
* recognised by the parser as DurationLit AST nodes and lowered to literal
|
||||
* int64 nanoseconds at codegen time. The runtime never sees the units. */
|
||||
|
||||
el_val_t el_now_instant(void);
|
||||
el_val_t now(void);
|
||||
el_val_t unix_seconds(el_val_t n);
|
||||
el_val_t unix_millis(el_val_t n);
|
||||
el_val_t instant_from_iso8601(el_val_t s);
|
||||
|
||||
el_val_t el_duration_from_nanos(el_val_t ns);
|
||||
el_val_t duration_seconds(el_val_t n);
|
||||
el_val_t duration_millis(el_val_t n);
|
||||
el_val_t duration_nanos(el_val_t n);
|
||||
|
||||
el_val_t el_instant_add_dur(el_val_t inst, el_val_t dur);
|
||||
el_val_t el_instant_sub_dur(el_val_t inst, el_val_t dur);
|
||||
el_val_t el_instant_diff(el_val_t a, el_val_t b);
|
||||
el_val_t el_duration_add(el_val_t a, el_val_t b);
|
||||
el_val_t el_duration_sub(el_val_t a, el_val_t b);
|
||||
el_val_t el_duration_scale(el_val_t dur, el_val_t scalar);
|
||||
el_val_t el_duration_div(el_val_t dur, el_val_t scalar);
|
||||
|
||||
el_val_t el_instant_lt(el_val_t a, el_val_t b);
|
||||
el_val_t el_instant_le(el_val_t a, el_val_t b);
|
||||
el_val_t el_instant_gt(el_val_t a, el_val_t b);
|
||||
el_val_t el_instant_ge(el_val_t a, el_val_t b);
|
||||
el_val_t el_instant_eq(el_val_t a, el_val_t b);
|
||||
el_val_t el_instant_ne(el_val_t a, el_val_t b);
|
||||
el_val_t el_duration_lt(el_val_t a, el_val_t b);
|
||||
el_val_t el_duration_le(el_val_t a, el_val_t b);
|
||||
el_val_t el_duration_gt(el_val_t a, el_val_t b);
|
||||
el_val_t el_duration_ge(el_val_t a, el_val_t b);
|
||||
el_val_t el_duration_eq(el_val_t a, el_val_t b);
|
||||
el_val_t el_duration_ne(el_val_t a, el_val_t b);
|
||||
|
||||
el_val_t instant_to_unix_seconds(el_val_t i);
|
||||
el_val_t instant_to_unix_millis(el_val_t i);
|
||||
el_val_t instant_to_iso8601(el_val_t i);
|
||||
el_val_t duration_to_seconds(el_val_t d);
|
||||
el_val_t duration_to_millis(el_val_t d);
|
||||
el_val_t duration_to_nanos(el_val_t d);
|
||||
|
||||
el_val_t el_sleep_duration(el_val_t dur);
|
||||
el_val_t unix_timestamp(void);
|
||||
|
||||
el_val_t ttl_cache_set(el_val_t key, el_val_t value);
|
||||
el_val_t ttl_cache_get(el_val_t key, el_val_t max_age);
|
||||
el_val_t ttl_cache_age(el_val_t key);
|
||||
|
||||
/* ── Calendar + CalendarTime + Rhythm + LocalDate/Time/DateTime ─────────────
|
||||
* Phase 1.5 of the time system. Calendar is pluggable: EarthCalendar (IANA
|
||||
* zones, Gregorian, DST) is the user-facing default; MarsCalendar,
|
||||
* CycleCalendar(period), NoCycleCalendar, RelativeCalendar handle non-Earth
|
||||
* domains.
|
||||
*
|
||||
* A Calendar interprets an Instant under a particular cycle convention and
|
||||
* produces a CalendarTime. CalendarTime carries the underlying Instant and
|
||||
* a back-pointer to its Calendar; arithmetic and formatting consult the
|
||||
* Calendar to convert ns since epoch into year/month/day/hour/minute/second
|
||||
* (or sol/phase, or cycle/phase, depending on kind).
|
||||
*
|
||||
* Storage convention: Calendar / CalendarTime / Rhythm / LocalDate /
|
||||
* LocalDateTime are heap-allocated structs whose pointers are cast into
|
||||
* el_val_t. A 24-bit magic header at offset 0 lets the runtime identify
|
||||
* the kind safely. LocalTime is small enough to live in the int64 slot
|
||||
* directly (nanos since midnight, signed). */
|
||||
|
||||
/* Zone — opaque IANA zone or fixed offset, used by EarthCalendar.
|
||||
* `zone_id` is either an IANA name ("America/New_York", "UTC") or a fixed
|
||||
* offset string ("+05:30", "-08:00"). The runtime resolves it via tzset()
|
||||
* on first use of the owning EarthCalendar. */
|
||||
el_val_t zone(el_val_t id);
|
||||
el_val_t zone_utc(void);
|
||||
el_val_t zone_local(void);
|
||||
el_val_t zone_offset(el_val_t hours, el_val_t minutes);
|
||||
|
||||
/* Calendar constructors. Each returns an el_val_t pointer to a heap-
|
||||
* allocated, magic-tagged Calendar struct. Calendars are interned by
|
||||
* (kind, zone_id, period_ns, epoch_ns) so identical constructors return
|
||||
* the same pointer — equality is reference equality. */
|
||||
el_val_t earth_calendar(el_val_t z);
|
||||
el_val_t earth_calendar_default(void);
|
||||
el_val_t mars_calendar(void);
|
||||
el_val_t cycle_calendar(el_val_t period_dur);
|
||||
el_val_t no_cycle_calendar(void);
|
||||
el_val_t relative_calendar(el_val_t epoch_inst);
|
||||
|
||||
/* CalendarTime constructors and methods. Returns a heap-allocated struct
|
||||
* whose pointer fits in el_val_t. */
|
||||
el_val_t now_in(el_val_t cal);
|
||||
el_val_t in_calendar(el_val_t inst, el_val_t cal);
|
||||
el_val_t cal_format(el_val_t ct, el_val_t pattern);
|
||||
el_val_t cal_to_instant(el_val_t ct);
|
||||
el_val_t cal_cycle_phase(el_val_t ct);
|
||||
el_val_t cal_in(el_val_t ct, el_val_t cal);
|
||||
|
||||
/* LocalDate / LocalTime / LocalDateTime — calendar-agnostic value types.
|
||||
* LocalTime carries nanoseconds since midnight as a signed int64 directly
|
||||
* in the el_val_t slot (no allocation). LocalDate / LocalDateTime are
|
||||
* heap-allocated structs with magic headers. */
|
||||
el_val_t local_date(el_val_t y, el_val_t m, el_val_t d);
|
||||
el_val_t local_time(el_val_t h, el_val_t m, el_val_t s, el_val_t ns);
|
||||
el_val_t local_datetime(el_val_t date, el_val_t time);
|
||||
el_val_t zoned(el_val_t date, el_val_t time, el_val_t cal);
|
||||
|
||||
el_val_t local_date_year(el_val_t ld);
|
||||
el_val_t local_date_month(el_val_t ld);
|
||||
el_val_t local_date_day(el_val_t ld);
|
||||
el_val_t local_time_hour(el_val_t lt);
|
||||
el_val_t local_time_minute(el_val_t lt);
|
||||
el_val_t local_time_second(el_val_t lt);
|
||||
el_val_t local_time_nanos(el_val_t lt);
|
||||
|
||||
el_val_t el_local_date_add_dur(el_val_t ld, el_val_t dur);
|
||||
el_val_t el_local_time_add_dur(el_val_t lt, el_val_t dur);
|
||||
el_val_t el_local_date_lt(el_val_t a, el_val_t b);
|
||||
el_val_t el_local_date_eq(el_val_t a, el_val_t b);
|
||||
|
||||
/* Rhythm — pluggable recurrence AST. Returns a heap-allocated struct
|
||||
* pointer in el_val_t; rhythms are immutable so callers may share them. */
|
||||
el_val_t rhythm_cycle_start(void);
|
||||
el_val_t rhythm_cycle_phase(el_val_t phase);
|
||||
el_val_t rhythm_duration(el_val_t d);
|
||||
el_val_t rhythm_session_start(void);
|
||||
el_val_t rhythm_event(el_val_t name);
|
||||
el_val_t rhythm_and(el_val_t a, el_val_t b);
|
||||
el_val_t rhythm_or(el_val_t a, el_val_t b);
|
||||
el_val_t rhythm_weekday(el_val_t day);
|
||||
el_val_t rhythm_weekly_at(el_val_t day, el_val_t hour, el_val_t minute);
|
||||
el_val_t rhythm_next_after(el_val_t r, el_val_t after, el_val_t cal);
|
||||
el_val_t rhythm_matches(el_val_t r, el_val_t ct);
|
||||
|
||||
/* ── UUID ────────────────────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t uuid_new(void);
|
||||
el_val_t uuid_v4(void);
|
||||
|
||||
/* ── Environment ─────────────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t env(el_val_t key);
|
||||
|
||||
/* ── In-process state K/V ────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t state_set(el_val_t key, el_val_t value);
|
||||
el_val_t state_get(el_val_t key);
|
||||
el_val_t state_del(el_val_t key);
|
||||
el_val_t state_keys(void);
|
||||
|
||||
/* ── Float formatting ────────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t float_to_str(el_val_t f);
|
||||
el_val_t int_to_float(el_val_t n);
|
||||
el_val_t float_to_int(el_val_t f);
|
||||
el_val_t format_float(el_val_t f, el_val_t decimals);
|
||||
el_val_t decimal_round(el_val_t f, el_val_t decimals);
|
||||
el_val_t str_to_float(el_val_t s);
|
||||
|
||||
/* ── Math (Float-aware) ──────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t math_sqrt(el_val_t f);
|
||||
el_val_t math_log(el_val_t f);
|
||||
el_val_t math_ln(el_val_t f);
|
||||
el_val_t math_sin(el_val_t f);
|
||||
el_val_t math_cos(el_val_t f);
|
||||
el_val_t math_pi(void);
|
||||
|
||||
/* ── String additions ────────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t str_index_of(el_val_t s, el_val_t sub);
|
||||
el_val_t str_split(el_val_t s, el_val_t sep);
|
||||
el_val_t str_char_at(el_val_t s, el_val_t i);
|
||||
el_val_t str_char_code(el_val_t s, el_val_t i);
|
||||
el_val_t str_pad_left(el_val_t s, el_val_t width, el_val_t pad);
|
||||
el_val_t str_pad_right(el_val_t s, el_val_t width, el_val_t pad);
|
||||
el_val_t str_format(el_val_t fmt, el_val_t data);
|
||||
el_val_t str_lower(el_val_t s);
|
||||
el_val_t str_upper(el_val_t s);
|
||||
|
||||
/* ── Text-processing primitives (Phase 1: byte/codepoint, ASCII char classes)
|
||||
* Phase 2 (filed): Unicode-grapheme awareness, NFC/NFD normalization, regex.
|
||||
* is_* predicates: empty input returns false; multi-char requires ALL bytes
|
||||
* to match. ASCII ranges only in Phase 1. */
|
||||
|
||||
/* Counting */
|
||||
el_val_t str_count(el_val_t s, el_val_t sub); /* non-overlapping */
|
||||
el_val_t str_count_chars(el_val_t s); /* codepoint count */
|
||||
el_val_t str_count_bytes(el_val_t s); /* alias of str_len */
|
||||
el_val_t str_count_lines(el_val_t s);
|
||||
el_val_t str_count_words(el_val_t s);
|
||||
el_val_t str_count_letters(el_val_t s); /* ASCII [A-Za-z] */
|
||||
el_val_t str_count_digits(el_val_t s); /* ASCII [0-9] */
|
||||
|
||||
/* Find / position */
|
||||
el_val_t str_index_of_all(el_val_t s, el_val_t sub); /* [Int] of byte offsets */
|
||||
el_val_t str_last_index_of(el_val_t s, el_val_t sub);
|
||||
el_val_t str_find_chars(el_val_t s, el_val_t any_of); /* first idx of any ch */
|
||||
|
||||
/* Transform */
|
||||
el_val_t str_repeat(el_val_t s, el_val_t n);
|
||||
el_val_t str_reverse(el_val_t s); /* by codepoint */
|
||||
el_val_t str_strip_prefix(el_val_t s, el_val_t prefix);
|
||||
el_val_t str_strip_suffix(el_val_t s, el_val_t suffix);
|
||||
el_val_t str_strip_chars(el_val_t s, el_val_t chars);
|
||||
el_val_t str_lstrip(el_val_t s);
|
||||
el_val_t str_rstrip(el_val_t s);
|
||||
|
||||
/* Char classification (Bool) */
|
||||
el_val_t is_letter(el_val_t s);
|
||||
el_val_t is_digit(el_val_t s);
|
||||
el_val_t is_alphanumeric(el_val_t s);
|
||||
el_val_t is_whitespace(el_val_t s);
|
||||
el_val_t is_punctuation(el_val_t s);
|
||||
el_val_t is_uppercase(el_val_t s);
|
||||
el_val_t is_lowercase(el_val_t s);
|
||||
|
||||
/* Split / join */
|
||||
el_val_t str_split_lines(el_val_t s);
|
||||
el_val_t str_split_chars(el_val_t s); /* alias of native_string_chars */
|
||||
el_val_t str_split_n(el_val_t s, el_val_t sep, el_val_t n);
|
||||
el_val_t str_join(el_val_t list, el_val_t sep); /* alias of list_join */
|
||||
|
||||
/* ── List additions ──────────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t list_push(el_val_t list, el_val_t elem);
|
||||
el_val_t list_push_front(el_val_t list, el_val_t elem);
|
||||
el_val_t list_join(el_val_t list, el_val_t sep);
|
||||
el_val_t list_range(el_val_t start, el_val_t end);
|
||||
|
||||
/* ── Bool helpers ────────────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t bool_to_str(el_val_t b);
|
||||
|
||||
/* ── Numeric parsing ─────────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t parse_int(el_val_t s, el_val_t default_val);
|
||||
|
||||
/* ── Process ─────────────────────────────────────────────────────────────── */
|
||||
|
||||
void exit_program(el_val_t code);
|
||||
el_val_t getpid_now(void);
|
||||
|
||||
/* ── CGI identity ─────────────────────────────────────────────────────────────
|
||||
* Called at the start of main() in CGI programs (those with a `cgi {}` block).
|
||||
* Records the program's DHARMA identity before any other code executes. */
|
||||
|
||||
void el_cgi_init(el_val_t name, el_val_t dharma_id, el_val_t principal,
|
||||
el_val_t network, el_val_t engram);
|
||||
|
||||
/* ── DHARMA network builtins ─────────────────────────────────────────────────
|
||||
* Available to CGI programs (declared with a `cgi {}` block).
|
||||
*
|
||||
* Peers are addressed by `dharma_id` of the form
|
||||
* "<registry-id>@<transport-url>" e.g. "ntn-genesis@http://localhost:7770"
|
||||
* If the @<url> portion is omitted, transport defaults to
|
||||
* "http://localhost:7770" (the local CGI daemon assumption).
|
||||
*
|
||||
* Wire protocol (all peers expose):
|
||||
* POST <url>/dharma/recv { channel, from, content } → response body
|
||||
* POST <url>/dharma/event { type, payload, source, timestamp }
|
||||
* POST <url>/api/activate { query } → list of nodes
|
||||
*
|
||||
* Hosting application's responsibility: an El program with a `cgi {}` block
|
||||
* runs http_serve() with its own request handler; that handler should route
|
||||
* "/dharma/event" requests by calling el_runtime_dharma_event_arrive() so
|
||||
* incoming events feed dharma_field() queues. The runtime itself does not
|
||||
* intercept any /dharma path. */
|
||||
|
||||
el_val_t dharma_connect(el_val_t cgi_id);
|
||||
el_val_t dharma_send(el_val_t channel, el_val_t content);
|
||||
el_val_t dharma_activate(el_val_t query);
|
||||
void dharma_emit(el_val_t event_type, el_val_t payload);
|
||||
el_val_t dharma_field(el_val_t event_type);
|
||||
void dharma_strengthen(el_val_t cgi_id, el_val_t weight);
|
||||
el_val_t dharma_relationship(el_val_t cgi_id);
|
||||
el_val_t dharma_peers(void);
|
||||
|
||||
/* Public C API: called by an El program's HTTP handler when a /dharma/event
|
||||
* request arrives. Pushes onto the per-event-type queue and signals any
|
||||
* pending dharma_field() blockers. All three arguments must be NUL-terminated
|
||||
* C strings (or NULL — then treated as empty). */
|
||||
void el_runtime_dharma_event_arrive(const char* event_type,
|
||||
const char* payload,
|
||||
const char* source);
|
||||
|
||||
/* ── Engram local graph primitives ───────────────────────────────────────────
|
||||
* Operate on the CGI's local Engram knowledge graph.
|
||||
* `engram_activate` queries the local graph only; `dharma_activate` is
|
||||
* network-wide across all connected CGI graphs. */
|
||||
|
||||
el_val_t engram_node(el_val_t content, el_val_t node_type, el_val_t salience);
|
||||
el_val_t engram_node_full(el_val_t content, el_val_t node_type, el_val_t label,
|
||||
el_val_t salience, el_val_t importance, el_val_t confidence,
|
||||
el_val_t tier, el_val_t tags);
|
||||
/* Layered consciousness — see el_runtime.c for the layered architecture
|
||||
* design notes (search "Layered consciousness architecture"). The five
|
||||
* canonical layers (safety / core-identity / domain-knowledge / imprint /
|
||||
* suit) are seeded automatically; engram_add_layer extends the registry
|
||||
* with imprint or suit overlays at runtime. Nodes default to layer 1
|
||||
* (core-identity) when created via engram_node / engram_node_full. */
|
||||
el_val_t engram_node_layered(el_val_t content, el_val_t node_type, el_val_t label,
|
||||
el_val_t salience, el_val_t certainty, el_val_t confidence,
|
||||
el_val_t status, el_val_t tags, el_val_t layer_id);
|
||||
el_val_t engram_add_layer(el_val_t name, el_val_t priority, el_val_t suppressible,
|
||||
el_val_t transparent, el_val_t injectable);
|
||||
el_val_t engram_remove_layer(el_val_t layer_id);
|
||||
el_val_t engram_list_layers(void);
|
||||
el_val_t engram_get_node(el_val_t id);
|
||||
void engram_strengthen(el_val_t node_id);
|
||||
void engram_forget(el_val_t node_id);
|
||||
el_val_t engram_node_count(void);
|
||||
el_val_t engram_search(el_val_t query, el_val_t limit);
|
||||
el_val_t engram_scan_nodes(el_val_t limit, el_val_t offset);
|
||||
void engram_connect(el_val_t from_id, el_val_t to_id, el_val_t weight, el_val_t relation);
|
||||
el_val_t engram_edge_between(el_val_t from_id, el_val_t to_id);
|
||||
el_val_t engram_neighbors(el_val_t node_id);
|
||||
el_val_t engram_neighbors_filtered(el_val_t node_id, el_val_t max_depth, el_val_t direction);
|
||||
el_val_t engram_edge_count(void);
|
||||
/* Three-pass activation: background fan-out → working-memory promotion →
|
||||
* Layer 0 override. See "Three-pass activation" in el_runtime.c. */
|
||||
el_val_t engram_activate(el_val_t query, el_val_t depth);
|
||||
el_val_t engram_save(el_val_t path);
|
||||
el_val_t engram_load(el_val_t path);
|
||||
|
||||
/* JSON-string accessors — return pre-serialized JSON so HTTP handlers
|
||||
* can pass results straight through without round-tripping ElList/ElMap
|
||||
* through json_stringify. */
|
||||
el_val_t engram_get_node_json(el_val_t id);
|
||||
el_val_t engram_search_json(el_val_t query, el_val_t limit);
|
||||
el_val_t engram_scan_nodes_json(el_val_t limit, el_val_t offset);
|
||||
el_val_t engram_scan_nodes_by_type_json(el_val_t node_type, el_val_t limit, el_val_t offset);
|
||||
el_val_t engram_neighbors_json(el_val_t node_id, el_val_t max_depth, el_val_t direction);
|
||||
el_val_t engram_activate_json(el_val_t query, el_val_t depth);
|
||||
el_val_t engram_stats_json(void);
|
||||
el_val_t engram_list_layers_json(void);
|
||||
/* engram_compile_layered_json — produce a prompt-ready text block split
|
||||
* into "[LAYER 0 — STRUCTURAL]" (non-suppressible layers, sacred fire)
|
||||
* and "[ENGRAM CONTEXT]" (standard suppressible layers). Returns "" if
|
||||
* no nodes promoted to working memory. */
|
||||
el_val_t engram_compile_layered_json(el_val_t intent, el_val_t depth);
|
||||
|
||||
/* ── LLM (Anthropic API client) ─────────────────────────────────────────────
|
||||
* All functions call https://api.anthropic.com/v1/messages with the API key
|
||||
* from env ANTHROPIC_API_KEY. Default model when empty: claude-sonnet-4-5. */
|
||||
|
||||
el_val_t llm_call(el_val_t model, el_val_t prompt);
|
||||
el_val_t llm_call_system(el_val_t model, el_val_t system_prompt, el_val_t user_prompt);
|
||||
el_val_t llm_call_agentic(el_val_t model, el_val_t system, el_val_t user, el_val_t tools);
|
||||
el_val_t llm_vision(el_val_t model, el_val_t system, el_val_t prompt, el_val_t image_url_or_b64);
|
||||
el_val_t llm_models(void);
|
||||
|
||||
/* Register a tool handler by name. The handler is looked up via dlsym
|
||||
* (mirroring http_set_handler), so any El `fn <name>(input)` compiles to
|
||||
* a global C symbol that this function can locate at runtime.
|
||||
* Handler signature: `el_val_t handler(el_val_t input_json)` — receives
|
||||
* the tool input as a JSON-string el_val_t and returns a JSON-string
|
||||
* el_val_t result. Used by llm_call_agentic. */
|
||||
void llm_register_tool(el_val_t name, el_val_t handler_fn_name);
|
||||
|
||||
/* ── args() ─────────────────────────────────────────────────────────────────
|
||||
* Provides access to command-line arguments passed to the program.
|
||||
* Populated by el_runtime_init_args() before main() runs. */
|
||||
|
||||
el_val_t args(void);
|
||||
void el_runtime_init_args(int argc, char** argv);
|
||||
|
||||
/* ── Crypto primitives ─────────────────────────────────────────────────────
|
||||
* SHA-256, HMAC-SHA-256, and base64 (standard + URL-safe).
|
||||
* Self-contained — no OpenSSL/libcrypto dependency. The implementations are
|
||||
* adapted from public-domain reference code (Brad Conte / RFC 4648).
|
||||
*
|
||||
* Bytes-returning variants (sha256_bytes, hmac_sha256_bytes) return a string
|
||||
* value whose contents are raw binary; callers usually feed these into
|
||||
* base64_encode. Note that el_val_t strings are NUL-terminated by convention,
|
||||
* so the binary payload may contain embedded NULs — pass it directly into
|
||||
* base64_encode (which uses an explicit length) rather than treating it as
|
||||
* a printable C string.
|
||||
*
|
||||
* The "base64" variants emit/accept RFC 4648 standard alphabet with padding.
|
||||
* The "base64url" variants use URL-safe alphabet (`-`/`_`) with no padding,
|
||||
* as used in JWTs. */
|
||||
|
||||
el_val_t sha256_hex(el_val_t input);
|
||||
el_val_t sha256_bytes(el_val_t input);
|
||||
el_val_t hmac_sha256_hex(el_val_t key, el_val_t message);
|
||||
el_val_t hmac_sha256_bytes(el_val_t key, el_val_t message);
|
||||
el_val_t base64_encode(el_val_t input);
|
||||
el_val_t base64_decode(el_val_t input);
|
||||
el_val_t base64url_encode(el_val_t input);
|
||||
el_val_t base64url_decode(el_val_t input);
|
||||
|
||||
/* Length-aware variants (internal — exposed for the rare caller that already
|
||||
* has a known-length binary buffer and doesn't want to round-trip through
|
||||
* a NUL-terminated el_val_t string). Sha256_bytes and hmac_sha256_bytes feed
|
||||
* these implicitly. */
|
||||
el_val_t el_sha256_bytes_n(const unsigned char* data, size_t len);
|
||||
el_val_t el_base64_encode_n(const unsigned char* data, size_t len, int url_safe);
|
||||
|
||||
/* ── Post-quantum primitives (liboqs-backed) ────────────────────────────────
|
||||
* All inputs/outputs hex-encoded. Algorithm choices:
|
||||
* Signature: CRYSTALS-Dilithium-3 (NIST level 3, balanced)
|
||||
* KEM: CRYSTALS-Kyber-768 (NIST level 3)
|
||||
* Hash: SHA3-256 (Keccak) (PQ-aware protocols favour SHA3 over SHA2)
|
||||
*
|
||||
* If liboqs is not linked (detected via __has_include(<oqs/oqs.h>) at compile
|
||||
* time), the pq_* entry points return a JSON-shaped error string so callers
|
||||
* fail loudly rather than silently fall back to classical schemes:
|
||||
* {"error":"liboqs not linked, post-quantum primitives unavailable"}
|
||||
*
|
||||
* The hybrid handshake pairs X25519 with Kyber-768 per NIST PQ guidance and
|
||||
* CNSA 2.0. Combined shared secret is HKDF-SHA256(x25519_ss || kyber_ss).
|
||||
* Even if Kyber falls, X25519 holds; if X25519 falls under quantum attack,
|
||||
* Kyber holds. SHA3-256 also remains usable independent of liboqs (the
|
||||
* Keccak permutation is PQ-OK as a primitive). */
|
||||
|
||||
el_val_t pq_keygen_signature(void);
|
||||
el_val_t pq_sign(el_val_t secret_key_hex, el_val_t message);
|
||||
el_val_t pq_verify(el_val_t public_key_hex, el_val_t message, el_val_t signature_hex);
|
||||
|
||||
el_val_t pq_kem_keygen(void);
|
||||
el_val_t pq_kem_encaps(el_val_t public_key_hex);
|
||||
el_val_t pq_kem_decaps(el_val_t secret_key_hex, el_val_t ciphertext_hex);
|
||||
|
||||
el_val_t pq_hybrid_keygen(void);
|
||||
el_val_t pq_hybrid_handshake(el_val_t remote_pub_combined);
|
||||
|
||||
el_val_t sha3_256_hex(el_val_t input);
|
||||
|
||||
/* ── AEAD: AES-256-GCM (libcrypto-backed) ───────────────────────────────────
|
||||
* Symmetric authenticated encryption used to wrap envelopes after a KEM
|
||||
* handshake. Caller MUST supply a 32-byte key (64 hex chars) — typically the
|
||||
* Kyber-768 / hybrid shared_secret, optionally normalized via SHA3-256.
|
||||
*
|
||||
* aead_encrypt returns a JSON map {"nonce":"...","ciphertext":"..."} where
|
||||
* ciphertext is the AES-256-GCM output with the 16-byte auth tag appended.
|
||||
* Nonce is a fresh 12-byte CSPRNG draw — callers never pick the nonce, which
|
||||
* structurally rules out the GCM nonce-reuse footgun.
|
||||
*
|
||||
* aead_decrypt returns the plaintext String, or "" on any failure (including
|
||||
* auth-tag mismatch). Callers MUST check for "" before trusting the result. */
|
||||
el_val_t aead_encrypt(el_val_t key_hex, el_val_t plaintext);
|
||||
el_val_t aead_decrypt(el_val_t key_hex, el_val_t nonce_hex, el_val_t ciphertext_hex);
|
||||
|
||||
/* ── Native VM builtin aliases (for compiled El source) ─────────────────────
|
||||
* These match the El VM's native_* builtins so that El source compiled
|
||||
* to C can call the same names without modification. */
|
||||
|
||||
el_val_t native_list_get(el_val_t list, el_val_t index);
|
||||
el_val_t native_list_len(el_val_t list);
|
||||
el_val_t native_list_append(el_val_t list, el_val_t elem);
|
||||
el_val_t native_list_empty(void);
|
||||
el_val_t native_list_clone(el_val_t list);
|
||||
el_val_t native_string_chars(el_val_t s);
|
||||
el_val_t native_int_to_str(el_val_t n);
|
||||
|
||||
/* ── Method-call shorthand aliases ──────────────────────────────────────────
|
||||
* The El method-call convention `obj.method(args)` compiles to
|
||||
* `method(obj, args)`. These aliases expose the runtime functions under
|
||||
* the short names that result from method calls in El source.
|
||||
*
|
||||
* Example: `myList.append(x)` → `append(myList, x)` (calls this alias)
|
||||
* `myList.len()` → `len(myList)` (calls this alias) */
|
||||
|
||||
el_val_t append(el_val_t list, el_val_t elem); /* el_list_append */
|
||||
el_val_t len(el_val_t list); /* el_list_len */
|
||||
el_val_t get(el_val_t list, el_val_t index); /* el_list_get */
|
||||
el_val_t map_get(el_val_t map, el_val_t key); /* el_map_get */
|
||||
el_val_t map_set(el_val_t map, el_val_t key, el_val_t value); /* el_map_set */
|
||||
|
||||
/* ── OTLP/HTTP Observability ─────────────────────────────────────────────── */
|
||||
/* See bottom of el_runtime.c for the implementation.
|
||||
* Configured by env vars OTLP_ENDPOINT, OTEL_SERVICE_NAME, OTEL_SERVICE_VERSION.
|
||||
* No-op when OTLP_ENDPOINT is unset. Drop-on-failure semantics. */
|
||||
/* ── Subprocess execution ────────────────────────────────────────────────── */
|
||||
el_val_t exec_command(el_val_t cmd); /* run shell command, return exit code */
|
||||
el_val_t exec_capture(el_val_t cmd); /* run shell command, capture stdout */
|
||||
el_val_t exec(el_val_t cmd); /* exec(cmd) → stdout String (30s timeout) */
|
||||
el_val_t exec_bg(el_val_t cmd); /* exec_bg(cmd) → PID String (non-blocking) */
|
||||
|
||||
el_val_t emit_log(el_val_t level, el_val_t msg, el_val_t fields_json);
|
||||
el_val_t emit_metric(el_val_t name, el_val_t value, el_val_t tags_json);
|
||||
el_val_t trace_span_start(el_val_t name);
|
||||
el_val_t trace_span_end(el_val_t span_handle);
|
||||
el_val_t emit_event(el_val_t name, el_val_t duration_ms);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,164 @@
|
||||
// channel.el — Go-style channels for El
|
||||
//
|
||||
// Channels are the communication primitive for concurrent El programs.
|
||||
// Threads send values into a channel; other threads receive them.
|
||||
// Channels are typed by convention — all values are Strings.
|
||||
//
|
||||
// Backed by four seed primitives in el_runtime.c:
|
||||
// __channel_new(capacity) -> Int create channel; cap=0 = unbounded
|
||||
// __channel_send(ch, msg) push msg; blocks if bounded and full
|
||||
// __channel_recv(ch) -> String pop msg; blocks until available; "" on close
|
||||
// __channel_try_recv(ch) -> String non-blocking pop; "" if empty
|
||||
// __channel_close(ch) mark closed; wake all blocked recvers
|
||||
//
|
||||
// Usage:
|
||||
// let ch: Int = channel_new(10) // buffered channel, capacity 10
|
||||
// spawn("producer", int_to_str(ch))
|
||||
// let msg: String = channel_recv(ch)
|
||||
|
||||
// ── Core channel API ─────────────────────────────────────────────────────────
|
||||
|
||||
// channel_new — create a channel with the given buffer capacity.
|
||||
//
|
||||
// capacity: 0 = unbounded (never blocks sender)
|
||||
// N = bounded buffer of N messages (sender blocks when full)
|
||||
//
|
||||
// Returns a channel handle (Int) to pass to send/recv/close.
|
||||
fn channel_new(capacity: Int) -> Int {
|
||||
return __channel_new(capacity)
|
||||
}
|
||||
|
||||
// channel_send — send a message into the channel.
|
||||
//
|
||||
// Blocks if the channel is bounded and full.
|
||||
// No-op if the channel is already closed.
|
||||
fn channel_send(ch: Int, msg: String) {
|
||||
__channel_send(ch, msg)
|
||||
}
|
||||
|
||||
// channel_recv — receive the next message from the channel.
|
||||
//
|
||||
// Blocks until a message is available.
|
||||
// Returns "" when the channel is closed and all buffered messages are drained.
|
||||
// The "" sentinel signals end-of-stream to consumers in a loop.
|
||||
fn channel_recv(ch: Int) -> String {
|
||||
return __channel_recv(ch)
|
||||
}
|
||||
|
||||
// channel_try_recv — non-blocking receive.
|
||||
//
|
||||
// Returns the next message if one is available, or "" if the channel is empty.
|
||||
// Does not block. Callers must distinguish "" (empty) from a legitimate ""
|
||||
// message by convention — use a non-empty sentinel in the message protocol.
|
||||
fn channel_try_recv(ch: Int) -> String {
|
||||
return __channel_try_recv(ch)
|
||||
}
|
||||
|
||||
// channel_close — signal that no more messages will be sent.
|
||||
//
|
||||
// After close, channel_recv continues to drain buffered messages then
|
||||
// returns "" on every subsequent call. channel_send on a closed channel
|
||||
// is a no-op (the message is dropped).
|
||||
fn channel_close(ch: Int) {
|
||||
__channel_close(ch)
|
||||
}
|
||||
|
||||
// ── channel_pipeline ─────────────────────────────────────────────────────────
|
||||
|
||||
// channel_pipeline — producer/consumer pipeline with parallel workers.
|
||||
//
|
||||
// Reads messages from in_ch, applies fn_name to each, writes results to out_ch.
|
||||
// Spawns `workers` concurrent worker threads — each drains in_ch independently,
|
||||
// so messages are processed in arrival order within each worker but not globally.
|
||||
//
|
||||
// fn_name must be an El fn with signature (String) -> String.
|
||||
//
|
||||
// Call channel_close(in_ch) to signal EOF. Workers exit when they receive "".
|
||||
// The caller must also close out_ch after all workers finish (via join).
|
||||
//
|
||||
// let in_ch: Int = channel_new(0)
|
||||
// let out_ch: Int = channel_new(0)
|
||||
// channel_pipeline(in_ch, out_ch, "process_item", 4)
|
||||
// channel_send(in_ch, "work-1")
|
||||
// channel_close(in_ch)
|
||||
// let result: String = channel_recv(out_ch)
|
||||
fn channel_pipeline(in_ch: Int, out_ch: Int, fn_name: String, workers: Int) {
|
||||
let i: Int = 0
|
||||
while i < workers {
|
||||
let arg: String = "{\"in_ch\":" + int_to_str(in_ch) +
|
||||
",\"out_ch\":" + int_to_str(out_ch) +
|
||||
",\"fn\":\"" + fn_name + "\"}"
|
||||
let _tid: Int = spawn("_channel_worker", arg)
|
||||
let i = i + 1
|
||||
}
|
||||
}
|
||||
|
||||
// _channel_worker — internal worker for channel_pipeline.
|
||||
//
|
||||
// Reads messages from in_ch until it receives "" (closed+empty), applies
|
||||
// fn_name to each, and writes results to out_ch. Runs in its own thread
|
||||
// (spawned by channel_pipeline).
|
||||
fn _channel_worker(arg: String) -> String {
|
||||
let in_ch: Int = str_to_int(json_get(arg, "in_ch"))
|
||||
let out_ch: Int = str_to_int(json_get(arg, "out_ch"))
|
||||
let fn_name: String = json_get(arg, "fn")
|
||||
let running: Bool = true
|
||||
while running {
|
||||
let msg: String = channel_recv(in_ch)
|
||||
if str_eq(msg, "") {
|
||||
let running = false
|
||||
} else {
|
||||
// Spawn fn_name in a child thread so it cannot block the worker loop.
|
||||
let tid: Int = spawn(fn_name, msg)
|
||||
let result: String = join(tid)
|
||||
channel_send(out_ch, result)
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── channel_drain ────────────────────────────────────────────────────────────
|
||||
|
||||
// channel_drain — collect all messages from ch into a list.
|
||||
//
|
||||
// Reads until the channel is closed and empty (recv returns "").
|
||||
// Returns a [String] of all messages received.
|
||||
//
|
||||
// Typical usage: close the channel from the producer side, then call
|
||||
// channel_drain from the consumer to collect results.
|
||||
fn channel_drain(ch: Int) -> [String] {
|
||||
let results: [String] = el_list_empty()
|
||||
let running: Bool = true
|
||||
while running {
|
||||
let msg: String = channel_recv(ch)
|
||||
if str_eq(msg, "") {
|
||||
let running = false
|
||||
} else {
|
||||
let results = el_list_append(results, msg)
|
||||
}
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
// ── channel_fan_out ───────────────────────────────────────────────────────────
|
||||
|
||||
// channel_fan_out — send every item in a list into a channel.
|
||||
//
|
||||
// items: [String] — items to send
|
||||
// ch: Int — destination channel
|
||||
//
|
||||
// Sends all items then closes the channel to signal end-of-stream.
|
||||
// Intended for the producer side of a pipeline:
|
||||
//
|
||||
// channel_fan_out(items, in_ch)
|
||||
// let results: [String] = channel_drain(out_ch)
|
||||
fn channel_fan_out(items: [String], ch: Int) {
|
||||
let n: Int = el_list_len(items)
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
let item: String = el_list_get(items, i)
|
||||
channel_send(ch, item)
|
||||
let i = i + 1
|
||||
}
|
||||
channel_close(ch)
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
// runtime/engram.el — El wrapper for the engram graph store
|
||||
//
|
||||
// Thin wrappers over the __engram_* seed primitives defined in el_seed.c.
|
||||
// Each function delegates directly to the corresponding seed — no logic here.
|
||||
// The seed layer owns all storage, indexing, and graph traversal.
|
||||
//
|
||||
// Dependencies: runtime/string.el, runtime/json.el
|
||||
|
||||
// --- Node creation ---
|
||||
|
||||
fn engram_node(content: String, node_type: String, salience: Float) -> String {
|
||||
return __engram_node(content, node_type, salience)
|
||||
}
|
||||
|
||||
fn engram_node_full(content: String, nt: String, sal: Float, imp: Float,
|
||||
source: String, lang: String, ts: Int, tags: String) -> String {
|
||||
return __engram_node_full(content, nt, sal, imp, source, lang, ts, tags)
|
||||
}
|
||||
|
||||
// --- Node retrieval ---
|
||||
|
||||
fn engram_get_node(id: String) -> String {
|
||||
return __engram_get_node(id)
|
||||
}
|
||||
|
||||
fn engram_node_count() -> Int {
|
||||
return __engram_node_count()
|
||||
}
|
||||
|
||||
// --- Node lifecycle ---
|
||||
|
||||
fn engram_strengthen(id: String) -> Bool {
|
||||
return __engram_strengthen(id)
|
||||
}
|
||||
|
||||
fn engram_forget(id: String) -> Bool {
|
||||
return __engram_forget(id)
|
||||
}
|
||||
|
||||
// --- Search and scan ---
|
||||
|
||||
fn engram_search(query: String, limit: Int) -> String {
|
||||
return __engram_search(query, limit)
|
||||
}
|
||||
|
||||
fn engram_scan_nodes(limit: Int, offset: Int) -> String {
|
||||
return __engram_scan_nodes(limit, offset)
|
||||
}
|
||||
|
||||
fn engram_scan_nodes_json(limit: Int, offset: Int) -> String {
|
||||
return __engram_scan_nodes_json(limit, offset)
|
||||
}
|
||||
|
||||
// --- Graph edges ---
|
||||
|
||||
fn engram_connect(from: String, to: String, rel: String, weight: Float) -> Bool {
|
||||
return __engram_connect(from, to, rel, weight)
|
||||
}
|
||||
|
||||
fn engram_edge_between(a: String, b: String) -> String {
|
||||
return __engram_edge_between(a, b)
|
||||
}
|
||||
|
||||
// --- Graph traversal ---
|
||||
|
||||
fn engram_neighbors(id: String) -> String {
|
||||
return __engram_neighbors(id)
|
||||
}
|
||||
|
||||
fn engram_neighbors_filtered(id: String, rel: String, min_w: Float) -> String {
|
||||
return __engram_neighbors_filtered(id, rel, min_w)
|
||||
}
|
||||
|
||||
fn engram_activate(query: String, depth: Int) -> String {
|
||||
return __engram_activate(query, depth)
|
||||
}
|
||||
|
||||
fn engram_activate_json(query: String, limit: Int) -> String {
|
||||
return __engram_activate_json(query, limit)
|
||||
}
|
||||
|
||||
// --- Generation ---
|
||||
|
||||
fn generate(form: String) -> String {
|
||||
return __generate(form)
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
// runtime/env.el — environment and process
|
||||
// Covers: environment variables, command-line args, process exit, in-process
|
||||
// state store, UUID generation, and list convenience helpers.
|
||||
|
||||
// env — read an environment variable. Returns "" if the variable is not set.
|
||||
fn env(key: String) -> String {
|
||||
return __env_get(key)
|
||||
}
|
||||
|
||||
// args — command-line arguments as a list of strings.
|
||||
// __args_json returns a JSON array (e.g. ["prog","arg1","arg2"]).
|
||||
// The list is built by iterating over the array.
|
||||
fn args() -> [String] {
|
||||
let json: String = __args_json()
|
||||
let n: Int = json_array_len(json)
|
||||
let result: [String] = el_list_empty()
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
let item: String = json_array_get_string(json, i)
|
||||
let result = el_list_append(result, item)
|
||||
let i = i + 1
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// exit_program — terminate the process with the given exit code.
|
||||
fn exit_program(code: Int) {
|
||||
__exit_program(code)
|
||||
}
|
||||
|
||||
// ── List convenience helpers ───────────────────────────────────────────────
|
||||
|
||||
// get — index into a list. Thin alias for el_list_get used throughout the
|
||||
// El stdlib so call sites read like `get(lst, i)` rather than the verbose form.
|
||||
fn get(lst: [String], i: Int) -> String {
|
||||
return el_list_get(lst, i)
|
||||
}
|
||||
|
||||
// len — length of a list.
|
||||
fn len(lst: [String]) -> Int {
|
||||
return el_list_len(lst)
|
||||
}
|
||||
|
||||
// ── In-process key-value state store ──────────────────────────────────────
|
||||
|
||||
// state_set — store a string value under key.
|
||||
fn state_set(key: String, val: String) {
|
||||
__state_set(key, val)
|
||||
}
|
||||
|
||||
// state_get — retrieve value for key; returns "" if key not present.
|
||||
fn state_get(key: String) -> String {
|
||||
return __state_get(key)
|
||||
}
|
||||
|
||||
// state_del — remove key from the store.
|
||||
fn state_del(key: String) {
|
||||
__state_del(key)
|
||||
}
|
||||
|
||||
// state_keys — all keys currently in the store as a JSON array string.
|
||||
fn state_keys() -> String {
|
||||
return __state_keys()
|
||||
}
|
||||
|
||||
// ── DHARMA runtime helpers ─────────────────────────────────────────────────
|
||||
|
||||
// config — read a configuration value from the environment.
|
||||
// Returns "" if the variable is not set. Alias for env().
|
||||
fn config(key: String) -> String {
|
||||
return __env_get(key)
|
||||
}
|
||||
|
||||
// log_info — write an [INFO] log line to stdout.
|
||||
fn log_info(msg: String) {
|
||||
__println("[INFO] " + msg)
|
||||
}
|
||||
|
||||
// log_warn — write a [WARN] log line to stdout.
|
||||
fn log_warn(msg: String) {
|
||||
__println("[WARN] " + msg)
|
||||
}
|
||||
|
||||
// list_len — return the number of elements in a list. Alias for el_list_len.
|
||||
fn list_len(lst: [String]) -> Int {
|
||||
return el_list_len(lst)
|
||||
}
|
||||
|
||||
// list_get — return the element at index i in a list. Alias for el_list_get.
|
||||
fn list_get(lst: [String], i: Int) -> String {
|
||||
return el_list_get(lst, i)
|
||||
}
|
||||
|
||||
// ── UUID generation ────────────────────────────────────────────────────────
|
||||
|
||||
// uuid_new — generate a new random UUID v4.
|
||||
fn uuid_new() -> String {
|
||||
return __uuid_v4()
|
||||
}
|
||||
|
||||
// uuid_v4 — alias for uuid_new(); explicit version name for callers that
|
||||
// need to be precise about the UUID variant.
|
||||
fn uuid_v4() -> String {
|
||||
return __uuid_v4()
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
// runtime/exec.el — subprocess execution
|
||||
// All four names resolve to the same seed primitives so callers can use
|
||||
// whichever name matches their mental model of the operation.
|
||||
|
||||
// exec — run a shell command, capture stdout, return as String.
|
||||
// Blocks until the subprocess exits (30-second wall-clock deadline in the
|
||||
// seed layer). Returns "" on any error.
|
||||
fn exec(cmd: String) -> String {
|
||||
return __exec(cmd)
|
||||
}
|
||||
|
||||
// exec_bg — fire-and-forget subprocess. Returns immediately; no stdout.
|
||||
fn exec_bg(cmd: String) {
|
||||
__exec_bg(cmd)
|
||||
}
|
||||
|
||||
// exec_command — alias for exec(); preferred when callers care about side
|
||||
// effects (e.g. invoking a build tool) rather than captured output.
|
||||
fn exec_command(cmd: String) -> String {
|
||||
return __exec(cmd)
|
||||
}
|
||||
|
||||
// exec_capture — alias for exec(); preferred when callers explicitly want
|
||||
// to capture and process stdout.
|
||||
fn exec_capture(cmd: String) -> String {
|
||||
return __exec(cmd)
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
// runtime/fs.el — filesystem operations
|
||||
// Thin El wrappers over seed primitives; no logic beyond what is needed
|
||||
// to present a clean API. The heavy lifting lives in el_runtime.c.
|
||||
|
||||
fn fs_read(path: String) -> String {
|
||||
return __fs_read(path)
|
||||
}
|
||||
|
||||
fn fs_write(path: String, content: String) -> Bool {
|
||||
return __fs_write(path, content)
|
||||
}
|
||||
|
||||
fn fs_exists(path: String) -> Bool {
|
||||
return __fs_exists(path)
|
||||
}
|
||||
|
||||
fn fs_mkdir(path: String) -> Bool {
|
||||
return __fs_mkdir(path)
|
||||
}
|
||||
|
||||
fn fs_write_bytes(path: String, bytes: String, n: Int) -> Bool {
|
||||
return __fs_write_bytes(path, bytes, n)
|
||||
}
|
||||
|
||||
// fs_list — return list of filenames in a directory.
|
||||
// __fs_list_raw returns a newline-separated string (possibly with a trailing
|
||||
// newline); callers that need a clean list should filter empty strings.
|
||||
fn fs_list(path: String) -> [String] {
|
||||
let raw: String = __fs_list_raw(path)
|
||||
return str_split(raw, "\n")
|
||||
}
|
||||
|
||||
// fs_list_json — return a JSON array of filenames in a directory.
|
||||
// Empty strings produced by a trailing newline are stripped before encoding.
|
||||
fn fs_list_json(path: String) -> String {
|
||||
let items: [String] = fs_list(path)
|
||||
let n: Int = el_list_len(items)
|
||||
let clean: [String] = el_list_empty()
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
let item: String = el_list_get(items, i)
|
||||
let trimmed: String = str_trim(item)
|
||||
if !str_eq(trimmed, "") {
|
||||
let clean = el_list_append(clean, "\"" + trimmed + "\"")
|
||||
}
|
||||
let i = i + 1
|
||||
}
|
||||
return json_build_array(clean)
|
||||
}
|
||||
@@ -0,0 +1,250 @@
|
||||
// runtime/http.el — El HTTP client and server wrappers
|
||||
//
|
||||
// Thin El layer over seed primitives. All network I/O is performed by the
|
||||
// seed; this file provides the public API that El programs import.
|
||||
//
|
||||
// Seed primitives consumed:
|
||||
// __http_do(method, url, body, headers_json, timeout_ms) -> String
|
||||
// __http_do_to_file(method, url, body, headers_json, out_path) -> Bool
|
||||
// __http_do_map(method, url, body, headers_map, timeout_ms) -> String
|
||||
// __http_do_map_to_file(method, url, body, headers_map, out_path) -> Bool
|
||||
// __http_serve(port, handler_name)
|
||||
// __http_serve_v2(port, handler_name)
|
||||
// __http_response(status, headers_json, body) -> String
|
||||
// __env_get(key) -> String
|
||||
//
|
||||
// NOTE FOR SEED AGENT: __http_do_map and __http_do_map_to_file must be added
|
||||
// to the seed. They are identical to __http_do / __http_do_to_file except
|
||||
// they accept an ElMap directly for headers instead of a pre-serialised JSON
|
||||
// string. This avoids needing map iteration in El (which has no for-loop or
|
||||
// map iterator primitive). The seed implementation maps to headers_from_map()
|
||||
// in el_runtime.c.
|
||||
//
|
||||
// Other builtins used:
|
||||
// str_eq(a, b) -> Bool
|
||||
// str_to_int(s) -> Int
|
||||
|
||||
// ── Timeout helper ────────────────────────────────────────────────────────────
|
||||
|
||||
// el_http_timeout_ms returns the configured HTTP timeout in milliseconds.
|
||||
// Reads EL_HTTP_TIMEOUT_MS from the environment; defaults to 60000 (60s).
|
||||
// Returns 60000 if the env var is absent, empty, or non-positive.
|
||||
fn el_http_timeout_ms() -> Int {
|
||||
let v: String = __env_get("EL_HTTP_TIMEOUT_MS")
|
||||
if str_eq(v, "") { return 60000 }
|
||||
let n: Int = str_to_int(v)
|
||||
if n <= 0 { return 60000 }
|
||||
return n
|
||||
}
|
||||
|
||||
// ── HTTP client — simple variants ────────────────────────────────────────────
|
||||
|
||||
// http_get performs an HTTP GET request and returns the response body.
|
||||
// On transport failure the seed returns an error JSON fragment.
|
||||
fn http_get(url: String) -> String {
|
||||
return __http_do("GET", url, "", "{}", el_http_timeout_ms())
|
||||
}
|
||||
|
||||
// http_post performs an HTTP POST request with the given body.
|
||||
// No Content-Type header is set; use http_post_json for JSON payloads.
|
||||
fn http_post(url: String, body: String) -> String {
|
||||
return __http_do("POST", url, body, "{}", el_http_timeout_ms())
|
||||
}
|
||||
|
||||
// http_post_json performs an HTTP POST request with Content-Type:
|
||||
// application/json. body must be a valid JSON string.
|
||||
fn http_post_json(url: String, body: String) -> String {
|
||||
return __http_do("POST", url, body, "{\"Content-Type\":\"application/json\"}", el_http_timeout_ms())
|
||||
}
|
||||
|
||||
// http_delete performs an HTTP DELETE request and returns the response body.
|
||||
fn http_delete(url: String) -> String {
|
||||
return __http_do("DELETE", url, "", "{}", el_http_timeout_ms())
|
||||
}
|
||||
|
||||
// ── HTTP client — header map variants ────────────────────────────────────────
|
||||
//
|
||||
// These accept a Map<String, String> of request headers. The seed's
|
||||
// __http_do_map converts the ElMap to a curl_slist internally, matching
|
||||
// the headers_from_map() logic in el_runtime.c.
|
||||
|
||||
// http_get_with_headers performs an HTTP GET with caller-supplied headers.
|
||||
fn http_get_with_headers(url: String, headers: Map<String, String>) -> String {
|
||||
return __http_do_map("GET", url, "", headers, el_http_timeout_ms())
|
||||
}
|
||||
|
||||
// http_post_with_headers performs an HTTP POST with caller-supplied headers.
|
||||
fn http_post_with_headers(url: String, body: String, headers: Map<String, String>) -> String {
|
||||
return __http_do_map("POST", url, body, headers, el_http_timeout_ms())
|
||||
}
|
||||
|
||||
// http_post_form_auth performs an HTTP POST with
|
||||
// Content-Type: application/x-www-form-urlencoded and an Authorization
|
||||
// header built from auth_header (the caller passes the full header value,
|
||||
// e.g. "Bearer <token>" or "Basic <base64>").
|
||||
//
|
||||
// Mirrors http_post_form_auth in el_runtime.c: two headers are injected,
|
||||
// Content-Type is always set; Authorization is omitted when auth_header is "".
|
||||
fn http_post_form_auth(url: String, form_body: String, auth_header: String) -> String {
|
||||
if str_eq(auth_header, "") {
|
||||
return __http_do("POST", url, form_body, "{\"Content-Type\":\"application/x-www-form-urlencoded\"}", el_http_timeout_ms())
|
||||
}
|
||||
return __http_do("POST", url, form_body, "{\"Content-Type\":\"application/x-www-form-urlencoded\",\"Authorization\":\"" + auth_header + "\"}", el_http_timeout_ms())
|
||||
}
|
||||
|
||||
// ── HTTP client — streaming to file ──────────────────────────────────────────
|
||||
//
|
||||
// These route the response body directly to a file via the seed, bypassing
|
||||
// the El string layer. This preserves embedded NUL bytes in binary payloads
|
||||
// (audio, images, etc.) — an El string would truncate at the first NUL.
|
||||
// Returns true on success, false on any transport or I/O error.
|
||||
|
||||
// http_post_to_file performs an HTTP POST and streams the response body to
|
||||
// output_path. Useful for large or binary response payloads.
|
||||
fn http_post_to_file(url: String, body: String, headers: Map<String, String>, output_path: String) -> Bool {
|
||||
return __http_do_map_to_file("POST", url, body, headers, output_path)
|
||||
}
|
||||
|
||||
// http_get_to_file performs an HTTP GET and streams the response body to
|
||||
// output_path. Useful for large or binary response payloads.
|
||||
fn http_get_to_file(url: String, headers: Map<String, String>, output_path: String) -> Bool {
|
||||
return __http_do_map_to_file("GET", url, "", headers, output_path)
|
||||
}
|
||||
|
||||
// ── HTTP server ───────────────────────────────────────────────────────────────
|
||||
//
|
||||
// El programs call http_set_handler(name) to register which El function
|
||||
// handles requests, then http_serve(port, name) to start listening.
|
||||
// The seed resolves handler names via dlsym — every El fn compiles to a
|
||||
// global C symbol with the same name, so self-registration works without
|
||||
// any El-level registry.
|
||||
//
|
||||
// v2 widens the handler signature from
|
||||
// (method, path, body) -> String
|
||||
// to
|
||||
// (method, path, headers_map, body) -> String
|
||||
// so handlers can inspect incoming headers. Use http_serve_v2 +
|
||||
// http_set_handler_v2 for v2 handlers.
|
||||
|
||||
// http_set_handler registers name as the active v1 request handler.
|
||||
// The seed resolves the symbol via dlsym at call time; no El-level
|
||||
// registration is needed. This is a no-op at the El layer.
|
||||
fn http_set_handler(name: String) {
|
||||
// no-op: the seed handles handler registration via dlsym
|
||||
}
|
||||
|
||||
// http_serve starts an HTTP/1.1 server on port, dispatching every request
|
||||
// to handler (a v1 handler: fn(method, path, body) -> String).
|
||||
// Blocks forever. Accepts both IPv4 and IPv6 (dual-stack).
|
||||
fn http_serve(port: Int, handler: String) {
|
||||
__http_serve(port, handler)
|
||||
}
|
||||
|
||||
// http_set_handler_v2 registers name as the active v2 request handler.
|
||||
// No-op at the El layer; the seed uses dlsym.
|
||||
fn http_set_handler_v2(name: String) {
|
||||
// no-op: the seed handles handler registration via dlsym
|
||||
}
|
||||
|
||||
// http_serve_v2 starts an HTTP/1.1 server on port, dispatching every
|
||||
// request to handler (a v2 handler: fn(method, path, headers, body) ->
|
||||
// String). Blocks forever. Accepts both IPv4 and IPv6 (dual-stack).
|
||||
fn http_serve_v2(port: Int, handler: String) {
|
||||
__http_serve_v2(port, handler)
|
||||
}
|
||||
|
||||
// ── Response construction ─────────────────────────────────────────────────────
|
||||
|
||||
// http_response builds a structured response envelope that the HTTP server
|
||||
// runtime unpacks into a real HTTP response with the given status code and
|
||||
// headers. status must be 100–599 (defaults to 200 outside that range).
|
||||
// headers_json must be a JSON object literal (e.g. "{}" or
|
||||
// "{\"Content-Type\":\"text/html\"}"); body is the response body string.
|
||||
//
|
||||
// The envelope format is:
|
||||
// {"el_http_response":1,"status":<n>,"headers":<obj>,"body":"<escaped>"}
|
||||
// The runtime detects this prefix and unpacks it; plain string returns from
|
||||
// handlers are still supported and are sent as HTTP 200 with auto-detected
|
||||
// Content-Type.
|
||||
fn http_response(status: Int, headers_json: String, body: String) -> String {
|
||||
return __http_response(status, headers_json, body)
|
||||
}
|
||||
|
||||
// ── HTTP client — PATCH ───────────────────────────────────────────────────────
|
||||
|
||||
// http_patch performs an HTTP PATCH request with Content-Type: application/json.
|
||||
fn http_patch(url: String, body: String) -> String {
|
||||
return __http_do("PATCH", url, body, "{\"Content-Type\":\"application/json\"}", el_http_timeout_ms())
|
||||
}
|
||||
|
||||
// ── HTTP client — Engram variants (optional API key) ──────────────────────────
|
||||
//
|
||||
// These are used by dharma's db.el to talk to Engram nodes.
|
||||
// The key parameter is the X-API-Key header value; pass "" for no auth.
|
||||
|
||||
// http_post_engram performs an HTTP POST with Content-Type: application/json
|
||||
// and an optional X-API-Key header. If key is "" no auth header is added.
|
||||
fn http_post_engram(url: String, key: String, body: String) -> String {
|
||||
if str_eq(key, "") {
|
||||
return __http_do("POST", url, body, "{\"Content-Type\":\"application/json\"}", el_http_timeout_ms())
|
||||
}
|
||||
return __http_do("POST", url, body, "{\"Content-Type\":\"application/json\",\"X-API-Key\":\"" + key + "\"}", el_http_timeout_ms())
|
||||
}
|
||||
|
||||
// http_get_engram performs an HTTP GET with an optional X-API-Key header.
|
||||
fn http_get_engram(url: String, key: String) -> String {
|
||||
if str_eq(key, "") {
|
||||
return __http_do("GET", url, "", "{}", el_http_timeout_ms())
|
||||
}
|
||||
return __http_do("GET", url, "", "{\"X-API-Key\":\"" + key + "\"}", el_http_timeout_ms())
|
||||
}
|
||||
|
||||
// ── SSE — Server-Sent Events streaming ───────────────────────────────────────
|
||||
//
|
||||
// Usage pattern for an SSE handler:
|
||||
//
|
||||
// fn my_handler(method: String, path: String, headers: Map<String, String>, body: String) -> String {
|
||||
// let fd: Int = http_conn_fd()
|
||||
// http_sse_open(fd)
|
||||
// http_sse_send(fd, "hello")
|
||||
// http_sse_send(fd, "world")
|
||||
// http_sse_close(fd)
|
||||
// return http_sse_sentinel()
|
||||
// }
|
||||
//
|
||||
// The sentinel return value tells http_serve_v2 NOT to close the connection
|
||||
// automatically — the handler already closed it via http_sse_close.
|
||||
|
||||
// http_conn_fd returns the raw file descriptor for the current HTTP connection.
|
||||
// Only valid inside an http_serve_v2 handler, before the handler returns.
|
||||
// Use with http_sse_open / http_sse_send / http_sse_close for streaming.
|
||||
fn http_conn_fd() -> Int {
|
||||
return __http_conn_fd()
|
||||
}
|
||||
|
||||
// http_sse_open sends SSE response headers on the current connection,
|
||||
// keeping it open for streaming. Call once at the start of an SSE handler.
|
||||
// Returns true on success.
|
||||
fn http_sse_open(fd: Int) -> Bool {
|
||||
return __http_sse_open(fd)
|
||||
}
|
||||
|
||||
// http_sse_send writes one SSE event to the connection.
|
||||
// data should not contain newlines (they are added automatically).
|
||||
// Returns true if the write succeeded (client still connected).
|
||||
fn http_sse_send(fd: Int, data: String) -> Bool {
|
||||
return __http_sse_send(fd, data)
|
||||
}
|
||||
|
||||
// http_sse_close closes the SSE connection.
|
||||
fn http_sse_close(fd: Int) {
|
||||
__http_sse_close(fd)
|
||||
return
|
||||
}
|
||||
|
||||
// http_sse_sentinel is the return value an SSE handler must return
|
||||
// to tell the HTTP server NOT to close the connection automatically.
|
||||
// The handler takes ownership of the fd and closes it via http_sse_close.
|
||||
fn http_sse_sentinel() -> String {
|
||||
return "__sse__"
|
||||
}
|
||||
@@ -0,0 +1,213 @@
|
||||
// runtime/json.el — El JSON operations
|
||||
//
|
||||
// Thin El wrappers over seed JSON primitives, plus pure-El builders and
|
||||
// helpers. Each function here corresponds to (and replaces) a C function
|
||||
// from el-compiler/runtime/legacy/el_runtime.c (lines 2692–3333).
|
||||
//
|
||||
// Seed primitives consumed by this module:
|
||||
// __json_get(json, key) -> String (value as string)
|
||||
// __json_get_raw(json, key) -> String (raw JSON token)
|
||||
// __json_parse_map(s) -> Map<String, Any>
|
||||
// __json_stringify_val(v) -> String
|
||||
// __json_array_len(arr) -> Int
|
||||
// __json_array_get(arr, i) -> String (element as JSON fragment)
|
||||
// __json_array_get_string(arr, i) -> String (element as string value)
|
||||
// __json_set(json, key, value) -> String (JSON mutation)
|
||||
// __str_to_int(s) -> Int
|
||||
// __str_to_float(s) -> Float
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Core — thin wrappers that delegate directly to seed
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// json_get — extract a value from a JSON object as a string.
|
||||
// Supports dot-path traversal ("a.b.c") and array indices ("items.0.name").
|
||||
fn json_get(json: String, key: String) -> String {
|
||||
return __json_get(json, key)
|
||||
}
|
||||
|
||||
// json_get_raw — extract a raw JSON token (the un-decoded fragment) for a key.
|
||||
// Useful when the caller wants to pass a sub-object to another JSON function.
|
||||
fn json_get_raw(json: String, key: String) -> String {
|
||||
return __json_get_raw(json, key)
|
||||
}
|
||||
|
||||
// json_parse — parse a JSON string into a Map<String, Any>.
|
||||
// Arrays become ElList; objects become ElMap; scalars are typed values.
|
||||
fn json_parse(s: String) -> Map<String, Any> {
|
||||
return __json_parse_map(s)
|
||||
}
|
||||
|
||||
// json_stringify — serialize an El value (ElMap, ElList, String, Int) to JSON.
|
||||
fn json_stringify(v: Any) -> String {
|
||||
return __json_stringify_val(v)
|
||||
}
|
||||
|
||||
// json_array_len — return the number of elements in a JSON array string.
|
||||
fn json_array_len(arr: String) -> Int {
|
||||
return __json_array_len(arr)
|
||||
}
|
||||
|
||||
// json_array_get — return the i-th element of a JSON array as a JSON fragment.
|
||||
// Nested objects and arrays are returned verbatim. Out-of-range -> "".
|
||||
fn json_array_get(arr: String, i: Int) -> String {
|
||||
return __json_array_get(arr, i)
|
||||
}
|
||||
|
||||
// json_array_get_string — return the i-th element of a JSON array as a plain
|
||||
// string value (quotes and escape sequences removed). Non-string elements
|
||||
// and out-of-range indices yield "".
|
||||
fn json_array_get_string(arr: String, i: Int) -> String {
|
||||
return __json_array_get_string(arr, i)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Typed extractors — delegate to seed then convert
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// json_get_string — extract a string value for a key.
|
||||
// Equivalent to json_get but named explicitly for readability.
|
||||
fn json_get_string(json: String, key: String) -> String {
|
||||
return __json_get(json, key)
|
||||
}
|
||||
|
||||
// json_get_int — extract an integer value for a key.
|
||||
fn json_get_int(json: String, key: String) -> Int {
|
||||
let s: String = __json_get(json, key)
|
||||
return str_to_int(s)
|
||||
}
|
||||
|
||||
// json_get_float — extract a floating-point value for a key.
|
||||
fn json_get_float(json: String, key: String) -> Float {
|
||||
let s: String = __json_get(json, key)
|
||||
return str_to_float(s)
|
||||
}
|
||||
|
||||
// json_get_bool — extract a boolean value for a key.
|
||||
// Returns true only when the raw JSON token is the literal "true".
|
||||
fn json_get_bool(json: String, key: String) -> Bool {
|
||||
let s: String = __json_get(json, key)
|
||||
return str_eq(s, "true")
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Mutation
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// json_set — set or insert a key/value pair in a JSON object string.
|
||||
// If the key already exists its value is replaced in-place; otherwise the
|
||||
// pair is appended before the closing brace. The value must already be a
|
||||
// valid JSON-encoded string (e.g. a quoted string, number, or sub-object).
|
||||
fn json_set(json: String, key: String, value: String) -> String {
|
||||
return __json_set(json, key, value)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Pure-El builders — no seed call required
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// json_build_object — build a JSON object from alternating key/value strings.
|
||||
//
|
||||
// keys_and_values must contain an even number of elements laid out as:
|
||||
// [key0, val0, key1, val1, ...]
|
||||
//
|
||||
// Both keys and values are assumed to be plain strings that will be
|
||||
// double-quoted and JSON-escaped by this function. Pass a pre-encoded
|
||||
// number or sub-object as the value if you need non-string JSON types.
|
||||
//
|
||||
// Example:
|
||||
// json_build_object(["name", "alice", "role", "admin"])
|
||||
// -> {"name":"alice","role":"admin"}
|
||||
fn json_build_object(keys_and_values: [String]) -> String {
|
||||
let n: Int = el_list_len(keys_and_values)
|
||||
let result: String = "{"
|
||||
let i: Int = 0
|
||||
while i < n - 1 {
|
||||
let key: String = el_list_get(keys_and_values, i)
|
||||
let val: String = el_list_get(keys_and_values, i + 1)
|
||||
let sep: String = if i == 0 { "" } else { "," }
|
||||
let escaped_key: String = json_escape_string(key)
|
||||
let escaped_val: String = json_escape_string(val)
|
||||
let result = result + sep + "\"" + escaped_key + "\":\"" + escaped_val + "\""
|
||||
let i = i + 2
|
||||
}
|
||||
return result + "}"
|
||||
}
|
||||
|
||||
// json_build_array — build a JSON array from a list of already-JSON-encoded
|
||||
// strings.
|
||||
//
|
||||
// Each element in items must be a valid JSON fragment (quoted string, number,
|
||||
// object, array, or literal). The function joins them with commas and wraps
|
||||
// the result in brackets.
|
||||
//
|
||||
// Example:
|
||||
// json_build_array(["\"alice\"", "\"bob\""])
|
||||
// -> ["alice","bob"]
|
||||
fn json_build_array(items: [String]) -> String {
|
||||
let n: Int = el_list_len(items)
|
||||
let result: String = "["
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
let item: String = el_list_get(items, i)
|
||||
let sep: String = if i == 0 { "" } else { "," }
|
||||
let result = result + sep + item
|
||||
let i = i + 1
|
||||
}
|
||||
return result + "]"
|
||||
}
|
||||
|
||||
// json_array_push — append a pre-encoded JSON element to a JSON array string.
|
||||
// elem must be a valid JSON fragment (e.g. "\"foo\"" or "42").
|
||||
// Returns a new JSON array string with elem appended.
|
||||
// Example: json_array_push("[]", "\"alice\"") -> "[\"alice\"]"
|
||||
fn json_array_push(arr: String, elem: String) -> String {
|
||||
let n: Int = json_array_len(arr)
|
||||
if n == 0 {
|
||||
return "[" + elem + "]"
|
||||
}
|
||||
// arr ends with ']'; insert before it
|
||||
let inner_end: Int = str_last_index_of(arr, "]")
|
||||
if inner_end < 0 {
|
||||
return "[" + elem + "]"
|
||||
}
|
||||
let prefix: String = str_slice(arr, 0, inner_end)
|
||||
return prefix + "," + elem + "]"
|
||||
}
|
||||
|
||||
// json_escape_string — escape a raw string so it can be safely embedded as a
|
||||
// JSON string value.
|
||||
//
|
||||
// Characters escaped: backslash, double-quote, newline, carriage return, tab.
|
||||
// The returned value does NOT include surrounding double-quotes; wrap it in
|
||||
// quotes if you need a complete JSON string literal.
|
||||
fn json_escape_string(s: String) -> String {
|
||||
let s1: String = str_replace(s, "\\", "\\\\")
|
||||
let s2: String = str_replace(s1, "\"", "\\\"")
|
||||
let s3: String = str_replace(s2, "\n", "\\n")
|
||||
let s4: String = str_replace(s3, "\r", "\\r")
|
||||
let s5: String = str_replace(s4, "\t", "\\t")
|
||||
return s5
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// DHARMA byte decoding
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// bytes_to_str — decode a JSON array of integer byte values back to a string.
|
||||
// "[104,105]" -> "hi"
|
||||
// Inverse of str_to_bytes (defined in string.el). Defined here because it
|
||||
// depends on json_array_len and json_array_get_string which live in this file.
|
||||
fn bytes_to_str(arr: String) -> String {
|
||||
let n: Int = json_array_len(arr)
|
||||
if n == 0 { return "" }
|
||||
let out: String = __str_alloc(n)
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
let elem: String = json_array_get_string(arr, i)
|
||||
let b: Int = __str_to_int(elem)
|
||||
out = __str_set_char(out, i, b)
|
||||
i = i + 1
|
||||
}
|
||||
return out
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
// runtime/manifest.el — El runtime module manifest
|
||||
//
|
||||
// Load order for runtime compilation. Each module may depend on modules
|
||||
// listed before it. The build system concatenates these in this order,
|
||||
// then compiles the combined source.
|
||||
//
|
||||
// Modules:
|
||||
// 1. runtime/string.el — string operations (no dependencies)
|
||||
// 2. runtime/math.el — numeric/float operations (no dependencies)
|
||||
// 3. runtime/state.el — in-process key-value (no dependencies)
|
||||
// 4. runtime/env.el — environment, process, args, uuid
|
||||
// 5. runtime/fs.el — filesystem operations (depends: string)
|
||||
// 6. runtime/exec.el — subprocess execution (depends: string)
|
||||
// 7. runtime/time.el — time, date, calendar (depends: string, math)
|
||||
// 8. runtime/json.el — JSON operations (depends: string)
|
||||
// 9. runtime/http.el — HTTP client+server (depends: string, json)
|
||||
// 10. runtime/engram.el — graph store (depends: string, json)
|
||||
// 11. runtime/thread.el — threading, parallel_map (depends: all above)
|
||||
// 12. runtime/collections.el — list/map higher-level ops (depends: string)
|
||||
//
|
||||
// Build command (from el/ root):
|
||||
// cat runtime/string.el runtime/math.el runtime/state.el runtime/env.el \
|
||||
// runtime/fs.el runtime/exec.el runtime/time.el runtime/json.el \
|
||||
// runtime/http.el runtime/engram.el runtime/thread.el \
|
||||
// runtime/collections.el \
|
||||
// <user-program.el> > combined.el
|
||||
// ./dist/platform/elc combined.el > output.c
|
||||
// cc -std=c11 -I el-compiler/runtime -lcurl -lpthread \
|
||||
// -o output output.c el-compiler/runtime/el_seed.c
|
||||
|
||||
// This file itself is not compiled — it is documentation only.
|
||||
fn runtime_version() -> String {
|
||||
return "2.0.0-el-native"
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
// runtime/math.el — Float math, integer utilities, and numeric conversions.
|
||||
//
|
||||
// Implements the math/float surface from el-compiler/runtime/legacy/el_runtime.c
|
||||
// (lines 303–305 for el_abs/max/min, lines 4725–4771 for float/format ops)
|
||||
// in pure El, using seed primitives.
|
||||
//
|
||||
// Seed primitives consumed:
|
||||
// __sqrt_f(f: Float) -> Float
|
||||
// __log_f(f: Float) -> Float
|
||||
// __ln_f(f: Float) -> Float
|
||||
// __sin_f(f: Float) -> Float
|
||||
// __cos_f(f: Float) -> Float
|
||||
// __pi_f() -> Float
|
||||
// __float_to_str(f: Float) -> String
|
||||
// __str_to_float(s: String) -> Float
|
||||
// __int_to_str(n: Int) -> String
|
||||
// __str_to_int(s: String) -> Int
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Integer math — el_abs, el_max, el_min.
|
||||
//
|
||||
// Matches legacy el_abs, el_max, el_min (lines 303–305).
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// el_abs — absolute value of an integer.
|
||||
fn el_abs(n: Int) -> Int {
|
||||
if n < 0 { return -n }
|
||||
return n
|
||||
}
|
||||
|
||||
// el_max — larger of two integers.
|
||||
fn el_max(a: Int, b: Int) -> Int {
|
||||
if a > b { return a }
|
||||
return b
|
||||
}
|
||||
|
||||
// el_min — smaller of two integers.
|
||||
fn el_min(a: Int, b: Int) -> Int {
|
||||
if a < b { return a }
|
||||
return b
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Float math — thin wrappers over seed primitives.
|
||||
//
|
||||
// Matches legacy math_sqrt, math_log, math_ln, math_sin, math_cos, math_pi
|
||||
// (lines 4766–4771).
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// math_sqrt — square root.
|
||||
fn math_sqrt(f: Float) -> Float {
|
||||
return __sqrt_f(f)
|
||||
}
|
||||
|
||||
// math_log — base-10 logarithm.
|
||||
fn math_log(f: Float) -> Float {
|
||||
return __log_f(f)
|
||||
}
|
||||
|
||||
// math_ln — natural logarithm.
|
||||
fn math_ln(f: Float) -> Float {
|
||||
return __ln_f(f)
|
||||
}
|
||||
|
||||
// math_sin — sine (radians).
|
||||
fn math_sin(f: Float) -> Float {
|
||||
return __sin_f(f)
|
||||
}
|
||||
|
||||
// math_cos — cosine (radians).
|
||||
fn math_cos(f: Float) -> Float {
|
||||
return __cos_f(f)
|
||||
}
|
||||
|
||||
// math_pi — the constant π.
|
||||
fn math_pi() -> Float {
|
||||
return __pi_f()
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Float conversions — float_to_str, int_to_float, float_to_int, str_to_float.
|
||||
//
|
||||
// Matches legacy float_to_str, int_to_float, float_to_int, str_to_float
|
||||
// (lines 4725–4762).
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// float_to_str — format a float using %g (shortest exact representation).
|
||||
// Matches legacy float_to_str() → snprintf "%g".
|
||||
fn float_to_str(f: Float) -> String {
|
||||
return __float_to_str(f)
|
||||
}
|
||||
|
||||
// int_to_float — convert an integer to a float.
|
||||
// Matches legacy int_to_float() → (double)(int64_t)n.
|
||||
fn int_to_float(n: Int) -> Float {
|
||||
return __int_to_float(n)
|
||||
}
|
||||
|
||||
// float_to_int — truncate a float to an integer (toward zero).
|
||||
// Matches legacy float_to_int() → (int64_t)el_to_float(f).
|
||||
fn float_to_int(f: Float) -> Int {
|
||||
return __float_to_int(f)
|
||||
}
|
||||
|
||||
// str_to_float — parse a float from a string. Returns 0.0 on failure.
|
||||
// Matches legacy str_to_float() → strtod(str, NULL).
|
||||
fn str_to_float(s: String) -> Float {
|
||||
return __str_to_float(s)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// format_float — format a float to a fixed number of decimal places.
|
||||
//
|
||||
// decimals is clamped to [0, 30]. Matches legacy format_float() → snprintf "%.*f".
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
fn format_float(f: Float, decimals: Int) -> String {
|
||||
let d: Int = decimals
|
||||
if d < 0 { d = 0 }
|
||||
if d > 30 { d = 30 }
|
||||
// Delegate to seed; the seed exposes __format_float(f, d) -> String.
|
||||
// This matches snprintf(buf, 128, "%.*f", d, v) in the legacy runtime.
|
||||
return __format_float(f, d)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// decimal_round — round a float to d decimal places (half-away-from-zero).
|
||||
//
|
||||
// Matches legacy decimal_round():
|
||||
// mul = pow(10, d)
|
||||
// r = (v >= 0 ? floor(v*mul + 0.5) : -floor(-v*mul + 0.5)) / mul
|
||||
//
|
||||
// We implement pow(10, d) via a loop (d <= 15, so at most 15 multiplications).
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// _pow10 — 10^n as a Float for n in [0, 15].
|
||||
fn _pow10(n: Int) -> Float {
|
||||
let result: Float = 1.0
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
result = result * 10.0
|
||||
i = i + 1
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// _floor_f — floor of a float: largest integer <= f.
|
||||
// Uses __float_to_int (truncation) with correction for negative non-integers.
|
||||
fn _floor_f(f: Float) -> Float {
|
||||
let t: Int = __float_to_int(f)
|
||||
let tf: Float = __int_to_float(t)
|
||||
// if f was negative and not already an integer, subtract 1
|
||||
if f < 0.0 {
|
||||
if tf > f {
|
||||
return tf - 1.0
|
||||
}
|
||||
}
|
||||
return tf
|
||||
}
|
||||
|
||||
fn decimal_round(f: Float, decimals: Int) -> Float {
|
||||
let d: Int = decimals
|
||||
if d < 0 { d = 0 }
|
||||
if d > 15 { d = 15 }
|
||||
let mul: Float = _pow10(d)
|
||||
if f >= 0.0 {
|
||||
return _floor_f(f * mul + 0.5) / mul
|
||||
}
|
||||
return 0.0 - _floor_f((0.0 - f) * mul + 0.5) / mul
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
// runtime/state.el — In-process key/value store.
|
||||
//
|
||||
// Thin El wrappers over the __state_* seed primitives. The backing store is
|
||||
// a process-wide hash map maintained by the El runtime (formerly el_runtime.c
|
||||
// lines 4632–4721: state_set, state_get, state_del, state_keys).
|
||||
//
|
||||
// Keys and values are Strings. Values are persistent across request boundaries
|
||||
// within the same process instance (they survive individual request lifetimes).
|
||||
// Concurrent access is serialized by the runtime; these wrappers are lock-free
|
||||
// from El's perspective.
|
||||
//
|
||||
// Seed primitives consumed:
|
||||
// __state_set(key: String, val: String)
|
||||
// __state_get(key: String) -> String
|
||||
// __state_del(key: String)
|
||||
// __state_keys() -> String (JSON array of key strings)
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Core — set / get / del / keys
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// state_set — store val under key. Overwrites any existing value.
|
||||
fn state_set(key: String, val: String) {
|
||||
__state_set(key, val)
|
||||
}
|
||||
|
||||
// state_get — retrieve the value for key. Returns "" if key is absent.
|
||||
fn state_get(key: String) -> String {
|
||||
return __state_get(key)
|
||||
}
|
||||
|
||||
// state_del — remove key from the store. No-op if key does not exist.
|
||||
fn state_del(key: String) {
|
||||
__state_del(key)
|
||||
}
|
||||
|
||||
// state_keys — return a JSON array string of all current keys.
|
||||
// e.g. ["foo","bar","baz"]
|
||||
// Matches legacy state_keys() which returns an ElList (here serialized as JSON).
|
||||
fn state_keys() -> String {
|
||||
return __state_keys()
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Convenience helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// state_has — true if key is present (value is non-empty string).
|
||||
// Note: a key set to "" is indistinguishable from absent via state_get alone.
|
||||
fn state_has(key: String) -> Bool {
|
||||
let v: String = state_get(key)
|
||||
if str_eq(v, "") { return false }
|
||||
return true
|
||||
}
|
||||
|
||||
// state_get_or — return val for key, or default_val if key is absent.
|
||||
fn state_get_or(key: String, default_val: String) -> String {
|
||||
let v: String = state_get(key)
|
||||
if str_eq(v, "") { return default_val }
|
||||
return v
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
// stdlib.el — El standard library master import file.
|
||||
//
|
||||
// Import this single file to get the full El runtime in the correct
|
||||
// dependency order. El programs can do:
|
||||
//
|
||||
// import "../foundation/el/runtime/stdlib.el"
|
||||
//
|
||||
// or, if El is installed via tools/install.sh:
|
||||
//
|
||||
// import "/usr/local/el/runtime/stdlib.el"
|
||||
//
|
||||
// Note: test.el is intentionally NOT included here — it is dev-only
|
||||
// and should be imported explicitly in test files only.
|
||||
//
|
||||
// Dependency order (each module may depend on earlier ones):
|
||||
// string — no deps
|
||||
// math — no deps
|
||||
// time — no deps
|
||||
// env — no deps
|
||||
// fs — no deps
|
||||
// exec — no deps
|
||||
// json — depends on string
|
||||
// http — depends on string, json
|
||||
// state — no deps
|
||||
// thread — depends on exec
|
||||
// channel — depends on thread, state
|
||||
// engram — depends on http, json, string
|
||||
// manifest — depends on fs, json, string
|
||||
|
||||
import "string.el"
|
||||
import "math.el"
|
||||
import "time.el"
|
||||
import "env.el"
|
||||
import "fs.el"
|
||||
import "exec.el"
|
||||
import "json.el"
|
||||
import "http.el"
|
||||
import "state.el"
|
||||
import "thread.el"
|
||||
import "channel.el"
|
||||
import "engram.el"
|
||||
import "manifest.el"
|
||||
@@ -0,0 +1,907 @@
|
||||
// runtime/string.el — String operations implemented in El.
|
||||
//
|
||||
// All functions delegate character-level work to the seed primitives declared
|
||||
// in el_seed.c. No C is written here; this is pure El source that compiles
|
||||
// to C via the normal El pipeline.
|
||||
//
|
||||
// Seed primitives used (provided by el_seed.c):
|
||||
// __str_len(s) -> Int
|
||||
// __str_char_at(s, i) -> Int (char code at byte index i)
|
||||
// __str_alloc(n) -> String (n-byte zero-filled mutable buffer)
|
||||
// __str_set_char(s, i, c) -> String (mutate s[i]=c, return s)
|
||||
// __str_cmp(a, b) -> Int (strcmp)
|
||||
// __str_ncmp(a, b, n) -> Int (strncmp)
|
||||
// __str_concat_raw(a, b) -> String
|
||||
// __str_slice_raw(s, lo, hi) -> String (substring copy [lo, hi))
|
||||
// __int_to_str(n) -> String
|
||||
// __str_to_int(s) -> Int
|
||||
// __float_to_str(f) -> String
|
||||
// __str_to_float(s) -> Float
|
||||
// __println(s)
|
||||
// __print(s)
|
||||
// __readline() -> String
|
||||
// __url_encode(s) -> String
|
||||
// __url_decode(s) -> String
|
||||
|
||||
// ── I/O ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
fn println(s: String) -> Void {
|
||||
__println(s)
|
||||
}
|
||||
|
||||
fn print(s: String) -> Void {
|
||||
__print(s)
|
||||
}
|
||||
|
||||
fn readline() -> String {
|
||||
return __readline()
|
||||
}
|
||||
|
||||
// ── Type conversions ──────────────────────────────────────────────────────────
|
||||
|
||||
fn int_to_str(n: Int) -> String {
|
||||
return __int_to_str(n)
|
||||
}
|
||||
|
||||
fn str_to_int(s: String) -> Int {
|
||||
return __str_to_int(s)
|
||||
}
|
||||
|
||||
fn float_to_str(f: Float) -> String {
|
||||
return __float_to_str(f)
|
||||
}
|
||||
|
||||
fn str_to_float(s: String) -> Float {
|
||||
return __str_to_float(s)
|
||||
}
|
||||
|
||||
fn bool_to_str(b: Bool) -> String {
|
||||
if b { return "true" }
|
||||
return "false"
|
||||
}
|
||||
|
||||
// ── URL encoding ──────────────────────────────────────────────────────────────
|
||||
|
||||
fn url_encode(s: String) -> String {
|
||||
return __url_encode(s)
|
||||
}
|
||||
|
||||
fn url_decode(s: String) -> String {
|
||||
return __url_decode(s)
|
||||
}
|
||||
|
||||
// ── Math ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
fn el_abs(n: Int) -> Int {
|
||||
if n < 0 { return 0 - n }
|
||||
return n
|
||||
}
|
||||
|
||||
fn el_max(a: Int, b: Int) -> Int {
|
||||
if a > b { return a }
|
||||
return b
|
||||
}
|
||||
|
||||
fn el_min(a: Int, b: Int) -> Int {
|
||||
if a < b { return a }
|
||||
return b
|
||||
}
|
||||
|
||||
// ── Core string primitives ────────────────────────────────────────────────────
|
||||
|
||||
fn str_len(s: String) -> Int {
|
||||
return __str_len(s)
|
||||
}
|
||||
|
||||
fn str_eq(a: String, b: String) -> Bool {
|
||||
return __str_cmp(a, b) == 0
|
||||
}
|
||||
|
||||
fn str_concat(a: String, b: String) -> String {
|
||||
return __str_concat_raw(a, b)
|
||||
}
|
||||
|
||||
fn str_slice(s: String, start: Int, end: Int) -> String {
|
||||
let slen: Int = __str_len(s)
|
||||
let lo: Int = start
|
||||
if lo < 0 { lo = 0 }
|
||||
if lo > slen { lo = slen }
|
||||
let hi: Int = end
|
||||
if hi < lo { hi = lo }
|
||||
if hi > slen { hi = slen }
|
||||
return __str_slice_raw(s, lo, hi)
|
||||
}
|
||||
|
||||
// ── Whitespace helpers (internal) ─────────────────────────────────────────────
|
||||
//
|
||||
// _is_ws: returns true for ASCII whitespace (space, tab, \n, \r, \f, \v).
|
||||
|
||||
fn _is_ws(c: Int) -> Bool {
|
||||
if c == 32 { return true } // space
|
||||
if c == 9 { return true } // tab
|
||||
if c == 10 { return true } // \n
|
||||
if c == 13 { return true } // \r
|
||||
if c == 12 { return true } // \f
|
||||
if c == 11 { return true } // \v
|
||||
return false
|
||||
}
|
||||
|
||||
// Scan forward from index 0; return index of first byte not in whitespace,
|
||||
// or n if the entire string is whitespace.
|
||||
fn _find_first_non_ws(s: String, n: Int) -> Int {
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
if !_is_ws(__str_char_at(s, i)) { return i }
|
||||
i = i + 1
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
// Scan backward from index n-1; return index of last non-whitespace byte,
|
||||
// or -1 if the entire string is whitespace.
|
||||
fn _find_last_non_ws(s: String, n: Int) -> Int {
|
||||
let i: Int = n - 1
|
||||
while i >= 0 {
|
||||
if !_is_ws(__str_char_at(s, i)) { return i }
|
||||
i = i - 1
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
// ── Comparison and search ─────────────────────────────────────────────────────
|
||||
|
||||
fn str_starts_with(s: String, prefix: String) -> Bool {
|
||||
let plen: Int = __str_len(prefix)
|
||||
let slen: Int = __str_len(s)
|
||||
if plen > slen { return false }
|
||||
return __str_ncmp(s, prefix, plen) == 0
|
||||
}
|
||||
|
||||
fn str_ends_with(s: String, suffix: String) -> Bool {
|
||||
let slen: Int = __str_len(s)
|
||||
let suflen: Int = __str_len(suffix)
|
||||
if suflen > slen { return false }
|
||||
let tail: String = __str_slice_raw(s, slen - suflen, slen)
|
||||
return __str_cmp(tail, suffix) == 0
|
||||
}
|
||||
|
||||
fn str_contains(s: String, sub: String) -> Bool {
|
||||
let slen: Int = __str_len(s)
|
||||
let sublen: Int = __str_len(sub)
|
||||
if sublen == 0 { return true }
|
||||
if sublen > slen { return false }
|
||||
let limit: Int = slen - sublen
|
||||
let i: Int = 0
|
||||
while i <= limit {
|
||||
let window: String = __str_slice_raw(s, i, i + sublen)
|
||||
if __str_cmp(window, sub) == 0 { return true }
|
||||
i = i + 1
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
fn str_index_of(s: String, sub: String) -> Int {
|
||||
let slen: Int = __str_len(s)
|
||||
let sublen: Int = __str_len(sub)
|
||||
if sublen == 0 { return 0 }
|
||||
if sublen > slen { return -1 }
|
||||
let limit: Int = slen - sublen
|
||||
let i: Int = 0
|
||||
while i <= limit {
|
||||
let window: String = __str_slice_raw(s, i, i + sublen)
|
||||
if __str_cmp(window, sub) == 0 { return i }
|
||||
i = i + 1
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
fn str_last_index_of(s: String, sub: String) -> Int {
|
||||
let slen: Int = __str_len(s)
|
||||
let sublen: Int = __str_len(sub)
|
||||
if sublen == 0 { return slen }
|
||||
if sublen > slen { return -1 }
|
||||
let last: Int = -1
|
||||
let limit: Int = slen - sublen
|
||||
let i: Int = 0
|
||||
while i <= limit {
|
||||
let window: String = __str_slice_raw(s, i, i + sublen)
|
||||
if __str_cmp(window, sub) == 0 {
|
||||
last = i
|
||||
i = i + sublen
|
||||
} else {
|
||||
i = i + 1
|
||||
}
|
||||
}
|
||||
return last
|
||||
}
|
||||
|
||||
fn str_index_of_all(s: String, sub: String) -> [Int] {
|
||||
let result: [Int] = el_list_empty()
|
||||
let slen: Int = __str_len(s)
|
||||
let sublen: Int = __str_len(sub)
|
||||
if sublen == 0 { return result }
|
||||
if sublen > slen { return result }
|
||||
let limit: Int = slen - sublen
|
||||
let i: Int = 0
|
||||
while i <= limit {
|
||||
let window: String = __str_slice_raw(s, i, i + sublen)
|
||||
if __str_cmp(window, sub) == 0 {
|
||||
result = el_list_append(result, i)
|
||||
i = i + sublen
|
||||
} else {
|
||||
i = i + 1
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// Return the byte index of the first character in s that appears in any_of,
|
||||
// or -1 if none found.
|
||||
fn str_find_chars(s: String, any_of: String) -> Int {
|
||||
let slen: Int = __str_len(s)
|
||||
let alen: Int = __str_len(any_of)
|
||||
if alen == 0 { return -1 }
|
||||
let i: Int = 0
|
||||
while i < slen {
|
||||
let c: Int = __str_char_at(s, i)
|
||||
let j: Int = 0
|
||||
while j < alen {
|
||||
if c == __str_char_at(any_of, j) { return i }
|
||||
j = j + 1
|
||||
}
|
||||
i = i + 1
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
// ── Character access ──────────────────────────────────────────────────────────
|
||||
|
||||
// Return a one-character string at byte index i, or "" if out of range.
|
||||
fn str_char_at(s: String, i: Int) -> String {
|
||||
let slen: Int = __str_len(s)
|
||||
if i < 0 { return "" }
|
||||
if i >= slen { return "" }
|
||||
return __str_slice_raw(s, i, i + 1)
|
||||
}
|
||||
|
||||
// Return the char code (byte value) at byte index i, or 0 if out of range.
|
||||
fn str_char_code(s: String, i: Int) -> Int {
|
||||
let slen: Int = __str_len(s)
|
||||
if i < 0 { return 0 }
|
||||
if i >= slen { return 0 }
|
||||
return __str_char_at(s, i)
|
||||
}
|
||||
|
||||
// ── Case conversion ───────────────────────────────────────────────────────────
|
||||
|
||||
fn str_to_upper(s: String) -> String {
|
||||
let n: Int = __str_len(s)
|
||||
if n == 0 { return "" }
|
||||
let out: String = __str_alloc(n)
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
let c: Int = __str_char_at(s, i)
|
||||
// a-z (97-122) -> A-Z (65-90): subtract 32
|
||||
if c >= 97 {
|
||||
if c <= 122 { c = c - 32 }
|
||||
}
|
||||
out = __str_set_char(out, i, c)
|
||||
i = i + 1
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
fn str_to_lower(s: String) -> String {
|
||||
let n: Int = __str_len(s)
|
||||
if n == 0 { return "" }
|
||||
let out: String = __str_alloc(n)
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
let c: Int = __str_char_at(s, i)
|
||||
// A-Z (65-90) -> a-z (97-122): add 32
|
||||
if c >= 65 {
|
||||
if c <= 90 { c = c + 32 }
|
||||
}
|
||||
out = __str_set_char(out, i, c)
|
||||
i = i + 1
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// Aliases used in existing El codebases.
|
||||
fn str_lower(s: String) -> String {
|
||||
return str_to_lower(s)
|
||||
}
|
||||
|
||||
fn str_upper(s: String) -> String {
|
||||
return str_to_upper(s)
|
||||
}
|
||||
|
||||
// ── Whitespace trimming ───────────────────────────────────────────────────────
|
||||
|
||||
fn str_trim(s: String) -> String {
|
||||
let n: Int = __str_len(s)
|
||||
if n == 0 { return "" }
|
||||
let lo: Int = _find_first_non_ws(s, n)
|
||||
if lo == n { return "" }
|
||||
let hi: Int = _find_last_non_ws(s, n)
|
||||
return __str_slice_raw(s, lo, hi + 1)
|
||||
}
|
||||
|
||||
fn str_lstrip(s: String) -> String {
|
||||
let n: Int = __str_len(s)
|
||||
if n == 0 { return "" }
|
||||
let lo: Int = _find_first_non_ws(s, n)
|
||||
if lo == n { return "" }
|
||||
return __str_slice_raw(s, lo, n)
|
||||
}
|
||||
|
||||
fn str_rstrip(s: String) -> String {
|
||||
let n: Int = __str_len(s)
|
||||
if n == 0 { return "" }
|
||||
let hi: Int = _find_last_non_ws(s, n)
|
||||
if hi < 0 { return "" }
|
||||
return __str_slice_raw(s, 0, hi + 1)
|
||||
}
|
||||
|
||||
// ── Replacement ───────────────────────────────────────────────────────────────
|
||||
|
||||
fn str_replace(s: String, from: String, to: String) -> String {
|
||||
let slen: Int = __str_len(s)
|
||||
let flen: Int = __str_len(from)
|
||||
if flen == 0 { return s }
|
||||
if slen == 0 { return s }
|
||||
// Scan s left-to-right; emit `to` on each match, otherwise emit one byte.
|
||||
let result: String = ""
|
||||
let i: Int = 0
|
||||
while i < slen {
|
||||
// Try to match `from` at position i
|
||||
if i + flen <= slen {
|
||||
let window: String = __str_slice_raw(s, i, i + flen)
|
||||
if __str_cmp(window, from) == 0 {
|
||||
result = __str_concat_raw(result, to)
|
||||
i = i + flen
|
||||
} else {
|
||||
let ch: String = __str_slice_raw(s, i, i + 1)
|
||||
result = __str_concat_raw(result, ch)
|
||||
i = i + 1
|
||||
}
|
||||
} else {
|
||||
// Not enough bytes left for a match — emit remainder and stop.
|
||||
let tail: String = __str_slice_raw(s, i, slen)
|
||||
result = __str_concat_raw(result, tail)
|
||||
i = slen
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// ── Repetition and reversal ───────────────────────────────────────────────────
|
||||
|
||||
fn str_repeat(s: String, n: Int) -> String {
|
||||
if n <= 0 { return "" }
|
||||
let slen: Int = __str_len(s)
|
||||
if slen == 0 { return "" }
|
||||
let result: String = ""
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
result = __str_concat_raw(result, s)
|
||||
i = i + 1
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// Byte-reverse (correct for ASCII; for multi-byte UTF-8 codepoints this
|
||||
// reverses bytes within a codepoint, which is intentional at this tier —
|
||||
// Phase 2 will add grapheme-aware reversal).
|
||||
fn str_reverse(s: String) -> String {
|
||||
let n: Int = __str_len(s)
|
||||
if n == 0 { return "" }
|
||||
let out: String = __str_alloc(n)
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
let c: Int = __str_char_at(s, i)
|
||||
out = __str_set_char(out, n - 1 - i, c)
|
||||
i = i + 1
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// ── Prefix/suffix stripping ───────────────────────────────────────────────────
|
||||
|
||||
fn str_strip_prefix(s: String, prefix: String) -> String {
|
||||
let slen: Int = __str_len(s)
|
||||
let plen: Int = __str_len(prefix)
|
||||
if plen == 0 { return s }
|
||||
if plen > slen { return s }
|
||||
if __str_ncmp(s, prefix, plen) == 0 {
|
||||
return __str_slice_raw(s, plen, slen)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
fn str_strip_suffix(s: String, suffix: String) -> String {
|
||||
let slen: Int = __str_len(s)
|
||||
let suflen: Int = __str_len(suffix)
|
||||
if suflen == 0 { return s }
|
||||
if suflen > slen { return s }
|
||||
let tail: String = __str_slice_raw(s, slen - suflen, slen)
|
||||
if __str_cmp(tail, suffix) == 0 {
|
||||
return __str_slice_raw(s, 0, slen - suflen)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// Strip leading and trailing bytes whose char code appears in `chars`.
|
||||
fn str_strip_chars(s: String, chars: String) -> String {
|
||||
let slen: Int = __str_len(s)
|
||||
let clen: Int = __str_len(chars)
|
||||
if slen == 0 { return "" }
|
||||
if clen == 0 { return s }
|
||||
let lo: Int = _find_first_not_in_charset(s, chars, slen, clen)
|
||||
if lo == slen { return "" }
|
||||
let hi: Int = _find_last_not_in_charset(s, chars, slen, clen)
|
||||
return __str_slice_raw(s, lo, hi + 1)
|
||||
}
|
||||
|
||||
// Internal: true if char code `c` is present in the charset string.
|
||||
fn _char_in_set(c: Int, chars: String, clen: Int) -> Bool {
|
||||
let j: Int = 0
|
||||
while j < clen {
|
||||
if c == __str_char_at(chars, j) { return true }
|
||||
j = j + 1
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
fn _find_first_not_in_charset(s: String, chars: String, slen: Int, clen: Int) -> Int {
|
||||
let i: Int = 0
|
||||
while i < slen {
|
||||
if !_char_in_set(__str_char_at(s, i), chars, clen) { return i }
|
||||
i = i + 1
|
||||
}
|
||||
return slen
|
||||
}
|
||||
|
||||
fn _find_last_not_in_charset(s: String, chars: String, slen: Int, clen: Int) -> Int {
|
||||
let i: Int = slen - 1
|
||||
while i >= 0 {
|
||||
if !_char_in_set(__str_char_at(s, i), chars, clen) { return i }
|
||||
i = i - 1
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
// ── Padding ───────────────────────────────────────────────────────────────────
|
||||
|
||||
// Pad s on the left to `width` total chars, repeating `pad` cyclically.
|
||||
fn str_pad_left(s: String, width: Int, pad: String) -> String {
|
||||
let slen: Int = __str_len(s)
|
||||
if slen >= width { return s }
|
||||
let plen: Int = __str_len(pad)
|
||||
if plen == 0 { return s }
|
||||
let need: Int = width - slen
|
||||
let prefix: String = ""
|
||||
let i: Int = 0
|
||||
while i < need {
|
||||
// Select pad character at position (i mod plen)
|
||||
let pad_idx: Int = i - (i / plen) * plen
|
||||
let pc: String = __str_slice_raw(pad, pad_idx, pad_idx + 1)
|
||||
prefix = __str_concat_raw(prefix, pc)
|
||||
i = i + 1
|
||||
}
|
||||
return __str_concat_raw(prefix, s)
|
||||
}
|
||||
|
||||
// Pad s on the right to `width` total chars, repeating `pad` cyclically.
|
||||
fn str_pad_right(s: String, width: Int, pad: String) -> String {
|
||||
let slen: Int = __str_len(s)
|
||||
if slen >= width { return s }
|
||||
let plen: Int = __str_len(pad)
|
||||
if plen == 0 { return s }
|
||||
let need: Int = width - slen
|
||||
let suffix: String = ""
|
||||
let i: Int = 0
|
||||
while i < need {
|
||||
let pad_idx: Int = i - (i / plen) * plen
|
||||
let pc: String = __str_slice_raw(pad, pad_idx, pad_idx + 1)
|
||||
suffix = __str_concat_raw(suffix, pc)
|
||||
i = i + 1
|
||||
}
|
||||
return __str_concat_raw(s, suffix)
|
||||
}
|
||||
|
||||
// ── Counting ──────────────────────────────────────────────────────────────────
|
||||
|
||||
// Count non-overlapping occurrences of `sub` in `s`. Empty sub returns 0.
|
||||
fn str_count(s: String, sub: String) -> Int {
|
||||
let slen: Int = __str_len(s)
|
||||
let sublen: Int = __str_len(sub)
|
||||
if sublen == 0 { return 0 }
|
||||
if sublen > slen { return 0 }
|
||||
let count: Int = 0
|
||||
let limit: Int = slen - sublen
|
||||
let i: Int = 0
|
||||
while i <= limit {
|
||||
let window: String = __str_slice_raw(s, i, i + sublen)
|
||||
if __str_cmp(window, sub) == 0 {
|
||||
count = count + 1
|
||||
i = i + sublen
|
||||
} else {
|
||||
i = i + 1
|
||||
}
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
// Byte count — alias of str_len.
|
||||
fn str_count_bytes(s: String) -> Int {
|
||||
return __str_len(s)
|
||||
}
|
||||
|
||||
// UTF-8 codepoint count: count bytes that are NOT continuation bytes (10xxxxxx).
|
||||
// Continuation bytes have the pattern 10xxxxxx = 0x80..0xBF (128..191).
|
||||
fn str_count_chars(s: String) -> Int {
|
||||
let n: Int = __str_len(s)
|
||||
let count: Int = 0
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
let c: Int = __str_char_at(s, i)
|
||||
// Continuation bytes are in range [128, 191]; skip them.
|
||||
// All other bytes (< 128 ASCII, or >= 192 leading bytes) start a codepoint.
|
||||
if c < 128 {
|
||||
count = count + 1
|
||||
} else {
|
||||
if c >= 192 { count = count + 1 }
|
||||
}
|
||||
i = i + 1
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
// Count newline-delimited lines. A trailing newline does NOT add an extra empty line.
|
||||
fn str_count_lines(s: String) -> Int {
|
||||
let n: Int = __str_len(s)
|
||||
if n == 0 { return 0 }
|
||||
let count: Int = 0
|
||||
let has_content: Bool = false
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
let c: Int = __str_char_at(s, i)
|
||||
has_content = true
|
||||
if c == 10 { // \n
|
||||
count = count + 1
|
||||
has_content = false
|
||||
}
|
||||
i = i + 1
|
||||
}
|
||||
if has_content { count = count + 1 }
|
||||
return count
|
||||
}
|
||||
|
||||
// Count whitespace-delimited words (non-empty tokens).
|
||||
fn str_count_words(s: String) -> Int {
|
||||
let n: Int = __str_len(s)
|
||||
let count: Int = 0
|
||||
let in_word: Bool = false
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
let c: Int = __str_char_at(s, i)
|
||||
if _is_ws(c) {
|
||||
in_word = false
|
||||
} else {
|
||||
if !in_word {
|
||||
in_word = true
|
||||
count = count + 1
|
||||
}
|
||||
}
|
||||
i = i + 1
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
// Count ASCII letters [A-Za-z].
|
||||
fn str_count_letters(s: String) -> Int {
|
||||
let n: Int = __str_len(s)
|
||||
let count: Int = 0
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
let c: Int = __str_char_at(s, i)
|
||||
if c >= 65 {
|
||||
if c <= 90 { count = count + 1 } // A-Z
|
||||
}
|
||||
if c >= 97 {
|
||||
if c <= 122 { count = count + 1 } // a-z
|
||||
}
|
||||
i = i + 1
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
// Count ASCII decimal digits [0-9].
|
||||
fn str_count_digits(s: String) -> Int {
|
||||
let n: Int = __str_len(s)
|
||||
let count: Int = 0
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
let c: Int = __str_char_at(s, i)
|
||||
if c >= 48 {
|
||||
if c <= 57 { count = count + 1 } // '0'-'9'
|
||||
}
|
||||
i = i + 1
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
// ── Character classification ──────────────────────────────────────────────────
|
||||
//
|
||||
// For all predicates: empty string -> false.
|
||||
// Multi-char string: ALL bytes must satisfy the predicate.
|
||||
|
||||
fn is_letter(s: String) -> Bool {
|
||||
let n: Int = __str_len(s)
|
||||
if n == 0 { return false }
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
let c: Int = __str_char_at(s, i)
|
||||
let ok: Bool = false
|
||||
if c >= 65 { if c <= 90 { ok = true } } // A-Z
|
||||
if c >= 97 { if c <= 122 { ok = true } } // a-z
|
||||
if !ok { return false }
|
||||
i = i + 1
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
fn is_digit(s: String) -> Bool {
|
||||
let n: Int = __str_len(s)
|
||||
if n == 0 { return false }
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
let c: Int = __str_char_at(s, i)
|
||||
if c < 48 { return false } // '0'
|
||||
if c > 57 { return false } // '9'
|
||||
i = i + 1
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
fn is_alphanumeric(s: String) -> Bool {
|
||||
let n: Int = __str_len(s)
|
||||
if n == 0 { return false }
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
let c: Int = __str_char_at(s, i)
|
||||
let ok: Bool = false
|
||||
if c >= 48 { if c <= 57 { ok = true } } // 0-9
|
||||
if c >= 65 { if c <= 90 { ok = true } } // A-Z
|
||||
if c >= 97 { if c <= 122 { ok = true } } // a-z
|
||||
if !ok { return false }
|
||||
i = i + 1
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
fn is_whitespace(s: String) -> Bool {
|
||||
let n: Int = __str_len(s)
|
||||
if n == 0 { return false }
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
if !_is_ws(__str_char_at(s, i)) { return false }
|
||||
i = i + 1
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// ASCII punctuation: 33-47, 58-64, 91-96, 123-126.
|
||||
fn is_punctuation(s: String) -> Bool {
|
||||
let n: Int = __str_len(s)
|
||||
if n == 0 { return false }
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
let c: Int = __str_char_at(s, i)
|
||||
let ok: Bool = false
|
||||
if c >= 33 { if c <= 47 { ok = true } }
|
||||
if c >= 58 { if c <= 64 { ok = true } }
|
||||
if c >= 91 { if c <= 96 { ok = true } }
|
||||
if c >= 123 { if c <= 126 { ok = true } }
|
||||
if !ok { return false }
|
||||
i = i + 1
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
fn is_uppercase(s: String) -> Bool {
|
||||
let n: Int = __str_len(s)
|
||||
if n == 0 { return false }
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
let c: Int = __str_char_at(s, i)
|
||||
if c < 65 { return false } // 'A'
|
||||
if c > 90 { return false } // 'Z'
|
||||
i = i + 1
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
fn is_lowercase(s: String) -> Bool {
|
||||
let n: Int = __str_len(s)
|
||||
if n == 0 { return false }
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
let c: Int = __str_char_at(s, i)
|
||||
if c < 97 { return false } // 'a'
|
||||
if c > 122 { return false } // 'z'
|
||||
i = i + 1
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// ── Splitting ─────────────────────────────────────────────────────────────────
|
||||
|
||||
fn str_split(s: String, sep: String) -> [String] {
|
||||
let result: [String] = el_list_empty()
|
||||
let slen: Int = __str_len(s)
|
||||
let seplen: Int = __str_len(sep)
|
||||
// Empty separator: return the whole string as a single element.
|
||||
if seplen == 0 {
|
||||
result = el_list_append(result, s)
|
||||
return result
|
||||
}
|
||||
let part_start: Int = 0
|
||||
let i: Int = 0
|
||||
while i < slen {
|
||||
if i + seplen <= slen {
|
||||
let window: String = __str_slice_raw(s, i, i + seplen)
|
||||
if __str_cmp(window, sep) == 0 {
|
||||
let part: String = __str_slice_raw(s, part_start, i)
|
||||
result = el_list_append(result, part)
|
||||
i = i + seplen
|
||||
part_start = i
|
||||
} else {
|
||||
i = i + 1
|
||||
}
|
||||
} else {
|
||||
i = i + 1
|
||||
}
|
||||
}
|
||||
// Append remaining tail (may be empty string if s ended with sep).
|
||||
let tail: String = __str_slice_raw(s, part_start, slen)
|
||||
result = el_list_append(result, tail)
|
||||
return result
|
||||
}
|
||||
|
||||
// Split into at most n parts. The nth part (index n-1) contains the remainder
|
||||
// verbatim, including any further separators. n <= 0 returns []. n == 1
|
||||
// returns [s].
|
||||
fn str_split_n(s: String, sep: String, n: Int) -> [String] {
|
||||
let result: [String] = el_list_empty()
|
||||
if n <= 0 { return result }
|
||||
if n == 1 {
|
||||
result = el_list_append(result, s)
|
||||
return result
|
||||
}
|
||||
let slen: Int = __str_len(s)
|
||||
let seplen: Int = __str_len(sep)
|
||||
if seplen == 0 {
|
||||
result = el_list_append(result, s)
|
||||
return result
|
||||
}
|
||||
let part_start: Int = 0
|
||||
let parts: Int = 0
|
||||
let i: Int = 0
|
||||
while i < slen {
|
||||
if parts >= n - 1 {
|
||||
// Reached the split limit — stop splitting, emit the rest below.
|
||||
i = slen
|
||||
} else {
|
||||
if i + seplen <= slen {
|
||||
let window: String = __str_slice_raw(s, i, i + seplen)
|
||||
if __str_cmp(window, sep) == 0 {
|
||||
let part: String = __str_slice_raw(s, part_start, i)
|
||||
result = el_list_append(result, part)
|
||||
i = i + seplen
|
||||
part_start = i
|
||||
parts = parts + 1
|
||||
} else {
|
||||
i = i + 1
|
||||
}
|
||||
} else {
|
||||
i = i + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
// Remainder verbatim.
|
||||
let tail: String = __str_slice_raw(s, part_start, slen)
|
||||
result = el_list_append(result, tail)
|
||||
return result
|
||||
}
|
||||
|
||||
// Split on newlines. \r\n is folded to \n. Trailing empty line after a
|
||||
// final \n is dropped — so "a\nb\n" yields ["a", "b"], not ["a", "b", ""].
|
||||
fn str_split_lines(s: String) -> [String] {
|
||||
let result: [String] = el_list_empty()
|
||||
let n: Int = __str_len(s)
|
||||
if n == 0 { return result }
|
||||
let line_start: Int = 0
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
let c: Int = __str_char_at(s, i)
|
||||
if c == 10 { // \n
|
||||
let lend: Int = i
|
||||
// Fold \r\n: if the byte before \n is \r, exclude it.
|
||||
if lend > line_start {
|
||||
if __str_char_at(s, lend - 1) == 13 { lend = lend - 1 }
|
||||
}
|
||||
let line: String = __str_slice_raw(s, line_start, lend)
|
||||
result = el_list_append(result, line)
|
||||
line_start = i + 1
|
||||
}
|
||||
i = i + 1
|
||||
}
|
||||
// Trailing content with no terminating \n.
|
||||
if line_start < n {
|
||||
let line: String = __str_slice_raw(s, line_start, n)
|
||||
result = el_list_append(result, line)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// Split into a list of one-byte strings (byte-level chars).
|
||||
fn str_split_chars(s: String) -> [String] {
|
||||
let result: [String] = el_list_empty()
|
||||
let n: Int = __str_len(s)
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
let ch: String = __str_slice_raw(s, i, i + 1)
|
||||
result = el_list_append(result, ch)
|
||||
i = i + 1
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// ── Joining ───────────────────────────────────────────────────────────────────
|
||||
|
||||
// Join a list of strings with a separator between consecutive elements.
|
||||
// Empty list yields "". Non-string elements should not be passed here.
|
||||
fn str_join(parts: [String], sep: String) -> String {
|
||||
let n: Int = el_list_len(parts)
|
||||
if n == 0 { return "" }
|
||||
let result: String = el_list_get(parts, 0)
|
||||
let i: Int = 1
|
||||
while i < n {
|
||||
result = __str_concat_raw(result, sep)
|
||||
result = __str_concat_raw(result, el_list_get(parts, i))
|
||||
i = i + 1
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// ── DHARMA byte encoding (str_to_bytes) ──────────────────────────────────────
|
||||
//
|
||||
// str_to_bytes — encode a string as a JSON array of unsigned byte values.
|
||||
// "hi" -> "[104,105]"
|
||||
// Used by db.el to store content in Engram JSON nodes as a byte array.
|
||||
// Note: bytes_to_str (the inverse) is defined in json.el because it depends
|
||||
// on json_array_get_string which is defined there.
|
||||
fn str_to_bytes(s: String) -> String {
|
||||
let n: Int = __str_len(s)
|
||||
if n == 0 { return "[]" }
|
||||
let result: String = "["
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
let b: Int = __str_char_at(s, i)
|
||||
if i > 0 { result = __str_concat_raw(result, ",") }
|
||||
result = __str_concat_raw(result, __int_to_str(b))
|
||||
i = i + 1
|
||||
}
|
||||
return __str_concat_raw(result, "]")
|
||||
}
|
||||
|
||||
// ── Cryptographic hashing ─────────────────────────────────────────────────────
|
||||
|
||||
// hash_sha256 — return the SHA-256 hex digest of a string.
|
||||
// Delegates to the __sha256_hex seed primitive.
|
||||
fn hash_sha256(s: String) -> String {
|
||||
return __sha256_hex(s)
|
||||
}
|
||||
@@ -0,0 +1,320 @@
|
||||
// runtime/test.el — El test framework: assertions, registration, and runner.
|
||||
//
|
||||
// Provides a minimal but complete test harness for El programs. No external
|
||||
// dependencies. Written entirely in El using existing runtime primitives.
|
||||
//
|
||||
// ── Quick-start ──────────────────────────────────────────────────────────────
|
||||
//
|
||||
// 1. Write a test function with the standard test signature:
|
||||
//
|
||||
// fn test_str_eq_works(_: String) -> String {
|
||||
// assert_true(str_eq("a", "a"), "same strings are equal")
|
||||
// assert_false(str_eq("a", "b"), "different strings are not equal")
|
||||
// return ""
|
||||
// }
|
||||
//
|
||||
// 2. Register it and run:
|
||||
//
|
||||
// fn main() -> Void {
|
||||
// test_case("str_eq works", "test_str_eq_works")
|
||||
// test_run_all()
|
||||
// }
|
||||
//
|
||||
// Test functions must have the signature (String) -> String. The argument is
|
||||
// a dummy passed by the threading mechanism (see runtime/thread.el) and should
|
||||
// be ignored. The return value is likewise ignored — results flow through the
|
||||
// state-based assertion primitives.
|
||||
//
|
||||
// ── State keys ───────────────────────────────────────────────────────────────
|
||||
//
|
||||
// _test_cases JSON array of {"name":"...","fn":"..."}
|
||||
// _test_pass_count Int as string — total passing assertions
|
||||
// _test_fail_count Int as string — total failing assertions
|
||||
// _test_failures JSON array of failure message strings
|
||||
// _test_current Name of the test case currently executing
|
||||
//
|
||||
// ── Dependencies ─────────────────────────────────────────────────────────────
|
||||
//
|
||||
// runtime/string.el — str_eq, str_concat, str_contains, int_to_str, ...
|
||||
// runtime/state.el — state_set, state_get
|
||||
// runtime/json.el — json_array_len, json_array_get_string, json_get,
|
||||
// json_escape_string
|
||||
// runtime/thread.el — spawn, join (for dynamic dispatch via dlsym)
|
||||
|
||||
// ── Internal: JSON array helpers ─────────────────────────────────────────────
|
||||
//
|
||||
// _test_json_append — append a quoted, escaped string element to a JSON array.
|
||||
//
|
||||
// Given an existing JSON array string (e.g. '["a","b"]') and a plain string
|
||||
// value, returns a new array with the value appended (e.g. '["a","b","c"]').
|
||||
//
|
||||
// The array must be non-empty — always init with "[]" before calling.
|
||||
fn _test_json_append(arr: String, val: String) -> String {
|
||||
let escaped: String = json_escape_string(val)
|
||||
let inner: String = str_slice(arr, 1, str_len(arr) - 1)
|
||||
if str_eq(inner, "") {
|
||||
return "[\"" + escaped + "\"]"
|
||||
}
|
||||
return "[" + inner + ",\"" + escaped + "\"]"
|
||||
}
|
||||
|
||||
// _test_json_obj_append — append a raw JSON object string to a JSON array.
|
||||
//
|
||||
// Used to build the _test_cases list where each element is already a
|
||||
// JSON object (not a plain string).
|
||||
fn _test_json_obj_append(arr: String, obj: String) -> String {
|
||||
let inner: String = str_slice(arr, 1, str_len(arr) - 1)
|
||||
if str_eq(inner, "") {
|
||||
return "[" + obj + "]"
|
||||
}
|
||||
return "[" + inner + "," + obj + "]"
|
||||
}
|
||||
|
||||
// ── Test registration ─────────────────────────────────────────────────────────
|
||||
|
||||
// test_case — register a named test case.
|
||||
//
|
||||
// name: Human-readable test case name (shown in output).
|
||||
// fn_name: Name of a top-level El function with signature
|
||||
// (String) -> String. The function should call assertion
|
||||
// primitives from this module. The String arg it receives is ""
|
||||
// and its return value is ignored.
|
||||
//
|
||||
// Test cases are stored in the _test_cases state key and executed in
|
||||
// registration order by test_run_all().
|
||||
fn test_case(name: String, fn_name: String) {
|
||||
let arr: String = state_get("_test_cases")
|
||||
if str_eq(arr, "") { arr = "[]" }
|
||||
let escaped_name: String = json_escape_string(name)
|
||||
let escaped_fn: String = json_escape_string(fn_name)
|
||||
let obj: String = "{\"name\":\"" + escaped_name + "\",\"fn\":\"" + escaped_fn + "\"}"
|
||||
let arr = _test_json_obj_append(arr, obj)
|
||||
state_set("_test_cases", arr)
|
||||
}
|
||||
|
||||
// ── Test state helpers ────────────────────────────────────────────────────────
|
||||
|
||||
// _test_init — reset all test counters and failure lists.
|
||||
//
|
||||
// Called at the start of test_run_all(). Safe to call multiple times.
|
||||
fn _test_init() {
|
||||
state_set("_test_pass_count", "0")
|
||||
state_set("_test_fail_count", "0")
|
||||
state_set("_test_failures", "[]")
|
||||
state_set("_test_current", "")
|
||||
}
|
||||
|
||||
// _test_inc_pass — increment the global pass counter by 1.
|
||||
fn _test_inc_pass() {
|
||||
let n: Int = str_to_int(state_get("_test_pass_count"))
|
||||
state_set("_test_pass_count", int_to_str(n + 1))
|
||||
}
|
||||
|
||||
// _test_inc_fail — increment the global fail counter by 1.
|
||||
fn _test_inc_fail() {
|
||||
let n: Int = str_to_int(state_get("_test_fail_count"))
|
||||
state_set("_test_fail_count", int_to_str(n + 1))
|
||||
}
|
||||
|
||||
// test_pass — record a passing assertion for the current test case.
|
||||
//
|
||||
// Increments the pass counter. Called internally by assertions.
|
||||
fn test_pass(name: String) {
|
||||
_test_inc_pass()
|
||||
}
|
||||
|
||||
// test_fail — record a failing assertion for the current test case.
|
||||
//
|
||||
// name: assertion label or description (usually the msg parameter)
|
||||
// msg: detailed failure message including expected/got values
|
||||
//
|
||||
// Increments the fail counter and appends the message to _test_failures.
|
||||
// Also prints the failure immediately for visibility.
|
||||
fn test_fail(name: String, msg: String) {
|
||||
_test_inc_fail()
|
||||
let failures: String = state_get("_test_failures")
|
||||
if str_eq(failures, "") { failures = "[]" }
|
||||
let entry: String = " " + msg
|
||||
let failures = _test_json_append(failures, entry)
|
||||
state_set("_test_failures", failures)
|
||||
println(" FAIL: " + msg)
|
||||
}
|
||||
|
||||
// ── Assertions ────────────────────────────────────────────────────────────────
|
||||
|
||||
// assert_true — assert that condition is true.
|
||||
//
|
||||
// condition: the boolean value to test
|
||||
// msg: description shown on failure
|
||||
fn assert_true(condition: Bool, msg: String) {
|
||||
if condition {
|
||||
test_pass(msg)
|
||||
return
|
||||
}
|
||||
let test_name: String = state_get("_test_current")
|
||||
test_fail(msg, "[" + test_name + "] " + msg + ": expected true, got false")
|
||||
}
|
||||
|
||||
// assert_false — assert that condition is false.
|
||||
//
|
||||
// condition: the boolean value to test
|
||||
// msg: description shown on failure
|
||||
fn assert_false(condition: Bool, msg: String) {
|
||||
if !condition {
|
||||
test_pass(msg)
|
||||
return
|
||||
}
|
||||
let test_name: String = state_get("_test_current")
|
||||
test_fail(msg, "[" + test_name + "] " + msg + ": expected false, got true")
|
||||
}
|
||||
|
||||
// assert_eq — assert that two strings are equal.
|
||||
//
|
||||
// a, b: strings to compare
|
||||
// msg: description shown on failure (quoted values appended automatically)
|
||||
fn assert_eq(a: String, b: String, msg: String) {
|
||||
if str_eq(a, b) {
|
||||
test_pass(msg)
|
||||
return
|
||||
}
|
||||
let test_name: String = state_get("_test_current")
|
||||
test_fail(msg, "[" + test_name + "] " + msg + ": expected \"" + b + "\", got \"" + a + "\"")
|
||||
}
|
||||
|
||||
// assert_int_eq — assert that two integers are equal.
|
||||
//
|
||||
// a, b: integers to compare
|
||||
// msg: description shown on failure
|
||||
fn assert_int_eq(a: Int, b: Int, msg: String) {
|
||||
if a == b {
|
||||
test_pass(msg)
|
||||
return
|
||||
}
|
||||
let test_name: String = state_get("_test_current")
|
||||
test_fail(msg, "[" + test_name + "] " + msg + ": expected " + int_to_str(b) + ", got " + int_to_str(a))
|
||||
}
|
||||
|
||||
// assert_neq — assert that two strings are NOT equal.
|
||||
//
|
||||
// a, b: strings to compare
|
||||
// msg: description shown on failure
|
||||
fn assert_neq(a: String, b: String, msg: String) {
|
||||
if !str_eq(a, b) {
|
||||
test_pass(msg)
|
||||
return
|
||||
}
|
||||
let test_name: String = state_get("_test_current")
|
||||
test_fail(msg, "[" + test_name + "] " + msg + ": expected values to differ, but both are \"" + a + "\"")
|
||||
}
|
||||
|
||||
// assert_contains — assert that string s contains substring sub.
|
||||
//
|
||||
// s: haystack string
|
||||
// sub: needle substring
|
||||
// msg: description shown on failure
|
||||
fn assert_contains(s: String, sub: String, msg: String) {
|
||||
if str_contains(s, sub) {
|
||||
test_pass(msg)
|
||||
return
|
||||
}
|
||||
let test_name: String = state_get("_test_current")
|
||||
test_fail(msg, "[" + test_name + "] " + msg + ": \"" + s + "\" does not contain \"" + sub + "\"")
|
||||
}
|
||||
|
||||
// assert_starts_with — assert that string s starts with prefix.
|
||||
//
|
||||
// s: string to inspect
|
||||
// prefix: expected prefix
|
||||
// msg: description shown on failure
|
||||
fn assert_starts_with(s: String, prefix: String, msg: String) {
|
||||
if str_starts_with(s, prefix) {
|
||||
test_pass(msg)
|
||||
return
|
||||
}
|
||||
let test_name: String = state_get("_test_current")
|
||||
test_fail(msg, "[" + test_name + "] " + msg + ": \"" + s + "\" does not start with \"" + prefix + "\"")
|
||||
}
|
||||
|
||||
// assert_ends_with — assert that string s ends with suffix.
|
||||
//
|
||||
// s: string to inspect
|
||||
// suffix: expected suffix
|
||||
// msg: description shown on failure
|
||||
fn assert_ends_with(s: String, suffix: String, msg: String) {
|
||||
if str_ends_with(s, suffix) {
|
||||
test_pass(msg)
|
||||
return
|
||||
}
|
||||
let test_name: String = state_get("_test_current")
|
||||
test_fail(msg, "[" + test_name + "] " + msg + ": \"" + s + "\" does not end with \"" + suffix + "\"")
|
||||
}
|
||||
|
||||
// fail — unconditional test failure.
|
||||
//
|
||||
// msg: failure message shown in output
|
||||
//
|
||||
// Use when a code path that must not be reached is reached, or when an
|
||||
// expected exception did not occur.
|
||||
fn fail(msg: String) {
|
||||
let test_name: String = state_get("_test_current")
|
||||
test_fail(msg, "[" + test_name + "] " + msg)
|
||||
}
|
||||
|
||||
// ── Runner ────────────────────────────────────────────────────────────────────
|
||||
|
||||
// _test_run_one — execute a single registered test case by name.
|
||||
//
|
||||
// name: the human-readable test case name (set as _test_current)
|
||||
// fn_name: the El function to invoke via the thread mechanism
|
||||
//
|
||||
// Sets _test_current so that assertions inside the test function know which
|
||||
// test they belong to. Spawns and immediately joins the test function in a
|
||||
// child thread (same dlsym mechanism as parallel_map) so dynamic dispatch
|
||||
// works without needing closures.
|
||||
fn _test_run_one(name: String, fn_name: String) {
|
||||
state_set("_test_current", name)
|
||||
let before_fail: Int = str_to_int(state_get("_test_fail_count"))
|
||||
let tid: Int = __thread_create(fn_name, "")
|
||||
__thread_join(tid)
|
||||
let after_fail: Int = str_to_int(state_get("_test_fail_count"))
|
||||
if after_fail == before_fail {
|
||||
println("[test] " + name + " ... PASS")
|
||||
} else {
|
||||
println("[test] " + name + " ... FAIL")
|
||||
}
|
||||
}
|
||||
|
||||
// test_run_all — execute all registered test cases and print a summary.
|
||||
//
|
||||
// Iterates through every test case registered via test_case(), runs each one,
|
||||
// prints per-test PASS/FAIL status, then prints a summary line.
|
||||
//
|
||||
// Returns the total number of failing assertions. Exit with this value to
|
||||
// signal CI failure:
|
||||
//
|
||||
// fn main() -> Int {
|
||||
// test_case("str_eq", "test_str_eq")
|
||||
// return test_run_all()
|
||||
// }
|
||||
fn test_run_all() -> Int {
|
||||
_test_init()
|
||||
|
||||
let cases: String = state_get("_test_cases")
|
||||
if str_eq(cases, "") { cases = "[]" }
|
||||
|
||||
let n: Int = json_array_len(cases)
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
let entry: String = json_array_get(cases, i)
|
||||
let name: String = json_get(entry, "name")
|
||||
let fn_name: String = json_get(entry, "fn")
|
||||
_test_run_one(name, fn_name)
|
||||
i = i + 1
|
||||
}
|
||||
|
||||
let pass_count: Int = str_to_int(state_get("_test_pass_count"))
|
||||
let fail_count: Int = str_to_int(state_get("_test_fail_count"))
|
||||
println("[test] Summary: " + int_to_str(pass_count) + " passed, " + int_to_str(fail_count) + " failed")
|
||||
|
||||
return fail_count
|
||||
}
|
||||
@@ -0,0 +1,225 @@
|
||||
// thread.el — El native threading model
|
||||
//
|
||||
// First-class parallelism for El. Eliminates bash fan-out hacks for parallel
|
||||
// HTTP dispatch, concurrent processing pipelines, and any other workload that
|
||||
// benefits from concurrent execution.
|
||||
//
|
||||
// Built on four seed primitives exposed by el_seed.c via dlsym+pthread:
|
||||
// __thread_create(fn_name, arg) -> Int spawn thread, return tid
|
||||
// __thread_join(tid) -> String join thread, return result
|
||||
// __mutex_new() -> Int allocate a mutex, return handle
|
||||
// __mutex_lock(m) lock mutex
|
||||
// __mutex_unlock(m) unlock mutex
|
||||
//
|
||||
// Every El fn compiles to a global C symbol. __thread_create uses dlsym to
|
||||
// look up the function by name and run it in a pthread. This means any El fn
|
||||
// with signature (String) -> String is directly threadable.
|
||||
|
||||
// ── Core primitives ──────────────────────────────────────────────────────────
|
||||
|
||||
// spawn — launch an El function in a new thread.
|
||||
//
|
||||
// fn_name: the name of an El fn with signature (String) -> String
|
||||
// arg: the argument to pass to that fn
|
||||
//
|
||||
// Returns a thread id (tid) that can be passed to join().
|
||||
// The El function must be a top-level fn — its C symbol must be globally
|
||||
// visible so dlsym can resolve it.
|
||||
fn spawn(fn_name: String, arg: String) -> Int {
|
||||
return __thread_create(fn_name, arg)
|
||||
}
|
||||
|
||||
// join — wait for a thread to finish and return its result.
|
||||
//
|
||||
// tid: the thread id returned by spawn()
|
||||
//
|
||||
// Blocks until the thread completes. Returns the String value the thread
|
||||
// function returned.
|
||||
fn join(tid: Int) -> String {
|
||||
return __thread_join(tid)
|
||||
}
|
||||
|
||||
// ── parallel_map ─────────────────────────────────────────────────────────────
|
||||
|
||||
// parallel_map — map an El function over a list of strings concurrently.
|
||||
//
|
||||
// items: [String] — the input list
|
||||
// fn_name: String — name of an El fn with signature (String) -> String
|
||||
//
|
||||
// Spawns one thread per item. All threads run concurrently. Joins each thread
|
||||
// in input order, so the output list preserves the same order as the input.
|
||||
//
|
||||
// This is the core primitive that replaces bash fan-out for parallel HTTP.
|
||||
// Example — dispatch to N rooms at once:
|
||||
// let responses: [String] = parallel_map(room_payloads, "dispatch_to_room")
|
||||
fn parallel_map(items: [String], fn_name: String) -> [String] {
|
||||
let n: Int = el_list_len(items)
|
||||
|
||||
// Phase 1: spawn all threads and collect tids in order.
|
||||
let tids: [String] = el_list_empty()
|
||||
let i = 0
|
||||
while i < n {
|
||||
let item: String = el_list_get(items, i)
|
||||
let tid: Int = spawn(fn_name, item)
|
||||
// Store tid as string so we can hold it in [String].
|
||||
// int_to_str is available as a builtin.
|
||||
let tids = el_list_append(tids, int_to_str(tid))
|
||||
let i = i + 1
|
||||
}
|
||||
|
||||
// Phase 2: join all threads in order, collecting results.
|
||||
let results: [String] = el_list_empty()
|
||||
let j = 0
|
||||
while j < n {
|
||||
let tid_str: String = el_list_get(tids, j)
|
||||
let tid: Int = str_to_int(tid_str)
|
||||
let result: String = join(tid)
|
||||
let results = el_list_append(results, result)
|
||||
let j = j + 1
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
// ── parallel_map_json ────────────────────────────────────────────────────────
|
||||
|
||||
// parallel_map_json — parallel_map over a JSON array string.
|
||||
//
|
||||
// items_json: String — a JSON array of strings, e.g. '["a","b","c"]'
|
||||
// fn_name: String — name of an El fn with signature (String) -> String
|
||||
//
|
||||
// Parses the JSON array into an [String], runs parallel_map, then serialises
|
||||
// the result list back to a JSON array string. Both input and output are JSON
|
||||
// strings — the common El inter-service format.
|
||||
//
|
||||
// Example:
|
||||
// let out_json: String = parallel_map_json(rooms_json, "dispatch_to_room")
|
||||
fn parallel_map_json(items_json: String, fn_name: String) -> String {
|
||||
let n: Int = json_array_len(items_json)
|
||||
|
||||
// Unpack JSON array into [String].
|
||||
let items: [String] = el_list_empty()
|
||||
let i = 0
|
||||
while i < n {
|
||||
let item: String = json_array_get(items_json, i)
|
||||
let items = el_list_append(items, item)
|
||||
let i = i + 1
|
||||
}
|
||||
|
||||
// Run the concurrent map.
|
||||
let results: [String] = parallel_map(items, fn_name)
|
||||
|
||||
// Repack results into a JSON array string.
|
||||
let m: Int = el_list_len(results)
|
||||
let out: String = "["
|
||||
let j = 0
|
||||
while j < m {
|
||||
let val: String = el_list_get(results, j)
|
||||
if j > 0 {
|
||||
let out = out + ","
|
||||
}
|
||||
// Each result is treated as a raw JSON value (object, array, or
|
||||
// quoted string as returned by the worker fn).
|
||||
let out = out + val
|
||||
let j = j + 1
|
||||
}
|
||||
let out = out + "]"
|
||||
return out
|
||||
}
|
||||
|
||||
// ── parallel_filter ──────────────────────────────────────────────────────────
|
||||
|
||||
// parallel_filter — keep items where fn_name returns "true", concurrently.
|
||||
//
|
||||
// items: [String] — the input list
|
||||
// fn_name: String — name of an El fn with signature (String) -> String
|
||||
// that returns "true" to keep the item or anything else
|
||||
// to discard it
|
||||
//
|
||||
// Runs the predicate fn on all items in parallel. Collects results in order,
|
||||
// preserving the relative order of kept items.
|
||||
fn parallel_filter(items: [String], fn_name: String) -> [String] {
|
||||
let n: Int = el_list_len(items)
|
||||
|
||||
// Spawn a predicate thread for every item.
|
||||
let tids: [String] = el_list_empty()
|
||||
let i = 0
|
||||
while i < n {
|
||||
let item: String = el_list_get(items, i)
|
||||
let tid: Int = spawn(fn_name, item)
|
||||
let tids = el_list_append(tids, int_to_str(tid))
|
||||
let i = i + 1
|
||||
}
|
||||
|
||||
// Join in order, keep item if the predicate returned "true".
|
||||
let kept: [String] = el_list_empty()
|
||||
let j = 0
|
||||
while j < n {
|
||||
let item: String = el_list_get(items, j)
|
||||
let tid_str: String = el_list_get(tids, j)
|
||||
let tid: Int = str_to_int(tid_str)
|
||||
let verdict: String = join(tid)
|
||||
if str_eq(verdict, "true") {
|
||||
let kept = el_list_append(kept, item)
|
||||
}
|
||||
let j = j + 1
|
||||
}
|
||||
|
||||
return kept
|
||||
}
|
||||
|
||||
// ── HTTP helpers ─────────────────────────────────────────────────────────────
|
||||
|
||||
// fire_http_post — worker fn for parallel_posts.
|
||||
//
|
||||
// Expects arg to be a JSON object with "url" and "body" keys:
|
||||
// {"url":"https://...","body":"{...}"}
|
||||
//
|
||||
// Returns the HTTP response body string. Registered as a global El fn so
|
||||
// parallel_map can locate it via dlsym.
|
||||
fn fire_http_post(arg: String) -> String {
|
||||
let url: String = json_get(arg, "url")
|
||||
let body: String = json_get(arg, "body")
|
||||
return http_post(url, body)
|
||||
}
|
||||
|
||||
// parallel_posts — fire a list of HTTP POSTs concurrently.
|
||||
//
|
||||
// requests: [String] — each element is a JSON object {"url":"...","body":"..."}
|
||||
//
|
||||
// Returns [String] of response bodies in the same order as the input.
|
||||
//
|
||||
// Example — fan out to N room endpoints at once:
|
||||
// let reqs: [String] = el_list_empty()
|
||||
// let reqs = el_list_append(reqs, "{\"url\":\"http://room-a/dispatch\",\"body\":\"" + payload + "\"}")
|
||||
// let reqs = el_list_append(reqs, "{\"url\":\"http://room-b/dispatch\",\"body\":\"" + payload + "\"}")
|
||||
// let responses: [String] = parallel_posts(reqs)
|
||||
fn parallel_posts(requests: [String]) -> [String] {
|
||||
return parallel_map(requests, "fire_http_post")
|
||||
}
|
||||
|
||||
// ── Mutex helpers ────────────────────────────────────────────────────────────
|
||||
|
||||
// with_mutex — call fn_name(arg) while holding mutex m.
|
||||
//
|
||||
// m: Int — mutex handle returned by __mutex_new()
|
||||
// fn_name: String — name of an El fn with signature (String) -> String
|
||||
// arg: String — argument to pass to fn_name
|
||||
//
|
||||
// Locks the mutex, spawns fn_name(arg) in a child thread, joins to collect
|
||||
// the result, then unlocks. The mutex is held across the entire duration of
|
||||
// fn_name's execution, serializing concurrent callers.
|
||||
//
|
||||
// Note: fn_name must NOT itself acquire the same mutex — that would deadlock.
|
||||
// This is the standard reentrant-mutex caveat.
|
||||
//
|
||||
// Usage:
|
||||
// let m: Int = __mutex_new()
|
||||
// let result: String = with_mutex(m, "update_shared_state", payload)
|
||||
fn with_mutex(m: Int, fn_name: String, arg: String) -> String {
|
||||
__mutex_lock(m)
|
||||
let tid: Int = spawn(fn_name, arg)
|
||||
let result: String = join(tid)
|
||||
__mutex_unlock(m)
|
||||
return result
|
||||
}
|
||||
@@ -0,0 +1,425 @@
|
||||
// runtime/time.el — Time operations, sleep, and formatting.
|
||||
//
|
||||
// Implements the time surface from el-compiler/runtime/legacy/el_runtime.c
|
||||
// (lines 3334–3440, 3471–3656) in pure El, using seed primitives.
|
||||
//
|
||||
// Seed primitives consumed:
|
||||
// __time_now_ns() -> Int (nanoseconds since Unix epoch)
|
||||
// __sleep_ms(n: Int)
|
||||
// __int_to_str(n: Int) -> String
|
||||
// __str_to_int(s: String) -> Int
|
||||
// __float_to_str(f: Float) -> String
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Core — now / sleep
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// time_now — milliseconds since Unix epoch (UTC). Matches legacy time_now().
|
||||
fn time_now() -> Int {
|
||||
return __time_now_ns() / 1000000
|
||||
}
|
||||
|
||||
// time_now_utc — same as time_now; UTC alias kept for compatibility.
|
||||
fn time_now_utc() -> Int {
|
||||
return __time_now_ns() / 1000000
|
||||
}
|
||||
|
||||
// now_ns — nanoseconds since Unix epoch. Matches el_now_instant().
|
||||
fn now_ns() -> Int {
|
||||
return __time_now_ns()
|
||||
}
|
||||
|
||||
// unix_timestamp — whole seconds since Unix epoch. Matches unix_timestamp().
|
||||
fn unix_timestamp() -> Int {
|
||||
return __time_now_ns() / 1000000000
|
||||
}
|
||||
|
||||
// sleep_secs — block for n seconds. Clamps negatives to 0.
|
||||
fn sleep_secs(n: Int) {
|
||||
if n < 0 {
|
||||
__sleep_ms(0)
|
||||
} else {
|
||||
__sleep_ms(n * 1000)
|
||||
}
|
||||
}
|
||||
|
||||
// sleep_ms — block for n milliseconds. Clamps negatives to 0.
|
||||
fn sleep_ms(n: Int) {
|
||||
if n < 0 {
|
||||
__sleep_ms(0)
|
||||
} else {
|
||||
__sleep_ms(n)
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Gregorian decomposition helpers — pure integer arithmetic.
|
||||
//
|
||||
// Algorithm: civil date from days since Unix epoch (1970-01-01).
|
||||
// Based on Howard Hinnant's public-domain civil_from_days formula
|
||||
// (http://howardhinnant.github.io/date_algorithms.html), which the legacy
|
||||
// gmtime_r call performs under the hood.
|
||||
//
|
||||
// We expose the pieces as private helpers (leading underscore convention).
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// _is_leap — 1 if year y is a Gregorian leap year, 0 otherwise.
|
||||
fn _is_leap(y: Int) -> Int {
|
||||
if y % 400 == 0 { return 1 }
|
||||
if y % 100 == 0 { return 0 }
|
||||
if y % 4 == 0 { return 1 }
|
||||
return 0
|
||||
}
|
||||
|
||||
// _days_in_month — number of days in month m of year y (m: 1..12).
|
||||
fn _days_in_month(y: Int, m: Int) -> Int {
|
||||
if m == 1 { return 31 }
|
||||
if m == 2 {
|
||||
if _is_leap(y) == 1 { return 29 }
|
||||
return 28
|
||||
}
|
||||
if m == 3 { return 31 }
|
||||
if m == 4 { return 30 }
|
||||
if m == 5 { return 31 }
|
||||
if m == 6 { return 30 }
|
||||
if m == 7 { return 31 }
|
||||
if m == 8 { return 31 }
|
||||
if m == 9 { return 30 }
|
||||
if m == 10 { return 31 }
|
||||
if m == 11 { return 30 }
|
||||
return 31
|
||||
}
|
||||
|
||||
// _pad2 — zero-pad an integer to at least 2 digits.
|
||||
fn _pad2(n: Int) -> String {
|
||||
if n < 10 { return "0" + __int_to_str(n) }
|
||||
return __int_to_str(n)
|
||||
}
|
||||
|
||||
// _pad4 — zero-pad an integer to at least 4 digits.
|
||||
fn _pad4(n: Int) -> String {
|
||||
if n < 10 { return "000" + __int_to_str(n) }
|
||||
if n < 100 { return "00" + __int_to_str(n) }
|
||||
if n < 1000 { return "0" + __int_to_str(n) }
|
||||
return __int_to_str(n)
|
||||
}
|
||||
|
||||
// _pad3 — zero-pad an integer to at least 3 digits (milliseconds).
|
||||
fn _pad3(n: Int) -> String {
|
||||
if n < 10 { return "00" + __int_to_str(n) }
|
||||
if n < 100 { return "0" + __int_to_str(n) }
|
||||
return __int_to_str(n)
|
||||
}
|
||||
|
||||
// _civil_year_month_day — decompose days-since-epoch (z, may be negative)
|
||||
// into year/month/day using the civil_from_days algorithm.
|
||||
// Returns a JSON object: {"year":Y,"month":M,"day":D}
|
||||
fn _civil_ymd(z: Int) -> String {
|
||||
// shift epoch to 0000-03-01 (makes leap-day math clean)
|
||||
let zz: Int = z + 719468
|
||||
// era: 400-year block
|
||||
let era: Int = zz / 146097
|
||||
if zz < 0 {
|
||||
era = (zz - 146096) / 146097
|
||||
}
|
||||
let doe: Int = zz - era * 146097 // day-of-era [0, 146096]
|
||||
let yoe: Int = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365 // year-of-era [0, 399]
|
||||
let y: Int = yoe + era * 400
|
||||
let doy: Int = doe - (365 * yoe + yoe / 4 - yoe / 100) // day-of-year [0, 365]
|
||||
let mp: Int = (5 * doy + 2) / 153 // month in [0, 11] from March
|
||||
let d: Int = doy - (153 * mp + 2) / 5 + 1 // day [1, 31]
|
||||
let m: Int = mp + 3
|
||||
if mp >= 10 { m = mp - 9 }
|
||||
if mp >= 10 { y = y + 1 }
|
||||
return "{\"year\":" + __int_to_str(y) + ",\"month\":" + __int_to_str(m) + ",\"day\":" + __int_to_str(d) + "}"
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// time_to_parts — decompose a millisecond timestamp into UTC components.
|
||||
//
|
||||
// Returns a JSON string:
|
||||
// {"year":Y,"month":M,"day":D,"hour":H,"minute":M,"second":S,"ms":MS}
|
||||
//
|
||||
// Matches legacy time_to_parts() which returns an ElMap with the same keys.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
fn time_to_parts(ts: Int) -> String {
|
||||
let ms_raw: Int = ts % 1000
|
||||
let ms: Int = ms_raw
|
||||
if ms_raw < 0 { ms = ms_raw + 1000 }
|
||||
|
||||
let s_raw: Int = ts / 1000
|
||||
let s: Int = s_raw
|
||||
if ms_raw < 0 { s = s_raw - 1 }
|
||||
|
||||
// seconds within the day and days since epoch
|
||||
let sec_of_day: Int = s % 86400
|
||||
let day_z: Int = s / 86400
|
||||
// handle negative: floor division
|
||||
if sec_of_day < 0 {
|
||||
sec_of_day = sec_of_day + 86400
|
||||
day_z = day_z - 1
|
||||
}
|
||||
|
||||
let hour: Int = sec_of_day / 3600
|
||||
let rem: Int = sec_of_day % 3600
|
||||
let minute: Int = rem / 60
|
||||
let second: Int = rem % 60
|
||||
|
||||
// date components via civil decomposition
|
||||
let ymd: String = _civil_ymd(day_z)
|
||||
let year: Int = __str_to_int(json_get(ymd, "year"))
|
||||
let month: Int = __str_to_int(json_get(ymd, "month"))
|
||||
let day: Int = __str_to_int(json_get(ymd, "day"))
|
||||
|
||||
return "{\"year\":" + __int_to_str(year) +
|
||||
",\"month\":" + __int_to_str(month) +
|
||||
",\"day\":" + __int_to_str(day) +
|
||||
",\"hour\":" + __int_to_str(hour) +
|
||||
",\"minute\":" + __int_to_str(minute) +
|
||||
",\"second\":" + __int_to_str(second) +
|
||||
",\"ms\":" + __int_to_str(ms) + "}"
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// time_format — format a millisecond timestamp as a string.
|
||||
//
|
||||
// fmt "ISO" (or empty) → "YYYY-MM-DDTHH:MM:SS.mmmZ" (ISO 8601 UTC)
|
||||
// Other fmt values are passed as a strftime-style hint; the El runtime
|
||||
// implements the most common tokens. Unsupported tokens are passed through.
|
||||
//
|
||||
// Matches legacy time_format().
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
fn time_format(ts: Int, fmt: String) -> String {
|
||||
let parts: String = time_to_parts(ts)
|
||||
let y: Int = __str_to_int(json_get(parts, "year"))
|
||||
let mo: Int = __str_to_int(json_get(parts, "month"))
|
||||
let d: Int = __str_to_int(json_get(parts, "day"))
|
||||
let h: Int = __str_to_int(json_get(parts, "hour"))
|
||||
let mi: Int = __str_to_int(json_get(parts, "minute"))
|
||||
let s: Int = __str_to_int(json_get(parts, "second"))
|
||||
let ms: Int = __str_to_int(json_get(parts, "ms"))
|
||||
|
||||
// ISO 8601 UTC: YYYY-MM-DDTHH:MM:SS.mmmZ
|
||||
if str_eq(fmt, "ISO") {
|
||||
return _pad4(y) + "-" + _pad2(mo) + "-" + _pad2(d) +
|
||||
"T" + _pad2(h) + ":" + _pad2(mi) + ":" + _pad2(s) +
|
||||
"." + _pad3(ms) + "Z"
|
||||
}
|
||||
if str_eq(fmt, "") {
|
||||
return _pad4(y) + "-" + _pad2(mo) + "-" + _pad2(d) +
|
||||
"T" + _pad2(h) + ":" + _pad2(mi) + ":" + _pad2(s) +
|
||||
"." + _pad3(ms) + "Z"
|
||||
}
|
||||
|
||||
// strftime-subset: replace common tokens
|
||||
let out: String = fmt
|
||||
let out = str_replace(out, "%Y", _pad4(y))
|
||||
let out = str_replace(out, "%m", _pad2(mo))
|
||||
let out = str_replace(out, "%d", _pad2(d))
|
||||
let out = str_replace(out, "%H", _pad2(h))
|
||||
let out = str_replace(out, "%M", _pad2(mi))
|
||||
let out = str_replace(out, "%S", _pad2(s))
|
||||
let out = str_replace(out, "%3N", _pad3(ms))
|
||||
return out
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// time_from_parts — construct a ms timestamp from seconds + nanosecond offset.
|
||||
//
|
||||
// Matches legacy time_from_parts(secs, ns, tz) — tz is ignored (UTC assumed).
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
fn time_from_parts(secs: Int, ns: Int, tz: String) -> Int {
|
||||
return secs * 1000 + ns / 1000000
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// time_add — add a duration to a millisecond timestamp.
|
||||
//
|
||||
// unit: "ms" | "sec" | "min" | "hour" | "day"
|
||||
// Matches legacy time_add().
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
fn time_add(ts: Int, n: Int, unit: String) -> Int {
|
||||
if str_eq(unit, "ms") { return ts + n }
|
||||
if str_eq(unit, "sec") { return ts + n * 1000 }
|
||||
if str_eq(unit, "min") { return ts + n * 60000 }
|
||||
if str_eq(unit, "hour") { return ts + n * 3600000 }
|
||||
if str_eq(unit, "day") { return ts + n * 86400000 }
|
||||
// default: treat as ms
|
||||
return ts + n
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// time_diff — compute the difference between two millisecond timestamps.
|
||||
//
|
||||
// Returns ts2 - ts1 in the given unit.
|
||||
// unit: "ms" | "sec" | "min" | "hour" | "day"
|
||||
// Matches legacy time_diff().
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
fn time_diff(ts1: Int, ts2: Int, unit: String) -> Int {
|
||||
let d: Int = ts2 - ts1
|
||||
if str_eq(unit, "ms") { return d }
|
||||
if str_eq(unit, "sec") { return d / 1000 }
|
||||
if str_eq(unit, "min") { return d / 60000 }
|
||||
if str_eq(unit, "hour") { return d / 3600000 }
|
||||
if str_eq(unit, "day") { return d / 86400000 }
|
||||
return d
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Instant / Duration — nanosecond-precision temporal types.
|
||||
//
|
||||
// These match the el_now_instant, duration_seconds, duration_millis, etc.
|
||||
// family from legacy lines 3471–3656. Both Instant and Duration are Int
|
||||
// (nanoseconds); the type distinction is at the call-site convention level.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// now — current Instant in nanoseconds. Alias for __time_now_ns().
|
||||
fn now() -> Int {
|
||||
return __time_now_ns()
|
||||
}
|
||||
|
||||
// unix_seconds — Instant from whole seconds since epoch.
|
||||
fn unix_seconds(n: Int) -> Int {
|
||||
return n * 1000000000
|
||||
}
|
||||
|
||||
// unix_millis — Instant from milliseconds since epoch.
|
||||
fn unix_millis(n: Int) -> Int {
|
||||
return n * 1000000
|
||||
}
|
||||
|
||||
// instant_to_unix_seconds — convert Instant nanoseconds to whole seconds.
|
||||
fn instant_to_unix_seconds(i: Int) -> Int {
|
||||
return i / 1000000000
|
||||
}
|
||||
|
||||
// instant_to_unix_millis — convert Instant nanoseconds to milliseconds.
|
||||
fn instant_to_unix_millis(i: Int) -> Int {
|
||||
return i / 1000000
|
||||
}
|
||||
|
||||
// instant_to_iso8601 — format an Instant (nanoseconds) as ISO 8601 UTC.
|
||||
fn instant_to_iso8601(i: Int) -> String {
|
||||
let ms: Int = i / 1000000
|
||||
return time_format(ms, "ISO")
|
||||
}
|
||||
|
||||
// duration_seconds — Duration from n whole seconds.
|
||||
fn duration_seconds(n: Int) -> Int {
|
||||
return n * 1000000000
|
||||
}
|
||||
|
||||
// duration_millis — Duration from n milliseconds.
|
||||
fn duration_millis(n: Int) -> Int {
|
||||
return n * 1000000
|
||||
}
|
||||
|
||||
// duration_nanos — Duration from n nanoseconds (identity).
|
||||
fn duration_nanos(n: Int) -> Int {
|
||||
return n
|
||||
}
|
||||
|
||||
// duration_to_seconds — convert a Duration (nanoseconds) to whole seconds.
|
||||
fn duration_to_seconds(d: Int) -> Int {
|
||||
return d / 1000000000
|
||||
}
|
||||
|
||||
// duration_to_millis — convert a Duration (nanoseconds) to milliseconds.
|
||||
fn duration_to_millis(d: Int) -> Int {
|
||||
return d / 1000000
|
||||
}
|
||||
|
||||
// duration_to_nanos — return the Duration as nanoseconds (identity).
|
||||
fn duration_to_nanos(d: Int) -> Int {
|
||||
return d
|
||||
}
|
||||
|
||||
// sleep_duration — sleep for a Duration (nanoseconds). Clamps negatives to 0.
|
||||
fn sleep_duration(dur: Int) {
|
||||
let ms: Int = dur / 1000000
|
||||
if ms < 0 {
|
||||
__sleep_ms(0)
|
||||
} else {
|
||||
__sleep_ms(ms)
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// TTL cache — time-bounded key/value backed by state.
|
||||
//
|
||||
// Matches legacy ttl_cache_set / ttl_cache_get / ttl_cache_age (lines 3663–3717).
|
||||
// max_age is a Duration (nanoseconds).
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// ttl_cache_set — store a value and record the current Instant for TTL checks.
|
||||
fn ttl_cache_set(key: String, value: String) {
|
||||
state_set(key, value)
|
||||
let stamp_key: String = "__ttl_at:" + key
|
||||
let now_str: String = __int_to_str(__time_now_ns())
|
||||
state_set(stamp_key, now_str)
|
||||
}
|
||||
|
||||
// ttl_cache_get — return value if age < max_age (nanoseconds), else "".
|
||||
fn ttl_cache_get(key: String, max_age: Int) -> String {
|
||||
let stamp_key: String = "__ttl_at:" + key
|
||||
let sv: String = state_get(stamp_key)
|
||||
if str_eq(sv, "") { return "" }
|
||||
let set_at: Int = __str_to_int(sv)
|
||||
let now_ns: Int = __time_now_ns()
|
||||
let age: Int = now_ns - set_at
|
||||
if age < 0 { return "" }
|
||||
if age > max_age { return "" }
|
||||
return state_get(key)
|
||||
}
|
||||
|
||||
// ttl_cache_age — nanoseconds since a key was last set (INT_MAX sentinel if missing).
|
||||
fn ttl_cache_age(key: String) -> Int {
|
||||
let stamp_key: String = "__ttl_at:" + key
|
||||
let sv: String = state_get(stamp_key)
|
||||
if str_eq(sv, "") { return 9223372036854775807 }
|
||||
let set_at: Int = __str_to_int(sv)
|
||||
return __time_now_ns() - set_at
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// uuid_new / uuid_v4 — generate a UUID v4 string.
|
||||
//
|
||||
// Delegates to the __uuid_v4() seed primitive.
|
||||
// Matches legacy uuid_new() / uuid_v4() (lines 4602–4621).
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
fn uuid_new() -> String {
|
||||
return __uuid_v4()
|
||||
}
|
||||
|
||||
fn uuid_v4() -> String {
|
||||
return __uuid_v4()
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// DHARMA-compatible aliases — millisecond-precision timestamps.
|
||||
//
|
||||
// now_millis, unix_timestamp_ms, and time_now_ms all return the same value:
|
||||
// milliseconds since the Unix epoch. They exist because different parts of
|
||||
// the dharma codebase use different names for the same concept.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// now_millis — milliseconds since Unix epoch. Alias for time_now().
|
||||
fn now_millis() -> Int {
|
||||
return __time_now_ns() / 1000000
|
||||
}
|
||||
|
||||
// unix_timestamp_ms — same as now_millis.
|
||||
fn unix_timestamp_ms() -> Int {
|
||||
return __time_now_ns() / 1000000
|
||||
}
|
||||
|
||||
// time_now_ms — same as now_millis.
|
||||
fn time_now_ms() -> Int {
|
||||
return __time_now_ns() / 1000000
|
||||
}
|
||||
@@ -0,0 +1,466 @@
|
||||
# El JavaScript Backend (codegen-js)
|
||||
|
||||
**Status:** Phase 5 complete. ~90% language coverage. Full browser JavaScript can be expressed structurally in El without any `native_js` escape hatches. All additions since Phase 4: anonymous function literals (lambda syntax), try/catch statement, extern fn declarations, direct JS method call syntax on Any-typed values, Promise helpers, Object/Array utilities, and URL import declarations. Proof: `examples/browser-auth.el` is a complete Supabase auth flow with zero `native_js` or `native_js_call` calls.
|
||||
|
||||
**Authoritative files**
|
||||
|
||||
| File | Role |
|
||||
|---|---|
|
||||
| `el-compiler/src/codegen-js.el` | El → JS code generator (mirrors `codegen.el`) |
|
||||
| `el-compiler/runtime/el_runtime.js` | Browser/Node runtime that compiled programs link against |
|
||||
| `el-compiler/src/compiler.el` | Adds `compile_js()` and `--target=js` CLI dispatch |
|
||||
| `spec/codegen-js.md` | This document |
|
||||
|
||||
---
|
||||
|
||||
## 1. Why a JS backend exists
|
||||
|
||||
El compiles to C today. C is the right substrate for the agent runtime, the DHARMA daemon, and Engram. But three first-class consumers of El need to **run in a browser**, where C is not an option:
|
||||
|
||||
1. **`el-ui/runtime/`** — the activation-based frontend framework written in JS. The long-term plan is to author components and the runtime itself in El and compile them down to JS.
|
||||
2. **`cgi-studio`** — the web app for cultivating CGIs. Today it is hand-written JS. Once the JS backend is mature, the studio's UI logic can be authored in El and share types/identifier names with the CGI it cultivates.
|
||||
3. **Marketplace plugin UIs** — third parties writing browser-side El that runs untrusted in a sandbox. They need a JS target.
|
||||
|
||||
A secondary motivation: **El-on-Node**. CLI tooling, build scripts, and tests benefit from a tight `el → js → node` cycle without a `cc` step.
|
||||
|
||||
---
|
||||
|
||||
## 2. Type representation strategy
|
||||
|
||||
The C backend pretends every value is `int64_t`. That is a deliberate runtime trick to avoid dynamic dispatch in generated C. JavaScript already has tagged dynamic values, so the JS backend is **simpler**: every El value is a native JS value, and the tag of `el_val_t` collapses into the JS type system.
|
||||
|
||||
| El type | C representation | JS representation |
|
||||
|---|---|---|
|
||||
| `Int` | `int64_t` (direct) | `number` (with `Number.isSafeInteger` caveat — see §6) |
|
||||
| `Float` | `int64_t` bit-cast of `double` via `el_from_float` | `number` (no bit-cast — JS number IS a double) |
|
||||
| `Bool` | `int64_t`, 0 = false, nonzero = true | `boolean` |
|
||||
| `String` | `(int64_t)(uintptr_t)cstring` | `string` |
|
||||
| `Void` | C `void` | `undefined` |
|
||||
| `[T]` (List) | `el_val_t` pointer to refcounted struct | `Array<any>` |
|
||||
| `Map<K,V>` | `el_val_t` pointer to refcounted struct | plain object `{[key]: any}` |
|
||||
| `EL_NULL` (`0`) | `(el_val_t)0` | `null` |
|
||||
| Any | `el_val_t` | `any` (no compile-time check) |
|
||||
|
||||
**Key consequences:**
|
||||
|
||||
- `+` on two strings is JS `+` (string concat) — no `el_str_concat()` runtime call needed for the common case. The runtime DOES export `el_str_concat` for the cases where codegen does not know the types.
|
||||
- `==` on strings is `===` — not `str_eq()`. Same disambiguation logic as the C backend (look at left/right kind, fall back to `str_eq` for identifiers without int annotation).
|
||||
- `Map` access `m["foo"]` compiles to JS `m["foo"]` (no `el_get_field`). For `Field` access (`m.foo`) we emit `m["foo"]` so it works on plain objects regardless of prototype shape.
|
||||
- List access `arr[i]` is JS `arr[i]`. No bounds checking — same as C (which segfaults on bad index). Could add `el_list_get` wrapper later for safe access.
|
||||
- `EL_NULL` becomes JS `null`, not `undefined`. The runtime checks for `=== null` consistently. This avoids the JS undefined/null fork and matches El's single null value.
|
||||
|
||||
---
|
||||
|
||||
## 3. Builtin runtime layer (`el_runtime.js`)
|
||||
|
||||
Same function names as `el_runtime.c` wherever possible, so codegen-js can emit the same call sites. The runtime is a single ES module that exposes every builtin as a named export AND attaches them to a `globalThis.__el` namespace (so generated code can do either `import * as el from './el_runtime.js'` or assume globals).
|
||||
|
||||
**The codegen-js generated output uses the global-namespace style:** every emitted file starts with `import './el_runtime.js'` (which side-effects the globals) so call sites stay flat — `println(x)` not `el.println(x)`. This matches the C backend's flat call surface and keeps the generated code grep-compatible across targets.
|
||||
|
||||
### Implemented (~90 builtins)
|
||||
|
||||
| Category | Functions |
|
||||
|---|---|
|
||||
| I/O | `println`, `print` |
|
||||
| String | `el_str_concat`, `str_concat`, `str_eq`, `str_starts_with`, `str_ends_with`, `str_len`, `int_to_str`, `str_to_int`, `str_slice`, `str_contains`, `str_replace`, `str_to_upper`, `str_to_lower`, `str_trim`, `str_index_of`, `str_split`, `str_char_at`, `str_char_code`, `str_lower`, `str_upper`, `str_pad_left`, `str_pad_right` |
|
||||
| Math | `el_abs`, `el_max`, `el_min`, `math_sqrt`, `math_log`, `math_ln`, `math_sin`, `math_cos`, `math_pi` |
|
||||
| Float | `float_to_str`, `int_to_float`, `float_to_int`, `format_float`, `decimal_round`, `str_to_float` |
|
||||
| List | `el_list_new`, `el_list_len`, `el_list_get`, `el_list_append`, `el_list_empty`, `el_list_clone`, `list_push`, `list_push_front`, `list_join`, `list_range` |
|
||||
| Map | `el_map_new`, `el_get_field`, `el_map_get`, `el_map_set` |
|
||||
| HTTP | `http_get`, `http_post`, `http_post_json`, `http_get_with_headers`, `http_post_with_headers` (via `fetch()`, return `Promise<string>`) |
|
||||
| FS | `fs_read`, `fs_write`, `fs_list` (Node-only) |
|
||||
| JSON | `json_parse`, `json_stringify`, `json_get`, `json_get_string`, `json_get_int`, `json_get_float`, `json_get_bool`, `json_get_raw`, `json_set`, `json_array_len` |
|
||||
| Time | `time_now`, `time_now_utc`, `sleep_secs` (Node), `sleep_ms` |
|
||||
| Bool | `bool_to_str` |
|
||||
| Process | `exit_program` (Node `process.exit`) |
|
||||
| Refcount | `el_retain`, `el_release` (no-ops) |
|
||||
| Method shortforms | `append`, `len`, `get`, `map_get`, `map_set` |
|
||||
| Native VM aliases | `native_list_get`, `native_list_len`, `native_list_append`, `native_list_empty`, `native_list_clone`, `native_string_chars`, `native_int_to_str` |
|
||||
| `args` / `env` / `state_*` | Process args, environment, in-memory state |
|
||||
| UUID | `uuid_v4`, `uuid_new` |
|
||||
| DOM bridge | `dom_get_element`, `dom_get_value`, `dom_set_value`, `dom_get_text`, `dom_set_text`, `dom_set_prop`, `dom_get_prop`, `dom_set_style`, `dom_add_class`, `dom_remove_class`, `dom_show`, `dom_hide`, `dom_listen`, `dom_query`, `dom_query_all`, `dom_create`, `dom_append`, `dom_remove`, `dom_is_null` (browser-only) |
|
||||
| DOM extended | `dom_set_attr`, `dom_get_attr`, `dom_remove_attr`, `dom_set_html`, `dom_get_html`, `dom_get_parent`, `dom_contains_class`, `dom_get_checked`, `dom_set_checked` (browser-only) |
|
||||
| Timers | `set_timeout(ms, cb)`, `set_interval(ms, cb) -> Int`, `clear_interval(handle)` |
|
||||
| Local storage | `local_storage_get`, `local_storage_set`, `local_storage_remove` (browser-only) |
|
||||
| Window | `window_location`, `window_redirect`, `window_on_load`, `window_set`, `window_get` |
|
||||
| Debug | `console_log` |
|
||||
| Promise helpers (Phase 5) | `promise_then(p, cb)`, `promise_catch(p, cb)`, `promise_resolve(val)`, `promise_reject(msg)` |
|
||||
| Object / Array (Phase 5) | `object_assign(t, s)`, `object_keys(obj)`, `object_values(obj)`, `json_deep_clone(obj)`, `array_from(iterable)`, `type_of(val)`, `instanceof_check(val, name)` |
|
||||
| native_js escape hatch | `native_js(code)` — eval; `native_js_call(obj, method, args)` — method call. Use only when no structural alternative exists |
|
||||
|
||||
### Stubbed (throw at runtime)
|
||||
|
||||
Every function in this list compiles successfully but throws `Error("not supported in JS target — needs server-side delegation: <name>")` when called. This is a **runtime** error, not a compile error, so it doesn't block compilation of code that has dead-code paths through these functions.
|
||||
|
||||
- All `dharma_*` (membership in DHARMA network requires the daemon)
|
||||
- All `engram_*` (needs the embedded SQLite + activation engine — could be reimplemented in JS later)
|
||||
- All `llm_*` (CORS + API key handling — must go through a server-side proxy)
|
||||
- `http_serve` (browsers don't host servers; Node could, but that's a separate runtime mode)
|
||||
- `el_cgi_init` (CGI identity is a server-side concept)
|
||||
- Crypto: `sha256_*`, `hmac_sha256_*`, `base64*` (deferred — can use `crypto.subtle` later)
|
||||
|
||||
### Browser-side specific behavior
|
||||
|
||||
When running in a browser:
|
||||
- `println` / `print` map to `console.log` (no stdout in browsers)
|
||||
- `http_get` / `http_post` use `fetch()` (CORS applies)
|
||||
- `fs_*` throws (browsers have no fs)
|
||||
- `args()` returns `[]`
|
||||
- `env(k)` throws (or could read from a global config object — TBD)
|
||||
|
||||
When running in Node:
|
||||
- `println` / `print` map to `console.log` and `process.stdout.write`
|
||||
- `fs_*` use `node:fs/promises` (sync versions for the simple cases)
|
||||
- `args()` returns `process.argv.slice(2)`
|
||||
- `env(k)` returns `process.env[k] ?? null`
|
||||
|
||||
The runtime auto-detects via `typeof window === 'undefined'`.
|
||||
|
||||
---
|
||||
|
||||
## 4. Tradeoffs vs the C backend
|
||||
|
||||
| Concern | C backend | JS backend |
|
||||
|---|---|---|
|
||||
| **Static types** | El's `Int` becomes `int64_t`, real arithmetic | El's `Int` becomes `number` — loses precision past 2^53 |
|
||||
| **Linking model** | Static link against `el_runtime.c` + libcurl + libpthread | ES module import of `el_runtime.js` |
|
||||
| **Dynamic dispatch** | `dlsym` for `http_set_handler` / `llm_register_tool` (requires `-rdynamic`) | JS function value lookup via `globalThis[name]` — no compiler flag |
|
||||
| **Tool registry** | dlsym walks symbol table; tool fns must be top-level C symbols | Tool fns live as exports of the generated module; trivially callable |
|
||||
| **Memory model** | Refcounted lists/maps with `el_retain`/`el_release` to avoid leaks | JS GC handles all of it; `el_retain`/`el_release` are no-ops |
|
||||
| **`+` overload** | Has to dispatch in codegen between `el_str_concat` and integer `+` because at C level both are `int64_t` | JS `+` is already overloaded: `"a" + "b"` → `"ab"`, `1 + 2` → `3`. Codegen still preserves the existing dispatch for safety, but the runtime fallback is correct |
|
||||
| **Concurrency** | `pthread`-backed `http_serve` | Single-threaded event loop; `http_serve` not supported in this target |
|
||||
| **HTTP client** | libcurl, blocking, returns body string | `fetch()` is async — see §5 |
|
||||
| **CGI identity** | `el_cgi_init` runs at start of `main()` | Not supported; UI code is not a CGI principal |
|
||||
| **DHARMA / LLM** | Native, blocking, libcurl-backed | Not supported — all such calls throw and the program is expected to delegate to a server-side El daemon via plain HTTP |
|
||||
| **Compile speed** | El → C → cc → binary (cc is the slow step) | El → JS → done. Faster iteration |
|
||||
| **Output size** | Static binary ~2MB | Source `.js` + ~10kb runtime |
|
||||
|
||||
---
|
||||
|
||||
## 5. The async problem
|
||||
|
||||
`fetch()` is async. The C backend's `http_get(url)` is synchronous and returns the body string directly. El source was written assuming sync. Three options:
|
||||
|
||||
1. **Pretend it's sync from El's POV; use synchronous XHR (browser) or `child_process.execSync('curl ...')` (Node).** Bad: synchronous XHR is deprecated and frozen on the main thread; `execSync` is a hack.
|
||||
2. **Make every `http_*` builtin in the JS runtime return a `Promise`, and rewrite codegen-js to insert `await` everywhere.** This requires turning every El function that transitively calls a network builtin into an `async fn` in JS. Doable, but invasive.
|
||||
3. **Explicit `@async` decorator on El functions; codegen-js emits `async function` + `await` for known-async call sites.** This is the approach implemented.
|
||||
|
||||
**Decision:** option 3, with an explicit opt-in decorator. `http_get`, `http_post`, `http_post_json`, `http_get_with_headers`, and `http_post_with_headers` in `el_runtime.js` return `Promise<string>`. `codegen-js.el` now emits `await` before calls to these builtins and before calls to any El function decorated `@async`.
|
||||
|
||||
### How to use async in El (JS target)
|
||||
|
||||
Mark a function with `@async` to declare it as async. Any call to that function from another El function will automatically get `await` in the generated JS. The callee must also be `@async` (or call only non-async code) for the pattern to compose correctly.
|
||||
|
||||
```el
|
||||
@async
|
||||
fn fetch_user(id: String) -> String {
|
||||
http_get("https://api.example.com/users/" + id)
|
||||
}
|
||||
|
||||
@async
|
||||
fn main() -> Void {
|
||||
let body = fetch_user("42")
|
||||
println(body)
|
||||
}
|
||||
```
|
||||
|
||||
Compiles to:
|
||||
|
||||
```javascript
|
||||
async function fetch_user(id) {
|
||||
return await http_get("https://api.example.com/users/" + id);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
let body = await fetch_user("42");
|
||||
println(body);
|
||||
}
|
||||
|
||||
main();
|
||||
```
|
||||
|
||||
**Limitations:**
|
||||
- `@async` is a JS-target-only convention. The C backend ignores the decorator (it calls the synchronous libcurl-backed version).
|
||||
- Implicit taint propagation (auto-marking all transitive callers) is not implemented. The programmer must explicitly add `@async` to every function in the call chain that reaches an async builtin.
|
||||
- Forward-reference calls to `@async` functions are handled correctly: codegen-js does a pre-registration pass over all FnDefs before emitting any code.
|
||||
|
||||
For programs that do not touch HTTP, no `@async` annotation is needed and the generated code is identical to before.
|
||||
|
||||
---
|
||||
|
||||
## 6. Number precision
|
||||
|
||||
JS `number` is IEEE 754 double — only 53 bits of integer precision. El `Int` is `int64_t` and the runtime sometimes uses the full 64 bits (e.g. `time_now_utc` returns nanoseconds-since-epoch, which exceeds 2^53 in practice).
|
||||
|
||||
**Decision for this scaffold:** accept the precision loss. Document it. UI code does not use 64-bit timestamps. If/when a use case demands it, `time_now_utc` can return a `BigInt` and we can introduce a `BigInt` sub-mode. That's a follow-up.
|
||||
|
||||
---
|
||||
|
||||
## 7. Language features — JS target coverage
|
||||
|
||||
### Fully supported
|
||||
|
||||
| Feature | Notes |
|
||||
|---|---|
|
||||
| `cgi {}` block | Compiled to a no-op + comment (UI code is not a CGI) |
|
||||
| `service {}` block | Compiled to a no-op + comment |
|
||||
| `match` expressions | LitInt/LitStr/LitBool/Wildcard/Binding/Variant via IIFE if/else chain |
|
||||
| `type` (struct) defs | Skipped; structs are plain JS objects. `t["field"]` works |
|
||||
| `enum` defs | Skipped; enum values are strings or ints |
|
||||
| `?` postfix (nil-prop) | `obj?.field` emits `(obj)?.["field"] ?? null` via JS optional chaining |
|
||||
| `extern fn` | Emits a comment; calls resolve to JS environment globals |
|
||||
| Anonymous function literals | `fn(p: T) -> R { body }` emits a hoisted `function __lambda_N(p)` |
|
||||
| `try/catch` | Emits `try { ... } catch (name) { ... }` directly |
|
||||
| URL imports | `import "https://..."` emits ES module import (or comment in bundle mode) |
|
||||
| Method call on `Any` | `obj.method(args)` emits `obj.method(args)` for non-El-shortform methods |
|
||||
| Field access on `Any` | `obj.field` emits `obj["field"]` (bracket notation, works on prototype chains) |
|
||||
| `@async` decorator | `async function` + `await` at call sites for async builtins and `@async` fns |
|
||||
|
||||
### Not supported (stub throws or no-op)
|
||||
|
||||
| Feature | Status | Notes |
|
||||
|---|---|---|
|
||||
| All `dharma_*` | Stub throws | Requires server-side daemon |
|
||||
| All `engram_*` | Stub throws | Could be ported to IndexedDB later |
|
||||
| All `llm_*` | Stub throws | Route through server |
|
||||
| `http_serve` | Stub throws | Browsers cannot host servers |
|
||||
| `el_cgi_init` | No-op | CGI identity is server-side |
|
||||
| Capability enforcement | Not enforced | Runtime stubs throw; compile-time check is a follow-up |
|
||||
| VBD role check | Not enforced | Same |
|
||||
| Float bit-cast | Not needed | JS number is already a double |
|
||||
| Crypto primitives | Stub throws | Add via `crypto.subtle` later |
|
||||
| `state_*` | In-memory only | Resets on page reload |
|
||||
| `args()` | Node-only | Browser returns `[]` |
|
||||
| `fs_*` | Node-only | Browser throws |
|
||||
|
||||
---
|
||||
|
||||
## 7a. Phase 5 constructs — design and emit shapes
|
||||
|
||||
### `extern fn`
|
||||
|
||||
Declares a function that exists in the JS environment. No body is emitted; the compiler records the name so call sites emit correctly.
|
||||
|
||||
```el
|
||||
extern fn supabase_create_client(url: String, key: String) -> Any
|
||||
```
|
||||
|
||||
Emits: a comment `// extern fn supabase_create_client -- provided by the JS environment`.
|
||||
Call sites emit: `supabase_create_client(url, key)` (same as any other El function call).
|
||||
|
||||
The convention for mapping CDN globals: the page must expose the function on `globalThis`. For Supabase, the CDN bundle exposes `supabase.createClient`; a thin adapter assigns `globalThis.supabase_create_client = supabase.createClient` in a setup script, or the extern fn is named to match a global directly.
|
||||
|
||||
### Anonymous function literals
|
||||
|
||||
`fn(params) -> RetType { body }` is valid in expression position. Emitted as a hoisted function declaration with a generated name.
|
||||
|
||||
```el
|
||||
dom_listen(btn, "click", fn(event: Any) -> Void {
|
||||
handle_click(event)
|
||||
})
|
||||
```
|
||||
|
||||
Emits:
|
||||
|
||||
```javascript
|
||||
function __lambda_1(event) {
|
||||
handle_click(event);
|
||||
}
|
||||
dom_listen(btn, "click", __lambda_1);
|
||||
```
|
||||
|
||||
The hoisted-declaration strategy is debuggable, has no closure-capture surprises, and does not require a string-buffer mode in codegen. The generated name appears in stack traces.
|
||||
|
||||
### `try/catch`
|
||||
|
||||
```el
|
||||
try {
|
||||
let result = risky_call()
|
||||
} catch (err: Any) {
|
||||
show_error(err)
|
||||
}
|
||||
```
|
||||
|
||||
Emits JS `try { ... } catch (err) { ... }` directly. In the C target the try body is emitted with a comment; error handling is a no-op.
|
||||
|
||||
### Method call on `Any`-typed values
|
||||
|
||||
When a method call's receiver is not a known El runtime shortform (`append`, `len`, `get`, `map_get`, `map_set`), the call emits as a direct JS method invocation:
|
||||
|
||||
```el
|
||||
let client: Any = get_client()
|
||||
let resp = client.auth.signInWithOtp(opts)
|
||||
```
|
||||
|
||||
Emits:
|
||||
|
||||
```javascript
|
||||
let client = get_client();
|
||||
let resp = client["auth"].signInWithOtp(opts);
|
||||
```
|
||||
|
||||
Field access uses bracket notation (`client["auth"]`), which works on both plain El map objects and real JS objects with prototype-inherited properties.
|
||||
|
||||
### URL imports
|
||||
|
||||
```el
|
||||
import "https://cdn.jsdelivr.net/npm/@supabase/supabase-js@2/dist/umd/supabase.js"
|
||||
```
|
||||
|
||||
In module mode: `import "https://...";` at the top of the generated file.
|
||||
In bundle/IIFE mode: `// external: https://...` comment.
|
||||
El source imports (`.el` files) are excluded -- they were already inlined by `resolve_imports`.
|
||||
|
||||
---
|
||||
|
||||
## 8. CLI dispatch — `--target=js`
|
||||
|
||||
The compiler entry point `compiler.el` adds a `compile_js(source: String) -> String` alongside the existing `compile()`. The CLI behavior:
|
||||
|
||||
```
|
||||
elc <source.el> <output> # default — emit C
|
||||
elc --target=c <source.el> <out> # explicit — emit C
|
||||
elc --target=js <source.el> <out> # emit JS
|
||||
|
||||
elc --target=js source.el # write JS to stdout (no out path)
|
||||
```
|
||||
|
||||
The argv parser scans for a `--target=<lang>` token; remaining positional args are `<source>` and optional `<out>`. The dispatch logic stays in El: a `compile_dispatch(target, source) -> String` switch.
|
||||
|
||||
---
|
||||
|
||||
## 8a. Production output — `--minify` and `--obfuscate`
|
||||
|
||||
Two post-processing flags produce production-ready browser JS in a single compiler invocation, replacing any external post-processing scripts.
|
||||
|
||||
### Usage
|
||||
|
||||
```
|
||||
elc --target=js --bundle --minify source.el > output.min.js
|
||||
elc --target=js --bundle --obfuscate source.el > output.obf.js
|
||||
elc --target=js --bundle --minify --obfuscate source.el > output.final.js
|
||||
```
|
||||
|
||||
Both flags require `--target=js`. Passing either without `--target=js` prints an error and exits with code 1.
|
||||
|
||||
`--obfuscate` implies `--minify` — obfuscating unminified code produces no benefit and only increases output size.
|
||||
|
||||
### Pipeline order
|
||||
|
||||
```
|
||||
generate JS -> (if --bundle, wrap in IIFE) -> (if --minify, run terser) -> (if --obfuscate, run javascript-obfuscator) -> output
|
||||
```
|
||||
|
||||
### Tool discovery
|
||||
|
||||
The compiler looks for each tool in this order:
|
||||
|
||||
1. `<src_dir>/node_modules/.bin/<tool>` — local install next to source file
|
||||
2. `<src_dir>/../node_modules/.bin/<tool>` — one level up (monorepo layout)
|
||||
3. `npx --yes <tool>` — fall back to npx (uses globally cached package or downloads on first use)
|
||||
|
||||
If no path resolves and npx is not on `PATH`, the compiler prints a clear error and exits non-zero:
|
||||
|
||||
```
|
||||
el-compiler: error: terser not found. Run 'npm install terser' in your project directory.
|
||||
el-compiler: error: javascript-obfuscator not found. Run 'npm install javascript-obfuscator' in your project directory.
|
||||
```
|
||||
|
||||
### Minification (terser)
|
||||
|
||||
Command issued internally:
|
||||
|
||||
```
|
||||
terser <tmpfile> --compress passes=2,drop_console=false,drop_debugger=true \
|
||||
--mangle 'reserved=[<reserved>]' --output <tmpfile.min>
|
||||
```
|
||||
|
||||
### Obfuscation (javascript-obfuscator)
|
||||
|
||||
Command issued internally (runs after minification):
|
||||
|
||||
```
|
||||
javascript-obfuscator <input> --output <output>
|
||||
--compact true
|
||||
--simplify true
|
||||
--string-array true
|
||||
--string-array-encoding base64
|
||||
--string-array-threshold 0.75
|
||||
--identifier-names-generator hexadecimal
|
||||
--rename-globals false
|
||||
--self-defending false
|
||||
--reserved-names <reserved>
|
||||
```
|
||||
|
||||
### Reserved names
|
||||
|
||||
These identifiers are protected from renaming by both tools. They are referenced directly from HTML `onclick=` attributes and other global-scope callsites:
|
||||
|
||||
```
|
||||
neuronDemoToggle, neuronDemoSend, neuronDemoReset,
|
||||
signInWith, signInWithEmail, signUpWithEmail, sendMagicLink,
|
||||
signOut, resetPassword, sendResetEmail, updatePassword,
|
||||
showSignIn, showSignUp, hideReset,
|
||||
setSort, addFamilyMember, removeFamilyMember, copyForPlatform, entHeadcountChange,
|
||||
NEURON_CFG
|
||||
```
|
||||
|
||||
### Temp files
|
||||
|
||||
The compiler uses `/tmp/elc-<pid>-<timestamp>.js` naming for temp files. All temp files are cleaned up on both success and failure paths.
|
||||
|
||||
### Implementation notes
|
||||
|
||||
- The compiler adds `stdout_to_file(path)` / `stdout_restore()` builtins to the C runtime (`el_runtime.c`) to capture codegen output (which is streamed via `println`) into a temp file before passing it to the external tools.
|
||||
- `--minify` and `--obfuscate` error messages are printed after stdout is restored, so they always reach the terminal regardless of output redirection.
|
||||
|
||||
---
|
||||
|
||||
## 9. The path to compiling el-ui/runtime through this backend
|
||||
|
||||
This is the real-world test. `el-ui/runtime/src/` is currently 5 hand-written `.js` files. The path to authoring them in El:
|
||||
|
||||
1. **Phase 1 — Hello-world.** DONE.
|
||||
2. **Phase 2 — Language coverage.** DONE. `match`, struct/enum field access, `?`-propagation, `for`-over-list, complete operators.
|
||||
3. **Phase 3 — DOM bridge.** DONE. Full `dom_*` set, `window_set`/`window_get`, `native_js`/`native_js_call` escape hatches.
|
||||
4. **Phase 4 — Production output.** DONE. `--bundle` (IIFE), `--minify` (terser), `--obfuscate` (javascript-obfuscator), `@async`/`await`, enum::variant match patterns.
|
||||
5. **Phase 5 — Full JS expression coverage.** DONE. This is the phase documented in this revision.
|
||||
- `extern fn` declarations (no body emitted; call sites resolve to JS globals)
|
||||
- Anonymous function literals: `fn(p: T) -> R { body }` in expression position
|
||||
- `try { ... } catch (name: T) { ... }` statement
|
||||
- Method call on `Any`-typed values: `client.auth.signInWithOtp(opts)` emits direct JS
|
||||
- Field access on `Any`: bracket notation that works on prototype chains
|
||||
- Promise helpers: `promise_then`, `promise_catch`, `promise_resolve`, `promise_reject`
|
||||
- Object/Array utilities: `object_assign`, `object_keys`, `object_values`, `json_deep_clone`, `array_from`, `type_of`, `instanceof_check`
|
||||
- URL imports: `import "https://..."` emits ES module import
|
||||
- **Proof**: `examples/browser-auth.el` -- complete Supabase auth flow with zero `native_js` or `native_js_call`
|
||||
6. **Phase 6 — Port `el-ui/runtime/`.** Translate the 5 JS files to El, compile to JS, swap in. Run el-ui's existing tests. The language is now expressive enough for this.
|
||||
7. **Phase 7 — Port cgi-studio UI.** Larger surface area; same pattern.
|
||||
8. **Phase 8 — Marketplace plugins.** Open the door for third-party UI El.
|
||||
|
||||
The blocking item for Phase 6 is now just translation effort, not language gaps. Phase 5 removed the last structural barriers.
|
||||
|
||||
---
|
||||
|
||||
## 10. Test
|
||||
|
||||
```bash
|
||||
echo 'fn main() -> Void { println("hello from el-js") }' > /tmp/hello.el
|
||||
elc --target=js /tmp/hello.el > /tmp/hello.js
|
||||
node /tmp/hello.js
|
||||
# → hello from el-js
|
||||
```
|
||||
|
||||
This should pass after the bootstrap rebuild. See §11.
|
||||
|
||||
---
|
||||
|
||||
## 11. Bootstrap status
|
||||
|
||||
Adding `--target=js` to `compile()` requires regenerating the shipped `elc` binary at `dist/platform/elc`. The rebuild path is:
|
||||
|
||||
1. Existing `elc` binary compiles updated `elc-combined.el` (which now includes `codegen-js.el` and the `--target=js` dispatch) → `elc.c`.
|
||||
2. `cc` compiles `elc.c` → new `elc` binary.
|
||||
3. New `elc` binary supports `--target=js`.
|
||||
|
||||
The scaffold checks all four scaffold files in. The bootstrap rebuild happens as a follow-up step, gated on review of this design doc.
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,22 @@
|
||||
// cross-calendar-comparison.el — same Instant under two different calendars
|
||||
// produces identical cal_to_instant() outputs. Calendar choice does not change
|
||||
// the underlying instant — only the projection.
|
||||
|
||||
fn run_test() -> Int {
|
||||
let i: Instant = unix_seconds(1782216000)
|
||||
let z: Zone = zone("America/New_York")
|
||||
let earth: Calendar = earth_calendar(z)
|
||||
let mars: Calendar = mars_calendar()
|
||||
let ct_earth: CalendarTime = in_calendar(i, earth)
|
||||
let ct_mars: CalendarTime = in_calendar(i, mars)
|
||||
let i_earth: Instant = cal_to_instant(ct_earth)
|
||||
let i_mars: Instant = cal_to_instant(ct_mars)
|
||||
if i_earth == i_mars {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
fn main() -> Void {
|
||||
println(int_to_str(run_test()))
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
// cycle-300yr.el — CycleCalendar with a 100-year period proves the math
|
||||
// holds at long periods. (300 years exceeds int64 nanos: 2^63 ns ≈ 292 yr;
|
||||
// 100 yr is the largest round period that fits while leaving headroom for
|
||||
// instants on either side.) One earth year apart yields phase_diff ~ 0.01.
|
||||
|
||||
fn run_test() -> String {
|
||||
// 100 Julian years = 100 * 31557600 = 3155760000 seconds.
|
||||
let period: Duration = duration_seconds(3155760000)
|
||||
let cal: Calendar = cycle_calendar(period)
|
||||
let base: Instant = unix_seconds(0)
|
||||
let later: Instant = unix_seconds(31557600)
|
||||
let ct1: CalendarTime = in_calendar(base, cal)
|
||||
let ct2: CalendarTime = in_calendar(later, cal)
|
||||
let p1: Float = cal_cycle_phase(ct1)
|
||||
let p2: Float = cal_cycle_phase(ct2)
|
||||
let diff: Float = p2 - p1
|
||||
// 1 year / 100 years = 0.01
|
||||
return format_float(diff, 2)
|
||||
}
|
||||
|
||||
fn main() -> Void {
|
||||
println(run_test())
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
// cycle-30hr.el — CycleCalendar with a 30-hour period.
|
||||
// Two CalendarTimes 15 hours apart should have cycle_phase differ by 0.5.
|
||||
// We compare phases via float subtraction with format_float for determinism.
|
||||
|
||||
fn run_test() -> String {
|
||||
let period: Duration = 30.hours
|
||||
let cal: Calendar = cycle_calendar(period)
|
||||
let base: Instant = unix_seconds(0)
|
||||
let later: Instant = base + 15.hours
|
||||
let ct1: CalendarTime = in_calendar(base, cal)
|
||||
let ct2: CalendarTime = in_calendar(later, cal)
|
||||
let p1: Float = cal_cycle_phase(ct1)
|
||||
let p2: Float = cal_cycle_phase(ct2)
|
||||
let diff: Float = p2 - p1
|
||||
return format_float(diff, 1)
|
||||
}
|
||||
|
||||
fn main() -> Void {
|
||||
println(run_test())
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
// dst-spring-forward.el — Earth calendar handles the DST transition.
|
||||
// 2026 spring DST: March 8 at 02:00 EST → clocks jump to 03:00 EDT.
|
||||
// 2026-03-08 06:30 UTC = 01:30 EST (just before the jump).
|
||||
// Add 1 hour → 07:30 UTC = 03:30 EDT (the wall clock skipped 02:30 entirely).
|
||||
|
||||
fn run_test() -> String {
|
||||
let z: Zone = zone("America/New_York")
|
||||
let cal: Calendar = earth_calendar(z)
|
||||
let i: Instant = unix_seconds(1772951400)
|
||||
let later: Instant = i + 1.hour
|
||||
let ct: CalendarTime = in_calendar(later, cal)
|
||||
return cal_format(ct, "HH:mm z")
|
||||
}
|
||||
|
||||
fn main() -> Void {
|
||||
println(run_test())
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
// earth-zone.el — EarthCalendar(zone) formats with the right zone abbreviation.
|
||||
// We use a fixed Instant on July 4, 2026 (definitely EDT in NYC) so the
|
||||
// abbreviation is deterministic across runs.
|
||||
|
||||
fn run_test() -> String {
|
||||
let z: Zone = zone("America/New_York")
|
||||
let cal: Calendar = earth_calendar(z)
|
||||
// 2026-07-04 12:00:00 UTC = 2026-07-04 08:00:00 EDT
|
||||
let i: Instant = unix_seconds(1782216000)
|
||||
let ct: CalendarTime = in_calendar(i, cal)
|
||||
return cal_format(ct, "z")
|
||||
}
|
||||
|
||||
fn main() -> Void {
|
||||
println(run_test())
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
// local-date-arithmetic.el — LocalDate + Duration produces a LocalDate.
|
||||
// 2026-05-28 + 7 days crosses the May/June boundary, yielding 2026-06-04.
|
||||
|
||||
fn run_test() -> String {
|
||||
let d1: LocalDate = local_date(2026, 5, 28)
|
||||
let week: Duration = 7.days
|
||||
let d2: LocalDate = d1 + week
|
||||
let y: Int = local_date_year(d2)
|
||||
let m: Int = local_date_month(d2)
|
||||
let day: Int = local_date_day(d2)
|
||||
return int_to_str(y) + "-" + int_to_str(m) + "-" + int_to_str(day)
|
||||
}
|
||||
|
||||
fn main() -> Void {
|
||||
println(run_test())
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
// mars-calendar.el — MarsCalendar uses sol period 88775.244 seconds.
|
||||
// Two instants exactly one sol apart should differ by 1 in sol number.
|
||||
|
||||
fn run_test() -> Int {
|
||||
let cal: Calendar = mars_calendar()
|
||||
let base: Instant = unix_seconds(0)
|
||||
// 88775.244 s * 1e9 nanos = 88775244000000 nanos
|
||||
let one_sol_ns: Int = 88775244000000
|
||||
let one_sol: Duration = el_duration_from_nanos(one_sol_ns)
|
||||
let later: Instant = base + one_sol
|
||||
let ct1: CalendarTime = in_calendar(base, cal)
|
||||
let ct2: CalendarTime = in_calendar(later, cal)
|
||||
let sol1: String = cal_format(ct1, "%sol")
|
||||
let sol2: String = cal_format(ct2, "%sol")
|
||||
let diff: Int = str_to_int(sol2) - str_to_int(sol1)
|
||||
return diff
|
||||
}
|
||||
|
||||
fn main() -> Void {
|
||||
println(int_to_str(run_test()))
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
// no-cycle.el — NoCycleCalendar's cycle_phase returns NaN sentinel.
|
||||
// Detection via float_to_str — NaN renders as "nan" under %g.
|
||||
|
||||
fn run_test() -> Int {
|
||||
let cal: Calendar = no_cycle_calendar()
|
||||
let i: Instant = unix_seconds(1000000000)
|
||||
let ct: CalendarTime = in_calendar(i, cal)
|
||||
let p: Float = cal_cycle_phase(ct)
|
||||
let s: String = float_to_str(p)
|
||||
if str_eq(s, "nan") {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
fn main() -> Void {
|
||||
println(int_to_str(run_test()))
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
// rhythm-cycle-30hr.el — rhythm_cycle_phase(0.5) under CycleCalendar(30.hours)
|
||||
// returns the next instant at the 15-hour mark of the cycle.
|
||||
|
||||
fn run_test() -> Int {
|
||||
let period: Duration = 30.hours
|
||||
let cal: Calendar = cycle_calendar(period)
|
||||
let r: Rhythm = rhythm_cycle_phase(0.5)
|
||||
let base: Instant = unix_seconds(0)
|
||||
let next: Instant = rhythm_next_after(r, base, cal)
|
||||
let elapsed_ns: Duration = next - base
|
||||
let elapsed_secs: Int = duration_to_seconds(elapsed_ns)
|
||||
// 15 hours = 54000 seconds.
|
||||
return elapsed_secs
|
||||
}
|
||||
|
||||
fn main() -> Void {
|
||||
println(int_to_str(run_test()))
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
// rhythm-grounding.el — Mondays at 9am, grounded against EarthCalendar(NYC),
|
||||
// from a Wednesday timestamp returns the next Monday at 9am EDT.
|
||||
// Wednesday 2026-05-06 00:00 UTC (1778025600) → next Monday 9am EDT
|
||||
// = 2026-05-11 09:00 EDT = 2026-05-11 13:00 UTC = 1778504400.
|
||||
|
||||
fn run_test() -> Int {
|
||||
let z: Zone = zone("America/New_York")
|
||||
let cal: Calendar = earth_calendar(z)
|
||||
let r: Rhythm = rhythm_weekly_at(1, 9, 0)
|
||||
let after: Instant = unix_seconds(1778025600)
|
||||
let next: Instant = rhythm_next_after(r, after, cal)
|
||||
let next_secs: Int = instant_to_unix_seconds(next)
|
||||
return next_secs
|
||||
}
|
||||
|
||||
fn main() -> Void {
|
||||
println(int_to_str(run_test()))
|
||||
}
|
||||
Executable
+107
@@ -0,0 +1,107 @@
|
||||
#!/usr/bin/env bash
|
||||
# run.sh — build and execute the calendar/ acceptance corpus.
|
||||
#
|
||||
# Each examples/<case>.el is a self-contained El program with a fn main()
|
||||
# that prints a single deterministic result line. The runner compiles each
|
||||
# via the canonical native elc, links it against the shared C runtime, runs
|
||||
# it, and asserts the output matches the expected value.
|
||||
|
||||
set -uo pipefail
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
EL_HOME="${EL_HOME:-$(cd ../.. && pwd)}"
|
||||
ELC="${EL_HOME}/dist/platform/elc"
|
||||
RUNTIME_DIR="${EL_HOME}/el-compiler/runtime"
|
||||
|
||||
if [ ! -x "${ELC}" ]; then
|
||||
echo "elc not found at ${ELC}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
PASS=0
|
||||
FAIL=0
|
||||
FAILED_NAMES=()
|
||||
|
||||
# run_runtime_case <name> <source> <expected> [<extra>]
|
||||
run_runtime_case() {
|
||||
local name="$1"
|
||||
local src="$2"
|
||||
local expected="$3"
|
||||
local mode="${4:-exact}"
|
||||
|
||||
local out_c
|
||||
local out_bin
|
||||
out_c="$(mktemp -t cal_test.XXXXXX).c"
|
||||
out_bin="$(mktemp -t cal_test.XXXXXX)"
|
||||
|
||||
if ! "${ELC}" "${src}" > "${out_c}" 2>/tmp/cal_test.elc.err; then
|
||||
echo "FAIL ${name} — elc emit failed:"
|
||||
cat /tmp/cal_test.elc.err | sed 's/^/ /'
|
||||
FAIL=$((FAIL+1))
|
||||
FAILED_NAMES+=("${name}")
|
||||
rm -f "${out_c}" "${out_bin}"
|
||||
return
|
||||
fi
|
||||
|
||||
if ! cc -O2 -I "${RUNTIME_DIR}" "${out_c}" "${RUNTIME_DIR}/el_runtime.c" \
|
||||
-lcurl -lpthread -o "${out_bin}" 2>/tmp/cal_test.cc.err; then
|
||||
echo "FAIL ${name} — cc failed:"
|
||||
cat /tmp/cal_test.cc.err | sed 's/^/ /'
|
||||
FAIL=$((FAIL+1))
|
||||
FAILED_NAMES+=("${name}")
|
||||
rm -f "${out_c}" "${out_bin}"
|
||||
return
|
||||
fi
|
||||
|
||||
local got
|
||||
got="$("${out_bin}" 2>&1 | tr -d '[:space:]')"
|
||||
|
||||
if [ "${mode}" = "either" ]; then
|
||||
local ok=0
|
||||
local IFS=','
|
||||
for choice in ${expected}; do
|
||||
if [ "${got}" = "${choice}" ]; then ok=1; break; fi
|
||||
done
|
||||
if [ "${ok}" = "1" ]; then
|
||||
echo "PASS ${name} (got: ${got})"
|
||||
PASS=$((PASS+1))
|
||||
else
|
||||
echo "FAIL ${name} expected one of {${expected}}, got: ${got}"
|
||||
FAIL=$((FAIL+1))
|
||||
FAILED_NAMES+=("${name}")
|
||||
fi
|
||||
else
|
||||
if [ "${got}" = "${expected}" ]; then
|
||||
echo "PASS ${name}"
|
||||
PASS=$((PASS+1))
|
||||
else
|
||||
echo "FAIL ${name} expected: ${expected}, got: ${got}"
|
||||
FAIL=$((FAIL+1))
|
||||
FAILED_NAMES+=("${name}")
|
||||
fi
|
||||
fi
|
||||
|
||||
rm -f "${out_c}" "${out_bin}"
|
||||
}
|
||||
|
||||
echo "==> Running calendar/ acceptance corpus"
|
||||
echo
|
||||
|
||||
run_runtime_case "earth-zone" examples/earth-zone.el "EDT"
|
||||
run_runtime_case "dst-spring-forward" examples/dst-spring-forward.el "03:30EDT"
|
||||
run_runtime_case "mars-calendar" examples/mars-calendar.el "1"
|
||||
run_runtime_case "cycle-30hr" examples/cycle-30hr.el "0.5"
|
||||
run_runtime_case "cycle-300yr" examples/cycle-300yr.el "0.01"
|
||||
run_runtime_case "no-cycle" examples/no-cycle.el "1"
|
||||
run_runtime_case "cross-calendar-comparison" examples/cross-calendar-comparison.el "1"
|
||||
run_runtime_case "local-date-arithmetic" examples/local-date-arithmetic.el "2026-6-4"
|
||||
run_runtime_case "rhythm-grounding" examples/rhythm-grounding.el "1778504400"
|
||||
run_runtime_case "rhythm-cycle-30hr" examples/rhythm-cycle-30hr.el "54000"
|
||||
|
||||
echo
|
||||
echo "${PASS} passed, ${FAIL} failed"
|
||||
if [ "${FAIL}" -gt 0 ]; then
|
||||
echo "failed: ${FAILED_NAMES[*]}"
|
||||
exit 1
|
||||
fi
|
||||
exit 0
|
||||
@@ -0,0 +1,13 @@
|
||||
// runner.el — entry point for the calendar/ acceptance corpus.
|
||||
//
|
||||
// Each calendar/examples/*.el is its own El program with its own fn main().
|
||||
// Compile, link, and run-output-diff is handled by tests/calendar/run.sh —
|
||||
// this file is the El-side stub kept for pattern parity.
|
||||
//
|
||||
// Run from the calendar/ directory:
|
||||
// ./run.sh
|
||||
|
||||
fn main() -> Void {
|
||||
println("calendar/ acceptance corpus is driven by run.sh — invoke that directly.")
|
||||
println("Each examples/*.el is a self-contained program; runner.el is a parity stub.")
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
{
|
||||
"allowlist": "{\"p\":[],\"br\":[],\"strong\":[],\"em\":[],\"u\":[],\"s\":[],\"code\":[],\"pre\":[],\"ul\":[],\"ol\":[],\"li\":[],\"h1\":[],\"h2\":[],\"h3\":[],\"h4\":[],\"blockquote\":[],\"a\":[\"href\",\"title\"]}",
|
||||
"cases": [
|
||||
{
|
||||
"name": "01-pass-through-list",
|
||||
"input": "<ol><li><strong>x</strong></li></ol>",
|
||||
"expected": "<ol><li><strong>x</strong></li></ol>"
|
||||
},
|
||||
{
|
||||
"name": "02-paragraphs-with-bold",
|
||||
"input": "<p>hello <strong>world</strong></p><p>second</p>",
|
||||
"expected": "<p>hello <strong>world</strong></p><p>second</p>"
|
||||
},
|
||||
{
|
||||
"name": "03-pre-code-block",
|
||||
"input": "<pre><code>npm install</code></pre>",
|
||||
"expected": "<pre><code>npm install</code></pre>"
|
||||
},
|
||||
{
|
||||
"name": "04-allowed-https-link",
|
||||
"input": "<a href=\"https://example.com\">click</a>",
|
||||
"expected": "<a href=\"https://example.com\">click</a>"
|
||||
},
|
||||
{
|
||||
"name": "05-allowed-anchor-link",
|
||||
"input": "<a href=\"#section\">jump</a>",
|
||||
"expected": "<a href=\"#section\">jump</a>"
|
||||
},
|
||||
{
|
||||
"name": "06-javascript-scheme-blocked",
|
||||
"input": "<a href=\"javascript:alert(1)\">click</a>",
|
||||
"expected": "<a>click</a>"
|
||||
},
|
||||
{
|
||||
"name": "07-about-scheme-blocked",
|
||||
"input": "<a href=\"about:blank#x\">click</a>",
|
||||
"expected": "<a>click</a>"
|
||||
},
|
||||
{
|
||||
"name": "08-data-uri-blocked",
|
||||
"input": "<a href=\"data:text/html,<script>alert(1)</script>\">x</a>",
|
||||
"expected": "<a>x</a>"
|
||||
},
|
||||
{
|
||||
"name": "09-script-content-dropped",
|
||||
"input": "before<script>alert(1)</script>after",
|
||||
"expected": "beforeafter"
|
||||
},
|
||||
{
|
||||
"name": "10-iframe-content-dropped",
|
||||
"input": "<iframe src=\"evil\">junk</iframe>safe",
|
||||
"expected": "safe"
|
||||
},
|
||||
{
|
||||
"name": "11-form-content-dropped",
|
||||
"input": "<form action=\"/steal\">x</form>safe",
|
||||
"expected": "safe"
|
||||
},
|
||||
{
|
||||
"name": "12-img-with-onerror-dropped",
|
||||
"input": "<img src=x onerror=alert(2)>",
|
||||
"expected": ""
|
||||
},
|
||||
{
|
||||
"name": "13-comment-injection-bypass-blocked",
|
||||
"input": "<form action=\"-->junk\">x</form>safe",
|
||||
"expected": "safe"
|
||||
},
|
||||
{
|
||||
"name": "14-mixed-legit-and-attack",
|
||||
"input": "<p>hello</p><script>alert(1)</script><p>world</p>",
|
||||
"expected": "<p>hello</p><p>world</p>"
|
||||
},
|
||||
{
|
||||
"name": "15-pre-encoded-entities-preserved",
|
||||
"input": "<script>alert(1)</script>",
|
||||
"expected": "<script>alert(1)</script>"
|
||||
},
|
||||
{
|
||||
"name": "16-unicode-in-href-preserved",
|
||||
"input": "<a href=\"https://example.com/?q=日本語\">x</a>",
|
||||
"expected": "<a href=\"https://example.com/?q=日本語\">x</a>"
|
||||
},
|
||||
{
|
||||
"name": "17-unclosed-tag-passes-through",
|
||||
"input": "<p>unclosed",
|
||||
"expected": "<p>unclosed"
|
||||
},
|
||||
{
|
||||
"name": "18-onclick-attribute-stripped-tag-survives",
|
||||
"input": "<p onclick=\"x\">hi</p>",
|
||||
"expected": "<p>hi</p>"
|
||||
},
|
||||
{
|
||||
"name": "19-tab-bypass-in-scheme-blocked",
|
||||
"input": "<a href=\"java\tscript:alert(1)\">x</a>",
|
||||
"expected": "<a>x</a>"
|
||||
},
|
||||
{
|
||||
"name": "20-uppercase-tag-and-attr-normalised",
|
||||
"input": "<A HREF=\"https://example.com\">x</A>",
|
||||
"expected": "<a href=\"https://example.com\">x</a>"
|
||||
},
|
||||
{
|
||||
"name": "21-style-content-dropped",
|
||||
"input": "<style>body{display:none}</style>visible",
|
||||
"expected": "visible"
|
||||
},
|
||||
{
|
||||
"name": "22-object-content-dropped",
|
||||
"input": "<object data=\"x.swf\">flash</object>safe",
|
||||
"expected": "safe"
|
||||
},
|
||||
{
|
||||
"name": "23-svg-onload-dropped",
|
||||
"input": "<svg onload=\"alert(1)\"><circle r=\"5\"/></svg>safe",
|
||||
"expected": "safe"
|
||||
},
|
||||
{
|
||||
"name": "24-blockquote-passthrough",
|
||||
"input": "<blockquote>quoted text</blockquote>",
|
||||
"expected": "<blockquote>quoted text</blockquote>"
|
||||
},
|
||||
{
|
||||
"name": "25-headings-passthrough",
|
||||
"input": "<h1>title</h1><h2>section</h2>",
|
||||
"expected": "<h1>title</h1><h2>section</h2>"
|
||||
},
|
||||
{
|
||||
"name": "26-attribute-value-with-gt-byte",
|
||||
"input": "<a href=\"https://example.com/?q=1>2\" title=\"a > b\">x</a>",
|
||||
"expected": "<a href=\"https://example.com/?q=1>2\" title=\"a > b\">x</a>"
|
||||
},
|
||||
{
|
||||
"name": "27-nested-script-after-text",
|
||||
"input": "lead<p>para</p><script>alert(1)</script>tail",
|
||||
"expected": "lead<p>para</p>tail"
|
||||
},
|
||||
{
|
||||
"name": "28-empty-input",
|
||||
"input": "",
|
||||
"expected": ""
|
||||
},
|
||||
{
|
||||
"name": "29-plain-text",
|
||||
"input": "just text, no tags",
|
||||
"expected": "just text, no tags"
|
||||
}
|
||||
]
|
||||
}
|
||||
Executable
+32
@@ -0,0 +1,32 @@
|
||||
#!/usr/bin/env bash
|
||||
# run.sh — build and execute the html_sanitizer acceptance corpus.
|
||||
#
|
||||
# Compiles tests/html_sanitizer/runner.el via the canonical native elc,
|
||||
# links against the shared C runtime, then runs the binary against
|
||||
# cases.json. Exits non-zero on any FAIL.
|
||||
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
EL_HOME="${EL_HOME:-$(cd ../.. && pwd)}"
|
||||
ELC="${EL_HOME}/dist/platform/elc"
|
||||
RUNTIME_DIR="${EL_HOME}/el-compiler/runtime"
|
||||
|
||||
if [ ! -x "${ELC}" ]; then
|
||||
echo "elc not found at ${ELC}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
OUT_C="$(mktemp -t html_sanitizer.XXXXXX).c"
|
||||
OUT_BIN="$(mktemp -t html_sanitizer.XXXXXX)"
|
||||
trap 'rm -f "${OUT_C}" "${OUT_BIN}"' EXIT
|
||||
|
||||
echo "==> Compiling runner.el via ${ELC}"
|
||||
"${ELC}" runner.el > "${OUT_C}"
|
||||
|
||||
echo "==> Linking against ${RUNTIME_DIR}/el_runtime.c"
|
||||
cc -O2 -I "${RUNTIME_DIR}" "${OUT_C}" "${RUNTIME_DIR}/el_runtime.c" \
|
||||
-lcurl -lpthread -o "${OUT_BIN}"
|
||||
|
||||
echo "==> Running"
|
||||
"${OUT_BIN}"
|
||||
@@ -0,0 +1,59 @@
|
||||
// runner.el — acceptance tests for el_html_sanitize.
|
||||
//
|
||||
// Reads cases.json from the cwd, runs every case through
|
||||
// el_html_sanitize, prints PASS/FAIL per case and a summary,
|
||||
// and exits non-zero if any case failed.
|
||||
//
|
||||
// cases.json shape:
|
||||
// {
|
||||
// "allowlist": "<json string passed to el_html_sanitize>",
|
||||
// "cases": [
|
||||
// { "name": "...", "input": "...", "expected": "..." },
|
||||
// ...
|
||||
// ]
|
||||
// }
|
||||
|
||||
fn run_case(name: String, input: String, expected: String, allowlist: String, idx: Int) -> Int {
|
||||
let got: String = el_html_sanitize(input, allowlist)
|
||||
if str_eq(got, expected) {
|
||||
println("PASS " + name)
|
||||
return 1
|
||||
} else {
|
||||
println("FAIL " + name)
|
||||
println(" input: " + input)
|
||||
println(" expected: " + expected)
|
||||
println(" got: " + got)
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> Int {
|
||||
let raw: String = fs_read("cases.json")
|
||||
if str_eq(raw, "") {
|
||||
println("FATAL could not read cases.json from cwd")
|
||||
return 2
|
||||
}
|
||||
let allowlist: String = json_get(raw, "allowlist")
|
||||
let cases_raw: String = json_get_raw(raw, "cases")
|
||||
let n: Int = json_array_len(cases_raw)
|
||||
let i: Int = 0
|
||||
let pass: Int = 0
|
||||
let fail: Int = 0
|
||||
while i < n {
|
||||
let case_obj: String = json_array_get(cases_raw, i)
|
||||
let cname: String = json_get(case_obj, "name")
|
||||
let cinp: String = json_get(case_obj, "input")
|
||||
let cexp: String = json_get(case_obj, "expected")
|
||||
let r: Int = run_case(cname, cinp, cexp, allowlist, i)
|
||||
if r == 1 {
|
||||
let pass = pass + 1
|
||||
} else {
|
||||
let fail = fail + 1
|
||||
}
|
||||
let i = i + 1
|
||||
}
|
||||
println("")
|
||||
println(int_to_str(pass) + " passed, " + int_to_str(fail) + " failed")
|
||||
if fail > 0 { return 1 }
|
||||
return 0
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
// test_codegen_js.el - basic tests for JS codegen features.
|
||||
//
|
||||
// These tests verify that core El language features produce correct values
|
||||
// when compiled and executed via the C backend. They serve as a
|
||||
// regression baseline for the codegen pipeline.
|
||||
|
||||
test "arithmetic" {
|
||||
let x: Int = 2 + 3
|
||||
assert x == 5, "addition"
|
||||
let y: Int = 10 - 4
|
||||
assert y == 6, "subtraction"
|
||||
let z: Int = 3 * 4
|
||||
assert z == 12, "multiplication"
|
||||
let w: Int = 15 / 3
|
||||
assert w == 5, "division"
|
||||
}
|
||||
|
||||
test "string-concat" {
|
||||
let a: String = "hello"
|
||||
let b: String = " world"
|
||||
let c: String = a + b
|
||||
assert c == "hello world", "string concatenation"
|
||||
}
|
||||
|
||||
test "str-len" {
|
||||
let n: Int = str_len("hello")
|
||||
assert n == 5, "str_len hello"
|
||||
let m: Int = str_len("")
|
||||
assert m == 0, "str_len empty"
|
||||
}
|
||||
|
||||
test "bool-logic" {
|
||||
let t = true
|
||||
let f = false
|
||||
assert t, "true is truthy"
|
||||
assert !f, "false negated is truthy"
|
||||
assert t && !f, "true and not false"
|
||||
assert t || f, "true or false"
|
||||
}
|
||||
|
||||
test "list-operations" {
|
||||
let lst: [Int] = native_list_empty()
|
||||
let lst = native_list_append(lst, 10)
|
||||
let lst = native_list_append(lst, 20)
|
||||
let lst = native_list_append(lst, 30)
|
||||
let n: Int = native_list_len(lst)
|
||||
assert n == 3, "list length 3"
|
||||
let v0: Int = native_list_get(lst, 0)
|
||||
let v1: Int = native_list_get(lst, 1)
|
||||
let v2: Int = native_list_get(lst, 2)
|
||||
assert v0 == 10, "first element"
|
||||
assert v1 == 20, "second element"
|
||||
assert v2 == 30, "third element"
|
||||
}
|
||||
|
||||
test "str-slice" {
|
||||
let s: String = str_slice("hello world", 6, 11)
|
||||
assert s == "world", "slice from 6 to 11"
|
||||
}
|
||||
|
||||
test "str-contains" {
|
||||
assert str_contains("hello world", "world"), "contains world"
|
||||
assert !str_contains("hello world", "xyz"), "does not contain xyz"
|
||||
}
|
||||
|
||||
test "int-to-str" {
|
||||
let s: String = int_to_str(42)
|
||||
assert s == "42", "int to string"
|
||||
}
|
||||
|
||||
test "str-to-int" {
|
||||
let n: Int = str_to_int("123")
|
||||
assert n == 123, "string to int"
|
||||
}
|
||||
|
||||
test "str-starts-ends" {
|
||||
assert str_starts_with("hello world", "hello"), "starts with hello"
|
||||
assert str_ends_with("hello world", "world"), "ends with world"
|
||||
assert !str_starts_with("hello world", "world"), "does not start with world"
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
// test_env.el - native test suite for runtime/env.el
|
||||
//
|
||||
// Covers: env() for reading environment variables, args() returning a list,
|
||||
// state_set/get/del/keys via the env module re-exports, uuid_new/uuid_v4
|
||||
// format validation.
|
||||
|
||||
test "env-missing-returns-empty" {
|
||||
let v: String = env("__EL_NO_SUCH_VAR_XYZ__")
|
||||
assert v == "", "missing env var returns empty string"
|
||||
}
|
||||
|
||||
test "env-path-is-set" {
|
||||
// PATH is expected to be set in virtually any UNIX environment.
|
||||
let v: String = env("PATH")
|
||||
assert str_len(v) > 0, "PATH env var is non-empty"
|
||||
}
|
||||
|
||||
test "env-home-is-set" {
|
||||
// HOME is present on macOS/Linux test environments.
|
||||
let v: String = env("HOME")
|
||||
assert str_len(v) > 0, "HOME env var is non-empty"
|
||||
}
|
||||
|
||||
test "args-returns-list" {
|
||||
let a: [String] = args()
|
||||
// Even with no arguments, the list should be non-null (at minimum the
|
||||
// program name is argv[0]).
|
||||
let n: Int = native_list_len(a)
|
||||
assert n >= 0, "args returns a list (may be empty if runtime strips argv)"
|
||||
}
|
||||
|
||||
test "uuid-new-format" {
|
||||
let id: String = uuid_new()
|
||||
// UUID v4: xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx (36 chars)
|
||||
let n: Int = str_len(id)
|
||||
assert n == 36, "uuid_new returns 36-character string"
|
||||
// Check the dashes at correct positions
|
||||
let d1: String = str_char_at(id, 8)
|
||||
let d2: String = str_char_at(id, 13)
|
||||
let d3: String = str_char_at(id, 18)
|
||||
let d4: String = str_char_at(id, 23)
|
||||
assert d1 == "-", "dash at position 8"
|
||||
assert d2 == "-", "dash at position 13"
|
||||
assert d3 == "-", "dash at position 18"
|
||||
assert d4 == "-", "dash at position 23"
|
||||
}
|
||||
|
||||
test "uuid-v4-format" {
|
||||
let id: String = uuid_v4()
|
||||
let n: Int = str_len(id)
|
||||
assert n == 36, "uuid_v4 returns 36-character string"
|
||||
// The version nibble must be '4'
|
||||
let version_char: String = str_char_at(id, 14)
|
||||
assert version_char == "4", "uuid_v4 version nibble is '4'"
|
||||
}
|
||||
|
||||
test "uuid-uniqueness" {
|
||||
let id1: String = uuid_new()
|
||||
let id2: String = uuid_new()
|
||||
assert !str_eq(id1, id2), "two uuid_new calls produce different UUIDs"
|
||||
}
|
||||
|
||||
test "env-state-set-get-via-env-module" {
|
||||
// runtime/env.el re-exports state_set / state_get.
|
||||
state_set("env_test_key", "env_test_val")
|
||||
let v: String = state_get("env_test_key")
|
||||
assert v == "env_test_val", "state_set/get work via env module"
|
||||
state_del("env_test_key")
|
||||
let after: String = state_get("env_test_key")
|
||||
assert after == "", "state_del removes key"
|
||||
}
|
||||
|
||||
test "env-state-keys-json-via-env-module" {
|
||||
state_set("esk_a", "1")
|
||||
state_set("esk_b", "2")
|
||||
let ks: String = state_keys()
|
||||
assert str_starts_with(ks, "["), "state_keys returns JSON array"
|
||||
assert str_contains(ks, "esk_a"), "esk_a in keys"
|
||||
assert str_contains(ks, "esk_b"), "esk_b in keys"
|
||||
state_del("esk_a")
|
||||
state_del("esk_b")
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
// test_fs.el - native test suite for runtime/fs.el
|
||||
//
|
||||
// Covers: fs_write/read round-trip, fs_exists, fs_mkdir, fs_list,
|
||||
// fs_list_json, and edge cases (empty file, overwrite, non-existent path).
|
||||
|
||||
test "fs-write-and-read" {
|
||||
let path: String = "/tmp/el_test_fs_basic.txt"
|
||||
let content: String = "hello from El"
|
||||
let ok: Bool = fs_write(path, content)
|
||||
assert ok, "fs_write returns true on success"
|
||||
let got: String = fs_read(path)
|
||||
assert got == content, "fs_read returns what was written"
|
||||
}
|
||||
|
||||
test "fs-exists" {
|
||||
let path: String = "/tmp/el_test_fs_exists.txt"
|
||||
fs_write(path, "exists check")
|
||||
let exists: Bool = fs_exists(path)
|
||||
assert exists, "fs_exists returns true for created file"
|
||||
let missing: Bool = fs_exists("/tmp/__el_no_such_file_xyz__.txt")
|
||||
assert !missing, "fs_exists returns false for non-existent file"
|
||||
}
|
||||
|
||||
test "fs-read-nonexistent" {
|
||||
let got: String = fs_read("/tmp/__el_no_such_file_abc__.txt")
|
||||
assert got == "", "reading nonexistent file returns empty string"
|
||||
}
|
||||
|
||||
test "fs-write-overwrite" {
|
||||
let path: String = "/tmp/el_test_fs_overwrite.txt"
|
||||
fs_write(path, "original content")
|
||||
fs_write(path, "new content")
|
||||
let got: String = fs_read(path)
|
||||
assert got == "new content", "second write overwrites first"
|
||||
}
|
||||
|
||||
test "fs-write-empty-file" {
|
||||
let path: String = "/tmp/el_test_fs_empty.txt"
|
||||
let ok: Bool = fs_write(path, "")
|
||||
assert ok, "writing empty file succeeds"
|
||||
let got: String = fs_read(path)
|
||||
assert got == "", "reading empty file returns empty string"
|
||||
let exists: Bool = fs_exists(path)
|
||||
assert exists, "empty file still exists"
|
||||
}
|
||||
|
||||
test "fs-write-multiline" {
|
||||
let path: String = "/tmp/el_test_fs_multiline.txt"
|
||||
let content: String = "line one\nline two\nline three"
|
||||
fs_write(path, content)
|
||||
let got: String = fs_read(path)
|
||||
assert got == content, "multiline content round-trips"
|
||||
let lines: [String] = str_split_lines(got)
|
||||
let n: Int = native_list_len(lines)
|
||||
assert n == 3, "three lines after read"
|
||||
}
|
||||
|
||||
test "fs-mkdir" {
|
||||
let dir: String = "/tmp/el_test_fs_mkdir_dir"
|
||||
let ok: Bool = fs_mkdir(dir)
|
||||
assert ok, "fs_mkdir returns true"
|
||||
let exists: Bool = fs_exists(dir)
|
||||
assert exists, "created directory exists"
|
||||
}
|
||||
|
||||
test "fs-list" {
|
||||
let dir: String = "/tmp/el_test_fs_list_dir"
|
||||
fs_mkdir(dir)
|
||||
fs_write(dir + "/a.txt", "a")
|
||||
fs_write(dir + "/b.txt", "b")
|
||||
let files: [String] = fs_list(dir)
|
||||
// Filter out empty strings from trailing newline
|
||||
let n: Int = native_list_len(files)
|
||||
// We expect at least 2 entries (a.txt and b.txt)
|
||||
assert n >= 2, "fs_list returns at least 2 entries"
|
||||
}
|
||||
|
||||
test "fs-list-json" {
|
||||
let dir: String = "/tmp/el_test_fs_listjson_dir"
|
||||
fs_mkdir(dir)
|
||||
fs_write(dir + "/x.txt", "x")
|
||||
fs_write(dir + "/y.txt", "y")
|
||||
let result: String = fs_list_json(dir)
|
||||
assert str_starts_with(result, "["), "fs_list_json returns JSON array"
|
||||
assert str_ends_with(result, "]"), "fs_list_json JSON array is closed"
|
||||
// Should contain at least one filename
|
||||
assert str_len(result) > 2, "fs_list_json result is non-empty array"
|
||||
}
|
||||
|
||||
test "fs-write-and-read-special-chars" {
|
||||
let path: String = "/tmp/el_test_fs_special.txt"
|
||||
let content: String = "tabs:\there\nnewlines: ok\n\"quoted\""
|
||||
fs_write(path, content)
|
||||
let got: String = fs_read(path)
|
||||
assert got == content, "special chars in content round-trip"
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
// test_json.el - native test suite for runtime/json.el
|
||||
//
|
||||
// Covers: json_get (dot-path), typed extractors (int, bool, float),
|
||||
// json_array_len/get, json_set, json_build_object, json_build_array,
|
||||
// and json_escape_string.
|
||||
|
||||
test "json-get-string" {
|
||||
let obj: String = "{\"name\":\"alice\",\"role\":\"admin\"}"
|
||||
let name: String = json_get(obj, "name")
|
||||
assert name == "alice", "get string field name"
|
||||
let role: String = json_get(obj, "role")
|
||||
assert role == "admin", "get string field role"
|
||||
let missing: String = json_get(obj, "xyz")
|
||||
assert missing == "", "missing key returns empty string"
|
||||
}
|
||||
|
||||
test "json-get-int" {
|
||||
let obj: String = "{\"count\":42,\"offset\":0}"
|
||||
let count: Int = json_get_int(obj, "count")
|
||||
assert count == 42, "get int field count"
|
||||
let offset: Int = json_get_int(obj, "offset")
|
||||
assert offset == 0, "get int field offset = 0"
|
||||
}
|
||||
|
||||
test "json-get-bool" {
|
||||
let obj: String = "{\"active\":true,\"deleted\":false}"
|
||||
let active: Bool = json_get_bool(obj, "active")
|
||||
assert active, "get bool true"
|
||||
let deleted: Bool = json_get_bool(obj, "deleted")
|
||||
assert !deleted, "get bool false"
|
||||
}
|
||||
|
||||
test "json-dot-path" {
|
||||
let obj: String = "{\"user\":{\"name\":\"bob\",\"age\":30}}"
|
||||
let name: String = json_get(obj, "user.name")
|
||||
assert name == "bob", "dot-path traversal user.name"
|
||||
let age: String = json_get(obj, "user.age")
|
||||
assert age == "30", "dot-path traversal user.age"
|
||||
}
|
||||
|
||||
test "json-array-len" {
|
||||
let arr: String = "[\"a\",\"b\",\"c\"]"
|
||||
let n: Int = json_array_len(arr)
|
||||
assert n == 3, "array length 3"
|
||||
let empty_arr: String = "[]"
|
||||
let m: Int = json_array_len(empty_arr)
|
||||
assert m == 0, "empty array length 0"
|
||||
}
|
||||
|
||||
test "json-array-get" {
|
||||
let arr: String = "[\"foo\",\"bar\",\"baz\"]"
|
||||
let first: String = json_array_get_string(arr, 0)
|
||||
assert first == "foo", "first element"
|
||||
let second: String = json_array_get_string(arr, 1)
|
||||
assert second == "bar", "second element"
|
||||
let third: String = json_array_get_string(arr, 2)
|
||||
assert third == "baz", "third element"
|
||||
}
|
||||
|
||||
test "json-get-raw" {
|
||||
let obj: String = "{\"items\":[1,2,3],\"meta\":{\"page\":1}}"
|
||||
let raw_arr: String = json_get_raw(obj, "items")
|
||||
let arr_len: Int = json_array_len(raw_arr)
|
||||
assert arr_len == 3, "raw array has 3 elements"
|
||||
let raw_meta: String = json_get_raw(obj, "meta")
|
||||
let page: String = json_get(raw_meta, "page")
|
||||
assert page == "1", "page from nested raw object"
|
||||
}
|
||||
|
||||
test "json-set" {
|
||||
let obj: String = "{\"name\":\"alice\"}"
|
||||
let updated: String = json_set(obj, "name", "\"bob\"")
|
||||
let name: String = json_get(updated, "name")
|
||||
assert name == "bob", "set replaces existing key"
|
||||
}
|
||||
|
||||
test "json-build-object" {
|
||||
let kvs: [String] = native_list_empty()
|
||||
let kvs = native_list_append(kvs, "name")
|
||||
let kvs = native_list_append(kvs, "alice")
|
||||
let kvs = native_list_append(kvs, "role")
|
||||
let kvs = native_list_append(kvs, "admin")
|
||||
let obj: String = json_build_object(kvs)
|
||||
let name: String = json_get(obj, "name")
|
||||
let role: String = json_get(obj, "role")
|
||||
assert name == "alice", "built object name field"
|
||||
assert role == "admin", "built object role field"
|
||||
}
|
||||
|
||||
test "json-build-array" {
|
||||
let items: [String] = native_list_empty()
|
||||
let items = native_list_append(items, "\"alpha\"")
|
||||
let items = native_list_append(items, "\"beta\"")
|
||||
let items = native_list_append(items, "\"gamma\"")
|
||||
let arr: String = json_build_array(items)
|
||||
let n: Int = json_array_len(arr)
|
||||
assert n == 3, "built array has 3 elements"
|
||||
let first: String = json_array_get_string(arr, 0)
|
||||
assert first == "alpha", "first built element"
|
||||
let third: String = json_array_get_string(arr, 2)
|
||||
assert third == "gamma", "third built element"
|
||||
}
|
||||
|
||||
test "json-escape-string" {
|
||||
let escaped: String = json_escape_string("say \"hello\"")
|
||||
assert str_contains(escaped, "\\\""), "double quotes are escaped"
|
||||
let tab_esc: String = json_escape_string("a\tb")
|
||||
assert str_contains(tab_esc, "\\t"), "tabs are escaped"
|
||||
let plain: String = json_escape_string("no special chars")
|
||||
assert plain == "no special chars", "plain string unchanged"
|
||||
}
|
||||
|
||||
test "json-get-float" {
|
||||
let obj: String = "{\"price\":9.99,\"tax\":0.0}"
|
||||
let price: Float = json_get_float(obj, "price")
|
||||
let diff: Float = price - 9.99
|
||||
assert diff > -0.001, "price close to 9.99 low"
|
||||
assert diff < 0.001, "price close to 9.99 high"
|
||||
let tax: Float = json_get_float(obj, "tax")
|
||||
assert tax == 0.0, "tax is zero"
|
||||
}
|
||||
|
||||
test "json-nested-array-index" {
|
||||
let obj: String = "{\"tags\":[\"go\",\"el\",\"rust\"]}"
|
||||
let tags_raw: String = json_get_raw(obj, "tags")
|
||||
let count: Int = json_array_len(tags_raw)
|
||||
assert count == 3, "tags array has 3 elements"
|
||||
let first_tag: String = json_array_get_string(tags_raw, 0)
|
||||
assert first_tag == "go", "first tag is go"
|
||||
let last_tag: String = json_array_get_string(tags_raw, 2)
|
||||
assert last_tag == "rust", "last tag is rust"
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
// test_math.el - native test suite for runtime/math.el
|
||||
//
|
||||
// Covers: integer math (abs, max, min), float math (sqrt, log, sin, cos, pi),
|
||||
// float conversions, format_float, and decimal_round.
|
||||
|
||||
test "el-abs" {
|
||||
let pos: Int = el_abs(5)
|
||||
assert pos == 5, "abs of positive"
|
||||
let neg: Int = el_abs(-5)
|
||||
assert neg == 5, "abs of negative"
|
||||
let zero: Int = el_abs(0)
|
||||
assert zero == 0, "abs of zero"
|
||||
let large: Int = el_abs(-1000000)
|
||||
assert large == 1000000, "abs of large negative"
|
||||
}
|
||||
|
||||
test "el-max" {
|
||||
let m: Int = el_max(3, 7)
|
||||
assert m == 7, "max of 3 and 7"
|
||||
let m2: Int = el_max(7, 3)
|
||||
assert m2 == 7, "max is commutative"
|
||||
let same: Int = el_max(5, 5)
|
||||
assert same == 5, "max of equal values"
|
||||
let neg: Int = el_max(-3, -7)
|
||||
assert neg == -3, "max of two negatives"
|
||||
}
|
||||
|
||||
test "el-min" {
|
||||
let m: Int = el_min(3, 7)
|
||||
assert m == 3, "min of 3 and 7"
|
||||
let m2: Int = el_min(7, 3)
|
||||
assert m2 == 3, "min is commutative"
|
||||
let same: Int = el_min(5, 5)
|
||||
assert same == 5, "min of equal values"
|
||||
let neg: Int = el_min(-3, -7)
|
||||
assert neg == -7, "min of two negatives"
|
||||
}
|
||||
|
||||
test "math-pi" {
|
||||
let pi: Float = math_pi()
|
||||
// pi ~ 3.14159265358979
|
||||
// Check it's between 3.141 and 3.142
|
||||
let too_low: Float = pi - 3.141
|
||||
let too_high: Float = 3.142 - pi
|
||||
assert too_low > 0.0, "pi > 3.141"
|
||||
assert too_high > 0.0, "pi < 3.142"
|
||||
}
|
||||
|
||||
test "math-sqrt" {
|
||||
let r4: Float = math_sqrt(4.0)
|
||||
let diff4: Float = r4 - 2.0
|
||||
assert diff4 == 0.0, "sqrt(4) == 2.0"
|
||||
let r9: Float = math_sqrt(9.0)
|
||||
let diff9: Float = r9 - 3.0
|
||||
assert diff9 == 0.0, "sqrt(9) == 3.0"
|
||||
let r1: Float = math_sqrt(1.0)
|
||||
let diff1: Float = r1 - 1.0
|
||||
assert diff1 == 0.0, "sqrt(1) == 1.0"
|
||||
let r0: Float = math_sqrt(0.0)
|
||||
assert r0 == 0.0, "sqrt(0) == 0.0"
|
||||
}
|
||||
|
||||
test "math-sin-cos" {
|
||||
// sin(0) == 0, cos(0) == 1
|
||||
let s0: Float = math_sin(0.0)
|
||||
assert s0 == 0.0, "sin(0) == 0.0"
|
||||
let c0: Float = math_cos(0.0)
|
||||
assert c0 == 1.0, "cos(0) == 1.0"
|
||||
// sin(pi/2) ~ 1.0, cos(pi/2) ~ 0.0
|
||||
let half_pi: Float = math_pi() / 2.0
|
||||
let s_half: Float = math_sin(half_pi)
|
||||
// Check within 0.000001 of 1.0
|
||||
let diff_s: Float = s_half - 1.0
|
||||
assert diff_s > -0.000001, "sin(pi/2) close to 1.0 low"
|
||||
assert diff_s < 0.000001, "sin(pi/2) close to 1.0 high"
|
||||
}
|
||||
|
||||
test "int-to-float-and-back" {
|
||||
let f: Float = int_to_float(42)
|
||||
let back: Int = float_to_int(f)
|
||||
assert back == 42, "42 round-trips int->float->int"
|
||||
let neg: Float = int_to_float(-7)
|
||||
let neg_back: Int = float_to_int(neg)
|
||||
assert neg_back == -7, "-7 round-trips"
|
||||
let zero: Float = int_to_float(0)
|
||||
let zero_back: Int = float_to_int(zero)
|
||||
assert zero_back == 0, "0 round-trips"
|
||||
}
|
||||
|
||||
test "float-to-int-truncates" {
|
||||
let t1: Int = float_to_int(3.9)
|
||||
assert t1 == 3, "3.9 truncates to 3"
|
||||
let t2: Int = float_to_int(3.1)
|
||||
assert t2 == 3, "3.1 truncates to 3"
|
||||
let t3: Int = float_to_int(-3.7)
|
||||
assert t3 == -3, "-3.7 truncates toward zero to -3"
|
||||
}
|
||||
|
||||
test "str-to-float-and-back" {
|
||||
let f: Float = str_to_float("3.14")
|
||||
// Check it's between 3.13 and 3.15
|
||||
let lo: Float = f - 3.13
|
||||
let hi: Float = 3.15 - f
|
||||
assert lo > 0.0, "3.14 parsed > 3.13"
|
||||
assert hi > 0.0, "3.14 parsed < 3.15"
|
||||
let zero: Float = str_to_float("0.0")
|
||||
assert zero == 0.0, "parse 0.0"
|
||||
}
|
||||
|
||||
test "format-float" {
|
||||
let s0: String = format_float(3.14159, 2)
|
||||
assert s0 == "3.14", "format to 2 decimals"
|
||||
let s1: String = format_float(1.0, 0)
|
||||
assert s1 == "1", "format to 0 decimals"
|
||||
let s2: String = format_float(0.0, 3)
|
||||
assert s2 == "0.000", "format zero to 3 decimals"
|
||||
let s3: String = format_float(-2.5, 1)
|
||||
assert s3 == "-2.5", "format negative to 1 decimal"
|
||||
}
|
||||
|
||||
test "decimal-round" {
|
||||
let r0: Float = decimal_round(2.5, 0)
|
||||
assert r0 == 3.0, "round 2.5 to 0 places"
|
||||
let r1: Float = decimal_round(2.45, 1)
|
||||
assert r1 == 2.5, "round 2.45 to 1 place"
|
||||
let neg: Float = decimal_round(-2.5, 0)
|
||||
assert neg == -3.0, "round -2.5 to 0 places (half-away-from-zero)"
|
||||
let exact: Float = decimal_round(1.0, 2)
|
||||
assert exact == 1.0, "rounding exact value unchanged"
|
||||
}
|
||||
|
||||
test "math-log" {
|
||||
// log10(100) == 2
|
||||
let l100: Float = math_log(100.0)
|
||||
let diff: Float = l100 - 2.0
|
||||
assert diff > -0.000001, "log10(100) close to 2 low"
|
||||
assert diff < 0.000001, "log10(100) close to 2 high"
|
||||
// log10(1) == 0
|
||||
let l1: Float = math_log(1.0)
|
||||
assert l1 == 0.0, "log10(1) == 0"
|
||||
}
|
||||
|
||||
test "math-ln" {
|
||||
// ln(1) == 0
|
||||
let l1: Float = math_ln(1.0)
|
||||
assert l1 == 0.0, "ln(1) == 0"
|
||||
// ln(e) ~ 1.0 — e ~ 2.71828
|
||||
let e: Float = 2.718281828
|
||||
let le: Float = math_ln(e)
|
||||
let diff: Float = le - 1.0
|
||||
assert diff > -0.000001, "ln(e) close to 1 low"
|
||||
assert diff < 0.000001, "ln(e) close to 1 high"
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
// test_state.el - native test suite for runtime/state.el
|
||||
//
|
||||
// Covers: state_set/get/del, state_has, state_get_or, state_keys,
|
||||
// and edge cases such as empty values, overwrite, and multiple keys.
|
||||
|
||||
test "state-set-get-basic" {
|
||||
state_set("test_key", "hello")
|
||||
let v: String = state_get("test_key")
|
||||
assert v == "hello", "get returns set value"
|
||||
}
|
||||
|
||||
test "state-get-missing" {
|
||||
let v: String = state_get("__nonexistent_key_xyz__")
|
||||
assert v == "", "missing key returns empty string"
|
||||
}
|
||||
|
||||
test "state-overwrite" {
|
||||
state_set("ow_key", "first")
|
||||
state_set("ow_key", "second")
|
||||
let v: String = state_get("ow_key")
|
||||
assert v == "second", "second write overwrites first"
|
||||
}
|
||||
|
||||
test "state-del" {
|
||||
state_set("del_key", "to be deleted")
|
||||
state_del("del_key")
|
||||
let v: String = state_get("del_key")
|
||||
assert v == "", "deleted key returns empty string"
|
||||
}
|
||||
|
||||
test "state-del-nonexistent" {
|
||||
// Should not panic or error on deleting a non-existent key.
|
||||
state_del("__never_set_key__")
|
||||
let v: String = state_get("__never_set_key__")
|
||||
assert v == "", "del of nonexistent key is a no-op"
|
||||
}
|
||||
|
||||
test "state-has" {
|
||||
state_set("has_key", "value")
|
||||
assert state_has("has_key"), "has returns true for set key"
|
||||
assert !state_has("__no_has_key__"), "has returns false for absent key"
|
||||
state_del("has_key")
|
||||
assert !state_has("has_key"), "has returns false after del"
|
||||
}
|
||||
|
||||
test "state-get-or" {
|
||||
state_set("gor_key", "actual")
|
||||
let v1: String = state_get_or("gor_key", "default")
|
||||
assert v1 == "actual", "get_or returns value when key set"
|
||||
let v2: String = state_get_or("__absent_gor_key__", "fallback")
|
||||
assert v2 == "fallback", "get_or returns default when key absent"
|
||||
}
|
||||
|
||||
test "state-multiple-keys" {
|
||||
state_set("mk_a", "alpha")
|
||||
state_set("mk_b", "beta")
|
||||
state_set("mk_c", "gamma")
|
||||
let a: String = state_get("mk_a")
|
||||
let b: String = state_get("mk_b")
|
||||
let c: String = state_get("mk_c")
|
||||
assert a == "alpha", "key a correct"
|
||||
assert b == "beta", "key b correct"
|
||||
assert c == "gamma", "key c correct"
|
||||
state_del("mk_a")
|
||||
state_del("mk_b")
|
||||
state_del("mk_c")
|
||||
}
|
||||
|
||||
test "state-keys-returns-json-array" {
|
||||
state_set("keys_test_1", "v1")
|
||||
state_set("keys_test_2", "v2")
|
||||
let ks: String = state_keys()
|
||||
// The result is a JSON array string like ["keys_test_1","keys_test_2",...]
|
||||
assert str_starts_with(ks, "["), "state_keys returns JSON array"
|
||||
assert str_ends_with(ks, "]"), "state_keys JSON array is closed"
|
||||
assert str_contains(ks, "keys_test_1"), "keys array contains keys_test_1"
|
||||
assert str_contains(ks, "keys_test_2"), "keys array contains keys_test_2"
|
||||
state_del("keys_test_1")
|
||||
state_del("keys_test_2")
|
||||
}
|
||||
|
||||
test "state-numeric-value-as-string" {
|
||||
state_set("num_key", "42")
|
||||
let v: String = state_get("num_key")
|
||||
let n: Int = str_to_int(v)
|
||||
assert n == 42, "stored numeric string round-trips to int"
|
||||
state_del("num_key")
|
||||
}
|
||||
|
||||
test "state-long-value" {
|
||||
let long_val: String = str_repeat("abcdefghij", 100)
|
||||
state_set("long_val_key", long_val)
|
||||
let got: String = state_get("long_val_key")
|
||||
assert got == long_val, "long value round-trips correctly"
|
||||
let got_len: Int = str_len(got)
|
||||
assert got_len == 1000, "long value is 1000 bytes"
|
||||
state_del("long_val_key")
|
||||
}
|
||||
@@ -0,0 +1,272 @@
|
||||
// test_string.el - native test suite for runtime/string.el
|
||||
//
|
||||
// Covers: type conversions, core primitives, comparison and search,
|
||||
// case conversion, whitespace trimming, replacement, repetition,
|
||||
// reversal, prefix/suffix stripping, padding, counting, character
|
||||
// classification, splitting, and joining.
|
||||
|
||||
test "int-to-str" {
|
||||
let s: String = int_to_str(42)
|
||||
assert s == "42", "int 42 to string"
|
||||
let neg: String = int_to_str(-7)
|
||||
assert neg == "-7", "negative int to string"
|
||||
let zero: String = int_to_str(0)
|
||||
assert zero == "0", "zero to string"
|
||||
}
|
||||
|
||||
test "str-to-int" {
|
||||
let n: Int = str_to_int("123")
|
||||
assert n == 123, "parse 123"
|
||||
let neg: Int = str_to_int("-5")
|
||||
assert neg == -5, "parse negative"
|
||||
let zero: Int = str_to_int("0")
|
||||
assert zero == 0, "parse zero"
|
||||
}
|
||||
|
||||
test "bool-to-str" {
|
||||
let t: String = bool_to_str(true)
|
||||
assert t == "true", "true to string"
|
||||
let f: String = bool_to_str(false)
|
||||
assert f == "false", "false to string"
|
||||
}
|
||||
|
||||
test "str-len" {
|
||||
let n: Int = str_len("hello")
|
||||
assert n == 5, "length of hello"
|
||||
let e: Int = str_len("")
|
||||
assert e == 0, "length of empty string"
|
||||
let space: Int = str_len("a b")
|
||||
assert space == 3, "length with spaces"
|
||||
}
|
||||
|
||||
test "str-eq" {
|
||||
assert str_eq("abc", "abc"), "identical strings are equal"
|
||||
assert !str_eq("abc", "ABC"), "case-sensitive comparison"
|
||||
assert str_eq("", ""), "empty strings are equal"
|
||||
assert !str_eq("a", "b"), "different single chars"
|
||||
}
|
||||
|
||||
test "str-slice" {
|
||||
let s: String = str_slice("hello world", 6, 11)
|
||||
assert s == "world", "slice end of string"
|
||||
let start: String = str_slice("hello world", 0, 5)
|
||||
assert start == "hello", "slice start of string"
|
||||
let empty: String = str_slice("hello", 2, 2)
|
||||
assert empty == "", "zero-length slice"
|
||||
}
|
||||
|
||||
test "str-starts-ends-with" {
|
||||
assert str_starts_with("hello world", "hello"), "starts with hello"
|
||||
assert str_ends_with("hello world", "world"), "ends with world"
|
||||
assert !str_starts_with("hello world", "world"), "does not start with world"
|
||||
assert !str_ends_with("hello world", "hello"), "does not end with hello"
|
||||
assert str_starts_with("abc", ""), "empty prefix always matches"
|
||||
assert str_ends_with("abc", ""), "empty suffix always matches"
|
||||
}
|
||||
|
||||
test "str-contains" {
|
||||
assert str_contains("hello world", "world"), "contains world"
|
||||
assert str_contains("hello world", "lo wo"), "contains interior substring"
|
||||
assert !str_contains("hello world", "xyz"), "does not contain xyz"
|
||||
assert str_contains("abc", ""), "empty sub is always contained"
|
||||
assert !str_contains("", "x"), "empty string does not contain nonempty sub"
|
||||
}
|
||||
|
||||
test "str-index-of" {
|
||||
let i: Int = str_index_of("hello world", "world")
|
||||
assert i == 6, "index of world"
|
||||
let j: Int = str_index_of("hello world", "xyz")
|
||||
assert j == -1, "not found returns -1"
|
||||
let k: Int = str_index_of("aabbcc", "bb")
|
||||
assert k == 2, "index of bb in aabbcc"
|
||||
}
|
||||
|
||||
test "str-last-index-of" {
|
||||
let i: Int = str_last_index_of("abcabc", "bc")
|
||||
assert i == 4, "last occurrence of bc"
|
||||
let j: Int = str_last_index_of("hello", "xyz")
|
||||
assert j == -1, "not found returns -1"
|
||||
let k: Int = str_last_index_of("aaa", "a")
|
||||
assert k == 2, "last single-char match"
|
||||
}
|
||||
|
||||
test "str-to-upper-lower" {
|
||||
let up: String = str_to_upper("hello")
|
||||
assert up == "HELLO", "to upper"
|
||||
let lo: String = str_to_lower("WORLD")
|
||||
assert lo == "world", "to lower"
|
||||
let mixed: String = str_to_upper("Hello World")
|
||||
assert mixed == "HELLO WORLD", "mixed to upper"
|
||||
let empty: String = str_to_lower("")
|
||||
assert empty == "", "empty stays empty"
|
||||
}
|
||||
|
||||
test "str-trim" {
|
||||
let s: String = str_trim(" hello ")
|
||||
assert s == "hello", "trim both ends"
|
||||
let lonly: String = str_trim(" hello")
|
||||
assert lonly == "hello", "trim left only"
|
||||
let ronly: String = str_trim("hello ")
|
||||
assert ronly == "hello", "trim right only"
|
||||
let tabs: String = str_trim("\thello\n")
|
||||
assert tabs == "hello", "trim tabs and newlines"
|
||||
let empty: String = str_trim(" ")
|
||||
assert empty == "", "all whitespace trims to empty"
|
||||
}
|
||||
|
||||
test "str-replace" {
|
||||
let s: String = str_replace("hello world", "world", "there")
|
||||
assert s == "hello there", "replace word"
|
||||
let none: String = str_replace("hello", "xyz", "abc")
|
||||
assert none == "hello", "no match leaves string unchanged"
|
||||
let multi: String = str_replace("aaa", "a", "b")
|
||||
assert multi == "bbb", "replace all occurrences"
|
||||
let empty_from: String = str_replace("hello", "", "x")
|
||||
assert empty_from == "hello", "empty from returns original"
|
||||
}
|
||||
|
||||
test "str-repeat" {
|
||||
let s: String = str_repeat("ab", 3)
|
||||
assert s == "ababab", "repeat 3 times"
|
||||
let once: String = str_repeat("x", 1)
|
||||
assert once == "x", "repeat once"
|
||||
let zero: String = str_repeat("abc", 0)
|
||||
assert zero == "", "repeat zero times"
|
||||
let neg: String = str_repeat("abc", -1)
|
||||
assert neg == "", "negative repeat is empty"
|
||||
}
|
||||
|
||||
test "str-reverse" {
|
||||
let s: String = str_reverse("hello")
|
||||
assert s == "olleh", "reverse hello"
|
||||
let single: String = str_reverse("a")
|
||||
assert single == "a", "reverse single char"
|
||||
let empty: String = str_reverse("")
|
||||
assert empty == "", "reverse empty string"
|
||||
let palindrome: String = str_reverse("racecar")
|
||||
assert palindrome == "racecar", "reverse palindrome"
|
||||
}
|
||||
|
||||
test "str-strip-prefix-suffix" {
|
||||
let p: String = str_strip_prefix("foobar", "foo")
|
||||
assert p == "bar", "strip prefix foo"
|
||||
let no_prefix: String = str_strip_prefix("foobar", "baz")
|
||||
assert no_prefix == "foobar", "no match leaves string unchanged"
|
||||
let s: String = str_strip_suffix("hello.md", ".md")
|
||||
assert s == "hello", "strip suffix .md"
|
||||
let no_suffix: String = str_strip_suffix("hello.md", ".txt")
|
||||
assert no_suffix == "hello.md", "non-matching suffix unchanged"
|
||||
}
|
||||
|
||||
test "str-strip-chars" {
|
||||
let s: String = str_strip_chars(" \thello \n", " \t\n")
|
||||
assert s == "hello", "strip whitespace chars"
|
||||
let dots: String = str_strip_chars("...hello...", ".")
|
||||
assert dots == "hello", "strip dot chars"
|
||||
let all: String = str_strip_chars("aaa", "a")
|
||||
assert all == "", "strip all chars leaves empty"
|
||||
}
|
||||
|
||||
test "str-pad-left-right" {
|
||||
let l: String = str_pad_left("42", 5, "0")
|
||||
assert l == "00042", "zero-pad left to width 5"
|
||||
let r: String = str_pad_right("hi", 5, "-")
|
||||
assert r == "hi---", "dash-pad right to width 5"
|
||||
let no_pad: String = str_pad_left("hello", 3, "x")
|
||||
assert no_pad == "hello", "no pad when string already wide enough"
|
||||
}
|
||||
|
||||
test "str-count" {
|
||||
let n: Int = str_count("abc abc abc", "abc")
|
||||
assert n == 3, "count three occurrences"
|
||||
let overlap: Int = str_count("aaa", "aa")
|
||||
assert overlap == 1, "non-overlapping count"
|
||||
let zero: Int = str_count("hello", "xyz")
|
||||
assert zero == 0, "not found gives 0"
|
||||
let empty_sub: Int = str_count("hello", "")
|
||||
assert empty_sub == 0, "empty sub gives 0"
|
||||
}
|
||||
|
||||
test "str-count-lines-words-letters" {
|
||||
let s: String = "hello world\nfoo bar"
|
||||
let lines: Int = str_count_lines(s)
|
||||
let words: Int = str_count_words(s)
|
||||
let letters: Int = str_count_letters(s)
|
||||
assert lines == 2, "line count"
|
||||
assert words == 4, "word count"
|
||||
assert letters == 16, "letter count"
|
||||
}
|
||||
|
||||
test "str-count-chars-and-digits" {
|
||||
let digits: Int = str_count_digits("abc123def456")
|
||||
assert digits == 6, "six digits"
|
||||
let none: Int = str_count_digits("hello")
|
||||
assert none == 0, "no digits"
|
||||
let chars: Int = str_count_chars("hello")
|
||||
assert chars == 5, "five ASCII chars"
|
||||
}
|
||||
|
||||
test "char-classes" {
|
||||
assert is_letter("A"), "A is a letter"
|
||||
assert is_digit("7"), "7 is a digit"
|
||||
assert is_whitespace(" "), "space is whitespace"
|
||||
assert !is_letter("3"), "3 is not a letter"
|
||||
assert !is_digit("X"), "X is not a digit"
|
||||
assert is_alphanumeric("abc123"), "abc123 is alphanumeric"
|
||||
assert !is_alphanumeric("abc!"), "abc! is not alphanumeric"
|
||||
assert is_uppercase("ABC"), "ABC is uppercase"
|
||||
assert is_lowercase("abc"), "abc is lowercase"
|
||||
assert !is_uppercase("Abc"), "mixed is not uppercase"
|
||||
}
|
||||
|
||||
test "str-split" {
|
||||
let parts: [String] = str_split("a,b,c", ",")
|
||||
let n: Int = native_list_len(parts)
|
||||
assert n == 3, "split into 3 parts"
|
||||
let p0: String = native_list_get(parts, 0)
|
||||
let p1: String = native_list_get(parts, 1)
|
||||
let p2: String = native_list_get(parts, 2)
|
||||
assert p0 == "a", "first part"
|
||||
assert p1 == "b", "second part"
|
||||
assert p2 == "c", "third part"
|
||||
}
|
||||
|
||||
test "str-split-lines" {
|
||||
let lines: [String] = str_split_lines("alpha\nbeta\r\ngamma\n")
|
||||
let n: Int = native_list_len(lines)
|
||||
assert n == 3, "split into 3 lines"
|
||||
let l0: String = native_list_get(lines, 0)
|
||||
let l1: String = native_list_get(lines, 1)
|
||||
let l2: String = native_list_get(lines, 2)
|
||||
assert l0 == "alpha", "first line"
|
||||
assert l1 == "beta", "second line strips CR"
|
||||
assert l2 == "gamma", "third line"
|
||||
}
|
||||
|
||||
test "str-join" {
|
||||
let parts: [String] = native_list_empty()
|
||||
let parts = native_list_append(parts, "alpha")
|
||||
let parts = native_list_append(parts, "beta")
|
||||
let parts = native_list_append(parts, "gamma")
|
||||
let result: String = str_join(parts, ", ")
|
||||
assert result == "alpha, beta, gamma", "join with separator"
|
||||
let empty_parts: [String] = native_list_empty()
|
||||
let empty_result: String = str_join(empty_parts, ",")
|
||||
assert empty_result == "", "join empty list gives empty string"
|
||||
}
|
||||
|
||||
test "str-char-at" {
|
||||
let c: String = str_char_at("hello", 1)
|
||||
assert c == "e", "char at index 1"
|
||||
let first: String = str_char_at("abc", 0)
|
||||
assert first == "a", "first char"
|
||||
let oob: String = str_char_at("abc", 10)
|
||||
assert oob == "", "out of bounds returns empty"
|
||||
}
|
||||
|
||||
test "url-encode-decode" {
|
||||
let encoded: String = url_encode("hello world")
|
||||
assert !str_contains(encoded, " "), "space is encoded"
|
||||
let decoded: String = url_decode(encoded)
|
||||
assert decoded == "hello world", "round-trip decode"
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user