Skip to content

Commit 1a5dc6d

Browse files
committed
Add Chialisp (cl-26) language support
Chialisp is the smart-contract language of the Chia blockchain — an s-expression language compiled to CLVM. This adds it as a first-class indexed language. Grammar: a clean-room tree-sitter grammar (github.com/irulast/tree-sitter-chialisp, MIT) — a generic s-expression core (list/symbol/number/hex/string/comment) authored against the clvm_tools_rs compiler frontend; parses the full cl-26 surface (mod / defun / defmacro / defconstant / assign / lambda / include / embed-file / namespace / ...). Wiring mirrors the existing Scheme lisp-family path (list/symbol nodes; zero new extractor functions): - CBM_LANG_CHIALISP enum + lang_specs row; .clsp/.clib/.clinc extension mapping. - CBM_LANG_CHIALISP added to the four lisp-family extractor gates (extract_defs / extract_calls / extract_imports / extract_unified). Chialisp-correct graph extraction: - def heads: defun, defun-inline, defmacro, defmac, defconstant, defconst, mod, namespace, export, embed-file, compile-file. - mod/export named by filename (their child(1) is the arg list, not a name). - CLVM primitive operators (the 49-entry clvm_tools_rs keyword table) filtered from CALLS, so (sha256 ...) / (+ ...) / (c ...) don't create phantom edges; sha256tree (a real defun) is kept. - dialect sigils (include *standard-cl-26*) are not import edges (they select a dialect, not a file); (include "x.clib") is. - quoted bodies (q ...)/(quote ...)/(qq ...) are data, not descended. - embed-file/compile-file emit a Constant node + a file-dependency edge. Verified against a 313-file cl-26 corpus: 514 Functions, 469 Constants; CALLS to real helpers only (zero primitive phantoms); imports to real .clib only (zero dialect-sigil phantoms). Clean -Werror build; Scheme/Clojure/CommonLisp regressions unchanged. MANIFEST.md updated (grammar entry + custom-handling row).
1 parent f092d92 commit 1a5dc6d

14 files changed

Lines changed: 1498 additions & 22 deletions

File tree

internal/cbm/cbm.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,7 @@ typedef enum {
170170
CBM_LANG_QML, // Qt QML (Qt Modeling Language — declarative UI + embedded JS)
171171
CBM_LANG_CFSCRIPT, // CFML script dialect (.cfc components — Lucee/ColdFusion)
172172
CBM_LANG_CFML, // CFML tag dialect (.cfm templates — Lucee/ColdFusion)
173+
CBM_LANG_CHIALISP, // Chialisp (.clsp/.clib/.clinc — Chia smart-coin s-expression language)
173174
CBM_LANG_COUNT
174175
} CBMLanguage;
175176

internal/cbm/extract_calls.c

Lines changed: 70 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -494,7 +494,63 @@ static char *extract_erlang_callee(CBMArena *a, TSNode node, const char *source,
494494
// Lisp dialects: a call is a list (`list` / `list_lit`) whose head (first named
495495
// child) is the function symbol (`symbol` / `sym_lit`). Generic field/first-child
496496
// extraction misses it because the head is not an `identifier` node.
497-
static char *extract_lisp_callee(CBMArena *a, TSNode node, const char *source, const char *nk) {
497+
/* Chialisp W2: a head atom that is a CLVM primitive/opcode, or a Chialisp
498+
* syntax/binding/intrinsic/def keyword, is NOT a call — emit no CALLS edge.
499+
* Sourced from clvm_tools_rs KW_PAIRS / clvm_rs operator set (pinned in #38).
500+
* The def/include heads are here too so `(defun …)`/`(include …)` heads never
501+
* mint a phantom call. Real helpers that merely *look* primitive — sha256tree
502+
* and the curry helpers — are deliberately absent, so they pass through. */
503+
static bool chialisp_head_is_not_call(const char *t) {
504+
if (!t) {
505+
return true;
506+
}
507+
static const char *filtered[] = {
508+
/* --- 49 CLVM primitives (VM ops) --- */
509+
"q", "a", "i", "c", "f", "r", "l", "x", "=", ">s", "sha256", "substr", "strlen", "concat",
510+
"+", "-", "*", "/", "divmod", ">", "ash", "lsh", "logand", "logior", "logxor", "lognot",
511+
"point_add", "pubkey_for_exp", "not", "any", "all", "softfork", "coinid", "g1_subtract",
512+
"g1_multiply", "g1_negate", "g2_add", "g2_subtract", "g2_multiply", "g2_negate", "g1_map",
513+
"g2_map", "bls_pairing_identity", "bls_verify", "modpow", "%", "secp256k1_verify",
514+
"secp256r1_verify", "keccak256",
515+
/* --- Chialisp syntax / binding / intrinsics (not calls or defs) --- */
516+
"quote", "qq", "unquote", "&rest", "let", "let*", "assign", "assign-inline",
517+
"assign-lambda", "lambda", "mod", "if", "list", "com", "opt", "@", "@*env*", "print",
518+
/* --- def / include heads (own their own node kinds, never a call) --- */
519+
"defun", "defun-inline", "defmacro", "defmac", "defconstant", "defconst", "namespace",
520+
"export", "embed-file", "compile-file", "include", NULL};
521+
for (int i = 0; filtered[i]; i++) {
522+
if (strcmp(t, filtered[i]) == 0) {
523+
return true;
524+
}
525+
}
526+
return false;
527+
}
528+
529+
/* W6: true when any ancestor list of `node` has a quote head (`q`/`quote`/`qq`)
530+
* — quoted data must not mint calls. */
531+
static bool chialisp_node_in_quote(CBMArena *a, TSNode node, const char *source) {
532+
TSNode cur = ts_node_parent(node);
533+
for (int guard = 0; guard < 256 && !ts_node_is_null(cur); guard++) {
534+
const char *ck = ts_node_type(cur);
535+
if ((strcmp(ck, "list") == 0 || strcmp(ck, "list_lit") == 0) &&
536+
ts_node_named_child_count(cur) > 0) {
537+
TSNode h = ts_node_named_child(cur, 0);
538+
const char *hk = ts_node_type(h);
539+
if (strcmp(hk, "symbol") == 0 || strcmp(hk, "sym_lit") == 0) {
540+
char *ht = cbm_node_text(a, h, source);
541+
if (ht && (strcmp(ht, "q") == 0 || strcmp(ht, "quote") == 0 ||
542+
strcmp(ht, "qq") == 0)) {
543+
return true;
544+
}
545+
}
546+
}
547+
cur = ts_node_parent(cur);
548+
}
549+
return false;
550+
}
551+
552+
static char *extract_lisp_callee(CBMArena *a, TSNode node, const char *source, const char *nk,
553+
CBMLanguage lang) {
498554
if (strcmp(nk, "list") != 0 && strcmp(nk, "list_lit") != 0) {
499555
return NULL;
500556
}
@@ -503,7 +559,16 @@ static char *extract_lisp_callee(CBMArena *a, TSNode node, const char *source, c
503559
const char *hk = ts_node_type(head);
504560
if (strcmp(hk, "symbol") == 0 || strcmp(hk, "sym_lit") == 0 ||
505561
strcmp(hk, "identifier") == 0) {
506-
return cbm_node_text(a, head, source);
562+
char *ht = cbm_node_text(a, head, source);
563+
if (lang == CBM_LANG_CHIALISP) {
564+
/* W2: drop CLVM ops / syntax / def-heads. W6: drop anything
565+
* inside quoted data. Real helpers (sha256tree, curry helpers)
566+
* survive both filters. */
567+
if (chialisp_head_is_not_call(ht) || chialisp_node_in_quote(a, node, source)) {
568+
return NULL;
569+
}
570+
}
571+
return ht;
507572
}
508573
}
509574
return NULL;
@@ -1061,8 +1126,9 @@ static char *extract_callee_lang_specific(CBMArena *a, TSNode node, const char *
10611126
}
10621127

10631128
if (lang == CBM_LANG_CLOJURE || lang == CBM_LANG_COMMONLISP || lang == CBM_LANG_SCHEME ||
1064-
lang == CBM_LANG_FENNEL || lang == CBM_LANG_RACKET || lang == CBM_LANG_EMACSLISP) {
1065-
return extract_lisp_callee(a, node, source, nk);
1129+
lang == CBM_LANG_FENNEL || lang == CBM_LANG_RACKET || lang == CBM_LANG_EMACSLISP ||
1130+
lang == CBM_LANG_CHIALISP) {
1131+
return extract_lisp_callee(a, node, source, nk, lang);
10661132
}
10671133
if (lang == CBM_LANG_FSHARP) {
10681134
return extract_fsharp_callee(a, node, source, nk);

internal/cbm/extract_defs.c

Lines changed: 149 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6024,36 +6024,157 @@ static bool lisp_is_def_head(const char *t) {
60246024
return false;
60256025
}
60266026

6027+
/* Chialisp (cl-26) definition-form heads. Distinct from the Clojure/Scheme
6028+
* lisp_is_def_head set because Chialisp shares the generic `list` node kind:
6029+
* treating e.g. `mod`/`defun`/`defconstant` as defs must NOT leak into the
6030+
* other lisp gates (in Scheme `(mod x y)` is a call, not a def). Excludes the
6031+
* expression-local binding forms (let, assign, lambda and friends) which, if
6032+
* made defs, would fragment call attribution. Mirror: chialisp_head_is_def()
6033+
* in extract_unified.c. */
6034+
static bool chialisp_is_def_head(const char *t) {
6035+
if (!t) {
6036+
return false;
6037+
}
6038+
static const char *heads[] = {"mod",
6039+
"defun",
6040+
"defun-inline",
6041+
"defmacro",
6042+
"defmac",
6043+
"defconstant",
6044+
"defconst",
6045+
"namespace",
6046+
"export",
6047+
"embed-file",
6048+
"compile-file",
6049+
NULL};
6050+
for (int i = 0; heads[i]; i++) {
6051+
if (strcmp(t, heads[i]) == 0) {
6052+
return true;
6053+
}
6054+
}
6055+
return false;
6056+
}
6057+
6058+
/* Chialisp W6: a `(q ...)`/`(quote ...)`/`(qq ...)` form is quoted DATA, not
6059+
* code — a def-head or call symbol inside it must not mint a node. True when
6060+
* any ancestor list of `node` has a quote head. Bounded walk (arena for the
6061+
* head-text read). */
6062+
static bool lisp_node_in_quote(CBMArena *a, TSNode node, const char *source) {
6063+
TSNode cur = ts_node_parent(node);
6064+
for (int guard = 0; guard < 256 && !ts_node_is_null(cur); guard++) {
6065+
const char *ck = ts_node_type(cur);
6066+
if ((strcmp(ck, "list") == 0 || strcmp(ck, "list_lit") == 0) &&
6067+
ts_node_named_child_count(cur) > 0) {
6068+
TSNode h = ts_node_named_child(cur, 0);
6069+
const char *hk = ts_node_type(h);
6070+
if (strcmp(hk, "symbol") == 0 || strcmp(hk, "sym_lit") == 0) {
6071+
char *ht = cbm_node_text(a, h, source);
6072+
if (ht && (strcmp(ht, "q") == 0 || strcmp(ht, "quote") == 0 ||
6073+
strcmp(ht, "qq") == 0)) {
6074+
return true;
6075+
}
6076+
}
6077+
}
6078+
cur = ts_node_parent(cur);
6079+
}
6080+
return false;
6081+
}
6082+
6083+
/* i-th named child skipping `comment` nodes (comments are NAMED in the
6084+
* Chialisp/Scheme grammar and count in named-child indexing). Cheap insurance
6085+
* for a comment interleaved between a def head and its name. */
6086+
static TSNode lisp_named_child_skip_comments(TSNode node, uint32_t want) {
6087+
uint32_t nc = ts_node_named_child_count(node);
6088+
uint32_t seen = 0;
6089+
for (uint32_t i = 0; i < nc; i++) {
6090+
TSNode c = ts_node_named_child(node, i);
6091+
if (strcmp(ts_node_type(c), "comment") == 0) {
6092+
continue;
6093+
}
6094+
if (seen == want) {
6095+
return c;
6096+
}
6097+
seen++;
6098+
}
6099+
TSNode null_node = {0};
6100+
return null_node;
6101+
}
6102+
6103+
/* Basename stem (no dir, no extension) of a path, into an arena string. */
6104+
static char *lisp_path_stem(CBMArena *a, const char *path) {
6105+
if (!path) {
6106+
return NULL;
6107+
}
6108+
const char *slash = strrchr(path, '/');
6109+
const char *base = slash ? slash + 1 : path;
6110+
const char *dot = strrchr(base, '.');
6111+
size_t len = (dot && dot != base) ? (size_t)(dot - base) : strlen(base);
6112+
return cbm_arena_strndup(a, base, len);
6113+
}
6114+
60276115
static void extract_lisp_def(CBMExtractCtx *ctx, TSNode node) {
60286116
CBMArena *a = ctx->arena;
6117+
bool chialisp = (ctx->language == CBM_LANG_CHIALISP);
60296118
if (ts_node_named_child_count(node) < 2) {
60306119
return;
60316120
}
6032-
char *head = cbm_node_text(a, ts_node_named_child(node, 0), ctx->source);
6033-
if (!lisp_is_def_head(head)) {
6121+
TSNode head_node = chialisp ? lisp_named_child_skip_comments(node, 0) : ts_node_named_child(node, 0);
6122+
if (ts_node_is_null(head_node)) {
60346123
return;
60356124
}
6036-
TSNode target = ts_node_named_child(node, 1);
6037-
const char *tk = ts_node_type(target);
6038-
TSNode name_node = target;
6039-
// (define (foo args) ...) — the name is the head symbol of the nested list.
6040-
if ((strcmp(tk, "list") == 0 || strcmp(tk, "list_lit") == 0) &&
6041-
ts_node_named_child_count(target) > 0) {
6042-
name_node = ts_node_named_child(target, 0);
6125+
char *head = cbm_node_text(a, head_node, ctx->source);
6126+
bool is_def = chialisp ? chialisp_is_def_head(head) : lisp_is_def_head(head);
6127+
if (!is_def) {
6128+
return;
60436129
}
6044-
if (ts_node_is_null(name_node)) {
6130+
/* W6: skip def-heads that live inside quoted data (cl-26 macros embed
6131+
* puzzle-shaped literals under `(q ...)`). */
6132+
if (chialisp && lisp_node_in_quote(a, node, ctx->source)) {
60456133
return;
60466134
}
6047-
char *name = cbm_node_text(a, name_node, ctx->source);
6135+
/* W1: a top-level `(mod (ARGS) ...)` is the puzzle entry point; its
6136+
* named_child(1) is the arg-LIST, so the generic nested-name path would
6137+
* name the module after the first curried arg. Name it by the filename. */
6138+
char *name = NULL;
6139+
if (chialisp && strcmp(head, "mod") == 0) {
6140+
name = lisp_path_stem(a, ctx->rel_path);
6141+
} else {
6142+
TSNode target = chialisp ? lisp_named_child_skip_comments(node, 1) : ts_node_named_child(node, 1);
6143+
if (ts_node_is_null(target)) {
6144+
return;
6145+
}
6146+
const char *tk = ts_node_type(target);
6147+
TSNode name_node = target;
6148+
// (define (foo args) ...) — the name is the head symbol of the nested list.
6149+
if ((strcmp(tk, "list") == 0 || strcmp(tk, "list_lit") == 0) &&
6150+
ts_node_named_child_count(target) > 0) {
6151+
name_node = ts_node_named_child(target, 0);
6152+
}
6153+
if (ts_node_is_null(name_node)) {
6154+
return;
6155+
}
6156+
name = cbm_node_text(a, name_node, ctx->source);
6157+
}
60486158
if (!name || !name[0]) {
60496159
return;
60506160
}
60516161
/* struct/record/type defining forms produce a type node, not a callable
60526162
* (Racket `(struct point ...)`, Clojure `(defrecord ...)`, etc.). */
60536163
const char *lisp_label = "Function";
6054-
if (strcmp(head, "struct") == 0 || strcmp(head, "define-struct") == 0 ||
6055-
strcmp(head, "define-record-type") == 0 || strcmp(head, "defrecord") == 0 ||
6056-
strcmp(head, "deftype") == 0) {
6164+
if (chialisp) {
6165+
/* W3/W4 label map: constants, macros, module entry, embedded artifacts. */
6166+
if (strcmp(head, "mod") == 0 || strcmp(head, "export") == 0 ||
6167+
strcmp(head, "namespace") == 0) {
6168+
lisp_label = "Module";
6169+
} else if (strcmp(head, "defconstant") == 0 || strcmp(head, "defconst") == 0 ||
6170+
strcmp(head, "embed-file") == 0 || strcmp(head, "compile-file") == 0) {
6171+
lisp_label = "Constant";
6172+
} else if (strcmp(head, "defmacro") == 0 || strcmp(head, "defmac") == 0) {
6173+
lisp_label = "Macro";
6174+
}
6175+
} else if (strcmp(head, "struct") == 0 || strcmp(head, "define-struct") == 0 ||
6176+
strcmp(head, "define-record-type") == 0 || strcmp(head, "defrecord") == 0 ||
6177+
strcmp(head, "deftype") == 0) {
60576178
lisp_label = "Struct";
60586179
} else if (strcmp(head, "definterface") == 0 || strcmp(head, "defprotocol") == 0) {
60596180
lisp_label = "Interface";
@@ -6242,9 +6363,22 @@ static void walk_defs(CBMExtractCtx *ctx, TSNode root, const CBMLangSpec *spec,
62426363
}
62436364

62446365
if ((ctx->language == CBM_LANG_CLOJURE || ctx->language == CBM_LANG_RACKET ||
6245-
ctx->language == CBM_LANG_SCHEME) &&
6366+
ctx->language == CBM_LANG_SCHEME || ctx->language == CBM_LANG_CHIALISP) &&
62466367
(strcmp(kind, "list") == 0 || strcmp(kind, "list_lit") == 0)) {
62476368
extract_lisp_def(ctx, node);
6369+
/* Chialisp nests every helper `(defun …)` inside the top-level
6370+
* `(mod …)` (and `.clib` files wrap defuns in an enclosing list —
6371+
* W7). The generic function_node_types match below fires on every
6372+
* `list` and `continue`s WITHOUT descending, so those nested defs
6373+
* would be lost. Push children and continue here so the whole tree
6374+
* is walked. Gated to Chialisp — other lisps keep prior behavior. */
6375+
if (ctx->language == CBM_LANG_CHIALISP) {
6376+
uint32_t cc = ts_node_child_count(node);
6377+
for (int i = (int)cc - SKIP_CHAR; i >= 0; i--) {
6378+
wd_push(&s, ts_node_child(node, (uint32_t)i), frame.enclosing_class_qn);
6379+
}
6380+
continue;
6381+
}
62486382
// fall through: descend into children so nested defs are captured too
62496383
}
62506384

internal/cbm/extract_imports.c

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1788,6 +1788,42 @@ static void lisp_process_list(CBMExtractCtx *ctx, TSNode node) {
17881788
return;
17891789
}
17901790

1791+
/* Chialisp (cl-26) include/embed forms. W3: `(include *standard-cl-26*)`
1792+
* and any `*…*` dialect sigil is a compiler directive, NOT a file
1793+
* dependency — mirror the compiler's own `!name.starts_with("*")` filter
1794+
* and drop it (record only real `(include "file.clib")` edges). W4:
1795+
* `(embed-file NAME kind "file")` / `(compile-file NAME "file")` create a
1796+
* file dependency on the embedded/compiled artifact — emit it as an import
1797+
* edge (no dedicated EMBEDS edge type; see task note). */
1798+
if (ctx->language == CBM_LANG_CHIALISP) {
1799+
if (strcmp(hn, "include") == 0) {
1800+
for (uint32_t j = 1; j < nc; j++) {
1801+
TSNode mod_node = ts_node_named_child(node, j);
1802+
const char *mk = ts_node_type(mod_node);
1803+
if (strcmp(mk, "symbol") == 0 || strcmp(mk, "sym_lit") == 0) {
1804+
char *sig = cbm_node_text(ctx->arena, mod_node, ctx->source);
1805+
if (sig && sig[0] == '*') {
1806+
continue; /* dialect sigil, not a file */
1807+
}
1808+
}
1809+
lisp_push_module(ctx, mod_node);
1810+
}
1811+
return;
1812+
}
1813+
if (strcmp(hn, "embed-file") == 0 || strcmp(hn, "compile-file") == 0) {
1814+
/* the embedded/compiled path is the last string arg. */
1815+
for (uint32_t j = nc; j-- > 1;) {
1816+
TSNode arg = ts_node_named_child(node, j);
1817+
const char *ak = ts_node_type(arg);
1818+
if (strcmp(ak, "string") == 0 || strcmp(ak, "str_lit") == 0) {
1819+
lisp_push_module(ctx, arg);
1820+
break;
1821+
}
1822+
}
1823+
return;
1824+
}
1825+
}
1826+
17911827
/* Plain import head: `(require :util)`, `(import ...)`, `(use ...)`. */
17921828
if (strcmp(hn, "import") == 0 || strcmp(hn, "require") == 0 || strcmp(hn, "load") == 0 ||
17931829
strcmp(hn, "use") == 0 || strcmp(hn, "include") == 0) {
@@ -2852,6 +2888,7 @@ void cbm_extract_imports(CBMExtractCtx *ctx) {
28522888
case CBM_LANG_FENNEL:
28532889
case CBM_LANG_COMMONLISP:
28542890
case CBM_LANG_CLOJURE:
2891+
case CBM_LANG_CHIALISP:
28552892
parse_lisp_imports(ctx);
28562893
break;
28572894
case CBM_LANG_STARLARK:

0 commit comments

Comments
 (0)