codegen: emit C instead of bytecode — El is now natively compiled

Rewrites codegen.el to produce C source instead of JSON bytecode,
eliminating the ELVM interpreter as a runtime dependency.

- All El values use el_val_t (int64_t) as the universal type; integers
  are stored directly, strings/pointers via uintptr_t cast
- String literals wrapped with EL_STR(), arithmetic works natively
- fn declarations become C functions returning el_val_t
- let bindings become el_val_t local variables
- if/else, while, for all map to native C control flow
- String + String uses el_str_concat(); numeric + uses C +
- strip_outer_parens() prevents double-paren warnings in if/while
- compiler.el updated to describe C output and correct CLI usage

Adds el-compiler/runtime/ with:
- el_runtime.h: declares all builtins using el_val_t
- el_runtime.c: implements I/O, strings, math, list, map, fs, JSON;
  HTTP builtins are stubs (return empty string) pending libcurl

Compile El programs with:
  cc -I<runtime-dir> -o hello hello.c el_runtime.c
This commit is contained in:
Will Anderson
2026-04-29 22:33:27 -05:00
parent 4f3543b068
commit ede087eb04
4 changed files with 948 additions and 518 deletions
+414
View File
@@ -0,0 +1,414 @@
/*
* el_runtime.c — El language C runtime implementation
*
* All functions use el_val_t (= int64_t) as the universal value type.
* Strings are transported as their pointer address cast to int64_t.
* On any 64-bit system sizeof(pointer) <= sizeof(int64_t), so this is safe.
*
* Compile with:
* cc -std=c11 -I<runtime-dir> -o <prog> <prog>.c el_runtime.c
*/
#include "el_runtime.h"
#include <stdarg.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
/* ── Internal allocators ─────────────────────────────────────────────────── */
static char* el_strdup(const char* s) {
if (!s) return strdup("");
return strdup(s);
}
static char* el_strbuf(size_t n) {
char* p = malloc(n + 1);
if (!p) { fputs("el_runtime: out of memory\n", stderr); exit(1); }
p[0] = '\0';
return p;
}
/* Wrap an allocated C string as el_val_t */
static el_val_t el_wrap_str(char* s) {
return EL_STR(s);
}
/* ── I/O ──────────────────────────────────────────────────────────────────── */
void println(el_val_t s) {
const char* str = EL_CSTR(s);
if (str) puts(str);
else puts("");
}
void print(el_val_t s) {
const char* str = EL_CSTR(s);
if (str) fputs(str, stdout);
}
el_val_t readline(void) {
char buf[4096];
if (!fgets(buf, sizeof(buf), stdin)) return el_wrap_str(el_strdup(""));
size_t len = strlen(buf);
if (len > 0 && buf[len - 1] == '\n') buf[len - 1] = '\0';
return el_wrap_str(el_strdup(buf));
}
/* ── String builtins ─────────────────────────────────────────────────────── */
el_val_t el_str_concat(el_val_t av, el_val_t bv) {
const char* a = EL_CSTR(av);
const char* b = EL_CSTR(bv);
if (!a) a = "";
if (!b) b = "";
size_t la = strlen(a);
size_t lb = strlen(b);
char* out = el_strbuf(la + lb);
memcpy(out, a, la);
memcpy(out + la, b, lb);
out[la + lb] = '\0';
return el_wrap_str(out);
}
el_val_t str_eq(el_val_t av, el_val_t bv) {
const char* a = EL_CSTR(av);
const char* b = EL_CSTR(bv);
if (!a || !b) return (el_val_t)(a == b);
return (el_val_t)(strcmp(a, b) == 0);
}
el_val_t str_starts_with(el_val_t sv, el_val_t prefv) {
const char* s = EL_CSTR(sv);
const char* prefix = EL_CSTR(prefv);
if (!s || !prefix) return 0;
size_t lp = strlen(prefix);
return (el_val_t)(strncmp(s, prefix, lp) == 0);
}
el_val_t str_ends_with(el_val_t sv, el_val_t sufv) {
const char* s = EL_CSTR(sv);
const char* suffix = EL_CSTR(sufv);
if (!s || !suffix) return 0;
size_t ls = strlen(s);
size_t lsuf = strlen(suffix);
if (lsuf > ls) return 0;
return (el_val_t)(strcmp(s + ls - lsuf, suffix) == 0);
}
el_val_t str_len(el_val_t sv) {
const char* s = EL_CSTR(sv);
if (!s) return 0;
return (el_val_t)strlen(s);
}
el_val_t str_concat(el_val_t a, el_val_t b) {
return el_str_concat(a, b);
}
el_val_t int_to_str(el_val_t n) {
char buf[32];
snprintf(buf, sizeof(buf), "%lld", (long long)n);
return el_wrap_str(el_strdup(buf));
}
el_val_t str_to_int(el_val_t sv) {
const char* s = EL_CSTR(sv);
if (!s) return 0;
return (el_val_t)atoll(s);
}
el_val_t str_slice(el_val_t sv, el_val_t start, el_val_t end) {
const char* s = EL_CSTR(sv);
if (!s) return el_wrap_str(el_strdup(""));
int64_t len = (int64_t)strlen(s);
if (start < 0) start = 0;
if (end > len) end = len;
if (start >= end) return el_wrap_str(el_strdup(""));
int64_t sz = end - start;
char* out = el_strbuf((size_t)sz);
memcpy(out, s + start, (size_t)sz);
out[sz] = '\0';
return el_wrap_str(out);
}
el_val_t str_contains(el_val_t sv, el_val_t subv) {
const char* s = EL_CSTR(sv);
const char* sub = EL_CSTR(subv);
if (!s || !sub) return 0;
return (el_val_t)(strstr(s, sub) != NULL);
}
el_val_t str_replace(el_val_t sv, el_val_t fromv, el_val_t tov) {
const char* s = EL_CSTR(sv);
const char* from = EL_CSTR(fromv);
const char* to = EL_CSTR(tov);
if (!s || !from || !to) return el_wrap_str(el_strdup(s ? s : ""));
size_t ls = strlen(s);
size_t lf = strlen(from);
size_t lt = strlen(to);
if (lf == 0) return el_wrap_str(el_strdup(s));
size_t count = 0;
const char* p = s;
while ((p = strstr(p, from)) != NULL) { count++; p += lf; }
size_t out_sz = ls + count * lt + 1;
char* out = el_strbuf(out_sz);
char* dst = out;
p = s;
const char* found;
while ((found = strstr(p, from)) != NULL) {
size_t chunk = (size_t)(found - p);
memcpy(dst, p, chunk); dst += chunk;
memcpy(dst, to, lt); dst += lt;
p = found + lf;
}
strcpy(dst, p);
return el_wrap_str(out);
}
el_val_t str_to_upper(el_val_t sv) {
const char* s = EL_CSTR(sv);
if (!s) return el_wrap_str(el_strdup(""));
size_t n = strlen(s);
char* out = el_strbuf(n);
for (size_t i = 0; i < n; i++) out[i] = (char)toupper((unsigned char)s[i]);
out[n] = '\0';
return el_wrap_str(out);
}
el_val_t str_to_lower(el_val_t sv) {
const char* s = EL_CSTR(sv);
if (!s) return el_wrap_str(el_strdup(""));
size_t n = strlen(s);
char* out = el_strbuf(n);
for (size_t i = 0; i < n; i++) out[i] = (char)tolower((unsigned char)s[i]);
out[n] = '\0';
return el_wrap_str(out);
}
el_val_t str_trim(el_val_t sv) {
const char* s = EL_CSTR(sv);
if (!s) return el_wrap_str(el_strdup(""));
while (*s && isspace((unsigned char)*s)) s++;
size_t n = strlen(s);
while (n > 0 && isspace((unsigned char)s[n - 1])) n--;
char* out = el_strbuf(n);
memcpy(out, s, n);
out[n] = '\0';
return el_wrap_str(out);
}
/* ── Math ────────────────────────────────────────────────────────────────── */
el_val_t el_abs(el_val_t n) { return n < 0 ? -n : n; }
el_val_t el_max(el_val_t a, el_val_t b) { return a > b ? a : b; }
el_val_t el_min(el_val_t a, el_val_t b) { return a < b ? a : b; }
/* ── List ────────────────────────────────────────────────────────────────── */
/*
* Dynamic array header:
* int64_t capacity
* int64_t length
* el_val_t elems[]
*/
typedef struct {
int64_t capacity;
int64_t length;
el_val_t elems[1];
} ElList;
static ElList* list_alloc(int64_t cap) {
ElList* lst = malloc(sizeof(ElList) + (size_t)(cap > 1 ? cap - 1 : 0) * sizeof(el_val_t));
if (!lst) { fputs("el_runtime: out of memory\n", stderr); exit(1); }
lst->capacity = cap;
lst->length = 0;
return lst;
}
el_val_t el_list_empty(void) {
return EL_STR(list_alloc(4));
}
el_val_t el_list_new(el_val_t count, ...) {
ElList* lst = list_alloc(count > 0 ? count : 4);
va_list ap;
va_start(ap, count);
for (int64_t i = 0; i < count; i++) {
lst->elems[i] = va_arg(ap, el_val_t);
}
va_end(ap);
lst->length = count;
return EL_STR(lst);
}
el_val_t el_list_len(el_val_t listv) {
ElList* lst = (ElList*)(uintptr_t)listv;
if (!lst) return 0;
return lst->length;
}
el_val_t el_list_get(el_val_t listv, el_val_t index) {
ElList* lst = (ElList*)(uintptr_t)listv;
if (!lst) return 0;
if (index < 0 || index >= lst->length) return 0;
return lst->elems[index];
}
el_val_t el_list_append(el_val_t listv, el_val_t elem) {
ElList* lst = (ElList*)(uintptr_t)listv;
if (!lst) { lst = list_alloc(4); }
if (lst->length >= lst->capacity) {
int64_t new_cap = lst->capacity * 2;
lst = realloc(lst, sizeof(ElList) + (size_t)(new_cap - 1) * sizeof(el_val_t));
if (!lst) { fputs("el_runtime: out of memory\n", stderr); exit(1); }
lst->capacity = new_cap;
}
lst->elems[lst->length++] = elem;
return EL_STR(lst);
}
/* ── Map ─────────────────────────────────────────────────────────────────── */
typedef struct {
int64_t count;
el_val_t* keys;
el_val_t* values;
} ElMap;
el_val_t el_map_new(el_val_t pair_count, ...) {
ElMap* m = malloc(sizeof(ElMap));
if (!m) { fputs("el_runtime: out of memory\n", stderr); exit(1); }
m->count = pair_count;
m->keys = malloc(sizeof(el_val_t) * (size_t)(pair_count > 0 ? pair_count : 1));
m->values = malloc(sizeof(el_val_t) * (size_t)(pair_count > 0 ? pair_count : 1));
va_list ap;
va_start(ap, pair_count);
for (int64_t i = 0; i < pair_count; i++) {
m->keys[i] = va_arg(ap, el_val_t);
m->values[i] = va_arg(ap, el_val_t);
}
va_end(ap);
return EL_STR(m);
}
static ElMap* as_map(el_val_t v) { return (ElMap*)(uintptr_t)v; }
el_val_t el_map_get(el_val_t mapv, el_val_t keyv) {
ElMap* m = as_map(mapv);
const char* key = EL_CSTR(keyv);
if (!m || !key) return 0;
for (int64_t i = 0; i < m->count; i++) {
const char* k = EL_CSTR(m->keys[i]);
if (k && strcmp(k, key) == 0) return m->values[i];
}
return 0;
}
el_val_t el_get_field(el_val_t mapv, el_val_t keyv) {
return el_map_get(mapv, keyv);
}
el_val_t el_map_set(el_val_t mapv, el_val_t keyv, el_val_t value) {
ElMap* m = as_map(mapv);
const char* key = EL_CSTR(keyv);
if (!m) return 0;
for (int64_t i = 0; i < m->count; i++) {
const char* k = EL_CSTR(m->keys[i]);
if (k && strcmp(k, key) == 0) { m->values[i] = value; return mapv; }
}
int64_t nc = m->count + 1;
m->keys = realloc(m->keys, sizeof(el_val_t) * (size_t)nc);
m->values = realloc(m->values, sizeof(el_val_t) * (size_t)nc);
m->keys[m->count] = keyv;
m->values[m->count] = value;
m->count = nc;
return mapv;
}
/* ── HTTP stubs ──────────────────────────────────────────────────────────── */
el_val_t http_get(el_val_t url) {
(void)url;
return el_wrap_str(el_strdup(""));
}
el_val_t http_post(el_val_t url, el_val_t body) {
(void)url; (void)body;
return el_wrap_str(el_strdup(""));
}
void http_serve(el_val_t port, el_val_t handler) {
(void)port; (void)handler;
}
/* ── Filesystem ──────────────────────────────────────────────────────────── */
el_val_t fs_read(el_val_t pathv) {
const char* path = EL_CSTR(pathv);
if (!path) return el_wrap_str(el_strdup(""));
FILE* f = fopen(path, "rb");
if (!f) return el_wrap_str(el_strdup(""));
fseek(f, 0, SEEK_END);
long sz = ftell(f);
rewind(f);
char* buf = el_strbuf((size_t)sz);
size_t got = fread(buf, 1, (size_t)sz, f);
buf[got] = '\0';
fclose(f);
return el_wrap_str(buf);
}
el_val_t fs_write(el_val_t pathv, el_val_t contentv) {
const char* path = EL_CSTR(pathv);
const char* content = EL_CSTR(contentv);
if (!path || !content) return 0;
FILE* f = fopen(path, "wb");
if (!f) return 0;
size_t n = strlen(content);
size_t written = fwrite(content, 1, n, f);
fclose(f);
return written == n ? 1 : 0;
}
/* ── JSON ────────────────────────────────────────────────────────────────── */
el_val_t json_get(el_val_t jsonv, el_val_t keyv) {
const char* json = EL_CSTR(jsonv);
const char* key = EL_CSTR(keyv);
if (!json || !key) return el_wrap_str(el_strdup(""));
size_t klen = strlen(key);
char* pattern = el_strbuf(klen + 4);
snprintf(pattern, klen + 5, "\"%s\":", key);
const char* p = strstr(json, pattern);
free(pattern);
if (!p) return el_wrap_str(el_strdup(""));
p += strlen(key) + 3; /* skip "key": */
while (*p == ' ' || *p == '\t' || *p == '\n') p++;
if (*p == '"') {
p++;
const char* start = p;
while (*p && !(*p == '"' && *(p-1) != '\\')) p++;
size_t len = (size_t)(p - start);
char* out = el_strbuf(len);
memcpy(out, start, len);
out[len] = '\0';
return el_wrap_str(out);
}
const char* start = p;
while (*p && *p != ',' && *p != '}' && *p != ']' && *p != '\n') p++;
size_t len = (size_t)(p - start);
char* out = el_strbuf(len);
memcpy(out, start, len);
out[len] = '\0';
return el_wrap_str(out);
}
/* ── Process ─────────────────────────────────────────────────────────────── */
void exit_program(el_val_t code) {
exit((int)code);
}
+108
View File
@@ -0,0 +1,108 @@
/*
* 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
*/
#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)
#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);
/* ── 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);
/* ── 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);
void http_serve(el_val_t port, el_val_t handler);
/* ── Filesystem ──────────────────────────────────────────────────────────── */
el_val_t fs_read(el_val_t path);
el_val_t fs_write(el_val_t path, el_val_t content);
/* ── JSON ────────────────────────────────────────────────────────────────── */
el_val_t json_get(el_val_t json, el_val_t key);
/* ── Process ─────────────────────────────────────────────────────────────── */
void exit_program(el_val_t code);
#ifdef __cplusplus
}
#endif
+416 -508
View File
File diff suppressed because it is too large Load Diff
+10 -10
View File
@@ -4,14 +4,14 @@
// This is the bootstrap entry point: compiled once by the Rust el-compiler,
// then self-hosted from that point forward.
//
// The returned JSON is an array of bytecode instruction objects matching
// the serde serialisation format of el-compiler's Bytecode enum.
// The returned string is C source code. Compile the output with:
// cc -o <prog> <prog>.c el_runtime.c
import "lexer.el"
import "parser.el"
import "codegen.el"
// compile full pipeline: source string -> JSON bytecode string
// compile full pipeline: source string -> C source string
fn compile(source: String) -> String {
let tokens: [Map<String, Any>] = lex(source)
let stmts: [Map<String, Any>] = parse(tokens)
@@ -20,23 +20,23 @@ fn compile(source: String) -> String {
// main CLI entry point for self-hosted compilation.
//
// Called by: elvm el-compiler.elc <source.el> <output.elc>
// Called by: elc <source.el> <output.c>
//
// Reads pre-resolved El source from args()[0], compiles it to JSON bytecode,
// and writes the result to args()[1]. The output is raw JSON (accepted by
// both elvm and el exec without an ELVM container header).
// Reads El source from args()[0], compiles it to C source, and writes the
// result to args()[1]. Then run:
// cc -o <prog> <output.c> el_runtime.c
fn main() -> Void {
let argv: [String] = args()
let argc: Int = native_list_len(argv)
if argc < 2 {
println("el-compiler: usage: elvm el-compiler.elc <source.el> <output.elc>")
println("el-compiler: usage: elc <source.el> <output.c>")
exit(1)
}
let src_path: String = native_list_get(argv, 0)
let out_path: String = native_list_get(argv, 1)
let source: String = fs_read(src_path)
let bytecode_json: String = compile(source)
let ok: Bool = fs_write(out_path, bytecode_json)
let c_source: String = compile(source)
let ok: Bool = fs_write(out_path, c_source)
if ok {
exit(0)
} else {