Commitbdac740fRecorded15 Jan 2026Repositorysigil-app
docs: Add computed goto experimental results to JIT design notes
Message
Documents the findings from the feature/computed-goto branch: - Only ~1-2% improvement on modern CPUs with good branch predictors - GCC already generates efficient jump tables for switch statements - Added complexity (pragma warnings, broken fallback) not justified
The branch is preserved for future reference.
Changed
notes/design/jit-support.md | 1405 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 1405 insertions(+)Diff
notes/design/jit-support.mdadded
@@ -0,0 +1,1405 @@
+1
# JIT Compilation Support for Sigil+2
+3
This document describes the design for optional JIT (Just-In-Time) compilation support in Sigil. The goal is to provide meaningful performance improvements for compute-intensive code while maintaining Sigil's zero-dependency philosophy.+4
+5
## Design Goals+6
+7
1. **No external dependencies** - JIT compiles with only a C compiler, no LLVM/DynASM/etc.+8
2. **Optional feature** - Sigil works fine without JIT; it's an optimization, not a requirement+9
3. **Incremental adoption** - Start with x86-64, add ARM64 later+10
4. **Graceful fallback** - Unknown patterns fall back to interpreter+11
5. **Minimal complexity** - Target 2-5x speedup for numeric code, not 20x+12
+13
## Architecture Overview+14
+15
```+16
┌─────────────────────────────────────────────────────────────┐+17
│ Sigil VM │+18
├─────────────────────────────────────────────────────────────┤+19
│ Interpreter (vm.c) │+20
│ │ │+21
│ ├─→ Profile call counts │+22
│ │ │+23
│ └─→ Hot function detected (>N calls) │+24
│ │ │+25
│ ▼ │+26
│ ┌─────────────────┐ ┌─────────────────┐ │+27
│ │ JIT Compiler │───→│ Code Buffer │ │+28
│ │ (jit.c) │ │ (executable) │ │+29
│ └─────────────────┘ └─────────────────┘ │+30
│ │ │ │+31
│ ▼ ▼ │+32
│ ┌─────────────────┐ ┌─────────────────┐ │+33
│ │ x86-64 Encoder │ │ Native Code │ │+34
│ │ (jit-x64.c) │ │ Execution │ │+35
│ └─────────────────┘ └─────────────────┘ │+36
└─────────────────────────────────────────────────────────────┘+37
```+38
+39
### Components+40
+41
| File | Purpose |+42
|------|---------|+43
| `jit.h` | Public JIT API and data structures |+44
| `jit.c` | JIT compiler: bytecode → IR → native |+45
| `jit-x64.c` | x86-64 instruction encoder |+46
| `jit-arm64.c` | ARM64 instruction encoder (future) |+47
+48
## Code Buffer Management+49
+50
The code buffer handles executable memory allocation:+51
+52
```c+53
// jit.h+54
+55
typedef struct {+56
uint8_t *base; // Start of allocated region+57
uint8_t *ptr; // Current write position+58
uint8_t *limit; // End of allocated region+59
size_t capacity; // Total size+60
} JitCodeBuf;+61
+62
// Allocate executable memory+63
JitCodeBuf *jit_codebuf_create(size_t initial_size);+64
+65
// Free executable memory+66
void jit_codebuf_destroy(JitCodeBuf *buf);+67
+68
// Ensure space for n bytes, grow if needed+69
bool jit_codebuf_ensure(JitCodeBuf *buf, size_t n);+70
+71
// Get current code size+72
static inline size_t jit_codebuf_size(JitCodeBuf *buf) {+73
return buf->ptr - buf->base;+74
}+75
+76
// Get function pointer to start of buffer+77
static inline void *jit_codebuf_fn(JitCodeBuf *buf) {+78
return (void *)buf->base;+79
}+80
```+81
+82
### Platform-Specific Implementation+83
+84
```c+85
// jit.c+86
+87
#if defined(__linux__) || defined(__FreeBSD__) || defined(__OpenBSD__)+88
#include <sys/mman.h>+89
+90
JitCodeBuf *jit_codebuf_create(size_t size)+91
{+92
JitCodeBuf *buf = malloc(sizeof(JitCodeBuf));+93
if (!buf) return NULL;+94
+95
// Round up to page size+96
size_t page_size = sysconf(_SC_PAGESIZE);+97
size = (size + page_size - 1) & ~(page_size - 1);+98
+99
void *mem = mmap(NULL, size,+100
PROT_READ | PROT_WRITE | PROT_EXEC,+101
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);+102
+103
if (mem == MAP_FAILED) {+104
free(buf);+105
return NULL;+106
}+107
+108
buf->base = mem;+109
buf->ptr = mem;+110
buf->limit = (uint8_t *)mem + size;+111
buf->capacity = size;+112
+113
return buf;+114
}+115
+116
void jit_codebuf_destroy(JitCodeBuf *buf)+117
{+118
if (buf) {+119
munmap(buf->base, buf->capacity);+120
free(buf);+121
}+122
}+123
+124
#elif defined(__APPLE__)+125
#include <sys/mman.h>+126
#include <pthread.h>+127
+128
JitCodeBuf *jit_codebuf_create(size_t size)+129
{+130
JitCodeBuf *buf = malloc(sizeof(JitCodeBuf));+131
if (!buf) return NULL;+132
+133
size_t page_size = sysconf(_SC_PAGESIZE);+134
size = (size + page_size - 1) & ~(page_size - 1);+135
+136
// macOS requires MAP_JIT for Apple Silicon+137
void *mem = mmap(NULL, size,+138
PROT_READ | PROT_WRITE | PROT_EXEC,+139
MAP_PRIVATE | MAP_ANONYMOUS | MAP_JIT, -1, 0);+140
+141
if (mem == MAP_FAILED) {+142
free(buf);+143
return NULL;+144
}+145
+146
buf->base = mem;+147
buf->ptr = mem;+148
buf->limit = (uint8_t *)mem + size;+149
buf->capacity = size;+150
+151
return buf;+152
}+153
+154
// Apple Silicon requires toggling write protection+155
void jit_codebuf_begin_write(JitCodeBuf *buf)+156
{+157
#if defined(__aarch64__)+158
pthread_jit_write_protect_np(0); // Disable write protection+159
#endif+160
(void)buf;+161
}+162
+163
void jit_codebuf_end_write(JitCodeBuf *buf)+164
{+165
#if defined(__aarch64__)+166
pthread_jit_write_protect_np(1); // Re-enable write protection+167
sys_icache_invalidate(buf->base, jit_codebuf_size(buf));+168
#endif+169
(void)buf;+170
}+171
+172
#elif defined(_WIN32)+173
#include <windows.h>+174
+175
JitCodeBuf *jit_codebuf_create(size_t size)+176
{+177
JitCodeBuf *buf = malloc(sizeof(JitCodeBuf));+178
if (!buf) return NULL;+179
+180
SYSTEM_INFO si;+181
GetSystemInfo(&si);+182
size = (size + si.dwPageSize - 1) & ~(si.dwPageSize - 1);+183
+184
void *mem = VirtualAlloc(NULL, size,+185
MEM_COMMIT | MEM_RESERVE,+186
PAGE_EXECUTE_READWRITE);+187
+188
if (!mem) {+189
free(buf);+190
return NULL;+191
}+192
+193
buf->base = mem;+194
buf->ptr = mem;+195
buf->limit = (uint8_t *)mem + size;+196
buf->capacity = size;+197
+198
return buf;+199
}+200
+201
void jit_codebuf_destroy(JitCodeBuf *buf)+202
{+203
if (buf) {+204
VirtualFree(buf->base, 0, MEM_RELEASE);+205
free(buf);+206
}+207
}+208
+209
#endif+210
```+211
+212
## x86-64 Instruction Encoder+213
+214
### Register Definitions+215
+216
```c+217
// jit-x64.h+218
+219
typedef enum {+220
RAX = 0, RCX = 1, RDX = 2, RBX = 3,+221
RSP = 4, RBP = 5, RSI = 6, RDI = 7,+222
R8 = 8, R9 = 9, R10 = 10, R11 = 11,+223
R12 = 12, R13 = 13, R14 = 14, R15 = 15+224
} X64Reg;+225
+226
// Calling convention (System V AMD64 ABI)+227
// Arguments: RDI, RSI, RDX, RCX, R8, R9+228
// Return: RAX+229
// Callee-saved: RBX, RBP, R12-R15+230
// Caller-saved: RAX, RCX, RDX, RSI, RDI, R8-R11+231
+232
#define ARG1 RDI+233
#define ARG2 RSI+234
#define ARG3 RDX+235
#define ARG4 RCX+236
#define ARG5 R8+237
#define ARG6 R9+238
#define RET RAX+239
+240
// Scratch registers for JIT use+241
#define SCRATCH1 R10+242
#define SCRATCH2 R11+243
```+244
+245
### Encoding Helpers+246
+247
```c+248
// jit-x64.c+249
+250
// Emit raw bytes+251
static inline void emit_u8(JitCodeBuf *buf, uint8_t v)+252
{+253
*buf->ptr++ = v;+254
}+255
+256
static inline void emit_u16(JitCodeBuf *buf, uint16_t v)+257
{+258
memcpy(buf->ptr, &v, 2);+259
buf->ptr += 2;+260
}+261
+262
static inline void emit_u32(JitCodeBuf *buf, uint32_t v)+263
{+264
memcpy(buf->ptr, &v, 4);+265
buf->ptr += 4;+266
}+267
+268
static inline void emit_u64(JitCodeBuf *buf, uint64_t v)+269
{+270
memcpy(buf->ptr, &v, 8);+271
buf->ptr += 8;+272
}+273
+274
static inline void emit_i32(JitCodeBuf *buf, int32_t v)+275
{+276
memcpy(buf->ptr, &v, 4);+277
buf->ptr += 4;+278
}+279
+280
// REX prefix for 64-bit operations+281
// W=1 for 64-bit operand size+282
// R=1 if ModRM.reg is R8-R15+283
// X=1 if SIB.index is R8-R15+284
// B=1 if ModRM.rm or SIB.base is R8-R15+285
static inline uint8_t rex(bool w, bool r, bool x, bool b)+286
{+287
return 0x40 | (w << 3) | (r << 2) | (x << 1) | b;+288
}+289
+290
static inline uint8_t rex_wrb(X64Reg reg, X64Reg rm)+291
{+292
return rex(true, reg >= R8, false, rm >= R8);+293
}+294
+295
// ModRM byte: mod(2) | reg(3) | rm(3)+296
static inline uint8_t modrm(uint8_t mod, uint8_t reg, uint8_t rm)+297
{+298
return (mod << 6) | ((reg & 7) << 3) | (rm & 7);+299
}+300
+301
// ModRM for register-to-register+302
static inline uint8_t modrm_rr(X64Reg reg, X64Reg rm)+303
{+304
return modrm(0b11, reg & 7, rm & 7);+305
}+306
```+307
+308
### Core Instructions+309
+310
```c+311
// jit-x64.c+312
+313
// MOV reg, imm64+314
void x64_mov_reg_imm64(JitCodeBuf *buf, X64Reg dst, uint64_t imm)+315
{+316
emit_u8(buf, rex(true, false, false, dst >= R8));+317
emit_u8(buf, 0xb8 + (dst & 7)); // MOV r64, imm64+318
emit_u64(buf, imm);+319
}+320
+321
// MOV reg, imm32 (zero-extended to 64-bit)+322
void x64_mov_reg_imm32(JitCodeBuf *buf, X64Reg dst, uint32_t imm)+323
{+324
if (dst >= R8) {+325
emit_u8(buf, rex(false, false, false, true));+326
}+327
emit_u8(buf, 0xb8 + (dst & 7)); // MOV r32, imm32+328
emit_u32(buf, imm);+329
}+330
+331
// MOV reg, reg+332
void x64_mov_reg_reg(JitCodeBuf *buf, X64Reg dst, X64Reg src)+333
{+334
emit_u8(buf, rex_wrb(src, dst));+335
emit_u8(buf, 0x89); // MOV r/m64, r64+336
emit_u8(buf, modrm_rr(src, dst));+337
}+338
+339
// MOV reg, [reg + offset]+340
void x64_mov_reg_mem(JitCodeBuf *buf, X64Reg dst, X64Reg base, int32_t offset)+341
{+342
emit_u8(buf, rex_wrb(dst, base));+343
emit_u8(buf, 0x8b); // MOV r64, r/m64+344
+345
if (offset == 0 && (base & 7) != RBP) {+346
emit_u8(buf, modrm(0b00, dst & 7, base & 7));+347
if ((base & 7) == RSP) emit_u8(buf, 0x24); // SIB for RSP+348
} else if (offset >= -128 && offset <= 127) {+349
emit_u8(buf, modrm(0b01, dst & 7, base & 7));+350
if ((base & 7) == RSP) emit_u8(buf, 0x24);+351
emit_u8(buf, (uint8_t)offset);+352
} else {+353
emit_u8(buf, modrm(0b10, dst & 7, base & 7));+354
if ((base & 7) == RSP) emit_u8(buf, 0x24);+355
emit_i32(buf, offset);+356
}+357
}+358
+359
// MOV [reg + offset], reg+360
void x64_mov_mem_reg(JitCodeBuf *buf, X64Reg base, int32_t offset, X64Reg src)+361
{+362
emit_u8(buf, rex_wrb(src, base));+363
emit_u8(buf, 0x89); // MOV r/m64, r64+364
+365
if (offset == 0 && (base & 7) != RBP) {+366
emit_u8(buf, modrm(0b00, src & 7, base & 7));+367
if ((base & 7) == RSP) emit_u8(buf, 0x24);+368
} else if (offset >= -128 && offset <= 127) {+369
emit_u8(buf, modrm(0b01, src & 7, base & 7));+370
if ((base & 7) == RSP) emit_u8(buf, 0x24);+371
emit_u8(buf, (uint8_t)offset);+372
} else {+373
emit_u8(buf, modrm(0b10, src & 7, base & 7));+374
if ((base & 7) == RSP) emit_u8(buf, 0x24);+375
emit_i32(buf, offset);+376
}+377
}+378
+379
// ADD reg, reg+380
void x64_add_reg_reg(JitCodeBuf *buf, X64Reg dst, X64Reg src)+381
{+382
emit_u8(buf, rex_wrb(src, dst));+383
emit_u8(buf, 0x01); // ADD r/m64, r64+384
emit_u8(buf, modrm_rr(src, dst));+385
}+386
+387
// ADD reg, imm32+388
void x64_add_reg_imm32(JitCodeBuf *buf, X64Reg dst, int32_t imm)+389
{+390
emit_u8(buf, rex(true, false, false, dst >= R8));+391
if (imm >= -128 && imm <= 127) {+392
emit_u8(buf, 0x83); // ADD r/m64, imm8+393
emit_u8(buf, modrm(0b11, 0, dst & 7));+394
emit_u8(buf, (uint8_t)imm);+395
} else {+396
emit_u8(buf, 0x81); // ADD r/m64, imm32+397
emit_u8(buf, modrm(0b11, 0, dst & 7));+398
emit_i32(buf, imm);+399
}+400
}+401
+402
// SUB reg, reg+403
void x64_sub_reg_reg(JitCodeBuf *buf, X64Reg dst, X64Reg src)+404
{+405
emit_u8(buf, rex_wrb(src, dst));+406
emit_u8(buf, 0x29); // SUB r/m64, r64+407
emit_u8(buf, modrm_rr(src, dst));+408
}+409
+410
// SUB reg, imm32+411
void x64_sub_reg_imm32(JitCodeBuf *buf, X64Reg dst, int32_t imm)+412
{+413
emit_u8(buf, rex(true, false, false, dst >= R8));+414
if (imm >= -128 && imm <= 127) {+415
emit_u8(buf, 0x83); // SUB r/m64, imm8+416
emit_u8(buf, modrm(0b11, 5, dst & 7));+417
emit_u8(buf, (uint8_t)imm);+418
} else {+419
emit_u8(buf, 0x81); // SUB r/m64, imm32+420
emit_u8(buf, modrm(0b11, 5, dst & 7));+421
emit_i32(buf, imm);+422
}+423
}+424
+425
// IMUL reg, reg (signed multiply)+426
void x64_imul_reg_reg(JitCodeBuf *buf, X64Reg dst, X64Reg src)+427
{+428
emit_u8(buf, rex_wrb(dst, src));+429
emit_u8(buf, 0x0f);+430
emit_u8(buf, 0xaf); // IMUL r64, r/m64+431
emit_u8(buf, modrm_rr(dst, src));+432
}+433
+434
// CMP reg, reg+435
void x64_cmp_reg_reg(JitCodeBuf *buf, X64Reg a, X64Reg b)+436
{+437
emit_u8(buf, rex_wrb(b, a));+438
emit_u8(buf, 0x39); // CMP r/m64, r64+439
emit_u8(buf, modrm_rr(b, a));+440
}+441
+442
// CMP reg, imm32+443
void x64_cmp_reg_imm32(JitCodeBuf *buf, X64Reg reg, int32_t imm)+444
{+445
emit_u8(buf, rex(true, false, false, reg >= R8));+446
if (imm >= -128 && imm <= 127) {+447
emit_u8(buf, 0x83); // CMP r/m64, imm8+448
emit_u8(buf, modrm(0b11, 7, reg & 7));+449
emit_u8(buf, (uint8_t)imm);+450
} else {+451
emit_u8(buf, 0x81); // CMP r/m64, imm32+452
emit_u8(buf, modrm(0b11, 7, reg & 7));+453
emit_i32(buf, imm);+454
}+455
}+456
+457
// TEST reg, reg (for zero checks)+458
void x64_test_reg_reg(JitCodeBuf *buf, X64Reg a, X64Reg b)+459
{+460
emit_u8(buf, rex_wrb(b, a));+461
emit_u8(buf, 0x85); // TEST r/m64, r64+462
emit_u8(buf, modrm_rr(b, a));+463
}+464
+465
// AND reg, imm32+466
void x64_and_reg_imm32(JitCodeBuf *buf, X64Reg dst, int32_t imm)+467
{+468
emit_u8(buf, rex(true, false, false, dst >= R8));+469
if (imm >= -128 && imm <= 127) {+470
emit_u8(buf, 0x83);+471
emit_u8(buf, modrm(0b11, 4, dst & 7));+472
emit_u8(buf, (uint8_t)imm);+473
} else {+474
emit_u8(buf, 0x81);+475
emit_u8(buf, modrm(0b11, 4, dst & 7));+476
emit_i32(buf, imm);+477
}+478
}+479
+480
// OR reg, imm32+481
void x64_or_reg_imm32(JitCodeBuf *buf, X64Reg dst, int32_t imm)+482
{+483
emit_u8(buf, rex(true, false, false, dst >= R8));+484
if (imm >= -128 && imm <= 127) {+485
emit_u8(buf, 0x83);+486
emit_u8(buf, modrm(0b11, 1, dst & 7));+487
emit_u8(buf, (uint8_t)imm);+488
} else {+489
emit_u8(buf, 0x81);+490
emit_u8(buf, modrm(0b11, 1, dst & 7));+491
emit_i32(buf, imm);+492
}+493
}+494
+495
// SHR reg, imm8 (logical right shift)+496
void x64_shr_reg_imm8(JitCodeBuf *buf, X64Reg dst, uint8_t imm)+497
{+498
emit_u8(buf, rex(true, false, false, dst >= R8));+499
if (imm == 1) {Showing the first 500 of 1406 diff lines for this file. This diff is INCOMPLETE; read the file or clone the repository for the rest.