add support for julia

This commit is contained in:
Dongdong Kong 2026-05-24 18:35:11 +08:00
parent 39e9b40136
commit 56065efaa4
17 changed files with 633 additions and 0 deletions

View file

@ -43,6 +43,7 @@ const EXTENSION_MAP: Record<SupportedLanguages, readonly string[]> = {
[SupportedLanguages.Dart]: ['.dart'],
[SupportedLanguages.Vue]: ['.vue'],
[SupportedLanguages.Cobol]: ['.cbl', '.cob', '.cpy', '.cobol'],
[SupportedLanguages.Julia]: ['.jl'],
} satisfies Record<SupportedLanguages, readonly string[]>; // Ensure exhaustiveness
/** Pre-built reverse lookup: extension → language (built once at module load). */
@ -111,6 +112,7 @@ const SYNTAX_MAP: Record<SupportedLanguages, string> = {
[SupportedLanguages.Dart]: 'dart',
[SupportedLanguages.Vue]: 'typescript',
[SupportedLanguages.Cobol]: 'cobol',
[SupportedLanguages.Julia]: 'julia',
} satisfies Record<SupportedLanguages, string>; // Ensure exhaustiveness
/** Non-code file extensions → Prism-compatible syntax identifiers */

View file

@ -22,4 +22,5 @@ export enum SupportedLanguages {
Vue = 'vue',
/** Standalone regex processor — no tree-sitter, no LanguageProvider. */
Cobol = 'cobol',
Julia = 'julia',
}

View file

@ -41,6 +41,7 @@ export const LanguageClassifications: Readonly<Record<SupportedLanguages, Langua
[SupportedLanguages.Dart]: 'production',
[SupportedLanguages.Vue]: 'experimental',
[SupportedLanguages.Cobol]: 'experimental',
[SupportedLanguages.Julia]: 'experimental',
};
/** Convenience predicate: is this language gating Ring 4 retirement? */

View file

@ -39,6 +39,7 @@
"tree-sitter-go": "^0.23.0",
"tree-sitter-java": "^0.23.5",
"tree-sitter-javascript": "^0.23.0",
"tree-sitter-julia": "^0.23.1",
"tree-sitter-php": "^0.23.0",
"tree-sitter-python": "0.23.4",
"tree-sitter-ruby": "^0.23.1",
@ -4908,6 +4909,25 @@
}
}
},
"node_modules/tree-sitter-julia": {
"version": "0.23.1",
"resolved": "https://registry.npmmirror.com/tree-sitter-julia/-/tree-sitter-julia-0.23.1.tgz",
"integrity": "sha512-3vShY0GIu8ajR6hXzE0pyUk6kkfg4pGx3Bfzm6lGmR9aC3fe+LgoBMlaFJ7JY+t0fNFccc77J8HVP67ukuDMxQ==",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
"node-addon-api": "^8.2.2",
"node-gyp-build": "^4.8.2"
},
"peerDependencies": {
"tree-sitter": "^0.21.1"
},
"peerDependenciesMeta": {
"tree-sitter": {
"optional": true
}
}
},
"node_modules/tree-sitter-kotlin": {
"version": "0.3.8",
"resolved": "https://registry.npmjs.org/tree-sitter-kotlin/-/tree-sitter-kotlin-0.3.8.tgz",

View file

@ -82,6 +82,7 @@
"tree-sitter-go": "^0.23.0",
"tree-sitter-java": "^0.23.5",
"tree-sitter-javascript": "^0.23.0",
"tree-sitter-julia": "^0.23.1",
"tree-sitter-php": "^0.23.0",
"tree-sitter-python": "0.23.4",
"tree-sitter-ruby": "^0.23.1",

View file

@ -0,0 +1,6 @@
import { SupportedLanguages } from 'gitnexus-shared';
import type { CallExtractionConfig } from '../../call-types.js';
export const juliaCallConfig: CallExtractionConfig = {
language: SupportedLanguages.Julia,
};

View file

@ -0,0 +1,8 @@
import { SupportedLanguages } from 'gitnexus-shared';
import type { ClassExtractionConfig } from '../../class-types.js';
export const juliaClassConfig: ClassExtractionConfig = {
language: SupportedLanguages.Julia,
typeDeclarationNodes: ['struct_definition', 'abstract_definition'],
ancestorScopeNodeTypes: ['struct_definition', 'abstract_definition', 'module_definition'],
};

View file

@ -246,3 +246,31 @@ export const rubyExportChecker: ExportChecker = (_node, _name) => true;
/** Dart: public if no leading underscore (convention, same as Python). */
export const dartExportChecker: ExportChecker = (_node, name) => !name.startsWith('_');
/**
* Julia: walk ancestors looking for an export_statement.
* Julia uses explicit `export` keyword to mark public symbols.
* Symbols not in an export list are still accessible via qualified names
* but are not exported — we treat un-exported symbols as non-public.
* Fallback: if no export statement is found in the module, treat all
* non-underscore names as public (common for scripts without explicit exports).
*/
export const juliaExportChecker: ExportChecker = (node, name) => {
if (name.startsWith('_')) return false;
let current: SyntaxNode | null = node;
while (current) {
if (current.type === 'module_definition') {
// Check siblings inside module body for export_statement listing this name
const body = current.childForFieldName('body') ?? current;
for (let i = 0; i < body.childCount; i++) {
const child = body.child(i);
if (child?.type === 'export_statement' && child.text?.includes(name)) return true;
}
// Module found but name not in any export — treat as internal
return false;
}
current = current.parent;
}
// Top-level (script) — treat all non-underscore names as public
return true;
};

View file

@ -0,0 +1,58 @@
// gitnexus/src/core/ingestion/field-extractors/configs/julia.ts
// Verified against tree-sitter-julia grammar
import { SupportedLanguages } from 'gitnexus-shared';
import type { FieldExtractionConfig } from '../generic.js';
/**
* Julia field extraction config.
*
* Julia struct fields appear as typed or untyped identifiers inside struct bodies:
* struct Point
* x::Float64
* y::Float64
* end
*
* In tree-sitter-julia the struct body contains field declarations.
* Each field is either an identifier or a typed_parameter node.
*/
export const juliaConfig: FieldExtractionConfig = {
language: SupportedLanguages.Julia,
typeDeclarationNodes: ['struct_definition'],
fieldNodeTypes: ['identifier', 'typed_parameter'],
bodyNodeTypes: ['field_declaration_list'],
defaultVisibility: 'public',
extractName(node) {
if (node.type === 'identifier') return node.text;
if (node.type === 'typed_parameter') {
const nameNode = node.childForFieldName('name') ?? node.firstNamedChild;
return nameNode?.type === 'identifier' ? nameNode.text : undefined;
}
return undefined;
},
extractType(node) {
if (node.type === 'typed_parameter') {
const typeNode = node.childForFieldName('type') ?? node.namedChild(1);
return typeNode?.text?.trim();
}
return undefined;
},
extractVisibility(_node) {
// Julia struct fields are always public (no access modifiers)
return 'public';
},
isStatic(_node) {
return false;
},
isReadonly(_node) {
// Immutable structs (struct, not mutable struct) have readonly fields.
// The mutable keyword is on the parent struct_definition, not on individual fields.
// We return false here; the struct-level immutability is handled elsewhere.
return false;
},
};

View file

@ -0,0 +1,33 @@
/**
* Julia import resolution config.
*
* Julia supports two import forms:
* import Foo → namespace import (Foo.bar access)
* using Foo → wildcard import (all exported names in scope)
* using Foo: bar → named import
* import Foo: bar → named import
*
* Within a package, `include("file.jl")` is used for relative file inclusion.
* We handle include() as a special import form resolved via suffix matching.
*/
import { SupportedLanguages } from 'gitnexus-shared';
import type { ImportResolutionConfig, ImportResolverStrategy } from '../types.js';
import { suffixResolve } from '../utils.js';
import { createStandardStrategy } from '../standard.js';
/** Julia include("file.jl") resolution strategy. */
export const juliaIncludeStrategy: ImportResolverStrategy = (rawImportPath, _filePath, ctx) => {
// Strip quotes and resolve as relative .jl path
const cleaned = rawImportPath.replace(/^["']|["']$/g, '');
if (!cleaned.endsWith('.jl')) return null;
const pathParts = cleaned.replace(/^\.\//, '').split('/').filter(Boolean);
const resolved = suffixResolve(pathParts, ctx.normalizedFileList, ctx.allFileList, ctx.index);
return resolved ? { kind: 'files', files: [resolved] } : null;
};
export const juliaImportConfig: ImportResolutionConfig = {
language: SupportedLanguages.Julia,
strategies: [juliaIncludeStrategy, createStandardStrategy(SupportedLanguages.Julia)],
};

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 { juliaProvider } from './julia.js';
export const providers = {
[SupportedLanguages.JavaScript]: javascriptProvider,
@ -43,6 +44,7 @@ export const providers = {
[SupportedLanguages.Dart]: dartProvider,
[SupportedLanguages.Vue]: vueProvider,
[SupportedLanguages.Cobol]: cobolProvider,
[SupportedLanguages.Julia]: juliaProvider,
} satisfies Record<SupportedLanguages, LanguageProvider>;
/** Get provider by language enum (always succeeds for SupportedLanguages). */

View file

@ -0,0 +1,108 @@
/**
* Julia language provider.
*
* Julia uses wildcard-leaf import semantics: `using Foo` brings all exported
* names into scope in one hop. `import Foo` uses namespace access (Foo.bar).
* Within packages, `include("file.jl")` handles relative file inclusion.
*
* Key Julia traits:
* - importSemantics: 'wildcard-leaf' (using Foo / import Foo)
* - No mandatory visibility modifiers — export list controls public API
* - Multiple dispatch: functions specialize on argument types
* - Structs are class-like (no methods in body; methods defined externally)
*/
import { SupportedLanguages } from 'gitnexus-shared';
import { createClassExtractor } from '../class-extractors/generic.js';
import { juliaClassConfig } from '../class-extractors/configs/julia.js';
import { defineLanguage } from '../language-provider.js';
import { typeConfig as juliaTypeConfig } from '../type-extractors/julia.js';
import { juliaExportChecker } from '../export-detection.js';
import { createImportResolver } from '../import-resolvers/resolver-factory.js';
import { juliaImportConfig } from '../import-resolvers/configs/julia.js';
import { JULIA_QUERIES } from '../tree-sitter-queries.js';
import { createFieldExtractor } from '../field-extractors/generic.js';
import { juliaConfig as juliaFieldConfig } from '../field-extractors/configs/julia.js';
import { createMethodExtractor } from '../method-extractors/generic.js';
import { juliaMethodConfig } from '../method-extractors/configs/julia.js';
import { createVariableExtractor } from '../variable-extractors/generic.js';
import { juliaVariableConfig } from '../variable-extractors/configs/julia.js';
import { createCallExtractor } from '../call-extractors/generic.js';
import { juliaCallConfig } from '../call-extractors/configs/julia.js';
import { createHeritageExtractor } from '../heritage-extractors/generic.js';
const BUILT_INS: ReadonlySet<string> = new Set([
'print',
'println',
'typeof',
'length',
'size',
'push!',
'pop!',
'append!',
'map',
'filter',
'reduce',
'sum',
'prod',
'minimum',
'maximum',
'sort',
'sort!',
'collect',
'enumerate',
'zip',
'keys',
'values',
'haskey',
'get',
'getindex',
'setindex!',
'error',
'throw',
'isa',
'isdefined',
'isnothing',
'ismissing',
'eltype',
'zeros',
'ones',
'rand',
'randn',
'copy',
'deepcopy',
'string',
'Symbol',
'parse',
'convert',
'promote',
'similar',
'fill',
'repeat',
'reshape',
'hcat',
'vcat',
'cat',
'tuple',
'nameof',
'supertype',
'subtypes',
]);
export const juliaProvider = defineLanguage({
id: SupportedLanguages.Julia,
extensions: ['.jl'],
entryPointPatterns: [/^main$/, /^run$/, /^execute$/],
treeSitterQueries: JULIA_QUERIES,
typeConfig: juliaTypeConfig,
exportChecker: juliaExportChecker,
importResolver: createImportResolver(juliaImportConfig),
importSemantics: 'wildcard-leaf',
callExtractor: createCallExtractor(juliaCallConfig),
fieldExtractor: createFieldExtractor(juliaFieldConfig),
methodExtractor: createMethodExtractor(juliaMethodConfig),
variableExtractor: createVariableExtractor(juliaVariableConfig),
classExtractor: createClassExtractor(juliaClassConfig),
heritageExtractor: createHeritageExtractor(SupportedLanguages.Julia),
builtInNames: BUILT_INS,
});

View file

@ -0,0 +1,119 @@
// gitnexus/src/core/ingestion/method-extractors/configs/julia.ts
// Verified against tree-sitter-julia grammar
import { SupportedLanguages } from 'gitnexus-shared';
import type { MethodExtractionConfig, ParameterInfo } from '../../method-types.js';
import type { SyntaxNode } from '../../utils/ast-helpers.js';
/**
* Extract parameters from a Julia function definition.
*
* Julia parameters can be:
* - Plain identifier: `x`
* - Typed: `x::Int`
* - With default: `x=1`
* - Typed with default: `x::Int=1`
* - Splat: `args...`
* - Keyword: `; kw=default`
*/
function extractJuliaParameters(node: SyntaxNode): ParameterInfo[] {
const paramList = node.childForFieldName('parameters');
if (!paramList) return [];
const params: ParameterInfo[] = [];
for (let i = 0; i < paramList.namedChildCount; i++) {
const param = paramList.namedChild(i);
if (!param) continue;
switch (param.type) {
case 'identifier': {
params.push({ name: param.text, type: null, rawType: null, isOptional: false, isVariadic: false });
break;
}
case 'typed_parameter': {
const nameNode = param.childForFieldName('name') ?? param.firstNamedChild;
const typeNode = param.childForFieldName('type') ?? param.namedChild(1);
if (nameNode) {
params.push({
name: nameNode.text,
type: typeNode?.text?.trim() ?? null,
rawType: typeNode?.text?.trim() ?? null,
isOptional: false,
isVariadic: false,
});
}
break;
}
case 'optional_parameter': {
const nameNode = param.childForFieldName('name') ?? param.firstNamedChild;
const typeNode = param.childForFieldName('type');
if (nameNode) {
params.push({
name: nameNode.text,
type: typeNode?.text?.trim() ?? null,
rawType: typeNode?.text?.trim() ?? null,
isOptional: true,
isVariadic: false,
});
}
break;
}
case 'splat_parameter': {
const nameNode = param.firstNamedChild;
if (nameNode) {
params.push({ name: nameNode.text, type: null, rawType: null, isOptional: false, isVariadic: true });
}
break;
}
default:
break;
}
}
return params;
}
function extractJuliaReturnType(node: SyntaxNode): string | undefined {
const retType = node.childForFieldName('return_type');
return retType?.text?.trim();
}
export const juliaMethodConfig: MethodExtractionConfig = {
language: SupportedLanguages.Julia,
// Julia methods are defined outside structs — no struct body method extraction
typeDeclarationNodes: ['struct_definition'],
methodNodeTypes: ['function_definition', 'short_function_definition'],
bodyNodeTypes: ['field_declaration_list'],
extractName(node) {
const nameNode = node.childForFieldName('name');
return nameNode?.text;
},
extractReturnType: extractJuliaReturnType,
extractParameters: extractJuliaParameters,
extractVisibility(_node) {
return 'public';
},
isStatic(_node) {
return false;
},
isAbstract(_node) {
return false;
},
isFinal(_node) {
return false;
},
extractAnnotations(_node) {
return [];
},
isAsync(_node) {
return false;
},
};

View file

@ -1512,6 +1512,61 @@ export const DART_QUERIES = `
(type_identifier) @heritage.trait))) @heritage
`;
// Julia queries - works with tree-sitter-julia
export const JULIA_QUERIES = `
; ── Structs (class-like types) ───────────────────────────────────────────────
(struct_definition
name: (identifier) @name) @definition.class
; ── Abstract types ────────────────────────────────────────────────────────────
(abstract_definition
name: (identifier) @name) @definition.class
; ── Modules ───────────────────────────────────────────────────────────────────
(module_definition
name: (identifier) @name) @definition.module
; ── Function definitions ──────────────────────────────────────────────────────
(function_definition
name: (identifier) @name) @definition.function
; ── Short-form function definitions: f(x) = expr ─────────────────────────────
(short_function_definition
name: (identifier) @name) @definition.function
; ── Macro definitions ─────────────────────────────────────────────────────────
(macro_definition
name: (identifier) @name) @definition.function
; ── Import statements ─────────────────────────────────────────────────────────
(import_statement) @import
(using_statement) @import
; ── Call expressions ──────────────────────────────────────────────────────────
(call_expression
(identifier) @call.name) @call
(call_expression
(field_expression
(identifier) @call.name)) @call
; ── Heritage: struct Foo <: Bar ───────────────────────────────────────────────
(struct_definition
name: (identifier) @heritage.class
supertype: (type_clause
(identifier) @heritage.extends)) @heritage
(abstract_definition
name: (identifier) @heritage.class
supertype: (type_clause
(identifier) @heritage.extends)) @heritage
; ── Variable assignments at module scope ──────────────────────────────────────
(assignment
left: (identifier) @name) @definition.variable
`;
import { SupportedLanguages } from 'gitnexus-shared';
export const LANGUAGE_QUERIES: Record<SupportedLanguages, string> = {
@ -1531,4 +1586,5 @@ export const LANGUAGE_QUERIES: Record<SupportedLanguages, string> = {
[SupportedLanguages.Dart]: DART_QUERIES,
[SupportedLanguages.Vue]: TYPESCRIPT_QUERIES, // Vue <script> blocks are parsed as TypeScript
[SupportedLanguages.Cobol]: '', // Standalone regex processor — no tree-sitter queries
[SupportedLanguages.Julia]: JULIA_QUERIES,
};

View file

@ -0,0 +1,123 @@
import type {
LanguageTypeConfig,
ParameterExtractor,
TypeBindingExtractor,
InitializerExtractor,
ConstructorBindingScanner,
PendingAssignmentExtractor,
} from './types.js';
import { extractSimpleTypeName, extractVarName } from './shared.js';
import type { SyntaxNode } from '../utils/ast-helpers.js';
/**
* Julia type extractor.
*
* Julia uses `::` for type annotations:
* x::Int
* function foo(x::Int, y::String)::Bool
*
* Constructor inference: `obj = MyType(args...)` binds `obj` → `MyType`.
*/
const DECLARATION_NODE_TYPES: ReadonlySet<string> = new Set([
'function_definition',
'short_function_definition',
'assignment',
]);
/**
* Extract type annotations from function parameter lists into env.
* Julia: `function foo(x::Int, y::String)` → env.set("x", "Int")
*/
const extractDeclaration: TypeBindingExtractor = (node: SyntaxNode, env: Map<string, string>): void => {
if (node.type !== 'function_definition' && node.type !== 'short_function_definition') return;
const paramList = node.childForFieldName('parameters');
if (!paramList) return;
for (let i = 0; i < paramList.namedChildCount; i++) {
const param = paramList.namedChild(i);
if (!param) continue;
if (param.type === 'typed_parameter') {
const nameNode = param.childForFieldName('name') ?? param.firstNamedChild;
const typeNode = param.childForFieldName('type') ?? param.namedChild(1);
if (nameNode && typeNode) {
const varName = extractVarName(nameNode);
const typeName = extractSimpleTypeName(typeNode) ?? typeNode.text?.trim();
if (varName && typeName) env.set(varName, typeName);
}
}
}
};
const extractParameter: ParameterExtractor = (_node: SyntaxNode, _env: Map<string, string>): void => {
// Parameter types are handled by extractDeclaration above
};
/**
* Julia constructor inference: obj = MyType(args...)
* MyType() is a constructor call when MyType matches a known type name.
*/
const extractInitializer: InitializerExtractor = (node, env, classNames): void => {
if (node.type !== 'assignment') return;
const left = node.childForFieldName('left');
const right = node.childForFieldName('right');
if (!left || !right) return;
const varName = extractVarName(left);
if (!varName || env.has(varName)) return;
// call_expression where the callee is an identifier matching a known type
if (right.type === 'call_expression') {
const callee = right.firstNamedChild ?? right.child(0);
if (callee?.type === 'identifier' && classNames.has(callee.text)) {
env.set(varName, callee.text);
}
}
};
const scanConstructorBinding: ConstructorBindingScanner = (node) => {
if (node.type !== 'assignment') return undefined;
const left = node.childForFieldName('left');
const right = node.childForFieldName('right');
if (!left || !right) return undefined;
const varName = extractVarName(left);
if (!varName) return undefined;
if (right.type === 'call_expression') {
const callee = right.firstNamedChild ?? right.child(0);
if (callee?.type === 'identifier') {
return { varName, calleeName: callee.text };
}
}
return undefined;
};
const extractPendingAssignment: PendingAssignmentExtractor = (node, scopeEnv) => {
if (node.type !== 'assignment') return undefined;
const lhsNode = node.childForFieldName('left');
if (!lhsNode || lhsNode.type !== 'identifier') return undefined;
const varName = lhsNode.text;
if (scopeEnv.has(varName)) return undefined;
const rhsNode = node.childForFieldName('right');
if (!rhsNode) return undefined;
if (rhsNode.type === 'identifier') return { kind: 'copy', lhs: varName, rhs: rhsNode.text };
if (rhsNode.type === 'call_expression') {
const callee = rhsNode.firstNamedChild ?? rhsNode.child(0);
if (callee?.type === 'identifier') {
return { kind: 'callResult', lhs: varName, callee: callee.text };
}
}
return undefined;
};
export const typeConfig: LanguageTypeConfig = {
declarationNodeTypes: DECLARATION_NODE_TYPES,
extractDeclaration,
extractParameter,
extractInitializer,
scanConstructorBinding,
extractPendingAssignment,
};

View file

@ -0,0 +1,62 @@
// gitnexus/src/core/ingestion/variable-extractors/configs/julia.ts
import { SupportedLanguages } from 'gitnexus-shared';
import type { VariableExtractionConfig, VariableVisibility } from '../../variable-types.js';
import type { SyntaxNode } from '../../utils/ast-helpers.js';
function extractNameFromJulia(node: SyntaxNode): string | undefined {
// assignment: x = value
if (node.type === 'assignment') {
const left = node.childForFieldName('left');
if (left?.type === 'identifier') return left.text;
}
return undefined;
}
function extractTypeFromJulia(node: SyntaxNode): string | undefined {
// Julia module-level assignments rarely carry inline type annotations.
// Typed declarations would be `x::Int = 5` via typed_parameter, but that's uncommon at module scope.
if (node.type === 'assignment') {
const left = node.childForFieldName('left');
if (left?.type === 'typed_parameter') {
const typeNode = left.childForFieldName('type') ?? left.namedChild(1);
return typeNode?.text?.trim();
}
}
return undefined;
}
function extractVisFromJulia(node: SyntaxNode): VariableVisibility {
const name = extractNameFromJulia(node);
if (!name) return 'public';
if (name.startsWith('_')) return 'private';
return 'public';
}
export const juliaVariableConfig: VariableExtractionConfig = {
language: SupportedLanguages.Julia,
constNodeTypes: [],
staticNodeTypes: [],
variableNodeTypes: ['assignment'],
extractName: extractNameFromJulia,
extractType: extractTypeFromJulia,
extractVisibility: extractVisFromJulia,
isConst(node) {
// Julia convention: SCREAMING_SNAKE_CASE names are constants
const name = extractNameFromJulia(node);
if (!name) return false;
return name === name.toUpperCase() && /^[A-Z][A-Z0-9_]*$/.test(name);
},
isStatic(_node) {
return false;
},
isMutable(node) {
const name = extractNameFromJulia(node);
if (!name) return true;
return !(name === name.toUpperCase() && /^[A-Z][A-Z0-9_]*$/.test(name));
},
};

View file

@ -161,6 +161,11 @@ const SOURCES: Record<string, GrammarSource> = {
'Kotlin parsing disabled: `tree-sitter-kotlin` is an optionalDependency ' +
'and is not installed (or its native binding failed to build).',
},
[SupportedLanguages.Julia]: {
load: () => _require('tree-sitter-julia'),
unavailableNote:
'Julia parsing requires `tree-sitter-julia`. Check that the package and its native binding installed cleanly (`npm ci`).',
},
};
type LoadResult =