← 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
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.c5758
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 value1819
// (`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, 229
+ 1, 2,30
+ 0, 1, 22931
};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_dir5052
if (id >= 30 && id <= 33) return vm_call_builtin_collection(id, args, argc); // pop, insert, slice, sort5153
if (id == 43) return vm_call_builtin_matrix(id, args, argc); // matrix_mul54
+ if (id >= 44 && id <= 46) return vm_call_builtin_proc(id, args, argc); // sys_fork, sys_wait, sys_exec5255
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 200809L2
+#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's10
+// (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 child14
+ if (argc != 0) { vm_runtime_error("sys_fork() expects no arguments\n"); }15
+ fflush(stdout); // buffered output must not be written by both processes16
+ pid_t pid = fork();17
+ return vm_num(pid < 0 ? -errno : pid);18
+ }19
+ case 45: { // sys_wait -- the waited pid, or negative errno20
+ 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.h3
+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
#endifhearth/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 ran2
+truehearth/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", NULL30
+ "write_file", "append_file", "file_exists", "rename_file", "delete_file", "make_dir", "list_dir",31
+ "sys_fork", "sys_wait", "sys_exec", NULL3132
};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's18
+// 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: fork47
+ 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(); // pid56
+ 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 = NULL67
+ emit_mov_reg_imm64(code, REG_RDX, 0); // options = 068
+ // 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 writes70
+ // r8-r15 (see emit.h), and fork() keeps it.71
+ emit_mov_reg_imm64(code, REG_RAX, 61); // syscall: wait472
+ emit_syscall(code);73
+ push_rax_as_number();74
+}75
+76
+void codegen_builtin_sys_exec(void) {77
+ codegen_expression(); // path78
+ expect(TOKEN_COMMA, "expected ',' after path");79
+ codegen_expression(); // arg80
+ 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(); // arg90
+ emit_store_mem_disp32(code, REG_RBP, base + 24, REG_RAX);91
+ pop_path_nullterm(); // path92
+ 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 string95
+ 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 terminator104
+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: execve112
+ emit_syscall(code);113
+ push_rax_as_number(); // only reached on failure114
+}\ No newline at end of filehearth/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.h1415
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.h11
+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_H2
+#define KILN_PROC_BUILTINS_H3
+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 filehearth/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 ran2
+true