Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

monkey-c-rs

monkey-c-rs logo

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
var no_trailing = [
    1,
    2,
    3
];

var trailing = [1, 2, 3,];
var no_trailing = [1, 2, 3];

var trailing = [
    1,
    2,
    3,
];

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
var no_trailing = {
    :keyOne => 1,
    :keyNumberTwo => 2
};

var trailing = {:keyOne=>1, :keyNumberTwo=>2,};
var no_trailing = {:keyOne => 1, :keyNumberTwo => 2};

var trailing = {
    :keyOne       => 1,
    :keyNumberTwo => 2,
};

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-formatter after 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

RuleAuto-fixNotes
unneeded-parensRemoves 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 / const binding (var x = (1 + 2);),
  • the value of a return statement (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 .member or [index](x + 1).foo needs 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:

  1. using Toybox.*
  2. import Toybox.*
  3. using <other>
  4. 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.