/* ** tests/test_embed.c -- v0.4.4 regression test for `%embed`. ** ** Sub-tests (10): ** ** 1. Build the fixture; assert generated _snapshot.c contains ** _embed_table with two entries (json, datalog). ** 2. Assert RegisterEmbedTriggers() function is emitted. ** 3. Assert SetEmbedSnapshot() declaration is in the ** generated .h. ** 4. Round-trip: `lime -F` preserves both %embed directives in ** declaration order. ** 5. Idempotence: format(format(F)) == format(F). ** 6. Error path: ENTRY_TOKEN bound to undeclared terminal -> error. ** 7. Error path: missing terminator `.` -> error. ** 8. No %embed directives: generated _snapshot.c does NOT contain ** _embed_table (zero-cost when unused). ** 9. Driver test: link against a fixture parser, register a stub ** snapshot via SetEmbedSnapshot, drive context_switch_classify_lexeme ** and verify the trigger lookup returns the expected mode. ** 10. Reverse validation tags (compile-time stub flags exercised ** manually during development; documented in the commit body). ** ** Exits 0 on full pass, 1 on any failure, 77 on missing binary ** (meson skip). */ #include #include "test_compat.h" #include #include #include #include #include #include "grammar_context.h" #include "snapshot.h" /* Generated by lime -n on test_embed_grammar.lime; the runtime ** linker resolves these symbols against the _snapshot.c emitted ** into the meson build directory. See tests/meson.build. */ extern ParserSnapshot *EmbedTestBuildSnapshot(void); extern int EmbedTestSetEmbedSnapshot(const char *name, ParserSnapshot *snap); extern int EmbedTestRegisterEmbedTriggers(GrammarContextStack *stack); static int failures = 0; #define FAIL(tag, fmt, ...) do { \ fprintf(stderr, "FAIL: %s -- " fmt "\n", (tag), ##__VA_ARGS__); \ failures++; \ } while (0) static char *slurp(const char *path) { FILE *f = fopen(path, "rb"); if (f == NULL) return NULL; fseek(f, 0, SEEK_END); long n = ftell(f); rewind(f); if (n < 0) { fclose(f); return NULL; } char *buf = (char *)malloc((size_t)n + 1); if (buf == NULL) { fclose(f); return NULL; } size_t got = fread(buf, 1, (size_t)n, f); fclose(f); buf[got] = 0; return buf; } static void ensure_dir(const char *path) { struct stat st; if (stat(path, &st) == 0 && S_ISDIR(st.st_mode)) return; char cmd[1024]; test_compat_mkdir_p(path); } static char g_scratch[256] = {0}; static void cleanup_scratch(void) { if (g_scratch[0] == 0) return; test_compat_chdir_temp(); test_compat_rmdir_recursive(g_scratch); g_scratch[0] = 0; } static int enter_scratch_dir(void) { if (test_compat_tmpdir("lime_test_embed", g_scratch, sizeof(g_scratch)) != 0) { g_scratch[0] = 0; return -1; } if (chdir(g_scratch) != 0) { cleanup_scratch(); return -1; } atexit(cleanup_scratch); return 0; } /* Run `lime -T -d `; capture stdout+stderr ** when err_out is non-NULL. */ static int run_lime(const char *lime_bin, const char *limpar, const char *flags, const char *outdir, const char *fixture, char *err_out, size_t err_cap) { char flagbuf[256]; snprintf(flagbuf, sizeof(flagbuf), "%s", flags ? flags : ""); char tflag[1024], dflag[1024]; snprintf(tflag, sizeof(tflag), "-T%s", limpar); snprintf(dflag, sizeof(dflag), "-d%s", outdir); char *argv[24] = { (char *)lime_bin }; int argc = 1; char *tok = strtok(flagbuf, " "); while (tok && argc < 18) { argv[argc++] = tok; tok = strtok(NULL, " "); } argv[argc++] = tflag; argv[argc++] = dflag; argv[argc++] = (char *)fixture; argv[argc] = NULL; int rc = 0; if (err_out != NULL) { char errfile[1024]; snprintf(errfile, sizeof(errfile), "%s/stderr.txt", outdir); if (test_compat_run_to_files(argv, NULL, errfile, &rc) != 0) return -1; FILE *f = fopen(errfile, "rb"); if (f != NULL) { size_t n = fread(err_out, 1, err_cap - 1, f); err_out[n] = 0; fclose(f); } else { err_out[0] = 0; } } else { if (test_compat_run(argv, &rc) != 0) return -1; } return rc; } static int contains(const char *hay, const char *needle) { return hay != NULL && strstr(hay, needle) != NULL; } static int find_pos(const char *hay, const char *needle) { if (hay == NULL) return -1; const char *p = strstr(hay, needle); return p ? (int)(p - hay) : -1; } int main(int argc, char **argv) { if (argc < 4) { fprintf(stderr, "usage: %s \n", argv[0]); return 2; } const char *lime_bin = argv[1]; const char *limpar = argv[2]; const char *fixture = argv[3]; struct stat st; if (stat(lime_bin, &st) != 0) { fprintf(stderr, "SKIP: %s not found\n", lime_bin); return 77; } char lime_abs[PATH_MAX], limpar_abs[PATH_MAX], fixture_abs[PATH_MAX]; if (test_compat_realpath(lime_bin, lime_abs, sizeof(lime_abs)) != 0) return 77; if (test_compat_realpath(limpar, limpar_abs, sizeof(limpar_abs)) != 0) return 77; if (test_compat_realpath(fixture, fixture_abs, sizeof(fixture_abs)) != 0) return 77; lime_bin = lime_abs; limpar = limpar_abs; fixture = fixture_abs; if (enter_scratch_dir() != 0) { fprintf(stderr, "FAIL: could not create scratch dir\n"); return 1; } const char *d_main = "out_main"; const char *d_err = "out_err"; ensure_dir(d_main); ensure_dir(d_err); /* ---- 1, 2, 3. Generate snapshot+header from fixture ---- */ if (run_lime(lime_bin, limpar, "-n", d_main, fixture, NULL, 0) != 0) { FAIL("build", "lime exited non-zero on test_embed_grammar.lime"); } else { char snapshot_path[1024], header_path[1024]; snprintf(snapshot_path, sizeof(snapshot_path), "%s/test_embed_grammar_snapshot.c", d_main); snprintf(header_path, sizeof(header_path), "%s/test_embed_grammar.h", d_main); char *snap_src = slurp(snapshot_path); char *header = slurp(header_path); if (snap_src == NULL) { FAIL("build", "no _snapshot.c emitted"); } else { /* Sub-test 1: table with two entries. */ if (!contains(snap_src, "EmbedTest_embed_table")) { FAIL("table_present", "EmbedTest_embed_table not in _snapshot.c"); } if (!contains(snap_src, "\"json\"") || !contains(snap_src, "\"datalog\"")) { FAIL("table_entries", "expected entries for json + datalog"); } /* Declaration-order: json must precede datalog in the ** static-array initializer so the runtime registration ** loop calls register_trigger in source order. */ int p_json = find_pos(snap_src, "\"json\", \"json\""); int p_dlog = find_pos(snap_src, "\"datalog\", \":-\""); if (p_json < 0 || p_dlog < 0 || p_json >= p_dlog) { FAIL("table_order", "table entries not in declaration order"); } /* Sub-test 2: register helper emitted. */ if (!contains(snap_src, "EmbedTestRegisterEmbedTriggers")) { FAIL("register_helper", "EmbedTestRegisterEmbedTriggers function missing"); } if (!contains(snap_src, "context_switch_register_trigger")) { FAIL("register_helper_calls", "register helper does not call " "context_switch_register_trigger"); } free(snap_src); } if (header == NULL) { FAIL("build", "no .h emitted"); } else { /* Sub-test 3: setter declaration in .h. */ if (!contains(header, "EmbedTestSetEmbedSnapshot")) { FAIL("setter_decl", "EmbedTestSetEmbedSnapshot decl missing in .h"); } if (!contains(header, "EmbedTestRegisterEmbedTriggers")) { FAIL("register_decl", "EmbedTestRegisterEmbedTriggers decl missing in .h"); } free(header); } } /* ---- 4. Round-trip: `lime -F` preserves both directives ---- */ { /* Copy fixture into scratch so the .formatted output lands ** in the scratch dir (lime -F writes alongside the input). */ char fix_local[PATH_MAX]; snprintf(fix_local, sizeof(fix_local), "%s/embed_round1.lime", g_scratch); char cmd[PATH_MAX * 2]; if (test_compat_copy_file(fixture, fix_local) != 0) { FAIL("roundtrip", "could not copy fixture to scratch"); } else { { char tflag[1024]; snprintf(tflag, sizeof(tflag), "-T%s", limpar); char *fmt_argv[] = { (char *)lime_bin, tflag, "-F", fix_local, NULL }; int rc = 0; test_compat_run(fmt_argv, &rc); (void)rc; } char fmt_path[PATH_MAX]; snprintf(fmt_path, sizeof(fmt_path), "%s.formatted", fix_local); char *fmt = slurp(fmt_path); if (fmt == NULL) { FAIL("roundtrip", "no .formatted output"); } else { int p_json = find_pos(fmt, "%embed json TRIGGER 'json'"); int p_dlog = find_pos(fmt, "%embed datalog TRIGGER ':-'"); if (p_json < 0) { FAIL("roundtrip", "json embed missing in formatted"); } if (p_dlog < 0) { FAIL("roundtrip", "datalog embed missing in formatted"); } if (p_json >= 0 && p_dlog >= 0 && p_json >= p_dlog) { FAIL("roundtrip_order", "formatted order is not declaration order"); } if (!contains(fmt, "ENTRY_TOKEN STRING.")) { FAIL("roundtrip_entry", "ENTRY_TOKEN STRING. missing"); } if (!contains(fmt, "ENTRY_TOKEN IDENT.")) { FAIL("roundtrip_entry", "ENTRY_TOKEN IDENT. missing"); } /* ---- 5. Idempotence ---- */ char fix_pass2[PATH_MAX]; snprintf(fix_pass2, sizeof(fix_pass2), "%s/embed_round2.lime", g_scratch); if (test_compat_copy_file(fmt_path, fix_pass2) == 0) { char tflag[1024]; snprintf(tflag, sizeof(tflag), "-T%s", limpar); char *fmt2_argv[] = { (char *)lime_bin, tflag, "-F", fix_pass2, NULL }; int rc2 = 0; test_compat_run(fmt2_argv, &rc2); (void)rc2; char fmt2_path[PATH_MAX]; snprintf(fmt2_path, sizeof(fmt2_path), "%s.formatted", fix_pass2); char *fmt2 = slurp(fmt2_path); if (fmt2 == NULL) { FAIL("idempotence", "second formatted pass missing"); } else { if (strcmp(fmt, fmt2) != 0) { FAIL("idempotence", "format(format(F)) != format(F); " "directive emit drifts on re-format"); } free(fmt2); } } free(fmt); } } } /* ---- 6. Error path: ENTRY_TOKEN bound to undeclared symbol ---- */ { FILE *f = fopen("undecl.lime", "wb"); if (f == NULL) { FAIL("undeclared_token", "could not write fixture"); } else { fputs("%name X\n%token A B.\n%type x {int}\n" "%embed foo TRIGGER 'x' ENTRY_TOKEN UNDECLARED.\n" "x ::= A B. { x = 0; }\n", f); fclose(f); char err[4096]; int rc = run_lime(lime_bin, limpar, "", d_err, "undecl.lime", err, sizeof(err)); if (rc == 0) { FAIL("undeclared_token", "lime accepted UNDECLARED ENTRY_TOKEN (expected error)"); } else if (!contains(err, "UNDECLARED") || !contains(err, "not a declared")) { FAIL("undeclared_token", "expected diagnostic mentioning UNDECLARED + " "'not a declared'; got: %s", err); } } } /* ---- 7. Error path: missing terminator `.` ---- */ { FILE *f = fopen("missdot.lime", "wb"); if (f == NULL) { FAIL("missing_dot", "could not write fixture"); } else { /* No `.` after STRING -- the next %name (or any directive) ** triggers the parser's expected-`.` diagnostic. We feed ** a follow-up directive so the trailing token stream ** reaches the embed-terminator state. */ fputs("%name X\n%token STRING.\n" "%embed foo TRIGGER 'x' ENTRY_TOKEN STRING\n" "%type x {int}\n" "x ::= STRING. { x = 0; }\n", f); fclose(f); char err[4096]; int rc = run_lime(lime_bin, limpar, "", d_err, "missdot.lime", err, sizeof(err)); if (rc == 0) { FAIL("missing_dot", "lime accepted %%embed without terminator (expected error)"); } else if (!contains(err, "embed") || !contains(err, "terminator")) { FAIL("missing_dot", "expected diagnostic mentioning embed + terminator; " "got: %s", err); } } } /* ---- 8. No %embed directives: generated _snapshot.c is clean ---- */ { FILE *f = fopen("noembed.lime", "wb"); if (f == NULL) { FAIL("no_embed", "could not write fixture"); } else { fputs("%name NoEmbed\n%token A B.\n%type x {int}\n" "x ::= A B. { x = 0; }\n", f); fclose(f); ensure_dir("out_noembed"); int rc = run_lime(lime_bin, limpar, "-n", "out_noembed", "noembed.lime", NULL, 0); if (rc != 0) { FAIL("no_embed", "lime failed on no-embed fixture"); } else { char *snap = slurp("out_noembed/noembed_snapshot.c"); if (snap == NULL) { FAIL("no_embed", "no _snapshot.c emitted"); } else { if (contains(snap, "_embed_table")) { FAIL("no_embed", "embed_table emitted for grammar with no " "%%embed (zero-cost guarantee broken)"); } if (contains(snap, "RegisterEmbedTriggers")) { FAIL("no_embed", "RegisterEmbedTriggers emitted for grammar " "with no %%embed"); } free(snap); } } } } /* ---- 9. Driver test: link against the fixture parser, register a ** stub snapshot, drive context_switch_classify_lexeme. ---- */ { ParserSnapshot *root = EmbedTestBuildSnapshot(); if (root == NULL) { FAIL("driver", "EmbedTestBuildSnapshot returned NULL"); } else { GrammarContextStack *stack = grammar_context_create(root); if (stack == NULL) { FAIL("driver", "grammar_context_create failed"); snapshot_release(root); } else { /* Wire the same root snapshot as the embedded snapshot ** for both modes -- we are exercising the registration ** path and the classify lookup, not actually parsing ** the embedded language. */ if (EmbedTestSetEmbedSnapshot("json", root) != 0) { FAIL("driver_set_json", "SetEmbedSnapshot(json) failed"); } if (EmbedTestSetEmbedSnapshot("datalog", root) != 0) { FAIL("driver_set_datalog", "SetEmbedSnapshot(datalog) failed"); } /* Negative: unknown name returns -1. */ if (EmbedTestSetEmbedSnapshot("nonsuch", root) == 0) { FAIL("driver_set_nonsuch", "SetEmbedSnapshot(nonsuch) returned 0; " "expected -1 for unknown mode label"); } int n = EmbedTestRegisterEmbedTriggers(stack); if (n != 2) { FAIL("driver_register", "RegisterEmbedTriggers returned %d; expected 2", n); } /* Classify: the trigger lexemes match. */ GrammarMode mj = context_switch_classify_lexeme(stack, "json"); if (mj == MODE_NONE) { FAIL("driver_classify_json", "classify_lexeme(\"json\") returned MODE_NONE; " "trigger registration did not take effect"); } GrammarMode md = context_switch_classify_lexeme(stack, ":-"); if (md == MODE_NONE) { FAIL("driver_classify_dlog", "classify_lexeme(\":-\") returned MODE_NONE"); } if (mj != MODE_NONE && md != MODE_NONE && mj == md) { FAIL("driver_classify_distinct", "json and datalog hashed to the same mode id"); } /* Negative: unrelated lexeme. */ GrammarMode mn = context_switch_classify_lexeme(stack, "wibble"); if (mn != MODE_NONE) { FAIL("driver_classify_neg", "classify_lexeme(\"wibble\") returned a mode; " "expected MODE_NONE"); } grammar_context_destroy(stack); } } } if (failures > 0) { fprintf(stderr, "test_embed: %d failure(s)\n", failures); return 1; } printf("test_embed: 9 sub-tests passed\n"); return 0; }