/* * nwc, a NanoWasm compiler * Copyright (C) 2025 Xavier Del Campo Romero * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ #include "instr.h" #include "types.h" #include #include #include #include #include #include #include struct imm { enum {IMM_VARUINT32, IMM_TABLE} type; union { varuint32 varuint32; struct imm_table { varuint32 target_count, *targets; } table; } u; }; static void free_imm(struct imm *const imm) { switch (imm->type) { case IMM_VARUINT32: break; case IMM_TABLE: { struct imm_table *const t = &imm->u.table; free(t->targets); } break; } } static int imm_none(FILE *const f, struct imm *const imm) { return 0; } static int instr_sig(FILE *const f, struct imm *const imm) { return read_varint7(f, &(varint7){0}); } static int imm_readvaruint1(FILE *const f, struct imm *const imm) { return read_varuint1(f, &(varuint1){0}); } static int imm_varuint32(FILE *const f, struct imm *const imm) { imm->type = IMM_VARUINT32; return read_varuint32(f, &imm->u.varuint32); } static int imm_varuint64(FILE *const f, struct imm *const imm) { return read_varuint64(f, &(varuint64){0}); } static int imm_varint32(FILE *const f, struct imm *const imm) { return read_varint32(f, &(varint32){0}); } static int imm_varint64(FILE *const f, struct imm *const imm) { return read_varint64(f, &(varint64){0}); } static int imm_memory(FILE *const f, struct imm *const imm) { return read_varuint32(f, &(varuint32){0}) || read_varuint32(f, &(varuint32){0}); } static int imm_uint32(FILE *const f, struct imm *const imm) { uint32_t v; return !fread(&v, sizeof v, 1, f); } static int imm_uint64(FILE *const f, struct imm *const imm) { uint64_t v; return !fread(&v, sizeof v, 1, f); } static int imm_table(FILE *const f, struct imm *const imm) { struct imm table = {.type = IMM_TABLE}; struct imm_table *const t = &table.u.table; if (read_varuint32(f, &t->target_count)) { fprintf(stderr, "%s: read_varuint32 failed\n", __func__); goto failure; } else if (!(t->targets = malloc((t->target_count + 1) * sizeof *t->targets))) { fprintf(stderr, "%s: malloc(3): %s\n", __func__, strerror(errno)); goto failure; } for (varuint32 i = 0; i < (t->target_count + 1); i++) if (read_varuint32(f, &t->targets[i])) { fprintf(stderr, "%s: read_varuint32 %lu failed\n", __func__, (unsigned long)i); goto failure; } *imm = table; return 0; failure: free_imm(&table); return -1; } static int imm_call_indirect(FILE *const f, struct imm *const imm) { varuint32 reserved; if (read_varuint32(f, &(varuint32){0})) { fprintf(stderr, "%s: read_varuint32 failed\n", __func__); return -1; } else if (read_varuint32(f, &reserved)) { fprintf(stderr, "%s: read_varuint1 failed\n", __func__); return -1; } else if (reserved) { fprintf(stderr, "%s: expected zero for reserved field\n", __func__); return -1; } return 0; } static int imm_extra(FILE *const f, struct imm *const imm) { uint8_t b; return !fread(&b, sizeof b, 1, f); } #define OPS \ X(OP_UNREACHABLE, imm_none) \ X(OP_NOP, imm_none) \ X(OP_BLOCK, instr_sig) \ X(OP_LOOP, instr_sig) \ X(OP_IF, instr_sig) \ X(OP_ELSE, imm_none) \ X(OP_END, imm_none) \ X(OP_BR, imm_varuint32) \ X(OP_BR_IF, imm_varuint32) \ X(OP_BR_TABLE, imm_table) \ X(OP_RETURN, imm_none) \ X(OP_CALL, imm_varuint32) \ X(OP_CALL_INDIRECT, imm_call_indirect) \ X(OP_DROP, imm_none) \ X(OP_SELECT, imm_none) \ X(OP_GET_LOCAL, imm_varuint32) \ X(OP_SET_LOCAL, imm_varuint32) \ X(OP_TEE_LOCAL, imm_varuint32) \ X(OP_GET_GLOBAL, imm_varuint32) \ X(OP_SET_GLOBAL, imm_varuint32) \ X(OP_I32_LOAD, imm_memory) \ X(OP_I64_LOAD, imm_memory) \ X(OP_F32_LOAD, imm_memory) \ X(OP_F64_LOAD, imm_memory) \ X(OP_I32_LOAD8_S, imm_memory) \ X(OP_I32_LOAD8_U, imm_memory) \ X(OP_I32_LOAD16_S, imm_memory) \ X(OP_I32_LOAD16_U, imm_memory) \ X(OP_I64_LOAD8_S, imm_memory) \ X(OP_I64_LOAD8_U, imm_memory) \ X(OP_I64_LOAD16_S, imm_memory) \ X(OP_I64_LOAD16_U, imm_memory) \ X(OP_I64_LOAD32_S, imm_memory) \ X(OP_I64_LOAD32_U, imm_memory) \ X(OP_I32_STORE, imm_memory) \ X(OP_I64_STORE, imm_memory) \ X(OP_F32_STORE, imm_memory) \ X(OP_F64_STORE, imm_memory) \ X(OP_I32_STORE8, imm_memory) \ X(OP_I32_STORE16, imm_memory) \ X(OP_I64_STORE8, imm_memory) \ X(OP_I64_STORE16, imm_memory) \ X(OP_I64_STORE132, imm_memory) \ X(OP_CURRENT_MEMORY, imm_readvaruint1) \ X(OP_GROW_MEMORY, imm_readvaruint1) \ X(OP_I32_CONST, imm_varint32) \ X(OP_I64_CONST, imm_varint64) \ X(OP_F32_CONST, imm_uint32) \ X(OP_F64_CONST, imm_uint64) \ X(OP_I32_EQZ, imm_none) \ X(OP_I32_EQ, imm_none) \ X(OP_I32_NE, imm_none) \ X(OP_I32_LT_S, imm_none) \ X(OP_I32_LT_U, imm_none) \ X(OP_I32_GT_S, imm_none) \ X(OP_I32_GT_U, imm_none) \ X(OP_I32_LE_S, imm_none) \ X(OP_I32_LE_U, imm_none) \ X(OP_I32_GE_S, imm_none) \ X(OP_I32_GE_U, imm_none) \ X(OP_I64_EQZ, imm_none) \ X(OP_I64_EQ, imm_none) \ X(OP_I64_NE, imm_none) \ X(OP_I64_LT_S, imm_none) \ X(OP_I64_LT_U, imm_none) \ X(OP_I64_GT_S, imm_none) \ X(OP_I64_GT_U, imm_none) \ X(OP_I64_LE_S, imm_none) \ X(OP_I64_LE_U, imm_none) \ X(OP_I64_GE_S, imm_none) \ X(OP_I64_GE_U, imm_none) \ X(OP_F32_EQ, imm_none) \ X(OP_F32_NE, imm_none) \ X(OP_F32_LT, imm_none) \ X(OP_F32_GT, imm_none) \ X(OP_F32_LE, imm_none) \ X(OP_F32_GE, imm_none) \ X(OP_F64_EQ, imm_none) \ X(OP_F64_NE, imm_none) \ X(OP_F64_LT, imm_none) \ X(OP_F64_GT, imm_none) \ X(OP_F64_LE, imm_none) \ X(OP_F64_GE, imm_none) \ X(OP_I32_CLZ, imm_none) \ X(OP_I32_CTZ, imm_none) \ X(OP_I32_POPCNT, imm_none) \ X(OP_I32_ADD, imm_none) \ X(OP_I32_SUB, imm_none) \ X(OP_I32_MUL, imm_none) \ X(OP_I32_DIV_S, imm_none) \ X(OP_I32_DIV_U, imm_none) \ X(OP_I32_REM_S, imm_none) \ X(OP_I32_REM_U, imm_none) \ X(OP_I32_AND, imm_none) \ X(OP_I32_OR, imm_none) \ X(OP_I32_XOR, imm_none) \ X(OP_I32_SHL, imm_none) \ X(OP_I32_SHR_S, imm_none) \ X(OP_I32_SHR_U, imm_none) \ X(OP_I32_ROTL, imm_none) \ X(OP_I32_ROTR, imm_none) \ X(OP_I64_CLZ, imm_none) \ X(OP_I64_CTZ, imm_none) \ X(OP_I64_POPCNT, imm_none) \ X(OP_I64_ADD, imm_none) \ X(OP_I64_SUB, imm_none) \ X(OP_I64_MUL, imm_none) \ X(OP_I64_DIV_S, imm_none) \ X(OP_I64_DIV_U, imm_none) \ X(OP_I64_REM_S, imm_none) \ X(OP_I64_REM_U, imm_none) \ X(OP_I64_AND, imm_none) \ X(OP_I64_OR, imm_none) \ X(OP_I64_XOR, imm_none) \ X(OP_I64_SHL, imm_none) \ X(OP_I64_SHR_S, imm_none) \ X(OP_I64_SHR_U, imm_none) \ X(OP_I64_ROTL, imm_none) \ X(OP_I64_ROTR, imm_none) \ X(OP_F32_ABS, imm_none) \ X(OP_F32_NEG, imm_none) \ X(OP_F32_CEIL, imm_none) \ X(OP_F32_FLOOR, imm_none) \ X(OP_F32_TRUNC, imm_none) \ X(OP_F32_NEAREST, imm_none) \ X(OP_F32_SQRT, imm_none) \ X(OP_F32_ADD, imm_none) \ X(OP_F32_SUB, imm_none) \ X(OP_F32_MUL, imm_none) \ X(OP_F32_DIV, imm_none) \ X(OP_F32_MIN, imm_none) \ X(OP_F32_MAX, imm_none) \ X(OP_F32_COPYSIGN, imm_none) \ X(OP_F64_ABS, imm_none) \ X(OP_F64_NEG, imm_none) \ X(OP_F64_CEIL, imm_none) \ X(OP_F64_FLOOR, imm_none) \ X(OP_F64_TRUNC, imm_none) \ X(OP_F64_NEAREST, imm_none) \ X(OP_F64_SQRT, imm_none) \ X(OP_F64_ADD, imm_none) \ X(OP_F64_SUB, imm_none) \ X(OP_F64_MUL, imm_none) \ X(OP_F64_DIV, imm_none) \ X(OP_F64_MIN, imm_none) \ X(OP_F64_MAX, imm_none) \ X(OP_F64_COPYSIGN, imm_none) \ X(OP_I32_WRAP_I64, imm_none) \ X(OP_I32_TRUNC_S_F32, imm_none) \ X(OP_I32_TRUNC_U_F32, imm_none) \ X(OP_I32_TRUNC_S_F64, imm_none) \ X(OP_I32_TRUNC_U_F64, imm_none) \ X(OP_I64_EXTEND_S_I32, imm_none) \ X(OP_I64_EXTEND_U_I32, imm_none) \ X(OP_I64_TRUNC_S_F32, imm_none) \ X(OP_I64_TRUNC_U_F32, imm_none) \ X(OP_I64_TRUNC_S_F64, imm_none) \ X(OP_I64_TRUNC_U_F64, imm_none) \ X(OP_F32_CONVERT_S_I32, imm_none) \ X(OP_F32_CONVERT_U_I32, imm_none) \ X(OP_F32_CONVERT_S_I64, imm_none) \ X(OP_F32_CONVERT_U_I64, imm_none) \ X(OP_F32_DEMOTE_F64, imm_none) \ X(OP_F64_CONVERT_S_I32, imm_none) \ X(OP_F64_CONVERT_U_I32, imm_none) \ X(OP_F64_CONVERT_S_I64, imm_none) \ X(OP_F64_CONVERT_U_I64, imm_none) \ X(OP_F64_PROMOTE_F32, imm_none) \ X(OP_I32_REINTERPRET_F32, imm_none) \ X(OP_I64_REINTERPRET_F64, imm_none) \ X(OP_F32_REINTERPRET_I32, imm_none) \ X(OP_F64_REINTERPRET_I64, imm_none) \ X(OP_EXTRA, imm_extra) static int (*const table[])(FILE *, struct imm *) = { #define X(x, y) [x] = y, OPS #undef X }; static const char *const op_str[] = { #define X(x, y) [x] = #x, OPS #undef X }; static bool scoped(const uint8_t op) { switch (op) { case OP_BLOCK: case OP_LOOP: case OP_IF: return true; default: break; } return false; } static bool branch(const uint8_t op) { switch (op) { case OP_BR: case OP_BR_IF: return true; default: break; } return false; } static bool cbranch(const uint8_t op) { switch (op) { case OP_IF: case OP_ELSE: return true; default: break; } return false; } struct block { uint8_t op; unsigned level; long pc; struct block *next, *prev; }; struct blocks { struct block *head, *tail; }; static int append_lo(const struct lo *const lo, struct parse *const p) { const size_t n = p->n_lo + 1; struct lo *const o = realloc(p->lo, n * sizeof *p->lo); if (!o) { fprintf(stderr, "%s: realloc(3): %s\n", __func__, strerror(errno)); return -1; } o[p->n_lo++] = *lo; p->lo = o; return 0; } static int add_block(struct blocks *const b, const uint8_t op, const unsigned level, FILE *const f) { const long offset = ftell(f); if (offset < 0) { fprintf(stderr, "%s: ftell(3): %s\n", __func__, strerror(errno)); return -1; } struct block *const v = malloc(sizeof *v); if (!v) { fprintf(stderr, "%s: realloc(3): %s\n", __func__, strerror(errno)); return -1; } *v = (const struct block) { .op = op, .level = level, .pc = offset, .prev = b->tail }; if (!b->head) b->head = v; else b->tail->next = v; b->tail = v; return 0; } static int add_branch(const uint8_t op, struct lo *const lo, const struct blocks *const b, const varuint32 relative_depth, const unsigned level, FILE *const f) { if (relative_depth >= level) { fprintf(stderr, "%s: invalid relative_depth=%lu, current level=%u\n", __func__, (unsigned long)relative_depth, level); return -1; } const long offset = ftell(f); if (offset < 0) { fprintf(stderr, "%s: ftell(3): %s\n", __func__, strerror(errno)); return -1; } const struct block *tgt = NULL; for (const struct block *bl = b->tail; bl; bl = bl->prev) if (bl->level == level - relative_depth) { tgt = bl; break; } if (!tgt) { fprintf(stderr, "%s: could not find matching block, pc=%ld\n", __func__, offset); return -1; } const size_t n = lo->n + 1; struct lo_entry *const ve = realloc(lo->entries, n * sizeof *lo->entries); if (!ve) { fprintf(stderr, "%s: realloc(3): %s\n", __func__, strerror(errno)); return -1; } struct lo_entry *const e = &ve[lo->n++]; *e = (const struct lo_entry){.op = op, .b = tgt, .pc = offset}; if (tgt->op == OP_LOOP) e->dst = tgt->pc; lo->entries = ve; return 0; } static int add_cbranch(const uint8_t op, struct lo *const lo, const struct blocks *const b, const unsigned level, FILE *const f) { const long offset = ftell(f); if (offset < 0) { fprintf(stderr, "%s: ftell(3): %s\n", __func__, strerror(errno)); return -1; } const struct block *tgt = NULL; for (const struct block *bl = b->tail; bl; bl = bl->prev) if (bl->op == OP_IF && bl->level == level) { tgt = bl; break; } if (!tgt) { fprintf(stderr, "%s: could not find matching block, pc=%ld\n", __func__, offset); return -1; } const size_t n = lo->n + 1; struct lo_entry *const ve = realloc(lo->entries, n * sizeof *lo->entries); if (!ve) { fprintf(stderr, "%s: realloc(3): %s\n", __func__, strerror(errno)); return -1; } struct lo_entry *const e = &ve[lo->n++]; *e = (const struct lo_entry){.op = op, .b = tgt, .pc = offset}; lo->entries = ve; return 0; } static int add_brtable(struct lo *const lo, const struct blocks *const b, const struct imm_table *const t, const unsigned level, FILE *const f) { for (varuint32 i = 0; i < (t->target_count + 1); i++) if (add_branch(OP_BR_TABLE, lo, b, t->targets[i], level, f)) { fprintf(stderr, "%s: add_branch failed\n", __func__); return -1; } return 0; } static int set_end(struct lo *const lo, const struct blocks *const b, const unsigned level, FILE *const f) { long offset = ftell(f); if (offset < 0) { fprintf(stderr, "%s: ftell(3): %s\n", __func__, strerror(errno)); return -1; } offset -= sizeof b->head->op; const struct block *tgt = NULL; for (const struct block *bl = b->tail; bl; bl = bl->prev) if (bl->level == level) { tgt = bl; break; } if (!tgt) { fprintf(stderr, "%s: could not find matching block for end, pc=%ld\n", __func__, offset); return -1; } for (size_t i = 0; i < lo->n; i++) { struct lo_entry *const e = &lo->entries[i]; if (e->b == tgt) { if (tgt->op == OP_BLOCK) e->dst = offset; else if (tgt->op == OP_IF && (e->op == OP_ELSE || (e->op == OP_IF && !e->dst))) e->dst = offset; } } return 0; } static int set_else(struct lo *const lo, const struct blocks *const b, const unsigned level, FILE *const f) { long offset = ftell(f); if (offset < 0) { fprintf(stderr, "%s: ftell(3): %s\n", __func__, strerror(errno)); return -1; } const struct block *tgt = NULL; for (const struct block *bl = b->tail; bl; bl = bl->prev) if (bl->level == level && bl->op == OP_IF) { tgt = bl; break; } if (!tgt) { fprintf(stderr, "%s: could not find matching block for end, pc=%ld\n", __func__, offset); return -1; } for (size_t i = 0; i < lo->n; i++) { struct lo_entry *const e = &lo->entries[i]; if (e->b == tgt && e->op == OP_IF) e->dst = offset; } return 0; } static void free_blocks(struct blocks *const b) { for (struct block *bl = b->head; bl;) { struct block *next = bl->next; free(bl); bl = next; } } static int read_one(struct lo *const lo, struct blocks *const b, unsigned *const level, FILE *const f) { int ret = -1; size_t n; uint8_t op; struct imm imm = {0}; const long offset = ftell(f); if (offset < 0) { fprintf(stderr, "%s: ftell(3): %s\n", __func__, strerror(errno)); goto end; } else if (!(n = fread(&op, sizeof op, 1, f))) { fprintf(stderr, "%s: fread(3) failed, ferror(3)=%d, feof(3)=%d\n", __func__, ferror(f), feof(f)); goto end; } else if (op >= sizeof table / sizeof *table || !table[op]) { fprintf(stderr, "%s: invalid or unsupported opcode %#" PRIx8 ", " "offset=%#lx\n", __func__, op, offset); goto end; } else if (table[op](f, &imm)) { fprintf(stderr, "%s: failed to read opcode %#" PRIx8 "\n", __func__, op); goto end; } else if (scoped(op) && add_block(b, op, ++(*level), f)) { fprintf(stderr, "%s: add_block failed\n", __func__); goto end; } else if (branch(op) && add_branch(op, lo, b, imm.u.varuint32, *level, f)) { fprintf(stderr, "%s: add_branch failed\n", __func__); goto end; } else if (cbranch(op) && add_cbranch(op, lo, b, *level, f)) { fprintf(stderr, "%s: add_cbranch failed\n", __func__); goto end; } else if (op == OP_BR_TABLE && add_brtable(lo, b, &imm.u.table, *level, f)) { fprintf(stderr, "%s: add_brtable failed\n", __func__); goto end; } else if (op == OP_ELSE && set_else(lo, b, *level, f)) { fprintf(stderr, "%s: set_else failed\n", __func__); goto end; } else if (op == OP_END) { if (*level) { if (set_end(lo, b, *level, f)) { fprintf(stderr, "%s: set_end failed\n", __func__); goto end; } (*level)--; } else { ret = 1; goto end; } } ret = 0; end: free_imm(&imm); return ret; } int read_instr(FILE *const f, struct parse *const p) { unsigned level = 0; struct lo lo = {0}; struct blocks b = {0}; for (;;) { const int n = read_one(&lo, &b, &level, f); if (n < 0) goto failure; else if (n) break; } if (append_lo(&lo, p)) { fprintf(stderr, "%s: append_lo failed\n", __func__); goto failure; } free_blocks(&b); return 0; failure: free(lo.entries); free_blocks(&b); return -1; }