Repository navigation
refactor(cypher): port the cypher module to Zig - #109
Merged
Merged
Conversation
Move the Cypher lexer, recursive-descent parser and AST free functions from cypher.c to src/cypher/parser.zig. cypher.h stays the ABI: the exports (cbm_lex, cbm_lex_free, cbm_parse, cbm_parse_free, cbm_query_free, cbm_cypher_parse) keep their signatures, the AST is built from the translated C structs with libc allocation, and every error string is unchanged. cypher.c keeps the executor for now, with small static copies of heap_strdup, cyp_ci_eq and is_scalar_value_func, which the executor also uses. Signed-off-by: ata-sesli <atasesli@protonmail.com>
Move the rest of cypher.c to src/cypher/cypher.zig and delete the C file: pattern scan and expansion, WHERE evaluation, WITH and RETURN projection, aggregation, ORDER BY/SKIP/LIMIT, DISTINCT, UNION, the cross join, the scope and variable-capacity checks, the deadline and the result builder. The exports (cbm_cypher_execute, cbm_cypher_result_free, cbm_cypher_classify_plan, cbm_cypher_cross_join_alloc and the two deadline test hooks) keep their cypher.h signatures and are checked against the header at compile time. Behaviour is kept byte for byte: %g/%lld formatting and number parsing still go through libc, fixed buffers keep their sizes, error and warning text is unchanged, and C narrowing becomes @truncate so the ReleaseSafe test library does not panic where C wrapped. parser.zig exports the shared helpers (CStr, ix, the allocation wrappers) to the executor. src/cypher/ no longer has any C source. Signed-off-by: ata-sesli <atasesli@protonmail.com>
edgeProp extracts each edge property into a ring of eight scratch buffers before the value is copied into a row. The ring and its index were process-wide (a plain static in the C engine, ported unchanged), so queries executing on different threads wrote into each other's slots and a row could pick up another query's value. The node property ring was already thread-local; the edge ring now is too. The new test runs the same edge-property query on four threads, each against its own store with a distinct url_path. Before the fix it saw 796 foreign values out of 768,000; after it, none. Signed-off-by: ata-sesli <atasesli@protonmail.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
This moves the Cypher query engine (
src/cypher/) from C to Zig, following the same approach as the earlier git, simhash, watcher and discover ports:cypher.hstays the interface, including every AST, token and result struct layout, and the Zig files export the samecbm_*functions. No caller changes. After this PR,src/cypher/has no.cfiles.Queries should return exactly what they returned before: the same rows, columns, warnings and error text. The third commit fixes one existing bug in the engine, a data race on edge property values, described below.
Commits
Each commit builds and passes the test suite on its own.
refactor(cypher): port the lexer and parser to Zig. Moves the lexer, the recursive-descent parser and the AST free functions tosrc/cypher/parser.zig. The exports arecbm_lex,cbm_lex_free,cbm_parse,cbm_parse_free,cbm_query_freeandcbm_cypher_parse. The AST is still made of the C structs fromcypher.hand allocated with libc, because C callers and tests read and free it.cypher.ckeeps the executor for this commit, with small static copies of the three helpers it shares with the parser.refactor(cypher): port the executor to Zig. Moves the rest ofcypher.ctosrc/cypher/cypher.zigand deletes the C file. This covers:cbm_regcomp/cbm_regexecwrappers;The exports are
cbm_cypher_execute,cbm_cypher_result_free,cbm_cypher_classify_plan,cbm_cypher_cross_join_allocand the two deadline test hooks.fix(cypher): keep the edge property buffers per thread. See "Bug fix" below.Build wiring:
build.zigandMakefile.cbmdropcypher.cand list the Zig files inCBM_ZIG_SRCS.src/cbm_zig.zigrunscheckExportson both modules, so a signature that drifts fromcypher.hfails the build.src/cbm_zig_c.hadds the headers the engine needs (cypher/cypher.h,foundation/compat_regex.h,foundation/limits.h).How output is kept identical
%g,%.10g,%lld) still goes through libcsnprintf. Zig's formatter has no%gand prints floats differently.strtol/strtoll/strtod/atoi, which accept input that Zig's parsers treat differently (leading whitespace, partial numbers,inf).@truncate, and arithmetic that C lets wrap uses wrapping operators. Since build: link test runners against a ReleaseSafe Zig library #108 the test runners link a ReleaseSafe Zig library, so a missed case would panic in the tests instead of silently differing.Bug fix: edge property values could leak between concurrent queries
When a query reads an edge property such as
r.url_path, the engine first copies the value into one of eight rotating scratch buffers and then copies it into the result row. The node property buffers were thread-local, but the edge property buffers and their index were a plain process-widestaticin the C code. Two queries running on different threads could therefore write into the same buffer, and a row could end up with the other query's value.The second commit ports this unchanged, so it stays a true port. The third commit makes the edge buffers thread-local, like the node buffers.
The new test
cypher_edge_prop_concurrent_queries_isolatedruns the same edge-property query 3,000 times on each of four threads, each against its own store with a differenturl_path. Before the fix, 796 of the 768,000 values read came from another thread. After the fix, none did. The test adds about 6 seconds to thecyphersuite.Verification
All runs were on macOS arm64.
Full test suite with
CBM_TEST_TIERS=all: 6,447 passed onmain(665f1d5) and after each of the first two commits. After the third commit, 6,448 passed, which is the same tests plus the new one.Focused suites after the fix:
cypher221,security34,zova_plugin8 andzova39 passed. These are the suites that call the query engine.Old vs new query output. I built
mainand this branch as separate CLI binaries and indexed this repository's source tree once for each, each binary with its own cache. Then I ran 352 queries throughcli query_graphon both binaries, one process at a time. The queries were:test_cypher.c,test_security.cand the cypher repro tests);cypher.hand each unsupported keyword.349 responses were byte-identical, apart from
elapsed_ms, and none differed. The other 3 queries match every Module against every Function with nothing linking them, so they produce very large results. They passed the 2 GB memory cap I set for the comparison on both binaries, so their output was not compared.Timing. These are 12 heavy queries on the same index, run 5 times each with old and new alternating. The table shows median wall time per CLI process.
MATCH (n:Project) RETURN n.name(process start-up baseline)MATCH (a)-[:CALLS*1..4]->(b) RETURN count(b)MATCH (a:Function)-[:CALLS*]->(b:Function) RETURN count(DISTINCT b.name)Functioncountwith ORDER BYMATCH (n) … ORDER BY … LIMIT 5000MATCH (a:File), (b:Folder) RETURN count(*)type(r)collectRETURN DISTINCTwith ORDER BYCALLScountIn one of the five pairs of
CALLS*1..4runs, the two outputs differed. A separate run of each binary gave identical output: the same count and the same "traversal budget was exhausted" warning. That query runs for 26–29 seconds against the engine's 30-second wall-clock deadline, so the likely cause is that one run hit the deadline. I did not confirm this, because the outputs of those runs were not saved.Cross-compile of
libcbm_zig.asucceeded for x86_64 and aarch64 Linux (gnu), x86_64 Windows (gnu) and aarch64 macOS. All 12 cypher exports were present in each archive.Header drift check: I temporarily changed the first parameter of
cbm_cypher_cross_join_allocincypher.htolong. The build failed withcbm_cypher_cross_join_alloc: parameter 0 is c_int in Zig but c_long in its C header. The change was then reverted.Lint: clang-format-diff (clang-format 22.1.8) is clean on the changed lines of
tests/test_cypher.c.zig fmt --check,git diff --checkandtests/test_github_workflows.shall pass.Limits
tests/repro/repro_issue627.candtests/repro/repro_new_cypher_limit_zero.cstill citecypher.cline numbers. They are left as they are, as in the discover port.