Skip to content

Commit 358eb60

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 are NOT vendored in this PR; they are a dependency to be vendored separately from https://github.com/intersystems/tree-sitter-objectscript (MIT). The build will not link until the grammar is present. Refs #462 Signed-off-by: Thomas Dyar <tdyar@intersystems.com>
1 parent 170d5a8 commit 358eb60

27 files changed

Lines changed: 3739 additions & 207 deletions

Makefile.cbm

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,8 @@ EXTRACTION_SRCS = \
130130
$(CBM_DIR)/extract_k8s.c \
131131
$(CBM_DIR)/helpers.c \
132132
$(CBM_DIR)/lang_specs.c \
133+
$(CBM_DIR)/macro_table.c \
134+
$(CBM_DIR)/iris_export_xml.c \
133135
$(CBM_DIR)/service_patterns.c
134136

135137
# LSP resolvers (compiled as one unit via lsp_all.c)
@@ -201,6 +203,7 @@ PIPELINE_SRCS = \
201203
src/pipeline/pass_semantic_edges.c \
202204
src/pipeline/pass_complexity.c \
203205
src/pipeline/pass_cross_repo.c \
206+
src/pipeline/pass_ensemble_routing.c \
204207
src/pipeline/artifact.c \
205208
src/pipeline/pass_pkgmap.c
206209

internal/cbm/cbm.c

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -497,6 +497,15 @@ static int count_params_from_signature(const char *sig) {
497497
CBMFileResult *cbm_extract_file(const char *source, int source_len, CBMLanguage language,
498498
const char *project, const char *rel_path, int64_t timeout_micros,
499499
const char **extra_defines, const char **include_paths) {
500+
return cbm_extract_file_ex(source, source_len, language, project, rel_path, timeout_micros,
501+
extra_defines, include_paths, NULL, NULL);
502+
}
503+
504+
CBMFileResult *cbm_extract_file_ex(const char *source, int source_len, CBMLanguage language,
505+
const char *project, const char *rel_path,
506+
int64_t timeout_micros, const char **extra_defines,
507+
const char **include_paths, const CBMMacroTable *macro_table,
508+
const CBMReturnTypeTable *return_type_table) {
500509
// Allocate result on heap (arena inside for all string data)
501510
enum { SINGLE = 1 };
502511
CBMFileResult *result = (CBMFileResult *)calloc(SINGLE, sizeof(CBMFileResult));
@@ -580,6 +589,8 @@ CBMFileResult *cbm_extract_file(const char *source, int source_len, CBMLanguage
580589
.rel_path = rel_path,
581590
.module_qn = result->module_qn,
582591
.root = root,
592+
.macro_table = macro_table,
593+
.return_type_table = return_type_table,
583594
};
584595

585596
// 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

@@ -486,6 +489,24 @@ typedef struct {
486489
int count;
487490
} CBMStringConstantMap;
488491

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

504527
// --- Public API ---
@@ -525,6 +548,18 @@ CBMFileResult *cbm_extract_file(const char *source, int source_len, CBMLanguage
525548
const char **include_paths // NULL-terminated, or NULL
526549
);
527550

551+
// Pipeline-internal variant of cbm_extract_file() carrying ObjectScript
552+
// per-project tables (macro table + method-return-type table). The public
553+
// cbm_extract_file() is a thin wrapper that passes NULL, NULL for both.
554+
CBMFileResult *cbm_extract_file_ex(
555+
const char *source, int source_len, CBMLanguage language, const char *project,
556+
const char *rel_path, int64_t timeout_micros,
557+
const char **extra_defines, // NULL-terminated, or NULL
558+
const char **include_paths, // NULL-terminated, or NULL
559+
const CBMMacroTable *macro_table, // ObjectScript macros, or NULL
560+
const CBMReturnTypeTable *return_type_table // OS return types, or NULL
561+
);
562+
528563
// Free all memory associated with a result.
529564
void cbm_free_result(CBMFileResult *result);
530565

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"
@@ -658,6 +659,60 @@ static char *extract_callee_lang_specific(CBMArena *a, TSNode node, const char *
658659
if (lang == CBM_LANG_SWIFT) {
659660
return extract_swift_callee(a, node, source, nk);
660661
}
662+
if (lang == CBM_LANG_OBJECTSCRIPT_UDL || lang == CBM_LANG_OBJECTSCRIPT_ROUTINE) {
663+
// ##class(Pkg.Class).Method() -> "Pkg.Class.Method"
664+
if (strcmp(nk, "class_method_call") == 0) {
665+
TSNode class_ref = cbm_find_child_by_kind(node, "class_ref");
666+
TSNode method_name = cbm_find_child_by_kind(node, "method_name");
667+
if (!ts_node_is_null(class_ref) && !ts_node_is_null(method_name)) {
668+
TSNode cname = cbm_find_child_by_kind(class_ref, "class_name");
669+
if (ts_node_is_null(cname)) {
670+
return NULL;
671+
}
672+
char *cls = cbm_node_text(a, cname, source);
673+
if (!cls || !cls[0]) {
674+
return NULL;
675+
}
676+
TSNode mname_ident = ts_node_named_child_count(method_name) > 0
677+
? ts_node_named_child(method_name, 0)
678+
: (TSNode){0};
679+
if (ts_node_is_null(mname_ident)) {
680+
return cls;
681+
}
682+
char *meth = cbm_node_text(a, mname_ident, source);
683+
if (!meth || !meth[0]) {
684+
return cls;
685+
}
686+
return cbm_arena_sprintf(a, "%s.%s", cls, meth);
687+
}
688+
return NULL;
689+
}
690+
// $$label^routine extrinsic / routine tag call -> the line_ref text
691+
if (strcmp(nk, "routine_tag_call") == 0) {
692+
TSNode line_ref = cbm_find_child_by_kind(node, "line_ref");
693+
if (!ts_node_is_null(line_ref)) {
694+
return cbm_node_text(a, line_ref, source);
695+
}
696+
return NULL;
697+
}
698+
// $$$Macro(...) -> raw "$$$Name" callee (expanded later in handle_calls)
699+
if (strcmp(nk, "macro") == 0) {
700+
char *raw = cbm_node_text(a, node, source);
701+
if (!raw || raw[0] != '$' || raw[1] != '$' || raw[2] != '$') {
702+
return NULL;
703+
}
704+
char *name_start = raw + 3;
705+
char *paren = strchr(name_start, '(');
706+
if (paren) {
707+
*paren = '\0';
708+
}
709+
if (!name_start[0]) {
710+
return NULL;
711+
}
712+
return cbm_arena_sprintf(a, "$$$%s", name_start);
713+
}
714+
return NULL;
715+
}
661716

662717
return extract_scripting_callee(a, node, source, lang, nk);
663718
}
@@ -1121,13 +1176,129 @@ static void extract_jsx_component_ref(CBMExtractCtx *ctx, TSNode node, const cha
11211176
}
11221177
}
11231178

1179+
// ObjectScript: resolve `var.Method(...)` / `..Property.Method(...)` instance
1180+
// calls against the per-method variable type map. Returns arena "Class.Method"
1181+
// or NULL if the receiver's type is unknown.
1182+
static char *resolve_objectscript_instance_call(CBMArena *a, TSNode node, const char *source,
1183+
os_type_map_t *type_map) {
1184+
TSNode receiver = {0};
1185+
TSNode oref = {0};
1186+
const char *nk_first = NULL;
1187+
for (uint32_t i = 0; i < ts_node_named_child_count(node); i++) {
1188+
TSNode child = ts_node_named_child(node, i);
1189+
const char *ck = ts_node_type(child);
1190+
if (strcmp(ck, "lvn") == 0 || strcmp(ck, "variable") == 0) {
1191+
receiver = child;
1192+
} else if (strcmp(ck, "relative_dot_property") == 0) {
1193+
receiver = child;
1194+
nk_first = "relative_dot_property";
1195+
} else if (strcmp(ck, "oref_method") == 0) {
1196+
oref = child;
1197+
}
1198+
}
1199+
if (ts_node_is_null(oref)) {
1200+
return NULL;
1201+
}
1202+
TSNode method_name_node = cbm_find_child_by_kind(oref, "method_name");
1203+
if (ts_node_is_null(method_name_node)) {
1204+
return NULL;
1205+
}
1206+
TSNode mn_ident = ts_node_named_child_count(method_name_node) > 0
1207+
? ts_node_named_child(method_name_node, 0)
1208+
: (TSNode){0};
1209+
if (ts_node_is_null(mn_ident)) {
1210+
return NULL;
1211+
}
1212+
char *method = cbm_node_text(a, mn_ident, source);
1213+
if (!method || !method[0]) {
1214+
return NULL;
1215+
}
1216+
if (ts_node_is_null(receiver)) {
1217+
return NULL;
1218+
}
1219+
char *var_text = NULL;
1220+
if (nk_first && strcmp(nk_first, "relative_dot_property") == 0) {
1221+
TSNode prop_name = cbm_find_child_by_kind(receiver, "member_name");
1222+
if (!ts_node_is_null(prop_name)) {
1223+
char *pname = cbm_node_text(a, prop_name, source);
1224+
if (pname && pname[0]) {
1225+
var_text = cbm_arena_sprintf(a, "..%s", pname);
1226+
}
1227+
}
1228+
if (!var_text) {
1229+
var_text = cbm_node_text(a, receiver, source);
1230+
}
1231+
} else {
1232+
var_text = cbm_node_text(a, receiver, source);
1233+
}
1234+
if (!var_text || !var_text[0]) {
1235+
return NULL;
1236+
}
1237+
for (int i = 0; i < type_map->count; i++) {
1238+
if (strcasecmp(type_map->entries[i].var_name, var_text) == 0) {
1239+
return cbm_arena_sprintf(a, "%s.%s", type_map->entries[i].class_name, method);
1240+
}
1241+
}
1242+
return NULL;
1243+
}
1244+
11241245
void handle_calls(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec *spec, WalkState *state) {
11251246
if (!spec->call_node_types || !spec->call_node_types[0]) {
11261247
return;
11271248
}
11281249

11291250
if (cbm_kind_in_set(node, spec->call_node_types)) {
11301251
char *callee = extract_callee_name(ctx->arena, node, ctx->source, ctx->language);
1252+
1253+
// ObjectScript: var.Method() / ..Property.Method() instance dispatch.
1254+
if (!callee &&
1255+
(ctx->language == CBM_LANG_OBJECTSCRIPT_UDL ||
1256+
ctx->language == CBM_LANG_OBJECTSCRIPT_ROUTINE) &&
1257+
strcmp(ts_node_type(node), "method_call") == 0) {
1258+
callee = resolve_objectscript_instance_call(ctx->arena, node, ctx->source,
1259+
&state->os_type_map);
1260+
}
1261+
1262+
// ObjectScript: ..Method() oref self-call resolves against the enclosing class.
1263+
if (!callee &&
1264+
(ctx->language == CBM_LANG_OBJECTSCRIPT_UDL ||
1265+
ctx->language == CBM_LANG_OBJECTSCRIPT_ROUTINE) &&
1266+
strcmp(ts_node_type(node), "relative_dot_method") == 0 && state->enclosing_class_qn &&
1267+
state->enclosing_class_qn[0]) {
1268+
TSNode oref = cbm_find_child_by_kind(node, "oref_method");
1269+
if (!ts_node_is_null(oref)) {
1270+
TSNode mname_node = cbm_find_child_by_kind(oref, "method_name");
1271+
if (!ts_node_is_null(mname_node)) {
1272+
TSNode ident = ts_node_named_child_count(mname_node) > 0
1273+
? ts_node_named_child(mname_node, 0)
1274+
: (TSNode){0};
1275+
if (!ts_node_is_null(ident)) {
1276+
char *mname = cbm_node_text(ctx->arena, ident, ctx->source);
1277+
if (mname && mname[0]) {
1278+
callee = cbm_arena_sprintf(ctx->arena, "%s.%s",
1279+
state->enclosing_class_qn, mname);
1280+
}
1281+
}
1282+
}
1283+
}
1284+
}
1285+
1286+
// ObjectScript: expand a $$$Macro callee via the macro table.
1287+
if (callee && callee[0] == '$' && callee[1] == '$' && callee[2] == '$' &&
1288+
ctx->macro_table) {
1289+
const char *macro_name = callee + 3;
1290+
const CBMMacroEntry *entry = cbm_macro_table_find(ctx->macro_table, macro_name);
1291+
if (entry) {
1292+
if (entry->resolved_callee) {
1293+
callee = cbm_arena_strdup(ctx->arena, entry->resolved_callee);
1294+
} else if (entry->expansion) {
1295+
callee = cbm_macro_extract_callee(ctx->arena, entry->expansion);
1296+
} else {
1297+
callee = NULL;
1298+
}
1299+
}
1300+
}
1301+
11311302
if (callee && callee[0] && !cbm_is_keyword(callee, ctx->language)) {
11321303
CBMCall call = {0};
11331304
call.callee_name = callee;
@@ -1145,12 +1316,47 @@ void handle_calls(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec *spec, Walk
11451316
}
11461317

11471318
TSNode args = ts_node_child_by_field_name(node, TS_FIELD("arguments"));
1319+
// ObjectScript stores args under oref_method/method_args, not the
1320+
// generic "arguments" field.
1321+
if (ts_node_is_null(args) && (ctx->language == CBM_LANG_OBJECTSCRIPT_UDL ||
1322+
ctx->language == CBM_LANG_OBJECTSCRIPT_ROUTINE)) {
1323+
TSNode oref = cbm_find_child_by_kind(node, "oref_method");
1324+
if (!ts_node_is_null(oref)) {
1325+
args = cbm_find_child_by_kind(oref, "method_args");
1326+
}
1327+
if (ts_node_is_null(args)) {
1328+
args = cbm_find_child_by_kind(node, "method_args");
1329+
}
1330+
}
11481331
if (!ts_node_is_null(args)) {
11491332
call.first_string_arg = extract_url_or_topic_arg(ctx, args);
11501333
if (call.first_string_arg && call.first_string_arg[0] == '/') {
11511334
call.second_arg_name = extract_handler_arg(ctx, args);
11521335
}
1153-
extract_call_args(ctx, args, &call);
1336+
if (ctx->language == CBM_LANG_OBJECTSCRIPT_UDL ||
1337+
ctx->language == CBM_LANG_OBJECTSCRIPT_ROUTINE) {
1338+
for (uint32_t ai = 0;
1339+
ai < ts_node_named_child_count(args) && call.arg_count < CBM_MAX_CALL_ARGS;
1340+
ai++) {
1341+
TSNode achild = ts_node_named_child(args, ai);
1342+
const char *ack = ts_node_type(achild);
1343+
if (strcmp(ack, "bracket") == 0) {
1344+
continue;
1345+
}
1346+
if (strcmp(ack, "method_arg") != 0) {
1347+
continue;
1348+
}
1349+
CBMCallArg *ca = &call.args[call.arg_count];
1350+
memset(ca, 0, sizeof(*ca));
1351+
ca->index = call.arg_count;
1352+
ca->expr = cbm_node_text(ctx->arena, achild, ctx->source);
1353+
if (ca->expr && ca->expr[0]) {
1354+
call.arg_count++;
1355+
}
1356+
}
1357+
} else {
1358+
extract_call_args(ctx, args, &call);
1359+
}
11541360
}
11551361

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

0 commit comments

Comments
 (0)