monkey-c-rs
monkey-c-rs is a collection of tools to work with the Monkey C programming
language which is Garmin’s closed-source programming language used for Garmin
devices. For example projects written in Monkey C, see awesome-garmin.
Ecosystem
Although Monkey C has been around for over a decade (founded in 2014), at the time of writing the tooling we’ve grown to love for modern programming languages such as linters and formatters based on static code analysis is almost completely lacking.
By writing a parser that can understand and represent the Monkey C language I thought I could write tools that could also help format and analyse the code. This project is an attempt to close this gap.
Parser
The monkey-c-parser is the core that allows us to work with Monkey C at a
higher level of abstraction — an Abstract Syntax Tree.
Heavily inspired (and motivated) by the work on astral-sh/ruff, which has the AST used by RustPython and an extremely fast formatter and linter.
Language Specification
The Garmin Connect IQ SDK does a decent job documenting the Monkey C language and has a language specification that the parser is based on.
Although not used in the parser, they also have API docs documenting the standard library.
Formatter
The Monkey C formatter aims to be a zero-config one-size-fits-all solution to
ensure consistent formatting of your Monkey C code. More opinionated suggestions
for the code is implemented in the monkey-c-linter.
Note
I’d love any input and testing on the formatter. Both help finding bugs and inconsistencies but also input on the formatting algorithm. Please create an issue for any bug or feature request.
Wrapping long lines
The formatter is using the Wadler-Lindig algorithm to wrap lines at a default width of 111 columns. 111 is chosen because 80 is too little and 222 is too much.
The magic comma
monkey-c-formatter uses the same trailing magic comma to determine if multiple
arguments should be wrapped over multiple lines even if they would fit on a
single line, just like ruff does.
| Original | Formatted |
|---|---|
|
|
Aligning fat-commas
The formatter will by default align fat-commas (=>) when creating dictionaries
to increase readability of values. Dictionaries have the same rule regarding the
magic trailing comma.
| Original | Formatted |
|---|---|
|
|
Linter
Linter for Monkey C code to find both stylistic and other issues in the code.
When possible the linter supports automatically fixing the issues by using the
--fix flag.
Note
The fixer doesn’t format the code to normalize after changes so the user is expected to run the
monkey-c-formatterafter applying fixes.
› monkey-c-linter monkey-c-linter/example/Example.mc
[import-order] Warning: imports should be sorted and grouped
╭─[ monkey-c-linter/example/Example.mc:1:1 ]
│
1 │ ╭─▶ using Toybox.Lang;
┆ ┆
5 │ ├─▶ using Toybox.Graphics as Gfx;
│ │
│ ╰─────────────────────────────────── imports should be sorted and grouped
│
│ Note: fix: replace with `using Toybox.Graphics as Gfx;
│ using Toybox.Lang;
│
│ import Toybox.System;
│
│ using MyModule.First;
│ using MyModule.Second;`
───╯
[unneeded-parens] Warning: unneeded parentheses around expression
╭─[ monkey-c-linter/example/Example.mc:10:17 ]
│
10 │ var value = (M + C + 180 + 101.11d);
│ ───────────┬───────────
│ ╰───────────── unneeded parentheses around expression
│
│ Note: fix: replace with `M + C + 180 + 101.11d`
────╯
[unneeded-parens] Warning: unneeded parentheses around expression
╭─[ monkey-c-linter/example/Example.mc:13:20 ]
│
13 │ var isPm = (hour >= 12);
│ ──────┬─────
│ ╰─────── unneeded parentheses around expression
│
│ Note: fix: replace with `hour >= 12`
────╯
[unneeded-parens] Warning: unneeded parentheses around expression
╭─[ monkey-c-linter/example/Example.mc:19:17 ]
│
19 │ value = (valueEnabled ? "V on" : "V off");
│ ────────────────┬────────────────
│ ╰────────────────── unneeded parentheses around expression
│
│ Note: fix: replace with `valueEnabled ? "V on" : "V off"`
────╯
Rules
Each lint rule walks the parsed AST looking for a specific pattern. When a rule
fires it produces a Diagnostic — the source range, a message,
and (when applicable) a machine-applicable Fix that replaces a byte
range with new text.
Fixes are byte-level text replacements, not AST rewrites. That means a fix only
touches the affected source range and leaves the surrounding formatting alone.
Once --fix has been applied the user is expected to run the
monkey-c-formatter if they want whitespace normalised.
Categories
| Rule | Auto-fix | Notes |
|---|---|---|
unneeded-parens | ✅ | Removes redundant parentheses |
import-order | ⚠️ | Suppressed when comments interleave |
unneeded-parens
Flags (<expr>) written in positions where the parentheses don’t change
parsing.
Rationale
A parenthesised expression in certain positions is unambiguous — the grammar accepts any expression there regardless of operator precedence. Wrapping it in extra parens reads as either a typo or copy-paste residue.
What triggers
The rule fires when a (<expr>) appears as:
- the right-hand side of an assignment (
x = (1 + 2);), - the initializer of a
var/constbinding (var x = (1 + 2);), - the value of a
returnstatement (return (x);).
In all three slots, removing the parentheses cannot affect operator precedence.
What does not trigger
The rule is intentionally conservative and skips positions where parentheses can matter:
- Operand of a binary operator —
1 * (2 + 3)needs the parens. - Object position of a
.memberor[index]—(x + 1).fooneeds the parens. - Anywhere else not in the explicit list above.
Example
Before:
var x = (1 + 2);
function f() {
return (foo());
}
After --fix:
var x = 1 + 2;
function f() {
return foo();
}
Fix
The fix replaces the source between the parentheses (trimmed of surrounding
whitespace) into the outer span. Comments inside the parens are preserved:
(/* tag */ 2 + 3) becomes /* tag */ 2 + 3.
import-order
Flags contiguous runs of using / import declarations that aren’t in
canonical order.
Rationale
A consistent import order makes files easier to scan, reduces merge conflicts,
and groups related declarations together. Sorting alphabetically eliminates
the need for editors to argue about placement; grouping Toybox.* separately
keeps SDK imports visually distinct from project ones.
What triggers
The rule reports a run of using / import declarations whose order doesn’t
match the canonical form. Canonical order is four groups, each sorted
alphabetically by the dotted path, separated by a blank line:
using Toybox.*import Toybox.*using <other>import <other>
A declaration is Toybox.* when its path is exactly Toybox or starts with
Toybox..
What does not trigger
Each contiguous run of using / import is treated as its own block. A
non-import declaration between two import blocks creates a hard boundary —
declarations in the second block aren’t pulled up to join the first. This
matches the behaviour of Ruff’s import sorter.
When a comment interleaves the imports (e.g., a // section line between two
using statements) the rule only enforces order — blank-line placement
around the user’s comments is left alone.
Example
Before:
import ModuleC;
using ModuleA;
import Toybox.D;
using Toybox.A;
import Toybox.C;
using Toybox.B as D;
After --fix:
using Toybox.A;
using Toybox.B as D;
import Toybox.C;
import Toybox.D;
using ModuleA;
import ModuleC;
Fix
The fix replaces the entire run with the canonical text. The auto-fix is suppressed when a comment interleaves the imports — rearranging would lose the user’s comment placement, so the edit is left to the user. The diagnostic is still reported in that case.