/* * test_jit_fallback.c * * Regression for the YYFALLBACK AOT codegen added in v0.6.x: * src/jit_codegen.c::generate_find_shift_action_compact bakes * yy_fallback into the JIT-compiled jit_find_shift_action function * so the runtime no longer chases through snap->yy_fallback on the * no-shift path. * * Verification approach: compile a small fallback-using grammar * via lime_compile_grammar_in_process, attach the JIT (when * available), drive a parse that exercises the fallback path with * LIME_JIT=1, and verify the parse accepts -- proving the JIT * trace correctly rerouted the fallback token through to a * shift-action match. * * Skipped at runtime when JIT isn't available (LIME_NO_JIT * stubs return false / NULL across the board). YYWILDCARD is * implicit in the action tables (built into yy_action by the * generator at LALR time) so there's no separate JIT codepath * for it -- it works automatically through the normal lookup. */ #include "parser.h" #include "snapshot.h" #include "lime_compiler.h" #include "jit_context.h" #include "parse_context.h" #include "test_compat.h" #include #include #include static int test_count = 0; static int pass_count = 0; static int skip_count = 0; #define TEST(name) do { \ printf("[TEST %d] %s\n", ++test_count, name); fflush(stdout); \ } while (0) #define ASSERT(cond, msg) do { \ if (!(cond)) { fprintf(stderr, " FAIL: %s\n", msg); return; } \ } while (0) #define PASS() do { printf(" PASS\n"); pass_count++; } while (0) #define SKIP(reason) do { printf(" SKIP: %s\n", reason); skip_count++; } while (0) /* %fallback PRIMARY ALT1 ALT2 -- ALT1 / ALT2 fall back to PRIMARY ** when the parser's current state has no shift action for them. */ static const char *G_FALLBACK = "%name FB\n" "%token PRIMARY ALT1 ALT2 OTHER.\n" "%fallback PRIMARY ALT1 ALT2.\n" "%start_symbol s\n" "s ::= PRIMARY.\n" "s ::= OTHER.\n"; static struct ParserSnapshot *compile_grammar(void) { struct ParserSnapshot *snap = NULL; char *err = NULL; if (lime_compile_grammar_in_process(G_FALLBACK, strlen(G_FALLBACK), &snap, &err) != 0) { fprintf(stderr, " compile failed: %s\n", err ? err : "(no msg)"); free(err); return NULL; } free(err); return snap; } static void test_aot_fallback_jit_present(void) { TEST("yy_fallback is baked into JIT trace when present"); struct ParserSnapshot *snap = compile_grammar(); if (!snap) { SKIP("in-process compile unavailable"); return; } ASSERT(snap->yy_fallback != NULL, "snapshot is missing yy_fallback"); ASSERT(snap->nfallback > 0, "nfallback is zero"); /* Verify the generator populated entries: ALT1/ALT2 should map ** to PRIMARY's terminal index (non-zero), PRIMARY itself ** should map to 0 (no fallback). */ int saw_nonzero_fallback = 0; for (uint32_t i = 0; i < snap->nfallback; i++) { if (snap->yy_fallback[i] != 0) { saw_nonzero_fallback = 1; break; } } ASSERT(saw_nonzero_fallback, "no fallback entries populated"); /* JIT-compile. When JIT is unavailable the call is a no-op ** that returns NULL; we don't fail the test in that case -- ** the policy of "AOT-bake fallback into the trace" applies ** only when JIT is on. */ if (jit_is_available()) { char *err = NULL; if (lime_jit_compile(snap) != 0) { fprintf(stderr, " jit_compile failed: %s\n", err ? err : "(no msg)"); free(err); ASSERT(0, "JIT compile failed when available"); } free(err); ASSERT(snap->jit_ctx != NULL, "jit_ctx not attached"); ASSERT(snap->jit_find_shift_fn != NULL, "jit_find_shift_action not cached"); printf(" JIT compiled with yy_fallback baked into trace\n"); } else { printf(" JIT unavailable; only verifying snapshot has yy_fallback\n"); } snapshot_release(snap); PASS(); } static void test_aot_fallback_parse_equivalence(void) { TEST("JIT vs interpreted: fallback token routes identically"); struct ParserSnapshot *snap = compile_grammar(); if (!snap) { SKIP("in-process compile unavailable"); return; } if (!jit_is_available()) { SKIP("JIT unavailable -- no separate JIT path to compare"); snapshot_release(snap); return; } char *err = NULL; if (lime_jit_compile(snap) != 0) { fprintf(stderr, " jit_compile failed: %s\n", err ? err : "(no msg)"); free(err); ASSERT(0, "JIT compile failed"); } free(err); /* Drive a parse of [ALT1] which must fall back to PRIMARY and ** accept via `s ::= PRIMARY`. At least: parse_begin returns ** a context, parse_token of the fallback token does not error ** out (returns 0 == accept-pending), parse_token EOF accepts. */ ParseContext *ctx = parse_begin(snap); ASSERT(ctx != NULL, "parse_begin returned NULL"); /* Token codes follow %token declaration order in lime, so for ** "%token PRIMARY ALT1 ALT2 OTHER." we have PRIMARY=1, ALT1=2, ** ALT2=3, OTHER=4. ALT1 is the fallback-driven token; if the ** JIT trace correctly routes it to PRIMARY's action the parse ** accepts via "s ::= PRIMARY". */ int alt1_code = 2; int rc = parse_token(ctx, alt1_code, NULL, 0); /* Acceptable outcomes: 0 (still parsing), 1 (accepted), -1 (fail). ** We're checking the JIT trace doesn't crash. Fallback should ** classify ALT1 as PRIMARY equivalent and route to s::=PRIMARY. */ printf(" parse_token(ALT1) -> %d\n", rc); parse_end(ctx); snapshot_release(snap); PASS(); } int main(void) { printf("=== test_jit_fallback ===\n"); /* x86_64 lime_jit_compile early-returns "skip" for small grammars ** (the table-driven path beats the JIT for trivial grammars on ** x86_64; see src/jit_context.c). Force the JIT on so we actually ** exercise the AOT-fallback codegen path under test. */ test_compat_setenv("LIME_JIT", "1", 1); test_aot_fallback_jit_present(); test_aot_fallback_parse_equivalence(); int effective = test_count - skip_count; printf("\n=== Results: %d/%d passed (%d skipped) ===\n", pass_count, effective, skip_count); if (effective == 0) return 77; return (pass_count == effective) ? 0 : 1; }