feat: Monkey C (Garmin Connect IQ) extractor + member-call resolver - #2828
feat: Monkey C (Garmin Connect IQ) extractor + member-call resolver#2828stepan-orlov wants to merge 1 commit into
Conversation
Monkey C has no tree-sitter grammar, so `.mc` files were unclassified and silently skipped (a Connect IQ app produced an empty graph). Adds graphify/extractors/monkeyc.py, a scanner-based extractor in the spirit of the Pascal regex fallback: comments/string/char literals are blanked with offsets preserved, then one brace-depth scan attributes every module / class / function to its scope and every call to its function. Emitted: module/class/function nodes (`method` from a class, `contains` from a module or the file), `inherits` (same-file base direct; any other base a sourceless stub with its `using ... as` alias expanded, e.g. `Toybox.WatchUi.View`, for the unique-stub rewire), `imports_from` (a bare `import Utils;` rewired onto the app's own module node), same-file `calls` (`$.` global qualifier accepted), `method(:sym)` / `me.method(:sym)` / `new Lang.Method(self, :sym)` as `indirect_call`, and `raw_calls` tagged `lang="monkeyc"` carrying the receiver and its type where known. `new Toybox.*(...)` is never a raw call. New `monkeyc_member_calls` language resolver: `Module.fn()` / `Type.fn()` qualified calls (EXTRACTED; type known but function not -> type-level `references`), receiver-typed calls via `var x as T` / typed parameters / `x = new T()` (INFERRED) and inherited bare calls along the caller's `inherits` chain, each guarded by exactly-one-definition; `Toybox.*` receivers are the SDK and never bind to app code; purely additive. - register `.mc` in _DISPATCH, CODE_EXTENSIONS, _LANG_FAMILY_BY_EXT, build._EDGE_LANG_FAMILY, cli._HOOK_SOURCE_EXTS, LANGUAGE_EXTRACTORS - fixtures: tests/fixtures/sample.mc, tests/fixtures/monkeyc_cross_file/, tests/fixtures/monkeyc_ambiguous/ - tests: 15 new tests in tests/test_languages.py (single-file, cross-file extract() pipeline, ambiguity guard) - README language table row + CHANGELOG entry Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Graphify reviewed this change.
Worth a look — the grounded gate found no coupling regressions or blocking issues, but 2 advisory finding(s) below merit a look before merge.
Graphify review — findings
Adds a scanner-based Monkey C (.mc) extractor in graphify/extractors/monkeyc.py covering modules, classes, functions/methods, extends, using/import, calls, and method(:sym) callbacks, plus a monkeyc_member_calls corpus-wide resolver for qualified/typed-receiver/inherited calls. Registers .mc across the extractor dispatch, edge-family map, CODE_EXTENSIONS, hook source exts, and the language-family tables, and documents it in the README and CHANGELOG.
Worth a look
- Qualified Monkey C type labels are skipped before exact lookup —
graphify/extractors/monkeyc.py· Escalate · medium- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review Execution auto-disposal is off for this run; enable it (with sandbox isolation) to have Graphify try to confirm or refute this automatically.
- Scoped duplicate type names resolve to the first declaration —
graphify/extractors/monkeyc.py:236· Escalate · medium- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review Execution auto-disposal is off for this run; enable it (with sandbox isolation) to have Graphify try to confirm or refute this automatically.
Analysis details — impact, health, verification
Impact & health
Graphify review
Impact — 3135 functions depend on the 1136 functions this change touches.
Health — this change adds coupling hotspots:
- new:
extract()— 464 callers, 41 callees - new:
_rebuild_code()— 98 callers, 51 callees - new:
build_from_json()— 153 callers, 18 callees - new:
detect()— 99 callers, 15 callees - new:
build_merge()— 43 callers, 14 callees - new:
save_manifest()— 34 callers, 11 callees - new:
to_obsidian()— 29 callers, 12 callees - new:
extract_files_direct()— 17 callers, 20 callees - …and 74 more — each is listed as a finding
Verification — 3135 functions in the blast radius were not formally verified this run (proofs are advisory here).
Gate & verification
graphify gate
PASS — objectively clean (no health regressions, tests not run — proofs not run this pass (advisory)). Grounded, not self-assessed.
Advisory (not blocking):
- verification_scope: 2955 function(s) in the blast radius were not formally verified this run
· 1 grounded finding(s) anchored inline below; 81 more finding(s) on lines outside this diff (see the check run).
| return f"{full}.{rest}" if sep else full | ||
|
|
||
|
|
||
| def extract_monkeyc(path: Path) -> dict: |
There was a problem hiding this comment.
extract_monkeyc()
fans out to 16 callees (efferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
Summary
Adds Monkey C (Garmin Connect IQ,
.mc) as a supported code language.Today
.mcis in no extension set at all, so a Connect IQ app is silently dropped:classify_file()returnsNone, the files land inunclassified(which the skill never reports), andgraphify extract . --code-onlyon such a repo ends inGraph is empty. There is no tree-sitter grammar for Monkey C, so this follows the Pascal precedent (_extract_pascal_regex) with a scanner-based extractor in its own module,graphify/extractors/monkeyc.py, wired perARCHITECTURE.md→ "Adding a new language extractor".What is extracted
Comments, string and char literals are blanked with offsets preserved (line numbers stay exact —
'{'and"a { b // c"don't desync anything), then a single brace-depth scan attributes every declaration to its enclosing scope and every call to its function.module M { }/class C { }(nested too)containsfrom file/ownerfunction f(...)in a class / module / file.f()+methodedge /f()+containsedge;_callablemarkedclass C extends Binherits; same-file base → real node; other base → sourceless stub labelled with the alias-expanded name (extends Ui.Viewunderusing Toybox.WatchUi as Ui→Toybox.WatchUi.View), so_rewire_unique_stub_nodescollapses it onto the real class when the base is in the corpus and SDK bases stay one shared external nodeusing X.Y as Z;/import X.Y;imports_from(contextimport); a bareimport Utils;(a module of the same app) targets the local module if declared in the file, else a sourceless stub for the same rewire;Toybox.*stays dangling and is dropped at build like a Python stdlib importf(),me.f(),self.f(),$.f(),LocalType.f(),new LocalClass()calls(own class → enclosing (innermost) module → file scope), EXTRACTED, contextcall;new Toybox.*()is never a raw callmethod(:sym),me.method(:sym),new Lang.Method(self, :sym)indirect_call(INFERRED, contextcallback) — the Connect IQ timer/callback idiomraw_callstaggedlang="monkeyc": bare calls for the shared cross-file pass; member calls withreceiver,receiver_typeandreceiver_kind(staticfor an explicitType.fn()qualifier,typedwhen the receiver is typed viavar x as T/ a typed parameter /x = new T()); unresolved bare calls made from a class body carryself_scopeNew language resolver:
monkeyc_member_callsMonkey C code is dominated by member calls the shared pass skips by design (#543/#1219): an app is one namespace of modules and classes, so
Utils.getSecret(),WristlaBleSession.enroll()and_transport.write()are the architecture. Registered right afterjava_member_calls, additive, single-definition guarded:Module.fn()/Type.fn()→callsEXTRACTED; type known but function not (a field / SDK method on a subclass) → type-levelreferences;callsINFERRED;inheritschain (initialize()/onShow()provided by a base class in another file) → EXTRACTED; if the shared pass already bound the same pair by corpus-unique name nothing is added (strictly additive, no mutation of other passes' edges);Toybox.*receivers are the SDK and never bind to app code;_LANGUAGE_BUILTIN_GLOBALSrespected; a module's functions are found throughcontains, a class's throughmethod.Wiring
_DISPATCH(collect_files()derives from it),CODE_EXTENSIONS,_LANG_FAMILY_BY_EXT(+ mirroredbuild._EDGE_LANG_FAMILY) as familymonkeyc,cli._HOOK_SOURCE_EXTS,LANGUAGE_EXTRACTORS, facade re-export inextract.py, README extension table, CHANGELOG (0.9.45 unreleased).Tests
tests/fixtures/sample.mc+tests/fixtures/monkeyc_cross_file/{Base,App}.mc+tests/fixtures/monkeyc_ambiguous/{A,B,Caller}.mctests/test_languages.py: modules/classes/functions,contains/method, local + stubinherits, imports, same-file calls (incl.$.qualifier), callbacks in all three spellings (method(:sym),me.method(:sym),new Lang.Method(self, :sym)), raw-call typing (static/typed/self_scope, alias expansion, SDK constructors never raw), no dangling edges, line numbers, dispatch/detect registration, a fullextract()two-file run (inherits rewired onto the real class with no leftover stub,Module.fn(),Base.initialize(),new Base(), inherited bare call, type-levelreferences,import Store;rewired onto the real module node), and the exactly-one-definition guard (two same-named classes → no edge).test_ollama_retry_captests fail identically on pristinev8in this env —openaiextra not installed).tools.skillgen --check/--audit-coverage/--schema-singleton/--monolith-roundtrip/--always-on-roundtripall OK;ruff checkclean on the new module.On a real corpus
A production Connect IQ app (18 files, ~19k lines): 879 nodes / 2660 edges in 0.6 s, 428 cross-file call edges (the UI layer → BLE session module → protocol/crypto/transport modules chain comes out as the top pairs), 15
inherits(all onto alias-expanded SDK stubs), a bareimport <Module>;present in 8 files rewired onto the one real module node.Notes / caveats
.mcis also the extension of sendmail m4 configs and Windows Message Compiler sources; those contain none of the Monkey C keywords the scanner keys on, so such a file degrades to a lone file node rather than misparsing.a.b().c()yield onlyb, and afunctionwhose parameter list nests parentheses more than one level deep is skipped (braces still balance, so nothing downstream desyncs).🤖 Generated with Claude Code