← Commit history

feat(builtins): add sys_fork, sys_wait and sys_exec to kiln and ashvm for the Ash OS

lennoxrose committed on Oct 18, 2026 at 04:59 UTC

ce3e1abparentf0d3be5

17 files changed+255−4

CHANGELOG.mdmodified

+3−0
@@ -1,3 +1,6 @@
1+## 2026-10-18 (process builtins for the Ash OS)
2+- New kiln builtins `sys_fork()`, `sys_wait(pid)` and `sys_exec(path, arg)` (Linux x86-64 syscalls fork, wait4, execve; `codegen/C/builtins/proc_builtins.c`). They are dispatched in `builtins.c`, listed in the builtin arity table (so they work as values) and reserved in the front end's `lower_expr.c`. Programs reach them through `@Lower` natives like the stdlib does. On a Windows target they emit a runtime error. `ashvm` implements them too (`ashvm/src/builtins/C/proc.c`, ids 44-46, POSIX `fork`/`waitpid`/`execve` with an empty environment, stdout flushed before fork/exec). Both projects cover them with `tests/cases/process/ProcFork.ash`.
3+
14 ## 2026-10-18 (apm license)
25 - `apm license` lists the built-in licenses; `apm license <id> [--author <name>] [--year <yyyy>]` writes `LICENSE` (replacing any existing one) and sets `license:` in `ash.pkg`, so running it again with another id swaps both and leaves every other field alone. Built in as C string data (`apm/src/C/license`, about 170 KB): MIT, Apache-2.0, BSD-3-Clause, BSD-2-Clause, ISC, OFL-1.1, GPL-2.0, GPL-3.0, LGPL-2.1, LGPL-3.0. Texts are verbatim SPDX texts; MIT, BSD, ISC and OFL get the author and year filled in.
36

hearth/ashvm/Makefilemodified

+1−0
@@ -53,6 +53,7 @@ ASHVM_SRC = \
5353 src/builtins/C/file.c \
5454 src/builtins/C/string.c \
5555 src/builtins/C/matrix.c \
56+ src/builtins/C/proc.c \
5657 src/diagnostics/C/diagnostics.c
5758
5859 # Objects and dependency files live in trees parallel to each C tree:

hearth/ashvm/src/builtins/C/builtins.cmodified

+5−2
@@ -12,7 +12,8 @@ static const char *builtin_names[] = {
1212 "pop", "insert", "slice", "sort",
1313 "rename_file", "delete_file", "make_dir", "list_dir",
1414 "contains", "starts_with", "ends_with", "repeat",
15- "exit", "matrix_mul"
15+ "exit", "matrix_mul",
16+ "sys_fork", "sys_wait", "sys_exec"
1617 };
1718 // Argument count each builtin is wrapped with when used as a value
1819 // (`map(xs, str)`); same order as builtin_names.
@@ -25,7 +26,8 @@ static const int builtin_arity[] = {
2526 1, 3, 3, 1,
2627 2, 1, 1, 1,
2728 2, 2, 2, 2,
28- 1, 2
29+ 1, 2,
30+ 0, 1, 2
2931 };
3032 #define NUM_BUILTINS (int)(sizeof(builtin_names) / sizeof(builtin_names[0]))
3133
@@ -49,5 +51,6 @@ VMValue vm_call_builtin(int id, VMValue *args, int argc) {
4951 if (id >= 34 && id <= 37) return vm_call_builtin_file(id, args, argc); // rename_file, delete_file, make_dir, list_dir
5052 if (id >= 30 && id <= 33) return vm_call_builtin_collection(id, args, argc); // pop, insert, slice, sort
5153 if (id == 43) return vm_call_builtin_matrix(id, args, argc); // matrix_mul
54+ if (id >= 44 && id <= 46) return vm_call_builtin_proc(id, args, argc); // sys_fork, sys_wait, sys_exec
5255 vm_runtime_error("unknown builtin id: %d\n", id);
5356 }

hearth/ashvm/src/builtins/C/proc.cadded

+35−0
@@ -0,0 +1,35 @@
1+#define _POSIX_C_SOURCE 200809L
2+#include <errno.h>
3+#include <stdio.h>
4+#include <sys/wait.h>
5+#include <unistd.h>
6+#include "builtins/H/internal.h"
7+#include "vm/H/vm.h"
8+
9+// Process builtins for the Ash OS userland; same contract as kiln's
10+// (kiln/src/codegen/C/builtins/proc_builtins.c). Linux/POSIX only.
11+VMValue vm_call_builtin_proc(int id, VMValue *args, int argc) {
12+ switch (id) {
13+ case 44: { // sys_fork -- pid, 0 in the child
14+ if (argc != 0) { vm_runtime_error("sys_fork() expects no arguments\n"); }
15+ fflush(stdout); // buffered output must not be written by both processes
16+ pid_t pid = fork();
17+ return vm_num(pid < 0 ? -errno : pid);
18+ }
19+ case 45: { // sys_wait -- the waited pid, or negative errno
20+ if (argc != 1 || args[0].type != VM_NUM) { vm_runtime_error("sys_wait(pid) expected\n"); }
21+ pid_t pid = waitpid((pid_t)args[0].number, NULL, 0);
22+ return vm_num(pid < 0 ? -errno : pid);
23+ }
24+ case 46: { // sys_exec -- only returns on failure (negative errno)
25+ if (argc != 2 || args[0].type != VM_STR || args[1].type != VM_STR) { vm_runtime_error("sys_exec(path, arg) expected\n"); }
26+ fflush(stdout);
27+ char *argv[3] = { args[0].str, args[1].str, NULL };
28+ if (args[1].str[0] == '\0') argv[1] = NULL; // "" means argv = [path]
29+ char *envp[1] = { NULL };
30+ execve(args[0].str, argv, envp);
31+ return vm_num(-errno);
32+ }
33+ }
34+ vm_runtime_error("unknown builtin id: %d\n", id);
35+}

hearth/ashvm/src/builtins/D/proc.dadded

+6−0
@@ -0,0 +1,6 @@
1+src/builtins/O/proc.o: src/builtins/C/proc.c src/builtins/H/internal.h \
2+ src/value/H/value.h src/vm/H/vm.h src/vm/H/chunk.h
3+src/builtins/H/internal.h:
4+src/value/H/value.h:
5+src/vm/H/vm.h:
6+src/vm/H/chunk.h:

hearth/ashvm/src/builtins/H/internal.hmodified

+1−0
@@ -9,5 +9,6 @@ VMValue vm_call_builtin_collection(int id, VMValue *args, int argc);
99 VMValue vm_call_builtin_string(int id, VMValue *args, int argc);
1010 VMValue vm_call_builtin_file(int id, VMValue *args, int argc);
1111 VMValue vm_call_builtin_matrix(int id, VMValue *args, int argc);
12+VMValue vm_call_builtin_proc(int id, VMValue *args, int argc);
1213
1314 #endif

hearth/ashvm/tests/cases/process/ProcFork.ashadded

+19−0
@@ -0,0 +1,19 @@
1+package process;
2+
3+/// sys_fork/sys_wait/sys_exec: the child execs /bin/echo, the parent waits for it.
4+class ProcFork {
5+ @Lower("sys_fork()") private static native int fork();
6+ @Lower("sys_wait($1)") private static native int await(int pid);
7+ @Lower("sys_exec($1, $2)") private static native int exec(String path, String arg);
8+
9+ public static void main(String[] args) {
10+ int pid = fork();
11+ if (pid == 0) {
12+ exec("/bin/echo", "child ran");
13+ System.out.println("exec failed");
14+ return;
15+ }
16+ int done = await(pid);
17+ System.out.println(done == pid);
18+ }
19+}

hearth/ashvm/tests/cases/process/ProcFork.outadded

+2−0
@@ -0,0 +1,2 @@
1+child ran
2+true

hearth/front/src/jlower/C/lower_expr.cmodified

+2−1
@@ -27,7 +27,8 @@ static const char *reserved[] = {
2727 "num", "exit", "sqrt", "abs", "floor", "chr", "ord", "push", "pop", "insert", "slice", "delete", "sort",
2828 "keys", "values", "has", "map", "filter", "reduce", "split", "join", "substring", "indexOf", "contains",
2929 "starts_with", "ends_with", "replace", "repeat", "upper", "lower", "trim", "input", "read_file",
30- "write_file", "append_file", "file_exists", "rename_file", "delete_file", "make_dir", "list_dir", NULL
30+ "write_file", "append_file", "file_exists", "rename_file", "delete_file", "make_dir", "list_dir",
31+ "sys_fork", "sys_wait", "sys_exec", NULL
3132 };
3233
3334 char *sname(const char *n) {

hearth/kiln/Makefilemodified

+1−0
@@ -18,6 +18,7 @@ KILN_SRC = \
1818 src/parser/C/statements/attempt_handle.c \
1919 src/codegen/C/builtins/builtins.c \
2020 src/codegen/C/builtins/exit_builtin.c \
21+ src/codegen/C/builtins/proc_builtins.c \
2122 src/codegen/C/builtins/filter_builtin.c \
2223 src/codegen/C/builtins/map_builtin.c \
2324 src/codegen/C/builtins/type_builtin.c \

hearth/kiln/src/codegen/C/builtins/builtins.cmodified

+5−0
@@ -14,6 +14,7 @@
1414 #include "codegen/H/io/argv_builtin.h"
1515 #include "codegen/H/builtins/type_builtin.h"
1616 #include "codegen/H/builtins/exit_builtin.h"
17+#include "codegen/H/builtins/proc_builtins.h"
1718 #include "parser/H/core/parser.h"
1819
1920 int codegen_try_builtin_call(const char *name, int len) {
@@ -118,6 +119,9 @@ int codegen_try_builtin_call(const char *name, int len) {
118119 if (len == 9 && strncmp(name, "ends_with", 9) == 0) { codegen_builtin_ends_with(); return 1; }
119120 if (len == 6 && strncmp(name, "repeat", 6) == 0) { codegen_builtin_repeat(); return 1; }
120121 if (len == 4 && strncmp(name, "exit", 4) == 0) { codegen_builtin_exit(); return 1; }
122+ if (len == 8 && strncmp(name, "sys_fork", 8) == 0) { codegen_builtin_sys_fork(); return 1; }
123+ if (len == 8 && strncmp(name, "sys_wait", 8) == 0) { codegen_builtin_sys_wait(); return 1; }
124+ if (len == 8 && strncmp(name, "sys_exec", 8) == 0) { codegen_builtin_sys_exec(); return 1; }
121125 if (len == 4 && strncmp(name, "sort", 4) == 0) {
122126 codegen_builtin_sort();
123127 return 1;
@@ -261,6 +265,7 @@ int codegen_builtin_arity(const char *name, int len) {
261265 {"type", 1}, {"chr", 1}, {"ord", 1}, {"pop", 1}, {"insert", 3}, {"slice", 3}, {"sort", 1},
262266 {"rename_file", 2}, {"delete_file", 1}, {"make_dir", 1}, {"list_dir", 1},
263267 {"contains", 2}, {"starts_with", 2}, {"ends_with", 2}, {"repeat", 2}, {"exit", 1}, {"argv", 0},
268+ {"sys_fork", 0}, {"sys_wait", 1}, {"sys_exec", 2},
264269 };
265270 for (size_t i = 0; i < sizeof(table) / sizeof(table[0]); i++) {
266271 if ((int)strlen(table[i].name) == len && strncmp(table[i].name, name, (size_t)len) == 0) return table[i].arity;

hearth/kiln/src/codegen/C/builtins/proc_builtins.cadded

+114−0
@@ -0,0 +1,114 @@
1+#include <stdint.h>
2+#include "codegen/H/builtins/proc_builtins.h"
3+#include "codegen/H/io/file_path.h"
4+#include "codegen/H/expressions/expr.h"
5+#include "codegen/H/emit/emit_sse.h"
6+#include "codegen/H/emit/value.h"
7+#include "codegen/H/emit/bytes.h"
8+#include "codegen/H/collections/arrays.h"
9+#include "codegen/H/runtime/errors.h"
10+#include "codegen/H/runtime/heap.h"
11+#include "codegen/H/strings/string_alloc.h"
12+#include "app/H/target.h"
13+#include "parser/H/core/parser.h"
14+#include "parser/H/declarations/vars.h"
15+
16+// Process builtins for the Ash OS userland. Linux x86-64 only.
17+// Values are (tag, payload) pairs, payload on top of the stack; a number's
18+// payload is a double's bit pattern.
19+
20+// RAX = int64 -> pushes it as a NUMBER.
21+static void push_rax_as_number(void) {
22+ emit_cvtsi2sd(code, XMM0, REG_RAX);
23+ emit_movq_reg_from_xmm(code, REG_RAX, XMM0);
24+ emit_mov_reg_imm64(code, REG_RBX, TAG_NUMBER);
25+ emit_push_reg(code, REG_RBX);
26+ emit_push_reg(code, REG_RAX);
27+}
28+
29+// Pops a string and leaves its null-terminated copy's address in RAX.
30+static void pop_path_nullterm(void) {
31+ emit_pop_reg(code, REG_RSI);
32+ emit_pop_reg(code, REG_RBX); // tag (ignored)
33+ emit_load_mem_disp32(code, REG_RDX, REG_RSI, -8);
34+ filepath_emit_nullterm(code);
35+}
36+
37+static int unsupported_target(void) {
38+ if (kiln_get_target() != KILN_TARGET_WINDOWS) return 0;
39+ errors_emit_die(code, "runtime error: process builtins are Linux only");
40+ return 1;
41+}
42+
43+void codegen_builtin_sys_fork(void) {
44+ expect(TOKEN_RPAREN, "expected ')' after 'sys_fork('");
45+ if (!unsupported_target()) {
46+ emit_mov_reg_imm64(code, REG_RAX, 57); // syscall: fork
47+ emit_syscall(code);
48+ } else {
49+ emit_mov_reg_imm64(code, REG_RAX, 0);
50+ }
51+ push_rax_as_number();
52+}
53+
54+void codegen_builtin_sys_wait(void) {
55+ codegen_expression(); // pid
56+ expect(TOKEN_RPAREN, "expected ')' after argument");
57+ emit_pop_reg(code, REG_RAX); // payload (double bits)
58+ emit_pop_reg(code, REG_RBX); // tag (ignored)
59+ if (unsupported_target()) {
60+ emit_mov_reg_imm64(code, REG_RAX, 0);
61+ push_rax_as_number();
62+ return;
63+ }
64+ emit_movq_xmm_from_reg(code, XMM0, REG_RAX);
65+ emit_cvttsd2si(code, REG_RDI, XMM0);
66+ emit_mov_reg_imm64(code, REG_RSI, 0); // status = NULL
67+ emit_mov_reg_imm64(code, REG_RDX, 0); // options = 0
68+ // wait4's 4th argument (rusage) lives in R10, which kiln cannot encode.
69+ // It is 0 anyway: Linux zeroes it at process start and kiln never writes
70+ // r8-r15 (see emit.h), and fork() keeps it.
71+ emit_mov_reg_imm64(code, REG_RAX, 61); // syscall: wait4
72+ emit_syscall(code);
73+ push_rax_as_number();
74+}
75+
76+void codegen_builtin_sys_exec(void) {
77+ codegen_expression(); // path
78+ expect(TOKEN_COMMA, "expected ',' after path");
79+ codegen_expression(); // arg
80+ expect(TOKEN_RPAREN, "expected ')' after arguments");
81+ int32_t base = higher_order_scratch_offset();
82+ if (unsupported_target()) {
83+ emit_pop_reg(code, REG_RAX); emit_pop_reg(code, REG_RBX);
84+ emit_pop_reg(code, REG_RAX); emit_pop_reg(code, REG_RBX);
85+ emit_mov_reg_imm64(code, REG_RAX, 0);
86+ push_rax_as_number();
87+ return;
88+ }
89+ pop_path_nullterm(); // arg
90+ emit_store_mem_disp32(code, REG_RBP, base + 24, REG_RAX);
91+ pop_path_nullterm(); // path
92+ emit_store_mem_disp32(code, REG_RBP, base + 32, REG_RAX); // argv[0]
93+
94+ // argv[1] = arg, or NULL if arg is the empty string
95+ emit_load_mem_disp32(code, REG_RAX, REG_RBP, base + 24);
96+ emit_load_byte_reg(code, REG_RCX, REG_RAX);
97+ emit_cmp_reg_imm32(code, REG_RCX, 0);
98+ int nonempty = emit_jcc_rel32(code, COND_NE);
99+ emit_mov_reg_imm64(code, REG_RAX, 0);
100+ emit_patch_jump(code, nonempty);
101+ emit_store_mem_disp32(code, REG_RBP, base + 40, REG_RAX);
102+ emit_mov_reg_imm64(code, REG_RAX, 0);
103+ emit_store_mem_disp32(code, REG_RBP, base + 48, REG_RAX); // argv terminator
104+
105+ // execve(path, argv, NULL)
106+ emit_load_mem_disp32(code, REG_RDI, REG_RBP, base + 32);
107+ emit_mov_reg_reg(code, REG_RSI, REG_RBP);
108+ emit_mov_reg_imm64(code, REG_RCX, (uint64_t)(int64_t)(base + 32));
109+ emit_add_reg_reg(code, REG_RSI, REG_RCX); // RSI = &argv[0]
110+ emit_mov_reg_imm64(code, REG_RDX, 0);
111+ emit_mov_reg_imm64(code, REG_RAX, 59); // syscall: execve
112+ emit_syscall(code);
113+ push_rax_as_number(); // only reached on failure
114+}
\ No newline at end of file

hearth/kiln/src/codegen/D/builtins/builtins.dmodified

+3−1
@@ -9,7 +9,8 @@ src/codegen/O/builtins/builtins.o: src/codegen/C/builtins/builtins.c \
99 src/codegen/H/strings/string_builtins.h src/codegen/H/io/file_builtins.h \
1010 src/codegen/H/io/input_builtin.h src/codegen/H/io/argv_builtin.h \
1111 src/codegen/H/builtins/type_builtin.h \
12- src/codegen/H/builtins/exit_builtin.h src/parser/H/core/parser.h \
12+ src/codegen/H/builtins/exit_builtin.h \
13+ src/codegen/H/builtins/proc_builtins.h src/parser/H/core/parser.h \
1314 src/codegen/H/emit/emit.h
1415 src/codegen/H/builtins/builtins.h:
1516 src/codegen/H/expressions/expr.h:
@@ -27,5 +28,6 @@ src/codegen/H/io/input_builtin.h:
2728 src/codegen/H/io/argv_builtin.h:
2829 src/codegen/H/builtins/type_builtin.h:
2930 src/codegen/H/builtins/exit_builtin.h:
31+src/codegen/H/builtins/proc_builtins.h:
3032 src/parser/H/core/parser.h:
3133 src/codegen/H/emit/emit.h:

hearth/kiln/src/codegen/D/builtins/proc_builtins.dadded

+25−0
@@ -0,0 +1,25 @@
1+src/codegen/O/builtins/proc_builtins.o: \
2+ src/codegen/C/builtins/proc_builtins.c \
3+ src/codegen/H/builtins/proc_builtins.h src/codegen/H/io/file_path.h \
4+ src/codegen/H/emit/emit.h src/codegen/H/expressions/expr.h \
5+ ../front/src/ir/H/ir_lex.h src/codegen/H/emit/emit_sse.h \
6+ src/codegen/H/emit/value.h src/codegen/H/emit/bytes.h \
7+ src/codegen/H/collections/arrays.h src/codegen/H/runtime/errors.h \
8+ src/codegen/H/runtime/heap.h src/codegen/H/strings/string_alloc.h \
9+ src/app/H/target.h src/parser/H/core/parser.h \
10+ src/parser/H/declarations/vars.h
11+src/codegen/H/builtins/proc_builtins.h:
12+src/codegen/H/io/file_path.h:
13+src/codegen/H/emit/emit.h:
14+src/codegen/H/expressions/expr.h:
15+../front/src/ir/H/ir_lex.h:
16+src/codegen/H/emit/emit_sse.h:
17+src/codegen/H/emit/value.h:
18+src/codegen/H/emit/bytes.h:
19+src/codegen/H/collections/arrays.h:
20+src/codegen/H/runtime/errors.h:
21+src/codegen/H/runtime/heap.h:
22+src/codegen/H/strings/string_alloc.h:
23+src/app/H/target.h:
24+src/parser/H/core/parser.h:
25+src/parser/H/declarations/vars.h:

hearth/kiln/src/codegen/H/builtins/proc_builtins.hadded

+12−0
@@ -0,0 +1,12 @@
1+#ifndef KILN_PROC_BUILTINS_H
2+#define KILN_PROC_BUILTINS_H
3+
4+// sys_fork() -> pid (0 in the child). Linux only.
5+// sys_wait(pid) -> wait4 result (pid, or negative errno).
6+// sys_exec(path, arg) -> only returns on failure (negative errno).
7+// argv = [path, arg]; arg == "" means argv = [path]. envp is empty.
8+void codegen_builtin_sys_fork(void);
9+void codegen_builtin_sys_wait(void);
10+void codegen_builtin_sys_exec(void);
11+
12+#endif
\ No newline at end of file

hearth/kiln/tests/cases/process/ProcFork.ashadded

+19−0
@@ -0,0 +1,19 @@
1+package process;
2+
3+/// sys_fork/sys_wait/sys_exec: the child execs /bin/echo, the parent waits for it.
4+class ProcFork {
5+ @Lower("sys_fork()") private static native int fork();
6+ @Lower("sys_wait($1)") private static native int await(int pid);
7+ @Lower("sys_exec($1, $2)") private static native int exec(String path, String arg);
8+
9+ public static void main(String[] args) {
10+ int pid = fork();
11+ if (pid == 0) {
12+ exec("/bin/echo", "child ran");
13+ System.out.println("exec failed");
14+ return;
15+ }
16+ int done = await(pid);
17+ System.out.println(done == pid);
18+ }
19+}

hearth/kiln/tests/cases/process/ProcFork.outadded

+2−0
@@ -0,0 +1,2 @@
1+child ran
2+true