Skip to content

feat: Monkey C (Garmin Connect IQ) extractor + member-call resolver - #2828

Open
stepan-orlov wants to merge 1 commit into
Graphify-Labs:v8from
stepan-orlov:feat/monkeyc-extractor
Open

feat: Monkey C (Garmin Connect IQ) extractor + member-call resolver#2828
stepan-orlov wants to merge 1 commit into
Graphify-Labs:v8from
stepan-orlov:feat/monkeyc-extractor

Conversation

@stepan-orlov

Copy link
Copy Markdown

Summary

Adds Monkey C (Garmin Connect IQ, .mc) as a supported code language.

Today .mc is in no extension set at all, so a Connect IQ app is silently dropped: classify_file() returns None, the files land in unclassified (which the skill never reports), and graphify extract . --code-only on such a repo ends in Graph 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 per ARCHITECTURE.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.

Construct Emitted
module M { } / class C { } (nested too) node + contains from file/owner
function f(...) in a class / module / file .f() + method edge / f() + contains edge; _callable marked
class C extends B inherits; same-file base → real node; other base → sourceless stub labelled with the alias-expanded name (extends Ui.View under using Toybox.WatchUi as UiToybox.WatchUi.View), so _rewire_unique_stub_nodes collapses it onto the real class when the base is in the corpus and SDK bases stay one shared external node
using X.Y as Z; / import X.Y; imports_from (context import); a bare import 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 import
f(), me.f(), self.f(), $.f(), LocalType.f(), new LocalClass() same-file calls (own class → enclosing (innermost) module → file scope), EXTRACTED, context call; new Toybox.*() is never a raw call
method(:sym), me.method(:sym), new Lang.Method(self, :sym) indirect_call (INFERRED, context callback) — the Connect IQ timer/callback idiom
everything else raw_calls tagged lang="monkeyc": bare calls for the shared cross-file pass; member calls with receiver, receiver_type and receiver_kind (static for an explicit Type.fn() qualifier, typed when the receiver is typed via var x as T / a typed parameter / x = new T()); unresolved bare calls made from a class body carry self_scope

New language resolver: monkeyc_member_calls

Monkey 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 after java_member_calls, additive, single-definition guarded:

  • Module.fn() / Type.fn()calls EXTRACTED; type known but function not (a field / SDK method on a subclass) → type-level references;
  • receiver typed by local inference → calls INFERRED;
  • an unresolved bare call from a class body → looked up along the caller's inherits chain (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_GLOBALS respected; a module's functions are found through contains, a class's through method.

Wiring

_DISPATCH (collect_files() derives from it), CODE_EXTENSIONS, _LANG_FAMILY_BY_EXT (+ mirrored build._EDGE_LANG_FAMILY) as family monkeyc, cli._HOOK_SOURCE_EXTS, LANGUAGE_EXTRACTORS, facade re-export in extract.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}.mc
  • 15 tests in tests/test_languages.py: modules/classes/functions, contains/method, local + stub inherits, 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 full extract() two-file run (inherits rewired onto the real class with no leftover stub, Module.fn(), Base.initialize(), new Base(), inherited bare call, type-level references, import Store; rewired onto the real module node), and the exactly-one-definition guard (two same-named classes → no edge).
  • Full suite: 4464 passed, 48 skipped (the 4 test_ollama_retry_cap tests fail identically on pristine v8 in this env — openai extra not installed). tools.skillgen --check/--audit-coverage/--schema-singleton/--monolith-roundtrip/--always-on-roundtrip all OK; ruff check clean 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 bare import <Module>; present in 8 files rewired onto the one real module node.

Notes / caveats

  • .mc is 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.
  • Scanner-based, so it will miss what a real grammar would not: chained calls a.b().c() yield only b, and a function whose parameter list nests parentheses more than one level deep is skipped (braces still balance, so nothing downstream desyncs).

🤖 Generated with Claude Code

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>

@graphify-labs graphify-labs Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 lookupgraphify/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 declarationgraphify/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:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Health regressionextract_monkeyc()

fans out to 16 callees (efferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant