kit

kit
git clone https://git.ryansepassi.com/git/kit.git
Log | Files | Refs | README

commit 69d47850d76ff863e4493092346bd41f5c1f6574
parent e55665b3c5ce178b17e7838c903429bf62eaf2cc
Author: Ryan Sepassi <rsepassi@gmail.com>
Date:   Fri,  5 Jun 2026 22:20:17 -0700

Implement raw syscall intrinsic lowering

Diffstat:
Mdoc/RUNTIME.md | 15++++++++-------
Mdoc/plan/TODO.md | 24------------------------
Mlang/c/parse/cg_adapter.c | 7+++++++
Mlang/c/parse/cg_adapter.h | 1+
Mlang/c/parse/parse.c | 9+++++++++
Mlang/c/parse/parse_expr.c | 34++++++++++++++++++++++++++++++++++
Mlang/c/parse/parse_priv.h | 1+
Mlang/toy/builtins.c | 8+++++++-
Mrt/include/kit/syscall.h | 25+++++++++++--------------
Msrc/arch/aa64/arch.c | 5+++--
Msrc/arch/aa64/native.c | 30++++++++++++++++++++++++++++++
Msrc/arch/c_target/c_emit.c | 3+++
Msrc/arch/riscv/arch.c | 5+++--
Msrc/arch/riscv/native.c | 18++++++++++++++++++
Msrc/arch/wasm/emit.c | 3+++
Msrc/arch/x64/arch.c | 5+++--
Msrc/arch/x64/native.c | 36++++++++++++++++++++++++++++++++++--
Msrc/cg/arith.c | 4++--
Msrc/cg/cgtarget.h | 4++++
Msrc/opt/pass_machinize.c | 4++--
Mtest/parse/CORPUS.md | 1-
Mtest/toy/cases/144_intrinsic_capability_query.toy | 8+++++---
Atest/toy/err/bad_syscall_arity.expected | 1+
Atest/toy/err/bad_syscall_arity.toy | 3+++
Dtest/toy/err/unsupported_syscall.expected | 1-
Dtest/toy/err/unsupported_syscall.toy | 3---
26 files changed, 192 insertions(+), 66 deletions(-)

diff --git a/doc/RUNTIME.md b/doc/RUNTIME.md @@ -252,13 +252,14 @@ excludes FP status and open-file state. exposes so low-level code stays pure C: - `<kit/coro.h>` — the coroutine API above. `coro_ctx` is the raw 256-byte register buffer; `coro_t` embeds it plus private scheduler storage. -- `<kit/syscall.h>` — `__kit_syscall0..6`, the bare kernel-trap primitive. - These are *compiler*-lowered (the backend emits `syscall`/`int 0x80`/`svc`/ - `ecall` inline as an opaque, full-memory-clobber operation) — there is no - library implementation in this archive. The result is normalized to the - Linux "non-negative success / -errno failure" convention on every target, - with the BSD/Darwin carry-flag form rewritten by the lowering. WASM is a - compile-time error (use WASI imports). +- `<kit/syscall.h>` — `__kit_syscall0..6`, the raw kernel/supervisor trap + primitive for Linux and freestanding native targets. These are + *compiler*-lowered (the backend emits `syscall`/`svc`/`ecall` inline as an + opaque, full-memory-clobber operation) — there is no library implementation + in this archive. The result is the raw target result register; Linux uses its + "non-negative success / -errno failure" convention, and freestanding + environments define their own ABI. Hosted non-Linux targets and WASM raise a + compile-time error (use WASI imports for WASM). - `<kit/baremetal.h>` — IRQ mask save/restore, CPU memory barriers (`__kit_dmb`/`dsb`/`isb`, distinct from C11 fences and meant for DMA / MMU / self-modifying code), range-based cache maintenance, and CPU hints diff --git a/doc/plan/TODO.md b/doc/plan/TODO.md @@ -5,30 +5,6 @@ fixed, remove it instead of checking it off or keeping a closed entry. Add new deferred fixes below as they are discovered. -## `__kit_syscallN` (`rt/include/kit/syscall.h`) is declared/documented but unimplemented - -The header declares `__kit_syscall0..6` and documents them as lowering to the -trap instruction inline ("kit emits the appropriate trap instruction inline; -there is no library call"). In practice nothing implements them: - -- the C frontend does not recognize the `__kit_syscallN` names (no intern in - `lang/c/parse/parse.c`, no handler in `try_parse_builtin_call`), so a call - compiles as an ordinary extern function reference; -- `KIT_CG_INTRIN_SYSCALL` maps to `INTRIN_NONE` in `src/cg/arith.c` and every - native `*_supports_intrinsic` returns 0, so the generic intrinsic path has no - backend lowering either; -- no rt object defines the symbols. - -Result: `#include <kit/syscall.h>` + `__kit_syscall1(93, 42)` fails to link with -`undefined reference to '__kit_syscall1'`. The only working way to issue a -syscall from kit-compiled C today is hand-written extended inline asm (the x64 -backend explicitly supports the register-pinned syscall idiom). Either wire the -`__kit_syscallN` names to an inline lowering (per-arch trap emit, normalizing the -BSD/Darwin carry-flag convention as the header promises) or drop the header. -Found while building a freestanding Linux backtrace demo (it needed `write`/ -`exit`); worked around with inline asm. The toy frontend maps `@syscall` to the -same `INTRIN_NONE` and likewise can't lower it (`test/toy/cases/unsupported_syscall`). - ## Inline asm: kit rejects machine-specific register constraints (e.g. x86 `"=a"`) kit's inline-asm lowering only recognizes the **architecture-neutral** register diff --git a/lang/c/parse/cg_adapter.c b/lang/c/parse/cg_adapter.c @@ -1146,6 +1146,13 @@ void pcg_intrinsic_void(Parser* p, IntrinKind k) { } } +void pcg_syscall(Parser* p, u32 nargs, const Type* long_ty) { + if (pcg_emit_enabled(p)) + kit_cg_intrinsic(p->cg, KIT_CG_INTRIN_SYSCALL, nargs, pcg_tid(p, long_ty)); + for (u32 i = 0; i < nargs; ++i) pcg_drop_type(p); + pcg_push_type(p, long_ty); +} + /* __builtin_return_address(level) / __builtin_frame_address(level): emit the * frame-pointer-chain intrinsic. The constant level rides as a single immediate * operand (kept as OPK_IMM by kit_cg_intrinsic); the result is void*. */ diff --git a/lang/c/parse/cg_adapter.h b/lang/c/parse/cg_adapter.h @@ -347,6 +347,7 @@ void pcg_atomic_cas(Parser*, MemOrder, MemOrder); void pcg_fence(Parser*, MemOrder); void pcg_intrinsic_unary_to_int(Parser*, IntrinKind); void pcg_intrinsic_void(Parser*, IntrinKind); +void pcg_syscall(Parser*, u32 nargs, const Type* long_ty); void pcg_frame_or_return_address(Parser*, int is_return, u32 level); void pcg_inline_asm(Parser*, const char*, const AsmConstraint*, u32, const AsmConstraint*, u32, const Sym*, u32); diff --git a/lang/c/parse/parse.c b/lang/c/parse/parse.c @@ -1509,6 +1509,7 @@ void parse_c(Compiler* c, Pool* pool, Pp* pp, DeclTable* decls, CG* cg, KitSymVis default_visibility) { Parser p; CKw i; + u32 syscall_i; memset(&p, 0, sizeof p); p.c = c; @@ -1537,6 +1538,14 @@ void parse_c(Compiler* c, Pool* pool, Pp* pp, DeclTable* decls, CG* cg, kit_sym_intern(p.pool->c, KIT_SLICE_LIT("__builtin_return_address")); p.sym_b_frame_address = kit_sym_intern(p.pool->c, KIT_SLICE_LIT("__builtin_frame_address")); + for (syscall_i = 0; syscall_i < 7u; ++syscall_i) { + char name[16]; + memcpy(name, "__kit_syscall", 13u); + name[13] = (char)('0' + syscall_i); + name[14] = '\0'; + p.sym_kit_syscall[syscall_i] = + kit_sym_intern(p.pool->c, kit_slice_cstr(name)); + } p.sym_b_memcpy = kit_sym_intern(p.pool->c, KIT_SLICE_LIT("__builtin_memcpy")); p.sym_b_memmove = kit_sym_intern(p.pool->c, KIT_SLICE_LIT("__builtin_memmove")); diff --git a/lang/c/parse/parse_expr.c b/lang/c/parse/parse_expr.c @@ -1691,10 +1691,44 @@ static int parse_builtin_abs_call(Parser* p, Sym name, SrcLoc loc) { return 1; } +static int parse_kit_syscall_call(Parser* p, Sym name, SrcLoc loc) { + const Type* long_ty; + u32 arity = 0; + u32 nargs; + int found = 0; + + for (u32 i = 0; i < 7u; ++i) { + if (name == p->sym_kit_syscall[i]) { + arity = i; + found = 1; + break; + } + } + if (!found) return 0; + + long_ty = type_prim(p->pool, TY_LONG); + nargs = arity + 1u; /* syscall number plus payload args */ + advance(p); /* IDENT */ + expect_punct(p, '(', "'(' after __kit_syscall"); + for (u32 i = 0; i < nargs; ++i) { + if (i) expect_punct(p, ',', "',' in __kit_syscall"); + parse_assign_expr(p); + to_rvalue(p); + coerce_top_to_type(p, long_ty); + } + expect_punct(p, ')', "')' after __kit_syscall"); + + pcg_set_loc(p, loc); + pcg_syscall(p, nargs, long_ty); + return 1; +} + static int try_parse_builtin_call(Parser* p) { Sym name = p->cur.v.ident; SrcLoc loc = p->cur.loc; + if (parse_kit_syscall_call(p, name, loc)) return 1; + if (name == p->sym_b_memcpy || name == p->sym_b_memmove || name == p->sym_b_memcmp || name == p->sym_b_memset) { return parse_builtin_mem_call(p, name, loc); diff --git a/lang/c/parse/parse_priv.h b/lang/c/parse/parse_priv.h @@ -280,6 +280,7 @@ typedef struct Parser { Sym sym_b_va_copy; Sym sym_b_return_address; /* __builtin_return_address */ Sym sym_b_frame_address; /* __builtin_frame_address */ + Sym sym_kit_syscall[7]; /* __kit_syscall0 .. __kit_syscall6 */ Sym sym_attribute; Sym sym_volatile_alias; Sym sym_alignof_alias; diff --git a/lang/toy/builtins.c b/lang/toy/builtins.c @@ -1434,6 +1434,7 @@ KitCgTypeId toy_parse_low_level_builtin_call(ToyParser* p, KitSym name, if (toy_sym_is(p, name, "syscall")) { uint32_t nargs = 0; + KitCgTypeId long_ty = p->int_type; if (!toy_parser_expect(p, TOK_LPAREN)) return KIT_CG_TYPE_NONE; if (!toy_parser_match(p, TOK_RPAREN)) { for (;;) { @@ -1444,6 +1445,8 @@ KitCgTypeId toy_parse_low_level_builtin_call(ToyParser* p, KitSym name, "syscall arguments must be integer or pointer"); return KIT_CG_TYPE_NONE; } + if (arg_ty != long_ty && !toy_emit_cast(p, arg_ty, long_ty)) + return KIT_CG_TYPE_NONE; nargs++; if (nargs > 7u) { toy_error(p, p->cur.loc, "too many syscall arguments"); @@ -1457,7 +1460,10 @@ KitCgTypeId toy_parse_low_level_builtin_call(ToyParser* p, KitSym name, toy_error(p, p->cur.loc, "syscall expects a syscall number"); return KIT_CG_TYPE_NONE; } - return toy_unsupported_intrinsic(p); + if (!kit_cg_target_supports_intrinsic(p->c, KIT_CG_INTRIN_SYSCALL)) + return toy_unsupported_intrinsic(p); + kit_cg_intrinsic(p->cg, KIT_CG_INTRIN_SYSCALL, nargs, long_ty); + return long_ty; } if (toy_sym_is(p, name, "setjmp")) { diff --git a/rt/include/kit/syscall.h b/rt/include/kit/syscall.h @@ -5,29 +5,26 @@ * other low-level code can stay pure C without resorting to inline * asm. * - * Numbering is the caller's responsibility -- kit provides no - * SYS_* table. Pass the platform-specific number (see Linux - * <asm/unistd.h>, Darwin <sys/syscall.h>, etc.) in nr; pointers, + * Trap numbering is the caller's responsibility -- kit provides no + * SYS_* table. Pass the environment's trap/syscall number in nr; on + * Linux that is the target-specific number from <asm/unistd.h>. Pointers, * sizes, and file descriptors are cast to long at the call site. * - * Result convention: normalized to Linux-style "non-negative on - * success, -errno on failure" on every supported target. On - * BSD/Darwin, where the kernel signals failure via the carry/C - * flag and returns the positive errno in the result register, the - * lowering inspects the flag and rewrites the value -- callers - * see the Linux convention regardless of host kernel. + * Result convention: the raw value left in the target result register. On + * Linux this is the kernel's non-negative success / -errno failure convention; + * on freestanding targets the monitor, kernel, or firmware ABI defines it. * * Optimizer view: each call is opaque, with full memory clobber * plus the target's syscall-clobber list. The optimizer cannot * reorder loads, stores, or other side effects across the trap. * - * Per-target lowering (see doc/builtins.md for the table): kit - * emits the appropriate trap instruction (`syscall`, `int 0x80`, + * Per-target lowering: on x86_64, aarch64, and RISC-V Linux/freestanding + * native targets, kit emits the appropriate trap instruction (`syscall`, * `svc`, `ecall`) inline; there is no library call. * - * Not available on WASM: invoking any of these on __wasm__ is a - * compile-time error. WASM programs reach the host via WASI - * imports, not a syscall instruction. + * Not available on hosted non-Linux targets or WASM: invoking any of these + * there is a compile-time error. WASM programs reach the host via WASI imports, + * not a syscall instruction. */ #ifndef KIT_SYSCALL_H #define KIT_SYSCALL_H diff --git a/src/arch/aa64/arch.c b/src/arch/aa64/arch.c @@ -192,7 +192,6 @@ static int aa64_supports_call_conv(const Compiler* c, KitCgCallConv cc) { /* Capability twin of aa_intrinsic (src/arch/aa64/native.c); keep the two in * sync. No default case, so a new KitCgIntrinsic trips -Wswitch here. */ static int aa64_supports_intrinsic(const Compiler* c, KitCgIntrinsic intrin) { - (void)c; switch (intrin) { case KIT_CG_INTRIN_TRAP: case KIT_CG_INTRIN_CLZ: @@ -223,10 +222,12 @@ static int aa64_supports_intrinsic(const Compiler* c, KitCgIntrinsic intrin) { case KIT_CG_INTRIN_FRAME_ADDRESS: case KIT_CG_INTRIN_RETURN_ADDRESS: return 1; + case KIT_CG_INTRIN_SYSCALL: + return c->target.os == KIT_OS_LINUX || + c->target.os == KIT_OS_FREESTANDING; case KIT_CG_INTRIN_SETJMP: case KIT_CG_INTRIN_LONGJMP: case KIT_CG_INTRIN_FMA: - case KIT_CG_INTRIN_SYSCALL: case KIT_CG_INTRIN_DCACHE_CLEAN: case KIT_CG_INTRIN_DCACHE_INVALIDATE: case KIT_CG_INTRIN_DCACHE_CLEAN_INVALIDATE: diff --git a/src/arch/aa64/native.c b/src/arch/aa64/native.c @@ -3572,6 +3572,23 @@ static void aa_intrinsic(NativeTarget* t, IntrinKind kind, case INTRIN_TRAP: aa_trap(t); return; + case INTRIN_SYSCALL: + if (ndst == 1u && narg >= 1u && narg <= 7u) { + static const u32 syscall_regs[7] = {AA_X8, 0u, 1u, 2u, 3u, 4u, 5u}; + AAArgMove moves[7]; + for (u32 i = 0; i < narg; ++i) { + AAArgMove* m = &moves[i]; + memset(m, 0, sizeof *m); + m->dst = + native_loc_reg(dsts[0].type, NATIVE_REG_INT, syscall_regs[i]); + m->src = args[i]; + m->size = t->c->target.ptr_size; + } + aa_emit_reg_arg_moves(t, moves, narg); + aa_emit32(t->mc, aa64_svc(0)); + aa_move(t, dsts[0], native_loc_reg(dsts[0].type, NATIVE_REG_INT, 0)); + } + return; case INTRIN_CPU_NOP: aa_emit32(t->mc, aa64_hint(AA64_HINT_OP_NOP)); return; @@ -3639,6 +3656,18 @@ static void aa_trap(NativeTarget* t) { aa_emit32(t->mc, aa64_brk(0)); } /* file_scope_asm + finalize are shared (cg/native_asm.h). */ +static int aa_machine_op_clobbers(NativeTarget* t, const NativeMachineOp* op, + u32 mask[NATIVE_CALL_PLAN_CLASSES]) { + (void)t; + mask[0] = mask[1] = mask[2] = 0; + if ((NativeMachineOpKind)op->kind != NATIVE_MOP_INTRINSIC || + (IntrinKind)op->intrin != INTRIN_SYSCALL) + return 0; + mask[NATIVE_REG_INT] = (1u << 0) | (1u << 1) | (1u << 2) | (1u << 3) | + (1u << 4) | (1u << 5) | (1u << AA_X8); + return 1; +} + static void aa_set_loc(NativeTarget* t, SrcLoc loc) { AANativeTarget* a = aa_of(t); a->loc = loc; @@ -3877,6 +3906,7 @@ NativeTarget* aa64_native_target_new(Compiler* c, ObjBuilder* obj, t->class_for_type = aa_class_for_type; t->imm_legal = aa_imm_legal; t->addr_legal = aa_addr_legal; + t->machine_op_clobbers = aa_machine_op_clobbers; t->func_begin = aa_func_begin; t->func_begin_known_frame = aa_func_begin_known_frame; t->note_frame_state = aa_note_frame_state; diff --git a/src/arch/c_target/c_emit.c b/src/arch/c_target/c_emit.c @@ -2813,6 +2813,9 @@ void c_emit_intrinsic(CTarget* t, IntrinKind k, Operand* dsts, u32 ndst, c_emit_local_assign_close(t); return; } + case INTRIN_SYSCALL: + compiler_panic(t->c, loc, "C target: syscall intrinsic not supported"); + return; case INTRIN_NONE: default: compiler_panic(t->c, loc, "C target: intrinsic kind %d not handled", diff --git a/src/arch/riscv/arch.c b/src/arch/riscv/arch.c @@ -371,7 +371,6 @@ static int rv64_supports_call_conv(const Compiler* c, KitCgCallConv cc) { * type.c matrix normalized rv32->rv64 for exactly this reason). No default * case, so a new KitCgIntrinsic trips -Wswitch here. */ static int rv64_supports_intrinsic(const Compiler* c, KitCgIntrinsic intrin) { - (void)c; switch (intrin) { case KIT_CG_INTRIN_TRAP: case KIT_CG_INTRIN_CLZ: @@ -396,10 +395,12 @@ static int rv64_supports_intrinsic(const Compiler* c, KitCgIntrinsic intrin) { case KIT_CG_INTRIN_FRAME_ADDRESS: case KIT_CG_INTRIN_RETURN_ADDRESS: return 1; + case KIT_CG_INTRIN_SYSCALL: + return c->target.os == KIT_OS_LINUX || + c->target.os == KIT_OS_FREESTANDING; case KIT_CG_INTRIN_SETJMP: case KIT_CG_INTRIN_LONGJMP: case KIT_CG_INTRIN_FMA: - case KIT_CG_INTRIN_SYSCALL: case KIT_CG_INTRIN_IRQ_SAVE: case KIT_CG_INTRIN_IRQ_RESTORE: case KIT_CG_INTRIN_IRQ_DISABLE: diff --git a/src/arch/riscv/native.c b/src/arch/riscv/native.c @@ -2993,6 +2993,24 @@ static void rv_intrinsic(NativeTarget* t, IntrinKind kind, case INTRIN_TRAP: rv64_emit32(mc, rv_ebreak()); return; + case INTRIN_SYSCALL: + if (ndst == 1u && narg >= 1u && narg <= 7u) { + static const u32 syscall_regs[7] = {RV_A7, RV_A0, RV_A1, RV_A2, + RV_A3, RV_A4, RV_A5}; + RvArgMove moves[7]; + for (u32 i = 0; i < narg; ++i) { + RvArgMove* m = &moves[i]; + memset(m, 0, sizeof *m); + m->dst = + native_loc_reg(dsts[0].type, NATIVE_REG_INT, syscall_regs[i]); + m->src = args[i]; + m->size = t->c->target.ptr_size; + } + rv_emit_reg_arg_moves(t, moves, narg); + rv64_emit32(mc, rv_ecall()); + rv_move(t, dsts[0], native_loc_reg(dsts[0].type, NATIVE_REG_INT, RV_A0)); + } + return; case INTRIN_BSWAP: { u32 width = abi_cg_sizeof(t->c->abi, dsts[0].type); switch (width) { diff --git a/src/arch/wasm/emit.c b/src/arch/wasm/emit.c @@ -1589,6 +1589,8 @@ static const char* intrin_name(IntrinKind k) { return "__builtin_expect"; case INTRIN_TRAP: return "__builtin_trap"; + case INTRIN_SYSCALL: + return "__kit_syscall"; case INTRIN_SETJMP: return "setjmp"; case INTRIN_LONGJMP: @@ -1806,6 +1808,7 @@ void wasm_intrinsic(CGTarget* tg, IntrinKind k, Operand* dst, u32 ndst, case INTRIN_IRQ_RESTORE: case INTRIN_IRQ_ENABLE: case INTRIN_IRQ_DISABLE: + case INTRIN_SYSCALL: /* No frame-pointer chain in wasm; reported unsupported up front. */ case INTRIN_FRAME_ADDRESS: case INTRIN_RETURN_ADDRESS: diff --git a/src/arch/x64/arch.c b/src/arch/x64/arch.c @@ -162,7 +162,6 @@ static int x64_supports_call_conv(const Compiler* c, KitCgCallConv cc) { /* Capability twin of x64_intrinsic (src/arch/x64/native.c); keep the two in * sync. No default case, so a new KitCgIntrinsic trips -Wswitch here. */ static int x64_supports_intrinsic(const Compiler* c, KitCgIntrinsic intrin) { - (void)c; switch (intrin) { case KIT_CG_INTRIN_TRAP: case KIT_CG_INTRIN_CLZ: @@ -187,10 +186,12 @@ static int x64_supports_intrinsic(const Compiler* c, KitCgIntrinsic intrin) { case KIT_CG_INTRIN_FRAME_ADDRESS: case KIT_CG_INTRIN_RETURN_ADDRESS: return 1; + case KIT_CG_INTRIN_SYSCALL: + return c->target.os == KIT_OS_LINUX || + c->target.os == KIT_OS_FREESTANDING; case KIT_CG_INTRIN_SETJMP: case KIT_CG_INTRIN_LONGJMP: case KIT_CG_INTRIN_FMA: - case KIT_CG_INTRIN_SYSCALL: case KIT_CG_INTRIN_IRQ_SAVE: case KIT_CG_INTRIN_IRQ_RESTORE: case KIT_CG_INTRIN_ISB: diff --git a/src/arch/x64/native.c b/src/arch/x64/native.c @@ -3356,6 +3356,11 @@ static void emit_ud2(MCEmitter* mc) { mc->emit_bytes(mc, b, 2); } +static void emit_syscall(MCEmitter* mc) { + u8 b[2] = {0x0F, 0x05}; + mc->emit_bytes(mc, b, 2); +} + static void x64_intrinsic(NativeTarget* t, IntrinKind kind, const NativeLoc* dsts, u32 ndst, const NativeLoc* args, u32 narg) { @@ -3378,6 +3383,25 @@ static void x64_intrinsic(NativeTarget* t, IntrinKind kind, case INTRIN_TRAP: emit_ud2(mc); return; + case INTRIN_SYSCALL: + if (ndst == 1u && narg >= 1u && narg <= 7u) { + static const u32 syscall_regs[7] = { + X64_RAX, X64_RDI, X64_RSI, X64_RDX, X64_R10, X64_R8, X64_R9}; + X64ArgMove moves[7]; + for (u32 i = 0; i < narg; ++i) { + X64ArgMove* m = &moves[i]; + memset(m, 0, sizeof *m); + m->dst = native_loc_reg(dsts[0].type, NATIVE_REG_INT, + syscall_regs[i]); + m->src = args[i]; + m->size = t->c->target.ptr_size; + } + x64_emit_reg_arg_moves(t, moves, narg, X64_TMP_INT2); + emit_syscall(mc); + x64_move(t, dsts[0], + native_loc_reg(dsts[0].type, NATIVE_REG_INT, X64_RAX)); + } + return; case INTRIN_POPCOUNT: emit_popcnt(mc, x64_is_64(t, args[0].type) ? 1 : 0, loc_reg(dsts[0]), loc_reg(args[0])); @@ -4089,12 +4113,20 @@ static int x64_machine_op_clobbers(NativeTarget* t, const NativeMachineOp* op, case NATIVE_MOP_INTRINSIC: /* The unsigned multiply-overflow intrinsic emits a one-operand MUL, whose * rdx:rax product clobbers both registers. The signed variant uses a - * two-operand IMUL (no fixed-register clobber); other intrinsics keep to - * the reserved emit scratch. */ + * two-operand IMUL (no fixed-register clobber). Linux syscall writes rax + * and the CPU instruction itself clobbers rcx/r11; the kernel ABI treats + * the integer caller-saved syscall registers as volatile. */ if ((IntrinKind)op->intrin == INTRIN_UMUL_OVERFLOW) { mask[NATIVE_REG_INT] = (1u << X64_RAX) | (1u << X64_RDX); return 1; } + if ((IntrinKind)op->intrin == INTRIN_SYSCALL) { + mask[NATIVE_REG_INT] = + (1u << X64_RAX) | (1u << X64_RCX) | (1u << X64_RDX) | + (1u << X64_RSI) | (1u << X64_RDI) | (1u << X64_R8) | + (1u << X64_R9) | (1u << X64_R10) | (1u << X64_R11); + return 1; + } return 0; default: return 0; diff --git a/src/cg/arith.c b/src/cg/arith.c @@ -1712,7 +1712,7 @@ void kit_cg_float_to_uint(KitCg* g, KitCgTypeId dst, KitCgRounding rounding) { /* One descriptor per KitCgIntrinsic, indexed by the enum value. The four * accessors below are field reads off this single source of truth; unmapped - * intrinsics (FMA/SYSCALL/cache/coro) use an INTRIN_NONE row. The table is laid + * intrinsics (FMA/cache/coro) use an INTRIN_NONE row. The table is laid * out in enum order; the _Static_assert guards its length so a new enumerator * is a compile error rather than a silently truncated index. */ typedef struct IntrinDesc { @@ -1747,7 +1747,7 @@ static const IntrinDesc kIntrinTable[] = { [KIT_CG_INTRIN_EXPECT] = {INTRIN_EXPECT, "expect", false, false}, [KIT_CG_INTRIN_ASSUME_ALIGNED] = {INTRIN_ASSUME_ALIGNED, "assume_aligned", false, false}, - [KIT_CG_INTRIN_SYSCALL] = {INTRIN_NONE, "syscall", false, false}, + [KIT_CG_INTRIN_SYSCALL] = {INTRIN_SYSCALL, "syscall", false, false}, [KIT_CG_INTRIN_IRQ_SAVE] = {INTRIN_IRQ_SAVE, "irq_save", false, false}, [KIT_CG_INTRIN_IRQ_RESTORE] = {INTRIN_IRQ_RESTORE, "irq_restore", true, false}, diff --git a/src/cg/cgtarget.h b/src/cg/cgtarget.h @@ -147,6 +147,10 @@ typedef enum IntrinKind { INTRIN_EXPECT, INTRIN_TRAP, + /* OS trap: args[0] is the syscall number, args[1..6] are integer/pointer + * payloads; dsts[0] receives the target long result. */ + INTRIN_SYSCALL, + /* non-local control */ INTRIN_SETJMP, INTRIN_LONGJMP, diff --git a/src/opt/pass_machinize.c b/src/opt/pass_machinize.c @@ -142,8 +142,8 @@ static void machinize_check_overlap(Func* f) { * clobbers as a side effect (x86 idiv → rax/rdx, variable shift → cl, atomics, * va_arg), so the allocator keeps values live across them out of those * registers. The target reports this through machine_op_clobbers; a NULL hook - * (aa64/rv64) means no instruction has fixed-register clobbers and the side - * table stays empty. */ + * means no instruction has fixed-register clobbers and the side table stays + * empty. */ static void machinize_inst_clobbers(Func* f, NativeTarget* target) { if (!target->machine_op_clobbers || !f->next_inst_id) return; for (u32 b = 0; b < f->nblocks; ++b) { diff --git a/test/parse/CORPUS.md b/test/parse/CORPUS.md @@ -565,7 +565,6 @@ ordinary calls. | `builtin_24_atomic_lock_free` | ★ | target-aware lock-free folding through `if`, `&&`, and `||`; dead 16-byte atomic arms suppress codegen | 42 | | `builtin_25_atomic_fetch_nand` | ★ | `__atomic_fetch_nand` lowers to atomic NAND RMW | 42 | | `builtin_26_sadd_overflow` | ★ | signed/unsigned typed overflow builtins store result and return overflow flag | 42 | -| `builtin_99_syscall0` | (deferred) | `__kit_syscall0` requires linking against the syscall stub; covered in `test/libc` | — | ## Variadic coverage diff --git a/test/toy/cases/144_intrinsic_capability_query.toy b/test/toy/cases/144_intrinsic_capability_query.toy @@ -7,11 +7,13 @@ fn __user_main(): i64 { let yield_ok: bool = @supports_intrinsic(.cpu_yield); let dmb_ok: bool = @supports_intrinsic(.dmb); let dsb_ok: bool = @supports_intrinsic(.dsb); - // syscall/coro_switch have no native lowering yet on any backend. - let syscall_unsupported: bool = !@supports_intrinsic(.syscall); + // syscall support is target/OS-specific; this case only checks that the + // query folds to a boolean without requiring a particular answer. + let syscall_known: bool = + @supports_intrinsic(.syscall) or !@supports_intrinsic(.syscall); let coro_unsupported: bool = !@supports_intrinsic(.coro_switch); if nop_ok and yield_ok and dmb_ok and dsb_ok and - syscall_unsupported and coro_unsupported { + syscall_known and coro_unsupported { return 42; } return 1; diff --git a/test/toy/err/bad_syscall_arity.expected b/test/toy/err/bad_syscall_arity.expected @@ -0,0 +1 @@ +too many syscall arguments diff --git a/test/toy/err/bad_syscall_arity.toy b/test/toy/err/bad_syscall_arity.toy @@ -0,0 +1,3 @@ +fn main(): i64 { + return @syscall(1, 0, 0, 0, 0, 0, 0, 0); +} diff --git a/test/toy/err/unsupported_syscall.expected b/test/toy/err/unsupported_syscall.expected @@ -1 +0,0 @@ -unsupported target intrinsic diff --git a/test/toy/err/unsupported_syscall.toy b/test/toy/err/unsupported_syscall.toy @@ -1,3 +0,0 @@ -fn main(): i64 { - return @syscall(1, 0); -}