From 951617a407acc133e370cb46fd22746caa7edd8d Mon Sep 17 00:00:00 2001 From: Bruno Macabeus Date: Tue, 4 Aug 2026 22:59:52 +0100 Subject: [PATCH 1/4] feat(bench): one closed feature vocabulary, shared by the harness and the site MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Function Explorer's Feature selector is 89 options long with no explanation of any option, and the cause is not the UI: the closed vocabulary PR #16 introduced governs only the REAL tier, so the synthetic tier carries 48 undeclared tags of its own, the six derived codegen tags are published without ever having been added to KNOWN_FEATURES, and apps/web cannot import the descriptions at all — apps/benchmark is not one of its dependencies, so every definition written in #16 is unreachable from the UI that would render it. The vocabulary moves to @asmlift/bench-schema as data both sides read: 69 ids, each with a label, a user-facing `group`, an `evidence` kind, a summary, and — where it earns one — prose and a worked C/asm example. Two orthogonal axes on purpose: `evidence` is how we know a tag is true (a producer's question, but worth surfacing), `group` is what kind of thing it is (a reader's question, and what the picker groups by). src/cases/features.ts keeps every detector and regex but stops declaring the vocabulary: SOURCE_CHECKED, CODEGEN_DERIVED and JUDGEMENT become filters over FEATURES by `evidence`. Two lists that must agree become one list with two views. Dataset changes, all metadata — no outcome, score or gap moves: - hard renames, no aliases: s64→int64, for/while→loop, multi-if, nested-if, logical-and, logical-or→branch, type-pun→union, pointer-arith→pointer, this-ptr→method - the eight compound divisor ids flatten to div/mod-{const,pow2,reg}, with signedness carried by the existing signed/unsigned tags — six orthogonal ids instead of eight compound ones - dropped: expr, void, byte, offset, range, select, mutate (unfalsifiable or true of everything), c++ (duplicates FunctionResult.language) and recursion (declared by #16, carried by no row) - `cast` removed from six specs whose conversion is IMPLICIT — the floor caught them, and their sign-extend/narrow/promotion tags already said it Source-level tags now derive per row, as the codegen ones already did: SynthSpec carries `src`, so sourceEvidence works on both tiers. Authored data therefore carries JUDGEMENT tags only — the machine decides what it can decide. Gates, all hard failures: every published tag has a definition; every definition is carried by a row; definitions are well-formed with resolvable seeAlso; derived tags match their evidence in both directions; judgement tags stay above a floor, now including break/continue/double/rotate/mask and the divisor family. Each was verified to fail when violated, not merely to pass. Two floor fixes found while wiring this up: a type tag reads the whole declaration rather than the body (`double dadd(double,double)` keeps its evidence in the signature), and `&&`/`||` count as conditional control flow for `branch`. bench-schema is no longer types-only, so its description, its header comment, README.md, docs/asmlift-101.md and apps/benchmark/README.md all say what it is now: the shared benchmark contract, zero dependencies, browser-safe. Co-Authored-By: Claude Opus 5 (1M context) --- README.md | 16 +- apps/benchmark/README.md | 10 +- apps/benchmark/dataset/real/af.json | 18 +- apps/benchmark/dataset/real/kleod.json | 104 +-- apps/benchmark/dataset/real/marioparty3.json | 57 +- apps/benchmark/dataset/real/pokeemerald.json | 78 +- apps/benchmark/dataset/real/sa3.json | 115 +-- .../dataset/real/snowboardkids2.json | 59 +- apps/benchmark/dataset/synthetic.ts | 195 +++-- apps/benchmark/src/cases/features.ts | 187 ++-- apps/benchmark/src/eval/evaluate.ts | 9 +- apps/benchmark/test/features.test.ts | 185 ++-- docs/asmlift-101.md | 2 +- packages/bench-schema/README.md | 65 ++ packages/bench-schema/package.json | 2 +- packages/bench-schema/src/features.ts | 809 ++++++++++++++++++ packages/bench-schema/src/index.ts | 8 +- 17 files changed, 1289 insertions(+), 630 deletions(-) create mode 100644 packages/bench-schema/README.md create mode 100644 packages/bench-schema/src/features.ts diff --git a/README.md b/README.md index b12728db..bafb03b1 100644 --- a/README.md +++ b/README.md @@ -99,14 +99,14 @@ asmlift: [score] unsigned: 0 (match) ## Layout (pnpm workspace monorepo) -| Package / app | What it is | -| ----------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------ | -| [`packages/core`](packages/core/README.md) ([`@asmlift/core`](https://www.npmjs.com/package/@asmlift/core)) | The decompile pipeline | -| [`packages/cli`](packages/cli/README.md) ([`@asmlift/cli`](https://www.npmjs.com/package/@asmlift/cli)) | The user-facing CLI package | -| [`packages/toolchains`](packages/toolchains/README.md) | The pinned calibration toolchains. Used only for the tests and benchmark | -| `packages/bench-schema` | The shared benchmark result/manifest schema types | -| [`apps/web`](apps/web/README.md) | The webapp including the **Playground** and the **Benchmark** | -| [`apps/benchmark`](apps/benchmark/README.md) | The asmlift and m2c harness | +| Package / app | What it is | +| ----------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------- | +| [`packages/core`](packages/core/README.md) ([`@asmlift/core`](https://www.npmjs.com/package/@asmlift/core)) | The decompile pipeline | +| [`packages/cli`](packages/cli/README.md) ([`@asmlift/cli`](https://www.npmjs.com/package/@asmlift/cli)) | The user-facing CLI package | +| [`packages/toolchains`](packages/toolchains/README.md) | The pinned calibration toolchains. Used only for the tests and benchmark | +| [`packages/bench-schema`](packages/bench-schema/README.md) | The shared benchmark contract: result/manifest types + feature vocabulary | +| [`apps/web`](apps/web/README.md) | The webapp including the **Playground** and the **Benchmark** | +| [`apps/benchmark`](apps/benchmark/README.md) | The asmlift and m2c harness | ## License diff --git a/apps/benchmark/README.md b/apps/benchmark/README.md index 75295503..69771230 100644 --- a/apps/benchmark/README.md +++ b/apps/benchmark/README.md @@ -175,9 +175,10 @@ Host prerequisites (macOS; verified empirically): | `decomp-config.ts` | candidate compilation through the real `decomp.yaml` user path | | `cache.ts` | content-keyed result cache (tmp-then-rename; m2c dirty-checkout fail-closed; versioned key) | -The result schema is `@asmlift/bench-schema` (types-only workspace package) — the ONE definition -this harness produces and the web Benchmark view consumes. The harness's own toolchain-free tests -live in `test/` and run in CI. +The result schema is [`@asmlift/bench-schema`](../../packages/bench-schema/README.md) — the ONE +definition this harness produces and the web Benchmark view consumes, including the closed feature +vocabulary every row's `features` is drawn from. The harness's own toolchain-free tests live in +`test/` and run in CI. ## Extending @@ -188,6 +189,9 @@ live in `test/` and run in CI. until they compile; then `pnpm bench run` + `pnpm bench:merge`. - **Add a toolchain**: an adapter in `toolchains.ts` + a `compile/.ts` module for the real tier (or a typed `null` while unwired — see `compile/mwcc.ts`). +- **Tag a function**: list only judgement tags (`memory`, `struct`, …) — the source- and + codegen-evidenced ones are derived per row. See + [`@asmlift/bench-schema`](../../packages/bench-schema/README.md). - Re-run and re-merge; the Benchmark view re-renders from the committed `results.json`. ## Committed artifacts diff --git a/apps/benchmark/dataset/real/af.json b/apps/benchmark/dataset/real/af.json index f70fda37..5363110b 100644 --- a/apps/benchmark/dataset/real/af.json +++ b/apps/benchmark/dataset/real/af.json @@ -36,7 +36,6 @@ { "sym": "mem_copy", "features": [ - "loop", "pointer", "memory" ], @@ -52,7 +51,6 @@ { "sym": "mem_clear", "features": [ - "loop", "pointer", "memory" ], @@ -68,7 +66,6 @@ { "sym": "mem_cmp", "features": [ - "loop", "pointer", "branch" ], @@ -297,8 +294,7 @@ "features": [ "array", "arithmetic", - "branch", - "shift" + "branch" ], "prependC": "typedef unsigned char u8;\ntypedef unsigned short u16;\ntypedef int s32;\n#define UNUSED\nextern u8 mRmTp_ftr_size[];", "funcC": "s32 mRmTp_ItemNo2FtrSize(u16 name) {\n if (name >= 0x1000 && name < 0x1ECD) {\n UNUSED s32 scoped;\n return mRmTp_ftr_size[(name - 0x1000) >> 2];\n }\n return 0;\n}", @@ -330,8 +326,7 @@ "sym": "chase_f3", "features": [ "float", - "branch", - "ternary" + "branch" ], "prependC": "typedef int s32;\ntypedef float f32;", "funcC": "s32 chase_f3(f32* pValue, f32 target, f32 incrStep, f32 decrStep) {\n f32 step = (target >= *pValue) ? incrStep : decrStep;\n\n if (step != 0.0f) {\n if (target < *pValue) {\n step = -step;\n }\n\n *pValue += step;\n\n if (((*pValue - target) * step) >= 0) {\n *pValue = target;\n return 1;\n }\n } else if (target == *pValue) {\n return 1;\n }\n\n return 0;\n}", @@ -417,11 +412,9 @@ "features": [ "union", "array", - "shift", "cast", "pointer", - "float", - "bitwise" + "float" ], "funcC": "Mtx* _MtxF_to_Mtx(MtxF* src, Mtx* dest) {\n s32 fp;\n u16* intPart = (u16*)&dest->m[0][0];\n u16* fracPart = (u16*)&dest->m[2][0];\n\n fp = src->xx * 0x10000;\n intPart[0] = (fp >> 0x10);\n fracPart[0] = fp & 0xFFFF;\n\n fp = src->yx * 0x10000;\n intPart[1] = (fp >> 0x10);\n fracPart[1] = fp & 0xFFFF;\n\n fp = src->zx * 0x10000;\n intPart[2] = (fp >> 0x10);\n fracPart[2] = fp & 0xFFFF;\n\n fp = src->wx * 0x10000;\n intPart[3] = (fp >> 0x10);\n fracPart[3] = fp & 0xFFFF;\n\n fp = src->xy * 0x10000;\n intPart[4] = (fp >> 0x10);\n fracPart[4] = fp & 0xFFFF;\n\n fp = src->yy * 0x10000;\n intPart[5] = (fp >> 0x10);\n fracPart[5] = fp & 0xFFFF;\n\n // Ideally these three would use fracPart instead of intPart, but it's required to match.\n fp = src->zy * 0x10000;\n intPart[6] = (fp >> 0x10);\n intPart[22] = fp & 0xFFFF;\n\n fp = src->wy * 0x10000;\n intPart[7] = (fp >> 0x10);\n intPart[23] = fp & 0xFFFF;\n\n fp = src->xz * 0x10000;\n intPart[8] = (fp >> 0x10);\n intPart[24] = fp & 0xFFFF;\n\n fp = src->yz * 0x10000;\n intPart[9] = (fp >> 0x10);\n fracPart[9] = fp & 0xFFFF;\n\n fp = src->zz * 0x10000;\n intPart[10] = (fp >> 0x10);\n fracPart[10] = fp & 0xFFFF;\n\n fp = src->wz * 0x10000;\n intPart[11] = (fp >> 0x10);\n fracPart[11] = fp & 0xFFFF;\n\n fp = src->xw * 0x10000;\n intPart[12] = (fp >> 0x10);\n fracPart[12] = fp & 0xFFFF;\n\n fp = src->yw * 0x10000;\n intPart[13] = (fp >> 0x10);\n fracPart[13] = fp & 0xFFFF;\n\n fp = src->zw * 0x10000;\n intPart[14] = (fp >> 0x10);\n fracPart[14] = fp & 0xFFFF;\n\n fp = src->ww * 0x10000;\n intPart[15] = (fp >> 0x10);\n fracPart[15] = fp & 0xFFFF;\n return dest;\n}", "prependC": "typedef unsigned char u8; /* unsigned 8-bit */\ntypedef unsigned short u16; /* unsigned 16-bit */\ntypedef unsigned long u32; /* unsigned 32-bit */\ntypedef unsigned long long u64; /* unsigned 64-bit */\ntypedef signed char s8; /* signed 8-bit */\ntypedef short s16; /* signed 16-bit */\ntypedef long s32; /* signed 32-bit */\ntypedef long long s64; /* signed 64-bit */\ntypedef float f32; /* single prec floating point */\ntypedef double f64; /* double prec floating point */\ntypedef struct xyz_t {\n /* 0x0 */ f32 x;\n /* 0x4 */ f32 y;\n /* 0x8 */ f32 z;\n} xyz_t; // size = 0xC\n\ntypedef float MtxF_t[4][4];\ntypedef union {\n MtxF_t mf;\n struct {\n float xx, yx, zx, wx, xy, yy, zy, wy, xz, yz, zz, wz, xw, yw, zw, ww;\n };\n} MtxF; // size = 0x40\ntypedef long\tMtx_t[4][4];\n\ntypedef union {\n Mtx_t\t\tm;\n long long int\tforce_structure_alignment;\n} Mtx;", @@ -432,9 +425,7 @@ "features": [ "float", "array", - "shift", - "matrix", - "bitwise" + "matrix" ], "funcC": "void suMtxMakeSRT(Mtx* mtx, f32 scaleX, f32 scaleY, f32 scaleZ, s16 rotX, s16 rotY, s16 rotZ, f32 translateX,\n f32 translateY, f32 translateZ) {\n struct {\n s16 intPart[4][4];\n u16 fracPart[4][4];\n }* mu = (void*)mtx;\n s32 fp;\n f32 sinX = sin_s(rotX);\n f32 sinY = sin_s(rotY);\n f32 sinZ = sin_s(rotZ);\n f32 cosX = cos_s(rotX);\n f32 cosY = cos_s(rotY);\n f32 cosZ = cos_s(rotZ);\n\n fp = cosY * cosZ * scaleX * 0x10000;\n mu->intPart[0][0] = ((u32)fp >> 0x10) & 0xFFFF;\n mu->fracPart[0][0] = fp & 0xFFFF;\n\n fp = cosY * sinZ * scaleX * 0x10000;\n mu->intPart[0][1] = ((u32)fp >> 0x10) & 0xFFFF;\n mu->fracPart[0][1] = fp & 0xFFFF;\n\n fp = -sinY * scaleX * 0x10000;\n mu->intPart[0][2] = ((u32)fp >> 0x10) & 0xFFFF;\n mu->fracPart[0][2] = fp & 0xFFFF;\n\n fp = ((sinX * sinY * cosZ) - (cosX * sinZ)) * scaleY * 0x10000;\n mu->intPart[1][0] = ((u32)fp >> 0x10) & 0xFFFF;\n mu->fracPart[1][0] = fp & 0xFFFF;\n\n fp = ((sinX * sinY * sinZ) + (cosX * cosZ)) * scaleY * 0x10000;\n mu->intPart[1][1] = ((u32)fp >> 0x10) & 0xFFFF;\n mu->fracPart[1][1] = fp & 0xFFFF;\n\n fp = sinX * cosY * scaleY * 0x10000;\n mu->intPart[1][2] = ((u32)fp >> 0x10) & 0xFFFF;\n mu->fracPart[1][2] = fp & 0xFFFF;\n\n fp = ((cosX * sinY * cosZ) + (sinX * sinZ)) * scaleZ * 0x10000;\n mu->intPart[2][0] = ((u32)fp >> 0x10) & 0xFFFF;\n mu->fracPart[2][0] = fp & 0xFFFF;\n\n fp = ((cosX * sinY * sinZ) - (sinX * cosZ)) * scaleZ * 0x10000;\n mu->intPart[2][1] = ((u32)fp >> 0x10) & 0xFFFF;\n mu->fracPart[2][1] = fp & 0xFFFF;\n\n fp = cosX * cosY * scaleZ * 0x10000;\n mu->intPart[2][2] = ((u32)fp >> 0x10) & 0xFFFF;\n mu->fracPart[2][2] = fp & 0xFFFF;\n\n fp = translateX * 0x10000;\n mu->intPart[3][0] = ((u32)fp >> 0x10) & 0xFFFF;\n mu->fracPart[3][0] = fp & 0xFFFF;\n\n fp = translateY * 0x10000;\n mu->intPart[3][1] = ((u32)fp >> 0x10) & 0xFFFF;\n mu->fracPart[3][1] = fp & 0xFFFF;\n\n fp = translateZ * 0x10000;\n mu->intPart[3][2] = ((u32)fp >> 0x10) & 0xFFFF;\n mu->fracPart[3][2] = fp & 0xFFFF;\n\n mu->intPart[0][3] = mu->intPart[1][3] = mu->intPart[2][3] = 0;\n mu->fracPart[0][3] = mu->fracPart[1][3] = mu->fracPart[2][3] = 0;\n mu->intPart[3][3] = 1;\n mu->fracPart[3][3] = 0;\n}", "prependC": "typedef unsigned char u8; /* unsigned 8-bit */\ntypedef unsigned short u16; /* unsigned 16-bit */\ntypedef unsigned long u32; /* unsigned 32-bit */\ntypedef unsigned long long u64; /* unsigned 64-bit */\ntypedef signed char s8; /* signed 8-bit */\ntypedef short s16; /* signed 16-bit */\ntypedef long s32; /* signed 32-bit */\ntypedef long long s64; /* signed 64-bit */\ntypedef float f32; /* single prec floating point */\ntypedef double f64; /* double prec floating point */\ntypedef struct xyz_t {\n /* 0x0 */ f32 x;\n /* 0x4 */ f32 y;\n /* 0x8 */ f32 z;\n} xyz_t; // size = 0xC\n\ntypedef float MtxF_t[4][4];\ntypedef union {\n MtxF_t mf;\n struct {\n float xx, yx, zx, wx, xy, yy, zy, wy, xz, yz, zz, wz, xw, yw, zw, ww;\n };\n} MtxF; // size = 0x40\ntypedef long\tMtx_t[4][4];\n\ntypedef union {\n Mtx_t\t\tm;\n long long int\tforce_structure_alignment;\n} Mtx;", @@ -448,7 +439,6 @@ { "sym": "sAdo_Get_KokeruLabel", "features": [ - "switch", "branch" ], "funcC": "s32 sAdo_Get_KokeruLabel(s32 type) {\n u16 label = sAdo_Get_WalkLabel(type);\n s32 ret;\n\n switch (label) {\n case 0x201:\n ret = 0xe;\n break;\n\n case 0x206:\n ret = 0x13;\n break;\n\n case 0x202:\n ret = 0xf;\n break;\n\n case 0x203:\n ret = 0x10;\n break;\n\n case 0x204:\n ret = 0x11;\n break;\n\n case 0x205:\n ret = 0x12;\n break;\n\n case 0x208:\n ret = 0x156;\n break;\n\n case 0x209:\n ret = 0x157;\n break;\n\n default:\n ret = 15;\n break;\n }\n return ret;\n}", diff --git a/apps/benchmark/dataset/real/kleod.json b/apps/benchmark/dataset/real/kleod.json index 990b31ce..cf3843db 100644 --- a/apps/benchmark/dataset/real/kleod.json +++ b/apps/benchmark/dataset/real/kleod.json @@ -29,9 +29,7 @@ "sym": "StrCpy", "features": [ "memory", - "loop", - "pointer", - "do-while" + "pointer" ], "funcC": "void StrCpy(u8 *dst, u8 *src) {\n u32 c;\n do {\n c = *src;\n *dst = c;\n c = *src;\n dst++;\n src++;\n } while (c != 0);\n}", "proto": { @@ -46,8 +44,7 @@ "features": [ "arithmetic", "fixed-point", - "branch", - "shift" + "branch" ], "funcC": "s16 MultiplyQ8(s16 num1, s16 num2) {\n s32 product;\n s32 rounded;\n\n product = num1 * num2;\n rounded = product;\n if (rounded < 0) {\n rounded += 0xFF;\n }\n product = rounded >> 8;\n return product;\n}", "sourceUrl": "https://github.com/Dream-Atelier/kl-eod-decomp/blob/494f49912be3c9f6d893dd5bc15fd81be64f4d13/src/system.c#L116-L127" @@ -57,8 +54,7 @@ "features": [ "arithmetic", "fixed-point", - "branch", - "shift" + "branch" ], "funcC": "s16 MultiplyQ4(s16 num1, s16 num2) {\n s32 product;\n s32 rounded;\n\n product = num1 * num2;\n rounded = product;\n if (rounded < 0) {\n rounded += 0xF;\n }\n product = rounded >> 4;\n return product;\n}", "sourceUrl": "https://github.com/Dream-Atelier/kl-eod-decomp/blob/494f49912be3c9f6d893dd5bc15fd81be64f4d13/src/math.c#L25-L36" @@ -66,8 +62,7 @@ { "sym": "DivideQ8", "features": [ - "arithmetic", - "shift" + "arithmetic" ], "funcC": "s16 DivideQ8(s16 num1, s16 num2) {\n return (num1 << 8) / num2;\n}", "sourceUrl": "https://github.com/Dream-Atelier/kl-eod-decomp/blob/494f49912be3c9f6d893dd5bc15fd81be64f4d13/src/math.c#L11-L13" @@ -83,8 +78,7 @@ { "sym": "DivideQ4", "features": [ - "arithmetic", - "shift" + "arithmetic" ], "funcC": "s16 DivideQ4(s16 num1, s16 num2) {\n return ((num1 << 4) / num2);\n}", "sourceUrl": "https://github.com/Dream-Atelier/kl-eod-decomp/blob/494f49912be3c9f6d893dd5bc15fd81be64f4d13/src/math.c#L42-L44" @@ -101,7 +95,6 @@ "sym": "ReadKeyInput", "features": [ "global", - "bitwise", "branch" ], "funcC": "void ReadKeyInput(void) {\n u16 pressed = REG_KEYINPUT ^ 0x3FF;\n\n gKeysPressed = pressed & ~gKeysPrevious;\n gKeysPrevious = pressed;\n if ((pressed & 0x0F) == 0x0F) {\n SoftResetRom(0xFF);\n }\n if (gKeysPrevious & 1) {\n gAButtonHold++;\n } else {\n gAButtonHold = 0;\n }\n}", @@ -131,8 +124,7 @@ "sym": "ReadUnalignedU32", "features": [ "pointer", - "memory", - "shift" + "memory" ], "funcC": "u32 ReadUnalignedU32(u8 *ptr) {\n return ptr[0] + (ptr[1] << 8) + (ptr[2] << 16) + (ptr[3] << 24);\n}", "sourceUrl": "https://github.com/Dream-Atelier/kl-eod-decomp/blob/494f49912be3c9f6d893dd5bc15fd81be64f4d13/src/gfx.c#L70-L72" @@ -158,7 +150,6 @@ { "sym": "UpdateEntities", "features": [ - "loop", "branch", "cast" ], @@ -190,10 +181,8 @@ "sym": "Decompress", "features": [ "branch", - "bitwise", "cast", - "memory", - "shift" + "memory" ], "funcC": "void Decompress(void *dest, void *src) {\n void *heapPtr;\n\n if (((u32 *)src)[0] & 0x80000000) {\n heapPtr = thunk_HeapAlloc(((u32 *)src)[1] >> 8, 0);\n HuffUnComp((u8 *)src + 4, heapPtr);\n LZ77UnCompWram(heapPtr, dest);\n thunk_HeapFree(heapPtr);\n } else {\n LZ77UnCompWram((u8 *)src + 4, dest);\n }\n}", "ctx": "void *thunk_HeapAlloc(u32, u32);\nvoid thunk_HeapFree(void *);\nvoid HuffUnComp(void *, void *);\nvoid LZ77UnCompWram(void *, void *);", @@ -207,7 +196,6 @@ { "sym": "DecompressDma", "features": [ - "bitwise", "cast", "memory" ], @@ -223,12 +211,10 @@ { "sym": "CheckTileCollisionVertical", "features": [ - "loop", "struct", "branch", "pointer", - "memory", - "goto" + "memory" ], "funcC": "struct Unk_08014184 *CheckTileCollisionVertical(struct Unk_08014184 *arg0, u16 arg1, u16 arg2, u8 arg3) {\n u32 var_r3;\n struct Unk_08014184 var_r4;\n\n for (var_r3 = gUnk_03004D80->unk2; var_r3 < gUnk_03004D80->unk0; var_r3++) {\n if ((arg2 >= gUnk_03004D80->unk4[var_r3].unk2) && (gUnk_03004D80->unk4[var_r3].unk6 >= (arg2 - arg3))\n && (arg1 < (gUnk_03004D80->unk4[var_r3].unk0 + 3)) && ((gUnk_03004D80->unk4[var_r3].unk0 - 3) < arg1)) {\n var_r4.unk0 = gUnk_03004D80->unk4[var_r3].unk0 - 3;\n var_r4.unk2 = gUnk_03004D80->unk4[var_r3].unk8;\n *arg0 = var_r4;\n goto exit;\n return arg0;\n }\n }\n\n var_r4.unk0 = -1;\n *arg0 = var_r4;\nexit:\n return arg0;\n}", "prependC": "struct Unk_08014184 {\n u16 unk0;\n u8 unk2;\n u8 pad3[0x4 - 0x3];\n};", @@ -239,9 +225,7 @@ "features": [ "branch", "memory", - "pointer", - "shift", - "bitwise" + "pointer" ], "funcC": "u32 ProcessHBlankWait(u32 idx) {\n volatile u16 *ie;\n volatile u16 *dispstat;\n u8 *buf;\n u32 off;\n u8 *entry;\n u32 timer;\n\n ie = ®_IE;\n *ie |= 2;\n dispstat = ®_DISPSTAT;\n *dispstat |= 0x10;\n\n buf = gBuffer_52A4;\n off = idx * 9;\n off <<= 2;\n off += (u32)buf;\n entry = (u8 *)off;\n\n if (entry[3] >> 7) {\n return 1;\n }\n timer = *(u16 *)(entry + 0x14) - 1;\n *(u16 *)(entry + 0x14) = timer;\n if ((s32)(timer << 16) < 0) {\n *ie &= 0xFFFD;\n *dispstat &= 0xFFEF;\n return 0;\n }\n return 1;\n}", "sourceUrl": "https://github.com/Dream-Atelier/kl-eod-decomp/blob/494f49912be3c9f6d893dd5bc15fd81be64f4d13/src/gfx.c#L536-L566" @@ -250,7 +234,6 @@ "sym": "DecompressAlloc", "features": [ "cast", - "bitwise", "memory", "pointer" ], @@ -278,10 +261,8 @@ { "sym": "ReadUnalignedU16", "features": [ - "bitwise", "pointer", - "memory", - "shift" + "memory" ], "funcC": "u32 ReadUnalignedU16(u8 *ptr) {\n return ptr[0] | (ptr[1] << 8);\n}", "sourceUrl": "https://github.com/Dream-Atelier/kl-eod-decomp/blob/494f49912be3c9f6d893dd5bc15fd81be64f4d13/src/gfx.c#L54-L56" @@ -308,9 +289,7 @@ "features": [ "pointer", "cast", - "memory", - "shift", - "bitwise" + "memory" ], "funcC": "void DmaSpriteToObjVram(u32 entryIdx, u32 frameIdx) {\n vu32 *dma3 = ®_DMA3SAD;\n u32 base = *(vu32 *)&gGfxStreamBuffer;\n u8 *entry = (u8 *)(entryIdx * 8 + base);\n u16 tileCount = *(u16 *)(entry + 6);\n dma3[0] = *(u32 *)entry + tileCount * (frameIdx << 5);\n\n dma3[1] = (u32) * (u16 *)(entry + 4) * 32 + OBJ_VRAM;\n\n dma3[2] = (*(u16 *)(entry + 6) << 4) | (0x80 << 24);\n dma3[2];\n}", "proto": { @@ -324,9 +303,7 @@ "sym": "StreamCmd_SetWindowRegs", "features": [ "pointer", - "bitwise", - "global", - "shift" + "global" ], "funcC": "void StreamCmd_SetWindowRegs(void) {\n vu16 *reg = ®_WININ;\n u8 **streamPtrAddr = &gStreamPtr;\n u8 *stream = *streamPtrAddr;\n\n *reg = stream[2] | (stream[3] << 8);\n reg++;\n *reg = stream[4] | (stream[5] << 8);\n *streamPtrAddr = stream + 6;\n}", "proto": { @@ -340,8 +317,7 @@ "sym": "RollRandomLevelVariant", "features": [ "arithmetic", - "global", - "bitwise" + "global" ], "funcC": "void RollRandomLevelVariant(void) {\n u8 *state = gGameFlags;\n u8 difficulty = state[0x0C];\n u32 difficultyIndex = (u8)(difficulty - 1);\n u32 rng = thunk_sub_080002A0();\n u8 *levelState = gControlBlock;\n u32 parity = difficultyIndex & 1;\n u32 variant = sub_0805193C((u8)rng, 5 - difficultyIndex);\n levelState[0x0E] = parity + variant + 1;\n}", "prependC": "u32 thunk_sub_080002A0(void);\nu32 sub_0805193C(u8, u32);", @@ -358,10 +334,7 @@ "global", "struct", "array", - "switch", - "arithmetic", - "shift", - "bitwise" + "arithmetic" ], "funcC": "void TransformSingleEntityToScreen(u8 arg0, s8 arg1, s8 arg2) {\n s32 temp_r0;\n s32 temp_r0_2;\n s16 temp_r1;\n s16 temp_r2;\n s32 var_r2;\n s32 var_r4;\n struct Unk_0300466C_4 *var_r0;\n\n arg1++, arg1--; /* fake */\n gUnk_03002920[arg0].xPosScreen = gUnk_03002920[arg0].xPosBg2 - arg1 - gUnk_03003430.bg2HOfs;\n gUnk_03002920[arg0].yPosScreen = gUnk_03002920[arg0].yPosBg2 - arg2 - gUnk_03003430.bg2VOfs;\n\n temp_r1 = gUnk_03003430.bg2HOfs - gUnk_03002920[arg0].xPosScreen;\n temp_r2 = gUnk_03003430.bg2VOfs - gUnk_03002920[arg0].yPosScreen;\n\n temp_r0 = temp_r1 * gBg2XMag;\n if (temp_r0 < 0) {\n temp_r0 += 0xFF;\n }\n var_r4 = temp_r0 >> 8;\n\n temp_r0_2 = temp_r2 * gBg2YMag;\n if (temp_r0_2 < 0) {\n temp_r0_2 += 0xFF;\n }\n var_r2 = temp_r0_2 >> 8;\n\n var_r4 = gUnk_03003430.bg2HOfs - var_r4;\n if (arg0 > 0xC) {\n var_r0 = (void *)gUnk_030051DC[arg0 - 0xD].unk4;\n } else {\n var_r0 = gUnk_08078FC8[arg0].unk4;\n }\n\n switch (var_r0->shape_size & 0xF) {\n case 3:\n case 11:\n var_r2 = gUnk_03003430.bg2VOfs - var_r2 + ((0x100 - gBg2YMag) >> 3);\n break;\n case 1:\n case 6:\n case 8:\n var_r2 = gUnk_03003430.bg2VOfs - var_r2 + ((0x100 - gBg2YMag) >> 5);\n break;\n case 0:\n case 4:\n case 5:\n var_r2 = gUnk_03003430.bg2VOfs - var_r2 + ((0x100 - gBg2YMag) >> 6);\n break;\n default:\n var_r2 = gUnk_03003430.bg2VOfs - var_r2 + ((0x100 - gBg2YMag) >> 4);\n break;\n }\n\n gUnk_03002920[arg0].xPosScreen = var_r4;\n gUnk_03002920[arg0].yPosScreen = var_r2;\n}", "sourceUrl": "https://github.com/Dream-Atelier/kl-eod-decomp/blob/9400132178f58db2d8b2638349ffa86155ed2e53/src/code_0.c#L1407-L1464" @@ -371,9 +344,7 @@ "features": [ "global", "array", - "loop", - "branch", - "shift" + "branch" ], "funcC": "void CopyBGScrollTiles(void) {\n u32 var_r5;\n u32 var_r6;\n\n for (var_r6 = 0; var_r6 < 3; var_r6++) {\n if (var_r6 < gUnk_03005220.hearts) {\n var_r5 = 0;\n } else {\n var_r5 = 2;\n }\n gBgTilemapBufs[0][(var_r6 * 2) + 0x241] = gBgTilemapBufs[0][(var_r6 * 2) + ((var_r5 + 0x14) << 5)];\n gBgTilemapBufs[0][(var_r6 * 2) + 0x242] = gBgTilemapBufs[0][(var_r6 * 2 + 1) + ((var_r5 + 0x14) << 5)];\n gBgTilemapBufs[0][(var_r6 * 2) + 0x261] = gBgTilemapBufs[0][(var_r6 * 2) + ((var_r5 + 0x15) << 5)];\n gBgTilemapBufs[0][(var_r6 * 2) + 0x262] = gBgTilemapBufs[0][(var_r6 * 2 + 1) + ((var_r5 + 0x15) << 5)];\n }\n}", "sourceUrl": "https://github.com/Dream-Atelier/kl-eod-decomp/blob/ed09229e28615a1bd585a5be6b1a9ba2c8a77315/src/code_1.c#L1165-L1180", @@ -395,10 +366,7 @@ "features": [ "global", "struct", - "nested-loop", - "bitwise", - "branch", - "loop" + "branch" ], "funcC": "u8 CheckWorldCompletion(u8 arg0) {\n u32 var_r0;\n u32 var_r2;\n u8 var_sl;\n u8 var_ip;\n u8 var_r6;\n u8 var_r8;\n u8 v50;\n if (arg0 < 4) {\n if (((gUnk_03004670->unk8[arg0][7] & 0x80) != 0) && ((gUnk_03004670->unk8[arg0 + 1][0] & 0x7F) != 0x7F))\n return 1;\n } else if ((gUnk_03004670->unk8[5][7] & 0x80) != 0) {\n var_r8 = 0;\n var_ip = 0;\n var_r6 = 0;\n var_sl = 0;\n for (var_r0 = 0; var_r0 < 5; var_r0++) {\n for (var_r2 = 0; var_r2 < 7; var_r2++) {\n if ((var_r2 == 3 || var_r2 == 5) && (gUnk_03004670->unk8[var_r0][var_r2] & 0x7F) == 0x64)\n var_r8 += 1;\n else if ((var_r2 != 7) && ((gUnk_03004670->unk8[var_r0][var_r2] & 0x7F) == 0x1E))\n var_ip += 1;\n if (gUnk_03004670->unk8[var_r0][var_r2] & 0x80)\n var_r6 += 1;\n }\n }\n v50 = gUnk_03004670->unk8[5][0] & 0x7F;\n if (v50 == 0x1E)\n var_sl += 1;\n if ((gUnk_03004670->unk8[5][1] & 0x7F) == 0x1E)\n var_sl += 1;\n if ((arg0 == 4) && (v50 != 0x7F) && (var_r6 == 0x23))\n return 1;\n if ((arg0 == 5) && ((gUnk_03004670->unk8[5][1] & 0x7F) != 0x7F) && ((var_ip + var_r8) > 0x18))\n return 1;\n if ((arg0 == 6) && ((gUnk_03004670->unk8[5][2] & 0x7F) != 0x7F) && ((var_ip + var_r8 + var_sl) == 0x25))\n return 1;\n }\n return 0;\n}", "sourceUrl": "https://github.com/Dream-Atelier/kl-eod-decomp/blob/ed09229e28615a1bd585a5be6b1a9ba2c8a77315/src/code_3.c#L1752-L1791" @@ -409,8 +377,7 @@ "global", "array", "branch", - "table", - "shift" + "table" ], "funcC": "void UpdateWorldMapNodeTile(u8 arg0) {\n vu32 a; // Required to match\n if (arg0 < 4) {\n gBgTilemapBufs[1][gUnk_08116748[arg0][0] + (gUnk_08116748[arg0][1] << 5)]\n = gBgTilemapBufs[1][((gUnk_08116880[arg0] + 0x1C) * 0x20) + 0];\n gBgTilemapBufs[1][gUnk_08116748[arg0][2] + (gUnk_08116748[arg0][3] << 5)]\n = gBgTilemapBufs[1][((gUnk_08116880[arg0] + 0x1C) * 0x20) + 1];\n gBgTilemapBufs[1][gUnk_08116748[arg0][4] + (gUnk_08116748[arg0][5] << 5)]\n = gBgTilemapBufs[1][((gUnk_08116880[arg0] + 0x1C) * 0x20) + 2];\n gBgTilemapBufs[1][gUnk_08116748[arg0][6] + (gUnk_08116748[arg0][7] << 5)]\n = gBgTilemapBufs[1][((gUnk_08116880[arg0] + 0x1C) * 0x20) + 3];\n } else {\n gBgTilemapBufs[1][gUnk_08116748[arg0][0] + (gUnk_08116748[arg0][1] << 5)]\n = gBgTilemapBufs[1][((gUnk_08116880[arg0] + 0x1C) * 0x20) + 4];\n gBgTilemapBufs[1][gUnk_08116748[arg0][2] + (gUnk_08116748[arg0][3] << 5)]\n = gBgTilemapBufs[1][((gUnk_08116880[arg0] + 0x1C) * 0x20) + 5];\n gBgTilemapBufs[1][gUnk_08116748[arg0][4] + (gUnk_08116748[arg0][5] << 5)]\n = gBgTilemapBufs[1][((gUnk_08116880[arg0] + 0x1C) * 0x20) + 6];\n gBgTilemapBufs[1][gUnk_08116748[arg0][6] + (gUnk_08116748[arg0][7] << 5)]\n = gBgTilemapBufs[1][((gUnk_08116880[arg0] + 0x1C) * 0x20) + 7];\n }\n}", "sourceUrl": "https://github.com/Dream-Atelier/kl-eod-decomp/blob/ed09229e28615a1bd585a5be6b1a9ba2c8a77315/src/code_3.c#L1849-L1870", @@ -419,9 +386,7 @@ { "sym": "UpdateFadeEffect", "features": [ - "global", - "shift", - "bitwise" + "global" ], "funcC": "void UpdateFadeEffect(void) {\n vu8 *vcount_reg = ®_VCOUNT_L;\n u8 *entity = gEntityArray;\n u8 fade = sub_08051A0C(*vcount_reg, entity[0x08]);\n\n if (fade <= 16) {\n REG_BLDALPHA = ((u32)fade << 8) | fade;\n }\n}", "sourceUrl": "https://github.com/Dream-Atelier/kl-eod-decomp/blob/ed09229e28615a1bd585a5be6b1a9ba2c8a77315/src/engine.c#L48-L56", @@ -432,8 +397,7 @@ "features": [ "global", "struct", - "pointer", - "shift" + "pointer" ], "funcC": "void StreamCmd_SetBGScroll(void) {\n u16 scrollX = ReadUnalignedU16(gStreamPtr + 3);\n struct BGLayerState *tbl = gBGLayerState;\n u8 *p = gStreamPtr;\n u16 scrollY;\n u8 *p2;\n tbl[p[2]].scrollX = scrollX << 4;\n scrollY = ReadUnalignedU16(p + 5);\n p2 = gStreamPtr;\n tbl[p2[2]].scrollY = scrollY << 4;\n gStreamPtr = p2 + 7;\n}", "sourceUrl": "https://github.com/Dream-Atelier/kl-eod-decomp/blob/494f49912be3c9f6d893dd5bc15fd81be64f4d13/src/gfx.c#L429-L440" @@ -441,14 +405,9 @@ { "sym": "ProcessInputAndUpdateEntities", "features": [ - "switch", "array", - "nested-loop", "branch", - "global", - "loop", - "shift", - "bitwise" + "global" ], "funcC": "void ProcessInputAndUpdateEntities(void) {\n u8 sp4;\n u32 var_r1;\n u8 var_r4;\n u8 var_r5;\n\n sp4 = (gNewKeys & (START_BUTTON | B_BUTTON)) != 0;\n if (gNewKeys & A_BUTTON) {\n sp4 = gUnk_03004658[0xC] + 1;\n }\n\n if (gNewKeys & (DPAD_DOWN | DPAD_UP)) {\n for (var_r5 = 0; var_r5 < 2; var_r5++) {\n DmaFill16(3, 0, &gBgTilemapBufs[gUnk_030034BC][((var_r5 + (gUnk_03004658[0xC] * 3)) << 5) + 0xA5], 0x4);\n DmaFill16(3, 0, &gBgTilemapBufs[gUnk_030034BC][((var_r5 + (gUnk_03004658[0xC] * 3)) << 5) + 0xB7], 0x4);\n }\n\n if ((gUnk_030034C0 == 0) || (gUnk_030034C0 == 2)) {\n if (gNewKeys & DPAD_DOWN) {\n m4aSongNumStart(0x51);\n gUnk_03004658[0xC] += 1;\n if (gUnk_03004658[0xC] > 3) {\n gUnk_03004658[0xC] = 0;\n }\n }\n\n if (gNewKeys & DPAD_UP) {\n m4aSongNumStart(0x51);\n gUnk_03004658[0xC] -= 1;\n if (gUnk_03004658[0xC] & 0x80) {\n gUnk_03004658[0xC] = 3;\n }\n }\n } else {\n if (gNewKeys & DPAD_DOWN) {\n m4aSongNumStart(0x51);\n gUnk_03004658[0xC] += 1;\n if (gUnk_03004658[0xC] > 2) {\n gUnk_03004658[0xC] = 0;\n }\n }\n\n if (gNewKeys & DPAD_UP) {\n m4aSongNumStart(0x51);\n gUnk_03004658[0xC] -= 1;\n if (gUnk_03004658[0xC] & 0x80) {\n gUnk_03004658[0xC] = 2;\n }\n }\n }\n\n for (var_r5 = 0; var_r5 < 2; var_r5++) {\n for (var_r4 = 0; var_r4 < 2; var_r4++) {\n if (gUnk_030034BC == 0) {\n gBgTilemapBufs[gUnk_030034BC][((var_r5 + (gUnk_03004658[0xC] * 3)) << 5) + 0xA5 + var_r4]\n = gBgDataPtrs.pBufBg3Tilemap[(var_r5 * 0x1E) + 0x9D + var_r4] + gUnk_03000800;\n gBgTilemapBufs[gUnk_030034BC][((var_r5 + (gUnk_03004658[0xC] * 3)) << 5) + 0xB7 + var_r4]\n = gBgDataPtrs.pBufBg3Tilemap[(var_r5 * 0x1E) + 0xAF + var_r4] + gUnk_03000800;\n } else {\n gBgTilemapBufs[gUnk_030034BC][((var_r5 + (gUnk_03004658[0xC] * 3)) << 5) + 0xA5 + var_r4]\n = gBgDataPtrs.pBufBg3Tilemap[(var_r5 * 0x1E) + 0x9D + var_r4];\n gBgTilemapBufs[gUnk_030034BC][((var_r5 + (gUnk_03004658[0xC] * 3)) << 5) + 0xB7 + var_r4]\n = gBgDataPtrs.pBufBg3Tilemap[(var_r5 * 0x1E) + 0xAF + var_r4];\n }\n }\n }\n }\n\n if (sp4 > 0) {\n sp4 = gUnk_081166F8[gUnk_030034C0][sp4 - 1];\n }\n\n switch (sp4 - 1) {\n case 0:\n gCallbackQueue.next[0] = ReadKeyInput;\n for (var_r1 = 0; var_r1 < 10; var_r1++) {\n gCallbackQueue.next[var_r1] = gCallbackQueue.previous[var_r1];\n }\n gCallbackQueue.current[gCallbackQueue.currentCount - 1] = NULL;\n gCallbackQueue.nextCount = gCallbackQueue.previousCount;\n\n UpdateOamSortOrder();\n m4aSoundVSyncOn();\n m4aMPlayAllContinue();\n break;\n\n case 1:\n UpdateOamSortOrder();\n gCallbackQueue.current[1] = TransitionFadeOutFull;\n gUnk_03005220.unk4 = gUnk_03005284->unk18;\n gUnk_03005220.lives = gUnk_03005284->unk0;\n gUnk_03005220.hearts = gUnk_03005284->unk8_0;\n REG_BLDCNT = 0;\n gBlendValue = 0;\n break;\n\n case 2:\n gUnk_03005284->unk0 = gUnk_03005220.lives = gUnk_03005284->unk1E;\n UpdateOamSortOrder();\n gBlendValue = 0;\n if (gUnk_03004C20.world == 6 && gUnk_03004C20.level == 8) {\n gUnk_03004C20.world = 5;\n }\n gUnk_030034B0.unk6_4 = gUnk_03004C20.level;\n gUnk_03004C20.level = 0;\n gCallbackQueue.current[1] = TransitionGameOver;\n break;\n\n case 3:\n gCallbackQueue.current[1] = SetupBG3WindowOverlay;\n thunk_HeapFree(gBgDataPtrs.pBufBg3Tilemap);\n thunk_HeapFree(gBgDataPtrs.pBufBg3Tiles);\n break;\n\n case 4:\n if (gUnk_03004C20.level != 0) {\n gUnk_03005284->unk0 = gUnk_03005220.lives = gUnk_03005284->unk1E;\n }\n UpdateOamSortOrder();\n REG_BLDCNT = 0;\n gBlendValue = 0;\n FreeAllDecompBuffers();\n gCallbackQueue.current[1] = TransitionToGameplayScreen;\n break;\n\n case 5:\n gBlendValue = 0;\n gUnk_03004C20.level = 9;\n gUnk_03004D9C = 0;\n InitOamEntries();\n gCallbackQueue.current[1] = TransitionSoftReset;\n thunk_HeapFree(gBgDataPtrs.pBufBg3Tilemap);\n thunk_HeapFree(gBgDataPtrs.pBufBg3Tiles);\n break;\n }\n}", "prependC": "extern struct EntityAnimationInfo gEntityAnimationInfo[];\nextern void SetupBG3WindowOverlay(void);\nextern void UpdateScrollPosition(void);\nvoid SetPaletteAnimEntry(s32, u8);\nextern s16 MultiplyQ8(s16, s16);\nextern s16 ReciprocalQ8(s16);\nextern u8 thunk_sub_080002A0();\nextern u8 thunk_sub_080002D0();\nextern void VBlankCallback_Dialog(void);\n/* Cross-module function prototypes referenced by the decompiled functions below.\n * (ROM data symbols live in structs/variables.h.) */\nvoid HBlankScrollUpdate(void);\ns32 Abs(s32 n);\nvoid IntroSequenceUpdate(void);\nvoid TransitionReturnToWorldMap(void);\nvoid TransitionClearAndRestart(void);\nvoid TransitionSoftReset(void);\nvoid TransitionToGameplayScreen(void);\nvoid TransitionGameOver(void);\nvoid TransitionFadeOutFull(void);\nvoid UpdatePlayerFinalBoss(u8 arg0);\nvoid UpdatePlayerAlternate(u8 arg0);\nvoid UpdatePlayerMinigame(u8 arg0);\nvoid VBlankHandler_ModeB(void);\nvoid ReadKeyInput(void);\nvoid VBlankCallback_Gameplay(void);\nvoid AnimatePaletteEffects(void);\nvoid IntroScrollAnimation(void);\n\n/* ── kleod code_08039D8C.c shared scaffolding ─────────────────────────────\n * Common globals, cross-module externs, and helper inlines for the logic\n * functions ported from kleod's code_08039D8C.c. */\n\nextern struct BgDataPtrs gBgDataPtrs; /* 0x03004790 */\nextern u8 gBlendValue; /* 0x03005498 */\n\n/* Tile-collision query result (kleod CheckTileCollisionSloped). */\nstruct Unk_08014184 {\n u16 unk0;\n u8 unk2;\n u8 pad3[0x4 - 0x3];\n};\n\n/* Cross-module functions (defined in code_1). */\nextern struct Unk_08014184 *CheckTileCollisionSloped(struct Unk_08014184 *, u16, u16, u8);\nstatic inline struct Unk_08014184 Call_CheckTileCollisionSloped(u16 arg1, u16 arg2, u8 arg3) {\n struct Unk_08014184 sp;\n CheckTileCollisionSloped(&sp, arg1, arg2, arg3);\n return sp;\n}\nextern void PlayerRespawnOrDeath(s32);\nextern void SpawnEntityAtPosition(u16, u16, u8, u8);\n\n/* Forward decls (code_08039D8C.c cluster). */\nvoid InitGameplayState(void);\nvoid UpdateOamSortOrder(void);\nvoid ProcessInputAndUpdateEntities(void);\nvoid UpdateWorldMapInput(void);\nu8 CheckWorldCompletion(u8 arg0);\nvoid CopyWorldMapTiles(u8 arg0);\nvoid SetWorldMapTilePalette(u8 arg0, u8 arg1);\nvoid UpdateWorldMapNodeTile(u8 arg0);\nvoid UpdateEntities(void);\nvoid CountCollectedGems(void);\nvoid UpdateWorldMapNodeAnim(void);\nvoid GameplayMainLoop(void);\nvoid InitLevelState(void);\nvoid SpawnEntitiesForVision(u8 arg0);\nvoid GetEntityLookupData(u8 arg0);\nvoid ComputeScrollLimits(void);\nvoid ApplyPlayerMovement(u8 arg0, struct Unk_0803D4AC arg1);\nvoid UpdatePlayerNormal(u8 arg0);\nvoid SetupEntitySpawnTable(u8 arg0);\nvoid RollRandomLevelVariant(void);\nvoid UpdatePlayerBoss(u8 arg0);\nvoid ConfigureEntityBehavior(u8 arg0, u8 arg1, u8 arg2);\nvoid TransitionLevelVariant(u8 arg0);\nvoid UpdateLevelProgression(void);\nvoid SetEntityVisibility(u8 arg0);\nvoid UpdatePlayerSpecial(u8 arg0);\n#include \"m4a_internal.h\"", @@ -462,10 +421,8 @@ { "sym": "ConfigureEntityBehavior", "features": [ - "switch", "table", "pointer", - "loop", "global" ], "funcC": "void ConfigureEntityBehavior(u8 arg0, u8 arg1, u8 arg2) {\n void *var_r3;\n void *var_r5;\n void *var_r6;\n u32 var_sb;\n u8 var_r1;\n\n if (arg2 != 0) {\n gEntityInfo[0x23].unkF = 0x19;\n gEntityInfo[0x22].unkF = 0x19;\n gEntityInfo[0x21].unkF = 0x19;\n gEntityInfo[0x20].unkF = 0x19;\n }\n\n switch (arg1) {\n case 0:\n var_sb = 0xD;\n var_r6 = &gUnk_03003D16[arg0];\n var_r5 = &gBgDataPtrs.pBufBg2Tilemap[6 + (arg0 * 8) + (gBgInfo[2].hLength * 0x1B)];\n var_r3 = gBgDataPtrs.pBufBg2Tilemap + 0x3C;\n break;\n\n case 1:\n var_sb = 4;\n var_r6 = &gUnk_03003F56[arg0];\n var_r5 = &gBgDataPtrs.pBufBg2Tilemap[6 + (arg0 * 8) + (gBgInfo[2].hLength * 0x24)];\n var_r3 = gBgDataPtrs.pBufBg2Tilemap + 0x3C + (gBgInfo[2].hLength * 0x13);\n break;\n\n case 2:\n var_sb = 6;\n var_r6 = &gUnk_03003E96[arg0];\n var_r5 = &gBgDataPtrs.pBufBg2Tilemap[6 + (arg0 * 8) + (gBgInfo[2].hLength * 0x21)];\n var_r3 = gBgDataPtrs.pBufBg2Tilemap + 0x3C + (gBgInfo[2].hLength * 0x13);\n break;\n\n case 3:\n var_sb = 6;\n var_r6 = &gUnk_03003DD6[arg0];\n var_r5 = &gBgDataPtrs.pBufBg2Tilemap[6 + (arg0 * 8) + (gBgInfo[2].hLength * 0x1E)];\n var_r3 = gBgDataPtrs.pBufBg2Tilemap + 0x3C + (gBgInfo[2].hLength * 0x13);\n break;\n\n case 4:\n var_sb = 6;\n var_r6 = &gUnk_03003D16[arg0];\n var_r5 = &gBgDataPtrs.pBufBg2Tilemap[6 + (arg0 * 8) + (gBgInfo[2].hLength * 0x1B)];\n var_r3 = gBgDataPtrs.pBufBg2Tilemap + 0x3C + (gBgInfo[2].hLength * 0xD);\n break;\n\n case 5:\n var_sb = 6;\n var_r6 = &gUnk_03003D16[arg0];\n var_r5 = &gBgDataPtrs.pBufBg2Tilemap[6 + (arg0 * 8) + (gBgInfo[2].hLength * 0x1B)];\n var_r3 = gBgDataPtrs.pBufBg2Tilemap + 0x3C + (gBgInfo[2].hLength * 0x16);\n break;\n\n case 6:\n var_sb = 4;\n var_r6 = &gUnk_03003F56[arg0];\n var_r5 = &gBgDataPtrs.pBufBg2Tilemap[6 + (arg0 * 8) + (gBgInfo[2].hLength * 0x24)];\n var_r3 = gBgDataPtrs.pBufBg2Tilemap + 0x3C;\n break;\n\n case 7:\n var_sb = 7;\n var_r6 = &gUnk_03003E96[arg0];\n var_r5 = &gBgDataPtrs.pBufBg2Tilemap[6 + (arg0 * 8) + (gBgInfo[2].hLength * 0x21)];\n var_r3 = gBgDataPtrs.pBufBg2Tilemap + 0x3C;\n break;\n\n case 8:\n var_sb = 0xA;\n var_r6 = &gUnk_03003DD6[arg0];\n var_r5 = &gBgDataPtrs.pBufBg2Tilemap[6 + (arg0 * 8) + (gBgInfo[2].hLength * 0x1E)];\n var_r3 = gBgDataPtrs.pBufBg2Tilemap + 0x3C;\n break;\n\n case 9:\n var_sb = 0xD;\n var_r6 = &gUnk_03003D16[arg0];\n var_r5 = &gBgDataPtrs.pBufBg2Tilemap[6 + (arg0 * 8) + (gBgInfo[2].hLength * 0x1B)];\n var_r3 = gBgDataPtrs.pBufBg2Tilemap + 0x3C;\n break;\n }\n\n for (var_r1 = 0; var_r1 < var_sb; var_r1++) {\n DmaCopy16Wait(3, var_r3, var_r6, 0x8);\n DmaCopy16Wait(3, var_r3, var_r5, 0x8);\n var_r6 += 0x40;\n var_r5 += gBgInfo[2].hLength;\n var_r3 += gBgInfo[2].hLength;\n }\n}", @@ -481,10 +438,8 @@ "sym": "UpdateCameraScroll", "features": [ "struct", - "shift", "branch", - "global", - "bitwise" + "global" ], "funcC": "void UpdateCameraScroll(void) {\n s32 temp_r2;\n s32 temp_r3_2;\n u16 var_r8;\n struct ScrollBGLayer_Args var_r6;\n u8 var_r7;\n\n var_r8 = 0;\n var_r7 = 0;\n var_r6.unk2 = var_r6.unk0 = 0;\n\n if (!(gUnk_030051B8 & 0x30)) {\n if ((s16)gUnk_03002920[0].xPosBg2 > (s16)(gUnk_03003430.bg2HOfs + DISPLAY_WIDTH_CENTER)) {\n var_r7 = 0x10;\n if ((gUnk_03002920[0].xPosBg2 - (gUnk_03003430.bg2HOfs + DISPLAY_WIDTH_CENTER)) > 5) {\n var_r6.unk0 = 2;\n } else {\n var_r6.unk0 = 1;\n }\n if (gHeldKeys & DPAD_LEFT) {\n var_r7 = 0;\n var_r6.unk0 = 0;\n }\n } else if ((s16)gUnk_03002920[0].xPosBg2 < (s16)(gUnk_03003430.bg2HOfs + DISPLAY_WIDTH_CENTER)) {\n var_r7 = 0x20;\n if (((gUnk_03003430.bg2HOfs + DISPLAY_WIDTH_CENTER) - gUnk_03002920[0].xPosBg2) > 5) {\n var_r6.unk0 = -2;\n } else {\n var_r6.unk0 = -1;\n }\n if (gHeldKeys & DPAD_RIGHT) {\n var_r7 = 0;\n var_r6.unk0 = 0;\n }\n }\n }\n if (!(gUnk_030051B8 & 0xC0)) {\n if (gUnk_03002920[0].yPosScreen > 0xA0) {\n var_r8 = 0x44;\n }\n if ((u16)(var_r8 + gUnk_03002920[0].yPosScreen) > 0x63) {\n var_r7 |= 0x80;\n if ((u16)((var_r8 + gUnk_03002920[0].yPosScreen) - 0x64) > 0x1E) {\n var_r6.unk2 = 3;\n } else {\n var_r6.unk2 = 1;\n }\n } else if ((u16)(var_r8 + gUnk_03002920[0].yPosScreen) <= 0x46) {\n var_r7 |= 0x40;\n if ((u16)(var_r8 - (gUnk_03002920[0].yPosScreen - 0x46)) > 0x19U) {\n var_r6.unk2 = -3;\n } else {\n var_r6.unk2 = -1;\n }\n }\n }\n\n var_r7 |= gUnk_030051B8;\n temp_r3_2 = gUnk_03005480 + gUnk_030034E8.unk0;\n temp_r2 = gUnk_030007C0 + gUnk_030034E8.unk4;\n var_r6.unk0 += (temp_r3_2 >> 0x10);\n var_r6.unk2 += (temp_r2 >> 0x10);\n gUnk_03005480 = temp_r3_2 & 0xFFFF;\n gUnk_030007C0 = temp_r2 & 0xFFFF;\n\n if (gUnk_03005220.deathSequenceTimer != 0) {\n var_r7 &= 0x30;\n var_r6.unk2 = var_r6.unk0 = 0;\n }\n\n if (var_r7 != 0) {\n ScrollBGLayer(var_r7, var_r6);\n }\n\n gUnk_03003430.bg1HOfs = (gUnk_03003430.bg2HOfs >> 1);\n if (!(gUnk_03004C20.globalFrameCounter & 3) && (gUnk_03004660 == 0)) {\n if (gUnk_03003430.bg2VOfs >= gPrevBg2VOfs) {\n gUnk_03003430.bg1VOfs += ((gUnk_03003430.bg2VOfs - gPrevBg2VOfs) & 1);\n } else {\n gUnk_03003430.bg1VOfs -= ((gPrevBg2VOfs - gUnk_03003430.bg2VOfs) & 1);\n }\n\n if (gUnk_03003430.bg1VOfs & 0x8000) {\n gUnk_03003430.bg1VOfs = 0;\n } else if (gUnk_03003430.bg1VOfs > 0x50) {\n gUnk_03003430.bg1VOfs = 0x50;\n }\n }\n\n gPrevBg2VOfs = gUnk_03003430.bg2VOfs;\n gBg2PA = MultiplyQ8(COS(gBg2Alpha), ReciprocalQ8(gBg2XMag));\n gBg2PB = MultiplyQ8(SIN(gBg2Alpha), ReciprocalQ8(gBg2XMag));\n gBg2PC = MultiplyQ8(-SIN(gBg2Alpha), ReciprocalQ8(gBg2YMag));\n gBg2PD = MultiplyQ8(COS(gBg2Alpha), ReciprocalQ8(gBg2YMag));\n gBg2X\n = (((gUnk_03003430.bg2HOfs + DISPLAY_WIDTH_CENTER) << 8) - (gBg2PA * DISPLAY_WIDTH_CENTER)) - (gBg2PB * DISPLAY_HEIGHT_CENTER);\n gBg2Y = (((gUnk_03003430.bg2VOfs + DISPLAY_HEIGHT_CENTER) << 8) - (gBg2PC * DISPLAY_WIDTH_CENTER))\n - (gBg2PD * DISPLAY_HEIGHT_CENTER);\n}", "prependC": "#include \"data/trig.h\"\n\nextern s16 MultiplyQ8(s16 a, s16 b);\nextern void VBlankHandler_OamOnly(void);\nextern void ComputeRotationMatrix(void);\nextern s16 ReciprocalQ8(s16 a);\nextern void TransitionGameplayInit(void);\nextern void ComputeScrollLimits(void);\nextern s32 Abs(s32 n);\nextern void InitScrollState(void);\nextern void InitOamEntries(void);\nextern void InitLevelGameplay(u32 arg0); /* InitLevelGameplay */\nextern void ReadKeyInput(void); /* ReadKeyInput == kleod InputHandler_Normal */\nextern void InitGameplayFromWorldMap(void);\nextern void VBlankCallback_Dialog(void); /* VBlankCallback_Dialog */\nextern void UpdateWorldMapLogic(void); /* sub_08048028 (off-by-2 in our split) */\nextern void TransitionInitLevelMusic(void); /* sub_080242C0 */\nextern void ScrollBGColumnLoad(u8 arg0); /* ScrollBGColumnLoad */\n/* InitLevelBG (InitLevelBG) extras */\nextern void m4aSoundVSyncOff(void);\nextern void m4aMPlayAllStop(void);\nextern void *DecompressAlloc(void *src);\nextern void Decompress(void *dest, void *src);\nextern void DecompressDma(void *src, void *dest, u16 size);\nextern void *thunk_HeapAlloc(u32 size, u32 flags);\nextern void CheckTileCollisionRect(u8); /* CheckTileCollisionRect */\nextern void UpdateHUDTimerAndLives(void); /* UpdateHUDTimerAndLives */\nextern void VBlankHandler(void); /* sub_080009D8 — installed for non-cutscene levels */\nextern void VBlankDmaTransfer(void); /* sub_08000CE0 — installed for cutscene levels */\nextern void InitLevelFromROMTable(void); /* InitLevelFromROMTable */\n\n/**\n * UpdateFadeEffect: applies brightness fade using REG_BLDY.\n *\n * Reads REG_VCOUNT and entity brightness value, computes fade level\n * via sub_08051A0C, then writes to REG_BLDY if within valid range (<=16).\n */\nstruct ScrollBGLayer_Args {\n u16 unk0;\n u16 unk2;\n};\nvoid ScrollBGLayer(u8 arg0, struct ScrollBGLayer_Args arg1);", @@ -500,11 +455,8 @@ "sym": "CountCollectedGems", "features": [ "array", - "nested-loop", "branch", - "global", - "loop", - "bitwise" + "global" ], "funcC": "void CountCollectedGems(void) {\n u8 sp0;\n u8 var_ip;\n u8 var_r0;\n u8 var_r3;\n u8 var_r6;\n u8 var_r8;\n\n if ((gUnk_03004670->unk8[5][7] & 0x80) != 0) {\n var_ip = 0;\n var_r8 = 0;\n var_r6 = 0;\n sp0 = 0;\n for (var_r0 = 0; var_r0 < 5; var_r0++) {\n for (var_r3 = 0; var_r3 < 7; var_r3++) {\n if ((var_r3 == 3 || var_r3 == 5) && (gUnk_03004670->unk8[var_r0][var_r3] & 0x7F) == 0x64) {\n var_ip += 1;\n } else if ((var_r3 != 7) && ((gUnk_03004670->unk8[var_r0][var_r3] & 0x7F) == 0x1E)) {\n var_r8 += 1;\n }\n if (gUnk_03004670->unk8[var_r0][var_r3] & 0x80) {\n var_r6 += 1;\n }\n }\n }\n\n if ((gUnk_03004670->unk8[5][0] & 0x7F) == 0x1E) {\n sp0 += 1;\n }\n if ((gUnk_03004670->unk8[5][1] & 0x7F) == 0x1E) {\n sp0 += 1;\n }\n\n if (((gUnk_03004670->unk8[5][0] & 0x7F) == 0x7F) && (var_r6 == 0x23)) {\n gUnk_03004C08.unk0_0 = 4;\n gUnk_03004C08.unk2 = 0;\n gUnk_03004670->unk8[5][0] = 0x80;\n gCallbackQueue.current[1] = UpdateWorldMapNodeAnim;\n return;\n } else if (((gUnk_03004670->unk8[5][1] & 0x7F) == 0x7F) && ((var_r8 + var_ip) > 0x18)) {\n gUnk_03004C08.unk0_0 = 5;\n gUnk_03004C08.unk2 = 0;\n gUnk_03004670->unk8[5][1] = 0x80;\n gCallbackQueue.current[1] = UpdateWorldMapNodeAnim;\n return;\n } else if (((gUnk_03004670->unk8[5][2] & 0x7F) == 0x7F) && ((sp0 + var_ip + var_r8) == 0x25)) {\n gUnk_03004C08.unk0_0 = 6;\n gUnk_03004C08.unk2 = 0;\n gUnk_03004670->unk8[5][2] = 0x80;\n gCallbackQueue.current[1] = UpdateWorldMapNodeAnim;\n return;\n } else {\n // gCallbackQueue.current[1] = GameplayMainLoop;\n }\n } else {\n if (((gUnk_03004670->unk8[0][7] & 0x80) != 0) && ((gUnk_03004670->unk8[1][0] & 0x7F) == 0x7F)) {\n gUnk_03004C08.unk0_0 = 0;\n gUnk_03004C08.unk2 = 0;\n gUnk_03004670->unk8[1][0] &= 0x80;\n gCallbackQueue.current[1] = UpdateWorldMapNodeAnim;\n return;\n } else if (((gUnk_03004670->unk8[1][7] & 0x80) != 0) && ((gUnk_03004670->unk8[2][0] & 0x7F) == 0x7F)) {\n gUnk_03004C08.unk0_0 = 1;\n gUnk_03004C08.unk2 = 0;\n gUnk_03004670->unk8[2][0] &= 0x80;\n gCallbackQueue.current[1] = UpdateWorldMapNodeAnim;\n return;\n } else if (((gUnk_03004670->unk8[2][7] & 0x80) != 0) && ((gUnk_03004670->unk8[3][0] & 0x7F) == 0x7F)) {\n gUnk_03004C08.unk0_0 = 2;\n gUnk_03004C08.unk2 = 0;\n gUnk_03004670->unk8[3][0] &= 0x80;\n gCallbackQueue.current[1] = UpdateWorldMapNodeAnim;\n return;\n } else if (((gUnk_03004670->unk8[3][7] & 0x80) != 0) && ((gUnk_03004670->unk8[4][0] & 0x7F) == 0x7F)) {\n gUnk_03004C08.unk0_0 = 3;\n gUnk_03004C08.unk2 = 0;\n gUnk_03004670->unk8[4][0] &= 0x80;\n gCallbackQueue.current[1] = UpdateWorldMapNodeAnim;\n return;\n } else {\n // gCallbackQueue.current[1] = GameplayMainLoop;\n }\n }\n gCallbackQueue.current[1] = GameplayMainLoop;\n}", "prependC": "extern struct EntityAnimationInfo gEntityAnimationInfo[];\nextern void SetupBG3WindowOverlay(void);\nextern void UpdateScrollPosition(void);\nvoid SetPaletteAnimEntry(s32, u8);\nextern s16 MultiplyQ8(s16, s16);\nextern s16 ReciprocalQ8(s16);\nextern u8 thunk_sub_080002A0();\nextern u8 thunk_sub_080002D0();\nextern void VBlankCallback_Dialog(void);\n/* Cross-module function prototypes referenced by the decompiled functions below.\n * (ROM data symbols live in structs/variables.h.) */\nvoid HBlankScrollUpdate(void);\ns32 Abs(s32 n);\nvoid IntroSequenceUpdate(void);\nvoid TransitionReturnToWorldMap(void);\nvoid TransitionClearAndRestart(void);\nvoid TransitionSoftReset(void);\nvoid TransitionToGameplayScreen(void);\nvoid TransitionGameOver(void);\nvoid TransitionFadeOutFull(void);\nvoid UpdatePlayerFinalBoss(u8 arg0);\nvoid UpdatePlayerAlternate(u8 arg0);\nvoid UpdatePlayerMinigame(u8 arg0);\nvoid VBlankHandler_ModeB(void);\nvoid ReadKeyInput(void);\nvoid VBlankCallback_Gameplay(void);\nvoid AnimatePaletteEffects(void);\nvoid IntroScrollAnimation(void);\n\n/* ── kleod code_08039D8C.c shared scaffolding ─────────────────────────────\n * Common globals, cross-module externs, and helper inlines for the logic\n * functions ported from kleod's code_08039D8C.c. */\n\nextern struct BgDataPtrs gBgDataPtrs; /* 0x03004790 */\nextern u8 gBlendValue; /* 0x03005498 */\n\n/* Tile-collision query result (kleod CheckTileCollisionSloped). */\nstruct Unk_08014184 {\n u16 unk0;\n u8 unk2;\n u8 pad3[0x4 - 0x3];\n};\n\n/* Cross-module functions (defined in code_1). */\nextern struct Unk_08014184 *CheckTileCollisionSloped(struct Unk_08014184 *, u16, u16, u8);\nstatic inline struct Unk_08014184 Call_CheckTileCollisionSloped(u16 arg1, u16 arg2, u8 arg3) {\n struct Unk_08014184 sp;\n CheckTileCollisionSloped(&sp, arg1, arg2, arg3);\n return sp;\n}\nextern void PlayerRespawnOrDeath(s32);\nextern void SpawnEntityAtPosition(u16, u16, u8, u8);\n\n/* Forward decls (code_08039D8C.c cluster). */\nvoid InitGameplayState(void);\nvoid UpdateOamSortOrder(void);\nvoid ProcessInputAndUpdateEntities(void);\nvoid UpdateWorldMapInput(void);\nu8 CheckWorldCompletion(u8 arg0);\nvoid CopyWorldMapTiles(u8 arg0);\nvoid SetWorldMapTilePalette(u8 arg0, u8 arg1);\nvoid UpdateWorldMapNodeTile(u8 arg0);\nvoid UpdateEntities(void);\nvoid CountCollectedGems(void);\nvoid UpdateWorldMapNodeAnim(void);\nvoid GameplayMainLoop(void);\nvoid InitLevelState(void);\nvoid SpawnEntitiesForVision(u8 arg0);\nvoid GetEntityLookupData(u8 arg0);\nvoid ComputeScrollLimits(void);\nvoid ApplyPlayerMovement(u8 arg0, struct Unk_0803D4AC arg1);\nvoid UpdatePlayerNormal(u8 arg0);\nvoid SetupEntitySpawnTable(u8 arg0);\nvoid RollRandomLevelVariant(void);\nvoid UpdatePlayerBoss(u8 arg0);\nvoid ConfigureEntityBehavior(u8 arg0, u8 arg1, u8 arg2);\nvoid TransitionLevelVariant(u8 arg0);\nvoid UpdateLevelProgression(void);\nvoid SetEntityVisibility(u8 arg0);\nvoid UpdatePlayerSpecial(u8 arg0);", @@ -519,8 +471,6 @@ { "sym": "UpdateWorldMapNodeAnim", "features": [ - "switch", - "shift", "array", "global" ], @@ -536,10 +486,8 @@ { "sym": "EntityItemDrop", "features": [ - "switch", "bitfield", "array", - "shift", "struct", "global" ], @@ -557,11 +505,8 @@ "sym": "SetupBG3WindowOverlay", "features": [ "array", - "shift", - "loop", "global", - "memory", - "bitwise" + "memory" ], "funcC": "void SetupBG3WindowOverlay(void) {\n s32 var_r5;\n s32 var_r6;\n s32 var_r7;\n\n if (gUnk_030034BC == 0) {\n var_r7 = gUnk_03000800;\n } else {\n var_r7 = 0;\n }\n\n REG_IE &= ~INTR_FLAG_VBLANK;\n REG_DISPSTAT &= ~DISPSTAT_VBLANK_INTR;\n m4aSoundVSyncOff();\n\n gBgDataPtrs.pBufBg3Tiles = thunk_HeapAlloc(gUnk_082EC8F4 & 0x7FFFFFFF, 0);\n gBgDataPtrs.pBufBg3Tilemap = thunk_HeapAlloc(gUnk_082ECD74 & 0x7FFFFFFF, 0);\n Decompress(gBgDataPtrs.pBufBg3Tiles, &gUnk_082EC8F4);\n Decompress(gBgDataPtrs.pBufBg3Tilemap, &gUnk_082ECD74);\n\n for (var_r5 = 0, var_r6 = 0; var_r5 < 0x21C; var_r6++, var_r5++) {\n if (((var_r5 % 30) == 0) && (var_r5 != 0)) {\n var_r6 += 2;\n }\n gBgTilemapBufs[gUnk_030034BC][var_r6] = gBgDataPtrs.pBufBg3Tilemap[var_r5 + 2] + var_r7;\n }\n\n REG_WIN1H = WIN_RANGE(0, 0xF0);\n REG_WIN1V = WIN_RANGE(0x1E, 0x90);\n\n if (gUnk_030034BC == 0) {\n REG_WININ = WININ_WIN0_BG0 | WININ_WIN0_CLR | WININ_WIN1_BG1 | WININ_WIN1_BG2 | WININ_WIN1_OBJ | WININ_WIN1_CLR;\n } else {\n REG_WININ = WININ_WIN0_BG0 | WININ_WIN0_CLR | WININ_WIN1_BG0 | WININ_WIN1_BG2 | WININ_WIN1_OBJ | WININ_WIN1_CLR;\n }\n\n if (gUnk_03004C20.level == 8) {\n gCallbackQueue.next[2] = AnimatePaletteEffects;\n } else {\n gCallbackQueue.next[2] = VBlankCallback_Gameplay;\n }\n gCallbackQueue.next[0] = ReadKeyInput;\n gCallbackQueue.next[1] = UpdateWorldMapInput;\n gCallbackQueue.next[3] = NULL + 1;\n gCallbackQueue.current[gCallbackQueue.currentCount - 1] = NULL;\n gCallbackQueue.nextCount = 4;\n\n DmaCopy16(3, gBgDataPtrs.pBufBg3Tiles + 4, gBgInfo[gUnk_030034BC].pTiles + (var_r7 << 5), 0xB80);\n\n REG_DISPCNT |= DISPCNT_WIN1_ON;\n REG_IE |= INTR_FLAG_VBLANK;\n REG_DISPSTAT |= DISPSTAT_VBLANK_INTR;\n m4aSoundVSyncOn();\n\n gUnk_03004C20.sceneFrameCounter = 0;\n}", "prependC": "extern struct EntityAnimationInfo gEntityAnimationInfo[];\nextern void SetupBG3WindowOverlay(void);\nextern void UpdateScrollPosition(void);\nvoid SetPaletteAnimEntry(s32, u8);\nextern s16 MultiplyQ8(s16, s16);\nextern s16 ReciprocalQ8(s16);\nextern u8 thunk_sub_080002A0();\nextern u8 thunk_sub_080002D0();\nextern void VBlankCallback_Dialog(void);\n/* Cross-module function prototypes referenced by the decompiled functions below.\n * (ROM data symbols live in structs/variables.h.) */\nvoid HBlankScrollUpdate(void);\ns32 Abs(s32 n);\nvoid IntroSequenceUpdate(void);\nvoid TransitionReturnToWorldMap(void);\nvoid TransitionClearAndRestart(void);\nvoid TransitionSoftReset(void);\nvoid TransitionToGameplayScreen(void);\nvoid TransitionGameOver(void);\nvoid TransitionFadeOutFull(void);\nvoid UpdatePlayerFinalBoss(u8 arg0);\nvoid UpdatePlayerAlternate(u8 arg0);\nvoid UpdatePlayerMinigame(u8 arg0);\nvoid VBlankHandler_ModeB(void);\nvoid ReadKeyInput(void);\nvoid VBlankCallback_Gameplay(void);\nvoid AnimatePaletteEffects(void);\nvoid IntroScrollAnimation(void);\n\n/* ── kleod code_08039D8C.c shared scaffolding ─────────────────────────────\n * Common globals, cross-module externs, and helper inlines for the logic\n * functions ported from kleod's code_08039D8C.c. */\n\nextern struct BgDataPtrs gBgDataPtrs; /* 0x03004790 */\nextern u8 gBlendValue; /* 0x03005498 */\n\n/* Tile-collision query result (kleod CheckTileCollisionSloped). */\nstruct Unk_08014184 {\n u16 unk0;\n u8 unk2;\n u8 pad3[0x4 - 0x3];\n};\n\n/* Cross-module functions (defined in code_1). */\nextern struct Unk_08014184 *CheckTileCollisionSloped(struct Unk_08014184 *, u16, u16, u8);\nstatic inline struct Unk_08014184 Call_CheckTileCollisionSloped(u16 arg1, u16 arg2, u8 arg3) {\n struct Unk_08014184 sp;\n CheckTileCollisionSloped(&sp, arg1, arg2, arg3);\n return sp;\n}\nextern void PlayerRespawnOrDeath(s32);\nextern void SpawnEntityAtPosition(u16, u16, u8, u8);\n\n/* Forward decls (code_08039D8C.c cluster). */\nvoid InitGameplayState(void);\nvoid UpdateOamSortOrder(void);\nvoid ProcessInputAndUpdateEntities(void);\nvoid UpdateWorldMapInput(void);\nu8 CheckWorldCompletion(u8 arg0);\nvoid CopyWorldMapTiles(u8 arg0);\nvoid SetWorldMapTilePalette(u8 arg0, u8 arg1);\nvoid UpdateWorldMapNodeTile(u8 arg0);\nvoid UpdateEntities(void);\nvoid CountCollectedGems(void);\nvoid UpdateWorldMapNodeAnim(void);\nvoid GameplayMainLoop(void);\nvoid InitLevelState(void);\nvoid SpawnEntitiesForVision(u8 arg0);\nvoid GetEntityLookupData(u8 arg0);\nvoid ComputeScrollLimits(void);\nvoid ApplyPlayerMovement(u8 arg0, struct Unk_0803D4AC arg1);\nvoid UpdatePlayerNormal(u8 arg0);\nvoid SetupEntitySpawnTable(u8 arg0);\nvoid RollRandomLevelVariant(void);\nvoid UpdatePlayerBoss(u8 arg0);\nvoid ConfigureEntityBehavior(u8 arg0, u8 arg1, u8 arg2);\nvoid TransitionLevelVariant(u8 arg0);\nvoid UpdateLevelProgression(void);\nvoid SetEntityVisibility(u8 arg0);\nvoid UpdatePlayerSpecial(u8 arg0);\n#include \"m4a_internal.h\"\nvoid m4aSoundVSyncOff(void);", @@ -577,8 +522,7 @@ "sym": "EntityDeathAnimation", "features": [ "struct", - "global", - "bitwise" + "global" ], "funcC": "void EntityDeathAnimation(u8 slot) {\n u8 phase;\n s16 byteMask;\n int newPhase;\n EntityDeathStruct *entities = (EntityDeathStruct *)gEntityArray;\n EntityDeathStruct *entity = &entities[slot];\n EntityDeathStruct *arr;\n EntityDeathStruct *self;\n\n entity->timer = entity->timer - 1;\n byteMask = 0xFF;\n if (entity->timer == 0xFF) {\n entity->timer = 25;\n phase = entity->phase;\n if (phase <= 5) {\n SpawnEntityAtPosition(entities[slot - *gEntityDeathState].x, entities[slot - *gEntityDeathState].y, (u8)(phase + 0x0C), 0);\n }\n newPhase = entity->phase - 1;\n entity->phase = newPhase;\n do {\n if ((newPhase & byteMask) == 0) {\n do {\n SpawnEntityAtPosition(entities[slot - *gEntityDeathState].x, entities[slot - *gEntityDeathState].y, 2,\n (u8)(slot - *gEntityDeathState));\n } while (0);\n } else {\n m4aSongNumStart(0x56);\n if (entity->phase > 9) {\n entities[slot + *gEntityDeathState].typeId = 0;\n LoadSpriteFrame((u8)(slot + *gEntityDeathState), sub_08051A0C(entity->phase, 0x0A));\n }\n LoadSpriteFrame(slot, sub_08051A84(entity->phase, 0x0A));\n if (entity->phase == 9) {\n entities[slot + *gEntityDeathState].typeId = 0x1C; /* inactive */\n entities[slot + *gEntityDeathState].subState = 0;\n }\n }\n } while (0);\n }\n arr = (EntityDeathStruct *)gEntityArray;\n self = &arr[slot];\n byteMask |= 0;\n self->x = arr[slot - *gEntityDeathState].x;\n self->y = arr[slot - *gEntityDeathState].y - 0x20;\n if (self->phase > 9) {\n arr[slot + *gEntityDeathState].x = arr[slot - *gEntityDeathState].x - 3;\n arr[slot + *gEntityDeathState].y = arr[slot - *gEntityDeathState].y - 0x20;\n self->x = self->x + 3;\n }\n}", "prependC": "void m4aSongNumStart(u16);\ntypedef struct EntityDeathStruct {\n u16 x; /* +0x00: screen-space X */\n u16 y; /* +0x02: screen-space Y */\n u8 unk04;\n u8 unk05;\n u8 unk06;\n u8 unk07;\n u8 phase; /* +0x08: death phase counter (decrements each cycle) */\n u8 timer; /* +0x09: frame countdown (resets to 25 on underflow) */\n u8 slotIdx; /* +0x0A */\n u8 unk0B;\n u8 unk0C;\n u8 unk0D;\n u8 unk0E;\n u8 typeId; /* +0x0F: entity type (0x1C = inactive) */\n u8 subState; /* +0x10 */\n u8 behaviorId; /* +0x11 */\n u8 direction; /* +0x12 */\n u8 unk13[9];\n} EntityDeathStruct;\n\nextern void SpawnEntityAtPosition(u16 x, u16 y, u8 type, u8 slot);\nextern u8 sub_08051A0C(u8 a, u8 b);\nextern u8 sub_08051A84(u8 a, u8 b);\nextern void LoadSpriteFrame(u8 frame, u8 tilesetIdx);", diff --git a/apps/benchmark/dataset/real/marioparty3.json b/apps/benchmark/dataset/real/marioparty3.json index 6a654819..680c25cf 100644 --- a/apps/benchmark/dataset/real/marioparty3.json +++ b/apps/benchmark/dataset/real/marioparty3.json @@ -37,8 +37,7 @@ "pointer", "branch", "global", - "arithmetic", - "bitwise" + "arithmetic" ], "funcC": "void func_800600C0_60CC0(s16 winId, s32 arg1) {\n TextWindow *window;\n\n window = &D_800CC69C_CD29C[winId];\n if (arg1 != 0) {\n window->unk_38 |= 0x10;\n } else {\n window->unk_38 &= ~0x10;\n }\n}", "sourceUrl": "https://github.com/macabeus/marioparty3/blob/89c0fafd30375f21222c39bcefd8456bac130c53/src/window.c#L325-L334", @@ -55,7 +54,6 @@ "features": [ "struct", "pointer", - "loop", "branch", "global", "arithmetic" @@ -106,10 +104,8 @@ "features": [ "struct", "pointer", - "loop", "branch", - "arithmetic", - "do-while" + "arithmetic" ], "funcC": "u32 HuMemUsedMemorySizeGet(HeapNode *heap) {\n HeapNode *cur_heap;\n u32 total_size;\n\n cur_heap = heap;\n total_size = 0;\n do {\n if (cur_heap->active == TRUE) {\n total_size += cur_heap->size;\n }\n cur_heap = cur_heap->next;\n } while (cur_heap != heap);\n\n return total_size;\n}", "sourceUrl": "https://github.com/macabeus/marioparty3/blob/89c0fafd30375f21222c39bcefd8456bac130c53/src/malloc.c#L139-L153", @@ -120,7 +116,6 @@ "features": [ "struct", "pointer", - "loop", "global", "arithmetic", "union", @@ -173,11 +168,7 @@ "features": [ "struct", "pointer", - "loop", - "arithmetic", - "do-while", - "ternary", - "bitwise" + "arithmetic" ], "funcC": "u32 HuMemUsedMemoryBlockGet(HeapNode *heap) {\n HeapNode *cur_heap;\n u32 count_free;\n\n cur_heap = heap;\n count_free = 0;\n do {\n count_free += (cur_heap->active ^ TRUE) == FALSE ? 1 : 0;\n cur_heap = cur_heap->next;\n } while (cur_heap != heap);\n\n return count_free;\n}", "sourceUrl": "https://github.com/macabeus/marioparty3/blob/89c0fafd30375f21222c39bcefd8456bac130c53/src/malloc.c#L158-L170", @@ -186,11 +177,9 @@ { "sym": "func_80045BF0_467F0", "features": [ - "loop", "branch", "global", - "arithmetic", - "bitwise" + "arithmetic" ], "funcC": "s32 func_80045BF0_467F0(s32 arg0) {\n u32 var_v0;\n u32 var_v1;\n\n for (var_v1 = 0; var_v1 < 9; var_v1++) {\n if ((arg0 & 0xFF) == D_800A1590_A2190[var_v1]) {\n break;\n }\n }\n if (var_v1 >= 9) {\n var_v1 = -1;\n }\n return var_v1;\n}", "sourceUrl": "https://github.com/macabeus/marioparty3/blob/89c0fafd30375f21222c39bcefd8456bac130c53/src/pause.c#L217-L230", @@ -241,11 +230,9 @@ "sym": "func_8005FA90_60690", "features": [ "pointer", - "loop", "branch", "global", - "arithmetic", - "nested-loop" + "arithmetic" ], "funcC": "u8 func_8005FA90_60690(u8 *arg0) {\n s16 i, j;\n\n for (i = 0; i < 10; i += 2) {\n if (D_800A232C_A2F2C[i] == arg0[0] && D_800A232C_A2F2C[i + 1] == arg0[1]) {\n return i / 2;\n }\n }\n for (i = 0; i < 16; i++) {\n for (j = 0; j < 32; j += 2) {\n if (D_800A2344_A2F44[i][j] == arg0[0] && D_800A2344_A2F44[i][j + 1] == arg0[1]) {\n return i * 16 + j / 2;\n }\n }\n }\n return 16;\n}", "sourceUrl": "https://github.com/macabeus/marioparty3/blob/89c0fafd30375f21222c39bcefd8456bac130c53/src/window.c#L297-L313", @@ -256,8 +243,7 @@ "sym": "func_80047E90_48A90", "features": [ "struct", - "pointer", - "bitwise" + "pointer" ], "funcC": "void func_80047E90_48A90(omObjData *obj, s32 arg1) {\n obj->unk10 |= arg1;\n}", "sourceUrl": "https://github.com/macabeus/marioparty3/blob/89c0fafd30375f21222c39bcefd8456bac130c53/src/objmain.c#L516-L518", @@ -289,8 +275,7 @@ "features": [ "struct", "pointer", - "global", - "bitwise" + "global" ], "funcC": "HuSprite_Unk84_Unk00_Struct *func_800560D8_56CD8(s16 arg0, u16 arg1) {\n return &D_800C9530_CA130[arg0]->unk00[arg1];\n}", "sourceUrl": "https://github.com/macabeus/marioparty3/blob/89c0fafd30375f21222c39bcefd8456bac130c53/src/sprman.c#L636-L638", @@ -450,8 +435,7 @@ "sym": "func_80045314_45F14", "features": [ "branch", - "arithmetic", - "switch" + "arithmetic" ], "funcC": "s32 func_80045314_45F14(void) {\n switch (omovl) {\n case 2:\n return 20;\n case 3:\n return -20;\n default:\n return 0;\n }\n}", "sourceUrl": "https://github.com/macabeus/marioparty3/blob/89c0fafd30375f21222c39bcefd8456bac130c53/src/gamemes.c#L743-L752", @@ -461,8 +445,7 @@ "sym": "func_80056C98_57898", "features": [ "pointer", - "arithmetic", - "shift" + "arithmetic" ], "funcC": "u32 func_80056C98_57898(u8 **arg0) {\n u8 *p = *arg0;\n u32 val = (p[0] << 24) + (p[1] << 16) + (p[2] << 8) + p[3];\n\n *arg0 = p + 4;\n return val;\n}", "sourceUrl": "https://github.com/macabeus/marioparty3/blob/89c0fafd30375f21222c39bcefd8456bac130c53/src/sprman.c#L777-L783" @@ -488,7 +471,6 @@ "sym": "func_80011460_12060", "features": [ "pointer", - "loop", "arithmetic" ], "funcC": "s16 func_80011460_12060(u8 *arg0) {\n s32 sum = 0;\n s16 i = 1;\n u8 c = arg0[0];\n\n while (c != 0 && i < 28) {\n sum += i * c;\n arg0++;\n i++;\n c = arg0[0];\n }\n return sum;\n}", @@ -514,8 +496,7 @@ "features": [ "pointer", "branch", - "arithmetic", - "bitwise" + "arithmetic" ], "funcC": "u8 *GWBoardRecordGet(s16 arg0) {\n s16 idx = arg0;\n\n if (arg0 < 0) {\n idx = GwSystem.current_board_index;\n }\n\n if (GwSystem.playMode & 1) {\n return (u8 *)&GwCommon.unk_1D[2] + idx * 9;\n }\n return (u8 *)&GwCommon.unk_1D[0x38] + idx * 9;\n}", "sourceUrl": "https://github.com/macabeus/marioparty3/blob/89c0fafd30375f21222c39bcefd8456bac130c53/src/save.c#L80-L91" @@ -523,12 +504,9 @@ { "sym": "ContDataUpdate", "features": [ - "shift", - "loop", "array", "global", - "branch", - "bitwise" + "branch" ], "funcC": "void ContDataUpdate(void) {\n s16 i;\n s32 temp_v0_3;\n s32 temp_v0_5;\n u16 var_a2;\n s8 temp_v0_2;\n s8 temp_v0_4;\n\n for (i = 0; i < MAXCONTROLLERS; ++i) {\n D_800D1350_D1F50[i] = D_800D5546_D6146[i];\n var_a2 = D_800CDA7C_CE67C[i];\n temp_v0_2 = D_800CBB6E_CC76E[i];\n temp_v0_3 = ((temp_v0_2 >> 7) | 1);\n\n if (((temp_v0_2 * temp_v0_3) - 0x1E) > 0) {\n if (temp_v0_3 > 0) {\n var_a2 = (var_a2 & 0xFDFF) | 0x100;\n } else {\n var_a2 = (var_a2 & 0xFEFF) | 0x200;\n }\n }\n\n temp_v0_4 = D_800D20A1_D2CA1[i];\n temp_v0_5 = ((temp_v0_4 >> 7) | 1);\n\n if (((temp_v0_4 * temp_v0_5) - 0x1E) > 0) {\n if (temp_v0_5 > 0) {\n var_a2 = (var_a2 & 0xFBFF) | 0x800;\n } else {\n var_a2 = (var_a2 & 0xF7FF) | 0x400;\n }\n }\n\n D_800D5546_D6146[i] = var_a2;\n D_800C9520_CA120[i] = var_a2 & (var_a2 ^ D_800D1350_D1F50[i]);\n\n if ((var_a2 & 0xFFFF) != D_800D1350_D1F50[i]) {\n D_800D10F8_D1CF8[i] = 0xA;\n D_800D0590_D1190[i] = D_800C9520_CA120[i];\n } else {\n if (D_800D10F8_D1CF8[i] > 0) {\n D_800D10F8_D1CF8[i] = D_800D10F8_D1CF8[i] - 1;\n D_800D0590_D1190[i] = D_800C9520_CA120[i];\n } else {\n D_800D10F8_D1CF8[i] = 1;\n D_800D0590_D1190[i] = var_a2;\n }\n }\n }\n}", "prependC": "#include \"common.h\"\n#include \"input.h\"", @@ -579,7 +557,6 @@ { "sym": "DecodeData", "features": [ - "switch", "pointer", "branch" ], @@ -598,9 +575,7 @@ "features": [ "struct", "pointer", - "branch", - "loop", - "nested-loop" + "branch" ], "funcC": "void func_800230F8_23CF8(HmfData *arg0) {\n s16 textureIndex;\n s16 i, j;\n\n textureIndex = 4; // TODO: why does this start at 4?\n\n for (i = 0; i < arg0->unk12; i++) {\n /* Skip entries marked as 0xFF */\n if (arg0->unk84[i].unk9 == 0xFF) {\n continue;\n }\n\n /* Find first entry with different unkC value */\n for (j = 0; j < i; j++) {\n if (arg0->unk84[i].unkC == arg0->unk84[j].unkC) {\n break;\n }\n }\n\n /* Copy or assign texture indices */\n if (j != i) {\n arg0->unk84[i].unk3 = arg0->unk84[j].unk3;\n arg0->unk84[i].unk4 = arg0->unk84[j].unk4;\n } else {\n arg0->unk84[i].unk3 = textureIndex++;\n if (arg0->unk84[i].unkC->unk_30 != NULL) {\n arg0->unk84[i].unk4 = textureIndex++;\n }\n\n if (textureIndex > 16) {\n osSyncPrintf(\"Texture Anime Over\\n\");\n return;\n }\n }\n }\n}", "prependC": "#include \"common.h\"\n#include \"mallocblock.h\"", @@ -617,7 +592,6 @@ "features": [ "struct", "pointer", - "loop", "global" ], "funcC": "s16 HuSprGrpCreate(u16 arg0, u16 arg1) {\n HuSprGrp *temp_v0_2;\n HuSprite **var_s2;\n HuSprite *temp_v0_3;\n s16 var_s4;\n s16 i;\n\n for (i = 0; i < HUSPR_GRP_MAX; i++) {\n if (HuSprGrpData[i] == NULL) {\n break;\n }\n }\n if (i == HUSPR_GRP_MAX) {\n return -1;\n }\n var_s4 = i;\n temp_v0_2 = func_80052468_53068(arg0, arg1);\n if (temp_v0_2 == NULL) {\n return -1;\n }\n HuSprGrpData[var_s4] = temp_v0_2;\n temp_v0_2->unk_0A = 0;\n temp_v0_2->unk_0C = 1;\n var_s2 = temp_v0_2->members;\n for (i = 0; i < arg0; i++) {\n *(var_s2++) = temp_v0_3 = HuMemAlloc(0x210);\n if (temp_v0_3 == NULL) {\n HuSprGrpKill(var_s4);\n return -1;\n }\n temp_v0_2->unk_0A++;\n }\n func_80052E68_53A68(temp_v0_2, arg0);\n return var_s4;\n}", @@ -628,12 +602,9 @@ { "sym": "FileSeek", "features": [ - "switch", "branch", "struct", - "pointer", - "ternary", - "bitwise" + "pointer" ], "funcC": "void FileSeek(HuFileInfoD *info, s32 arg1, s32 arg2) {\n switch (arg2) {\n case 0:\n arg2 = (u32)(info->bytes + arg1);\n break;\n case 1:\n arg2 = (u32)(info->bytesCopy + info->unkE + arg1);\n break;\n case 2:\n arg2 = (u32)(info->bytes + info->size + arg1);\n break;\n default:\n return;\n }\n arg2 = ((u32)arg2 < (u32)info->bytes) ? (u32)info->bytes : (u32)arg2;\n arg2 = ((u32)arg2 >= (u32)(info->bytes + info->size)) ? (u32)(info->bytes + info->size - 1) : (u32)arg2;\n\n if (((u32)arg2 < (u32)info->bytesCopy) || ((u32)arg2 >= (u32)(info->bytesCopy + 0x400))) {\n info->unkC = 1;\n info->unkE = arg2 & 1;\n info->bytesCopy = (u8 *)(arg2 - info->unkE);\n } else {\n info->unkE = arg2 - (u32)info->bytesCopy;\n }\n}", "prependC": "#include \"common.h\"\n#include \"game/data.h\"\n#include \"rom.h\"\n#include \"malloc.h\"\n#include \"mallocblock.h\"\n#include \"decode.h\"", @@ -647,10 +618,8 @@ { "sym": "HuMemAllocTag", "features": [ - "sizeof", "pointer", "struct", - "bitwise", "fnptr", "global" ], diff --git a/apps/benchmark/dataset/real/pokeemerald.json b/apps/benchmark/dataset/real/pokeemerald.json index 8c68570c..d75e5331 100644 --- a/apps/benchmark/dataset/real/pokeemerald.json +++ b/apps/benchmark/dataset/real/pokeemerald.json @@ -36,8 +36,7 @@ "sym": "MathUtil_Mul16Shift", "features": [ "arithmetic", - "fixed-point", - "shift" + "fixed-point" ], "funcC": "s16 MathUtil_Mul16Shift(u8 s, s16 x, s16 y)\n{\n s32 result;\n\n result = x;\n result *= y;\n result /= (1 << s);\n return result;\n}", "sourceUrl": "https://github.com/macabeus/pokeemerald/blob/25ffb2c12c069ac6b2040f9238b2978f1de72e3f/src/math_util.c#L13-L21" @@ -56,8 +55,7 @@ "sym": "MathUtil_Div16", "features": [ "arithmetic", - "branch", - "shift" + "branch" ], "funcC": "s16 MathUtil_Div16(s16 x, s16 y)\n{\n if (y == 0)\n {\n return 0;\n }\n return (x << 8) / y;\n}", "sourceUrl": "https://github.com/macabeus/pokeemerald/blob/25ffb2c12c069ac6b2040f9238b2978f1de72e3f/src/math_util.c#L33-L40" @@ -66,8 +64,7 @@ "sym": "MathUtil_Div16Shift", "features": [ "arithmetic", - "branch", - "shift" + "branch" ], "funcC": "s16 MathUtil_Div16Shift(u8 s, s16 x, s16 y)\n{\n if (y == 0)\n {\n return 0;\n }\n return (x << s) / y;\n}", "sourceUrl": "https://github.com/macabeus/pokeemerald/blob/25ffb2c12c069ac6b2040f9238b2978f1de72e3f/src/math_util.c#L42-L49" @@ -85,8 +82,7 @@ "features": [ "memory", "pointer", - "array", - "shift" + "array" ], "funcC": "void StoreWordInTwoHalfwords(u16 *h, u32 w)\n{\n h[0] = (u16)(w);\n h[1] = (u16)(w >> 16);\n}", "proto": { @@ -101,9 +97,7 @@ "features": [ "memory", "pointer", - "array", - "bitwise", - "shift" + "array" ], "funcC": "void LoadWordFromTwoHalfwords(u16 *h, u32 *w)\n{\n *w = h[0] | (s16)h[1] << 16;\n}", "proto": { @@ -116,10 +110,7 @@ { "sym": "CountTrailingZeroBits", "features": [ - "bitwise", - "loop", - "branch", - "shift" + "branch" ], "funcC": "int CountTrailingZeroBits(u32 value)\n{\n u8 i;\n\n for (i = 0; i < 32; i++)\n {\n if ((value & 1) == 0)\n value >>= 1;\n else\n return i;\n }\n return 0;\n}", "sourceUrl": "https://github.com/macabeus/pokeemerald/blob/25ffb2c12c069ac6b2040f9238b2978f1de72e3f/src/util.c#L208-L220" @@ -128,7 +119,6 @@ "sym": "CalcByteArraySum", "features": [ "arithmetic", - "loop", "pointer", "array" ], @@ -138,11 +128,7 @@ { "sym": "CalcCRC16", "features": [ - "bitwise", - "loop", - "branch", - "nested-loop", - "shift" + "branch" ], "funcC": "u16 CalcCRC16(const u8 *data, s32 length)\n{\n u16 i, j;\n u16 crc = 0x1121;\n\n for (i = 0; i < length; i++)\n {\n crc ^= data[i];\n for (j = 0; j < 8; j++)\n {\n if (crc & 1)\n crc = (crc >> 1) ^ 0x8408;\n else\n crc >>= 1;\n }\n }\n return ~crc;\n}", "sourceUrl": "https://github.com/macabeus/pokeemerald/blob/25ffb2c12c069ac6b2040f9238b2978f1de72e3f/src/util.c#L222-L239" @@ -151,8 +137,7 @@ "sym": "Random", "features": [ "global", - "arithmetic", - "shift" + "arithmetic" ], "funcC": "u16 Random(void)\n{\n gRngValue = ISO_RANDOMIZE1(gRngValue);\n sRandCount++;\n return gRngValue >> 16;\n}", "prependC": "#include \"random.h\"\nstatic u32 sRandCount;", @@ -161,7 +146,6 @@ { "sym": "StringLength", "features": [ - "loop", "pointer", "array", "branch" @@ -173,7 +157,6 @@ { "sym": "StringCompare", "features": [ - "loop", "branch", "pointer", "arithmetic" @@ -185,7 +168,6 @@ { "sym": "StringCompareN", "features": [ - "loop", "branch", "pointer", "arithmetic" @@ -197,7 +179,6 @@ { "sym": "StringCopy", "features": [ - "loop", "pointer", "branch" ], @@ -208,7 +189,6 @@ { "sym": "StringAppend", "features": [ - "loop", "pointer", "branch" ], @@ -220,7 +200,6 @@ { "sym": "IsStringLengthAtLeast", "features": [ - "loop", "branch", "pointer", "array" @@ -232,10 +211,8 @@ { "sym": "StringCopyN", "features": [ - "loop", "pointer", - "array", - "bitwise" + "array" ], "funcC": "u8 *StringCopyN(u8 *dest, const u8 *src, u8 n)\n{\n u16 i;\n\n for (i = 0; i < n; i++)\n dest[i] = src[i];\n\n return &dest[n];\n}", "sourceUrl": "https://github.com/macabeus/pokeemerald/blob/25ffb2c12c069ac6b2040f9238b2978f1de72e3f/src/string_util.c#L96-L104" @@ -243,12 +220,9 @@ { "sym": "CalcCRC16WithTable", "features": [ - "loop", "array", - "bitwise", "cast", - "table", - "shift" + "table" ], "funcC": "u16 CalcCRC16WithTable(const u8 *data, u32 length)\n{\n u16 i;\n u16 crc = 0x1121;\n u8 byte;\n\n for (i = 0; i < length; i++)\n {\n byte = crc >> 8;\n crc ^= data[i];\n crc = byte ^ sCrc16Table[(u8)crc];\n }\n return ~crc;\n}", "prependC": "static const u16 sCrc16Table[] = {0x0000,0x1189,0x2312,0x329B,0x4624,0x57AD,0x6536,0x74BF,0x8C48,0x9DC1,0xAF5A,0xBED3,0xCA6C,0xDBE5,0xE97E,0xF8F7,0x1081,0x0108,0x3393,0x221A,0x56A5,0x472C,0x75B7,0x643E,0x9CC9,0x8D40,0xBFDB,0xAE52,0xDAED,0xCB64,0xF9FF,0xE876,0x2102,0x308B,0x0210,0x1399,0x6726,0x76AF,0x4434,0x55BD,0xAD4A,0xBCC3,0x8E58,0x9FD1,0xEB6E,0xFAE7,0xC87C,0xD9F5,0x3183,0x200A,0x1291,0x0318,0x77A7,0x662E,0x54B5,0x453C,0xBDCB,0xAC42,0x9ED9,0x8F50,0xFBEF,0xEA66,0xD8FD,0xC974,0x4204,0x538D,0x6116,0x709F,0x0420,0x15A9,0x2732,0x36BB,0xCE4C,0xDFC5,0xED5E,0xFCD7,0x8868,0x99E1,0xAB7A,0xBAF3,0x5285,0x430C,0x7197,0x601E,0x14A1,0x0528,0x37B3,0x263A,0xDECD,0xCF44,0xFDDF,0xEC56,0x98E9,0x8960,0xBBFB,0xAA72,0x6306,0x728F,0x4014,0x519D,0x2522,0x34AB,0x0630,0x17B9,0xEF4E,0xFEC7,0xCC5C,0xDDD5,0xA96A,0xB8E3,0x8A78,0x9BF1,0x7387,0x620E,0x5095,0x411C,0x35A3,0x242A,0x16B1,0x0738,0xFFCF,0xEE46,0xDCDD,0xCD54,0xB9EB,0xA862,0x9AF9,0x8B70,0x8408,0x9581,0xA71A,0xB693,0xC22C,0xD3A5,0xE13E,0xF0B7,0x0840,0x19C9,0x2B52,0x3ADB,0x4E64,0x5FED,0x6D76,0x7CFF,0x9489,0x8500,0xB79B,0xA612,0xD2AD,0xC324,0xF1BF,0xE036,0x18C1,0x0948,0x3BD3,0x2A5A,0x5EE5,0x4F6C,0x7DF7,0x6C7E,0xA50A,0xB483,0x8618,0x9791,0xE32E,0xF2A7,0xC03C,0xD1B5,0x2942,0x38CB,0x0A50,0x1BD9,0x6F66,0x7EEF,0x4C74,0x5DFD,0xB58B,0xA402,0x9699,0x8710,0xF3AF,0xE226,0xD0BD,0xC134,0x39C3,0x284A,0x1AD1,0x0B58,0x7FE7,0x6E6E,0x5CF5,0x4D7C,0xC60C,0xD785,0xE51E,0xF497,0x8028,0x91A1,0xA33A,0xB2B3,0x4A44,0x5BCD,0x6956,0x78DF,0x0C60,0x1DE9,0x2F72,0x3EFB,0xD68D,0xC704,0xF59F,0xE416,0x90A9,0x8120,0xB3BB,0xA232,0x5AC5,0x4B4C,0x79D7,0x685E,0x1CE1,0x0D68,0x3FF3,0x2E7A,0xE70E,0xF687,0xC41C,0xD595,0xA12A,0xB0A3,0x8238,0x93B1,0x6B46,0x7ACF,0x4854,0x59DD,0x2D62,0x3CEB,0x0E70,0x1FF9,0xF78F,0xE606,0xD49D,0xC514,0xB1AB,0xA022,0x92B9,0x8330,0x7BC7,0x6A4E,0x58D5,0x495C,0x3DE3,0x2C6A,0x1EF1,0x0F78};", @@ -258,7 +232,6 @@ { "sym": "ModifyStatByNature", "features": [ - "switch", "branch", "arithmetic", "global" @@ -270,11 +243,9 @@ { "sym": "GetGenderFromSpeciesAndPersonality", "features": [ - "switch", "branch", "global", - "struct", - "bitwise" + "struct" ], "funcC": "u8 GetGenderFromSpeciesAndPersonality(u16 species, u32 personality)\n{\n switch (gSpeciesInfo[species].genderRatio)\n {\n case MON_MALE:\n case MON_FEMALE:\n case MON_GENDERLESS:\n return gSpeciesInfo[species].genderRatio;\n }\n\n if (gSpeciesInfo[species].genderRatio > (personality & 0xFF))\n return MON_FEMALE;\n else\n return MON_MALE;\n}", "m2cCtx": true, @@ -284,7 +255,6 @@ "sym": "Sin2", "features": [ "arithmetic", - "bitwise", "branch", "table" ], @@ -322,8 +292,7 @@ "sym": "PutFirstMemBlockHeader", "features": [ "pointer", - "struct", - "sizeof" + "struct" ], "funcC": "void PutFirstMemBlockHeader(void *block, u32 size)\n{\n PutMemBlockHeader(block, (struct MemBlock *)block, (struct MemBlock *)block, size - sizeof(struct MemBlock));\n}", "prependC": "#define MALLOC_SYSTEM_ID 0xA3A3\nstruct MemBlock {\n bool16 flag;\n u16 magic;\n u32 size;\n struct MemBlock *prev;\n struct MemBlock *next;\n u8 data[0];\n};\nvoid PutMemBlockHeader(void *block, struct MemBlock *prev, struct MemBlock *next, u32 size);", @@ -365,7 +334,6 @@ { "sym": "TranslateBigMonSizeTableIndex", "features": [ - "loop", "branch", "array", "struct", @@ -378,7 +346,6 @@ { "sym": "FeebasRandom", "features": [ - "shift", "global", "macro" ], @@ -390,8 +357,6 @@ "sym": "CalculatePPWithBonus", "features": [ "arithmetic", - "shift", - "bitwise", "global", "table" ], @@ -419,7 +384,6 @@ "branch", "global", "struct", - "bitwise", "bitfield" ], "funcC": "u8 TrySetCantSelectMoveBattleScript(void)\n{\n u8 limitations = 0;\n u16 move = gBattleMons[gActiveBattler].moves[gBattleBufferB[gActiveBattler][2]];\n u8 holdEffect;\n u16 *choicedMove = &gBattleStruct->choicedMove[gActiveBattler];\n\n if (gDisableStructs[gActiveBattler].disabledMove == move && move != MOVE_NONE)\n {\n gBattleScripting.battler = gActiveBattler;\n gCurrentMove = move;\n if (gBattleTypeFlags & BATTLE_TYPE_PALACE)\n {\n gPalaceSelectionBattleScripts[gActiveBattler] = BattleScript_SelectingDisabledMoveInPalace;\n gProtectStructs[gActiveBattler].palaceUnableToUseMove = TRUE;\n }\n else\n {\n gSelectionBattleScripts[gActiveBattler] = BattleScript_SelectingDisabledMove;\n limitations = 1;\n }\n }\n\n if (move == gLastMoves[gActiveBattler] && move != MOVE_STRUGGLE && (gBattleMons[gActiveBattler].status2 & STATUS2_TORMENT))\n {\n CancelMultiTurnMoves(gActiveBattler);\n if (gBattleTypeFlags & BATTLE_TYPE_PALACE)\n {\n gPalaceSelectionBattleScripts[gActiveBattler] = BattleScript_SelectingTormentedMoveInPalace;\n gProtectStructs[gActiveBattler].palaceUnableToUseMove = TRUE;\n }\n else\n {\n gSelectionBattleScripts[gActiveBattler] = BattleScript_SelectingTormentedMove;\n limitations++;\n }\n }\n\n if (gDisableStructs[gActiveBattler].tauntTimer != 0 && gBattleMoves[move].power == 0)\n {\n gCurrentMove = move;\n if (gBattleTypeFlags & BATTLE_TYPE_PALACE)\n {\n gPalaceSelectionBattleScripts[gActiveBattler] = BattleScript_SelectingNotAllowedMoveTauntInPalace;\n gProtectStructs[gActiveBattler].palaceUnableToUseMove = TRUE;\n }\n else\n {\n gSelectionBattleScripts[gActiveBattler] = BattleScript_SelectingNotAllowedMoveTaunt;\n limitations++;\n }\n }\n\n if (GetImprisonedMovesCount(gActiveBattler, move))\n {\n gCurrentMove = move;\n if (gBattleTypeFlags & BATTLE_TYPE_PALACE)\n {\n gPalaceSelectionBattleScripts[gActiveBattler] = BattleScript_SelectingImprisonedMoveInPalace;\n gProtectStructs[gActiveBattler].palaceUnableToUseMove = TRUE;\n }\n else\n {\n gSelectionBattleScripts[gActiveBattler] = BattleScript_SelectingImprisonedMove;\n limitations++;\n }\n }\n\n if (gBattleMons[gActiveBattler].item == ITEM_ENIGMA_BERRY)\n holdEffect = gEnigmaBerries[gActiveBattler].holdEffect;\n else\n holdEffect = GetItemHoldEffect(gBattleMons[gActiveBattler].item);\n\n gPotentialItemEffectBattler = gActiveBattler;\n\n if (holdEffect == HOLD_EFFECT_CHOICE_BAND && *choicedMove != MOVE_NONE && *choicedMove != MOVE_UNAVAILABLE && *choicedMove != move)\n {\n gCurrentMove = *choicedMove;\n gLastUsedItem = gBattleMons[gActiveBattler].item;\n if (gBattleTypeFlags & BATTLE_TYPE_PALACE)\n {\n gProtectStructs[gActiveBattler].palaceUnableToUseMove = TRUE;\n }\n else\n {\n gSelectionBattleScripts[gActiveBattler] = BattleScript_SelectingNotAllowedMoveChoiceItem;\n limitations++;\n }\n }\n\n if (gBattleMons[gActiveBattler].pp[gBattleBufferB[gActiveBattler][2]] == 0)\n {\n if (gBattleTypeFlags & BATTLE_TYPE_PALACE)\n {\n gProtectStructs[gActiveBattler].palaceUnableToUseMove = TRUE;\n }\n else\n {\n gSelectionBattleScripts[gActiveBattler] = BattleScript_SelectingMoveWithNoPP;\n limitations++;\n }\n }\n\n return limitations;\n}", @@ -430,11 +394,9 @@ { "sym": "SetMauvilleOldManLanguage", "features": [ - "switch", "union", "struct", - "memory", - "loop" + "memory" ], "funcC": "static void UNUSED SetMauvilleOldManLanguage(union OldMan *oldMan, u32 language1, u32 language2, u32 language3)\n{\n s32 i;\n\n switch (oldMan->common.id)\n {\n case MAUVILLE_MAN_TRADER:\n {\n struct MauvilleOldManTrader *trader = &oldMan->trader;\n\n for (i = 0; i < NUM_TRADER_ITEMS; i++)\n {\n if (IsStringJapanese(trader->playerNames[i]))\n trader->language[i] = language1;\n else\n trader->language[i] = language2;\n }\n }\n break;\n case MAUVILLE_MAN_STORYTELLER:\n {\n struct MauvilleManStoryteller *storyteller = &oldMan->storyteller;\n\n for (i = 0; i < NUM_STORYTELLER_TALES; i++)\n {\n if (IsStringJapanese(storyteller->trainerNames[i]))\n storyteller->language[i] = language1;\n else\n storyteller->language[i] = language2;\n }\n }\n break;\n case MAUVILLE_MAN_BARD:\n {\n struct MauvilleManBard *bard = &oldMan->bard;\n\n if (language3 == LANGUAGE_JAPANESE)\n bard->language = language1;\n else\n bard->language = language2;\n }\n break;\n case MAUVILLE_MAN_HIPSTER:\n {\n struct MauvilleManHipster *hipster = &oldMan->hipster;\n\n if (language3 == LANGUAGE_JAPANESE)\n hipster->language = language1;\n else\n hipster->language = language2;\n }\n break;\n case MAUVILLE_MAN_GIDDY:\n {\n struct MauvilleManGiddy *giddy = &oldMan->giddy;\n\n if (language3 == LANGUAGE_JAPANESE)\n giddy->language = language1;\n else\n giddy->language = language2;\n }\n break;\n }\n}", "prependC": "#include \"global.h\"\n#include \"main.h\"\n#include \"constants/songs.h\"\n#include \"constants/event_objects.h\"\n#include \"mauville_old_man.h\"\n#include \"event_data.h\"\n#include \"string_util.h\"\n#include \"text.h\"\n#include \"easy_chat.h\"\n#include \"script.h\"\n#include \"random.h\"\n#include \"event_scripts.h\"\n#include \"task.h\"\n#include \"menu.h\"\n#include \"m4a.h\"\n#include \"bard_music.h\"\n#include \"sound.h\"\n#include \"strings.h\"\n#include \"overworld.h\"\n#include \"field_message_box.h\"\n#include \"script_menu.h\"\n#include \"trader.h\"\n#include \"m4a.h\"\n#include \"constants/mauville_old_man.h\"", @@ -464,7 +426,6 @@ "features": [ "fnptr", "branch", - "bitwise", "global" ], "funcC": "static bool8 DoForcedMovement(u8 direction, void (*moveFunc)(u8))\n{\n struct PlayerAvatar *playerAvatar = &gPlayerAvatar;\n u8 collision = CheckForPlayerAvatarCollision(direction);\n\n playerAvatar->flags |= PLAYER_AVATAR_FLAG_FORCED_MOVE;\n if (collision)\n {\n ForcedMovement_None();\n if (collision < COLLISION_STOP_SURFING)\n {\n return FALSE;\n }\n else\n {\n if (collision == COLLISION_LEDGE_JUMP)\n PlayerJumpLedge(direction);\n playerAvatar->flags |= PLAYER_AVATAR_FLAG_FORCED_MOVE;\n playerAvatar->runningState = MOVING;\n return TRUE;\n }\n }\n else\n {\n playerAvatar->runningState = MOVING;\n moveFunc(direction);\n return TRUE;\n }\n}", @@ -475,12 +436,8 @@ { "sym": "GetMoveTarget", "features": [ - "switch", - "do-while", "array", - "global", - "loop", - "bitwise" + "global" ], "funcC": "u8 GetMoveTarget(u16 move, u8 setTarget)\n{\n u8 targetBattler = 0;\n u8 moveTarget;\n u8 side;\n\n if (setTarget != NO_TARGET_OVERRIDE)\n moveTarget = setTarget - 1;\n else\n moveTarget = gBattleMoves[move].target;\n\n switch (moveTarget)\n {\n case MOVE_TARGET_SELECTED:\n side = BATTLE_OPPOSITE(GetBattlerSide(gBattlerAttacker));\n if (gSideTimers[side].followmeTimer && gBattleMons[gSideTimers[side].followmeTarget].hp)\n {\n targetBattler = gSideTimers[side].followmeTarget;\n }\n else\n {\n side = GetBattlerSide(gBattlerAttacker);\n do\n {\n targetBattler = Random() % gBattlersCount;\n } while (targetBattler == gBattlerAttacker || side == GetBattlerSide(targetBattler) || gAbsentBattlerFlags & gBitTable[targetBattler]);\n if (gBattleMoves[move].type == TYPE_ELECTRIC\n && AbilityBattleEffects(ABILITYEFFECT_COUNT_OTHER_SIDE, gBattlerAttacker, ABILITY_LIGHTNING_ROD, 0, 0)\n && gBattleMons[targetBattler].ability != ABILITY_LIGHTNING_ROD)\n {\n targetBattler ^= BIT_FLANK;\n RecordAbilityBattle(targetBattler, gBattleMons[targetBattler].ability);\n gSpecialStatuses[targetBattler].lightningRodRedirected = 1;\n }\n }\n break;\n case MOVE_TARGET_DEPENDS:\n case MOVE_TARGET_BOTH:\n case MOVE_TARGET_FOES_AND_ALLY:\n case MOVE_TARGET_OPPONENTS_FIELD:\n targetBattler = GetBattlerAtPosition(BATTLE_OPPOSITE(GET_BATTLER_SIDE(gBattlerAttacker)));\n if (gAbsentBattlerFlags & gBitTable[targetBattler])\n targetBattler ^= BIT_FLANK;\n break;\n case MOVE_TARGET_RANDOM:\n side = BATTLE_OPPOSITE(GetBattlerSide(gBattlerAttacker));\n if (gSideTimers[side].followmeTimer && gBattleMons[gSideTimers[side].followmeTarget].hp)\n {\n targetBattler = gSideTimers[side].followmeTarget;\n }\n else if (gBattleTypeFlags & BATTLE_TYPE_DOUBLE && moveTarget & MOVE_TARGET_RANDOM)\n {\n if (GetBattlerSide(gBattlerAttacker) == B_SIDE_PLAYER)\n {\n if (Random() & 1)\n targetBattler = GetBattlerAtPosition(B_POSITION_OPPONENT_LEFT);\n else\n targetBattler = GetBattlerAtPosition(B_POSITION_OPPONENT_RIGHT);\n }\n else\n {\n if (Random() & 1)\n targetBattler = GetBattlerAtPosition(B_POSITION_PLAYER_LEFT);\n else\n targetBattler = GetBattlerAtPosition(B_POSITION_PLAYER_RIGHT);\n }\n if (gAbsentBattlerFlags & gBitTable[targetBattler])\n targetBattler ^= BIT_FLANK;\n }\n else\n {\n targetBattler = GetBattlerAtPosition(BATTLE_OPPOSITE(GET_BATTLER_SIDE(gBattlerAttacker)));\n }\n break;\n case MOVE_TARGET_USER_OR_SELECTED:\n case MOVE_TARGET_USER:\n targetBattler = gBattlerAttacker;\n break;\n }\n\n *(gBattleStruct->moveTarget + gBattlerAttacker) = targetBattler;\n\n return targetBattler;\n}", "prependC": "#include \"global.h\"\n#include \"battle.h\"\n#include \"battle_anim.h\"\n#include \"battle_arena.h\"\n#include \"battle_pyramid.h\"\n#include \"battle_util.h\"\n#include \"pokemon.h\"\n#include \"international_string_util.h\"\n#include \"item.h\"\n#include \"util.h\"\n#include \"battle_scripts.h\"\n#include \"random.h\"\n#include \"text.h\"\n#include \"safari_zone.h\"\n#include \"sound.h\"\n#include \"sprite.h\"\n#include \"string_util.h\"\n#include \"task.h\"\n#include \"trig.h\"\n#include \"window.h\"\n#include \"battle_message.h\"\n#include \"battle_ai_script_commands.h\"\n#include \"battle_controllers.h\"\n#include \"event_data.h\"\n#include \"link.h\"\n#include \"field_weather.h\"\n#include \"constants/abilities.h\"\n#include \"constants/battle_anim.h\"\n#include \"constants/battle_move_effects.h\"\n#include \"constants/battle_script_commands.h\"\n#include \"constants/battle_string_ids.h\"\n#include \"constants/hold_effects.h\"\n#include \"constants/items.h\"\n#include \"constants/moves.h\"\n#include \"constants/songs.h\"\n#include \"constants/species.h\"\n#include \"constants/weather.h\"", @@ -489,11 +446,8 @@ { "sym": "Cmd_tryconversiontypechange", "features": [ - "do-while", - "loop", "array", - "struct", - "nested-loop" + "struct" ], "funcC": "static void Cmd_tryconversiontypechange(void)\n{\n u8 validMoves = 0;\n u8 moveChecked;\n u8 moveType;\n\n while (validMoves < MAX_MON_MOVES)\n {\n if (gBattleMons[gBattlerAttacker].moves[validMoves] == MOVE_NONE)\n break;\n\n validMoves++;\n }\n\n for (moveChecked = 0; moveChecked < validMoves; moveChecked++)\n {\n moveType = gBattleMoves[gBattleMons[gBattlerAttacker].moves[moveChecked]].type;\n\n if (moveType == TYPE_MYSTERY)\n {\n if (IS_BATTLER_OF_TYPE(gBattlerAttacker, TYPE_GHOST))\n moveType = TYPE_GHOST;\n else\n moveType = TYPE_NORMAL;\n }\n if (moveType != gBattleMons[gBattlerAttacker].types[0]\n && moveType != gBattleMons[gBattlerAttacker].types[1])\n {\n break;\n }\n }\n\n if (moveChecked == validMoves)\n {\n gBattlescriptCurrInstr = T1_READ_PTR(gBattlescriptCurrInstr + 1);\n }\n else\n {\n do\n {\n while ((moveChecked = MOD(Random(), MAX_MON_MOVES)) >= validMoves);\n\n moveType = gBattleMoves[gBattleMons[gBattlerAttacker].moves[moveChecked]].type;\n\n if (moveType == TYPE_MYSTERY)\n {\n if (IS_BATTLER_OF_TYPE(gBattlerAttacker, TYPE_GHOST))\n moveType = TYPE_GHOST;\n else\n moveType = TYPE_NORMAL;\n }\n }\n while (moveType == gBattleMons[gBattlerAttacker].types[0] || moveType == gBattleMons[gBattlerAttacker].types[1]);\n\n SET_BATTLER_TYPE(gBattlerAttacker, moveType);\n PREPARE_TYPE_BUFFER(gBattleTextBuff1, moveType);\n\n gBattlescriptCurrInstr += 5;\n }\n}", "prependC": "#include \"global.h\"\n#include \"battle.h\"\n#include \"battle_message.h\"\n#include \"battle_anim.h\"\n#include \"battle_ai_script_commands.h\"\n#include \"battle_scripts.h\"\n#include \"item.h\"\n#include \"util.h\"\n#include \"pokemon.h\"\n#include \"random.h\"\n#include \"battle_controllers.h\"\n#include \"battle_interface.h\"\n#include \"text.h\"\n#include \"sound.h\"\n#include \"pokedex.h\"\n#include \"recorded_battle.h\"\n#include \"window.h\"\n#include \"reshow_battle_screen.h\"\n#include \"main.h\"\n#include \"palette.h\"\n#include \"money.h\"\n#include \"bg.h\"\n#include \"string_util.h\"\n#include \"pokemon_icon.h\"\n#include \"m4a.h\"\n#include \"mail.h\"\n#include \"event_data.h\"\n#include \"pokemon_storage_system.h\"\n#include \"task.h\"\n#include \"naming_screen.h\"\n#include \"battle_setup.h\"\n#include \"overworld.h\"\n#include \"party_menu.h\"\n#include \"battle_arena.h\"\n#include \"battle_pike.h\"\n#include \"battle_pyramid.h\"\n#include \"field_specials.h\"\n#include \"pokemon_summary_screen.h\"\n#include \"pokenav.h\"\n#include \"menu_specialized.h\"\n#include \"data.h\"\n#include \"constants/abilities.h\"\n#include \"constants/battle_anim.h\"\n#include \"constants/battle_move_effects.h\"\n#include \"constants/battle_string_ids.h\"\n#include \"constants/hold_effects.h\"\n#include \"constants/items.h\"\n#include \"constants/map_types.h\"\n#include \"constants/moves.h\"\n#include \"constants/party_menu.h\"\n#include \"constants/rgb.h\"\n#include \"constants/songs.h\"\n#include \"constants/trainers.h\"", @@ -508,8 +462,6 @@ { "sym": "AnimTask_FlashHealthboxOnLevelUp_Step", "features": [ - "switch", - "ternary", "struct", "field", "array" diff --git a/apps/benchmark/dataset/real/sa3.json b/apps/benchmark/dataset/real/sa3.json index bfdd9f36..d81cc929 100644 --- a/apps/benchmark/dataset/real/sa3.json +++ b/apps/benchmark/dataset/real/sa3.json @@ -55,8 +55,7 @@ "features": [ "global", "pointer", - "arithmetic", - "bitwise" + "arithmetic" ], "prependC": "u32 gRngPrevValue;\nu32 gRngValue;\n#define RAND_CONST 0x37119371", "funcC": "u16 Random(void)\n{\n u32 new;\n u32 *pPrev = &gRngPrevValue;\n u32 *pCurrent = &gRngValue;\n\n u32 prev = *pPrev;\n gRngPrevValue = *pCurrent;\n\n new = prev + RAND_CONST;\n new += *pCurrent;\n\n gRngValue = new;\n\n return ((u16 *)&gRngValue)[1];\n}", @@ -67,9 +66,7 @@ "sym": "sa2__sub_808558C", "features": [ "arithmetic", - "bitwise", - "branch", - "shift" + "branch" ], "funcC": "s16 SA2_LABEL(sub_808558C)(u16 angleA, u16 angleB, u8 numDecimalBits)\n{\n u32 c1 = (1 << numDecimalBits);\n u16 c2 = c1 - 1;\n\n angleB -= angleA;\n angleB &= c2;\n\n if (angleB <= (c1 / 2)) {\n return angleB;\n } else {\n return (angleB - c1);\n }\n}", "sourceUrl": "https://github.com/macabeus/sa3/blob/b0321cdc57ef829b24d4179ed625cb3403e26633/src/game/math.c#L469-L482" @@ -77,11 +74,8 @@ { "sym": "sa2__sub_8085654", "features": [ - "loop", "arithmetic", - "int64", - "shift", - "do-while" + "int64" ], "funcC": "s32 SA2_LABEL(sub_8085654)(s32 a, s32 b, s32 c, u8 d, u8 e)\n{\n do {\n a -= (((s64)c * (s64)(a - b))) >> d;\n e -= 1;\n } while (e != 0xFF);\n\n return a;\n}", "sourceUrl": "https://github.com/macabeus/sa3/blob/b0321cdc57ef829b24d4179ed625cb3403e26633/src/game/math.c#L509-L517" @@ -90,8 +84,7 @@ "sym": "sa2__sub_80855C0", "features": [ "arithmetic", - "int64", - "shift" + "int64" ], "funcC": "u32 SA2_LABEL(sub_80855C0)(s32 a, s32 b, s32 c, u8 d)\n{\n s64 e = (s64)c * (a - b);\n\n return a - (e >> d);\n}", "sourceUrl": "https://github.com/macabeus/sa3/blob/b0321cdc57ef829b24d4179ed625cb3403e26633/src/game/math.c#L484-L489" @@ -107,8 +100,7 @@ { "sym": "sa2__sub_8085714", "features": [ - "arithmetic", - "shift" + "arithmetic" ], "funcC": "s32 SA2_LABEL(sub_8085714)(s32 a, s32 b, s32 c) { return ((a + b * 8) - c) >> 3; }", "sourceUrl": "https://github.com/macabeus/sa3/blob/b0321cdc57ef829b24d4179ed625cb3403e26633/src/game/math.c#L533-L533" @@ -127,9 +119,7 @@ "struct", "field", "global", - "pointer", - "sizeof", - "bitwise" + "pointer" ], "funcC": "void EwramInitHeap(void)\n{\n struct EwramNode *root = (struct EwramNode *)&gEwramHeap[0];\n root->next = NULL;\n root->state = sizeof(gEwramHeap);\n}", "proto": { @@ -143,12 +133,10 @@ { "sym": "EwramMalloc", "features": [ - "loop", "struct", "field", "pointer", - "branch", - "sizeof" + "branch" ], "funcC": "void *EwramMalloc(u32 req)\n{\n struct EwramNode *node;\n s32 requestedSpace = req;\n\n requestedSpace = (req + 3) / 4;\n if (requestedSpace != 0) {\n requestedSpace = requestedSpace * 4 + sizeof(struct EwramNode);\n node = (struct EwramNode *)gEwramHeap;\n while (1) {\n if (requestedSpace <= (u32)node->state) {\n if (requestedSpace == node->state) {\n node->state = -requestedSpace;\n return node->space;\n }\n\n if (requestedSpace + (s32)sizeof(struct EwramNode) <= node->state) {\n struct EwramNode *addr = (void *)node + requestedSpace;\n\n addr->next = node->next;\n ++node;\n --node;\n addr->state = node->state - requestedSpace;\n node->next = addr;\n node->state = -requestedSpace;\n return node->space;\n }\n }\n ++requestedSpace;\n --requestedSpace;\n if (!node->next)\n return ewram_end;\n node = node->next;\n }\n }\n return ewram_end;\n}", "m2cCtx": true, @@ -160,7 +148,6 @@ "struct", "field", "pointer", - "loop", "branch" ], "funcC": "void EwramFree(void *p)\n{\n struct EwramNode *node, *slow, *fast, *tmp;\n\n if (p && ewram_end != p) {\n node = p - offsetof(struct EwramNode, space);\n\n for (fast = slow = (struct EwramNode *)gEwramHeap; node != fast; fast = fast->next)\n slow = fast;\n\n if (node->state < 0)\n node->state = -node->state;\n\n if ((void *)slow + slow->state == node && slow->state > 0) {\n slow->next = fast->next;\n slow->state += node->state;\n node = slow;\n }\n\n tmp = (void *)node + node->state;\n if (tmp == node->next && tmp->state > 0) {\n node->state += tmp->state;\n node->next = tmp->next;\n }\n }\n}", @@ -175,11 +162,8 @@ { "sym": "numToASCII", "features": [ - "loop", "array", - "bitwise", - "branch", - "shift" + "branch" ], "funcC": "void numToASCII(u8 digits[5], u16 number)\n{\n u8 i;\n\n for (i = 0; i < 4; number <<= 4, i++) {\n u16 value = ((number & 0xF000) >> 12);\n if (value > 9) {\n digits[i] = value + 87;\n } else {\n digits[i] = value + '0';\n }\n }\n\n digits[i] = 0;\n}", "proto": { @@ -191,11 +175,7 @@ }, { "sym": "Base10DigitsToHexNibbles", - "features": [ - "loop", - "bitwise", - "shift" - ], + "features": [], "funcC": "u32 Base10DigitsToHexNibbles(u16 num)\n{\n u8 i;\n u16 result;\n u8 lowDigit;\n u16 remainder = num;\n\n result = 0;\n for (i = 0; i < 4; i++) {\n s32 divisor = Div(remainder, 10);\n lowDigit = remainder - (divisor * 10);\n remainder = divisor;\n\n result |= lowDigit << (i * 4);\n }\n\n return result;\n}\n", "note": "Div() resolves to an inline macro (int division) — soft-div idiom.", "sourceUrl": "https://github.com/macabeus/sa3/blob/b0321cdc57ef829b24d4179ed625cb3403e26633/src/sprite.c#L147-L164" @@ -203,7 +183,6 @@ { "sym": "VerifyFlashSector_Core", "features": [ - "loop", "pointer", "cast", "branch" @@ -218,10 +197,7 @@ "cast", "fixed-point", "array", - "memory", - "sizeof", - "shift", - "bitwise" + "memory" ], "prependC": "#include \nstatic const u8 unkFractions[8] = { I(Q(0.5 * 2)), I(Q(0.0 * 2)), I(Q(1.0 * 2)), I(Q(1.5 * 2)), I(Q(3.0 * 2)), I(Q(3.5 * 2)), I(Q(2.5 * 2)), I(Q(2.0 * 2)) };", "funcC": "s16 sa2__sub_8004418(s16 x, s16 y)\n{\n s16 fraction;\n s32 result;\n u8 index = 0;\n u8 array[8];\n memcpy(array, unkFractions, sizeof(array));\n\n if ((x | y) == 0) {\n result = -1;\n } else {\n if (x <= 0) {\n x = -x;\n index = 4;\n }\n if (y <= 0) {\n y = -y;\n index += 2;\n }\n if (x >= y) {\n // fraction = y*0.5 / x\n y *= Q(0.5);\n\n if (x == 0) {\n fraction = y;\n } else {\n fraction = y / x;\n }\n } else {\n index += 1;\n\n x *= Q(0.5);\n\n if (y == 0) {\n fraction = x;\n } else {\n fraction = x / y;\n }\n }\n\n if (array[index] & 0x01) {\n fraction = Q(0.5) - fraction;\n }\n\n {\n s32 val = array[index] * Q(0.5);\n fraction += val;\n result = ((u32)(fraction << 22) >> 22);\n }\n }\n\n return result;\n}", @@ -231,10 +207,8 @@ { "sym": "GetSaveSectorChecksum", "features": [ - "loop", "pointer", "cast", - "sizeof", "struct" ], "prependC": "#include \"game/save.h\"", @@ -244,12 +218,10 @@ { "sym": "VramMalloc", "features": [ - "loop", "global", "array", "branch", - "pointer", - "nested-loop" + "pointer" ], "prependC": "#include \"core.h\"\n#include \"malloc_vram.h\"", "funcC": "void *VramMalloc(u32 numTiles)\n{\n u16 i, j;\n u32 count = numTiles;\n count = (count + (VRAM_TILE_SLOTS_PER_SEGMENT - 1)) / VRAM_TILE_SLOTS_PER_SEGMENT; // round up\n\n for (i = 0; i < gVramHeapMaxTileSlots / VRAM_TILE_SLOTS_PER_SEGMENT; i++) {\n if (gVramHeapState[i] == 0) {\n for (j = 0; j < count; j++) {\n if (i + j >= (gVramHeapMaxTileSlots / VRAM_TILE_SLOTS_PER_SEGMENT)) {\n return ewram_end;\n }\n if (gVramHeapState[i + j] != 0) {\n break;\n }\n }\n\n if (j == count) {\n gVramHeapState[i] = count;\n return (void *)(gVramHeapStartAddr + i * VRAM_HEAP_SEGMENT_SIZE);\n }\n } else {\n i = (gVramHeapState[i] - 1) + i; // next slot to check, -1 because of ++i\n }\n }\n return ewram_end;\n}", @@ -259,7 +231,6 @@ { "sym": "VramGetTotalAllocatedTiles", "features": [ - "loop", "global", "array", "branch" @@ -272,12 +243,9 @@ { "sym": "GetInput", "features": [ - "loop", - "bitwise", "array", "branch", - "global", - "shift" + "global" ], "prependC": "#include \"core.h\"\n#include \"input_recorder.h\"", "funcC": "void GetInput(void)\n{\n s8 i;\n u8 *repeatKeyCounters = gRepeatedKeysTestCounter, *firstIntervals = gKeysFirstRepeatIntervals,\n *continuedHoldIntervals = gKeysContinuedRepeatIntervals;\n\n gInput = (~REG_KEYINPUT & KEYS_MASK);\n\n // My guess is that whilst the input recorder\n // is running we still want to know the actual\n // input. For example; to be able to know when\n // to exit the demo\n gPhysicalInput = gInput;\n\n#ifndef COLLECT_RINGS_ROM\n if (gInputRecorder.mode == RECORDER_RECORD) {\n InputRecorderWrite(gInput);\n } else if (gInputRecorder.mode == RECORDER_PLAYBACK) {\n gInput = InputRecorderRead();\n }\n#endif\n\n gPressedKeys = (gInput ^ gPrevInput) & gInput;\n gReleasedKeys = (gInput ^ gPrevInput) & gPrevInput;\n gPrevInput = gInput;\n\n // Repeated keys will be the currently pressed keys\n gRepeatedKeys = gPressedKeys;\n\n // Calculate the next repeated state for every key\n for (i = 0; i < 10; i++) {\n if (!GetBit(gInput, i)) {\n // If a key is not pressed, reset its counter\n // to the settings for that key's\n // first repeat interval\n repeatKeyCounters[i] = firstIntervals[i];\n } else if (repeatKeyCounters[i] > 0) {\n // If the key is not yet ready to be repeated\n // continue to wait\n repeatKeyCounters[i]--;\n } else {\n // If the key is ready to be repeated,\n // add the key to the repeated keys\n gRepeatedKeys |= 1 << i;\n // And reset its counter to the settings\n // for continued hold interval\n repeatKeyCounters[i] = continuedHoldIntervals[i];\n }\n }\n}", @@ -292,10 +260,8 @@ { "sym": "sa2__sub_8007858", "features": [ - "loop", "pointer", "global", - "bitwise", "cast" ], "prependC": "#include \"core.h\"\n#include \"flags.h\"", @@ -310,10 +276,8 @@ { "sym": "sa2__sub_8007958", "features": [ - "loop", "pointer", "global", - "bitwise", "arithmetic" ], "prependC": "#include \"core.h\"\n#include \"flags.h\"", @@ -336,8 +300,7 @@ { "sym": "sa2__sub_8085720", "features": [ - "arithmetic", - "shift" + "arithmetic" ], "funcC": "s32 SA2_LABEL(sub_8085720)(s32 a, s32 b, s32 c) { return ((b * 8 - a) + c) >> 3; }", "sourceUrl": "https://github.com/macabeus/sa3/blob/b0321cdc57ef829b24d4179ed625cb3403e26633/src/game/math.c#L535-L535" @@ -345,8 +308,7 @@ { "sym": "sa2__sub_8085798", "features": [ - "arithmetic", - "shift" + "arithmetic" ], "funcC": "s32 SA2_LABEL(sub_8085798)(s32 a, s32 b, s32 c) { return ((c - a) + ((c - a) >> 1)) - (b >> 1); }", "sourceUrl": "https://github.com/macabeus/sa3/blob/b0321cdc57ef829b24d4179ed625cb3403e26633/src/game/math.c#L549-L549" @@ -354,8 +316,7 @@ { "sym": "sa2__sub_808595C", "features": [ - "arithmetic", - "shift" + "arithmetic" ], "funcC": "s32 SA2_LABEL(sub_808595C)(s32 a, s32 b, s32 c)\n{\n s32 e = (b - a);\n s32 f = (c - b);\n f -= e;\n f >>= 1;\n return e + f;\n}", "sourceUrl": "https://github.com/macabeus/sa3/blob/b0321cdc57ef829b24d4179ed625cb3403e26633/src/game/math.c#L580-L587" @@ -365,8 +326,7 @@ "features": [ "struct", "field", - "arithmetic", - "shift" + "arithmetic" ], "prependC": "typedef struct {\n s16 unk0;\n s16 unk2;\n s16 unk4;\n s16 unk6;\n u16 unk8;\n u16 unkA;\n u16 unkC;\n u16 unkE;\n u16 unk10;\n u16 unk12;\n u32 unk14;\n u32 unk18;\n u32 unk1C;\n} UNK_8085D14;", "proto": { @@ -382,8 +342,7 @@ "features": [ "struct", "field", - "arithmetic", - "shift" + "arithmetic" ], "prependC": "typedef struct {\n s16 unk0;\n s16 unk2;\n s16 unk4;\n s16 unk6;\n u16 unk8;\n u16 unkA;\n u16 unkC;\n u16 unkE;\n u16 unk10;\n u16 unk12;\n u32 unk14;\n u32 unk18;\n u32 unk1C;\n} UNK_8085D14;", "proto": { @@ -416,9 +375,7 @@ "struct", "field", "branch", - "fixed-point", - "shift", - "bitwise" + "fixed-point" ], "prependC": "typedef struct {\n u16 unk0;\n s16 unk2;\n s16 unk4;\n s16 unk6;\n s32 unk8;\n s32 unkC;\n u16 unk10;\n u16 unk12;\n u32 unk14;\n u32 unk18;\n u32 unk1C;\n} UNK_8085D14_2;\n#include \"trig.h\"", "proto": { @@ -435,10 +392,6 @@ "features": [ "global", "struct", - "loop", - "nested-loop", - "sizeof", - "bitwise", "union", "bitfield", "field" @@ -451,9 +404,7 @@ { "sym": "sa2__sub_80078D4", "features": [ - "global", - "bitwise", - "shift" + "global" ], "prependC": "#include \"core.h\"\n#include \"flags.h\"", "funcC": "void sa2__sub_80078D4(u8 bg, int_vcount minY, int_vcount maxY, u16 offsetEven, u16 offsetOdd)\n{\n s32 fillVal;\n\n gFlags |= FLAGS_EXECUTE_HBLANK_COPY;\n\n gHBlankCopyTarget = (void *)&((u8 *)®_BG0HOFS)[bg * 4];\n gHBlankCopySize = 4;\n\n if (minY < maxY) {\n fillVal = (offsetEven %= 512u) | ((offsetOdd % 512u) << 16);\n\n DmaFill32(3, fillVal, &((u16 *)gBgOffsetsHBlankPrimary)[minY * 2], (maxY - minY) * 4);\n }\n}", @@ -462,11 +413,8 @@ { "sym": "PackSaveSector", "features": [ - "switch", "array", - "loop", - "memory", - "nested-loop" + "memory" ], "funcC": "void PackSaveSector(SaveSectorData *sector, SaveGame *save)\n{\n s16 i;\n s16 j;\n s16 k;\n\n sector->header.magicNumber = 0x47544E4C;\n sector->header.sectorId += 1;\n sector->playerId = save->playerId;\n\n for (i = 0; i < 6; i++) {\n sector->playerName[i] = save->playerName[i];\n }\n\n sector->unk18 = 0;\n sector->unlockedCharacters = save->unlockedCharacters;\n sector->unlockedZones = save->unlockedZones;\n sector->continueZone = save->continueZone;\n sector->unk1C = 0;\n\n for (i = 0; i < 7; i++) {\n sector->chaoCount[i] = save->chaoCount[i];\n }\n\n for (i = 0; i < 7; i++) {\n sector->specialKeys[i] = save->specialKeys[i];\n }\n\n for (i = 0; i < 9; i++) {\n sector->unlockedStages[i] = save->unlockedStages[i];\n }\n\n for (i = 0; i < 9; i++) {\n for (j = 0; j < 4; j++) {\n sector->collectedMedals[i][j] = save->collectedMedals[i][j];\n }\n }\n\n sector->collectedEmeralds = save->collectedEmeralds;\n sector->unk62 = save->unk34;\n sector->unlockFlags = save->unlockFlags;\n sector->vsWins = save->vsWins;\n sector->vsDraws = save->vsDraws;\n sector->vsLosses = save->vsLosses;\n sector->unk67 = 0;\n\n for (i = 0; i < 10; i++) {\n sector->vsRecords[i].slotFilled = save->vsRecords[i].slotFilled;\n sector->vsRecords[i].wins = save->vsRecords[i].wins;\n sector->vsRecords[i].draws = save->vsRecords[i].draws;\n sector->vsRecords[i].losses = save->vsRecords[i].losses;\n\n sector->vsRecords[i].playerId = save->vsRecords[i].playerId;\n\n for (j = 0; j < 6; j++) {\n sector->vsRecords[i].playerName[j] = save->vsRecords[i].playerName[j];\n }\n }\n\n#define Zone i\n#define Act j\n#define Rank k\n for (Zone = 0; Zone < (s32)ARRAY_COUNT(save->timeRecords.table); Zone++) {\n for (Act = 0; Act < (s32)ARRAY_COUNT(save->timeRecords.table[0]); Act++) {\n for (Rank = 0; Rank < (s32)ARRAY_COUNT(save->timeRecords.table[0][0]); Rank++) {\n sector->timeRecords.table[Zone][Act][Rank].character1 = save->timeRecords.table[Zone][Act][Rank].character1;\n sector->timeRecords.table[Zone][Act][Rank].character2 = save->timeRecords.table[Zone][Act][Rank].character2;\n sector->timeRecords.table[Zone][Act][Rank].time = save->timeRecords.table[Zone][Act][Rank].time;\n }\n }\n }\n#undef Rank\n#undef Act\n#undef Zone\n\n switch (save->buttonConfig.jump) {\n case A_BUTTON:\n sector->buttonConfig.jump = 1;\n break;\n case B_BUTTON:\n sector->buttonConfig.attack = 1;\n break;\n case R_BUTTON:\n sector->buttonConfig.trick = 1;\n break;\n }\n\n switch (save->buttonConfig.attack) {\n case A_BUTTON:\n sector->buttonConfig.jump = 2;\n break;\n case B_BUTTON:\n sector->buttonConfig.attack = 2;\n break;\n case R_BUTTON:\n sector->buttonConfig.trick = 2;\n break;\n }\n\n switch (save->buttonConfig.trick) {\n case A_BUTTON:\n sector->buttonConfig.jump = 4;\n break;\n case B_BUTTON:\n sector->buttonConfig.attack = 4;\n break;\n case R_BUTTON:\n sector->buttonConfig.trick = 4;\n break;\n }\n\n sector->difficulty = save->difficulty;\n sector->disableTimeLimit = save->disableTimeLimit;\n sector->language = save->language;\n sector->unk367 = 0;\n sector->checksum = GetSaveSectorChecksum(sector);\n}", "prependC": "#include \"global.h\"\n#include \"core.h\"\n#include \"flags.h\"\n#include \"lib/m4a/m4a.h\"\n#include \"lib/agb_flash/agb_flash.h\"\n#include \"lib/agb_flash/flash_internal.h\"\n#include \"malloc_ewram.h\"\n#include \"malloc_vram.h\"\n#include \"game/save.h\"\n#include \"game/stage.h\"\n#include \"gba/defines.h\"\n#include \"constants/characters.h\"\n#include \"constants/zones.h\"", @@ -480,12 +428,8 @@ { "sym": "sub_804DC38", "features": [ - "switch", - "shift", "struct", - "field", - "loop", - "bitwise" + "field" ], "funcC": "u16 sub_804DC38(u8 kind, s32 worldX, s32 worldY, MapEntity *me)\n{\n s32 r0;\n s32 r1;\n s32 r2_32;\n s16 r2;\n bool32 r6;\n u16 i;\n\n // TODO(Jace): This was likely not written this way, and might actually be a bug?\n // I don't think MapEntity structs are always aligned in memory.\n u32 data = *((u32 *)&me->d.uData[1]);\n if (data & 0x00FFFF00) {\n if (me->d.uData[3] < me->d.uData[2]) {\n r2_32 = (me->d.uData[2] * (TILE_WIDTH / 2));\n r1 = worldX;\n r0 = worldX + (me->d.sData[0] * TILE_WIDTH);\n r6 = FALSE;\n } else {\n r2_32 = (me->d.uData[3] * (TILE_WIDTH / 2));\n r1 = worldY;\n r0 = worldY + (me->d.sData[1] * TILE_WIDTH);\n r6 = TRUE;\n }\n\n r2 = ((r1 - (r0 + r2_32)) << 14) / r2_32;\n if (r2 >= 0) {\n for (i = 0; (i < Q(1)) && (r2 > SIN(i));) {\n i += 4;\n }\n\n switch (kind) {\n case 0: {\n return Q(2) - i;\n } break;\n\n case 1: {\n if (r6) {\n return i;\n } else {\n return Q(2) - i;\n }\n } break;\n\n case 2: {\n if (!r6) {\n return i;\n } else {\n return Q(2) - i;\n }\n } break;\n\n default: {\n return i;\n }\n }\n\n } else {\n for (i = Q(2); (i < Q(3)) && (r2 < SIN(i));) {\n i += 4;\n }\n\n switch (kind) {\n case 0: {\n return i;\n } break;\n\n case 1: {\n if (!r6) {\n return i;\n } else {\n return Q(6) - i;\n }\n } break;\n\n case 2: {\n if (r6) {\n return i;\n } else {\n return Q(6) - i;\n }\n } break;\n\n case 3: {\n return Q(6) - i;\n } break;\n }\n }\n }\n\n return 0;\n}", "prependC": "#include \"global.h\"\n#include \"trig.h\"\n#include \"game/interactables/platform_shared.h\"\n#include \"game/camera.h\"\n#include \"game/entity.h\"\n#include \"game/stage.h\"", @@ -495,11 +439,7 @@ { "sym": "Sio32MultiLoadMain", "features": [ - "switch", - "shift", - "global", - "loop", - "bitwise" + "global" ], "funcC": "u32 Sio32MultiLoadMain(u32 *progressCounterp)\n{\n s32 dataCounter, dataCounterBak;\n UNUSED s32 i, ii; // declared in SDK\n\n switch (gSio32MultiLoadArea.state) {\n case 0:\n if (gSio32MultiLoadArea.type || gSio32MultiLoadArea.frameCounter) // Slave SIO Mode switch delay wait\n gSio32MultiLoadArea.state = 1;\n break;\n case 1:\n if (gSio32MultiLoadArea.type == SIO_SCK_IN) {\n if (gSio32MultiLoadArea.frameCounter < SIO32ML_MODE_WAIT_FRAMES) // Master wait\n break;\n } else // Slave\n REG_SIOCNT = SIO_32BIT_MODE; // Switches to SIO mode\n REG_SIODATA32 = 0; // Clears data registery\n REG_IF = INTR_FLAG_SERIAL | SIO32ML_TIMER_INTR_FLAG; // Resets IF\n if (gSio32MultiLoadArea.type == SIO_SCK_IN) { // Sets Master\n REG_SIOCNT |= SIO_ENABLE; // Starts communication\n REG_SIO32ML_TIMER = SIO32ML_TIMER_COUNT | ((TIMER_1CLK | TIMER_INTR_ENABLE | TIMER_ENABLE) << 16); // Starts timer\n REG_IME = 0;\n REG_IE |= SIO32ML_TIMER_INTR_FLAG; // Enables timer interrupt\n REG_IME = 1;\n } else { // Sets Slave\n REG_SIOCNT |= SIO_INTR_ENABLE | SIO_ENABLE; // Starts communication\n REG_IME = 0;\n REG_IE |= INTR_FLAG_SERIAL; // Enables SIO interrupt\n REG_IME = 1;\n }\n gSio32MultiLoadArea.frameCounter = 0;\n gSio32MultiLoadArea.state = 2;\n break;\n case 2:\n dataCounter = gSio32MultiLoadArea.dataCounter;\n dataCounterBak = dataCounter;\n if (dataCounter > SIO32ML_BLOCK_SIZE / 4)\n dataCounter = SIO32ML_BLOCK_SIZE / 4;\n else if (dataCounter < 0)\n dataCounter = 0;\n if (progressCounterp)\n *progressCounterp = dataCounter; // Set progress counter\n if (gSio32MultiLoadArea.type != SIO_SCK_IN) { // Slave\n while (gSio32MultiLoadArea.checkSumCounter < dataCounter) // Receiving data sum check\n gSio32MultiLoadArea.checkSumTmp += ((s32 *)gSio32MultiLoadArea.datap)[gSio32MultiLoadArea.checkSumCounter++];\n if (dataCounterBak > SIO32ML_BLOCK_SIZE / 4)\n if ((gSio32MultiLoadArea.checkSum += gSio32MultiLoadArea.checkSumTmp) == -1)\n gSio32MultiLoadArea.downloadSuccessFlag = 1; // Succeeds download\n }\n if (dataCounterBak > SIO32ML_BLOCK_SIZE / 4 // Complete communication\n || gSio32MultiLoadArea.frameCounter == 0x8C) // SIO32ML_LD_TIMEOUT_FRAMES in SDK doesn't match\n gSio32MultiLoadArea.state = 3; // Load time out\n break;\n case 3:\n REG_IME = 0;\n REG_IE &= ~(INTR_FLAG_SERIAL | SIO32ML_TIMER_INTR_FLAG); // Disables SIO and timer interrupt\n REG_IME = 1;\n REG_SIOCNT = SIO_32BIT_MODE; // Stops SIO32\n *(vu32 *)REG_ADDR_SIOCNT = SIO_MULTI_MODE; // Switches to SIO mode\n *(vu32 *)REG_ADDR_SIOCNT = SIO_MULTI_MODE | MULTI_SIO_BAUD_RATE_NO;\n *(vu64 *)REG_ADDR_SIODATA32 = 0; // Clears data register\n if (gSio32MultiLoadArea.type) // Master\n REG_SIO32ML_TIMER = 0; // Resets timer\n REG_IF = INTR_FLAG_SERIAL | SIO32ML_TIMER_INTR_FLAG; // Resets IF\n if (!gSio32MultiLoadArea.type)\n return 1; // Slave comes back first\n gSio32MultiLoadArea.frameCounter = 0;\n gSio32MultiLoadArea.state = 4;\n break;\n case 4:\n if (gSio32MultiLoadArea.frameCounter >= 3) // Master comes back later\n return 1;\n break;\n }\n ++gSio32MultiLoadArea.frameCounter;\n return 0;\n}", "prependC": "#include \"sio32_multi_load.h\"\n#include \"global.h\"", @@ -508,8 +448,6 @@ { "sym": "sub_802DFC8", "features": [ - "bitwise", - "ternary", "struct", "field" ], @@ -543,11 +481,9 @@ { "sym": "sub_804D360", "features": [ - "switch", "struct", "field", - "branch", - "bitwise" + "branch" ], "funcC": "void sub_804D360(Sprite *s, s16 i, s16 param2)\n{\n u32 flags = 0;\n\n switch (i) {\n case 0: {\n flags |= SPRITE_FLAG(X_FLIP, 1);\n if (param2 == 2) {\n flags |= SPRITE_FLAG(Y_FLIP, 1);\n }\n } break;\n\n case 1: {\n if (param2 == 2) {\n flags |= SPRITE_FLAG(Y_FLIP, 1);\n }\n } break;\n\n case 2: {\n flags |= SPRITE_FLAG(Y_FLIP, 1);\n\n if (param2 == 0) {\n flags |= SPRITE_FLAG(X_FLIP, 1);\n }\n } break;\n\n case 3: {\n if (param2 == 0) {\n flags |= SPRITE_FLAG(X_FLIP, 1);\n }\n } break;\n }\n\n s->tiles = ALLOC_TILES_VARIANT(ANIM_MAZE, 6);\n s->anim = ANIM_MAZE;\n s->variant = 6;\n s->oamFlags = SPRITE_OAM_ORDER(24);\n s->animCursor = 0;\n s->qAnimDelay = 0;\n s->qAnimDelay = 0;\n s->prevVariant = -1;\n s->animSpeed = SPRITE_ANIM_SPEED(1.0);\n s->palId = 0;\n s->hitboxes[0].index = -1;\n s->frameFlags = flags | SPRITE_FLAG(PRIORITY, 1);\n UpdateSpriteAnimation(s);\n}", "prependC": "#include \"global.h\"\n#include \"module_unclear.h\"\n#include \"malloc_vram.h\"\n#include \"game/camera.h\"\n#include \"game/entity.h\"\n#include \"game/player.h\"\n#include \"game/player_callbacks.h\"\n#include \"game/stage.h\"\n#include \"constants/animations.h\"\n#include \"constants/anim_sizes.h\"\n#include \"constants/move_states.h\"", @@ -563,8 +499,7 @@ "sym": "Sio32MultiLoadIntr", "features": [ "global", - "branch", - "bitwise" + "branch" ], "funcC": "void Sio32MultiLoadIntr(void)\n{\n // Saves received data\n u32 recvTmp = REG_SIODATA32;\n // Slave prepares to receive\n if (gSio32MultiLoadArea.type != SIO_SCK_IN)\n REG_SIOCNT |= SIO_ENABLE;\n if (gSio32MultiLoadArea.type == SIO_SCK_IN) {\n REG_SIO32ML_TIMER_H = 0; // Stops timer\n // Process sending data\n if (gSio32MultiLoadArea.dataCounter < 0) // Sets synchronous data\n REG_SIODATA32 = SIO32ML_SYNC_DATA;\n else if (gSio32MultiLoadArea.dataCounter < SIO32ML_BLOCK_SIZE / 4) // Sets sending\n // data\n REG_SIODATA32 = gSio32MultiLoadArea.datap[gSio32MultiLoadArea.dataCounter];\n else\n REG_SIODATA32 = gSio32MultiLoadArea.checkSum; // Sets check sum\n } else {\n // Process receiving data\n if (gSio32MultiLoadArea.dataCounter < 0) { // Checks synchronous data\n if (recvTmp != SIO32ML_SYNC_DATA)\n --gSio32MultiLoadArea.dataCounter;\n } else if (gSio32MultiLoadArea.dataCounter < SIO32ML_BLOCK_SIZE / 4) // Saves\n // receiving\n // data\n gSio32MultiLoadArea.datap[gSio32MultiLoadArea.dataCounter] = recvTmp;\n else\n gSio32MultiLoadArea.checkSum = recvTmp; // Saves check sum\n }\n if (gSio32MultiLoadArea.dataCounter < SIO32ML_BLOCK_SIZE / 4 + 3) {\n ++gSio32MultiLoadArea.dataCounter;\n // Master starts to sending\n if (gSio32MultiLoadArea.type == SIO_SCK_IN) {\n REG_SIOCNT |= SIO_ENABLE; // Restarts sending\n REG_SIO32ML_TIMER_H = TIMER_1CLK | TIMER_INTR_ENABLE | TIMER_ENABLE; // Restarts timer\n }\n }\n}", "prependC": "#include \"sio32_multi_load.h\"\n#include \"global.h\"", @@ -579,8 +514,6 @@ { "sym": "sub_803213C", "features": [ - "bitwise", - "ternary", "struct", "field" ], diff --git a/apps/benchmark/dataset/real/snowboardkids2.json b/apps/benchmark/dataset/real/snowboardkids2.json index b9bd8fb6..5aa9cd94 100644 --- a/apps/benchmark/dataset/real/snowboardkids2.json +++ b/apps/benchmark/dataset/real/snowboardkids2.json @@ -64,8 +64,7 @@ "branch", "global", "struct", - "field", - "bitwise" + "field" ], "note": "src/38BE0.c: settings getter with if-chain over a global struct; leaf", "prependC": "#include \"EepromSaveData_type.h\"", @@ -186,7 +185,6 @@ { "sym": "func_80014440_15040", "features": [ - "loop", "global", "array" ], @@ -204,11 +202,8 @@ "sym": "func_8002ADB4_2B9B4", "features": [ "fixed-point", - "bitwise", "branch", - "compare", - "goto", - "shift" + "compare" ], "note": "src/2AF90.c: angle interpolation with wrap handling; pure leaf, uses ABS macro", "funcC": "s16 func_8002ADB4_2B9B4(s16 arg0, s16 arg1) {\n s16 target;\n s16 current;\n s32 diff;\n s16 absDiff;\n s16 direction;\n\n target = arg0;\n current = arg1;\n\n if (arg0 == arg1) {\n goto done;\n }\n\n diff = arg0 - arg1;\n diff = ABS(diff);\n\n if ((s16)diff >= 0x1001) {\n if (arg1 < arg0) {\n current = arg1 + 0x2000;\n } else {\n target = arg0 + 0x2000;\n }\n }\n\n arg0 = target;\n arg1 = current;\n\n diff = arg0 - arg1;\n absDiff = ABS(diff);\n\n if (absDiff >= 0xAAB) {\n current += 0x1000;\n goto done;\n }\n\n if (arg1 < arg0) {\n direction = 1;\n } else {\n direction = -1;\n }\n\n if (absDiff >= 0x80) {\n current += direction << 7;\n goto done;\n }\n\n current = (current + absDiff * direction) & 0x1FFF;\n\ndone:\n return current;\n}", @@ -217,7 +212,6 @@ { "sym": "func_8002B4B8_2C0B8", "features": [ - "bitwise", "branch", "fixed-point" ], @@ -244,12 +238,10 @@ { "sym": "func_8001BCDC_1C8DC", "features": [ - "loop", "struct", "field", "global", - "branch", - "nested-loop" + "branch" ], "note": "src/1BBA0.c: nested loop counting active players, clears a flag row when none remain", "prependC": "#include \"gamestate.h\"\n#include \"D_800AFE8C_A71FC_type.h\"\n#include \"task_scheduler.h\"", @@ -264,7 +256,6 @@ { "sym": "func_8002BE94_2CA94", "features": [ - "loop", "struct", "array" ], @@ -281,7 +272,6 @@ { "sym": "loadAssetGroupResources", "features": [ - "loop", "struct", "array" ], @@ -299,7 +289,6 @@ { "sym": "func_800167B0_173B0", "features": [ - "bitwise", "struct", "field" ], @@ -328,7 +317,6 @@ { "sym": "func_8002A390_2AF90", "features": [ - "switch", "struct", "branch" ], @@ -340,9 +328,7 @@ { "sym": "func_800154A8_160A8", "features": [ - "loop", - "branch", - "do-while" + "branch" ], "funcC": "void func_800154A8_160A8(s8 *arg0, u16 arg1) {\n s32 end;\n s32 ptr;\n\n if (arg1 == 0) {\n return;\n }\n\n ptr = (s32)arg0;\n end = arg1 + ptr;\n do {\n *(s8 *)ptr = 0;\n ptr++;\n } while (ptr < end);\n}", "sourceUrl": "https://github.com/macabeus/snowboardkids2-decomp/blob/66e9f600be2b82dc08c87ccf0d2c6ed14509e489/src/15690.c#L304-L318" @@ -387,8 +373,7 @@ { "sym": "func_80028BB0_297B0", "features": [ - "struct", - "bitwise" + "struct" ], "prependC": "#include \"297B0.h\"\n#include \"20F0.h\"\n#include \"3E160.h\"\n#include \"B040.h\"\n#include \"common.h\"\n#include \"geometry.h\"\n#include \"graphics.h\"\n#include \"rand.h\"\n#include \"task_scheduler.h\"", "funcC": "void func_80028BB0_297B0(Func297D8Arg *arg0) {\n arg0->unk5E = 0;\n arg0->unk61 = 0;\n arg0->unk62 = 0;\n arg0->unk5A = randB() & 0xF;\n createYRotationMatrix(&arg0->matrix, arg0->rotation);\n func_8002A290_2AE90(arg0);\n setCallback(func_80028C08_29808);\n}", @@ -398,8 +383,7 @@ { "sym": "func_80028D90_29990", "features": [ - "struct", - "bitwise" + "struct" ], "prependC": "#include \"297B0.h\"\n#include \"20F0.h\"\n#include \"3E160.h\"\n#include \"B040.h\"\n#include \"common.h\"\n#include \"geometry.h\"\n#include \"graphics.h\"\n#include \"rand.h\"\n#include \"task_scheduler.h\"", "funcC": "void func_80028D90_29990(Func297D8Arg *arg0) {\n arg0->unk5E = 0;\n arg0->unk61 = 0;\n arg0->unk62 = 0;\n arg0->unk5A = arg0->unk5A + (randB() & 0xF);\n createYRotationMatrix(&arg0->matrix, arg0->rotation);\n func_8002A290_2AE90(arg0);\n setCallback(func_80028DF0_299F0);\n}", @@ -410,10 +394,7 @@ "sym": "func_800B7620_1E46D0", "features": [ "struct", - "bitwise", "branch", - "ternary", - "shift", "union" ], "prependC": "#include \"1E36C0.h\"", @@ -443,7 +424,6 @@ "sym": "func_8002A2D0_2AED0", "features": [ "struct", - "bitwise", "branch" ], "prependC": "#include \"297B0.h\"\n#include \"20F0.h\"\n#include \"3E160.h\"\n#include \"B040.h\"\n#include \"common.h\"\n#include \"geometry.h\"\n#include \"graphics.h\"\n#include \"rand.h\"\n#include \"task_scheduler.h\"", @@ -464,7 +444,6 @@ { "sym": "func_8005DF10_5EB10", "features": [ - "shift", "pointer", "array" ], @@ -482,7 +461,6 @@ "features": [ "struct", "pointer", - "loop", "branch", "union" ], @@ -493,13 +471,9 @@ { "sym": "func_8005AE8C_5BA8C", "features": [ - "do-while", "int64", - "goto", "memory", - "struct", - "loop", - "bitwise" + "struct" ], "funcC": "void func_8005AE8C_5BA8C(Player *player) {\n Vec3i deltaPos;\n u8 pad[0x8];\n void *collisionBoxPtr;\n s32 unused1;\n s32 unused2;\n s32 combinedRadius;\n s32 negRadius;\n s32 dist;\n s32 boxIndex;\n Player *targetPlayer;\n Player *players;\n\n players = ((GameState *)getCurrentAllocation())->players;\n targetPlayer = players + 1;\n\n /* Skip collision if players are on the same team */\n if (player->unkBB8 == targetPlayer->unkBB8) {\n return;\n }\n /* Skip if either player has collision disabled (flag 0x10) */\n if (player->unkB88 & 0x10) {\n return;\n }\n if (targetPlayer->unkB88 & 0x10) {\n return;\n }\n\n if ((s8)targetPlayer->unkBB4 > 0) {\n boxIndex = 0;\n collisionBoxPtr = targetPlayer;\n do {\n /* Copy target's collision box local position (offset 0xAE4 is unkAE4 array) */\n memcpy(&deltaPos, (u8 *)collisionBoxPtr + 0xAE4, 0xC);\n\n /* Convert to world space */\n deltaPos.x += targetPlayer->worldPos.x;\n deltaPos.y += targetPlayer->worldPos.y;\n deltaPos.z += targetPlayer->worldPos.z;\n\n /* Calculate relative position to player's collision box */\n deltaPos.x -= player->worldPos.x + player->unkAD4[0];\n deltaPos.y -= player->worldPos.y + player->unkAD4[1];\n deltaPos.z -= player->worldPos.z + player->unkAD4[2];\n\n /* Sum of both collision box radii */\n combinedRadius = (&targetPlayer->unkB2C)[boxIndex] + player->unkAE0;\n negRadius = -combinedRadius;\n\n /* Quick AABB check before expensive distance calculation */\n if (negRadius < deltaPos.x && deltaPos.x < combinedRadius && negRadius < deltaPos.y &&\n deltaPos.y < combinedRadius && negRadius < deltaPos.z && deltaPos.z < combinedRadius) {\n\n dist =\n isqrt64((s64)deltaPos.x * deltaPos.x + (s64)deltaPos.y * deltaPos.y + (s64)deltaPos.z * deltaPos.z);\n\n if (dist < combinedRadius) {\n /* Check for special knockback on collision boxes 4-5 when target is in state 1 */\n if (targetPlayer->unkBD9 == 1) {\n if ((targetPlayer->unkB84 & 0x40000) && (u32)(boxIndex - 4) < 2U) {\n func_80058B30_59730(player);\n goto next;\n }\n }\n /* Check for special knockback on collision boxes 1-2 when target is in state 3 */\n if (targetPlayer->unkBD9 == 3 && (targetPlayer->unkB84 & 0x40000) && (u32)(boxIndex - 1) < 2U) {\n func_80058B30_59730(player);\n goto next;\n }\n\n /* Avoid divide by zero */\n if (dist == 0) {\n dist = 1;\n }\n\n /* Calculate push vector to separate the players */\n deltaPos.x = ((s64)deltaPos.x * combinedRadius / dist) - deltaPos.x;\n deltaPos.y = ((s64)deltaPos.y * combinedRadius / dist) - deltaPos.y;\n deltaPos.z = ((s64)deltaPos.z * combinedRadius / dist) - deltaPos.z;\n\n /* Push player out of collision */\n player->worldPos.x -= deltaPos.x;\n player->worldPos.y -= deltaPos.y;\n player->worldPos.z -= deltaPos.z;\n\n /* Apply knockback if target is in state 2 and colliding with main collision box */\n if ((targetPlayer->unkBD9 == 2) & (boxIndex == 0)) {\n dist = isqrt64((s64)deltaPos.x * deltaPos.x + (s64)deltaPos.z * deltaPos.z);\n if (dist > 0x30000) {\n func_80058950_59550(\n player,\n func_8006D21C_6DE1C(\n targetPlayer->worldPos.x,\n targetPlayer->worldPos.z,\n player->worldPos.x,\n player->worldPos.z\n ),\n dist\n );\n }\n }\n }\n }\n next:\n boxIndex++;\n collisionBoxPtr = (u8 *)collisionBoxPtr + 0xC;\n } while (boxIndex < (s8)targetPlayer->unkBB4);\n }\n}", "prependC": "#include \"5AA90.h\"\n#include \"3E160.h\"\n#include \"56910.h\"\n#include \"594E0.h\"\n#include \"5DBC0.h\"\n#include \"common.h\"\n#include \"displaylist.h\"\n#include \"gamestate.h\"\n#include \"geometry.h\"\n#include \"task_scheduler.h\"", @@ -514,11 +488,9 @@ { "sym": "func_800B2C18_A2AC8", "features": [ - "goto", "struct", "pointer", - "branch", - "bitwise" + "branch" ], "funcC": "s32 func_800B2C18_A2AC8(Player *arg0) {\n GameState *state;\n s32 flags;\n u8 stateUnk7A;\n s16 var_a0;\n s16 var_v1;\n s32 temp_v0;\n\n state = getCurrentAllocation();\n flags = arg0->unkB84;\n\n if (flags & 1) {\n return 0;\n }\n\n if (flags & 0x1000) {\n func_800B46BC_A456C(arg0);\n return 1;\n }\n\n if (flags & 0x80000) {\n goto skip_to_end;\n }\n\n stateUnk7A = state->unk7A;\n if (stateUnk7A != 0 && stateUnk7A != 8) {\n if (!(stateUnk7A == 9 || stateUnk7A == 10)) {\n arg0->unkBAC = 0;\n }\n }\n\n if (arg0->unkBAC != 0) {\n func_80059A48_5A648(arg0, arg0->unkBAC);\n if (arg0->unkBC7 == 0) {\n func_8004D890_4E490(arg0->unkBB8, arg0->unkBAC);\n\n var_v1 = arg0->unkBAC;\n if (var_v1 < 0x96) {\n var_a0 = 0x11C;\n } else {\n var_a0 = 0x11D;\n }\n\n if (var_v1 >= 0x12C) {\n var_a0 = 0x11E;\n }\n if (var_v1 >= 0x190) {\n var_a0 = 0x11F;\n }\n if (var_v1 >= 0x1F4) {\n var_a0 = 0x120;\n }\n if (var_v1 >= 0xBB8) {\n var_a0 = 0x121;\n }\n\n func_80058530_59130(var_a0, 6);\n }\n }\n\n if (state->unk7A == 6) {\n if (arg0->unkBAA != 0) {\n func_8004FCF0_508F0(arg0->unkBAA);\n func_80059A88_5A688(arg0, arg0->unkBAA);\n\n var_v1 = arg0->unkBAA;\n if (var_v1 < 0xF) {\n var_a0 = 0x11C;\n } else {\n var_a0 = 0x11D;\n }\n\n if (var_v1 >= 0x1E) {\n var_a0 = 0x11E;\n }\n if (var_v1 >= 0x37) {\n var_a0 = 0x11F;\n }\n if (var_v1 >= 0x5F) {\n var_a0 = 0x120;\n }\n if (var_v1 >= 0x3E7) {\n var_a0 = 0x121;\n }\n\n func_80058530_59130(var_a0, 6);\n }\n }\n\nskip_to_end:\n if (arg0->unkBAE != 0) {\n func_80059BD4_5A7D4(arg0);\n func_8005D804_5E404(arg0, 1, 0xF);\n }\n\n func_800B00D4_9FF84(arg0, 3);\n return 1;\n}", "prependC": "#include \"9FF70.h\"\n#include \"3E160.h\"\n#include \"42170.h\"\n#include \"46080.h\"\n#include \"4CD70.h\"\n#include \"51060.h\"\n#include \"56910.h\"\n#include \"594E0.h\"\n#include \"5AA90.h\"\n#include \"5DBC0.h\"\n#include \"5E590.h\"\n#include \"6E840.h\"\n#include \"A9A40.h\"\n#include \"common.h\"\n#include \"gamestate.h\"\n#include \"geometry.h\"\n#include \"graphics.h\"\n#include \"levels/starlight_highway.h\"\n#include \"rand.h\"\n#include \"task_scheduler.h\"", @@ -531,9 +503,7 @@ "int64", "memory", "struct", - "arithmetic", - "loop", - "bitwise" + "arithmetic" ], "funcC": "void func_8005AB58_5B758(Player *player) {\n Vec3i deltaPos;\n GameState *allocation;\n s32 unused1;\n s32 combinedRadius;\n s32 playerIndex;\n Player *targetPlayer;\n s32 numPlayers;\n\n allocation = (GameState *)getCurrentAllocation();\n numPlayers = allocation->numPlayers;\n\n for (playerIndex = 0; playerIndex < allocation->numPlayers; playerIndex++) {\n targetPlayer = &allocation->players[playerIndex];\n\n /* Skip collision if players are on the same team */\n if (player->unkBB8 == targetPlayer->unkBB8) {\n continue;\n }\n\n /* Skip if either player has collision disabled (flag 0x10) */\n if (player->unkB88 & 0x10) {\n continue;\n }\n if (targetPlayer->unkB88 & 0x10) {\n continue;\n }\n\n /* Copy target's collision box local position */\n memcpy(&deltaPos, &targetPlayer->unkAD4, 0xC);\n\n /* Convert to world space */\n deltaPos.x += targetPlayer->worldPos.x;\n deltaPos.y += targetPlayer->worldPos.y;\n deltaPos.z += targetPlayer->worldPos.z;\n\n /* Calculate relative position to player's collision box */\n deltaPos.x -= player->worldPos.x + player->unkAD4[0];\n deltaPos.y -= player->worldPos.y + player->unkAD4[1];\n deltaPos.z -= player->worldPos.z + player->unkAD4[2];\n\n /* Sum of both collision box radii */\n combinedRadius = targetPlayer->unkAE0 + player->unkAE0;\n\n /* Quick AABB check before expensive distance calculation */\n if (-combinedRadius < deltaPos.x && deltaPos.x < combinedRadius && -combinedRadius < deltaPos.y &&\n deltaPos.y < combinedRadius && -combinedRadius < deltaPos.z && deltaPos.z < combinedRadius) {\n s32 dist;\n\n dist = isqrt64((s64)deltaPos.x * deltaPos.x + (s64)deltaPos.y * deltaPos.y + (s64)deltaPos.z * deltaPos.z);\n\n if (dist < combinedRadius) {\n /* Avoid divide by zero */\n if (dist == 0) {\n dist = 1;\n }\n\n /* Calculate push vector to separate the players */\n deltaPos.x = ((s64)deltaPos.x * combinedRadius / dist) - deltaPos.x;\n deltaPos.y = ((s64)deltaPos.y * combinedRadius / dist) - deltaPos.y;\n deltaPos.z = ((s64)deltaPos.z * combinedRadius / dist) - deltaPos.z;\n\n /* Skip push if player has flag 0x80 */\n if (player->unkB84 & 0x80) {\n continue;\n }\n\n /* If target has flag 0x80, do full push */\n if (targetPlayer->unkB84 & 0x80) {\n player->worldPos.x -= deltaPos.x;\n player->worldPos.y -= deltaPos.y;\n player->worldPos.z -= deltaPos.z;\n } else {\n /* Half push */\n player->worldPos.x -= deltaPos.x / 2;\n player->worldPos.y -= deltaPos.y / 2;\n player->worldPos.z -= deltaPos.z / 2;\n }\n }\n }\n }\n}", "prependC": "#include \"5AA90.h\"\n#include \"3E160.h\"\n#include \"56910.h\"\n#include \"594E0.h\"\n#include \"5DBC0.h\"\n#include \"common.h\"\n#include \"displaylist.h\"\n#include \"gamestate.h\"\n#include \"geometry.h\"\n#include \"task_scheduler.h\"", @@ -548,11 +518,9 @@ { "sym": "func_800B3F2C_A3DDC", "features": [ - "switch", "struct", "pointer", - "branch", - "bitwise" + "branch" ], "funcC": "s32 func_800B3F2C_A3DDC(Player *arg0) {\n u32 temp_v0;\n s32 temp_v1;\n s32 temp_v0_2;\n u8 state;\n s32 temp;\n\n temp_v0 = arg0->unkB84;\n temp_v1 = arg0->unk450;\n\n arg0->unk44C = 0;\n arg0->unk454 = 0;\n\n temp_v0 |= 0x60;\n temp_v1 += -0x6000;\n\n arg0->unkB84 = temp_v0;\n arg0->unk450 = temp_v1;\n\n func_800B40D4_A3F84(arg0);\n func_800B02AC_A015C(arg0);\n\n temp = arg0->unkAE0;\n if (!(0xDFFFF < temp)) {\n arg0->unkAE0 = temp + 0x8000;\n }\n\n state = arg0->unkBC0;\n\n switch (state) {\n case 0:\n if (func_8005D308_5DF08(arg0, 0x1E)) {\n temp_v0_2 = arg0->unkBC0;\n temp_v0_2++;\n arg0->unkBC0 = temp_v0_2;\n }\n break;\n\n case 1:\n if (func_8005D308_5DF08(arg0, 0x20)) {\n temp_v1 = 0x14;\n temp_v0_2 = arg0->unkBC0;\n arg0->unkB8C = temp_v1;\n temp_v0_2++;\n arg0->unkBC0 = temp_v0_2;\n }\n break;\n\n case 2:\n temp_v0_2 = arg0->unkB8C;\n if (temp_v0_2 != 0) {\n temp_v0_2--;\n arg0->unkB8C = temp_v0_2;\n } else {\n temp_v0_2 = arg0->unkBC0;\n temp_v0_2++;\n arg0->unkBC0 = temp_v0_2;\n }\n break;\n\n case 3:\n if (func_8005D308_5DF08(arg0, 0x1F)) {\n temp_v0_2 = arg0->unkBC0;\n temp_v0_2++;\n arg0->unkBC0 = temp_v0_2;\n }\n break;\n\n case 4:\n if (func_8005D308_5DF08(arg0, 0x20)) {\n arg0->unkBC0 = 2;\n arg0->unkB8C = 0x14;\n }\n break;\n }\n\n func_8005D804_5E404(arg0, 4, 0);\n\n return 0;\n}", "prependC": "#include \"9FF70.h\"\n#include \"3E160.h\"\n#include \"42170.h\"\n#include \"46080.h\"\n#include \"4CD70.h\"\n#include \"51060.h\"\n#include \"56910.h\"\n#include \"594E0.h\"\n#include \"5AA90.h\"\n#include \"5DBC0.h\"\n#include \"5E590.h\"\n#include \"6E840.h\"\n#include \"A9A40.h\"\n#include \"common.h\"\n#include \"gamestate.h\"\n#include \"geometry.h\"\n#include \"graphics.h\"\n#include \"levels/starlight_highway.h\"\n#include \"rand.h\"\n#include \"task_scheduler.h\"", @@ -562,11 +530,9 @@ { "sym": "func_800B3784_A3634", "features": [ - "switch", "struct", "pointer", - "branch", - "bitwise" + "branch" ], "funcC": "void func_800B3784_A3634(Player *arg0) {\n s32 result;\n u8 temp;\n\n switch (arg0->unkBD5) {\n default:\n break;\n\n case 0:\n arg0->unkBD5++;\n arg0->unkBD6 = 4;\n case 1:\n result = func_800B36F0_A35A0(arg0);\n if (result != 0) {\n arg0->unkBD6 = result;\n arg0->unkBD7 = 0;\n arg0->unkBD5 = arg0->unkBD5 + 1;\n arg0->unkB84 |= 0x8000;\n }\n func_8005D308_5DF08(arg0, arg0->unkBD6);\n break;\n\n case 2:\n result = func_8005D308_5DF08(arg0, arg0->unkBD6);\n if (result != 0) {\n if (arg0->unkBD6 == 0x17) {\n func_800B2950_A2800(arg0, 0);\n }\n\n if (arg0->unkBD6 == 0x19) {\n func_800B2950_A2800(arg0, 1);\n }\n\n if (arg0->unkBD6 == 0x15) {\n func_800B2950_A2800(arg0, 2);\n }\n\n if (arg0->unkBD6 == 0x13) {\n func_800B2950_A2800(arg0, 3);\n }\n\n arg0->unkBD5++;\n arg0->unkBD6++;\n }\n\n result = func_800B36F0_A35A0(arg0);\n if (result != 0) {\n arg0->unkBD7 = result;\n }\n\n break;\n\n case 3:\n result = func_8005D308_5DF08(arg0, arg0->unkBD6);\n if (result != 0) {\n if (arg0->unkBD7 != 0) {\n arg0->unkBD5 = 2;\n arg0->unkBD6 = arg0->unkBD7;\n arg0->unkBD7 = 0;\n break;\n } else {\n arg0->unkB84 &= 0xFFFF7FFF;\n arg0->unkBD5 = 1;\n }\n }\n\n result = func_800B36F0_A35A0(arg0);\n if (result != 0) {\n arg0->unkBD7 = result;\n }\n break;\n }\n\n if (arg0->unkB84 & 0xC000) {\n arg0->unkB84 |= 0x1000;\n } else {\n arg0->unkB84 &= ~0x1000;\n }\n}", "prependC": "#include \"9FF70.h\"\n#include \"3E160.h\"\n#include \"42170.h\"\n#include \"46080.h\"\n#include \"4CD70.h\"\n#include \"51060.h\"\n#include \"56910.h\"\n#include \"594E0.h\"\n#include \"5AA90.h\"\n#include \"5DBC0.h\"\n#include \"5E590.h\"\n#include \"6E840.h\"\n#include \"A9A40.h\"\n#include \"common.h\"\n#include \"gamestate.h\"\n#include \"geometry.h\"\n#include \"graphics.h\"\n#include \"levels/starlight_highway.h\"\n#include \"rand.h\"\n#include \"task_scheduler.h\"", @@ -582,8 +548,7 @@ "sym": "func_80038090_38C90", "features": [ "struct", - "pointer", - "bitwise" + "pointer" ], "funcC": "void func_80038090_38C90(s16 arg0) {\n u8 temp;\n\n temp = D_800AFE8C_A71FC->saveSlotIndex;\n if (temp == 0xB) {\n EepromSaveData->setting_4E = 1;\n func_800383D8_38FD8(0xF);\n }\n\n temp = D_800AFE8C_A71FC->saveSlotIndex;\n if (temp == 0xE) {\n EepromSaveData->setting_4F = 1;\n func_800383D8_38FD8(0xD);\n }\n\n temp = D_800AFE8C_A71FC->saveSlotIndex;\n if (temp == 0xD) {\n if (arg0 == 7) {\n EepromSaveData->setting_50 = 1;\n func_800383D8_38FD8(0xE);\n }\n }\n\n if (D_800AFE8C_A71FC->unk24 != 0) {\n if (D_800AFE8C_A71FC->saveSlotIndex == 1) {\n if (func_80038388_38F88(0xA) & 0xFF) {\n D_800AFE8C_A71FC->errorFlag = 2;\n }\n }\n\n if (D_800AFE8C_A71FC->saveSlotIndex == 3) {\n if (func_80038388_38F88(0xB) & 0xFF) {\n D_800AFE8C_A71FC->errorFlag = 3;\n }\n }\n\n if (D_800AFE8C_A71FC->saveSlotIndex == 9) {\n if (func_80038388_38F88(0xC) & 0xFF) {\n D_800AFE8C_A71FC->errorFlag = 4;\n }\n }\n\n if (D_800AFE8C_A71FC->saveSlotIndex == 5) {\n if (func_80038388_38F88(0xE) & 0xFF) {\n D_800AFE8C_A71FC->errorFlag = 6;\n }\n }\n\n if (D_800AFE8C_A71FC->saveSlotIndex == 8) {\n if (func_80038388_38F88(0xF) & 0xFF) {\n D_800AFE8C_A71FC->errorFlag = 7;\n }\n }\n\n if (D_800AFE8C_A71FC->saveSlotIndex == 0xB) {\n if (func_80038388_38F88(0x10) & 0xFF) {\n D_800AFE8C_A71FC->errorFlag = 8;\n }\n }\n\n if (D_800AFE8C_A71FC->saveSlotIndex == 0) {\n if (func_80038388_38F88(0xD) & 0xFF) {\n D_800AFE8C_A71FC->errorFlag = 5;\n }\n }\n\n if (D_800AFE8C_A71FC->saveSlotIndex == 4) {\n if (func_80038388_38F88(9) & 0xFF) {\n D_800AFE8C_A71FC->errorFlag = 1;\n }\n }\n\n if (D_800AFE8C_A71FC->saveSlotIndex == 7) {\n if (func_80038388_38F88(0x11) & 0xFF) {\n D_800AFE8C_A71FC->errorFlag = 9;\n }\n }\n }\n}", "prependC": "#include \"D_800AFE8C_A71FC_type.h\"\n#include \"EepromSaveData_type.h\"\n#include \"common.h\"", diff --git a/apps/benchmark/dataset/synthetic.ts b/apps/benchmark/dataset/synthetic.ts index f2b42416..8e136204 100644 --- a/apps/benchmark/dataset/synthetic.ts +++ b/apps/benchmark/dataset/synthetic.ts @@ -31,18 +31,28 @@ export const SYNTHETIC: SynthSpec[] = [ { sym: 'sub', src: 'int sub(int a,int b){ return a-b; }', features: ['arithmetic'], toolchains: ALL }, { sym: 'mul', src: 'int mul(int a,int b){ return a*b; }', features: ['arithmetic'], toolchains: ALL }, { sym: 'mulc', src: 'int mulc(int a){ return a*10; }', features: ['arithmetic'], toolchains: ALL }, - { sym: 'divc', src: 'int divc(int a){ return a/7; }', features: ['arithmetic', 'signed-div-const'], toolchains: ALL }, - { sym: 'div2', src: 'int div2(int a){ return a/2; }', features: ['arithmetic', 'signed-div-pow2'], toolchains: ALL }, + { + sym: 'divc', + src: 'int divc(int a){ return a/7; }', + features: ['arithmetic', 'div-const', 'signed'], + toolchains: ALL, + }, + { + sym: 'div2', + src: 'int div2(int a){ return a/2; }', + features: ['arithmetic', 'div-pow2', 'signed'], + toolchains: ALL, + }, { sym: 'udivc', src: 'unsigned udivc(unsigned a){ return a/7; }', - features: ['arithmetic', 'unsigned-div-const'], + features: ['arithmetic', 'div-const', 'unsigned'], toolchains: ALL, }, { sym: 'modc', src: 'int modc(int a){ return a%10; }', - features: ['arithmetic', 'signed-mod-const'], + features: ['arithmetic', 'mod-const', 'signed'], toolchains: ALL, }, // register-divisor division — exercises the soft-division fold (`bl __divsi3(a,b)` on agbcc) @@ -69,81 +79,81 @@ export const SYNTHETIC: SynthSpec[] = [ { sym: 'expr1', src: 'int expr1(int a,int b,int c){ return a*b + c - 3; }', - features: ['arithmetic', 'expr'], + features: ['arithmetic'], toolchains: ALL, }, // ── bitwise ─────────────────────────────────────────────────────────────────────────── - { sym: 'band', src: 'int band(int a,int b){ return a&b; }', features: ['bitwise'], toolchains: ALL }, - { sym: 'bor', src: 'int bor(int a,int b){ return a|b; }', features: ['bitwise'], toolchains: ALL }, - { sym: 'bxor', src: 'int bxor(int a,int b){ return a^b; }', features: ['bitwise'], toolchains: ALL }, - { sym: 'bnot', src: 'int bnot(int a){ return ~a; }', features: ['bitwise'], toolchains: ALL }, - { sym: 'shl', src: 'int shl(int a,int b){ return a<>b; }', - features: ['bitwise', 'shift'], + features: [], toolchains: ALL, }, { sym: 'shrs', src: 'int shrs(int a,int b){ return a>>b; }', - features: ['bitwise', 'shift', 'signed'], + features: ['signed'], toolchains: ALL, }, { sym: 'mask8', src: 'unsigned mask8(unsigned x){ return x & 0xff; }', - features: ['bitwise', 'mask'], + features: ['mask'], toolchains: ALL, }, { sym: 'bittest', src: 'int bittest(int x,int n){ return (x>>n)&1; }', - features: ['bitwise', 'shift', 'mask'], + features: ['mask'], toolchains: ALL, }, { sym: 'setbit', src: 'int setbit(int x,int n){ return x | (1<b?a:b; }', features: ['compare', 'ternary'], toolchains: ALL }, - { sym: 'mini', src: 'int mini(int a,int b){ return ab?a:b; }', features: ['compare'], toolchains: ALL }, + { sym: 'mini', src: 'int mini(int a,int b){ return a0)return 1; if(x<0)return -1; return 0; }', - features: ['compare', 'branch', 'multi-if'], + features: ['compare', 'branch'], toolchains: ALL, }, { sym: 'iszero', src: 'int iszero(int x){ return x==0; }', features: ['compare', 'bool'], toolchains: ALL }, { sym: 'land', src: 'int land(int a,int b){ return a && b; }', - features: ['compare', 'logical-and'], + features: ['compare', 'branch'], toolchains: ALL, }, - { sym: 'lor', src: 'int lor(int a,int b){ return a || b; }', features: ['compare', 'logical-or'], toolchains: ALL }, + { sym: 'lor', src: 'int lor(int a,int b){ return a || b; }', features: ['compare', 'branch'], toolchains: ALL }, { sym: 'inrange', src: 'int inrange(int x,int lo,int hi){ return x>=lo && x<=hi; }', - features: ['compare', 'logical-and', 'range'], + features: ['compare', 'branch'], toolchains: ALL, }, { sym: 'clampr', src: 'int clampr(int x,int lo,int hi){ if(xhi)x=hi; return x; }', - features: ['compare', 'branch', 'multi-if'], + features: ['compare', 'branch'], toolchains: ALL, }, @@ -151,8 +161,8 @@ export const SYNTHETIC: SynthSpec[] = [ { sym: 'tou8', src: 'u8 tou8(int x){ return (u8)x; }', features: ['cast', 'narrow'], toolchains: ALL }, { sym: 'tos8', src: 's8 tos8(int x){ return (s8)x; }', features: ['cast', 'narrow', 'signed'], toolchains: ALL }, { sym: 'tou16', src: 'u16 tou16(int x){ return (u16)x; }', features: ['cast', 'narrow'], toolchains: ALL }, - { sym: 'sextb', src: 'int sextb(s8 x){ return x; }', features: ['cast', 'sign-extend'], toolchains: ALL }, - { sym: 'zextb', src: 'int zextb(u8 x){ return x; }', features: ['cast', 'zero-extend'], toolchains: ALL }, + { sym: 'sextb', src: 'int sextb(s8 x){ return x; }', features: ['sign-extend'], toolchains: ALL }, + { sym: 'zextb', src: 'int zextb(u8 x){ return x; }', features: ['zero-extend'], toolchains: ALL }, // ── memory ────────────────────────────────────────────────────────────────────────────── { sym: 'deref', src: 'int deref(int *p){ return *p; }', features: ['memory', 'load'], toolchains: ALL }, @@ -167,7 +177,7 @@ export const SYNTHETIC: SynthSpec[] = [ { sym: 'loadoff', src: 'int loadoff(int *p){ return p[2]; }', - features: ['memory', 'load', 'offset'], + features: ['memory', 'load'], toolchains: ALL, }, { @@ -187,13 +197,13 @@ export const SYNTHETIC: SynthSpec[] = [ { sym: 'byteidx', src: 'u8 byteidx(u8 *p,int i){ return p[i]; }', - features: ['memory', 'array', 'byte'], + features: ['memory', 'array'], toolchains: ALL, }, { sym: 'ptradd', src: 'int *ptradd(int *p,int n){ return p+n; }', - features: ['memory', 'pointer-arith'], + features: ['memory', 'pointer'], toolchains: ALL, }, @@ -244,14 +254,14 @@ export const SYNTHETIC: SynthSpec[] = [ { sym: 'upun', src: 'union F{float f;u32 u;};\nu32 upun(float x){ union F t; t.f=x; return t.u; }', - features: ['union', 'field', 'float', 'type-pun'], + features: ['union', 'field', 'float'], toolchains: ALL, ctx: 'u32 upun(float);', }, { sym: 'utag', src: 'struct T{int kind;union{int i;u16 h;u8 b;}v;};\nint utag(struct T*t){ switch(t->kind){case 0:return t->v.i;case 1:return t->v.h;default:return t->v.b;} }', - features: ['union', 'struct', 'field', 'switch', 'mixed-width'], + features: ['union', 'struct', 'field', 'mixed-width'], toolchains: ALL, }, { @@ -267,31 +277,31 @@ export const SYNTHETIC: SynthSpec[] = [ { sym: 'sumto', src: 'int sumto(int n){ int s=0,i; for(i=0;i0){c++;n--;} return c; }', - features: ['loop', 'while'], + features: [], toolchains: ALL, }, { sym: 'arraysum', src: 'int arraysum(int *a,int n){ int s=0,i; for(i=0;i0){ if(b>0) return a+b; return a; } return 0; }', - features: ['branch', 'nested-if'], + features: ['branch'], toolchains: ALL, }, { sym: 'loopif', src: 'int loopif(int *a,int n){ int s=0,i; for(i=0;i0) s+=a[i]; } return s; }', - features: ['loop', 'branch', 'memory'], + features: ['branch', 'memory'], toolchains: ALL, }, @@ -349,31 +359,31 @@ export const SYNTHETIC: SynthSpec[] = [ { sym: 'sw_ret', src: 'int sw_ret(int x){ switch(x){case 0:return 10;case 1:return 20;case 2:return 30;case 3:return 40;default:return -1;} }', - features: ['switch'], + features: [], toolchains: ALL, }, { sym: 'sw_op', src: 'int sw_op(int op,int a,int b){ switch(op){case 0:return a+b;case 1:return a-b;case 2:return a*b;case 3:return a&b;default:return 0;} }', - features: ['switch', 'arithmetic'], + features: ['arithmetic'], toolchains: ALL, }, { sym: 'sw_fall', src: 'int sw_fall(int x){ int r=0; switch(x){case 3:r++;case 2:r++;case 1:r++;} return r; }', - features: ['switch', 'fallthrough'], + features: ['fallthrough'], toolchains: ALL, }, { sym: 'sw_sparse', src: 'int sw_sparse(int x){ switch(x){case 1:return 1;case 10:return 2;case 100:return 3;case 1000:return 4;default:return 0;} }', - features: ['switch', 'sparse'], + features: ['sparse'], toolchains: ALL, }, { sym: 'sw_void', src: 'void sw_void(int x,int *p){ switch(x){case 0:*p=1;break;case 1:*p=2;break;default:*p=0;} }', - features: ['switch', 'void', 'memory'], + features: ['memory'], toolchains: ALL, ctx: 'void sw_void(int,int*);', proto: { sw_void: { returnsVoid: true } }, @@ -383,7 +393,7 @@ export const SYNTHETIC: SynthSpec[] = [ { sym: 'sw_jt', src: 'int sw_jt(int x){ switch(x){case 0:return 3;case 1:return 5;case 2:return 7;case 3:return 9;case 4:return 11;case 5:return 13;case 6:return 15;case 7:return 17;default:return -1;} }', - features: ['switch', 'dense'], + features: ['dense'], toolchains: ALL, }, @@ -434,7 +444,7 @@ export const SYNTHETIC: SynthSpec[] = [ { sym: 'fma1', src: 'float fma1(float a,float b,float c){ return a*b+c; }', - features: ['float', 'arithmetic', 'expr'], + features: ['float', 'arithmetic'], toolchains: ALL, ctx: 'float fma1(float,float,float);', }, @@ -449,33 +459,38 @@ export const SYNTHETIC: SynthSpec[] = [ { sym: 'lladd', src: 'long long lladd(long long a,long long b){ return a+b; }', - features: ['s64', 'arithmetic'], + features: ['int64', 'arithmetic'], toolchains: ALL, }, { sym: 'llsub', src: 'long long llsub(long long a,long long b){ return a-b; }', - features: ['s64', 'arithmetic'], + features: ['int64', 'arithmetic'], toolchains: ALL, }, { sym: 'llshl', src: 'long long llshl(long long a,int b){ return a<>b; }', - features: ['s64', 'shift', 'signed'], + features: ['int64', 'signed'], + toolchains: ALL, + }, + { sym: 'i2ll', src: 'long long i2ll(int x){ return x; }', features: ['int64', 'sign-extend'], toolchains: ALL }, + { + sym: 'll2i', + src: 'int ll2i(long long x){ return (int)x; }', + features: ['int64', 'cast', 'narrow'], toolchains: ALL, }, - { sym: 'i2ll', src: 'long long i2ll(int x){ return x; }', features: ['s64', 'cast', 'sign-extend'], toolchains: ALL }, - { sym: 'll2i', src: 'int ll2i(long long x){ return (int)x; }', features: ['s64', 'cast', 'narrow'], toolchains: ALL }, { sym: 'llcmp', src: 'int llcmp(long long a,long long b){ return a>(32-n)); }', - features: ['bitwise', 'rotate', 'shift'], + features: ['rotate'], toolchains: ALL, }, { sym: 'rotr', src: 'unsigned rotr(unsigned x,int n){ return (x>>n)|(x<<(32-n)); }', - features: ['bitwise', 'rotate', 'shift'], + features: ['rotate'], toolchains: ALL, ctx: 'unsigned rotr(unsigned,int);', }, { sym: 'extractbits', src: 'unsigned extractbits(unsigned x){ return (x>>4)&0xF; }', - features: ['bitwise', 'shift', 'mask'], + features: ['mask'], toolchains: ALL, }, { sym: 'clearbit', src: 'int clearbit(int x,int n){ return x & ~(1<>16; }', features: ['bitwise', 'shift'], toolchains: ALL }, + { sym: 'hi16', src: 'unsigned hi16(unsigned x){ return x>>16; }', features: [], toolchains: ALL }, { sym: 'mergebits', src: 'unsigned mergebits(unsigned a,unsigned b){ return (a&0xFFFF)|(b<<16); }', - features: ['bitwise', 'mask', 'shift'], + features: ['mask'], toolchains: ALL, }, { @@ -567,25 +582,25 @@ export const SYNTHETIC: SynthSpec[] = [ { sym: 'absdiff', src: 'int absdiff(int a,int b){ return a>b?a-b:b-a; }', - features: ['compare', 'ternary'], + features: ['compare'], toolchains: ALL, }, { sym: 'clampu8', src: 'int clampu8(int x){ if(x<0)return 0; if(x>255)return 255; return x; }', - features: ['compare', 'branch', 'multi-if'], + features: ['compare', 'branch'], toolchains: ALL, }, { sym: 'max3', src: 'int max3(int a,int b,int c){ int m=a>b?a:b; return m>c?m:c; }', - features: ['compare', 'ternary'], + features: ['compare'], toolchains: ALL, }, { sym: 'selnz', src: 'int selnz(int c,int a,int b){ return c?a:b; }', - features: ['compare', 'ternary', 'select'], + features: ['compare'], toolchains: ALL, }, { sym: 'notb', src: 'int notb(int x){ return !x; }', features: ['compare', 'bool'], toolchains: ALL }, @@ -610,7 +625,7 @@ export const SYNTHETIC: SynthSpec[] = [ { sym: 'memcpy1', src: 'void memcpy1(u8 *d,u8 *s,int n){ int i; for(i=0;im)m=a[i]; return m; }', - features: ['loop', 'memory', 'branch'], + features: ['memory', 'branch'], toolchains: ALL, }, { sym: 'countpos', src: 'int countpos(int *a,int n){ int c=0,i; for(i=0;i0)c++; return c; }', - features: ['loop', 'memory', 'branch'], + features: ['memory', 'branch'], toolchains: ALL, }, { sym: 'dotprod', src: 'int dotprod(int *a,int *b,int n){ int s=0,i; for(i=0;i0); return s; }', - features: ['loop', 'do-while'], + features: [], toolchains: ALL, }, { sym: 'breakloop', src: 'int breakloop(int *a,int n){ int i; for(i=0;ilen2(); }', - features: ['c++', 'method', 'this-ptr'], + features: ['method'], toolchains: ['mwcc_242_81'], note: 'C++ method via this-pointer', }, @@ -754,7 +769,7 @@ export const SYNTHETIC_CPP: SynthSpec[] = [ sym: 'Counter__inc', lang: 'c++', src: 'struct Counter{int n;void inc(){ n++; }};\nextern "C" void Counter__inc(Counter*c){ c->inc(); }', - features: ['c++', 'method', 'mutate'], + features: ['method'], toolchains: ['mwcc_242_81'], // `struct` spelled out: the ctx must parse as C (m2c's context parser) AND as C++ ctx: 'struct Counter; void Counter__inc(struct Counter*);', diff --git a/apps/benchmark/src/cases/features.ts b/apps/benchmark/src/cases/features.ts index cf854272..994abd6d 100644 --- a/apps/benchmark/src/cases/features.ts +++ b/apps/benchmark/src/cases/features.ts @@ -1,96 +1,44 @@ -// The `features` vocabulary — what each tag means and, where possible, how it is CHECKED. +// Feature-tag DETECTORS. The vocabulary itself lives in @asmlift/bench-schema, because apps/web +// renders it too and cannot import this app; here we only decide, per row, which tags the evidence +// supports. // -// Tags were authored per extraction session with no shared definition (`features: string[]` and -// nothing else), and a four-way audit found roughly a fifth of the real tier carrying at least one -// tag its function does not support: `bitfield` produced by a regex that matched ternaries, -// `soft-div` applied to `/` by a constant that compiles to a shift, `call` on leaf functions that -// emit no call instruction, `loop` on straight-line bodies, `arithmetic` on a function whose entire -// compiled form is `lui/jr/sb/nop`. A tag nobody can falsify drifts into a comment. +// Three evidence kinds, and only the third is authored: // -// So the vocabulary is split in two: -// -// MACHINE-CHECKED — decidable from evidence the dataset already carries (the function's own -// source, or the compiled `targetAsm`). `features.test.ts` asserts BOTH directions for these: -// tagged ⇒ evidence exists, and evidence exists ⇒ tagged. They cannot rot. -// -// JUDGEMENT — a human call (is this "bulk" memory movement? is this arithmetic "interesting"?). -// Defined here so the call is at least made against a written standard, but not asserted. -// -// A tag outside both lists fails validation: the vocabulary is closed, so a typo (`fixedpoint` for -// `fixed-point`) is an error rather than a silently-new category that splits every aggregate. +// source — from the function's own C. Derived into every row by eval/evaluate.ts, so a dataset +// cannot disagree with its own source. +// codegen — from the row's compiled `targetAsm`. Also per row: what a compiler does with a +// constant divide or a switch is a property of (function × toolchain), and one +// synthetic spec runs on four. +// judgement — a human call. Authored in the dataset, and held to the floor below. +import { type FeatureDef, KNOWN_FEATURES, featuresByEvidence } from '@asmlift/bench-schema'; -/** Tags decided from the function's own C source. */ -export const SOURCE_CHECKED = { - switch: 'a `switch` statement appears in the body', - goto: 'a `goto` appears in the body', - /** A REAL do-while loop. `do { … } while (0)` is a macro idiom with no back edge — not a loop. */ - 'do-while': 'a `do { … } while (cond)` loop, where cond is not the literal 0', - loop: 'a `for`, `while`, or real do-while loop appears in the body', - 'nested-loop': 'a loop lexically inside another loop', - ternary: 'a `?:` conditional expression appears in the body', - sizeof: 'the `sizeof` operator appears in the body', - shift: '`<<` or `>>` appears in the body', - bitwise: '`&`, `|`, `^`, or `~` appears in the body (as an operator, not `&&`/`||`/address-of)', -} as const; +const idsOf = (kind: Parameters[0]): Set => + new Set(featuresByEvidence(kind).map((f: FeatureDef) => f.id)); -/** Tags decided from the compiled reference assembly (`targetAsm`). */ -export const ASM_CHECKED = { - /** The COMPILED code calls a soft-division helper. `/` by a constant that becomes a shift is not - * soft-div, and neither is a hardware `div` (MIPS) or a BIOS division syscall (GBA `svc #6`). */ - 'soft-div': 'the compiled code calls __divsi3/__udivsi3/__modsi3/__umodsi3', - /** ANY call instruction. The callee's NAME is deliberately not used: these are unlinked objects, - * so an external MIPS `jal` renders as `jal 0 ` — the callee lives in a - * relocation the harness's objdump flags do not emit. Filtering by name would discard every - * real call on MIPS and keep none. */ - call: 'the compiled code contains a call instruction', - mmio: 'the compiled code references a hardware I/O register address (0x04000000-0x040003FF)', - dma: 'the compiled code programs the DMA registers (0x040000B0-0x040000DF)', -} as const; +/** Tags decided from the function's own C source. */ +export const SOURCE_CHECKED = idsOf('source'); +/** Tags decided from the row's compiled reference assembly. */ +export const CODEGEN_DERIVED = idsOf('codegen'); +/** Tags that are a human call: defined in the vocabulary, held to a floor here, never asserted. */ +export const JUDGEMENT = idsOf('judgement'); -/** Tags that are a human call. Defined, not asserted. */ -export const JUDGEMENT = { - arithmetic: 'integer arithmetic is a POINT of the function, not merely an index computation', - branch: 'conditional control flow is a point of the function', - compare: 'the function is essentially a comparison', - struct: 'a struct type is used', - union: 'a union type is accessed (including through a project typedef)', - bitfield: 'a declared C bitfield (`u32 x : 2`) is read or written', - field: 'a named struct/union member is accessed', - array: 'an array is indexed', - pointer: 'pointer arithmetic or dereference beyond plain member access', - cast: 'an explicit cast that changes the value or its width', - memory: 'bulk memory movement (copy/clear/compress), not any single load or store', - store: 'a store is the point of the function', - global: 'a file-scope or extern variable is referenced', - table: 'a constant lookup table is read with a computed index', - float: 'floating-point types are used', - 'fixed-point': 'Q-format integer math', - int64: '64-bit integer arithmetic', - matrix: 'matrix math', - fnptr: 'a call through a function pointer', - recursion: 'the function calls itself', - macro: 'a project macro is load-bearing for the shape', - abs: 'absolute value is computed', - baseline: 'a trivial function carrying no other feature', -} as const; +export { KNOWN_FEATURES }; /** A NECESSARY condition for a JUDGEMENT tag: failing it makes the tag indefensible. * - * These tags are a human call — "is this *bulk* memory movement?" cannot be decided by a regex — - * so the sufficient condition stays with the reviewer. But most of them have a floor that CAN be - * decided, and the audit found tags sitting below it: `arithmetic` on a function whose whole - * compiled form is `lui/jr/sb/nop`, `array` on a body containing no `[`, `table` on a body with no - * array at all, `branch` on two straight-line assignments. Checking the floor catches the + * "Is this *bulk* memory movement?" cannot be decided by a regex, so the sufficient condition + * stays with the reviewer — but most tags have a floor that can be, and checking it catches * fabrications without pretending the judgement is mechanical. * - * `global`, `memory`, `union`, `bitfield`, `pointer` and the type-ish tags have NO reliable floor and are - * absent here on purpose: kleod spells several globals as address macros - * (`#define gStreamPtr (*(u8**)0x03004D84)`), which emit a raw `.word 0x3004d84` rather than a - * symbol, and `union`/`bitfield` need the project's headers to resolve. */ -export const JUDGEMENT_FLOOR: Record boolean> = { + * `global`, `memory`, `union`, `bitfield`, `pointer` and the type-ish tags have NO reliable floor + * and are absent on purpose: kleod spells several globals as address macros + * (`#define gStreamPtr (*(u8**)0x03004D84)`), which emit a raw `.word` rather than a symbol, and + * `union`/`bitfield` need the project's headers to resolve. */ +export const JUDGEMENT_FLOOR: Record boolean> = { arithmetic: (b) => /[+%]|(?)|(? /\[/.test(b), table: (b) => /\w+\s*\[\s*[^\]\d\s]/.test(b), // indexed by something that is not a literal + 'variable-index': (b) => /\w+\s*\[\s*[^\]\d\s]/.test(b), cast: (b) => /\(\s*\w+\s*\*+\s*\)/.test(b) || /\(\s*(?:struct|union|enum|const|unsigned|signed|void|int|char|short|long|float|double|[us]\d+|f\d+|\w+_t|[A-Z]\w*)[\w\s]*\**\s*\)/.test( @@ -99,17 +47,28 @@ export const JUDGEMENT_FLOOR: Record bool struct: (b) => /\bstruct\b|\bunion\b|->|\.\s*[A-Za-z_]/.test(b), field: (b) => /->|\.\s*[A-Za-z_]/.test(b), fnptr: (_b, asm) => /\bjalr\b|\bblx\b|_call_via_r/.test(asm), + // `&&`/`||` count: a short-circuit is conditional control flow, and the compiler branches on it branch: (b, asm) => - /\bif\b|\bswitch\b|\?|\bfor\b|\bwhile\b/.test(b) || + /\bif\b|\bswitch\b|\?|\bfor\b|\bwhile\b|&&|\|\|/.test(b) || /\bb(eq|ne|ge|gt|le|lt|hi|ls|cs|cc)\b|beqz|bnez|blez|bgtz|bltz|bgez/.test(asm), + break: (b) => /\bbreak\b/.test(b), + continue: (b) => /\bcontinue\b/.test(b), + // a TYPE tag: the evidence is in the signature, not the body + double: (_b, _asm, whole) => /\bdouble\b/.test(whole), + dense: (b) => /\bswitch\s*\(/.test(b), + sparse: (b) => /\bswitch\s*\(/.test(b), + fallthrough: (b) => /\bswitch\s*\(/.test(b), + // C has no rotate operator; it is spelled as a shift pair + rotate: (b) => /<<|>>/.test(b), + mask: (b) => /&/.test(b), + 'div-const': (b) => /\//.test(b), + 'div-pow2': (b) => /\//.test(b), + 'div-reg': (b) => /\//.test(b), + 'mod-const': (b) => /%/.test(b), + 'mod-pow2': (b) => /%/.test(b), + 'mod-reg': (b) => /%/.test(b), }; -export const KNOWN_FEATURES = new Set([ - ...Object.keys(SOURCE_CHECKED), - ...Object.keys(ASM_CHECKED), - ...Object.keys(JUDGEMENT), -]); - /** Strip comments and string/char literals so operator scans cannot match inside them. */ export function stripLiterals(c: string): string { return c @@ -178,7 +137,7 @@ function hasNestedLoop(body: string): boolean { return nested; } -/** Which SOURCE_CHECKED tags the function's own C supports. */ +/** Which `source` tags the function's own C supports. */ export function sourceEvidence(funcC: string): Set { const b = stripLiterals(funcC); const body = neutralizeDoWhileZero(b.slice(b.indexOf('{'))); @@ -201,8 +160,8 @@ export function sourceEvidence(funcC: string): Set { const DIV_HELPERS = /__(u?divsi3|u?modsi3|divdi3|moddi3)/; -/** Which ASM_CHECKED tags the compiled reference supports. `sym` is the function's own name. */ -export function asmEvidence(targetAsm: string, _sym: string): Set { +/** The `codegen` tags decidable from the assembly ALONE (no source needed). */ +function asmEvidence(targetAsm: string): Set { const out = new Set(); if (DIV_HELPERS.test(targetAsm)) out.add('soft-div'); // direct (`bl`/`jal`) and indirect (`jalr`/`blx`, agbcc's `_call_via_rN` thunk) alike @@ -213,31 +172,6 @@ export function asmEvidence(targetAsm: string, _sym: string): Set { return out; } -// ── codegen tags: derived PER ROW, never authored ─────────────────────────────────────────── -// -// What a compiler does with a constant divisor, a switch, or a multiply is a property of -// (function × toolchain), not of the source. A synthetic spec runs on four toolchains from ONE -// `features` array, so authoring these guaranteed a wrong answer: `divc10` was tagged `magic-div` -// while agbcc emits `bl __divsi3` and IDO emits a hardware `div` — true for two of its four rows, -// and simultaneously missing from `divc`/`modc`, which magic-multiply on exactly the same targets. -// -// So these are computed from the row's own `targetAsm` (plus the source, where the tag is a claim -// about a TRANSFORMATION — "the multiply became shifts" needs to know there was a multiply). -// `evaluate.ts` unions them into every row; a manifest or spec that authors one fails the test. - -export const CODEGEN_DERIVED = { - 'soft-div': 'calls a soft-division helper (__divsi3 and friends)', - 'hw-div': 'uses a hardware divide instruction (MIPS `div`/`divu`, PPC `divw`)', - 'magic-div': 'a constant divide became a multiply-high by a magic reciprocal', - 'jump-table': 'a computed jump through a table (`mov pc`, `jr` on a non-link register, `bctr`)', - 'comparison-tree': 'a source switch became compare-and-branch rather than a jump table', - branchless: 'a source conditional produced no conditional branch', - 'strength-reduce': 'a constant multiply became shifts/adds rather than a multiply instruction', - call: 'contains a call instruction', - mmio: 'references a hardware I/O register (0x04000000-0x040003FF)', - dma: 'programs the DMA registers (0x040000B0-0x040000DF)', -} as const; - const MUL_HIGH = /\b(mulhw|mulhwu|mulhi)\b|\b(mult|multu)\b[\s\S]{0,120}?\bmf(hi|lo)\b/; const HW_DIV = /\b(div|divu|divw|divwu)\b\s+[^\n]*,/; const ANY_MUL = /\b(mul|muls|mult|multu|mullw|mulli|mulhw|mulhwu|smull|umull)\b/; @@ -250,9 +184,11 @@ function hasComputedJump(asm: string): boolean { return /\bmov\s+pc\s*,\s*r\d/.test(asm) || /\bjr\s+(?!ra\b)\w+/.test(asm) || /\bbctr\b/.test(asm); } -/** Codegen tags for one row. `funcC` is the source the row was built from. */ +/** Codegen tags for one row. `funcC` is the source the row was built from — three of these tags are + * claims about a TRANSFORMATION ("the multiply became shifts"), so they need to know there was a + * multiply. */ export function codegenEvidence(funcC: string, targetAsm: string): Set { - const out = asmEvidence(targetAsm, ''); + const out = asmEvidence(targetAsm); const body = neutralizeDoWhileZero(stripLiterals(funcC)); const src = body.slice(body.indexOf('{')); @@ -266,18 +202,21 @@ export function codegenEvidence(funcC: string, targetAsm: string): Set { if (computed) out.add('jump-table'); else if (/\bswitch\s*\(/.test(src)) out.add('comparison-tree'); - // a relational operator counts even without an `if`: `(a>0) - (a<0)` is a conditional the - // compiler may or may not branch on, and whether it does is the whole point of the tag - // a relational operator counts even without an `if`: `(a>0) - (a<0)` is a conditional the - // compiler may or may not branch on, and whether it does is the whole point of the tag. `->` - // and the shift operators must go first or every struct access reads as a comparison. + // A relational operator counts without an `if`: `(a>0) - (a<0)` is a conditional the compiler + // may or may not branch on, which is the point of the tag. `->` and the shifts must go first or + // every struct access reads as a comparison. const rel = src.replace(/->/g, ' ').replace(/<<|>>/g, ' '); const conditional = /\bif\s*\(|\?[^;{}]*:|\bswitch\s*\(|\bfor\s*\(|\bwhile\s*\(|[<>]=?|[=!]=/.test(rel); - // a computed jump is not "branchless" in the useful sense — the conditional became an indirect - // jump, not straight-line code + // a computed jump is not branchless: the conditional became an indirect jump, not straight-line if (conditional && !computed && !COND_BRANCH.test(targetAsm)) out.add('branchless'); // `a * 10` with no multiply instruction anywhere ⇒ the compiler reduced it if (/\*\s*\d/.test(src) && !ANY_MUL.test(targetAsm)) out.add('strength-reduce'); return out; } + +/** Every tag one row publishes: the judgement tags its dataset authored, plus whatever its own + * source and assembly support. Authored data therefore carries judgement tags ONLY. */ +export function rowFeatures(authored: readonly string[], funcC: string, targetAsm: string): string[] { + return [...new Set([...authored, ...sourceEvidence(funcC), ...codegenEvidence(funcC, targetAsm)])]; +} diff --git a/apps/benchmark/src/eval/evaluate.ts b/apps/benchmark/src/eval/evaluate.ts index 8f0f57cc..73e5204d 100644 --- a/apps/benchmark/src/eval/evaluate.ts +++ b/apps/benchmark/src/eval/evaluate.ts @@ -7,7 +7,7 @@ import type { Prototypes } from '@asmlift/core/proto'; import type { SymbolMap } from '@asmlift/core/symbols'; import { cachedAsmDumpText, cachedM2cResult } from '../cache'; -import { codegenEvidence } from '../cases/features'; +import { rowFeatures } from '../cases/features'; import type { Toolchain } from '../toolchains'; import { type Scorer, runAsmlift } from './asmlift'; import { countCompileErrors } from './asmlift'; @@ -159,9 +159,10 @@ export function evaluate( isa: tc.isa, compiler: tc.compiler, language: spec.language, - // Codegen tags are DERIVED per row, never authored: what the compiler did with a constant - // divide or a switch differs per toolchain, and a synthetic spec feeds four of them. - features: [...new Set([...spec.features, ...codegenEvidence(spec.refSource ?? '', asm)])], + // Source and codegen tags are DERIVED per row; the dataset carries judgement tags only. Codegen + // because what the compiler did with a constant divide differs per toolchain and one synthetic + // spec feeds four of them. + features: rowFeatures(spec.features, spec.refSource ?? '', asm), loc: spec.loc, refSource: spec.refSource, sourceUrl: spec.sourceUrl, diff --git a/apps/benchmark/test/features.test.ts b/apps/benchmark/test/features.test.ts index 1763eab7..5ff4189f 100644 --- a/apps/benchmark/test/features.test.ts +++ b/apps/benchmark/test/features.test.ts @@ -1,19 +1,16 @@ -// The `features` vocabulary is only worth something if it can be falsified. These tests do that -// for the machine-checkable half (see src/cases/features.ts): a tag must have evidence, and -// evidence must have a tag. The judgement half is deliberately not asserted — it is defined in -// prose there and reviewed by humans. -// -// Why this exists: an audit found ~20% of the real tier carrying a tag its function does not -// support — `bitfield` invented by a regex that matched ternaries, `soft-div` on a `/` that -// compiles to a shift, `call` on leaf functions, `loop` on straight-line bodies. +// The `features` vocabulary is only worth something if it can be falsified. The vocabulary lives in +// @asmlift/bench-schema and the detectors in src/cases/features.ts; these assert that they and the +// dataset agree — published tags are defined, definitions are used and well-formed, derived tags +// match their evidence in both directions, and authored tags stay above their floor. +import { FEATURES, FEATURE_BY_ID, GROUP_ORDER, KNOWN_FEATURES, featuresByEvidence } from '@asmlift/bench-schema'; import { readFileSync, readdirSync } from 'node:fs'; import { join } from 'node:path'; import { describe, expect, it } from 'vitest'; +import { SYNTHETIC } from '../dataset/synthetic'; import { CODEGEN_DERIVED, JUDGEMENT_FLOOR, - KNOWN_FEATURES, SOURCE_CHECKED, codegenEvidence, sourceEvidence, @@ -42,61 +39,124 @@ const rows = ( targetAsm: string; }[] ).filter((r) => r.targetAsm); -const asmOf = new Map(rows.map((r) => [`${r.project}:${r.sym}`, r.targetAsm])); -const every = manifests.flatMap((m) => m.functions.map((fn) => ({ project: m.project, fn }))); +/** Everything the dataset AUTHORS, real tier and synthetic tier alike. */ +const authored = [ + ...manifests.flatMap((m) => + m.functions.map((fn) => ({ where: `${m.project}:${fn.sym}`, tags: fn.features, src: fn.funcC })), + ), + ...SYNTHETIC.map((s) => ({ where: `synthetic:${s.sym}`, tags: s.features, src: s.src })), +]; -describe('feature tags', () => { - it('uses only the closed vocabulary (a typo must fail, not silently split an aggregate)', () => { - const unknown = every - .flatMap(({ project, fn }) => - fn.features.filter((t) => !KNOWN_FEATURES.has(t)).map((t) => `${project}:${fn.sym} ${t}`), - ) - .sort(); - expect(unknown).toEqual([]); +describe('the vocabulary is closed over the published data', () => { + it('every published tag has a definition', () => { + const undefined_ = new Set(); + for (const r of rows) { + for (const t of r.features) { + if (!KNOWN_FEATURES.has(t)) undefined_.add(`${t} (e.g. ${r.id})`); + } + } + expect([...undefined_].sort()).toEqual([]); }); - it('never tags a source-level feature the function does not contain', () => { - const checked = new Set(Object.keys(SOURCE_CHECKED)); - const bad = every - .flatMap(({ project, fn }) => { - const ev = sourceEvidence(fn.funcC); - return fn.features.filter((t) => checked.has(t) && !ev.has(t)).map((t) => `${project}:${fn.sym} claims ${t}`); - }) + it('every definition is carried by at least one row', () => { + const published = new Set(rows.flatMap((r) => r.features)); + const unused = FEATURES.filter((f) => !f.deprecated && !published.has(f.id)).map((f) => f.id); + expect(unused.sort()).toEqual([]); + }); + + it('every AUTHORED tag has a definition', () => { + const bad = authored + .flatMap(({ where, tags }) => tags.filter((t) => !KNOWN_FEATURES.has(t)).map((t) => `${where} ${t}`)) .sort(); expect(bad).toEqual([]); }); - it('never omits a source-level feature the function does contain', () => { - const bad = every - .flatMap(({ project, fn }) => { - const have = new Set(fn.features); - return [...sourceEvidence(fn.funcC)] - .filter((t) => !have.has(t)) - .map((t) => `${project}:${fn.sym} missing ${t}`); - }) - .sort(); + it('gives every row at least one tag', () => { + expect(rows.filter((r) => r.features.length === 0).map((r) => r.id)).toEqual([]); + }); +}); + +describe('the definitions are well-formed', () => { + it('ids are unique and kebab-case', () => { + expect(FEATURES.length).toBe(KNOWN_FEATURES.size); + expect(FEATURES.map((f) => f.id).filter((id) => !/^[a-z0-9]+(-[a-z0-9]+)*$/.test(id))).toEqual([]); + }); + + it('every definition carries a label, a group and a non-empty summary', () => { + const bad = FEATURES.filter((f) => !f.label.trim() || !f.summary.trim() || !GROUP_ORDER.includes(f.group)).map( + (f) => f.id, + ); expect(bad).toEqual([]); }); - it('never AUTHORS a codegen tag — those are derived per row', () => { - const derived = new Set(Object.keys(CODEGEN_DERIVED)); - const bad = every - .flatMap(({ project, fn }) => - fn.features.filter((t) => derived.has(t)).map((t) => `${project}:${fn.sym} authors ${t}`), - ) + it('every seeAlso resolves to a real id', () => { + const bad = FEATURES.flatMap((f) => + (f.seeAlso ?? []).filter((s) => !FEATURE_BY_ID.has(s)).map((s) => `${f.id} → ${s}`), + ); + expect(bad.sort()).toEqual([]); + }); + + it('a codegen tag never claims to be checkable from the source, and vice versa', () => { + // the detectors are keyed off `evidence`, so a mislabelled entry would silently stop being + // checked rather than fail + expect([...SOURCE_CHECKED].sort()).toEqual([ + 'bitwise', + 'do-while', + 'goto', + 'loop', + 'nested-loop', + 'shift', + 'sizeof', + 'switch', + 'ternary', + ]); + expect([...CODEGEN_DERIVED].sort()).toEqual([ + 'branchless', + 'call', + 'comparison-tree', + 'dma', + 'hw-div', + 'jump-table', + 'magic-div', + 'mmio', + 'soft-div', + 'strength-reduce', + ]); + }); +}); + +describe('tags match their evidence', () => { + it('authored data carries JUDGEMENT tags only (source and codegen are derived per row)', () => { + const derived = new Set([...SOURCE_CHECKED, ...CODEGEN_DERIVED]); + const bad = authored + .flatMap(({ where, tags }) => tags.filter((t) => derived.has(t)).map((t) => `${where} authors ${t}`)) .sort(); expect(bad).toEqual([]); }); + it("publishes exactly the source tags each function's own C supports", () => { + const srcOf = new Map(authored.map((a) => [a.where, a.src])); + const bad: string[] = []; + for (const r of rows) { + const src = srcOf.get(`${r.project}:${r.sym}`); + if (src === undefined) continue; + const want = sourceEvidence(src); + const got = new Set(r.features.filter((t) => SOURCE_CHECKED.has(t))); + for (const t of want) if (!got.has(t)) bad.push(`${r.id} missing ${t}`); + for (const t of got) if (!want.has(t)) bad.push(`${r.id} claims ${t}`); + } + expect(bad.sort()).toEqual([]); + }); + it("publishes exactly the codegen tags each row's own assembly supports", () => { - const bySym = new Map(every.map(({ project, fn }) => [`${project}:${fn.sym}`, fn])); + const srcOf = new Map(authored.map((a) => [a.where, a.src])); const bad: string[] = []; for (const r of rows) { - const fn = bySym.get(`${r.project}:${r.sym}`); - if (!fn) continue; - const want = codegenEvidence(fn.funcC, r.targetAsm); - const got = new Set(r.features.filter((t) => t in CODEGEN_DERIVED)); + const src = srcOf.get(`${r.project}:${r.sym}`); + if (src === undefined) continue; + const want = codegenEvidence(src, r.targetAsm); + const got = new Set(r.features.filter((t) => CODEGEN_DERIVED.has(t))); for (const t of want) if (!got.has(t)) bad.push(`${r.id} missing ${t}`); for (const t of got) if (!want.has(t)) bad.push(`${r.id} claims ${t}`); } @@ -104,22 +164,33 @@ describe('feature tags', () => { }); it('keeps every judgement tag above its floor', () => { - const asmOf = new Map(rows.map((r) => [`${r.project}:${r.sym}`, r.targetAsm])); - const bad = every - .flatMap(({ project, fn }) => { - const stripped = stripLiterals(fn.funcC); + // EVERY toolchain's assembly for the symbol: a tag defensible on the row that branches must + // not be failed by the row the compiler made branchless. + const asmOf = new Map(); + for (const r of rows) { + const k = `${r.project}:${r.sym}`; + asmOf.set(k, (asmOf.get(k) ?? '') + '\n' + r.targetAsm); + } + const bad = authored + .flatMap(({ where, tags, src }) => { + const stripped = stripLiterals(src); const body = stripped.slice(stripped.indexOf('{')); - const asm = asmOf.get(`${project}:${fn.sym}`) ?? ''; - return fn.features - .filter((t) => JUDGEMENT_FLOOR[t] && !JUDGEMENT_FLOOR[t](body, asm)) - .map((t) => `${project}:${fn.sym} claims ${t}`); + const asm = asmOf.get(where) ?? ''; + return tags + .filter((t) => JUDGEMENT_FLOOR[t] && !JUDGEMENT_FLOOR[t](body, asm, stripped)) + .map((t) => `${where} claims ${t}`); }) .sort(); expect(bad).toEqual([]); }); - it('gives every function at least one tag', () => { - expect(every.filter(({ fn }) => fn.features.length === 0).map(({ fn }) => fn.sym)).toEqual([]); + it('every judgement tag with a floor is actually a judgement tag', () => { + const judgement = new Set(featuresByEvidence('judgement').map((f) => f.id)); + expect( + Object.keys(JUDGEMENT_FLOOR) + .filter((k) => !judgement.has(k)) + .sort(), + ).toEqual([]); }); }); @@ -186,7 +257,7 @@ describe('the detectors themselves', () => { }); it('holds judgement tags to a floor without pretending to decide them', () => { - // the floor rejects the fabrications the audit found … + // rejects the fabrications … expect(JUDGEMENT_FLOOR.arithmetic('{ GwSystem.minigame_index = arg0; }', '')).toBe(false); expect(JUDGEMENT_FLOOR.array('{ return gPlayerAvatar.flags; }', '')).toBe(false); expect(JUDGEMENT_FLOOR.table('{ return gEntityInfo[0x23].unkF; }', '')).toBe(false); diff --git a/docs/asmlift-101.md b/docs/asmlift-101.md index 16ea83e2..46ef3d94 100644 --- a/docs/asmlift-101.md +++ b/docs/asmlift-101.md @@ -411,7 +411,7 @@ packages/cli/ @asmlift/cli — the user-facing asmlift interface test/matching/ the byte-exactness suite (needs real toolchains) test/offline/ Node-side suites that run on hosted CI (no toolchains needed) packages/toolchains/ @asmlift/toolchains — pinned calibration toolchains (tests + benchmark only) -packages/bench-schema/ @asmlift/bench-schema — the shared benchmark result/manifest types +packages/bench-schema/ @asmlift/bench-schema — the benchmark contract (types + feature vocabulary) apps/web/ the webapp: Playground + Benchmark view apps/benchmark/ the m2c-vs-asmlift harness; the Benchmark view renders its results ``` diff --git a/packages/bench-schema/README.md b/packages/bench-schema/README.md new file mode 100644 index 00000000..ee8ce0aa --- /dev/null +++ b/packages/bench-schema/README.md @@ -0,0 +1,65 @@ +# @asmlift/bench-schema + +The **benchmark contract**: the result/manifest types the harness ([`apps/benchmark`](../../apps/benchmark)) +produces and the web Benchmark view ([`apps/web`](../../apps/web)) consumes, plus the closed feature +vocabulary they range over. + +**Types and data only** — no I/O, no dependencies, browser-safe by construction. That is what lets +both sides import it; `apps/web` cannot import `apps/benchmark`, so anything both need lives here or +nowhere. + +**Private by design, never published.** It exists to stop the producer and the consumer drifting +apart, not to be a product. + +## The feature vocabulary + +Every benchmark row carries `features`, drawn from a **closed** list in `src/features.ts`. Each tag +declares two orthogonal things: + +- **`evidence`** — how the tag is established. `source` and `codegen` tags are DERIVED per row by + the harness; `judgement` tags are authored in the dataset. +- **`group`** — what kind of construct it names (`control-flow`, `arithmetic`, `data-types`, + `memory`, `calls`, `meta`). This is the axis the web app's filter groups by. A jump table is + control flow whether or not we learned about it from the assembly. + +| evidence | decided from | authored? | +| ----------- | ------------------------- | --------------------------- | +| `source` | the function's own C | no — derived per row | +| `codegen` | the row's own `targetAsm` | no — derived per row | +| `judgement` | a human call | yes, in the manifest / spec | + +So **a dataset entry lists judgement tags only**; `eval/evaluate.ts` unions in the rest. Codegen tags +have to be per row because what a compiler does with a constant divide is a property of +(function × toolchain), and one synthetic spec runs on four of them — `divc10` publishes `soft-div` +on agbcc, `hw-div` on IDO and `magic-div` on KMC and mwcc. + +Each definition also carries the prose and worked C/asm example the web app renders in its +definition drawer, so the vocabulary is documented where it is defined rather than beside the UI. + +### Closed, and enforced + +`apps/benchmark/test/features.test.ts` fails on any of: + +- a published tag with no definition, or a definition carried by no row; +- a definition missing a label, a summary, a known group, or with a dangling `seeAlso`; +- a derived tag whose evidence does not support it — or evidence with no tag, in both directions; +- an authored judgement tag below its floor (a necessary condition; the sufficient one stays with + the reviewer); +- an authored tag of a derived kind. + +`apps/web/test/feature-vocabulary.test.ts` re-checks the first of those against the **committed** +`results.json`, so a snapshot regenerated by a checkout whose vocabulary had drifted cannot ship a +tag the site has no definition for. + +The closure is the point: without it a typo (`fixedpoint` for `fixed-point`) becomes a +silently-new category that halves every aggregate over that feature. + +## Adding a tag + +Add a `FeatureDef` to `FEATURES` with an `id`, `label`, `group`, `evidence` and `summary`; add +`detail` and `example` if the tag needs explaining. A `judgement` tag may want a floor in +`apps/benchmark/src/cases/features.ts`; `source` and `codegen` tags need their detector there. +Then tag at least one row, or the "carried by no row" gate fails. + +Retiring an id: mark it `deprecated: true` to keep an archived dataset readable — it is then exempt +from that gate and never offered in the picker. diff --git a/packages/bench-schema/package.json b/packages/bench-schema/package.json index e8c91925..23d44fab 100644 --- a/packages/bench-schema/package.json +++ b/packages/bench-schema/package.json @@ -4,7 +4,7 @@ "private": true, "license": "MIT", "type": "module", - "description": "The asmlift's benchmark result schema (types only)", + "description": "The asmlift's benchmark contract: result/manifest types + the closed feature vocabulary", "exports": { ".": "./src/index.ts" } diff --git a/packages/bench-schema/src/features.ts b/packages/bench-schema/src/features.ts new file mode 100644 index 00000000..f13c6f94 --- /dev/null +++ b/packages/bench-schema/src/features.ts @@ -0,0 +1,809 @@ +// The closed feature vocabulary: every tag a benchmark row may carry, defined once for the producer +// (apps/benchmark, which derives and validates them) and the consumer (apps/web, which renders them +// in the filter picker and the definition drawer). It lives here because apps/web cannot import +// apps/benchmark. Data only — no I/O, no dependencies, browser-safe. +// +// Two orthogonal axes: `evidence` is how a tag is established, `group` is what kind of construct it +// names. A jump table is control flow whether or not we learned about it from the assembly. +// +// CLOSED: a published tag with no definition, or a definition carried by no row, fails +// apps/benchmark/test/features.test.ts — so a typo cannot become a silently-new category that +// halves every aggregate over it. + +export type FeatureGroup = 'control-flow' | 'arithmetic' | 'data-types' | 'memory' | 'calls' | 'meta'; + +export type EvidenceKind = + /** decided from the function's own C source, derived per row */ + | 'source' + /** decided from the row's compiled reference assembly, derived per row */ + | 'codegen' + /** a human call, authored in the dataset and held to a necessary condition */ + | 'judgement'; + +export const FEATURE_GROUP_LABEL: Record = { + 'control-flow': 'Control flow', + arithmetic: 'Arithmetic', + 'data-types': 'Data & types', + memory: 'Memory', + calls: 'Calls', + meta: 'Meta', +}; + +export const EVIDENCE_LABEL: Record = { + source: 'checked from the source', + codegen: 'derived from the compiled code', + judgement: 'human judgement', +}; + +export interface FeatureExample { + /** the C construct the tag names */ + c: string; + /** what it compiles to, when the tag is a claim about codegen */ + asm?: string; + /** which toolchain produced `asm` */ + toolchain?: string; +} + +export interface FeatureDef { + id: string; + /** human title for the picker and the definition drawer */ + label: string; + group: FeatureGroup; + evidence: EvidenceKind; + /** one line — the picker subtitle and the chip tooltip */ + summary: string; + /** the definition drawer's prose */ + detail?: string; + example?: FeatureExample; + /** related ids, rendered as links in the definition drawer */ + seeAlso?: string[]; + /** a retired id, kept only so an archived dataset stays readable. Exempt from the + * "every definition is carried by a row" gate; never offered in the picker. */ + deprecated?: true; +} + +export const FEATURES: readonly FeatureDef[] = [ + // ── control flow ──────────────────────────────────────────────────────────────────────────── + { + id: 'branch', + label: 'Conditional branch', + group: 'control-flow', + evidence: 'judgement', + summary: 'conditional control flow is a point of the function', + detail: + 'Reserved for functions whose shape is decided by conditionals — not every function that ' + + 'happens to contain an `if`. The floor rejects bodies with no conditional construct and no ' + + 'conditional branch in the compiled code.', + seeAlso: ['compare', 'ternary', 'branchless', 'switch'], + }, + { + id: 'compare', + label: 'Comparison', + group: 'control-flow', + evidence: 'judgement', + summary: 'the function is essentially a comparison', + seeAlso: ['branch', 'bool', 'branchless'], + }, + { + id: 'bool', + label: 'Boolean result', + group: 'control-flow', + evidence: 'judgement', + summary: 'a truth value is produced, not merely tested', + detail: + 'The distinction that matters for recovery: a compiler may materialize a boolean without ' + + 'branching (`slt`, `setcc`), so a boolean-returning function often has no control flow at ' + + 'all in its compiled form.', + seeAlso: ['compare', 'branchless'], + }, + { + id: 'ternary', + label: 'Ternary', + group: 'control-flow', + evidence: 'source', + summary: 'a `?:` conditional expression appears in the body', + seeAlso: ['branch', 'branchless'], + }, + { + id: 'switch', + label: 'Switch', + group: 'control-flow', + evidence: 'source', + summary: 'a `switch` statement appears in the body', + detail: + 'What a switch BECOMES is a separate, per-toolchain question — see `jump-table` and ' + + "`comparison-tree`, which are derived from each row's own assembly rather than authored.", + seeAlso: ['jump-table', 'comparison-tree', 'dense', 'sparse', 'fallthrough'], + }, + { + id: 'dense', + label: 'Dense case range', + group: 'control-flow', + evidence: 'judgement', + summary: 'switch labels are contiguous, inviting a jump table', + seeAlso: ['switch', 'sparse', 'jump-table'], + }, + { + id: 'sparse', + label: 'Sparse case range', + group: 'control-flow', + evidence: 'judgement', + summary: 'switch labels are scattered, inviting compare-and-branch', + seeAlso: ['switch', 'dense', 'comparison-tree'], + }, + { + id: 'fallthrough', + label: 'Case fall-through', + group: 'control-flow', + evidence: 'judgement', + summary: 'a switch case falls through into the next', + detail: + 'A shape structuring must reproduce exactly: recovering fall-through as duplicated bodies ' + + 'compiles to different code, so this is a common source of declines rather than diffs.', + seeAlso: ['switch', 'goto'], + }, + { + id: 'goto', + label: 'Goto', + group: 'control-flow', + evidence: 'source', + summary: 'a `goto` appears in the body', + seeAlso: ['loop', 'fallthrough'], + }, + { + id: 'loop', + label: 'Loop', + group: 'control-flow', + evidence: 'source', + summary: 'a `for`, `while`, or real do-while loop appears in the body', + detail: '`do { … } while (0)` does NOT count: it is a macro idiom with no back edge.', + seeAlso: ['nested-loop', 'do-while', 'break', 'continue'], + }, + { + id: 'do-while', + label: 'Do-while loop', + group: 'control-flow', + evidence: 'source', + summary: 'a `do { … } while (cond)` loop, where cond is not the literal 0', + detail: + 'Distinguished because it is the one loop shape whose condition sits at the BOTTOM, which is ' + + 'also what a `for`/`while` loop becomes after the compiler rotates it — so recovering the ' + + 'original spelling is a real decision, not a formatting one.', + seeAlso: ['loop'], + }, + { + id: 'nested-loop', + label: 'Nested loop', + group: 'control-flow', + evidence: 'source', + summary: 'a loop lexically inside another loop', + seeAlso: ['loop'], + }, + { + id: 'break', + label: 'Break', + group: 'control-flow', + evidence: 'judgement', + summary: 'a `break` exits a loop or switch early', + seeAlso: ['loop', 'continue', 'switch'], + }, + { + id: 'continue', + label: 'Continue', + group: 'control-flow', + evidence: 'judgement', + summary: 'a `continue` skips to the next loop iteration', + seeAlso: ['loop', 'break'], + }, + { + id: 'jump-table', + label: 'Jump table', + group: 'control-flow', + evidence: 'codegen', + summary: 'a computed jump through a table (`mov pc`, `jr` on a non-link register, `bctr`)', + detail: + 'The compiler turned a switch into an indirect jump through an address table in the data ' + + 'section. Recovering it needs the table CONTENTS, not just the instruction — which is why ' + + "rows carrying this tag also carry the object's data dump.", + example: { + c: 'switch (a) { case 0: … case 1: … case 2: … }', + asm: '\tldr\tr3, .L4\n\tmov\tpc, r3\t@ computed jump into the table below', + toolchain: 'agbcc', + }, + seeAlso: ['switch', 'comparison-tree', 'dense', 'table'], + }, + { + id: 'comparison-tree', + label: 'Comparison tree', + group: 'control-flow', + evidence: 'codegen', + summary: 'a source switch became compare-and-branch rather than a jump table', + detail: + 'The same C, compiled for a different target or a sparser case set, produces a chain of ' + + 'compares. The pair (`jump-table`, `comparison-tree`) is exactly why codegen tags are ' + + 'derived per row: one `switch` in the dataset can be both, on different toolchains.', + seeAlso: ['switch', 'jump-table', 'sparse'], + }, + { + id: 'branchless', + label: 'Branchless', + group: 'control-flow', + evidence: 'codegen', + summary: 'a source conditional produced no conditional branch', + detail: + 'The conditional survives only as arithmetic — a set-on-less-than, a conditional move, a ' + + 'mask. Structuring has nothing to recover from the control-flow graph, so the recovery has ' + + 'to happen in the expression layer instead.', + example: { + c: 'return (a > 0) - (a < 0);', + asm: '\tslt\tv0,zero,a0\n\tslt\tv1,a0,zero\n\tsubu\tv0,v0,v1', + toolchain: 'ido7.1', + }, + seeAlso: ['branch', 'compare', 'ternary', 'bool'], + }, + + // ── arithmetic ────────────────────────────────────────────────────────────────────────────── + { + id: 'arithmetic', + label: 'Arithmetic', + group: 'arithmetic', + evidence: 'judgement', + summary: 'integer arithmetic is a POINT of the function, not merely an index computation', + detail: + 'Every function that indexes an array multiplies something. This tag is for functions where ' + + 'the arithmetic is the content, and the floor rejects bodies containing no arithmetic ' + + 'operator at all.', + seeAlso: ['shift', 'bitwise', 'strength-reduce'], + }, + { + id: 'shift', + label: 'Shift', + group: 'arithmetic', + evidence: 'source', + summary: '`<<` or `>>` appears in the body', + seeAlso: ['bitwise', 'rotate', 'mask', 'div-pow2'], + }, + { + id: 'bitwise', + label: 'Bitwise', + group: 'arithmetic', + evidence: 'source', + summary: '`&`, `|`, `^`, or `~` appears in the body (as an operator, not `&&`/`||`/address-of)', + detail: + 'The operator scan requires a LEFT operand, so `&x` (address-of) and the short-circuit ' + + '`&&`/`||` do not count — separating them is the difference between a bit-twiddling ' + + 'function and one that merely passes a pointer.', + seeAlso: ['mask', 'shift', 'rotate', 'bitfield'], + }, + { + id: 'mask', + label: 'Bit mask', + group: 'arithmetic', + evidence: 'judgement', + summary: 'a bit field is isolated or cleared with an AND mask', + seeAlso: ['bitwise', 'shift', 'bitfield'], + }, + { + id: 'rotate', + label: 'Rotate', + group: 'arithmetic', + evidence: 'judgement', + summary: 'a bit rotation, spelled in C as a shift pair', + detail: + 'C has no rotate operator, so the source says `(x << n) | (x >> (32 - n))`. ARM folds the ' + + 'whole thing into one barrel-shifted instruction, which means the recovered spelling has to ' + + 'be re-expanded to match.', + seeAlso: ['shift', 'bitwise'], + }, + { + id: 'abs', + label: 'Absolute value', + group: 'arithmetic', + evidence: 'judgement', + summary: 'absolute value is computed', + seeAlso: ['branchless', 'compare'], + }, + { + id: 'fixed-point', + label: 'Fixed point', + group: 'arithmetic', + evidence: 'judgement', + summary: 'Q-format integer math', + detail: + 'Integer arithmetic standing in for fractional values, with a shift as the implicit binary ' + + 'point. Dominant in GBA game code, where hardware floating point does not exist.', + seeAlso: ['shift', 'float', 'int64'], + }, + { + id: 'div-const', + label: 'Divide by a constant', + group: 'arithmetic', + evidence: 'judgement', + summary: 'division by a compile-time constant that is not a power of two', + detail: + "The probe's INTENT. What it becomes is per-toolchain and derived separately: a magic " + + 'multiply, a hardware divide, or a call to a soft-division helper. Signedness is carried by ' + + 'the `signed`/`unsigned` tags on the same row rather than baked into this id.', + example: { c: 'return a / 7;' }, + seeAlso: ['magic-div', 'soft-div', 'hw-div', 'div-pow2', 'signed', 'unsigned'], + }, + { + id: 'div-pow2', + label: 'Divide by a power of two', + group: 'arithmetic', + evidence: 'judgement', + summary: 'division by a constant power of two', + detail: + 'Never a division in the compiled code. Signed division rounds toward zero, so the compiler ' + + 'emits a bias-then-shift sequence that does not look like a shift at all — recovering `/ 2` ' + + 'rather than the literal add/shift pair is the whole test.', + example: { + c: 'return a / 2;', + asm: '\tmov\tr3, r0, lsr #31\n\tadd\tr0, r0, r3\n\tmov\tr0, r0, asr #1', + toolchain: 'agbcc', + }, + seeAlso: ['div-const', 'shift', 'strength-reduce', 'signed'], + }, + { + id: 'div-reg', + label: 'Divide by a variable', + group: 'arithmetic', + evidence: 'judgement', + summary: 'division by a runtime value', + detail: + 'The only divisor kind that cannot be strength-reduced, so it always reaches a real divide or a helper call.', + seeAlso: ['soft-div', 'hw-div', 'div-const'], + }, + { + id: 'mod-const', + label: 'Modulo by a constant', + group: 'arithmetic', + evidence: 'judgement', + summary: 'remainder by a compile-time constant that is not a power of two', + seeAlso: ['div-const', 'magic-div', 'soft-div'], + }, + { + id: 'mod-pow2', + label: 'Modulo by a power of two', + group: 'arithmetic', + evidence: 'judgement', + summary: 'remainder by a constant power of two', + detail: + 'Signed remainder is not a plain AND — the sign has to be restored — which is why this is a ' + + 'separate probe from the unsigned case rather than a mask.', + seeAlso: ['mod-const', 'mask', 'signed'], + }, + { + id: 'mod-reg', + label: 'Modulo by a variable', + group: 'arithmetic', + evidence: 'judgement', + summary: 'remainder by a runtime value', + seeAlso: ['div-reg', 'soft-div', 'hw-div'], + }, + { + id: 'soft-div', + label: 'Soft division', + group: 'arithmetic', + evidence: 'codegen', + summary: 'the compiled code calls __divsi3/__udivsi3/__modsi3/__umodsi3', + detail: + 'ARMv4 has no divide instruction, so agbcc calls a helper. Recovering `a / b` from a call ' + + 'means recognizing the helper by name and folding the call back into an operator — get it ' + + 'wrong and the output contains a call to a function the source never mentioned. Neither a ' + + 'hardware `div` nor the GBA BIOS division syscall counts.', + example: { c: 'return a / b;', asm: '\tbl\t__divsi3', toolchain: 'agbcc' }, + seeAlso: ['hw-div', 'magic-div', 'div-reg', 'call'], + }, + { + id: 'hw-div', + label: 'Hardware division', + group: 'arithmetic', + evidence: 'codegen', + summary: 'uses a hardware divide instruction (MIPS `div`/`divu`, PPC `divw`)', + example: { c: 'return a / 10;', asm: '\tli\tat,10\n\tdiv\tzero,a0,at\n\tmflo\tv0', toolchain: 'ido7.1' }, + seeAlso: ['soft-div', 'magic-div'], + }, + { + id: 'magic-div', + label: 'Magic-number division', + group: 'arithmetic', + evidence: 'codegen', + summary: 'a constant divide became a multiply-high by a magic reciprocal', + detail: + 'The compiler replaced `/ 10` with a multiply by a magic constant and a shift. Nothing in ' + + 'the compiled code resembles a division, and the constant is a function of the divisor — ' + + 'recovering it means inverting the reciprocal, not pattern-matching a call.', + example: { + c: 'return a / 10;', + asm: '\tlui\tv0,0x6666\n\tori\tv0,v0,0x6667\n\tmult\ta0,v0\n\tmfhi\tv1', + toolchain: 'gcc2.7.2kmc', + }, + seeAlso: ['div-const', 'mod-const', 'soft-div', 'hw-div'], + }, + { + id: 'strength-reduce', + label: 'Strength reduction', + group: 'arithmetic', + evidence: 'codegen', + summary: 'a constant multiply became shifts/adds rather than a multiply instruction', + example: { + c: 'return a * 10;', + asm: '\tmov\tr3, r0, lsl #2\n\tadd\tr3, r3, r0\n\tmov\tr0, r3, lsl #1', + toolchain: 'agbcc', + }, + seeAlso: ['arithmetic', 'shift', 'div-pow2'], + }, + + // ── data & types ──────────────────────────────────────────────────────────────────────────── + { + id: 'struct', + label: 'Struct', + group: 'data-types', + evidence: 'judgement', + summary: 'a struct type is used', + detail: + 'Struct layout is not present in the compiled code — only offsets are. Every struct tag is ' + + 'therefore a claim that the decompiler must INVENT a type whose field offsets happen to ' + + 'match, which is why struct-carrying rows dominate the nonmatch column.', + seeAlso: ['field', 'union', 'bitfield', 'array', 'pointer'], + }, + { + id: 'field', + label: 'Field access', + group: 'data-types', + evidence: 'judgement', + summary: 'a named struct/union member is accessed', + seeAlso: ['struct', 'union', 'bitfield', 'pointer'], + }, + { + id: 'union', + label: 'Union', + group: 'data-types', + evidence: 'judgement', + summary: 'a union type is accessed (including through a project typedef)', + detail: + 'Only measurable when the same bytes are reached through members of DIFFERENT width or ' + + 'domain — a union whose members are never aliased compiles identically to a struct, so the ' + + 'tag would mark a property the bytes cannot falsify.', + example: { c: 'u->w = v;\nreturn u->h[0] + u->h[1];' }, + seeAlso: ['struct', 'bitfield', 'mixed-width'], + }, + { + id: 'bitfield', + label: 'Bitfield', + group: 'data-types', + evidence: 'judgement', + summary: 'a declared C bitfield (`u32 x : 2`) is read or written', + detail: + 'A DECLARED bitfield, not a hand-rolled shift-and-mask. The compiled code is identical either ' + + 'way, so the tag records source intent that cannot be recovered from the bytes.', + seeAlso: ['mask', 'shift', 'struct'], + }, + { + id: 'array', + label: 'Array', + group: 'data-types', + evidence: 'judgement', + summary: 'an array is indexed', + seeAlso: ['table', 'variable-index', 'pointer'], + }, + { + id: 'variable-index', + label: 'Variable index', + group: 'data-types', + evidence: 'judgement', + summary: 'an array subscript is a runtime value, not a literal', + detail: + 'A constant subscript folds into the address at compile time and leaves nothing to recover. ' + + 'A variable one leaves a scaled add, and the scale is the only surviving evidence of the ' + + 'element type.', + seeAlso: ['array', 'table', 'struct'], + }, + { + id: 'table', + label: 'Lookup table', + group: 'data-types', + evidence: 'judgement', + summary: 'a constant lookup table is read with a computed index', + detail: + 'Both halves are required: the data must be CONSTANT (a `const` array or `.rodata`), and the ' + + 'index must be COMPUTED — `gEntityInfo[0x23].unkF` is a plain field access, not a table read. ' + + "The distinction matters because the table contents live in the object's data section, so the " + + 'decompiler must read the bytes and re-emit them as an initializer.', + example: { + c: 'return gSineTable[angle & 0xFF];', + asm: '\tldr\tr3, .L2\t@ &gSineTable\n\tand\tr0, r0, #255\n\tldrh\tr0, [r3, r0, lsl #1]', + toolchain: 'agbcc', + }, + seeAlso: ['array', 'variable-index', 'global', 'jump-table'], + }, + { + id: 'cast', + label: 'Cast', + group: 'data-types', + evidence: 'judgement', + summary: 'an explicit cast that changes the value or its width', + seeAlso: ['narrow', 'promotion', 'sign-extend', 'zero-extend'], + }, + { + id: 'narrow', + label: 'Narrowing', + group: 'data-types', + evidence: 'judgement', + summary: 'a value is truncated to a smaller width', + seeAlso: ['cast', 'mixed-width', 'promotion'], + }, + { + id: 'promotion', + label: 'Integer promotion', + group: 'data-types', + evidence: 'judgement', + summary: "C's implicit widening to `int` changes the result", + detail: + 'Invisible in the source and load-bearing in the output: the compiler inserts the widening, ' + + 'so a recovered expression that omits it computes something else at the same width.', + seeAlso: ['cast', 'narrow', 'sign-extend', 'zero-extend'], + }, + { + id: 'mixed-width', + label: 'Mixed widths', + group: 'data-types', + evidence: 'judgement', + summary: 'operands of different widths meet in one expression', + seeAlso: ['narrow', 'promotion', 'union'], + }, + { + id: 'sign-extend', + label: 'Sign extension', + group: 'data-types', + evidence: 'judgement', + summary: 'a signed narrow value is widened, preserving its sign', + seeAlso: ['zero-extend', 'signed', 'promotion'], + }, + { + id: 'zero-extend', + label: 'Zero extension', + group: 'data-types', + evidence: 'judgement', + summary: 'an unsigned narrow value is widened with zeroes', + seeAlso: ['sign-extend', 'unsigned', 'promotion'], + }, + { + id: 'signed', + label: 'Signed', + group: 'data-types', + evidence: 'judgement', + summary: 'signedness of the operands is load-bearing', + detail: + 'Paired with the divisor and shift tags rather than baked into their names: `div-const` + ' + + '`signed` is one row, `div-const` + `unsigned` another, and the two compile very ' + + 'differently. Keeping the axis separate is what makes that cross-product filterable.', + seeAlso: ['unsigned', 'sign-extend', 'div-const', 'mod-pow2'], + }, + { + id: 'unsigned', + label: 'Unsigned', + group: 'data-types', + evidence: 'judgement', + summary: 'unsignedness of the operands is load-bearing', + seeAlso: ['signed', 'zero-extend', 'div-const'], + }, + { + id: 'int64', + label: '64-bit integer', + group: 'data-types', + evidence: 'judgement', + summary: '64-bit integer arithmetic', + detail: + 'On every toolchain in the benchmark a 64-bit value occupies a register PAIR, so recovery ' + + 'has to fuse two registers into one variable and re-split them at every use.', + seeAlso: ['arithmetic', 'mixed-width'], + }, + { + id: 'float', + label: 'Floating point', + group: 'data-types', + evidence: 'judgement', + summary: 'floating-point types are used', + seeAlso: ['double', 'fixed-point', 'int-to-float', 'float-to-int'], + }, + { + id: 'double', + label: 'Double precision', + group: 'data-types', + evidence: 'judgement', + summary: 'double-precision floating point is used', + seeAlso: ['float'], + }, + { + id: 'int-to-float', + label: 'Int → float', + group: 'data-types', + evidence: 'judgement', + summary: 'an integer is converted to floating point', + seeAlso: ['float-to-int', 'float', 'cast'], + }, + { + id: 'float-to-int', + label: 'Float → int', + group: 'data-types', + evidence: 'judgement', + summary: 'a floating-point value is converted to an integer', + seeAlso: ['int-to-float', 'float', 'cast'], + }, + { + id: 'matrix', + label: 'Matrix math', + group: 'data-types', + evidence: 'judgement', + summary: 'matrix math', + seeAlso: ['array', 'nested-loop', 'fixed-point'], + }, + { + id: 'sizeof', + label: 'sizeof', + group: 'data-types', + evidence: 'source', + summary: 'the `sizeof` operator appears in the body', + detail: + 'Folded to a literal before any code is emitted, so the compiled form carries no trace of ' + + 'it — the recovered spelling can only ever be the number.', + seeAlso: ['struct', 'memory'], + }, + + // ── memory ────────────────────────────────────────────────────────────────────────────────── + { + id: 'memory', + label: 'Bulk memory', + group: 'memory', + evidence: 'judgement', + summary: 'bulk memory movement (copy/clear/compress), not any single load or store', + seeAlso: ['load', 'store', 'loop'], + }, + { + id: 'load', + label: 'Load', + group: 'memory', + evidence: 'judgement', + summary: 'a load is the point of the function', + seeAlso: ['store', 'memory', 'global'], + }, + { + id: 'store', + label: 'Store', + group: 'memory', + evidence: 'judgement', + summary: 'a store is the point of the function', + seeAlso: ['load', 'memory', 'global'], + }, + { + id: 'global', + label: 'Global', + group: 'memory', + evidence: 'judgement', + summary: 'a file-scope or extern variable is referenced', + detail: + 'Deliberately has NO machine-checked floor: several projects spell globals as address ' + + 'macros (`#define gStreamPtr (*(u8**)0x03004D84)`), which emit a raw `.word` rather than a ' + + 'symbol, so no scan over source or assembly can decide the tag.', + seeAlso: ['table', 'load', 'store', 'mmio'], + }, + { + id: 'pointer', + label: 'Pointer', + group: 'memory', + evidence: 'judgement', + summary: 'pointer arithmetic or dereference beyond plain member access', + seeAlso: ['array', 'struct', 'field'], + }, + { + id: 'mmio', + label: 'Memory-mapped I/O', + group: 'memory', + evidence: 'codegen', + summary: 'references a hardware I/O register (0x04000000–0x040003FF)', + detail: + 'The address range decides it, not the name. Palette RAM (0x05000000) and IWRAM ' + + '(0x03007FF8) are hardware addresses but not I/O registers, and both were tagged `mmio` ' + + 'before the range was checked.', + seeAlso: ['dma', 'global', 'store'], + }, + { + id: 'dma', + label: 'DMA', + group: 'memory', + evidence: 'codegen', + summary: 'programs the DMA registers (0x040000B0–0x040000DF)', + seeAlso: ['mmio', 'memory'], + }, + + // ── calls ─────────────────────────────────────────────────────────────────────────────────── + { + id: 'call', + label: 'Call', + group: 'calls', + evidence: 'codegen', + summary: 'the compiled code contains a call instruction', + detail: + "ANY call instruction — the callee's name is deliberately not consulted. These are unlinked " + + 'objects, so an external MIPS `jal` renders as `jal 0 `: the callee lives ' + + "in a relocation the harness's objdump flags do not emit, and filtering by name would " + + 'discard every real call on MIPS while keeping none.', + seeAlso: ['fnptr', 'multi-arg', 'soft-div'], + }, + { + id: 'fnptr', + label: 'Function pointer', + group: 'calls', + evidence: 'judgement', + summary: 'a call through a function pointer', + seeAlso: ['call', 'table', 'jump-table'], + }, + { + id: 'multi-arg', + label: 'Many arguments', + group: 'calls', + evidence: 'judgement', + summary: 'enough arguments that some are passed on the stack', + detail: + 'Past the register-argument limit the ABI spills to the stack, and recovering the call means ' + + 'reading those slots back as arguments rather than as locals.', + seeAlso: ['call'], + }, + { + id: 'method', + label: 'C++ method', + group: 'calls', + evidence: 'judgement', + summary: 'a C++ member function, with its implicit `this`', + detail: + 'Carries the implicit `this` pointer in the first argument register and a mangled symbol ' + + "name. The row's `language` field records that it is C++; this tag records that the " + + 'function is a MEMBER, which is what changes the calling convention.', + seeAlso: ['call', 'struct', 'pointer'], + }, + + // ── meta ──────────────────────────────────────────────────────────────────────────────────── + { + id: 'macro', + label: 'Macro-shaped', + group: 'meta', + evidence: 'judgement', + summary: 'a project macro is load-bearing for the shape', + detail: + 'The source spells something as a macro that expands to code no one would write by hand. ' + + 'Recovery cannot reproduce the macro, only its expansion — so these rows measure whether ' + + 'the expansion itself is recoverable.', + seeAlso: ['baseline'], + }, + { + id: 'baseline', + label: 'Baseline', + group: 'meta', + evidence: 'judgement', + summary: 'a trivial function carrying no other feature', + detail: + 'The control group. A decompiler that cannot match these has a problem unrelated to any ' + + 'feature, so they exist to make that visible rather than to be interesting.', + }, +]; + +export const FEATURE_BY_ID: ReadonlyMap = new Map(FEATURES.map((f) => [f.id, f])); + +/** Every id the vocabulary defines, including deprecated ones. */ +export const KNOWN_FEATURES: ReadonlySet = new Set(FEATURES.map((f) => f.id)); + +/** The ids of one evidence kind — the producer's detectors and validators are keyed off these. */ +export function featuresByEvidence(kind: EvidenceKind): FeatureDef[] { + return FEATURES.filter((f) => f.evidence === kind && !f.deprecated); +} + +export const GROUP_ORDER: readonly FeatureGroup[] = [ + 'control-flow', + 'arithmetic', + 'data-types', + 'memory', + 'calls', + 'meta', +]; diff --git a/packages/bench-schema/src/index.ts b/packages/bench-schema/src/index.ts index f9de9b2a..84259591 100644 --- a/packages/bench-schema/src/index.ts +++ b/packages/bench-schema/src/index.ts @@ -1,6 +1,8 @@ -// The benchmark result schema: ONE definition imported by both the producer (apps/benchmark) -// and the consumer (apps/web). Types only, zero deps, zero runtime code — browser-safe by -// construction. +// The benchmark contract: ONE definition imported by both the producer (apps/benchmark) and the +// consumer (apps/web) — the result/manifest types here, plus the closed feature vocabulary they +// range over in ./features. Zero dependencies, no I/O, browser-safe by construction. + +export * from './features'; export type ToolchainId = 'agbcc' | 'ido7.1' | 'gcc2.7.2kmc' | 'gcc2.7.2' | 'mwcc_242_81'; From c2909e6b3ffbd9f479ee0b553017b8878466458c Mon Sep 17 00:00:00 2001 From: Bruno Macabeus Date: Tue, 4 Aug 2026 22:59:52 +0100 Subject: [PATCH 2/4] feat(web): shared MultiSelect, HoverCard, Pill and overlay primitives MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The building blocks the feature picker and the definition drawer need, added first and on their own. All four live in src/shared/, which .dependency-cruiser.cjs forbids from importing src/pages/ — so none of them can learn anything about features, benchmarks or routes. Genericity is enforced by the build rather than promised in a comment. MultiSelect — a searchable, grouped multi-select popover. Takes options, returns a string[]; what those strings mean, and whether they AND or OR, is the consumer's. Its searching/grouping is a pure sibling module because apps/web has no component-testing setup and adding one for a single component is a worse trade: the logic that can be wrong sits where a test reaches it, and the rendering stays untested, which is the standard the rest of apps/web already holds. HoverCard — floating content on hover or focus, for anchors whose explanation needs markup or a link (the native `title` can hold neither, and appears on the browser's own ~1s delay). Two things it has to get right: - it is PORTALLED to and positioned from the anchor's viewport rect, because anchors sit in scroll containers — the benchmark's table wrapper is `overflow-x-auto`, and a container that is `auto` on one axis stops being `visible` on the other, so an absolutely positioned card would be clipped by the very row it belongs to. Placement is a pure hoverCardPosition(), unit tested for the flip threshold and the left-edge clamp. - it RE-MEASURES on scroll rather than dismissing. The listener has to be capturing (an anchor inside a scroll container moves when that container scrolls, and those events do not bubble to window), so it hears every scroll on the page, including the ones an `overflow-auto` code pane emits as its syntax highlighting resolves. Dismissing on those makes the card vanish about a second after opening. Dismiss-on-click runs in the capture phase, because anchors commonly stopPropagation (a chip inside a clickable table row must not also open the row) and a bubble handler would be skipped by exactly those anchors — with the unmount deferred to a later task so the click finishes propagating to the card's own buttons first. Pill — the small rounded label. Two families: TINTED, where a hex drives the text color and a 13%-alpha wash plus an optional dot, and NEUTRAL slate. The color stays the caller's, because what a color means is the caller's business. overlay — the two things stacked drawers need, since a chip inside one drawer opens another over it: a REF-COUNTED scroll lock (two drawers each restoring body.overflow on unmount unlocks the page behind the one still open) and Escape that closes only the TOPMOST (two window listeners both fire, so one keypress would dismiss the whole stack; the top one listens in the capture phase and stops the event). Which overlay is on top is tracked there, so neither drawer has to know the other exists. Co-Authored-By: Claude Opus 5 (1M context) --- apps/web/src/shared/components/HoverCard.tsx | 105 +++++++++ .../web/src/shared/components/MultiSelect.tsx | 210 ++++++++++++++++++ apps/web/src/shared/components/Pill.tsx | 42 ++++ .../shared/components/hover-card-position.ts | 33 +++ .../shared/components/multi-select-options.ts | 65 ++++++ apps/web/src/shared/utils/overlay.ts | 81 +++++++ apps/web/test/hover-card-position.test.ts | 43 ++++ apps/web/test/multi-select-options.test.ts | 66 ++++++ 8 files changed, 645 insertions(+) create mode 100644 apps/web/src/shared/components/HoverCard.tsx create mode 100644 apps/web/src/shared/components/MultiSelect.tsx create mode 100644 apps/web/src/shared/components/Pill.tsx create mode 100644 apps/web/src/shared/components/hover-card-position.ts create mode 100644 apps/web/src/shared/components/multi-select-options.ts create mode 100644 apps/web/src/shared/utils/overlay.ts create mode 100644 apps/web/test/hover-card-position.test.ts create mode 100644 apps/web/test/multi-select-options.test.ts diff --git a/apps/web/src/shared/components/HoverCard.tsx b/apps/web/src/shared/components/HoverCard.tsx new file mode 100644 index 00000000..162e4a65 --- /dev/null +++ b/apps/web/src/shared/components/HoverCard.tsx @@ -0,0 +1,105 @@ +// Floating content shown next to whatever it wraps, on hover or keyboard focus — for anchors whose +// explanation needs markup or a link, which the native `title` cannot hold. +// +// Portalled to and positioned from the anchor's viewport rect, because anchors sit in scroll +// containers: the benchmark's table wrapper is `overflow-x-auto`, and a container that is `auto` on +// one axis stops being `visible` on the other, so an absolutely positioned card would be clipped by +// the row it belongs to. +import { type ReactNode, useEffect, useRef, useState } from 'react'; +import { createPortal } from 'react-dom'; + +import { type CardPosition, hoverCardPosition } from './hover-card-position'; + +/** Grace period before closing, so the pointer can travel from the anchor onto the card. */ +const CLOSE_DELAY_MS = 120; +/** Below this much room underneath, the card opens upward instead. */ +const MIN_SPACE_BELOW = 180; + +export function HoverCard({ + content, + children, + width = 288, + className = '', +}: { + /** what the card shows — omit to render the anchor with no card at all */ + content?: ReactNode; + children: ReactNode; + width?: number; + /** classes for the inline wrapper around `children` */ + className?: string; +}) { + const anchor = useRef(null); + const [pos, setPos] = useState(null); + const isOpen = pos !== null; + const closing = useRef | undefined>(undefined); + + /** Recompute the card's position from the anchor's current rect. */ + const measure = () => { + const rect = anchor.current?.getBoundingClientRect(); + if (rect) { + setPos(hoverCardPosition(rect, { width: window.innerWidth, height: window.innerHeight }, width, MIN_SPACE_BELOW)); + } + }; + const open = () => { + clearTimeout(closing.current); + measure(); + }; + const close = () => { + clearTimeout(closing.current); + closing.current = setTimeout(() => setPos(null), CLOSE_DELAY_MS); + }; + // Deferred out of the current dispatch: this runs in the capture phase, so unmounting + // synchronously would remove the element whose own bubble-phase onClick has not run yet. + const dismiss = () => { + clearTimeout(closing.current); + setTimeout(() => setPos(null), 0); + }; + + useEffect(() => () => clearTimeout(closing.current), []); + + // Scrolling or resizing invalidates a viewport measurement, so RE-MEASURE rather than dismiss. + // The listener must capture (an anchor inside a scroll container moves when that container + // scrolls, and those events do not bubble to window), which means it hears every scroll on the + // page — including the ones an `overflow-auto` code pane emits as its highlighting resolves. + // Re-measuring makes those harmless; dismissing on them makes the card vanish unprompted. + useEffect(() => { + if (!isOpen) { + return; + } + window.addEventListener('scroll', measure, true); + window.addEventListener('resize', measure); + return () => { + window.removeEventListener('scroll', measure, true); + window.removeEventListener('resize', measure); + }; + }, [isOpen]); + + return ( + + {children} + {content && + pos && + createPortal( +
+ {content} +
, + document.body, + )} +
+ ); +} diff --git a/apps/web/src/shared/components/MultiSelect.tsx b/apps/web/src/shared/components/MultiSelect.tsx new file mode 100644 index 00000000..b1e0c1c6 --- /dev/null +++ b/apps/web/src/shared/components/MultiSelect.tsx @@ -0,0 +1,210 @@ +// A searchable, grouped, multi-select popover. Consumers hand it options and get back a string[]; +// what those strings mean, and whether they AND or OR, is theirs. +import { useEffect, useId, useMemo, useRef, useState } from 'react'; + +import { Pill } from './Pill'; +import { type MultiSelectOption, filterOptions, toggleValue } from './multi-select-options'; + +export type { MultiSelectOption } from './multi-select-options'; + +export function MultiSelect({ + label, + options, + value, + onChange, + placeholder = 'All', + searchPlaceholder = 'Search…', + emptyText = 'Nothing matches.', + onNavigate, +}: { + label: string; + options: readonly MultiSelectOption[]; + value: string[]; + onChange: (next: string[]) => void; + placeholder?: string; + searchPlaceholder?: string; + emptyText?: string; + /** Handle an option's `href` in-app instead of letting the browser reload. The component stays + * routing-agnostic: it knows an option has a destination, not how this app reaches one. */ + onNavigate?: (href: string) => void; +}) { + const [open, setOpen] = useState(false); + const listId = useId(); + const [query, setQuery] = useState(''); + const root = useRef(null); + const searchRef = useRef(null); + + const groups = useMemo(() => filterOptions(options, query, value), [options, query, value]); + const byValue = useMemo(() => new Map(options.map((o) => [o.value, o])), [options]); + + // close on click-outside and on Escape — both, or the popover strands itself over the table + useEffect(() => { + if (!open) { + return; + } + const onDown = (e: MouseEvent) => { + if (root.current && !root.current.contains(e.target as Node)) { + setOpen(false); + } + }; + const onKey = (e: KeyboardEvent) => { + if (e.key === 'Escape') { + setOpen(false); + } + }; + document.addEventListener('mousedown', onDown); + document.addEventListener('keydown', onKey); + return () => { + document.removeEventListener('mousedown', onDown); + document.removeEventListener('keydown', onKey); + }; + }, [open]); + + useEffect(() => { + if (open) { + searchRef.current?.focus(); + } else { + setQuery(''); + } + }, [open]); + + const toggle = (v: string) => onChange(toggleValue(value, v)); + + return ( +
+ {label} +
+ {/* A `
` rather than a ` + + ))} + +
+ + {open && ( +
+
+ setQuery(e.target.value)} + placeholder={searchPlaceholder} + className="w-full rounded-md border border-slate-700 bg-slate-800 px-2 py-1.5 text-sm text-slate-200 focus:border-teal-500 focus:outline-hidden" + /> +
+ + {value.length > 0 && ( +
+ +
+ )} +
+ )} +
+
+ ); +} diff --git a/apps/web/src/shared/components/Pill.tsx b/apps/web/src/shared/components/Pill.tsx new file mode 100644 index 00000000..f5ea8226 --- /dev/null +++ b/apps/web/src/shared/components/Pill.tsx @@ -0,0 +1,42 @@ +// The small rounded label: outcome badges, gap sizes, evidence kinds, feature tags, symbol names. +// +// TINTED — a `tint` hex drives the text color and a wash of it behind. The color stays the +// caller's, since what it MEANS belongs to the caller's scale, not here. NEUTRAL — no tint, slate +// on slate, for labels that identify rather than classify. +import type { ComponentPropsWithoutRef } from 'react'; + +export interface PillProps extends ComponentPropsWithoutRef<'span'> { + /** hex color giving the text its color and the background a wash of it; omit for neutral slate */ + tint?: string; + /** leading dot in the tint color — reads as a status marker rather than a label */ + dot?: boolean; + mono?: boolean; + /** `sm` for prose-sized badges, `xs` for the dense tags that sit inside table cells */ + size?: 'sm' | 'xs'; +} + +export function Pill({ + tint, + dot = false, + mono = false, + size = 'sm', + className = '', + style, + children, + ...rest +}: PillProps) { + const metrics = size === 'sm' ? 'gap-1.5 rounded-md px-2 py-0.5 text-xs' : 'gap-1 rounded px-1.5 py-0.5 text-[11px]'; + const palette = tint ? 'font-medium' : 'bg-slate-700/60 text-slate-300'; + return ( + + {dot && tint && } + {children} + + ); +} diff --git a/apps/web/src/shared/components/hover-card-position.ts b/apps/web/src/shared/components/hover-card-position.ts new file mode 100644 index 00000000..a529b4c4 --- /dev/null +++ b/apps/web/src/shared/components/hover-card-position.ts @@ -0,0 +1,33 @@ +// Where a puts its card. Pure, so it can be unit-tested without a DOM. + +export interface Viewport { + width: number; + height: number; +} + +/** A fixed-position box: `top` XOR `bottom` is set, depending on which way the card opened. */ +export interface CardPosition { + left: number; + top?: number; + bottom?: number; +} + +/** Distance kept from the viewport edges. */ +const MARGIN = 8; +/** Gap between the anchor and the card. */ +const OFFSET = 6; + +/** Place a card of `width` next to `anchor`, in viewport (fixed-position) coordinates: below when + * at least `minSpaceBelow` pixels remain there, flipped above otherwise, with the left edge + * clamped into the viewport so a chip in the last column stays readable. */ +export function hoverCardPosition( + anchor: { top: number; bottom: number; left: number }, + viewport: Viewport, + width: number, + minSpaceBelow: number, +): CardPosition { + const below = viewport.height - anchor.bottom > minSpaceBelow; + // `Math.max` last, so a viewport narrower than the card yields the left margin, not a negative + const left = Math.max(MARGIN, Math.min(anchor.left, viewport.width - width - MARGIN)); + return below ? { left, top: anchor.bottom + OFFSET } : { left, bottom: viewport.height - anchor.top + OFFSET }; +} diff --git a/apps/web/src/shared/components/multi-select-options.ts b/apps/web/src/shared/components/multi-select-options.ts new file mode 100644 index 00000000..d6a793eb --- /dev/null +++ b/apps/web/src/shared/components/multi-select-options.ts @@ -0,0 +1,65 @@ +// The searching and grouping behind . Pure, so it can be unit-tested without a DOM. + +export interface MultiSelectOption { + value: string; + label: string; + /** renders a section header; ungrouped options are listed first, under no header */ + group?: string; + /** shown under the label, and matched by the search box */ + description?: string; + /** right-aligned figure — how many things this option would select */ + count?: number; + /** greyed and unselectable, but still VISIBLE: a zero-count option is information */ + disabled?: boolean; + /** renders a "read more" affordance on the row */ + href?: string; +} + +export interface OptionGroup { + /** undefined for the ungrouped bucket */ + group?: string; + options: MultiSelectOption[]; +} + +/** Case-insensitive match over value, label and description. Multi-word queries are AND-ed, so + * "div const" finds "Divide by a constant" regardless of the word order it was typed in. */ +export function matchesQuery(option: MultiSelectOption, query: string): boolean { + const terms = query.toLowerCase().split(/\s+/).filter(Boolean); + if (terms.length === 0) { + return true; + } + const haystack = `${option.value} ${option.label} ${option.description ?? ''}`.toLowerCase(); + return terms.every((t) => haystack.includes(t)); +} + +/** Filter by `query` and bucket into groups, preserving the caller's option order within each + * group and the order in which groups first appear. Selected values are always kept, even when + * they do not match the query — a filter you cannot see is a filter you cannot remove. + * + * Empty groups are dropped, so a search that eliminates a whole section does not leave its + * header behind. */ +export function filterOptions( + options: readonly MultiSelectOption[], + query: string, + selected: readonly string[] = [], +): OptionGroup[] { + const keep = new Set(selected); + const visible = options.filter((o) => keep.has(o.value) || matchesQuery(o, query)); + const buckets: OptionGroup[] = []; + const byGroup = new Map(); + for (const o of visible) { + let bucket = byGroup.get(o.group); + if (!bucket) { + byGroup.set(o.group, (bucket = { group: o.group, options: [] })); + buckets.push(bucket); + } + bucket.options.push(o); + } + // the ungrouped bucket first, then groups in first-seen order + return buckets.sort((a, b) => (a.group === undefined ? -1 : b.group === undefined ? 1 : 0)); +} + +/** Toggle one value in a selection, preserving order of first selection. */ +export function toggleValue(selected: readonly string[], value: string): string[] { + return selected.includes(value) ? selected.filter((v) => v !== value) : [...selected, value]; +} diff --git a/apps/web/src/shared/utils/overlay.ts b/apps/web/src/shared/utils/overlay.ts new file mode 100644 index 00000000..ce6e7dcc --- /dev/null +++ b/apps/web/src/shared/utils/overlay.ts @@ -0,0 +1,81 @@ +// Behaviour for STACKED overlays: a tag chip inside the row drawer opens the definition drawer over +// it, and the naive per-drawer version breaks as soon as two are open. +// +// - the scroll lock is REF-COUNTED, or closing the top drawer unlocks the page behind the one +// still open. +// - Escape closes only the TOPMOST. Two `window` listeners both fire, so one keypress would +// dismiss the whole stack; the top overlay listens in the capture phase and stops the event +// before the ones underneath see it. +// +// Which overlay is on top is tracked here, so neither drawer has to know the other exists. +import { useEffect, useRef, useSyncExternalStore } from 'react'; + +// ── the layer stack ───────────────────────────────────────────────────────────────────────────── + +let stack: number[] = []; +let nextId = 1; +const subscribers = new Set<() => void>(); + +const emit = () => subscribers.forEach((f) => f()); +const subscribe = (f: () => void) => { + subscribers.add(f); + return () => void subscribers.delete(f); +}; +const topOfStack = () => stack[stack.length - 1]; + +/** True while this overlay is the newest one mounted. */ +function useTopmost(): boolean { + const id = useRef(0); + if (id.current === 0) { + id.current = nextId++; + } + const self = id.current; + useEffect(() => { + stack = [...stack, self]; + emit(); + return () => { + stack = stack.filter((x) => x !== self); + emit(); + }; + }, [self]); + return useSyncExternalStore(subscribe, topOfStack) === self; +} + +// ── the pieces ────────────────────────────────────────────────────────────────────────────────── + +let locks = 0; + +/** Lock body scroll while mounted; the page unlocks only when the LAST holder unmounts. */ +function useScrollLock(): void { + useEffect(() => { + if (locks++ === 0) { + document.body.style.overflow = 'hidden'; + } + return () => { + if (--locks === 0) { + document.body.style.overflow = ''; + } + }; + }, []); +} + +function useEscapeToClose(onClose: () => void, topmost: boolean): void { + useEffect(() => { + const onKey = (e: KeyboardEvent) => { + if (e.key === 'Escape' && topmost) { + e.stopPropagation(); // exactly one overlay closes per keypress + onClose(); + } + }; + window.addEventListener('keydown', onKey, { capture: topmost }); + return () => window.removeEventListener('keydown', onKey, { capture: topmost }); + }, [onClose, topmost]); +} + +/** Everything a modal overlay needs: a ref-counted scroll lock, and Escape that closes only this + * overlay when it is the one on top. */ +export function useOverlay(onClose: () => void): void { + const topmost = useTopmost(); + useScrollLock(); + useEscapeToClose(onClose, topmost); +} diff --git a/apps/web/test/hover-card-position.test.ts b/apps/web/test/hover-card-position.test.ts new file mode 100644 index 00000000..4585712e --- /dev/null +++ b/apps/web/test/hover-card-position.test.ts @@ -0,0 +1,43 @@ +// Where puts its card — the part of a floating element a reader notices when it is +// wrong. +import { describe, expect, it } from 'vitest'; + +import { hoverCardPosition } from '../src/shared/components/hover-card-position'; + +const VIEWPORT = { width: 1000, height: 800 }; +const WIDTH = 288; +const MIN_BELOW = 180; + +const at = (left: number, top: number, height = 20) => + hoverCardPosition({ left, top, bottom: top + height }, VIEWPORT, WIDTH, MIN_BELOW); + +describe('hoverCardPosition', () => { + it('opens below the anchor when there is room', () => { + const pos = at(100, 200); + expect(pos.top).toBe(226); // anchor bottom + 6 + expect(pos.bottom).toBeUndefined(); + expect(pos.left).toBe(100); + }); + + it('flips above when the card would run off the bottom', () => { + // an anchor in the last table row: 800 - 770 = 30px below, far under the 180 it needs + const pos = at(100, 750); + expect(pos.top).toBeUndefined(); + expect(pos.bottom).toBe(56); // viewport height - anchor top + 6 + }); + + it('flips exactly at the threshold, not one pixel early', () => { + expect(at(100, 800 - MIN_BELOW - 20 - 1).top).toBeDefined(); // 181px below — fits + expect(at(100, 800 - MIN_BELOW - 20).top).toBeUndefined(); // 180px below — flips + }); + + it('clamps the left edge so a chip in the last column stays fully visible', () => { + // aligning with this anchor would put the right edge at 1248, off a 1000px viewport + expect(at(960, 200).left).toBe(1000 - WIDTH - 8); + }); + + it('never leaves the left margin, even on a viewport narrower than the card', () => { + const pos = hoverCardPosition({ left: 4, top: 10, bottom: 30 }, { width: 200, height: 800 }, WIDTH, MIN_BELOW); + expect(pos.left).toBe(8); + }); +}); diff --git a/apps/web/test/multi-select-options.test.ts b/apps/web/test/multi-select-options.test.ts new file mode 100644 index 00000000..440616ee --- /dev/null +++ b/apps/web/test/multi-select-options.test.ts @@ -0,0 +1,66 @@ +// The searching and grouping behind . Pure, so it can be tested without a DOM. +import { describe, expect, it } from 'vitest'; + +import { + type MultiSelectOption, + filterOptions, + matchesQuery, + toggleValue, +} from '../src/shared/components/multi-select-options'; + +const OPTIONS: MultiSelectOption[] = [ + { value: 'loop', label: 'Loop', group: 'Control flow', description: 'a for, while or do-while loop' }, + { value: 'switch', label: 'Switch', group: 'Control flow', description: 'a switch statement' }, + { value: 'div-const', label: 'Divide by a constant', group: 'Arithmetic', description: 'division by a constant' }, + { value: 'ungrouped', label: 'No group here' }, +]; + +describe('matchesQuery', () => { + it('matches on the id, the label and the description alike', () => { + expect(matchesQuery(OPTIONS[0], 'loop')).toBe(true); // id + expect(matchesQuery(OPTIONS[2], 'divide')).toBe(true); // label + expect(matchesQuery(OPTIONS[1], 'statement')).toBe(true); // description + expect(matchesQuery(OPTIONS[1], 'nonsense')).toBe(false); + }); + + it('AND-s the words, so word order does not matter', () => { + expect(matchesQuery(OPTIONS[2], 'constant divide')).toBe(true); + expect(matchesQuery(OPTIONS[2], 'divide loop')).toBe(false); + }); + + it('is case-insensitive and treats an empty query as "everything"', () => { + expect(matchesQuery(OPTIONS[0], 'LOOP')).toBe(true); + expect(matchesQuery(OPTIONS[0], ' ')).toBe(true); + }); +}); + +describe('filterOptions', () => { + it('buckets by group, ungrouped first, preserving caller order within a group', () => { + const groups = filterOptions(OPTIONS, ''); + expect(groups.map((g) => g.group)).toEqual([undefined, 'Control flow', 'Arithmetic']); + expect(groups[1].options.map((o) => o.value)).toEqual(['loop', 'switch']); + }); + + it('drops a group whose every option was filtered out, header included', () => { + const groups = filterOptions(OPTIONS, 'switch'); + expect(groups.map((g) => g.group)).toEqual(['Control flow']); + expect(groups[0].options.map((o) => o.value)).toEqual(['switch']); + }); + + it('KEEPS a selected option that does not match the query', () => { + // otherwise typing a search hides the filter you already applied, and it cannot be removed + const groups = filterOptions(OPTIONS, 'switch', ['div-const']); + expect(groups.flatMap((g) => g.options.map((o) => o.value)).sort()).toEqual(['div-const', 'switch']); + }); + + it('returns no groups at all when nothing matches', () => { + expect(filterOptions(OPTIONS, 'zzz')).toEqual([]); + }); +}); + +describe('toggleValue', () => { + it('adds at the end and removes in place, preserving selection order', () => { + expect(toggleValue(['a'], 'b')).toEqual(['a', 'b']); + expect(toggleValue(['a', 'b', 'c'], 'b')).toEqual(['a', 'c']); + }); +}); From 50aa12e80bee71fe6f7f69b9b5f6e84bdef087bc Mon Sep 17 00:00:00 2001 From: Bruno Macabeus Date: Tue, 4 Aug 2026 22:59:53 +0100 Subject: [PATCH 3/4] feat(web): replace the feature selector with a picker and a definition drawer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The two complaints about the Function Explorer's Feature control: 89 options long, and nothing says what any option means. The s: short, mutually exclusive, no complaint behind them. Definitions open in the same right-side drawer the row detail uses, rather than a separate page: "what does `table` mean?" is asked while reading the table, and answering it by navigating away would discard the filters and scroll position that prompted the question. Same chrome, same Escape, same URL-state shape (`?about=` beside `?fn=`), so a definition is a shareable link — and featureHref() builds from the live query string, so opening one preserves the filters already set. It renders above every tab rather than inside one, because a tag can be asked about from the table, the picker or a chart and the answer should not depend on which. Chips inside the row drawer open it too, stacking over that drawer, which is what the ref-counted scroll lock and topmost-wins Escape in shared/utils/overlay are for. Tag chips are hoverable for a one-line definition plus a "read more" link, and clickable for the full one. The evidence badge in the drawer header carries its own explanation on hover, rather than a paragraph in the body competing with the definition it annotates. `feature` and `decline` become comma-separated lists in the URL; a single-value link written before this still parses, pinned by a test. The benchmark's badges are now Pill plus the domain rule that picks their color, so OutcomeBadge reads as a color lookup and GapBadge as a bucket lookup. feature-vocabulary.test.ts is the fifth gate, and the one that matters for the site: it re-checks the closed vocabulary against the COMMITTED results.json, so a snapshot regenerated by a checkout whose vocabulary had drifted cannot ship a tag with no definition for a chip to open. Co-Authored-By: Claude Opus 5 (1M context) --- apps/web/src/pages/benchmark/Benchmark.tsx | 32 ++- .../pages/benchmark/components/Explorer.tsx | 46 +++-- .../benchmark/components/FeatureDetail.tsx | 184 ++++++++++++++++++ .../benchmark/components/FeaturePicker.tsx | 90 +++++++++ .../benchmark/components/FunctionDetail.tsx | 24 +-- .../benchmark/components/GapAnalysis.tsx | 2 +- .../pages/benchmark/components/Overview.tsx | 8 +- .../pages/benchmark/components/ui/Badge.tsx | 89 +++++++-- .../src/pages/benchmark/lib/explorer-url.ts | 20 +- apps/web/src/pages/benchmark/lib/stats.ts | 11 ++ apps/web/test/explorer-url.test.ts | 56 ++++++ apps/web/test/feature-vocabulary.test.ts | 48 +++++ 12 files changed, 547 insertions(+), 63 deletions(-) create mode 100644 apps/web/src/pages/benchmark/components/FeatureDetail.tsx create mode 100644 apps/web/src/pages/benchmark/components/FeaturePicker.tsx create mode 100644 apps/web/test/explorer-url.test.ts create mode 100644 apps/web/test/feature-vocabulary.test.ts diff --git a/apps/web/src/pages/benchmark/Benchmark.tsx b/apps/web/src/pages/benchmark/Benchmark.tsx index 5d173fc3..a5570b6b 100644 --- a/apps/web/src/pages/benchmark/Benchmark.tsx +++ b/apps/web/src/pages/benchmark/Benchmark.tsx @@ -11,11 +11,20 @@ import summary from '../../data/summary.json'; import type { ShareState } from '../../shared/utils/permalink'; import { Disclaimer } from './components/Disclaimer'; import { Explorer, type ExplorerPreset } from './components/Explorer'; +import { FeatureDetail } from './components/FeatureDetail'; import { GapAnalysis } from './components/GapAnalysis'; import { Methodology } from './components/Methodology'; import { Overview } from './components/Overview'; import { meta, results } from './lib/data'; -import { FILTERS_RESET, FILTER_PARSERS, FILTER_URL_KEYS, type TabId, tabParser } from './lib/explorer-url'; +import { + FEATURE_TERM_KEY, + FEATURE_TERM_PARSER, + FILTERS_RESET, + FILTER_PARSERS, + FILTER_URL_KEYS, + type TabId, + tabParser, +} from './lib/explorer-url'; import { DECOMPILER_COLOR } from './theme'; const TABS: { id: TabId; label: string }[] = [ @@ -30,6 +39,7 @@ export default function Benchmark({ onOpenInPlayground }: { onOpenInPlayground: const [tab, setTab] = useQueryState('tab', tabParser.withOptions({ history: 'push' })); const [, setFilters] = useQueryStates(FILTER_PARSERS, { urlKeys: FILTER_URL_KEYS }); const [, setSelectedId] = useQueryState('fn', parseAsString); + const [aboutFeature, setAboutFeature] = useQueryState(FEATURE_TERM_KEY, FEATURE_TERM_PARSER); // An aggregate's preset replaces the WHOLE filter set and closes any open detail; all three // writes land as one pushed history entry, so Back returns to the aggregate that was clicked. const openExplorer = useCallback( @@ -41,6 +51,10 @@ export default function Benchmark({ onOpenInPlayground }: { onOpenInPlayground: [setFilters, setSelectedId, setTab], ); + // Pushed, so Back closes the drawer and returns to the filter that was open — as the row drawer + // does. + const openFeature = useCallback((id: string) => void setAboutFeature(id, { history: 'push' }), [setAboutFeature]); + return (
@@ -92,10 +106,24 @@ export default function Benchmark({ onOpenInPlayground }: { onOpenInPlayground:
{tab === 'overview' && } - {tab === 'explorer' && } + {tab === 'explorer' && ( + + )} {tab === 'gap' && } {tab === 'methodology' && }
+ + {/* Above every tab, not inside one: a tag can be asked about from the table, the picker or a + chart, and the answer should not depend on which. */} + {aboutFeature && ( + void setAboutFeature(null, { history: 'replace' })} + onOpenFeature={openFeature} + onExplore={openExplorer} + /> + )}
); } diff --git a/apps/web/src/pages/benchmark/components/Explorer.tsx b/apps/web/src/pages/benchmark/components/Explorer.tsx index 7393fdec..e3024102 100644 --- a/apps/web/src/pages/benchmark/components/Explorer.tsx +++ b/apps/web/src/pages/benchmark/components/Explorer.tsx @@ -3,13 +3,14 @@ import { parseAsString, useQueryState, useQueryStates } from 'nuqs'; import { useMemo } from 'react'; import type { ShareState } from '../../../shared/utils/permalink'; -import { DECLINE_CLASSES, OTHER_CLASS, declineClassesOf } from '../lib/declines'; +import { declineClassesOf } from '../lib/declines'; import { FILTER_PARSERS, FILTER_URL_KEYS, SORT_PARSERS, type SortKey, type Verdict } from '../lib/explorer-url'; import { canOpenInPlayground, playgroundShare } from '../lib/playground'; -import { distinct, distinctFeatures } from '../lib/stats'; +import { distinct, tally } from '../lib/stats'; import { DECOMPILER_COLOR, ISA_LABEL, OUTCOME_LABEL, OUTCOME_ORDER, TOOLCHAIN_LABEL } from '../theme'; +import { DeclinePicker, FeaturePicker } from './FeaturePicker'; import { FunctionDetail } from './FunctionDetail'; -import { Chip, GapBadge, OutcomeBadge } from './ui/Badge'; +import { FeatureChip, GapBadge, OutcomeBadge } from './ui/Badge'; // Re-exported so the aggregates (Overview, Gap Analysis) keep their preset-type import here. export type { ExplorerPreset, Filters } from '../lib/explorer-url'; @@ -64,10 +65,13 @@ function ScoreCell({ result }: { result: FunctionResult['asmlift'] }) { export function Explorer({ rows, onOpenInPlayground, + onOpenFeature, }: { rows: FunctionResult[]; /** hand a row's input to the playground view (the shell switches views + seeds the editor) */ onOpenInPlayground: (s: ShareState) => void; + /** open a feature's definition drawer (the "read more →" in the picker, or a tag chip) */ + onOpenFeature: (id: string) => void; }) { // All URL state: filters replace history (no spam while narrowing), the selected row pushes // (Back closes the detail). Benchmark.tsx writes the same keys for the preset deep links. @@ -79,7 +83,6 @@ export function Explorer({ const projects = useMemo(() => distinct(rows, (r) => r.project), [rows]); const isas = useMemo(() => distinct(rows, (r) => r.isa), [rows]); const toolchains = useMemo(() => distinct(rows, (r) => r.toolchain), [rows]); - const features = useMemo(() => distinctFeatures(rows), [rows]); const set = (patch: Partial) => void setFilters(patch); @@ -98,11 +101,15 @@ export function Explorer({ if (filters.tier && r.tier !== filters.tier) { return false; } - if (filters.feature && !r.features.includes(filters.feature)) { + // AND: the row must carry EVERY selected tag / exhibit EVERY selected decline class + if (!filters.feature.every((f) => r.features.includes(f))) { return false; } - if (filters.decline && !declineClassesOf(r).includes(filters.decline)) { - return false; + if (filters.decline.length) { + const classes = declineClassesOf(r); + if (!filters.decline.every((d) => classes.includes(d))) { + return false; + } } if (filters.symbols === 'with' && !r.asmlift.symbolMap) { return false; @@ -162,6 +169,11 @@ export function Explorer({ return out; }, [rows, filters, sortKey, sortDir]); + // Over the ALREADY-FILTERED rows, so an unselected option reads as "how many rows survive if I + // AND this in too" — a selection that would empty the table says so before it is clicked. + const featureCounts = useMemo(() => tally(filtered, (r) => r.features), [filtered]); + const declineCounts = useMemo(() => tally(filtered, declineClassesOf), [filtered]); + const toggleSort = (key: SortKey) => { if (sortKey === key) { void setSort({ dir: sortDir === 'asc' ? 'desc' : 'asc' }); @@ -248,21 +260,12 @@ export function Explorer({ { value: 'disagree', label: 'outcomes differ' }, ]} /> - set({ decline: v })} counts={declineCounts} /> + set({ feature: v })} - options={[{ value: '', label: 'All' }, ...features.map((f) => ({ value: f, label: f }))]} + counts={featureCounts} + onOpenFeature={onOpenFeature} /> +
+ + ); + } + + const color = EVIDENCE_COLOR[def.evidence]; + + return ( +
+
+
+
+
+
+

{def.label}

+ + {def.id} + +
+
+ {FEATURE_GROUP_LABEL[def.group]} + · + {EVIDENCE_HELP[def.evidence]}

} + > + + {EVIDENCE_LABEL[def.evidence]} + +
+
+
+ +
+ +
+ {/* Summaries are written as lowercase fragments, to read as a clause in the picker and + the hover card; here one opens a paragraph. */} +

{def.summary}

+ {def.detail &&

{def.detail}

} + + {def.example && ( +
+
+
C
+ +
+ {def.example.asm && ( +
+
+ compiled{def.example.toolchain ? ` · ${def.example.toolchain}` : ''} +
+ +
+ )} +
+ )} + +
+
In this benchmark
+
+ + {carrying.length} rows + + {carrying.length > 0 && ( + + match rate{' '} + + {(matchRate(carrying, 'asmlift') * 100).toFixed(0)}% + + {' / '} + + {(matchRate(carrying, 'm2c') * 100).toFixed(0)}% + + (asmlift / m2c) + + )} +
+ +
+ + {def.seeAlso && def.seeAlso.length > 0 && ( +
+
See also
+
+ {def.seeAlso + .filter((s) => FEATURE_BY_ID.has(s)) + .map((s) => ( + + ))} +
+
+ )} +
+
+
+ ); +} diff --git a/apps/web/src/pages/benchmark/components/FeaturePicker.tsx b/apps/web/src/pages/benchmark/components/FeaturePicker.tsx new file mode 100644 index 00000000..d3e2a2c8 --- /dev/null +++ b/apps/web/src/pages/benchmark/components/FeaturePicker.tsx @@ -0,0 +1,90 @@ +// The two domain adapters over the generic : each turns a closed vocabulary into +// options. What a tag means lives in @asmlift/bench-schema, what a decline class means in +// lib/declines — neither belongs inside a shared UI component. +import { FEATURES, FEATURE_GROUP_LABEL, GROUP_ORDER } from '@asmlift/bench-schema'; +import { useMemo } from 'react'; + +import { MultiSelect, type MultiSelectOption } from '../../../shared/components/MultiSelect'; +import { DECLINE_CLASSES, OTHER_CLASS } from '../lib/declines'; +import { FEATURE_TERM_KEY, featureHref } from '../lib/explorer-url'; + +/** Feature tags, grouped by the vocabulary's user-facing axis. + * + * `counts` is over the CURRENTLY FILTERED rows, not the whole dataset: the selection is AND-ed, so + * an option that would empty the table has to say so before it is clicked. Zero-count options go + * disabled but stay visible — that a tag exists and selects nothing here is worth seeing. */ +export function FeaturePicker({ + value, + onChange, + counts, + onOpenFeature, +}: { + value: string[]; + onChange: (next: string[]) => void; + counts: Map; + onOpenFeature: (id: string) => void; +}) { + const options = useMemo(() => { + const rank = new Map(GROUP_ORDER.map((g, i) => [g, i])); + return [...FEATURES] + .filter((f) => !f.deprecated) + .sort((a, b) => rank.get(a.group)! - rank.get(b.group)! || a.label.localeCompare(b.label)) + .map((f) => ({ + value: f.id, + label: f.label, + group: FEATURE_GROUP_LABEL[f.group], + description: f.summary, + count: counts.get(f.id) ?? 0, + disabled: (counts.get(f.id) ?? 0) === 0, + href: featureHref(f.id), + })); + }, [counts]); + + return ( + onOpenFeature(new URLSearchParams(href.slice(1)).get(FEATURE_TERM_KEY) ?? '')} + /> + ); +} + +/** asmlift's decline classes: the same closed-vocabulary shape, without a group axis or + * definitions to link to. */ +export function DeclinePicker({ + value, + onChange, + counts, +}: { + value: string[]; + onChange: (next: string[]) => void; + counts: Map; +}) { + const options = useMemo( + () => + [...DECLINE_CLASSES, OTHER_CLASS].map((c) => ({ + value: c.key, + label: c.label, + count: counts.get(c.key) ?? 0, + disabled: (counts.get(c.key) ?? 0) === 0, + })), + [counts], + ); + + return ( + + ); +} diff --git a/apps/web/src/pages/benchmark/components/FunctionDetail.tsx b/apps/web/src/pages/benchmark/components/FunctionDetail.tsx index abe28593..88f350af 100644 --- a/apps/web/src/pages/benchmark/components/FunctionDetail.tsx +++ b/apps/web/src/pages/benchmark/components/FunctionDetail.tsx @@ -1,12 +1,13 @@ import type { DecompilerId, DecompilerResult, FunctionResult } from '@asmlift/bench-schema'; -import { useEffect, useMemo, useState } from 'react'; +import { useMemo, useState } from 'react'; import { CodeBlock, type CodeLanguage } from '../../../shared/components/CodeBlock'; +import { useOverlay } from '../../../shared/utils/overlay'; import type { ShareState } from '../../../shared/utils/permalink'; import { formatC } from '../lib/format-c'; import { playgroundShare } from '../lib/playground'; import { DECOMPILER_COLOR, TOOLCHAIN_LABEL } from '../theme'; -import { Chip, GapBadge, OutcomeBadge } from './ui/Badge'; +import { Chip, FeatureChip, GapBadge, OutcomeBadge } from './ui/Badge'; // The Benchmark's code-block chrome (the shared CodeBlock only fixes scroll/whitespace/mono). const CODE_PRE = 'max-h-[46vh] rounded-md bg-slate-950/70 p-3 text-[12px] leading-relaxed text-slate-200'; @@ -366,10 +367,13 @@ export function FunctionDetail({ fn, onClose, onOpenInPlayground, + onOpenFeature, }: { fn: FunctionResult; onClose: () => void; onOpenInPlayground: (s: ShareState) => void; + /** open a tag's definition — stacks the feature drawer over this one (see shared/utils/overlay) */ + onOpenFeature: (id: string) => void; }) { const share = useMemo(() => playgroundShare(fn), [fn]); const [m2cPath, setM2cPath] = usePersistedPath(PATH_STORAGE_KEYS.m2c); @@ -378,19 +382,7 @@ export function FunctionDetail({ const [projectPath, setProjectPath] = usePersistedPath(`${PATH_STORAGE_KEYS.project}:${fn.project}`); const projectDefault = fn.scripts ? projectPlaceholder(fn.scripts.asmlift) : null; - useEffect(() => { - const onKey = (e: KeyboardEvent) => { - if (e.key === 'Escape') { - onClose(); - } - }; - window.addEventListener('keydown', onKey); - document.body.style.overflow = 'hidden'; - return () => { - window.removeEventListener('keydown', onKey); - document.body.style.overflow = ''; - }; - }, [onClose]); + useOverlay(onClose); return (
@@ -412,7 +404,7 @@ export function FunctionDetail({
{fn.features.map((f) => ( - {f} + ))}
diff --git a/apps/web/src/pages/benchmark/components/GapAnalysis.tsx b/apps/web/src/pages/benchmark/components/GapAnalysis.tsx index 565d876b..078c9495 100644 --- a/apps/web/src/pages/benchmark/components/GapAnalysis.tsx +++ b/apps/web/src/pages/benchmark/components/GapAnalysis.tsx @@ -22,7 +22,7 @@ export function GapAnalysis({ > onExplore({ outcomeDecompiler: 'asmlift', outcome: 'declined', decline: key })} + onBarClick={(key) => onExplore({ outcomeDecompiler: 'asmlift', outcome: 'declined', decline: [key] })} /> diff --git a/apps/web/src/pages/benchmark/components/Overview.tsx b/apps/web/src/pages/benchmark/components/Overview.tsx index 40872338..11260be0 100644 --- a/apps/web/src/pages/benchmark/components/Overview.tsx +++ b/apps/web/src/pages/benchmark/components/Overview.tsx @@ -1,4 +1,4 @@ -import type { FunctionResult } from '@asmlift/bench-schema'; +import { FEATURE_BY_ID, type FunctionResult } from '@asmlift/bench-schema'; import { featureStats, headToHead, headToHeadBy, headline, matchRate, matchRateBy } from '../lib/stats'; import { LOC_BUCKETS, locBucketOf, readabilityStats } from '../lib/stats'; @@ -145,7 +145,7 @@ export function Overview({ ({ @@ -154,11 +154,11 @@ export function Overview({ asmlift: f.asmlift, m2c: f.m2c, }))} - labelOf={(k) => k} + labelOf={(k) => FEATURE_BY_ID.get(k)?.label ?? k} horizontal showCount height={Math.max(360, topFeatures.length * 26)} - onBarClick={(k) => onExplore({ feature: k })} + onBarClick={(k) => onExplore({ feature: [k] })} /> diff --git a/apps/web/src/pages/benchmark/components/ui/Badge.tsx b/apps/web/src/pages/benchmark/components/ui/Badge.tsx index 22fc2989..57b5c069 100644 --- a/apps/web/src/pages/benchmark/components/ui/Badge.tsx +++ b/apps/web/src/pages/benchmark/components/ui/Badge.tsx @@ -1,41 +1,98 @@ -import type { GapSize, Outcome } from '@asmlift/bench-schema'; +// The benchmark's badges: a plus the domain rule that picks its color. +import { FEATURE_BY_ID, type GapSize, type Outcome } from '@asmlift/bench-schema'; +import { HoverCard } from '../../../../shared/components/HoverCard'; +import { Pill } from '../../../../shared/components/Pill'; import { GAP_BUCKETS, GAP_BUCKET_COLOR, OUTCOME_COLOR, OUTCOME_LABEL } from '../../theme'; /** Solid-dot + tinted-pill badge, colored by outcome. */ export function OutcomeBadge({ outcome }: { outcome: Outcome }) { - const color = OUTCOME_COLOR[outcome]; return ( - - + {OUTCOME_LABEL[outcome]} - + ); } /** Measured gap-size pill: differing / total objdiff instruction rows of the best scored candidate. */ export function GapBadge({ gap }: { gap: GapSize }) { const bucket = GAP_BUCKETS.find((b) => gap.score <= b.max) ?? GAP_BUCKETS[GAP_BUCKETS.length - 1]; - const color = GAP_BUCKET_COLOR[bucket.key]; return ( - {gap.score}/{gap.maxScore} - + ); } -/** Neutral tag chip for features. */ +/** Neutral tag chip. */ export function Chip({ children }: { children: React.ReactNode }) { return ( - + {children} - + + ); +} + +/** A feature tag: hover for the summary and a link into the full definition, click for the + * definition directly. Shows the id rather than the human label — it is what the URL, the dataset + * and the vocabulary key on, and a table of 743 rows has no room for prose. */ +export function FeatureChip({ id, onClick }: { id: string; onClick?: (id: string) => void }) { + const def = FEATURE_BY_ID.get(id); + + if (!def) { + // Unreachable from published data (closed vocabulary, enforced by tests), so if one appears it + // should look like the bug it is. + return ( + + {id} + + ); + } + + return ( + +
+ {def.label} + + {def.id} + +
+

{def.summary}

+ {onClick && ( + + )} + + } + > + { + if (onClick) { + e.stopPropagation(); // the row underneath opens the function detail + onClick(id); + } + }} + className={onClick ? 'cursor-pointer hover:bg-teal-900/60 hover:text-teal-200 focus:outline-hidden' : ''} + > + {id} + +
); } diff --git a/apps/web/src/pages/benchmark/lib/explorer-url.ts b/apps/web/src/pages/benchmark/lib/explorer-url.ts index ff444625..513140fa 100644 --- a/apps/web/src/pages/benchmark/lib/explorer-url.ts +++ b/apps/web/src/pages/benchmark/lib/explorer-url.ts @@ -1,7 +1,7 @@ // The Benchmark view's URL state, shared between Benchmark.tsx (sub-tab, preset deep links) and // Explorer.tsx (filters, sort, selected row): every explorer view — including an open // FunctionDetail — is a shareable link. Defaults are cleared from the URL (nuqs clearOnDefault). -import { type inferParserType, parseAsString, parseAsStringLiteral } from 'nuqs'; +import { type inferParserType, parseAsArrayOf, parseAsString, parseAsStringLiteral } from 'nuqs'; export const TAB_IDS = ['overview', 'explorer', 'gap', 'methodology'] as const; export type TabId = (typeof TAB_IDS)[number]; @@ -28,8 +28,10 @@ export const FILTER_PARSERS = { outcomeDecompiler: parseAsStringLiteral(['any', 'asmlift', 'm2c'] as const).withDefault('any'), outcome: parseAsString.withDefault(''), verdict: parseAsStringLiteral(VERDICTS).withDefault(''), - feature: parseAsString.withDefault(''), - decline: parseAsString.withDefault(''), + // Multi-select, AND-ed: a row must carry EVERY selected tag / exhibit every selected class. + // Comma-separated, so a single-value link still parses as a one-element list. + feature: parseAsArrayOf(parseAsString).withDefault([]), + decline: parseAsArrayOf(parseAsString).withDefault([]), // 'with' = only rows asmlift ran WITH the project's symbol map (asmlift.symbolMap provenance) symbols: parseAsStringLiteral(['', 'with'] as const).withDefault(''), search: parseAsString.withDefault(''), @@ -53,6 +55,18 @@ export const FILTER_URL_KEYS = { export type Filters = inferParserType; export type ExplorerPreset = Partial; +/** The definition drawer's subject — the same URL-state shape as `fn`, the row drawer. */ +export const FEATURE_TERM_PARSER = parseAsString.withDefault(''); +export const FEATURE_TERM_KEY = 'about'; + +/** Link that opens the definition drawer for `id`. A real `href`, so middle-click and copy-link + * work; built from the CURRENT query, so opening one never discards the filters already set. */ +export function featureHref(id: string, search = typeof window === 'undefined' ? '' : window.location.search): string { + const params = new URLSearchParams(search); + params.set(FEATURE_TERM_KEY, id); + return `?${params.toString()}`; +} + /** A preset deep-link REPLACES the whole filter set: spread this under it to reset the rest. */ export const FILTERS_RESET: { [K in keyof Filters]: null } = { project: null, diff --git a/apps/web/src/pages/benchmark/lib/stats.ts b/apps/web/src/pages/benchmark/lib/stats.ts index e049a13f..2268bc6a 100644 --- a/apps/web/src/pages/benchmark/lib/stats.ts +++ b/apps/web/src/pages/benchmark/lib/stats.ts @@ -111,6 +111,17 @@ export function distinctFeatures(rows: FunctionResult[]): string[] { return featureStats(rows).map((f) => f.feature); } +/** How many of these rows carry each key — the per-option counts a multi-select facet needs. */ +export function tally(rows: FunctionResult[], keysOf: (r: FunctionResult) => string[]): Map { + const acc = new Map(); + for (const r of rows) { + for (const k of keysOf(r)) { + acc.set(k, (acc.get(k) ?? 0) + 1); + } + } + return acc; +} + export interface H2HRow { key: K; total: number; diff --git a/apps/web/test/explorer-url.test.ts b/apps/web/test/explorer-url.test.ts new file mode 100644 index 00000000..83cfd0d3 --- /dev/null +++ b/apps/web/test/explorer-url.test.ts @@ -0,0 +1,56 @@ +// The Explorer's URL contract: every filter view is a shareable link, so these parsers are what a +// permalink is built on. +import { expect, test } from 'vitest'; + +import { + FEATURE_TERM_KEY, + FILTERS_RESET, + FILTER_PARSERS, + FILTER_URL_KEYS, + TAB_IDS, + featureHref, + tabParser, +} from '../src/pages/benchmark/lib/explorer-url'; + +test('feature and decline parse as lists', () => { + expect(FILTER_PARSERS.feature.parse('loop,table')).toEqual(['loop', 'table']); + expect(FILTER_PARSERS.decline.parse('float,mips-calls')).toEqual(['float', 'mips-calls']); +}); + +test('a single-value link written before the list change still parses', () => { + // ?feature=loop was the whole filter for the entire life of the old