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

Jungle

The monkey-c-jungle crate parses jungle files, the build language that tells monkeyc which sources, resources, barrels and annotations to use for each device.

Jungle is a small, line-oriented language, so the crate is small too: a lexer, a recursive descent parser and an AST that knows how to write itself back out.

use monkey_c_jungle::ast::{JungleFile, Value};

let mut jungle = JungleFile::parse("base.sourcePath = source\n")?;

jungle.set(
    "fenix5.resourcePath",
    vec![
        Value::reference("fenix5.resourcePath"),
        Value::text("fenix-resources"),
    ],
);

print!("{jungle}");

Building a file from scratch

There is no separate builder — a JungleFile is Default, and the same methods that edit a parsed file fill an empty one. to_string() gives you the bytes.

use monkey_c_jungle::ast::{JungleFile, Value};

let mut jungle = JungleFile::default();

jungle.set("project.manifest", [Value::text("manifest.xml")]);
jungle.push_blank_line();

jungle.push_comment("Only the shared sources");
jungle.set("base.sourcePath", [Value::text("source")]);
jungle.push_blank_line();

jungle.push_comment("Older devices lack the newer API");
for device in ["fenix3", "fr230"] {
    jungle.set(
        &format!("{device}.excludeAnnotations"),
        [Value::text("experimental")],
    );
}
jungle.push_blank_line();

// Extend a qualifier rather than replace it, and note why.
jungle.set(
    "round.resourcePath",
    [
        Value::reference("round.resourcePath"),
        Value::text("resources-round").with_comment("shared by every round device"),
    ],
);

std::fs::write("monkey.jungle", jungle.to_string())?;
project.manifest = manifest.xml

# Only the shared sources
base.sourcePath = source

# Older devices lack the newer API
fenix3.excludeAnnotations = experimental
fr230.excludeAnnotations = experimental

round.resourcePath = $(round.resourcePath);resources-round # shared by every round device

The pieces: Value::text for a literal, Value::reference for a $(…), Value::group for a […], and with_comment to note one. push_comment and push_blank_line add the lines between instructions — push_comment spaces the text off the # itself, so pass content rather than formatting.

set replaces the last instruction assigning a target, or appends one if there is none, so the same call works whether you are building or editing. Reach for remove to drop a target and get to read one back.

The AST

A JungleFile is a flat list of Entry in source order: an instruction, a comment, or a blank line. Keeping the last two is what lets a file be edited and written back without losing the notes around it.

An instruction is a QualifiedName target and a list of Value:

SourceAST
baseone qualifier segment, no property
fenix5.lang.engqualifier fenix5, property lang.eng
a;btwo values
$(base.sourcePath)/sharedone value, a reference part and a text part
[round.jungle;rect.jungle]one ValueKind::Group of two values
"my sources/app.mc"one quoted text value

Only a ; starts a new value. That is why appending to a path ($(fenix5.resourcePath);fenix-resources) and extending one ($(base.sourcePath)/extra) mean different things. Every position in a list must hold a value — monkeyc rejects a bare qualifier =, a trailing ;, a gap between two ; and an empty [] alike.

Line breaks

An instruction ends at a line break. Only three things hold one open, all verified against monkeyc:

foo =              # a break after the `=`, before the first value
  bar

foo = bar; #       # a comment after a `;` — it eats its own line break
  baz

foo = bar;\        # a `\` after a `;`
  baz

Everything else terminates, including the near-misses: a bare break after a ;, a break between two values with no ;, and any break inside a […] group. The \ is picky — it only works directly after a ;, so foo = bar \ and foo =\ both fail, and source\ at the end of a line is just the value source\.

Two deliberate differences from monkeyc. It allows a break before a ; (source on one line, ;xx on the next); this crate rejects that, because the form is a trap — monkeyc accepts it and then silently discards the instruction that follows, with no error at all. It also allows a break between a name and its =; rejecting that keeps the error for a forgotten = pointing at the line that forgot it, rather than at the line after.

In the other direction the parser is a little laxer than monkeyc about where a \ may appear. Those forms are all nonsense that no one writes, and being lax there costs nothing — a file that relies on it wouldn’t build anyway.

Comments

A comment runs from its # to the end of the line and takes the line break with itmonkeyc treats the lot as whitespace. So a comment can’t end an instruction; the instruction carries on below it:

project.manifest = # Foo
    manifest.xml

That is one instruction, project.manifest = manifest.xml. The same rule is what lets a list annotate its entries, the comment absorbing each break:

base.sourcePath = source; # shared by everything
  $(round.sourcePath); # round devices
  wearable-source # everything else

A comment therefore belongs to the value it was written against and prints after it. One with no value to attach to — the usual kind, alone on its line — is an Entry::Comment.

The catch: a comment on the last value eats the instruction’s terminator, so the line below runs into it. monkeyc rejects that, and so does this parser; the printer avoids emitting it by always leaving a blank line after one.

A # can’t appear in a value at all. Quoting doesn’t help — the comment starts inside the quotes and leaves the string unclosed.

Writing files back

Display emits the line structure the grammar requires and normalises the rest: one space around =, none around ;, one instruction per line, trailing newline. Paths, globs and redundant quotes are kept verbatim.

A blank line separates, so one survives, but a run of them collapses to a single line — the second says nothing the first didn’t. monkey-c-formatter treats blank runs in Monkey C the same way.

A break inside an instruction survives only when something forces it. A comment does; a \ doesn’t, so a continued instruction folds back onto one line.

The printer doesn’t remember where the author broke a line. If wrapping long value lists is ever wanted, the way to add it is a width rule the printer applies itself, the way monkey-c-formatter wraps Monkey C — not a record of the input’s breaks.

That gives two guarantees, and fixtures cover both. A file written the way monkeyc projects write them comes back byte for byte, annotated lists included. A file leaning on the other break forms can’t — folding a \ is a rewrite. There, what holds is that the rewrite settles in one pass: printing the output again is a no-op, so a file doesn’t drift each time a tool touches it.