Skip to content

Commit 7bf7b46

Browse files
committed
feat(objectscript): add InterSystems IRIS ObjectScript language support
Add ObjectScript (InterSystems IRIS / Caché) as a supported language, covering the UDL class format (.cls), MAC/INT routines (.mac/.int/.rtn), include/macro files (.inc), and IRIS Studio Export XML. Definition extraction (extract_defs.c): Class, Method, ClassMethod, Property, Parameter, Index, Trigger (with body text), XData, Storage, and Query members as graph nodes; base classes from the Extends clause. Call dispatch resolution (extract_calls.c) — four ObjectScript patterns that are structurally invisible to text search: 1. ##class(Pkg.Class).Method() explicit cross-class call 2. ..Method() relative-dot self-call (the dominant intra-class form; large impact on CALLS completeness) 3. $$$Macro macro expansion via a per-project table built from .inc files 4. type inference from %New/%OpenId + declared return types Ensemble production topology (pass_ensemble_routing.c): EnsembleItem nodes per production component and ROUTES_TO edges resolved from ProductionDefinition XData, plus WorkMgr .Queue("##class(X).method") dispatch — all parsed statically at index time, no live IRIS required. Language detection (language.c): .mac/.int/.rtn map to ObjectScript routine directly; .cls (shared with Apex) and .inc (shared with BitBake) are disambiguated by content, defaulting to the existing language on any doubt so neither Apex nor BitBake detection regresses. The two new per-project tables (macros, return types) are threaded through a new internal cbm_extract_file_ex() so the public cbm_extract_file() signature is unchanged. The tree-sitter grammars for ObjectScript UDL and routine are vendored in internal/cbm/vendored/grammars/objectscript_{udl,routine}/ from https://github.com/intersystems/tree-sitter-objectscript (MIT, ABI 15). Refs #462 Signed-off-by: Thomas Dyar <tdyar@intersystems.com>
1 parent 9987a4e commit 7bf7b46

28 files changed

Lines changed: 3761 additions & 218 deletions

Makefile.cbm

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,8 @@ EXTRACTION_SRCS = \
154154
$(CBM_DIR)/extract_k8s.c \
155155
$(CBM_DIR)/helpers.c \
156156
$(CBM_DIR)/lang_specs.c \
157+
$(CBM_DIR)/macro_table.c \
158+
$(CBM_DIR)/iris_export_xml.c \
157159
$(CBM_DIR)/service_patterns.c
158160

159161
# LSP resolvers (compiled as one unit via lsp_all.c)
@@ -237,6 +239,7 @@ PIPELINE_SRCS = \
237239
src/pipeline/pass_semantic_edges.c \
238240
src/pipeline/pass_complexity.c \
239241
src/pipeline/pass_cross_repo.c \
242+
src/pipeline/pass_ensemble_routing.c \
240243
src/pipeline/artifact.c \
241244
src/pipeline/pass_pkgmap.c
242245

internal/cbm/cbm.c

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -724,6 +724,15 @@ static void cbm_test_fault_inject(const char *rel_path) {
724724
CBMFileResult *cbm_extract_file(const char *source, int source_len, CBMLanguage language,
725725
const char *project, const char *rel_path, int64_t timeout_micros,
726726
const char **extra_defines, const char **include_paths) {
727+
return cbm_extract_file_ex(source, source_len, language, project, rel_path, timeout_micros,
728+
extra_defines, include_paths, NULL, NULL);
729+
}
730+
731+
CBMFileResult *cbm_extract_file_ex(const char *source, int source_len, CBMLanguage language,
732+
const char *project, const char *rel_path,
733+
int64_t timeout_micros, const char **extra_defines,
734+
const char **include_paths, const CBMMacroTable *macro_table,
735+
const CBMReturnTypeTable *return_type_table) {
727736
// Allocate result on heap (arena inside for all string data)
728737
enum { SINGLE = 1 };
729738
CBMFileResult *result = (CBMFileResult *)calloc(SINGLE, sizeof(CBMFileResult));
@@ -825,6 +834,8 @@ CBMFileResult *cbm_extract_file(const char *source, int source_len, CBMLanguage
825834
.rel_path = rel_path,
826835
.module_qn = result->module_qn,
827836
.root = root,
837+
.macro_table = macro_table,
838+
.return_type_table = return_type_table,
828839
};
829840

830841
// Run extractors: defs + imports use separate walks (unique recursion patterns),

internal/cbm/cbm.h

Lines changed: 44 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -164,12 +164,15 @@ typedef enum {
164164
CBM_LANG_APEX,
165165
CBM_LANG_SOQL,
166166
CBM_LANG_SOSL,
167-
CBM_LANG_KUSTOMIZE, // kustomization.yaml — Kubernetes overlay tool
168-
CBM_LANG_K8S, // Generic Kubernetes manifest (apiVersion: detected)
169-
CBM_LANG_PINE, // Pine Script (TradingView indicator / strategy language)
170-
CBM_LANG_QML, // Qt QML (Qt Modeling Language — declarative UI + embedded JS)
171-
CBM_LANG_CFSCRIPT, // CFML script dialect (.cfc components — Lucee/ColdFusion)
172-
CBM_LANG_CFML, // CFML tag dialect (.cfm templates — Lucee/ColdFusion)
167+
CBM_LANG_KUSTOMIZE, // kustomization.yaml — Kubernetes overlay tool
168+
CBM_LANG_K8S, // Generic Kubernetes manifest (apiVersion: detected)
169+
CBM_LANG_PINE, // Pine Script (TradingView indicator / strategy language)
170+
CBM_LANG_QML, // Qt QML (Qt Modeling Language — declarative UI + embedded JS)
171+
CBM_LANG_CFSCRIPT, // CFML script dialect (.cfc components — Lucee/ColdFusion)
172+
CBM_LANG_CFML, // CFML tag dialect (.cfm templates — Lucee/ColdFusion)
173+
CBM_LANG_OBJECTSCRIPT_UDL, // InterSystems ObjectScript UDL (.cls class files)
174+
CBM_LANG_OBJECTSCRIPT_ROUTINE, // InterSystems ObjectScript routine (.mac/.int/.rtn/.inc)
175+
CBM_LANG_OBJECTSCRIPT_EXPORT, // InterSystems Studio Export XML (<Export generator="Cache">)
173176
CBM_LANG_COUNT
174177
} CBMLanguage;
175178

@@ -488,6 +491,24 @@ typedef struct {
488491
int count;
489492
} CBMStringConstantMap;
490493

494+
// Forward declaration: ObjectScript macro table (defined in macro_table.h).
495+
typedef struct CBMMacroTable CBMMacroTable;
496+
497+
// Method-return-type table for ObjectScript variable type inference. Populated
498+
// from definition nodes (method QN -> declared return type) so a later
499+
// `Set x = obj.Method()` can resolve x's class.
500+
#define CBM_RETURN_TYPE_TABLE_CAP 2048
501+
502+
typedef struct {
503+
const char *method_qn;
504+
const char *return_type;
505+
} CBMReturnTypeEntry;
506+
507+
typedef struct {
508+
CBMReturnTypeEntry entries[CBM_RETURN_TYPE_TABLE_CAP];
509+
int count;
510+
} CBMReturnTypeTable;
511+
491512
typedef struct {
492513
CBMArena *arena;
493514
CBMFileResult *result;
@@ -498,9 +519,11 @@ typedef struct {
498519
const char *rel_path;
499520
const char *module_qn;
500521
TSNode root;
501-
EFCache ef_cache; // enclosing function cache
502-
const char *enclosing_class_qn; // for nested class QN computation
503-
CBMStringConstantMap string_constants; // module-level NAME = "value" pairs
522+
EFCache ef_cache; // enclosing function cache
523+
const char *enclosing_class_qn; // for nested class QN computation
524+
CBMStringConstantMap string_constants; // module-level NAME = "value" pairs
525+
const CBMMacroTable *macro_table; // ObjectScript $$$macro table (NULL if none)
526+
const CBMReturnTypeTable *return_type_table; // ObjectScript method return types (NULL if none)
504527
} CBMExtractCtx;
505528

506529
// --- Public API ---
@@ -541,6 +564,18 @@ CBMFileResult *cbm_extract_file(const char *source, int source_len, CBMLanguage
541564
const char **include_paths // NULL-terminated, or NULL
542565
);
543566

567+
// Pipeline-internal variant of cbm_extract_file() carrying ObjectScript
568+
// per-project tables (macro table + method-return-type table). The public
569+
// cbm_extract_file() is a thin wrapper that passes NULL, NULL for both.
570+
CBMFileResult *cbm_extract_file_ex(
571+
const char *source, int source_len, CBMLanguage language, const char *project,
572+
const char *rel_path, int64_t timeout_micros,
573+
const char **extra_defines, // NULL-terminated, or NULL
574+
const char **include_paths, // NULL-terminated, or NULL
575+
const CBMMacroTable *macro_table, // ObjectScript macros, or NULL
576+
const CBMReturnTypeTable *return_type_table // OS return types, or NULL
577+
);
578+
544579
// Free all memory associated with a result.
545580
void cbm_free_result(CBMFileResult *result);
546581

internal/cbm/extract_calls.c

Lines changed: 207 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
#include "arena.h" // CBMArena, cbm_arena_sprintf
33
#include "helpers.h"
44
#include "lang_specs.h"
5+
#include "macro_table.h"
56
#include "extract_unified.h"
67
#include "foundation/constants.h"
78
#include "extract_node_stack.h"
@@ -1160,6 +1161,60 @@ static char *extract_callee_lang_specific(CBMArena *a, TSNode node, const char *
11601161
return c;
11611162
}
11621163
}
1164+
if (lang == CBM_LANG_OBJECTSCRIPT_UDL || lang == CBM_LANG_OBJECTSCRIPT_ROUTINE) {
1165+
// ##class(Pkg.Class).Method() -> "Pkg.Class.Method"
1166+
if (strcmp(nk, "class_method_call") == 0) {
1167+
TSNode class_ref = cbm_find_child_by_kind(node, "class_ref");
1168+
TSNode method_name = cbm_find_child_by_kind(node, "method_name");
1169+
if (!ts_node_is_null(class_ref) && !ts_node_is_null(method_name)) {
1170+
TSNode cname = cbm_find_child_by_kind(class_ref, "class_name");
1171+
if (ts_node_is_null(cname)) {
1172+
return NULL;
1173+
}
1174+
char *cls = cbm_node_text(a, cname, source);
1175+
if (!cls || !cls[0]) {
1176+
return NULL;
1177+
}
1178+
TSNode mname_ident = ts_node_named_child_count(method_name) > 0
1179+
? ts_node_named_child(method_name, 0)
1180+
: (TSNode){0};
1181+
if (ts_node_is_null(mname_ident)) {
1182+
return cls;
1183+
}
1184+
char *meth = cbm_node_text(a, mname_ident, source);
1185+
if (!meth || !meth[0]) {
1186+
return cls;
1187+
}
1188+
return cbm_arena_sprintf(a, "%s.%s", cls, meth);
1189+
}
1190+
return NULL;
1191+
}
1192+
// $$label^routine extrinsic / routine tag call -> the line_ref text
1193+
if (strcmp(nk, "routine_tag_call") == 0) {
1194+
TSNode line_ref = cbm_find_child_by_kind(node, "line_ref");
1195+
if (!ts_node_is_null(line_ref)) {
1196+
return cbm_node_text(a, line_ref, source);
1197+
}
1198+
return NULL;
1199+
}
1200+
// $$$Macro(...) -> raw "$$$Name" callee (expanded later in handle_calls)
1201+
if (strcmp(nk, "macro") == 0) {
1202+
char *raw = cbm_node_text(a, node, source);
1203+
if (!raw || raw[0] != '$' || raw[1] != '$' || raw[2] != '$') {
1204+
return NULL;
1205+
}
1206+
char *name_start = raw + 3;
1207+
char *paren = strchr(name_start, '(');
1208+
if (paren) {
1209+
*paren = '\0';
1210+
}
1211+
if (!name_start[0]) {
1212+
return NULL;
1213+
}
1214+
return cbm_arena_sprintf(a, "$$$%s", name_start);
1215+
}
1216+
return NULL;
1217+
}
11631218

11641219
return extract_scripting_callee(a, node, source, lang, nk);
11651220
}
@@ -1866,13 +1921,129 @@ static void extract_java_method_reference(CBMExtractCtx *ctx, TSNode node, const
18661921
cbm_calls_push(&ctx->result->calls, ctx->arena, call);
18671922
}
18681923

1924+
// ObjectScript: resolve `var.Method(...)` / `..Property.Method(...)` instance
1925+
// calls against the per-method variable type map. Returns arena "Class.Method"
1926+
// or NULL if the receiver's type is unknown.
1927+
static char *resolve_objectscript_instance_call(CBMArena *a, TSNode node, const char *source,
1928+
os_type_map_t *type_map) {
1929+
TSNode receiver = {0};
1930+
TSNode oref = {0};
1931+
const char *nk_first = NULL;
1932+
for (uint32_t i = 0; i < ts_node_named_child_count(node); i++) {
1933+
TSNode child = ts_node_named_child(node, i);
1934+
const char *ck = ts_node_type(child);
1935+
if (strcmp(ck, "lvn") == 0 || strcmp(ck, "variable") == 0) {
1936+
receiver = child;
1937+
} else if (strcmp(ck, "relative_dot_property") == 0) {
1938+
receiver = child;
1939+
nk_first = "relative_dot_property";
1940+
} else if (strcmp(ck, "oref_method") == 0) {
1941+
oref = child;
1942+
}
1943+
}
1944+
if (ts_node_is_null(oref)) {
1945+
return NULL;
1946+
}
1947+
TSNode method_name_node = cbm_find_child_by_kind(oref, "method_name");
1948+
if (ts_node_is_null(method_name_node)) {
1949+
return NULL;
1950+
}
1951+
TSNode mn_ident = ts_node_named_child_count(method_name_node) > 0
1952+
? ts_node_named_child(method_name_node, 0)
1953+
: (TSNode){0};
1954+
if (ts_node_is_null(mn_ident)) {
1955+
return NULL;
1956+
}
1957+
char *method = cbm_node_text(a, mn_ident, source);
1958+
if (!method || !method[0]) {
1959+
return NULL;
1960+
}
1961+
if (ts_node_is_null(receiver)) {
1962+
return NULL;
1963+
}
1964+
char *var_text = NULL;
1965+
if (nk_first && strcmp(nk_first, "relative_dot_property") == 0) {
1966+
TSNode prop_name = cbm_find_child_by_kind(receiver, "member_name");
1967+
if (!ts_node_is_null(prop_name)) {
1968+
char *pname = cbm_node_text(a, prop_name, source);
1969+
if (pname && pname[0]) {
1970+
var_text = cbm_arena_sprintf(a, "..%s", pname);
1971+
}
1972+
}
1973+
if (!var_text) {
1974+
var_text = cbm_node_text(a, receiver, source);
1975+
}
1976+
} else {
1977+
var_text = cbm_node_text(a, receiver, source);
1978+
}
1979+
if (!var_text || !var_text[0]) {
1980+
return NULL;
1981+
}
1982+
for (int i = 0; i < type_map->count; i++) {
1983+
if (strcasecmp(type_map->entries[i].var_name, var_text) == 0) {
1984+
return cbm_arena_sprintf(a, "%s.%s", type_map->entries[i].class_name, method);
1985+
}
1986+
}
1987+
return NULL;
1988+
}
1989+
18691990
void handle_calls(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec *spec, WalkState *state) {
18701991
if (!spec->call_node_types || !spec->call_node_types[0]) {
18711992
return;
18721993
}
18731994

18741995
if (cbm_kind_in_set(node, spec->call_node_types)) {
18751996
char *callee = extract_callee_name(ctx->arena, node, ctx->source, ctx->language);
1997+
1998+
// ObjectScript: var.Method() / ..Property.Method() instance dispatch.
1999+
if (!callee &&
2000+
(ctx->language == CBM_LANG_OBJECTSCRIPT_UDL ||
2001+
ctx->language == CBM_LANG_OBJECTSCRIPT_ROUTINE) &&
2002+
strcmp(ts_node_type(node), "method_call") == 0) {
2003+
callee = resolve_objectscript_instance_call(ctx->arena, node, ctx->source,
2004+
&state->os_type_map);
2005+
}
2006+
2007+
// ObjectScript: ..Method() oref self-call resolves against the enclosing class.
2008+
if (!callee &&
2009+
(ctx->language == CBM_LANG_OBJECTSCRIPT_UDL ||
2010+
ctx->language == CBM_LANG_OBJECTSCRIPT_ROUTINE) &&
2011+
strcmp(ts_node_type(node), "relative_dot_method") == 0 && state->enclosing_class_qn &&
2012+
state->enclosing_class_qn[0]) {
2013+
TSNode oref = cbm_find_child_by_kind(node, "oref_method");
2014+
if (!ts_node_is_null(oref)) {
2015+
TSNode mname_node = cbm_find_child_by_kind(oref, "method_name");
2016+
if (!ts_node_is_null(mname_node)) {
2017+
TSNode ident = ts_node_named_child_count(mname_node) > 0
2018+
? ts_node_named_child(mname_node, 0)
2019+
: (TSNode){0};
2020+
if (!ts_node_is_null(ident)) {
2021+
char *mname = cbm_node_text(ctx->arena, ident, ctx->source);
2022+
if (mname && mname[0]) {
2023+
callee = cbm_arena_sprintf(ctx->arena, "%s.%s",
2024+
state->enclosing_class_qn, mname);
2025+
}
2026+
}
2027+
}
2028+
}
2029+
}
2030+
2031+
// ObjectScript: expand a $$$Macro callee via the macro table.
2032+
if (callee && callee[0] == '$' && callee[1] == '$' && callee[2] == '$' &&
2033+
ctx->macro_table) {
2034+
const char *macro_name = callee + 3;
2035+
const CBMMacroEntry *entry = cbm_macro_table_find(ctx->macro_table, macro_name);
2036+
if (entry) {
2037+
if (entry->resolved_callee) {
2038+
callee = cbm_arena_strdup(ctx->arena, entry->resolved_callee);
2039+
} else if (entry->expansion) {
2040+
callee = cbm_macro_extract_callee(ctx->arena, entry->expansion);
2041+
} else {
2042+
callee = NULL;
2043+
}
2044+
}
2045+
}
2046+
18762047
// Keyword-filter callees, but keep builtins we mint a node for (len, str,
18772048
// ...) so the LSP-resolved builtin call still forms a CALLS edge.
18782049
if (callee && callee[0] &&
@@ -1917,12 +2088,47 @@ void handle_calls(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec *spec, Walk
19172088
}
19182089

19192090
TSNode args = ts_node_child_by_field_name(node, TS_FIELD("arguments"));
2091+
// ObjectScript stores args under oref_method/method_args, not the
2092+
// generic "arguments" field.
2093+
if (ts_node_is_null(args) && (ctx->language == CBM_LANG_OBJECTSCRIPT_UDL ||
2094+
ctx->language == CBM_LANG_OBJECTSCRIPT_ROUTINE)) {
2095+
TSNode oref = cbm_find_child_by_kind(node, "oref_method");
2096+
if (!ts_node_is_null(oref)) {
2097+
args = cbm_find_child_by_kind(oref, "method_args");
2098+
}
2099+
if (ts_node_is_null(args)) {
2100+
args = cbm_find_child_by_kind(node, "method_args");
2101+
}
2102+
}
19202103
if (!ts_node_is_null(args)) {
19212104
call.first_string_arg = extract_url_or_topic_arg(ctx, args);
19222105
if (call.first_string_arg && call.first_string_arg[0] == '/') {
19232106
call.second_arg_name = extract_handler_arg(ctx, args);
19242107
}
1925-
extract_call_args(ctx, args, &call);
2108+
if (ctx->language == CBM_LANG_OBJECTSCRIPT_UDL ||
2109+
ctx->language == CBM_LANG_OBJECTSCRIPT_ROUTINE) {
2110+
for (uint32_t ai = 0;
2111+
ai < ts_node_named_child_count(args) && call.arg_count < CBM_MAX_CALL_ARGS;
2112+
ai++) {
2113+
TSNode achild = ts_node_named_child(args, ai);
2114+
const char *ack = ts_node_type(achild);
2115+
if (strcmp(ack, "bracket") == 0) {
2116+
continue;
2117+
}
2118+
if (strcmp(ack, "method_arg") != 0) {
2119+
continue;
2120+
}
2121+
CBMCallArg *ca = &call.args[call.arg_count];
2122+
memset(ca, 0, sizeof(*ca));
2123+
ca->index = call.arg_count;
2124+
ca->expr = cbm_node_text(ctx->arena, achild, ctx->source);
2125+
if (ca->expr && ca->expr[0]) {
2126+
call.arg_count++;
2127+
}
2128+
}
2129+
} else {
2130+
extract_call_args(ctx, args, &call);
2131+
}
19262132
}
19272133

19282134
cbm_calls_push(&ctx->result->calls, ctx->arena, call);

0 commit comments

Comments
 (0)