Skip to content

Commit 17f856c

Browse files
authored
Merge branch 'main' into fix/issue-842-clobber-class
2 parents 6b2cd08 + 424e4dc commit 17f856c

9 files changed

Lines changed: 198 additions & 16 deletions

File tree

.github/workflows/_test.yml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,18 @@ jobs:
8888
env:
8989
CBM_SKIP_PERF: ${{ inputs.skip_perf && '1' || '' }}
9090

91+
test-tsan:
92+
runs-on: ubuntu-latest
93+
timeout-minutes: 120
94+
steps:
95+
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
96+
97+
- name: Install deps (Ubuntu)
98+
run: sudo apt-get update && sudo apt-get install -y clang zlib1g-dev
99+
100+
- name: ThreadSanitizer tests
101+
run: make -f Makefile.cbm test-tsan CC=clang CXX=clang++
102+
91103
test-windows:
92104
needs: setup-matrix
93105
strategy:

Makefile.cbm

Lines changed: 55 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -67,10 +67,11 @@ CFLAGS_TEST = $(CFLAGS_COMMON) -g -O1 $(SANITIZE)
6767
CXXFLAGS_TEST = $(CXXFLAGS_COMMON) -g -O1 $(SANITIZE)
6868

6969
# TSan (can't combine with ASan)
70+
TSAN_SANITIZE = -fsanitize=thread -fno-omit-frame-pointer
7071
CFLAGS_TSAN = $(CFLAGS_COMMON) -g -O1 \
71-
-fsanitize=thread -fno-omit-frame-pointer
72+
$(TSAN_SANITIZE)
7273
CXXFLAGS_TSAN = $(CXXFLAGS_COMMON) -g -O1 \
73-
-fsanitize=thread -fno-omit-frame-pointer
74+
$(TSAN_SANITIZE)
7475

7576
# Windows needs ws2_32 (Winsock), psapi (GetProcessMemoryInfo), shell32
7677
# (CommandLineToArgvW — the UTF-8 argv path in main.c, #423/#20), and
@@ -89,7 +90,7 @@ endif
8990

9091
LDFLAGS = -lm -lstdc++ -lpthread -lz $(WIN32_LIBS) $(STATIC_FLAGS)
9192
LDFLAGS_TEST = -lm -lstdc++ -lpthread -lz $(SANITIZE) $(WIN32_LIBS)
92-
LDFLAGS_TSAN = -lm -lstdc++ -lpthread -lz -fsanitize=thread $(WIN32_LIBS)
93+
LDFLAGS_TSAN = -lm -lstdc++ -lpthread -lz $(TSAN_SANITIZE) $(WIN32_LIBS)
9394

9495
# ── Source files ─────────────────────────────────────────────────
9596

@@ -485,13 +486,17 @@ GRAMMAR_CFLAGS = -std=c11 -D_DEFAULT_SOURCE -O2 -w -I$(CBM_DIR) -I$(TS_INCLUDE)
485486
GRAMMAR_CFLAGS_TEST = -std=c11 -D_DEFAULT_SOURCE -g -O1 -w -I$(CBM_DIR) -I$(TS_INCLUDE) -I$(TS_SRC) \
486487
$(SANITIZE)
487488
GRAMMAR_CFLAGS_TSAN = -std=c11 -D_DEFAULT_SOURCE -g -O1 -w -I$(CBM_DIR) -I$(TS_INCLUDE) -I$(TS_SRC) \
488-
-fsanitize=thread -fno-omit-frame-pointer
489+
$(TSAN_SANITIZE)
489490

490491
# Object files for grammars + ts_runtime + lsp_all + preprocessor
491492
GRAMMAR_OBJS_TEST = $(patsubst $(CBM_DIR)/%.c,$(BUILD_DIR)/%.o,$(GRAMMAR_SRCS))
492493
TS_RUNTIME_OBJ_TEST = $(BUILD_DIR)/ts_runtime.o
493494
LSP_OBJ_TEST = $(BUILD_DIR)/lsp_all.o
494495
PP_OBJ_TEST = $(BUILD_DIR)/preprocessor.o
496+
GRAMMAR_OBJS_TSAN = $(patsubst $(CBM_DIR)/%.c,$(BUILD_DIR)/tsan_%.o,$(GRAMMAR_SRCS))
497+
TS_RUNTIME_OBJ_TSAN = $(BUILD_DIR)/tsan_ts_runtime.o
498+
LSP_OBJ_TSAN = $(BUILD_DIR)/tsan_lsp_all.o
499+
PP_OBJ_TSAN = $(BUILD_DIR)/tsan_preprocessor.o
495500

496501
# ── Targets ──────────────────────────────────────────────────────
497502

@@ -522,60 +527,94 @@ $(BUILD_DIR)/lsp_all.o: $(LSP_UNITY_DEPS) | $(BUILD_DIR)
522527
$(BUILD_DIR)/preprocessor.o: $(CBM_DIR)/preprocessor.cpp | $(BUILD_DIR)
523528
$(CXX) $(CXXFLAGS_TEST) -w -I$(CBM_DIR)/vendored -c -o $@ $<
524529

530+
$(BUILD_DIR)/tsan_%.o: $(CBM_DIR)/%.c | $(BUILD_DIR)
531+
$(CC) $(GRAMMAR_CFLAGS_TSAN) -c -o $@ $<
532+
533+
$(BUILD_DIR)/tsan_ts_runtime.o: $(CBM_DIR)/ts_runtime.c | $(BUILD_DIR)
534+
$(CC) $(GRAMMAR_CFLAGS_TSAN) -c -o $@ $<
535+
536+
$(BUILD_DIR)/tsan_lsp_all.o: $(LSP_UNITY_DEPS) | $(BUILD_DIR)
537+
$(CC) $(GRAMMAR_CFLAGS_TSAN) -c -o $@ $<
538+
539+
$(BUILD_DIR)/tsan_preprocessor.o: $(CBM_DIR)/preprocessor.cpp | $(BUILD_DIR)
540+
$(CXX) $(CXXFLAGS_TSAN) -w -I$(CBM_DIR)/vendored -c -o $@ $<
541+
525542
# ── Full test binary ─────────────────────────────────────────────
526543

527544
# mimalloc object files
528545
MIMALLOC_OBJ_TEST = $(BUILD_DIR)/mimalloc.o
546+
MIMALLOC_OBJ_TSAN = $(BUILD_DIR)/tsan_mimalloc.o
529547
MIMALLOC_OBJ_PROD = $(BUILD_DIR)/prod_mimalloc.o
530548

531549
$(BUILD_DIR)/mimalloc.o: $(MIMALLOC_SRC) | $(BUILD_DIR)
532550
$(CC) $(MIMALLOC_CFLAGS_TEST) -c -o $@ $<
533551

552+
$(BUILD_DIR)/tsan_mimalloc.o: $(MIMALLOC_SRC) | $(BUILD_DIR)
553+
$(CC) $(MIMALLOC_CFLAGS_TEST) $(TSAN_SANITIZE) -c -o $@ $<
554+
534555
$(BUILD_DIR)/prod_mimalloc.o: $(MIMALLOC_SRC) | $(BUILD_DIR)
535556
$(CC) $(MIMALLOC_CFLAGS) -c -o $@ $<
536557

537558
# sqlite3 object files (vendored amalgamation)
538559
SQLITE3_OBJ_TEST = $(BUILD_DIR)/sqlite3.o
560+
SQLITE3_OBJ_TSAN = $(BUILD_DIR)/tsan_sqlite3.o
539561
SQLITE3_OBJ_PROD = $(BUILD_DIR)/prod_sqlite3.o
540562

541563
$(BUILD_DIR)/sqlite3.o: $(SQLITE3_SRC) | $(BUILD_DIR)
542564
$(CC) $(SQLITE3_CFLAGS_TEST) $(SANITIZE) -c -o $@ $<
543565

566+
$(BUILD_DIR)/tsan_sqlite3.o: $(SQLITE3_SRC) | $(BUILD_DIR)
567+
$(CC) $(SQLITE3_CFLAGS_TEST) $(TSAN_SANITIZE) -c -o $@ $<
568+
544569
$(BUILD_DIR)/prod_sqlite3.o: $(SQLITE3_SRC) | $(BUILD_DIR)
545570
$(CC) $(SQLITE3_CFLAGS) -c -o $@ $<
546571

547572
# TRE regex (only compiled on Windows — POSIX uses system <regex.h>)
548573
TRE_OBJ_TEST :=
574+
TRE_OBJ_TSAN :=
549575
TRE_OBJ_PROD :=
550576
ifeq ($(IS_MINGW),yes)
551577
TRE_OBJ_TEST = $(BUILD_DIR)/tre.o
578+
TRE_OBJ_TSAN = $(BUILD_DIR)/tsan_tre.o
552579
TRE_OBJ_PROD = $(BUILD_DIR)/prod_tre.o
553580

554581
$(BUILD_DIR)/tre.o: $(TRE_SRC) | $(BUILD_DIR)
555582
$(CC) $(TRE_CFLAGS) $(SANITIZE) -fno-sanitize=alignment -c -o $@ $<
556583

584+
$(BUILD_DIR)/tsan_tre.o: $(TRE_SRC) | $(BUILD_DIR)
585+
$(CC) $(TRE_CFLAGS) $(TSAN_SANITIZE) -c -o $@ $<
586+
557587
$(BUILD_DIR)/prod_tre.o: $(TRE_SRC) | $(BUILD_DIR)
558588
$(CC) $(TRE_CFLAGS) -O2 -c -o $@ $<
559589
endif
560590

561591
# Vendored LZ4 (test build)
562592
LZ4_OBJ_TEST = $(BUILD_DIR)/test_lz4.o $(BUILD_DIR)/test_lz4hc.o
593+
LZ4_OBJ_TSAN = $(BUILD_DIR)/tsan_lz4.o $(BUILD_DIR)/tsan_lz4hc.o
563594
$(BUILD_DIR)/test_lz4.o: $(CBM_DIR)/vendored/lz4/lz4.c | $(BUILD_DIR)
564595
$(CC) -std=c11 -D_DEFAULT_SOURCE -g -O1 $(SANITIZE) -w -I$(CBM_DIR) -c -o $@ $<
565596
$(BUILD_DIR)/test_lz4hc.o: $(CBM_DIR)/vendored/lz4/lz4hc.c | $(BUILD_DIR)
566597
$(CC) -std=c11 -D_DEFAULT_SOURCE -g -O1 $(SANITIZE) -w -I$(CBM_DIR)/vendored/lz4 -c -o $@ $<
598+
$(BUILD_DIR)/tsan_lz4.o: $(CBM_DIR)/vendored/lz4/lz4.c | $(BUILD_DIR)
599+
$(CC) -std=c11 -D_DEFAULT_SOURCE -g -O1 $(TSAN_SANITIZE) -w -I$(CBM_DIR) -c -o $@ $<
600+
$(BUILD_DIR)/tsan_lz4hc.o: $(CBM_DIR)/vendored/lz4/lz4hc.c | $(BUILD_DIR)
601+
$(CC) -std=c11 -D_DEFAULT_SOURCE -g -O1 $(TSAN_SANITIZE) -w -I$(CBM_DIR)/vendored/lz4 -c -o $@ $<
567602

568603
# Vendored zstd (test build)
569604
ZSTD_OBJ_TEST = $(BUILD_DIR)/test_zstd.o
605+
ZSTD_OBJ_TSAN = $(BUILD_DIR)/tsan_zstd.o
570606
$(BUILD_DIR)/test_zstd.o: $(CBM_DIR)/vendored/zstd/zstd.c | $(BUILD_DIR)
571607
$(CC) -std=c11 -D_DEFAULT_SOURCE -g -O1 $(SANITIZE) -w -I$(CBM_DIR)/vendored/zstd -c -o $@ $<
608+
$(BUILD_DIR)/tsan_zstd.o: $(CBM_DIR)/vendored/zstd/zstd.c | $(BUILD_DIR)
609+
$(CC) -std=c11 -D_DEFAULT_SOURCE -g -O1 $(TSAN_SANITIZE) -w -I$(CBM_DIR)/vendored/zstd -c -o $@ $<
572610

573611
# nomic-embed-code pretrained vector blob
574612
UNIXCODER_OBJ = $(BUILD_DIR)/unixcoder_blob.o
575613
$(UNIXCODER_OBJ): $(UNIXCODER_BLOB_SRC) vendored/nomic/code_vectors.bin | $(BUILD_DIR)
576614
$(CC) -c -o $@ $<
577615

578616
OBJS_VENDORED_TEST = $(MIMALLOC_OBJ_TEST) $(SQLITE3_OBJ_TEST) $(TRE_OBJ_TEST) $(GRAMMAR_OBJS_TEST) $(TS_RUNTIME_OBJ_TEST) $(LSP_OBJ_TEST) $(PP_OBJ_TEST) $(LZ4_OBJ_TEST) $(ZSTD_OBJ_TEST) $(UNIXCODER_OBJ)
617+
OBJS_VENDORED_TSAN = $(MIMALLOC_OBJ_TSAN) $(SQLITE3_OBJ_TSAN) $(TRE_OBJ_TSAN) $(GRAMMAR_OBJS_TSAN) $(TS_RUNTIME_OBJ_TSAN) $(LSP_OBJ_TSAN) $(PP_OBJ_TSAN) $(LZ4_OBJ_TSAN) $(ZSTD_OBJ_TSAN) $(UNIXCODER_OBJ)
579618

580619
$(BUILD_DIR)/test-runner: $(ALL_TEST_SRCS) $(PROD_SRCS) $(EXTRACTION_SRCS) $(AC_LZ4_SRCS) $(ZSTD_SRCS) $(SQLITE_WRITER_SRC) $(OBJS_VENDORED_TEST) | $(BUILD_DIR)
581620
$(CC) $(CFLAGS_TEST) -Itests -Itests/repro -o $@ \
@@ -603,8 +642,18 @@ test-repro: $(BUILD_DIR)/test-repro-runner
603642

604643
# ── TSan full test ───────────────────────────────────────────────
605644

606-
test-tsan:
607-
@echo "TSan not yet wired for full extraction tests"
645+
TEST_TSAN_SUITES ?= mem slab_alloc parallel
646+
TSAN_OPTIONS ?= halt_on_error=1
647+
648+
$(BUILD_DIR)/test-runner-tsan: $(ALL_TEST_SRCS) $(PROD_SRCS) $(EXTRACTION_SRCS) $(AC_LZ4_SRCS) $(ZSTD_SRCS) $(SQLITE_WRITER_SRC) $(OBJS_VENDORED_TSAN) | $(BUILD_DIR)
649+
$(CC) $(CFLAGS_TSAN) -Itests -Itests/repro -o $@ \
650+
$(ALL_TEST_SRCS) $(PROD_SRCS) \
651+
$(EXTRACTION_SRCS) $(AC_LZ4_SRCS) $(ZSTD_SRCS) $(SQLITE_WRITER_SRC) \
652+
$(OBJS_VENDORED_TSAN) \
653+
$(LDFLAGS_TSAN)
654+
655+
test-tsan: $(BUILD_DIR)/test-runner-tsan
656+
cd $(CURDIR) && TSAN_OPTIONS="$(TSAN_OPTIONS)" $(BUILD_DIR)/test-runner-tsan $(TEST_TSAN_SUITES)
608657

609658
# ── Production binary ────────────────────────────────────────────
610659

docs/EVALUATION_PLAN.md

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1127,10 +1127,20 @@ The evaluation's most serious flaw is not operational — it is that the bespoke
11271127

11281128
# Appendix A — Per-language chapters (159)
11291129

1130-
> Each chapter is a *draft specification* (per §12): repo + 5 dimension-tagged questions
1131-
> (symmetric-authoring split), the per-type graph-stats block (all 32 edge types, zeros
1132-
> kept), and — for the 9 LSP languages — the cross-repo + semantic/similarity deep-dive.
1133-
> Final question symbols are confirmed grep-first against the pinned commit at execution.
1130+
> **⚠️ STATUS — these are DRAFT specifications, not the final question bank.** The chapters below
1131+
> were **LLM-drafted from model knowledge of each repo** to establish the format, dimension mapping,
1132+
> and deep-dive shape. Symbols that could not be confirmed carry a `[verify]` tag. They are **not yet
1133+
> ground-truth-derived** and **must be regenerated at execution time** per §3.1 / §12:
1134+
>
1135+
> - **Question *types* are already grounded** in the Sillito et al. taxonomy (§3.1) — that part is final.
1136+
> - **Question *targets* (the specific symbols/files) are NOT yet from ground truth.** At execution they
1137+
> are replaced by: **SWE-QA** items for the major languages, and for the rest, targets **seeded from
1138+
> independent LSP symbol/reference data + git history — never from the model**, then confirmed
1139+
> grep-first against the pinned commit (symmetric-authoring split, [CR-1]).
1140+
>
1141+
> In short: each chapter shows *what kind* of question each dimension asks and *how* it is evaluated
1142+
> (output, stats block with all 32 edge types, aggregation, LSP deep-dive). It does **not** yet contain
1143+
> the final, ground-truth-sourced question set — do not read the drafted symbol names as validated.
11341144

11351145
---
11361146

internal/cbm/lsp/php_lsp.c

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3459,13 +3459,31 @@ static void flatten_trait_into_class(PHPLSPContext *ctx, CBMTypeRegistry *reg, c
34593459
t = lookup_type_with_project(ctx, trait_qn);
34603460
const char *canonical_trait_qn = t ? t->qualified_name : trait_qn;
34613461

3462+
/* A trait cannot meaningfully flatten into itself. PHP itself rejects
3463+
* `trait T { use T; }` ("Trait T cannot use itself"), and an aliased
3464+
* `use X as Y; use Y;` can resolve back onto the enclosing trait by short
3465+
* name. Without this guard the loop below copies the trait's own methods
3466+
* back onto it with receiver_type == canonical_trait_qn; because each copy
3467+
* then re-matches the loop's filter while cbm_registry_add_func() keeps
3468+
* growing reg->func_count, the iteration never terminates — it arena-
3469+
* allocates a fresh method every pass until the process exhausts all
3470+
* memory (observed: 40 GB+, freezing the host). See regression test
3471+
* phplsp_trait_self_use_terminates. */
3472+
if (strcmp(class_qn, canonical_trait_qn) == 0)
3473+
return;
3474+
34623475
/* Iterate registry funcs whose receiver_type is the trait.
34633476
*
34643477
* Self-substitution: when the trait's method has a return type that
34653478
* names the trait itself (e.g. `tap(): self` registered as
34663479
* NAMED(trait_qn)), rewrite it to NAMED(using_class_qn) so chains
3467-
* like `$c->tap()->classMethod()` resolve correctly. */
3468-
for (int i = 0; i < reg->func_count; i++) {
3480+
* like `$c->tap()->classMethod()` resolve correctly.
3481+
*
3482+
* Snapshot the count before iterating: cbm_registry_add_func() below
3483+
* appends to reg->funcs, so the loop must never visit entries it adds
3484+
* during iteration (a mutate-while-iterating hazard). */
3485+
const int func_count_before = reg->func_count;
3486+
for (int i = 0; i < func_count_before; i++) {
34693487
const CBMRegisteredFunc *src = &reg->funcs[i];
34703488
if (!src->receiver_type || strcmp(src->receiver_type, canonical_trait_qn) != 0) {
34713489
continue;

src/cypher/cypher.c

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4115,7 +4115,7 @@ static void build_return_columns(result_builder_t *rb, cbm_return_clause_t *ret)
41154115
static void execute_return_simple(cbm_return_clause_t *ret, binding_t *bindings, int bind_count,
41164116
int max_rows, result_builder_t *rb) {
41174117
int proj_cap = max_rows;
4118-
if (ret->limit > 0 && !ret->order_by && ret->skip <= 0) {
4118+
if (ret->limit > 0 && !ret->distinct && !ret->order_by && ret->skip <= 0) {
41194119
proj_cap = ret->limit;
41204120
}
41214121
for (int bi = 0; bi < bind_count && rb->row_count < proj_cap; bi++) {
@@ -4397,11 +4397,11 @@ static void execute_return_clause(cbm_query_t *q, cbm_return_clause_t *ret, bind
43974397
}
43984398
}
43994399

4400-
rb_apply_order_by(rb, ret);
4401-
rb_apply_skip_limit(rb, ret->skip, ret->limit >= 0 ? ret->limit : max_rows);
44024400
if (ret->distinct) {
44034401
rb_apply_distinct(rb);
44044402
}
4403+
rb_apply_order_by(rb, ret);
4404+
rb_apply_skip_limit(rb, ret->skip, ret->limit >= 0 ? ret->limit : max_rows);
44054405
}
44064406

44074407
static int execute_single(cbm_store_t *store, cbm_query_t *q, const char *project, int max_rows,

src/pipeline/artifact.c

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -389,9 +389,13 @@ static void ensure_gitattributes(const char *repo_path) {
389389
} else {
390390
FILE *fp = fdopen(fd, "w");
391391
if (fp) {
392+
/* Order matters: attributes apply left to right and the `binary`
393+
* macro expands to `-diff -merge -text`, so a trailing `binary`
394+
* unsets `merge=ours` and the conflict prevention this file
395+
* exists for never engages. The macro must come first. */
392396
(void)fputs("# Auto-generated by codebase-memory-mcp\n"
393397
"# Prevent merge conflicts on compressed artifact\n" CBM_ARTIFACT_FILENAME
394-
" merge=ours binary\n",
398+
" binary merge=ours\n",
395399
fp);
396400
(void)fclose(fp);
397401
} else {

tests/test_artifact.c

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -310,6 +310,20 @@ TEST(artifact_gitattributes_created) {
310310
struct stat st;
311311
ASSERT_EQ(stat(ga, &st), 0);
312312

313+
/* Attribute ORDER is load-bearing: gitattributes apply left to right and
314+
* the `binary` macro expands to `-diff -merge -text`, so a trailing
315+
* `binary` unsets a preceding `merge=ours` (git check-attr merge reports
316+
* "unset" and concurrent artifact refreshes produce binary conflicts
317+
* instead of auto-resolving). The driver must come after the macro. */
318+
FILE *gaf = fopen(ga, "r");
319+
ASSERT_NOT_NULL(gaf);
320+
char content[512] = {0};
321+
size_t rd = fread(content, 1, sizeof(content) - 1, gaf);
322+
(void)fclose(gaf);
323+
ASSERT_TRUE(rd > 0);
324+
ASSERT_NOT_NULL(strstr(content, CBM_ARTIFACT_FILENAME " binary merge=ours"));
325+
ASSERT_TRUE(strstr(content, "merge=ours binary") == NULL);
326+
313327
cleanup_dir(g_tmpdir);
314328
PASS();
315329
}

tests/test_cypher.c

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2528,6 +2528,52 @@ TEST(cypher_issue237_distinct_order_limit) {
25282528
PASS();
25292529
}
25302530

2531+
/* #873: duplicate projected rows must be deduped before ORDER BY + LIMIT */
2532+
TEST(cypher_issue873_distinct_order_limit_dedupes_before_limit) {
2533+
cbm_store_t *s = setup_cypher_store();
2534+
cbm_cypher_result_t r = {0};
2535+
int rc = cbm_cypher_execute(
2536+
s, "MATCH (n) RETURN DISTINCT n.label AS label ORDER BY label LIMIT 2", "test", 0, &r);
2537+
ASSERT_EQ(rc, 0);
2538+
ASSERT_NULL(r.error);
2539+
ASSERT_EQ(r.row_count, 2);
2540+
ASSERT_STR_EQ(r.rows[0][0], "Function");
2541+
ASSERT_STR_EQ(r.rows[1][0], "Module");
2542+
cbm_cypher_result_free(&r);
2543+
cbm_store_close(s);
2544+
PASS();
2545+
}
2546+
2547+
/* #873: early LIMIT must not truncate rows before DISTINCT for simple RETURN */
2548+
TEST(cypher_issue873_distinct_limit_dedupes_before_limit) {
2549+
cbm_store_t *s = setup_cypher_store();
2550+
cbm_cypher_result_t r = {0};
2551+
int rc = cbm_cypher_execute(
2552+
s, "MATCH (n) RETURN DISTINCT n.label AS label LIMIT 2", "test", 0, &r);
2553+
ASSERT_EQ(rc, 0);
2554+
ASSERT_NULL(r.error);
2555+
ASSERT_EQ(r.row_count, 2);
2556+
cbm_cypher_result_free(&r);
2557+
cbm_store_close(s);
2558+
PASS();
2559+
}
2560+
2561+
/* #873: SKIP is applied after DISTINCT and ORDER BY, not before dedupe */
2562+
TEST(cypher_issue873_distinct_order_skip_limit_dedupes_before_skip) {
2563+
cbm_store_t *s = setup_cypher_store();
2564+
cbm_cypher_result_t r = {0};
2565+
int rc = cbm_cypher_execute(
2566+
s, "MATCH (n) RETURN DISTINCT n.label AS label ORDER BY label SKIP 1 LIMIT 1", "test",
2567+
0, &r);
2568+
ASSERT_EQ(rc, 0);
2569+
ASSERT_NULL(r.error);
2570+
ASSERT_EQ(r.row_count, 1);
2571+
ASSERT_STR_EQ(r.rows[0][0], "Module");
2572+
cbm_cypher_result_free(&r);
2573+
cbm_store_close(s);
2574+
PASS();
2575+
}
2576+
25312577
/* #252: toInteger() */
25322578
TEST(cypher_issue252_tointeger) {
25332579
cbm_store_t *s = setup_cypher_store();
@@ -2669,6 +2715,9 @@ SUITE(cypher) {
26692715
RUN_TEST(cypher_exec_match_all_functions);
26702716
RUN_TEST(cypher_issue240_labels_function);
26712717
RUN_TEST(cypher_issue237_distinct_order_limit);
2718+
RUN_TEST(cypher_issue873_distinct_order_limit_dedupes_before_limit);
2719+
RUN_TEST(cypher_issue873_distinct_limit_dedupes_before_limit);
2720+
RUN_TEST(cypher_issue873_distinct_order_skip_limit_dedupes_before_skip);
26722721
RUN_TEST(cypher_issue252_tointeger);
26732722
RUN_TEST(cypher_issue305_count_star_alias);
26742723
RUN_TEST(cypher_exec_where_eq);

tests/test_php_lsp.c

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -543,6 +543,31 @@ TEST(phplsp_trait_method_flattened) {
543543
PASS();
544544
}
545545

546+
/* ── 23b. Regression: a trait that uses itself must terminate ─────
547+
*
548+
* `trait T { use T; ... }` — directly, or via an alias that resolves back
549+
* to T by short name (`use Other\T as A; use A;`) — previously sent
550+
* flatten_trait_into_class() into an unbounded loop: each copied method
551+
* re-matched the loop filter while cbm_registry_add_func() grew the
552+
* registry, exhausting all memory (40 GB+, freezing the host). The fix
553+
* short-circuits self-flattening and snapshots the loop bound. The
554+
* assertion is simply that extraction returns: pre-fix this never
555+
* completed, and under the ASan test build a regression would surface as
556+
* an OOM/abort here rather than a silent hang. */
557+
TEST(phplsp_trait_self_use_terminates) {
558+
const char *src =
559+
"<?php\n"
560+
"namespace App;\n"
561+
"trait EnumTrait {\n"
562+
" use EnumTrait;\n"
563+
" public function getRandom(): int { return 1; }\n"
564+
"}\n";
565+
CBMFileResult *r = extract_php(src);
566+
ASSERT(r);
567+
cbm_free_result(r);
568+
PASS();
569+
}
570+
546571
/* ── 24. Match expression result type ──────────────────────────── */
547572

548573
TEST(phplsp_match_result_type) {
@@ -5160,6 +5185,7 @@ SUITE(php_lsp) {
51605185
RUN_TEST(phplsp_phpdoc_property_class_tag);
51615186
RUN_TEST(phplsp_phpdoc_method_class_tag);
51625187
RUN_TEST(phplsp_trait_method_flattened);
5188+
RUN_TEST(phplsp_trait_self_use_terminates);
51635189
RUN_TEST(phplsp_match_result_type);
51645190
RUN_TEST(phplsp_ternary_result_type);
51655191
RUN_TEST(phplsp_method_chain_depth_three);

0 commit comments

Comments
 (0)