Files
codebase-memory-mcp/tests/test_language_count_contract.sh
T
Martin Vogelanddarkverbito 706bb2ce81 feat(lang): add Chialisp support with a first-party tree-sitter grammar
Chialisp (.clsp/.clib/.clinc) is the Chia smart-coin language. The only
public grammar, Quexington/tree-sitter-chialisp, cannot parse it: on the five
reference files from chia-blockchain@main it produced 3-34 ERROR nodes each.
Three defects account for all of them, and each desynchronised the rest of the
file rather than losing one node:

  * comments were `/;.*\r\n/` -- CRLF-only, while real files are LF;
  * `(include foo.clib)` was rejected because of the dot in the filename;
  * `(defconstant NAME <expr>)` accepted only a primitive after the name.

So the grammar is written here instead, in tools/tree-sitter-chialisp/, and
files as first-party: its vendored LICENSE is the project's own, byte-identical
to the repository root, carrying no third-party copyright.

It is deliberately GENERIC -- source_file / list / symbol / string / number /
hex / dot / comment, modelling the clvm_tools reader rather than the Chialisp
form vocabulary. `mod`, `defun`, `defconstant` and `include` are ordinary head
symbols; which lists are definitions is decided in extract_defs.c. A dialect
that adds a form (cl-21 -> cl-23 -> cl-26 each added several) therefore needs
no regenerated parser. Acceptance: all five reference files parse with zero
ERROR and zero MISSING nodes; the three defects above are pinned as tests.

Extraction, and the four decisions in it:

  * `Constant` is admitted to the cross-file name registry
    (cbm_label_is_registry_symbol). Chialisp's condition codes live as
    `(defconstant ...)` in a shared .clib and are referenced by name from every
    puzzle; labelled but unregistered they are seeded nowhere, and roughly half
    of a Chialisp graph would be present as nodes yet never the target of an
    edge. All three registry-seeding sites route through that one predicate, so
    they cannot diverge. No other language emits `Constant`, so the blast radius
    is exactly Chialisp. Pinned by a two-file graph test that goes red when the
    label is removed from the predicate.
  * `export` / `namespace` are NOT definition heads. `(export foo)` names a
    function already defined in the same file, so treating it as a def head
    mints a second node for one symbol. They stay in the not-a-call filter.
  * A parameter list is a `list` too, but it binds rather than invokes. Without
    that gate `(defun check_conditions (HEIGHTLOCK conditions) ...)` mints a
    phantom call to HEIGHTLOCK, and real Chialisp is mostly such definitions.
  * `(include *standard-cl-26*)` is a dialect directive, not a file dependency;
    recording it would invent a dependency on a file that does not exist.

Three helpers that the defs, calls and unified walks all need -- the quote-data
gate, comment-skipping named-child access, and the Chialisp def-head set -- live
in helpers.c rather than being copied per translation unit. They previously
existed as byte-identical duplicates that had already drifted: extract_defs.c
skipped comments when reading a def head while its mirror in extract_unified.c
did not, so a comment between a head and its name desynchronised defs from call
scope. Both now use the same accessor.

Ledger: 161 -> 162 grammars across the seven published surfaces, ABI-14 86 -> 87,
first-party 13 -> 14, CBM_LANG_COUNT 164 -> 165.

Distilled from PR #919 by darkverbito, whose extraction logic -- the CLVM
not-a-call vocabulary, the quoted-data gate, the filename-derived `mod` name and
the include/embed import handling -- is kept and is the substance of this change.
The grammar that PR vendored is not used.

Co-authored-by: darkverbito <8049985+darkverbito@users.noreply.github.com>
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-08-28 21:30:06 +02:00

132 lines
5.6 KiB
Bash

#!/usr/bin/env bash
# The published language count IS the number of vendored tree-sitter grammars,
# and every public surface must agree with it.
#
# Why this exists: the count was hand-maintained on seven separate surfaces, so
# it drifted apart from reality AND from itself — README/docs/npm said 158 while
# server.json/nuspec/package-release.sh said 159.
#
# Why grammars and not CBM_LANG_COUNT: the registry contains entries with no
# parser at all. CBM_LANG_NIM (grammar removed 2026-06-12) and
# CBM_LANG_OBJECTSCRIPT_EXPORT have no grammar directory, no tree_sitter
# function and no MANIFEST row, yet files still route to them by extension.
# Counting them would publish languages we do not parse. A vendored grammar
# directory, by contrast, demonstrably exists and is checksum-gated.
#
# Known and accepted: this UNDERCOUNTS languages that share one grammar
# (TypeScript/TSX, objectscript_udl/_routine). We publish the number we can
# prove rather than the larger number we cannot.
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
cd "$ROOT"
GRAMMAR_DIR="internal/cbm/vendored/grammars"
# --- 1. The truth: count the vendored grammar directories. ---
expected=$(find "$GRAMMAR_DIR" -mindepth 1 -maxdepth 1 -type d | wc -l | tr -d ' ')
# A parse that silently returns nothing would make every check below vacuous.
if ((expected < 100 || expected > 500)); then
echo "FAIL: counted $expected grammar directories under $GRAMMAR_DIR —" \
"the layout changed and this contract can no longer read it" >&2
exit 1
fi
# --- 2. MANIFEST.md must already agree; it is the human-facing ledger. ---
manifest_count=$(
grep -oE '^- Grammars: \*\*[0-9]+\*\*' "$GRAMMAR_DIR/MANIFEST.md" |
grep -oE '[0-9]+' || true
)
if [[ "$manifest_count" != "$expected" ]]; then
echo "FAIL: $GRAMMAR_DIR/MANIFEST.md declares ${manifest_count:-no} grammars," \
"but $expected directories exist" >&2
exit 1
fi
# --- 3. Every published surface must claim exactly that number. ---
SURFACES=(
README.md
docs/index.html
docs/llms.txt
pkg/npm/README.md
pkg/chocolatey/codebase-memory-mcp.nuspec
scripts/package-release.sh
server.json
)
# Files that legitimately say "<N> languages" about something OTHER than the
# product's registry size. Each entry states what its number actually counts,
# so an unexplained exemption cannot hide here.
EXEMPT=(
'docs/EVALUATION_PLAN.md' # historical plan, pinned to its authoring date
'docs/BENCHMARK.md' # benchmark corpus subsets (63/17 scored)
'scripts/clone-bench-repos.sh' # bench repo tiers (44 + 22)
'tests/repro/repro_invariant_breadth.c' # breadth of one invariant (27/27)
'tests/repro/repro_grammar_scripting.c' # scripting-grammar probe scope (12)
'tests/test_grammar_probe_b.c' # grammar probe scope (12)
'tests/test_language_count_contract.sh' # this file, which names the numbers above
'pkg/winget/manifests/' # version-pinned published manifests: 0.8.1
# really did ship 155, and rewriting a
# released manifest would falsify it
)
failures=0
distinct=0
for path in "${SURFACES[@]}"; do
if [[ ! -f "$path" ]]; then
echo "FAIL: registered surface $path does not exist — update SURFACES" >&2
failures=$((failures + 1))
continue
fi
# Two claim forms. Prose says "<N> languages"; a shields.io badge says
# "languages-<N>-colour". The badge was MISSED by the first version of this
# contract: it gated only the prose form, so README's badge sat three behind
# the prose in the same file while this test reported success. The lesson is
# that the enumeration and the gate must not share a regex — if they do, the
# gate can only ever confirm what the enumeration already saw.
hits=$( { grep -oE '[0-9]{2,3} languages' "$path" | grep -oE '^[0-9]+'
grep -oiE 'languages-[0-9]{2,3}' "$path" | grep -oE '[0-9]+$'
} | sort -u || true)
if [[ -z "$hits" ]]; then
echo "FAIL: $path is registered as a language-count surface but states no" \
"count — either it lost the claim (drop it from SURFACES) or the" \
"wording changed and this check went vacuous" >&2
failures=$((failures + 1))
continue
fi
while IFS= read -r n; do
distinct=$((distinct + 1))
if [[ "$n" != "$expected" ]]; then
echo "FAIL: $path claims $n languages, $expected grammars are vendored" >&2
failures=$((failures + 1))
fi
done <<<"$hits"
done
# --- 4. No UNREGISTERED file may make the claim. ---
# Without this, adding an eighth surface reintroduces exactly the drift this
# contract exists to prevent, and every check above would still pass.
while IFS= read -r path; do
[[ -z "$path" ]] && continue
for known in "${SURFACES[@]}"; do
[[ "$path" == "$known" ]] && continue 2
done
for skip in "${EXEMPT[@]}"; do
[[ "$path" == "$skip"* ]] && continue 2
done
echo "FAIL: $path states a language count but is not registered. Add it to" \
"SURFACES so it stays in step with the registry, or to EXEMPT with a" \
"note saying what its number actually counts." >&2
failures=$((failures + 1))
done < <(git grep -IlE '[0-9]{2,3} languages|languages-[0-9]{2,3}' || true)
if ((failures > 0)); then
echo "FAIL: $failures language-count contract violation(s)" >&2
exit 1
fi
echo "Language-count contract passed (${#SURFACES[@]} surfaces, $distinct distinct value(s) checked, $expected grammars vendored)"