Skip to content

Commit 593b161

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 e599df1 commit 593b161

25 files changed

Lines changed: 3299 additions & 12 deletions

Makefile.cbm

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

134136
# LSP resolvers (compiled as one unit via lsp_all.c)
@@ -200,6 +202,7 @@ PIPELINE_SRCS = \
200202
src/pipeline/pass_semantic_edges.c \
201203
src/pipeline/pass_complexity.c \
202204
src/pipeline/pass_cross_repo.c \
205+
src/pipeline/pass_ensemble_routing.c \
203206
src/pipeline/artifact.c \
204207
src/pipeline/pass_pkgmap.c
205208

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: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,9 @@ typedef enum {
170170
CBM_LANG_QML, // Qt QML (Qt Modeling Language — declarative UI + embedded JS)
171171
CBM_LANG_CFSCRIPT, // CFML script dialect (.cfc components — Lucee/ColdFusion)
172172
CBM_LANG_CFML, // CFML tag dialect (.cfm templates — Lucee/ColdFusion)
173+
CBM_LANG_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

@@ -485,6 +488,24 @@ typedef struct {
485488
int count;
486489
} CBMStringConstantMap;
487490

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

503526
// --- Public API ---
@@ -524,6 +547,19 @@ CBMFileResult *cbm_extract_file(const char *source, int source_len, CBMLanguage
524547
const char **include_paths // NULL-terminated, or NULL
525548
);
526549

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

internal/cbm/extract_calls.c

Lines changed: 208 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"
@@ -592,6 +593,60 @@ static char *extract_callee_lang_specific(CBMArena *a, TSNode node, const char *
592593
if (lang == CBM_LANG_SWIFT) {
593594
return extract_swift_callee(a, node, source, nk);
594595
}
596+
if (lang == CBM_LANG_OBJECTSCRIPT_UDL || lang == CBM_LANG_OBJECTSCRIPT_ROUTINE) {
597+
// ##class(Pkg.Class).Method() -> "Pkg.Class.Method"
598+
if (strcmp(nk, "class_method_call") == 0) {
599+
TSNode class_ref = cbm_find_child_by_kind(node, "class_ref");
600+
TSNode method_name = cbm_find_child_by_kind(node, "method_name");
601+
if (!ts_node_is_null(class_ref) && !ts_node_is_null(method_name)) {
602+
TSNode cname = cbm_find_child_by_kind(class_ref, "class_name");
603+
if (ts_node_is_null(cname)) {
604+
return NULL;
605+
}
606+
char *cls = cbm_node_text(a, cname, source);
607+
if (!cls || !cls[0]) {
608+
return NULL;
609+
}
610+
TSNode mname_ident = ts_node_named_child_count(method_name) > 0
611+
? ts_node_named_child(method_name, 0)
612+
: (TSNode){0};
613+
if (ts_node_is_null(mname_ident)) {
614+
return cls;
615+
}
616+
char *meth = cbm_node_text(a, mname_ident, source);
617+
if (!meth || !meth[0]) {
618+
return cls;
619+
}
620+
return cbm_arena_sprintf(a, "%s.%s", cls, meth);
621+
}
622+
return NULL;
623+
}
624+
// $$label^routine extrinsic / routine tag call -> the line_ref text
625+
if (strcmp(nk, "routine_tag_call") == 0) {
626+
TSNode line_ref = cbm_find_child_by_kind(node, "line_ref");
627+
if (!ts_node_is_null(line_ref)) {
628+
return cbm_node_text(a, line_ref, source);
629+
}
630+
return NULL;
631+
}
632+
// $$$Macro(...) -> raw "$$$Name" callee (expanded later in handle_calls)
633+
if (strcmp(nk, "macro") == 0) {
634+
char *raw = cbm_node_text(a, node, source);
635+
if (!raw || raw[0] != '$' || raw[1] != '$' || raw[2] != '$') {
636+
return NULL;
637+
}
638+
char *name_start = raw + 3;
639+
char *paren = strchr(name_start, '(');
640+
if (paren) {
641+
*paren = '\0';
642+
}
643+
if (!name_start[0]) {
644+
return NULL;
645+
}
646+
return cbm_arena_sprintf(a, "$$$%s", name_start);
647+
}
648+
return NULL;
649+
}
595650

596651
return extract_scripting_callee(a, node, source, lang, nk);
597652
}
@@ -1120,13 +1175,129 @@ static void extract_jsx_component_ref(CBMExtractCtx *ctx, TSNode node, const cha
11201175
}
11211176
}
11221177

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

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

11381309
TSNode args = ts_node_child_by_field_name(node, TS_FIELD("arguments"));
1310+
// ObjectScript stores args under oref_method/method_args, not the
1311+
// generic "arguments" field.
1312+
if (ts_node_is_null(args) &&
1313+
(ctx->language == CBM_LANG_OBJECTSCRIPT_UDL ||
1314+
ctx->language == CBM_LANG_OBJECTSCRIPT_ROUTINE)) {
1315+
TSNode oref = cbm_find_child_by_kind(node, "oref_method");
1316+
if (!ts_node_is_null(oref)) {
1317+
args = cbm_find_child_by_kind(oref, "method_args");
1318+
}
1319+
if (ts_node_is_null(args)) {
1320+
args = cbm_find_child_by_kind(node, "method_args");
1321+
}
1322+
}
11391323
if (!ts_node_is_null(args)) {
11401324
call.first_string_arg = extract_url_or_topic_arg(ctx, args);
11411325
if (call.first_string_arg && call.first_string_arg[0] == '/') {
11421326
call.second_arg_name = extract_handler_arg(ctx, args);
11431327
}
1144-
extract_call_args(ctx, args, &call);
1328+
if (ctx->language == CBM_LANG_OBJECTSCRIPT_UDL ||
1329+
ctx->language == CBM_LANG_OBJECTSCRIPT_ROUTINE) {
1330+
for (uint32_t ai = 0; ai < ts_node_named_child_count(args) &&
1331+
call.arg_count < CBM_MAX_CALL_ARGS;
1332+
ai++) {
1333+
TSNode achild = ts_node_named_child(args, ai);
1334+
const char *ack = ts_node_type(achild);
1335+
if (strcmp(ack, "bracket") == 0) {
1336+
continue;
1337+
}
1338+
if (strcmp(ack, "method_arg") != 0) {
1339+
continue;
1340+
}
1341+
CBMCallArg *ca = &call.args[call.arg_count];
1342+
memset(ca, 0, sizeof(*ca));
1343+
ca->index = call.arg_count;
1344+
ca->expr = cbm_node_text(ctx->arena, achild, ctx->source);
1345+
if (ca->expr && ca->expr[0]) {
1346+
call.arg_count++;
1347+
}
1348+
}
1349+
} else {
1350+
extract_call_args(ctx, args, &call);
1351+
}
11451352
}
11461353

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

0 commit comments

Comments
 (0)