feat(languages): add Lua language support

Add Lua as a supported language with symbol extraction and scope-resolution
at parity with existing languages.

Vendored grammar:
- Vendor tree-sitter-lua (v2.1.3, MIT). Upstream ships nan-based node
  bindings incompatible with the pinned tree-sitter@0.21.1 runtime
  (setLanguage rejects with "Invalid language object"), so the grammar C
  sources are rebuilt against a Napi binding adapted from tree-sitter-c.

Symbol extraction (legacy tree-sitter queries):
- Function, Method, and Class definitions; call sites.

Scope-resolution (CALLS + IMPORTS edges):
- emitScopeCaptures, interpretImport, and a Lua ScopeResolver registered
  in the scope-resolution pipeline.
- Import resolution via a SuffixIndex so root-level modules (e.g.
  require("middleclass")) resolve, not just dotted paths.

Class structure (EXTENDS + HAS_METHOD + METHOD_OVERRIDES):
- middleclass class("Name", Parent) and function Class:method() handled
  via a heritage edge hook (emitHeritageEdges), since middleclass has no
  syntactic class body for the standard heritage/ownership walk.

Registered in all dispatch tables (SupportedLanguages enum, language
detection, parser-loader, parse-worker languageMap, language
classification, scope-resolver registry, vendored-grammar manifest).

Verified: tsc --noEmit clean; analyze on a Lua sample produces Class,
Method, EXTENDS, HAS_METHOD, METHOD_OVERRIDES, IMPORTS, and CALLS.

Co-Authored-By: Claude Sonnet 4 <noreply@anthropic.com>
This commit is contained in:
taoxin 2026-07-30 18:09:11 +08:00 • committed by taoxin
parent c056d136ad
commit 6deba51db0
34 changed files with 13909 additions and 1 deletions

View file

@ -22,6 +22,11 @@
"proto": {
"name": "tree-sitter-proto",
"upstream": { "github": "coder3101/tree-sitter-proto" }
},
"lua": {
"name": "tree-sitter-lua",
"upstream": { "npm": "tree-sitter-lua" },
"hold": "tree-sitter-lua@2.1.3 ships nan-based node bindings incompatible with the pinned tree-sitter@0.21.1 runtime (setLanguage rejects with 'Invalid language object'); vendored with a Napi binding.cc rebuilt from source. Needs the tree-sitter runtime upgrade (#858) plus an upstream migration to node-addon-api before the monitor can auto-bump"
}
}
}

View file

@ -52,6 +52,7 @@ const EXTENSION_MAP: Record<SupportedLanguages, readonly string[]> = {
[SupportedLanguages.Swift]: ['.swift'],
[SupportedLanguages.Dart]: ['.dart'],
[SupportedLanguages.Vue]: ['.vue'],
[SupportedLanguages.Lua]: ['.lua'],
[SupportedLanguages.Cobol]: ['.cbl', '.cob', '.cpy', '.cobol'],
} satisfies Record<SupportedLanguages, readonly string[]>; // Ensure exhaustiveness
@ -120,6 +121,7 @@ const SYNTAX_MAP: Record<SupportedLanguages, string> = {
[SupportedLanguages.Swift]: 'swift',
[SupportedLanguages.Dart]: 'dart',
[SupportedLanguages.Vue]: 'typescript',
[SupportedLanguages.Lua]: 'lua',
[SupportedLanguages.Cobol]: 'cobol',
} satisfies Record<SupportedLanguages, string>; // Ensure exhaustiveness

View file

@ -20,6 +20,7 @@ export enum SupportedLanguages {
Swift = 'swift',
Dart = 'dart',
Vue = 'vue',
Lua = 'lua',
/** Standalone regex processor — no tree-sitter, no LanguageProvider. */
Cobol = 'cobol',
}

View file

@ -10,7 +10,9 @@
* - production: javascript, typescript, python, java, c, cpp, csharp, go,
* ruby, rust, php, kotlin, swift, dart
* - experimental: vue (embedded-language / SFC complexity),
* cobol (regex-provider path)
* cobol (regex-provider path),
* lua (definition-only legacy DAG path; scope-resolution
* hooks pending — Phase B)
* - quarantined: (none)
*/
@ -39,6 +41,7 @@ export const LanguageClassifications: Readonly<Record<SupportedLanguages, Langua
[SupportedLanguages.Kotlin]: 'production',
[SupportedLanguages.Swift]: 'production',
[SupportedLanguages.Dart]: 'production',
[SupportedLanguages.Lua]: 'experimental',
[SupportedLanguages.Vue]: 'experimental',
[SupportedLanguages.Cobol]: 'experimental',
};

View file

@ -44,6 +44,7 @@ const GRAMMARS = {
proto: { required: false, display: 'Proto', ext: '.proto' },
swift: { required: false, display: 'Swift', ext: '.swift' },
kotlin: { required: false, display: 'Kotlin', ext: '.kt/.kts' },
lua: { required: false, display: 'Lua', ext: '.lua' },
};
const skipOptional = process.env.GITNEXUS_SKIP_OPTIONAL_GRAMMARS === '1';

View file

@ -0,0 +1,10 @@
import { SupportedLanguages } from 'gitnexus-shared';
import type { CallExtractionConfig } from '../../call-types.js';
// Lua call extraction is fully handled by the generic createCallExtractor
// (derive calledName + callForm + receiver from @call/@call.name captures in
// LUA_QUERIES). No language-specific call shapes (no `::` method references),
// so the config carries only the language id.
export const luaCallConfig: CallExtractionConfig = {
language: SupportedLanguages.Lua,
};

View file

@ -240,5 +240,10 @@ export const swiftExportChecker: ExportChecker = (node, _name) => {
/** Ruby: all top-level definitions are public (no export syntax). */
export const rubyExportChecker: ExportChecker = (_node, _name) => true;
/** Lua: `function foo()` is global (reachable from other files via require/_G);
* `local function foo()` is module-private. */
export const luaExportChecker: ExportChecker = (node, _name) =>
node.type !== 'local_function_definition_statement';
/** Dart: public if no leading underscore (convention, same as Python). */
export const dartExportChecker: ExportChecker = (_node, name) => !name.startsWith('_');

View file

@ -0,0 +1,34 @@
/**
* Lua import resolution config.
*
* `require("a.b.c")` → module path `a/b/c`, resolved via suffixResolve
* (tries .lua then /init.lua — see EXTENSIONS in utils.ts). Mirrors
* rubyRequireStrategy but splits on `.` (Lua's module separator) and strips
* surrounding quotes (the @import.source capture is a string literal).
*
* NOTE: inert in Phase A. The legacy parse-worker skips @import captures
* (parse-worker.ts:1654) — IMPORTS edges are emitted by the scope-resolution
* phase once lua/query.ts + emitScopeCaptures land (Phase B). This config is
* provided because LanguageProvider.importResolver is a required field.
*/
import { SupportedLanguages } from 'gitnexus-shared';
import type { ImportResolutionConfig, ImportResolverStrategy } from '../types.js';
import { suffixResolve } from '../utils.js';
export const luaRequireStrategy: ImportResolverStrategy = (rawImportPath, _filePath, ctx) => {
const cleaned = rawImportPath.replace(/^["']|["']$/g, '');
const pathParts = cleaned.split('.').filter(Boolean);
if (pathParts.length === 0) return null;
const resolved = suffixResolve(
pathParts,
ctx.normalizedFileList,
ctx.allFileList,
ctx.index,
);
return resolved ? { kind: 'files', files: [resolved] } : null;
};
export const luaImportConfig: ImportResolutionConfig = {
language: SupportedLanguages.Lua,
strategies: [luaRequireStrategy],
};

View file

@ -22,6 +22,9 @@ export const EXTENSIONS = [
// Python
'.py',
'/__init__.py',
// Lua — require("a.b.c") resolves to a/b/c.lua or a/b/c/init.lua
'.lua',
'/init.lua',
// Java
'.java',
// Kotlin

View file

@ -25,6 +25,7 @@ import { swiftProvider } from './swift.js';
import { dartProvider } from './dart.js';
import { vueProvider } from './vue.js';
import { cobolProvider } from './cobol.js';
import { luaProvider } from './lua.js';
export const providers = {
[SupportedLanguages.JavaScript]: javascriptProvider,
@ -43,6 +44,7 @@ export const providers = {
[SupportedLanguages.Dart]: dartProvider,
[SupportedLanguages.Vue]: vueProvider,
[SupportedLanguages.Cobol]: cobolProvider,
[SupportedLanguages.Lua]: luaProvider,
} satisfies Record<SupportedLanguages, LanguageProvider>;
/** Get provider by language enum (always succeeds for SupportedLanguages). */

View file

@ -0,0 +1,41 @@
/**
* Lua language provider.
*
* Phase A (legacy DAG): emits Function/Method DEFINITION nodes from
* LUA_QUERIES — `function foo()`, `local function foo()`, `function Obj:m()`
* / `function Obj.m()`.
*
* Phase B1 (scope resolution): `emitScopeCaptures` (lua/captures.ts) runs the
* scope query (lua/query.ts) and the central ScopeExtractor builds the scope
* tree + declarations + imports + reference sites. `interpretImport` turns
* require()'s `@import.source` into a wildcard ParsedImport; the legacy
* `importResolver` (luaRequireStrategy / suffixResolve) is bridged to resolve
* `targetRaw` → file (no separate resolveImportTarget needed, mirroring
* Ruby). This unlocks CALLS edges (from @reference.call.*) and IMPORTS edges.
*
* Still Phase B (pending): middleclass `class("Name", Parent)` → Class nodes
* + EXTENDS + HAS_METHOD (call-router pattern); receiverBinding/arity polish
* for higher-quality call-target linking.
*/
import { SupportedLanguages } from 'gitnexus-shared';
import { defineLanguage } from '../language-provider.js';
import { LUA_QUERIES } from '../tree-sitter-queries.js';
import { typeConfig as luaTypeConfig } from '../type-extractors/lua.js';
import { luaExportChecker } from '../export-detection.js';
import { createImportResolver } from '../import-resolvers/resolver-factory.js';
import { luaImportConfig } from '../import-resolvers/configs/lua.js';
import { createCallExtractor } from '../call-extractors/generic.js';
import { luaCallConfig } from '../call-extractors/configs/lua.js';
import { emitLuaScopeCaptures, interpretLuaImport } from './lua/index.js';
export const luaProvider = defineLanguage({
id: SupportedLanguages.Lua,
extensions: ['.lua'],
treeSitterQueries: LUA_QUERIES,
typeConfig: luaTypeConfig,
exportChecker: luaExportChecker,
importResolver: createImportResolver(luaImportConfig),
callExtractor: createCallExtractor(luaCallConfig),
emitScopeCaptures: emitLuaScopeCaptures,
interpretImport: interpretLuaImport,
});

View file

@ -0,0 +1,59 @@
/**
* Lua scope-capture emitter (RFC #909 Ring 3).
*
* Minimal grouping: parse (or reuse the worker's cached AST) → run the scope
* query → group each match's captures into a CaptureMatch keyed by `@name`.
* No Ruby-style decomposition (Lua's require is captured directly in the query
* as @import.statement + @import.source), no YARD, no receiver synthesis —
* those are deferred (receiver/arity polish, Phase B3).
*
* The central ScopeExtractor partitions the output by capture prefix
* (@scope.* / @declaration.* / @import.* / @reference.*) and builds the scope
* tree, declarations, imports, and reference sites that finalize turns into
* CALLS + IMPORTS edges.
*/
import Parser from 'tree-sitter';
import type { Capture, CaptureMatch } from 'gitnexus-shared';
import { nodeToCapture } from '../../utils/ast-helpers.js';
import { getLuaParser, getLuaScopeQuery } from './query.js';
import { getTreeSitterBufferSize } from '../../constants.js';
import { parseSourceSafe } from '../../../tree-sitter/safe-parse.js';
export function emitLuaScopeCaptures(
sourceText: string,
_filePath: string,
cachedTree?: unknown,
): readonly CaptureMatch[] {
let tree: Parser.Tree;
if (cachedTree !== undefined && cachedTree !== null) {
tree = cachedTree as Parser.Tree;
} else {
tree = parseSourceSafe(getLuaParser(), sourceText, undefined, {
bufferSize: getTreeSitterBufferSize(sourceText),
});
}
const out: CaptureMatch[] = [];
for (const match of getLuaScopeQuery().matches(tree.rootNode)) {
const grouped: Record<string, Capture> = {};
for (const c of match.captures) {
const tag = '@' + c.name;
// Skip tree-sitter predicate captures (e.g. @_req used by #eq?).
if (tag.startsWith('@_')) continue;
if (grouped[tag] === undefined) grouped[tag] = nodeToCapture(tag, c.node);
}
if (Object.keys(grouped).length === 0) continue;
// middleclass `class("Name", ...)`: strip surrounding quotes from the name
// so the Class node is named `BattleSkill`, not `"BattleSkill"`. tree-sitter-lua's
// string node has no content child, so .text carries the quotes.
const nameCap = grouped['@declaration.name'];
if (grouped['@declaration.class'] !== undefined && nameCap !== undefined) {
const stripped = nameCap.text.replace(/^["']|["']$/g, '');
if (stripped !== nameCap.text) {
grouped['@declaration.name'] = { ...nameCap, text: stripped };
}
}
out.push(grouped);
}
return out;
}

View file

@ -0,0 +1,182 @@
/**
* Lua middleclass inheritance edges (EXTENDS).
*
* middleclass has no syntactic class body — `class("Name", Parent)` is a plain
* call, and methods are file-top-level `function Name:method()`. So neither
* lexical heritage nor lexical HAS_METHOD applies. This hook re-parses each
* file for `class("Name", Parent)` calls and emits EXTENDS from the child Class
* graph node to the parent Class graph node, resolving both via `nodeLookup`.
*
* Mirrors `emitRubyMixinEdges` (the only other `emitHeritageEdges` impl), but
* reads the heritage pair straight from the AST rather than threading markers
* through `parsedImports` — middleclass's single-arg form needs no marker
* decomposition, and the parent is a bare identifier in the source.
*/
import { readFileSync } from 'node:fs';
import Parser from 'tree-sitter';
import { SupportedLanguages, type ParsedFile, type NodeLabel } from 'gitnexus-shared';
import { getLanguageGrammar } from '../../../tree-sitter/parser-loader.js';
import { isClassLike } from '../../scope-resolution/scope/walkers.js';
import { resolveDefGraphId } from '../../scope-resolution/graph-bridge/ids.js';
import { positionKey, type GraphNodeLookup } from '../../scope-resolution/graph-bridge/node-lookup.js';
import type { KnowledgeGraph } from '../../../graph/types.js';
import { generateId } from '../../../../lib/utils.js';
import { getLuaParser } from './query.js';
// `local Foo = class("Foo", Parent)` — the 2nd arg is a bare (variable name:).
// `class("Foo")` (no parent) does not match: this pattern requires the trailing
// variable, so parentless classes correctly emit no EXTENDS edge.
const HERITAGE_QUERY = `
(call
function: (variable name: (identifier) @_class)
arguments: (argument_list
(expression_list
(string) @child.name
(variable name: (identifier) @parent.name)))
(#eq? @_class "class")) @heritage
`;
// `function ClassName:method()` (colon) and `function ClassName.method()` (dot).
// The receiver (table) is captured as @method.owner; resolves to a Class node
// for HAS_METHOD. middleclass methods are file-top-level (not in a class body),
// so lexical HAS_METHOD cannot produce these.
const METHOD_OWNER_QUERY = `
(function_definition_statement
name: (variable
table: (identifier) @method.owner
method: (identifier) @method.name)) @method.def
(function_definition_statement
name: (variable
table: (identifier) @method.owner
field: (identifier) @method.name)) @method.def
`;
let _heritageQuery: Parser.Query | null = null;
function getHeritageQuery(): Parser.Query {
if (_heritageQuery === null) {
_heritageQuery = new Parser.Query(
getLanguageGrammar(SupportedLanguages.Lua) as Parameters<Parser['setLanguage']>[0],
HERITAGE_QUERY,
);
}
return _heritageQuery;
}
let _methodOwnerQuery: Parser.Query | null = null;
function getMethodOwnerQuery(): Parser.Query {
if (_methodOwnerQuery === null) {
_methodOwnerQuery = new Parser.Query(
getLanguageGrammar(SupportedLanguages.Lua) as Parameters<Parser['setLanguage']>[0],
METHOD_OWNER_QUERY,
);
}
return _methodOwnerQuery;
}
function stripQuotes(s: string): string {
return s.replace(/^["']|["']$/g, '');
}
export function emitLuaHeritageEdges(
graph: KnowledgeGraph,
parsedFiles: readonly ParsedFile[],
nodeLookup: GraphNodeLookup,
): void {
// name → graphId (global, for parent resolution). First-wins; same-named
// classes in different files are rare in middleclass codebases, and a
// collision here only risks a wrong parent — acceptable for a 0.85-confidence
// heuristic edge (better an imperfect EXTENDS than none).
const graphIdByName = new Map<string, string>();
// (filePath, name) → graphId (per-file, for child resolution).
const graphIdByFileAndName = new Map<string, string>();
for (const parsed of parsedFiles) {
for (const def of parsed.localDefs) {
if (!isClassLike(def.type)) continue;
const gid = resolveDefGraphId(parsed.filePath, def, nodeLookup);
if (gid === undefined) continue;
const qn = def.qualifiedName ?? '';
if (qn.length > 0) {
graphIdByFileAndName.set(`${parsed.filePath}::${qn}`, gid);
if (!graphIdByName.has(qn)) graphIdByName.set(qn, gid);
}
}
}
const emittedExtends = new Set<string>();
const emittedHasMethod = new Set<string>();
const parser = getLuaParser();
const heritageQuery = getHeritageQuery();
const methodOwnerQuery = getMethodOwnerQuery();
for (const parsed of parsedFiles) {
let src: string;
try {
src = readFileSync(parsed.filePath, 'utf8');
} catch {
continue;
}
let tree;
try {
tree = parser.parse(src);
} catch {
continue;
}
// ── EXTENDS: class("Name", Parent) ──────────────────────────────────────
const heritageMatches = heritageQuery.matches(tree.rootNode);
for (const m of heritageMatches) {
const caps: Record<string, Parser.SyntaxNode> = {};
for (const c of m.captures) caps[c.name] = c.node;
const childName = stripQuotes(caps['child.name']?.text ?? '');
const parentName = caps['parent.name']?.text;
if (childName.length === 0 || parentName === undefined) continue;
const childGid =
graphIdByFileAndName.get(`${parsed.filePath}::${childName}`) ??
graphIdByName.get(childName);
const parentGid = graphIdByName.get(parentName);
if (childGid === undefined || parentGid === undefined) continue;
const edgeKey = `${childGid}->${parentGid}`;
if (emittedExtends.has(edgeKey)) continue;
emittedExtends.add(edgeKey);
graph.addRelationship({
id: generateId('EXTENDS', edgeKey),
sourceId: childGid,
targetId: parentGid,
type: 'EXTENDS',
confidence: 0.85,
reason: 'lua-scope: middleclass inherits',
});
}
// ── HAS_METHOD: function ClassName:method() / function ClassName.method() ─
const methodMatches = methodOwnerQuery.matches(tree.rootNode);
for (const m of methodMatches) {
const caps: Record<string, Parser.SyntaxNode> = {};
for (const c of m.captures) caps[c.name] = c.node;
const ownerName = caps['method.owner']?.text;
const methodName = caps['method.name']?.text;
const defNode = caps['method.def'];
if (ownerName === undefined || methodName === undefined || defNode === undefined) continue;
const classGid =
graphIdByFileAndName.get(`${parsed.filePath}::${ownerName}`) ??
graphIdByName.get(ownerName);
if (classGid === undefined) continue;
// Resolve the Method graph node by position (0-based row + simple name).
const methodGid = nodeLookup.get(
positionKey(parsed.filePath, 'Method' as NodeLabel, defNode.startPosition.row, methodName),
);
if (methodGid === undefined) continue;
const edgeKey = `${classGid}->${methodGid}`;
if (emittedHasMethod.has(edgeKey)) continue;
emittedHasMethod.add(edgeKey);
graph.addRelationship({
id: generateId('HAS_METHOD', edgeKey),
sourceId: classGid,
targetId: methodGid,
type: 'HAS_METHOD',
confidence: 0.85,
reason: 'lua-scope: middleclass method owner',
});
}
}
}

View file

@ -0,0 +1,7 @@
/**
* Lua scope-resolution hooks (RFC #909 Ring 3). Barrel.
*/
export { emitLuaScopeCaptures } from './captures.js';
export { interpretLuaImport } from './interpret.js';
export { getLuaParser, getLuaScopeQuery } from './query.js';
export { emitLuaHeritageEdges } from './heritage.js';

View file

@ -0,0 +1,23 @@
/**
* Lua import interpretation (RFC #909).
*
* `require("a.b.c")` loads the module and (conventionally) returns its table.
* We emit a `wildcard` ParsedImport so the IMPORTS edge (this file → required
* file) is materialized and the target's exported names become resolvable as
* the module's local name. Call-target linking for `util.split` style member
* calls is handled by the scope-resolution registry against the import
* binding — refined in Phase B3 (receiver/arity polish).
*/
import type { CaptureMatch, ParsedImport } from 'gitnexus-shared';
function stripQuotes(s: string): string {
return s.replace(/^["']|["']$/g, '');
}
export function interpretLuaImport(captures: CaptureMatch): ParsedImport | null {
const source = captures['@import.source']?.text;
if (source === undefined) return null;
const targetRaw = stripQuotes(source);
if (!targetRaw) return null;
return { kind: 'wildcard', targetRaw };
}

View file

@ -0,0 +1,110 @@
/**
* Lua scope-resolution query (RFC #909).
*
* Captures the structural skeleton the central ScopeExtractor consumes:
* - scopes: (chunk) root + function bodies
* - declarations: function / local function / method (function Obj:m())
* - imports: require("a.b.c") — the call + its string arg
* - references: free calls foo() + member calls obj:m() / obj.f()
*
* `tree-sitter-lua` is a vendored grammar (loaded lazily via parser-loader, NOT
* statically imported) — mirroring languages/dart/query.ts: a top-level
* `import Lua from 'tree-sitter-lua'` would throw ERR_MODULE_NOT_FOUND on the
* main thread (where the vendored grammar is not in node_modules) and crash
* analyze even for repos with no Lua files (#2091/#2093).
*
* Node names verified against the vendored tree-sitter-lua grammar via the
* smoke test in ~/gitnexus-lua-work/abi-test/smoke2.cjs:
* (chunk) (function_definition_statement name: (identifier|variable))
* (local_function_definition_statement name: (identifier))
* (call function: (variable name:|table:+method:|table:+field:) arguments: (argument_list (expression_list ...)))
*/
import Parser from 'tree-sitter';
import { SupportedLanguages } from 'gitnexus-shared';
import { getLanguageGrammar } from '../../../tree-sitter/parser-loader.js';
const LUA_SCOPE_QUERY = `
;; ── Scopes ───────────────────────────────────────────────────────────────────
(chunk) @scope.module
(function_definition_statement) @scope.function
(local_function_definition_statement) @scope.function
;; ── Declarations — functions ─────────────────────────────────────────────────
(function_definition_statement
name: (identifier) @declaration.name) @declaration.function
(local_function_definition_statement
name: (identifier) @declaration.name) @declaration.function
;; ── Declarations — methods: function Obj:method() / function Obj.field() ────
;; name is a (variable) with table + method (colon) or table + field (dot).
(function_definition_statement
name: (variable
method: (identifier) @declaration.name)) @declaration.method
(function_definition_statement
name: (variable
field: (identifier) @declaration.name)) @declaration.method
;; ── Declarations — middleclass classes: class("Name"[, Parent]) ──────────────
;; local Foo = class("Foo", Parent) — class() is a plain call; the first
;; string arg is the class name (quotes stripped in captures.ts). The second
;; arg (Parent) → EXTENDS is Phase B2b (heritage marker).
(call
function: (variable name: (identifier) @_class)
arguments: (argument_list (expression_list (string) @declaration.name))
(#eq? @_class "class")) @declaration.class
;; ── Imports — require("a.b.c") ──────────────────────────────────────────────
;; require is a plain (call) whose function is a (variable name: (identifier)).
;; The string arg is captured as @import.source; interpretLuaImport strips
;; quotes and resolves it via the legacy luaRequireStrategy (suffixResolve).
(call
function: (variable name: (identifier) @_req)
arguments: (argument_list (expression_list (string) @import.source))
(#eq? @_req "require")) @import.statement
;; ── References — free calls: foo() ───────────────────────────────────────────
;; Also matches require(...) — harmless: require has no def, so it stays an
;; unresolved reference (no CALLS edge) while the @import.statement pattern
;; above emits the IMPORTS edge.
(call
function: (variable
name: (identifier) @reference.name)) @reference.call.free
;; ── References — member calls: obj:method() (table + method) ─────────────────
(call
function: (variable
table: (_) @reference.receiver
method: (identifier) @reference.name)) @reference.call.member
;; ── References — field calls: obj.field() (table + field) ────────────────────
(call
function: (variable
table: (_) @reference.receiver
field: (identifier) @reference.name)) @reference.call.member
`;
let _parser: Parser | null = null;
let _query: Parser.Query | null = null;
export function getLuaParser(): Parser {
if (_parser === null) {
_parser = new Parser();
_parser.setLanguage(
getLanguageGrammar(SupportedLanguages.Lua) as Parameters<Parser['setLanguage']>[0],
);
}
return _parser;
}
export function getLuaScopeQuery(): Parser.Query {
if (_query === null) {
_query = new Parser.Query(
getLanguageGrammar(SupportedLanguages.Lua) as Parameters<Parser['setLanguage']>[0],
LUA_SCOPE_QUERY,
);
}
return _query;
}

View file

@ -0,0 +1,74 @@
/**
* Lua `ScopeResolver` registered in `SCOPE_RESOLVERS` and consumed by the
* generic `runScopeResolution` orchestrator.
*
* Minimal Phase B1 wiring: Lua's scope model (Module + Function/Method, no
* static types) plugs into `runScopeResolution` with the least configuration
* that unlocks CALLS + IMPORTS edges. middleclass class inheritance (EXTENDS,
* HAS_METHOD, MRO) is Phase B2; receiver/arity polish is Phase B3.
*
* Reference: `languages/cobol/scope-resolver.ts` (minimal) + `languages/go/`.
*/
import type { ParsedFile } from 'gitnexus-shared';
import { SupportedLanguages } from 'gitnexus-shared';
import { populateClassOwnedMembers } from '../../scope-resolution/scope/walkers.js';
import { buildSuffixIndex, suffixResolve, type SuffixIndex } from '../../import-resolvers/utils.js';
import type { ScopeResolver } from '../../scope-resolution/contract/scope-resolver.js';
import { luaProvider } from '../lua.js';
import { emitLuaHeritageEdges } from './heritage.js';
// Cache the suffix index across calls within one analyze run — `allFilePaths`
// is the same ReadonlySet for every Lua import in the run, so keying on its
// reference avoids rebuilding a 500-entry index per require(). Must use the
// index path: the linear-scan fallback in suffixResolve prepends `/` to the
// suffix, which fails to match root-level files like `middleclass.lua`.
let _cachedSet: ReadonlySet<string> | null = null;
let _cachedIndex: SuffixIndex | null = null;
const luaScopeResolver: ScopeResolver = {
language: SupportedLanguages.Lua,
languageProvider: luaProvider,
importEdgeReason: 'lua-scope: require',
// require("a.b.c") → module path a/b/c (+ EXTENSIONS: .lua / /init.lua).
// targetRaw arrives quote-stripped (interpretLuaImport); Lua's module
// separator is `.`, so split on it before joining to a path.
resolveImportTarget: (targetRaw, _fromFile, allFilePaths) => {
const parts = targetRaw.replace(/^["']|["']$/g, '').split('.').filter(Boolean);
if (parts.length === 0) return null;
if (_cachedSet !== allFilePaths) {
const list = [...allFilePaths];
_cachedIndex = buildSuffixIndex(list, list);
_cachedSet = allFilePaths;
}
const list = [...allFilePaths];
return suffixResolve(parts, list, list, _cachedIndex ?? undefined);
},
// Lua: default local-first-then-imports merge (no language-specific precedence).
mergeBindings: (existing) => [...existing],
// Lua varargs (...) + optional params make static arity checks unreliable —
// 'unknown' (no signal) is the safe minimal choice.
arityCompatibility: () => 'unknown',
// middleclass single inheritance — MRO populated in Phase B2 (buildMro +
// defaultLinearize). Empty for now (no Class scopes yet).
buildMro: () => new Map(),
populateOwners: (parsed: ParsedFile) => populateClassOwnedMembers(parsed),
// Lua has no super-call construct (middleclass __base access is Phase B2).
isSuperReceiver: () => false,
// middleclass `class("Name", Parent)` — emits EXTENDS. middleclass has no
// syntactic class body, so lexical heritage cannot produce these; the hook
// re-parses for the class() call's parent arg.
emitHeritageEdges: emitLuaHeritageEdges,
// Lua has globals (`function foo()` is global) — let unresolved free calls
// fall back to the global symbol table.
allowGlobalFreeCallFallback: true,
};
export { luaScopeResolver };

View file

@ -27,6 +27,7 @@ import { cobolScopeResolver } from '../../languages/cobol/scope-resolver.js';
import { swiftScopeResolver } from '../../languages/swift/scope-resolver.js';
import { dartScopeResolver } from '../../languages/dart/scope-resolver.js';
import { vueScopeResolver } from '../../languages/vue/scope-resolver.js';
import { luaScopeResolver } from '../../languages/lua/scope-resolver.js';
/** Map of `SupportedLanguages` → `ScopeResolver`. The scope-resolution phase
* iterates this map directly — every registered resolver runs. This is the
@ -51,4 +52,5 @@ export const SCOPE_RESOLVERS: ReadonlyMap<SupportedLanguages, ScopeResolver> = n
[SupportedLanguages.Swift, swiftScopeResolver],
[SupportedLanguages.Dart, dartScopeResolver],
[SupportedLanguages.Vue, vueScopeResolver],
[SupportedLanguages.Lua, luaScopeResolver],
]);

View file

@ -2447,6 +2447,67 @@ export const DART_QUERIES = `
right: (_)) @assignment
`;
// Lua queries — works with vendored tree-sitter-lua (rebuilt against the
// tree-sitter@0.21.1 runtime + Napi convention; see vendor/tree-sitter-lua).
// AST node names verified by parsing a sample with the smoke test in
// ~/gitnexus-lua-work/abi-test/smoke2.cjs.
//
// Phase A (legacy DAG): definition NODES + call SITES. NOTE: CALLS and
// IMPORTS EDGES are NOT emitted by the legacy path — call/import resolution
// was migrated to the scope-resolution pipeline (RFC #909, gated on
// emitScopeCaptures; see call-processor.ts:4-7 + parse-worker.ts:1654). The
// @call captures here produce call sites in parse-worker, but those sites
// only become CALLS edges once lua's scope query (lua/query.ts) + the
// emitScopeCaptures hooks land (Phase B). The definition captures DO produce
// Function/Method nodes regardless.
export const LUA_QUERIES = `
; ── Functions: function foo() ... end (name is a bare identifier) ────────────
(function_definition_statement
name: (identifier) @name) @definition.function
; ── Methods: function Obj:method() (name is a variable: table + method) ──────
(function_definition_statement
name: (variable
method: (identifier) @name)) @definition.method
; ── Dot-methods: function Obj.method() (name is a variable: table + field) ───
(function_definition_statement
name: (variable
field: (identifier) @name)) @definition.method
; ── Local functions: local function foo() ... end ─────────────────────────────
(local_function_definition_statement
name: (identifier) @name) @definition.function
; ── middleclass classes: local Foo = class("Foo"[, Parent]) ─────────────────────
; class() is a plain call; the Class node's name comes from the local var
; (quote-free). EXTENDS (Parent arg) + HAS_METHOD (function Foo:method() → Foo)
; need scope-resolution receiver modeling and the heritage marker; middleclass
; methods are file-top-level (not class-body nested), so the class is not a
; lexical scope — deferred (Phase B2b/B3).
(local_variable_declaration
(variable_list (variable name: (identifier) @name))
(expression_list
value: (call
function: (variable name: (identifier) @_class)
(#eq? @_class "class")))) @definition.class
; ── Calls: foo(), obj:bar(), obj.baz() ────────────────────────────────────────
; call.function is a (variable); @call.name lands on the invoked identifier
; so the generic call extractor can derive calledName + callForm + receiver.
(call
function: (variable
name: (identifier) @call.name)) @call
(call
function: (variable
method: (identifier) @call.name)) @call
(call
function: (variable
field: (identifier) @call.name)) @call
`;
import { SupportedLanguages } from 'gitnexus-shared';
export const LANGUAGE_QUERIES: Record<SupportedLanguages, string> = {
@ -2464,6 +2525,7 @@ export const LANGUAGE_QUERIES: Record<SupportedLanguages, string> = {
[SupportedLanguages.Ruby]: RUBY_QUERIES,
[SupportedLanguages.Swift]: SWIFT_QUERIES,
[SupportedLanguages.Dart]: DART_QUERIES,
[SupportedLanguages.Lua]: LUA_QUERIES,
[SupportedLanguages.Vue]: TYPESCRIPT_QUERIES, // Vue <script> blocks are parsed as TypeScript
[SupportedLanguages.Cobol]: '', // Standalone regex processor — no tree-sitter queries
};

View file

@ -0,0 +1,16 @@
/**
* Lua type extractor — minimal.
*
* Lua has no static type annotations, so no (varName → typeName) bindings are
* extracted: declarationNodeTypes is empty and extractDeclaration/extractParameter
* are no-ops. Constructor-inference and return-type inference are deferred
* (would belong in Phase B scope-resolution). Satisfies the LanguageTypeConfig
* required fields with the least-possible surface.
*/
import type { LanguageTypeConfig } from './types.js';
export const typeConfig: LanguageTypeConfig = {
declarationNodeTypes: new Set<string>(),
extractDeclaration: () => {},
extractParameter: () => {},
};

View file

@ -81,6 +81,11 @@ let C: TreeSitterLanguage | null = null;
try {
C = requireVendoredGrammar('tree-sitter-c') as TreeSitterLanguage;
} catch {}
let Lua: TreeSitterLanguage | null = null;
try {
Lua = requireVendoredGrammar('tree-sitter-lua') as TreeSitterLanguage;
} catch {}
import { getLanguageFromFilename } from 'gitnexus-shared';
import {
buildDefinitionPreScan,
@ -548,6 +553,7 @@ const languageMap: Record<string, TreeSitterLanguage> = {
[SupportedLanguages.Vue]: TypeScript.typescript,
...(Dart ? { [SupportedLanguages.Dart]: Dart } : {}),
...(Swift ? { [SupportedLanguages.Swift]: Swift } : {}),
...(Lua ? { [SupportedLanguages.Lua]: Lua } : {}),
};
/**

View file

@ -177,6 +177,17 @@ const SOURCES: Record<string, GrammarSource> = {
'Likely cause: no prebuilt `.node` for this platform/architecture. ' +
`See ${ISSUES_URL}/2107.`,
},
[SupportedLanguages.Lua]: {
load: () => requireVendoredGrammar('tree-sitter-lua'),
optional: true,
userSkippable: true,
unavailableNote:
'Lua parsing disabled: vendored `tree-sitter-lua` (under ' +
'`gitnexus/vendor/tree-sitter-lua`) failed to load. ' +
'Likely cause: the native binding did not build at install (missing ' +
'python3/make/clang) or no prebuilt `.node` for this platform/architecture. ' +
`See ${ISSUES_URL}.`,
},
};
/**

View file

@ -33,6 +33,7 @@ export const VENDORED_GRAMMAR_PACKAGES: ReadonlySet<string> = new Set([
'tree-sitter-proto',
'tree-sitter-swift',
'tree-sitter-kotlin',
'tree-sitter-lua',
]);
/** Absolute directory of a vendored grammar package under `vendor/`. */

21
gitnexus/vendor/tree-sitter-lua/LICENSE vendored Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) tree-sitter-grammars/tree-sitter-lua contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -0,0 +1,16 @@
# tree-sitter-lua (vendored)
Vendored copy of [tree-sitter-lua](https://github.com/tree-sitter-grammars/tree-sitter-lua) (v2.1.3, MIT).
## Why vendored
The upstream npm package ships `nan`-based node bindings that are ABI-incompatible with the `tree-sitter@0.21.1` runtime GitNexus pins (`setLanguage` rejects with "Invalid language object"). The grammar C sources (`src/parser.c`, `src/scanner.c`) are vendored unmodified and rebuilt against a Napi `binding.cc` adapted from `tree-sitter-c`'s vendored binding, so the grammar loads under the pinned runtime.
## Files
- `src/` — upstream grammar sources (unmodified)
- `bindings/node/binding.cc` — Napi binding (adapted from `tree-sitter-c`)
- `bindings/node/index.js`, `index.d.ts` — node entry + types (adapted from `tree-sitter-c`)
- `binding.gyp`, `package.json` — build config
Built by `scripts/build-tree-sitter-grammars.cjs` at install time; `build/` artifacts are gitignored.

View file

@ -0,0 +1,30 @@
{
"targets": [
{
"target_name": "tree_sitter_lua_binding",
"dependencies": [
"<!(node -p \"require('node-addon-api').targets\"):node_addon_api_except",
],
"include_dirs": [
"src"
],
"sources": [
"bindings/node/binding.cc",
"src/parser.c",
"src/scanner.c"
],
"conditions": [
["OS!='win'", {
"cflags_c": [
"-std=c11"
]
}, {
"cflags_c": [
"/std:c11",
"/utf-8"
]
}]
]
}
]
}

View file

@ -0,0 +1,22 @@
#include <napi.h>
typedef struct TSLanguage TSLanguage;
extern "C" TSLanguage *tree_sitter_lua();
// "tree-sitter", "language" hashed with BLAKE2 — the type tag every tree-sitter
// grammar's language external carries; the tree-sitter@0.21.1 runtime's
// Parser.setLanguage checks it to validate the language object.
const napi_type_tag LANGUAGE_TYPE_TAG = {
0x8AF2E5212AD58ABF, 0xD5006CAD83ABBA16
};
Napi::Object Init(Napi::Env env, Napi::Object exports) {
exports["name"] = Napi::String::New(env, "lua");
auto language = Napi::External<TSLanguage>::New(env, tree_sitter_lua());
language.TypeTag(&LANGUAGE_TYPE_TAG);
exports["language"] = language;
return exports;
}
NODE_API_MODULE(tree_sitter_lua_binding, Init)

View file

@ -0,0 +1,5 @@
// Type stub mirroring gitnexus/vendor/tree-sitter-c/bindings/node/index.d.ts.
// The runtime binding is a native .node loaded by node-gyp-build; this stub
// gives TypeScript a Language-shaped module to import.
declare const language: unknown;
export = language;

View file

@ -0,0 +1,7 @@
const root = require("path").join(__dirname, "..", "..");
module.exports = require("node-gyp-build")(root);
try {
module.exports.nodeTypeInfo = require("../../src/node-types.json");
} catch (_) {}

View file

@ -0,0 +1,18 @@
{
"name": "tree-sitter-lua",
"version": "2.1.3",
"description": "Lua grammar for tree-sitter (vendored by gitnexus)",
"repository": "https://github.com/tree-sitter-grammars/tree-sitter-lua",
"license": "MIT",
"main": "bindings/node/index.js",
"types": "bindings/node/index.d.ts",
"_vendoredBy": "gitnexus - runtime package derived from tree-sitter-lua@2.1.3 (tree-sitter-grammars/tree-sitter-lua). HELD at 2.1.3 grammar sources but REBUILT against the bundled tree-sitter@0.21.1 runtime + node-addon-api Napi convention (the same convention as vendor/tree-sitter-c). Upstream's npm package ships a nan/node.h binding that the tree-sitter@0.21.1 runtime rejects with 'Invalid language object' (nan External lacks the LANGUAGE_TYPE_TAG the 0.21.1 setLanguage validates); vendoring with this Napi binding.cc fixes that. Do not bump without verifying ABI against the 0.21.1 runtime. Loaded from vendor/ by absolute path at runtime (vendored-grammars.ts) — NEVER copied to node_modules (#2111). No prebuilds committed yet; build-tree-sitter-grammars.cjs source-builds on a toolchain host (darwin/linux x64+arm64 via clang+python).",
"peerDependencies": {
"tree-sitter": "^0.21.0"
},
"peerDependenciesMeta": {
"tree-sitter": {
"optional": true
}
}
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,315 @@
#include <tree_sitter/parser.h>
#include <wctype.h>
enum TokenType
{
COMMENT_START,
COMMENT_CONTENT,
COMMENT_END,
STRING_START,
STRING_CONTENT,
STRING_END,
};
static void consume(TSLexer *lexer)
{
lexer->advance(lexer, false);
}
static void skip(TSLexer *lexer)
{
lexer->advance(lexer, true);
}
static bool consume_if(TSLexer *lexer, const int32_t character)
{
if (lexer->lookahead == character)
{
consume(lexer);
return true;
}
return false;
}
const char SQ_STRING_DELIMITER = '\'';
const char DQ_STRING_DELIMITER = '"';
enum StartedToken
{
SHORT_COMMENT = 1,
SHORT_SQ_STRING,
SHORT_DQ_STRING,
LONG_COMMENT,
LONG_STRING,
};
struct ScannerState
{
enum StartedToken started;
unsigned int depth;
};
void *tree_sitter_lua_external_scanner_create()
{
return malloc(sizeof(struct ScannerState));
}
void tree_sitter_lua_external_scanner_destroy(void *payload)
{
free(payload);
}
unsigned int tree_sitter_lua_external_scanner_serialize(void *payload, char *buffer)
{
struct ScannerState *state = payload;
buffer[0] = state->started;
buffer[1] = state->depth;
return 2;
}
void tree_sitter_lua_external_scanner_deserialize(void *payload, const char *buffer, unsigned int length)
{
if (length == 2)
{
struct ScannerState *state = payload;
state->started = buffer[0];
state->depth = buffer[1];
}
}
static unsigned int get_depth(TSLexer *lexer)
{
unsigned int current_depth = 0;
while (consume_if(lexer, '='))
{
current_depth += 1;
}
return current_depth;
}
static bool scan_depth(TSLexer *lexer, unsigned int remaining_depth)
{
while (remaining_depth > 0 && consume_if(lexer, '='))
{
remaining_depth -= 1;
}
return remaining_depth == 0;
}
bool tree_sitter_lua_external_scanner_scan(void *payload, TSLexer *lexer, const bool *valid_symbols)
{
struct ScannerState *state = payload;
switch (state->started)
{
case SHORT_COMMENT:
{
// try to match the short comment's end (new line or eof)
if (lexer->lookahead == '\n' || lexer->eof(lexer))
{
if (valid_symbols[COMMENT_END])
{
state->started = 0;
lexer->result_symbol = COMMENT_END;
return true;
}
}
else if (valid_symbols[COMMENT_CONTENT])
{
// consume all characters till a short comment's end
do
{
consume(lexer);
} while (lexer->lookahead != '\n' && !lexer->eof(lexer));
lexer->result_symbol = COMMENT_CONTENT;
return true;
}
break;
}
case SHORT_SQ_STRING:
case SHORT_DQ_STRING:
{
// define the short string's delimiter
const char delimiter = state->started == SHORT_SQ_STRING ? SQ_STRING_DELIMITER : DQ_STRING_DELIMITER;
// try to match the short string's end (" or ')
if (consume_if(lexer, delimiter))
{
if (valid_symbols[STRING_END])
{
state->started = 0;
lexer->result_symbol = STRING_END;
return true;
}
}
else if (valid_symbols[STRING_CONTENT] && lexer->lookahead != '\n' && !lexer->eof(lexer))
{
// consume any character till a short string's end, new line or eof
do
{
// consume any character after a backslash, unless it's a new line or eof
if (consume_if(lexer, '\\') && (lexer->lookahead == '\n' || lexer->eof(lexer)))
{
break;
}
consume(lexer);
} while (lexer->lookahead != delimiter && lexer->lookahead != '\n' && !lexer->eof(lexer));
lexer->result_symbol = STRING_CONTENT;
return true;
}
break;
}
case LONG_COMMENT:
case LONG_STRING:
{
const bool is_inside_a_comment = state->started == LONG_COMMENT;
bool some_characters_were_consumed = false;
if (is_inside_a_comment ? valid_symbols[COMMENT_END] : valid_symbols[STRING_END])
{
// try to match the long comment's/string's end (]=*])
if (consume_if(lexer, ']'))
{
if (scan_depth(lexer, state->depth) && consume_if(lexer, ']'))
{
state->started = 0;
state->depth = 0;
lexer->result_symbol = is_inside_a_comment ? COMMENT_END : STRING_END;
return true;
}
some_characters_were_consumed = true;
}
}
if (is_inside_a_comment ? valid_symbols[COMMENT_CONTENT] : valid_symbols[STRING_CONTENT])
{
if (!some_characters_were_consumed)
{
if (lexer->eof(lexer))
{
break;
}
// consume the next character as it can't start a long comment's/string's end ([)
consume(lexer);
}
// consume any character till a long comment's/string's end or eof
while (true)
{
lexer->mark_end(lexer);
if (consume_if(lexer, ']'))
{
if (scan_depth(lexer, state->depth))
{
if (consume_if(lexer, ']'))
{
break;
}
}
else
{
continue;
}
}
if (lexer->eof(lexer))
{
break;
}
consume(lexer);
}
lexer->result_symbol = is_inside_a_comment ? COMMENT_CONTENT : STRING_CONTENT;
return true;
}
break;
}
default:
{
// ignore all whitespace
while (iswspace(lexer->lookahead))
{
skip(lexer);
}
if (valid_symbols[COMMENT_START])
{
// try to match a short comment's start (--)
if (consume_if(lexer, '-'))
{
if (consume_if(lexer, '-'))
{
state->started = SHORT_COMMENT;
// try to match a long comment's start (--[=*[)
lexer->mark_end(lexer);
if (consume_if(lexer, '['))
{
unsigned int possible_depth = get_depth(lexer);
if (consume_if(lexer, '['))
{
state->started = LONG_COMMENT;
state->depth = possible_depth;
lexer->mark_end(lexer);
}
}
lexer->result_symbol = COMMENT_START;
return true;
}
break;
}
}
if (valid_symbols[STRING_START])
{
// try to match a short single-quoted string's start (")
if (consume_if(lexer, SQ_STRING_DELIMITER))
{
state->started = SHORT_SQ_STRING;
}
// try to match a short double-quoted string's start (')
else if (consume_if(lexer, DQ_STRING_DELIMITER))
{
state->started = SHORT_DQ_STRING;
}
// try to match a long string's start ([=*[)
else if (consume_if(lexer, '['))
{
unsigned int possible_depth = get_depth(lexer);
if (consume_if(lexer, '['))
{
state->started = LONG_STRING;
state->depth = possible_depth;
}
}
if (state->started)
{
lexer->result_symbol = STRING_START;
return true;
}
}
break;
}
}
return false;
}

View file

@ -0,0 +1,224 @@
#ifndef TREE_SITTER_PARSER_H_
#define TREE_SITTER_PARSER_H_
#ifdef __cplusplus
extern "C" {
#endif
#include <stdbool.h>
#include <stdint.h>
#include <stdlib.h>
#define ts_builtin_sym_error ((TSSymbol)-1)
#define ts_builtin_sym_end 0
#define TREE_SITTER_SERIALIZATION_BUFFER_SIZE 1024
typedef uint16_t TSStateId;
#ifndef TREE_SITTER_API_H_
typedef uint16_t TSSymbol;
typedef uint16_t TSFieldId;
typedef struct TSLanguage TSLanguage;
#endif
typedef struct {
TSFieldId field_id;
uint8_t child_index;
bool inherited;
} TSFieldMapEntry;
typedef struct {
uint16_t index;
uint16_t length;
} TSFieldMapSlice;
typedef struct {
bool visible;
bool named;
bool supertype;
} TSSymbolMetadata;
typedef struct TSLexer TSLexer;
struct TSLexer {
int32_t lookahead;
TSSymbol result_symbol;
void (*advance)(TSLexer *, bool);
void (*mark_end)(TSLexer *);
uint32_t (*get_column)(TSLexer *);
bool (*is_at_included_range_start)(const TSLexer *);
bool (*eof)(const TSLexer *);
};
typedef enum {
TSParseActionTypeShift,
TSParseActionTypeReduce,
TSParseActionTypeAccept,
TSParseActionTypeRecover,
} TSParseActionType;
typedef union {
struct {
uint8_t type;
TSStateId state;
bool extra;
bool repetition;
} shift;
struct {
uint8_t type;
uint8_t child_count;
TSSymbol symbol;
int16_t dynamic_precedence;
uint16_t production_id;
} reduce;
uint8_t type;
} TSParseAction;
typedef struct {
uint16_t lex_state;
uint16_t external_lex_state;
} TSLexMode;
typedef union {
TSParseAction action;
struct {
uint8_t count;
bool reusable;
} entry;
} TSParseActionEntry;
struct TSLanguage {
uint32_t version;
uint32_t symbol_count;
uint32_t alias_count;
uint32_t token_count;
uint32_t external_token_count;
uint32_t state_count;
uint32_t large_state_count;
uint32_t production_id_count;
uint32_t field_count;
uint16_t max_alias_sequence_length;
const uint16_t *parse_table;
const uint16_t *small_parse_table;
const uint32_t *small_parse_table_map;
const TSParseActionEntry *parse_actions;
const char * const *symbol_names;
const char * const *field_names;
const TSFieldMapSlice *field_map_slices;
const TSFieldMapEntry *field_map_entries;
const TSSymbolMetadata *symbol_metadata;
const TSSymbol *public_symbol_map;
const uint16_t *alias_map;
const TSSymbol *alias_sequences;
const TSLexMode *lex_modes;
bool (*lex_fn)(TSLexer *, TSStateId);
bool (*keyword_lex_fn)(TSLexer *, TSStateId);
TSSymbol keyword_capture_token;
struct {
const bool *states;
const TSSymbol *symbol_map;
void *(*create)(void);
void (*destroy)(void *);
bool (*scan)(void *, TSLexer *, const bool *symbol_whitelist);
unsigned (*serialize)(void *, char *);
void (*deserialize)(void *, const char *, unsigned);
} external_scanner;
const TSStateId *primary_state_ids;
};
/*
* Lexer Macros
*/
#define START_LEXER() \
bool result = false; \
bool skip = false; \
bool eof = false; \
int32_t lookahead; \
goto start; \
next_state: \
lexer->advance(lexer, skip); \
start: \
skip = false; \
lookahead = lexer->lookahead;
#define ADVANCE(state_value) \
{ \
state = state_value; \
goto next_state; \
}
#define SKIP(state_value) \
{ \
skip = true; \
state = state_value; \
goto next_state; \
}
#define ACCEPT_TOKEN(symbol_value) \
result = true; \
lexer->result_symbol = symbol_value; \
lexer->mark_end(lexer);
#define END_STATE() return result;
/*
* Parse Table Macros
*/
#define SMALL_STATE(id) id - LARGE_STATE_COUNT
#define STATE(id) id
#define ACTIONS(id) id
#define SHIFT(state_value) \
{{ \
.shift = { \
.type = TSParseActionTypeShift, \
.state = state_value \
} \
}}
#define SHIFT_REPEAT(state_value) \
{{ \
.shift = { \
.type = TSParseActionTypeShift, \
.state = state_value, \
.repetition = true \
} \
}}
#define SHIFT_EXTRA() \
{{ \
.shift = { \
.type = TSParseActionTypeShift, \
.extra = true \
} \
}}
#define REDUCE(symbol_val, child_count_val, ...) \
{{ \
.reduce = { \
.type = TSParseActionTypeReduce, \
.symbol = symbol_val, \
.child_count = child_count_val, \
__VA_ARGS__ \
}, \
}}
#define RECOVER() \
{{ \
.type = TSParseActionTypeRecover \
}}
#define ACCEPT_INPUT() \
{{ \
.type = TSParseActionTypeAccept \
}}
#ifdef __cplusplus
}
#endif
#endif // TREE_SITTER_PARSER_H_