Skip to content

Repository files navigation

Mecojoni

A Markdown-like language for compositional, localized generative text.

Mecojoni

Mecojoni is a readable, typed, modular language for generative dialogue and text. It retains the useful core of a weighted context-free grammar—headings define rules and list items define alternatives— while adding the structure needed for game data, conditions, reuse, localization, and reliable long-running generation.

Status: the current source-language and compiled-artifact roadmaps are feature-complete. Representative native/WASM measurements justified freezing the hostile-input-checked bytecode/1 format and its single-WASM deployment path. The dependency-free core parses and compiles complete packages, executes exact typed weighted/1 generation, and resolves complete localized messages through a synchronous host formatter in Rust, Deno, and Chrome. Transactional diverse/1, span-aware provenance, repetition audits, replay receipts, and versioned session/history snapshots now run across Rust and WASM. The dependency-free std authoring CLI, initial editor grammar, committed native/WASM workloads, and frozen compatibility contracts are also executable. The crates remain unpublished pending the owner's distribution version and license choice. The bytecode design and implementation status are tracked in BYTECODE_FORMAT_PLAN.md; recorded evidence lives in BENCHMARKS.md.

The current Rust API can canonically encode_artifact, decode_artifact, inspect_artifact, and disassemble_artifact. The hostile-input-checked format is frozen as bytecode/1; see BYTECODE_FORMAT.md for its exact layout and COMPATIBILITY.md for evolution rules.

The authoring CLI can compile, inspect, verify, and generate .mecob files. Deno and browser hosts can inspect or load the same bytes through Mecojoni.inspectArtifact and Mecojoni.loadArtifact; loaded artifacts use the ordinary grammar-handle lifecycle and leak telemetry.

deno task wasm:embedded:build produces a content-specific WASM whose private data segment contains the fully resolved Harbor artifact. openEmbeddedGrammar() opens it without a content fetch; the generic WASM returns a capability error. The Chrome smoke test asserts that the content build requests neither .meco nor .mecob at runtime.

Design goals

Mecojoni keeps authored text at the centre. The visible output remains easy to scan:

# greeting
- Hello, @person!
- @person, @observation.

The additional syntax is reserved for structure that does not itself emit text:

- {mood is tense}
  {common.name as hero}
  &arrival <- hero: $hero

The braces establish eligibility and data; the final line is the complete localized message that will be rendered.

Build, test, and deploy

Run all commands below from the repository root. Rust 1.85 with the wasm32-unknown-unknown target and Deno are required; Chrome or Chromium is optional for the browser tests.

Build the generic WASM runtime

The generic build contains the runtime but no application grammar:

deno task wasm:build
cargo +1.85.0 build -p mecojoni-wasm --target wasm32-unknown-unknown --release

The release artifact is target/wasm32-unknown-unknown/release/mecojoni_wasm.wasm.

Run the JavaScript and WASM tests

deno task js:check
deno task wasm:test
deno task wasm:browser:test

wasm:test runs the Deno integration corpus. wasm:browser:test bundles the same browser-neutral wrapper and exercises external source and .mecob loading in headless Chrome when an installed browser is available. The complete Rust, no_std, WASM, documentation, Deno, and browser release gate is listed in CONFORMANCE.md.

Compile a package to .mecob

The CLI resolves every import from the root module and writes one canonical, fully resolved artifact:

cargo +1.85.0 run -p mecojoni-cli -- \
  compile-artifact path/to/root.meco \
  --profile full \
  --write target/content.mecob

cargo +1.85.0 run -p mecojoni-cli -- inspect-artifact target/content.mecob
cargo +1.85.0 run -p mecojoni-cli -- verify-artifact target/content.mecob

Add --messages path/to/messages.manifest when the package emits complete localized messages. The exact container is specified in BYTECODE_FORMAT.md; all artifact commands are documented in the CLI guide.

Embed the artifact in one content-specific WASM

Point the WASM build at an already compiled artifact. An absolute path is recommended because relative paths are resolved from crates/mecojoni-wasm:

MECO_EMBEDDED_ARTIFACT=/absolute/path/to/content.mecob \
CARGO_TARGET_DIR=target/content-wasm \
cargo +1.85.0 build -p mecojoni-wasm \
  --target wasm32-unknown-unknown \
  --release

The content-bearing binary is target/content-wasm/wasm32-unknown-unknown/release/mecojoni_wasm.wasm. After instantiation, call openEmbeddedGrammar(); it returns the ordinary disposable grammar handle. A generic build instead returns E_BYTECODE_CAPABILITY.

The repository's ready-made embedded conformance build uses the Harbor fixture:

deno task wasm:embedded:test
deno task wasm:embedded:browser:test

The browser test proves that the content-specific application requests one WASM file and no separate .meco or .mecob. See the WASM crate guide and JavaScript wrapper guide for ABI ownership and application API details.

The syntax in this README is authoritative. SPECIFICATION.md must be updated with every syntax change; if the documents temporarily disagree, this README wins.

MCP server

Mecojoni includes an MCP server that exposes the meco CLI to MCP-compatible agents. It provides tools for checking, generating, tracing, formatting, auditing, and compiling Mecojoni sources and artifacts while keeping compiler behavior in the CLI and mecojoni-core.

Build the CLI, then run the server with Deno 2.x:

cargo +1.85.0 build -p mecojoni-cli --release
deno run --allow-read --allow-run --allow-env=MECO_BIN,MECO_PROJECT_ROOT \
  /absolute/path/to/mecojoni/mcp/server.ts

The server restricts source and artifact paths to MECO_PROJECT_ROOT (the repository root by default). See the MCP server guide for the tool list, configuration examples for common MCP clients, and development commands.

Quick start

Mecojoni source begins with a small, strict front-matter header. This sample has no default entry, so a host must request one of its exported rules explicitly.

---
meco: 1.0
module: npc
sampler: diverse/1

types:
  Mood: [calm, tense]

inputs:
  playerName: text
  itemCount: number
  mood: Mood
  urgency: number
  inputValue: number

imports:
  common: "./common.meco"

exports: [pickup, greeting, warning]
---

# pickup
- [3] &pickup-common <- player: $playerName, count: $itemCount
- [1] {mood is tense}
  &pickup-alert <-
    player: $playerName
    count: $itemCount

# local-intro
- @{common.name as hero} arrived. $hero looked tired.

pickup chooses a complete localized message. The second production is eligible only when the mood input is tense. The argument values are data, not visible text; &pickup-common or &pickup-alert owns the complete rendered result.

Add entry: pickup to a package root when it should have a default generation target. Mecojoni never guesses a default from the first rule or the order of exports.

Run the language walkthrough

Run the Rust walkthrough to see Mecojoni parse the complete README syntax corpus and execute every branch of the host-persisted NPC-memory example. It prints rule names, generated text, selected productions, and deterministic work counters:

cargo +1.85.0 test -p mecojoni-core --test print_hello_generation -- --nocapture

The corpus is deliberately a parsing walkthrough: it places independent syntax examples with different localized-message schemas in one source block. The test therefore parses the full corpus and separately runs the executable branching-memory package for unmet, cautious, and trusted NPC paths.

Complete example corpus

The following source is the canonical syntax corpus. It intentionally places many independent examples in one file so each form can be reviewed in context.

---
meco: 1.0
module: npc
sampler: diverse/1

types:
  Mood: [calm, tense]

inputs:
  playerName: text
  itemCount: number
  mood: Mood
  urgency: number

imports:
  common: "./common.meco"

exports: [pickup, greeting, warning]
---

<!-- No entry is declared: a caller must select an exported rule. -->

<!-- Arguments after <- supply data; they are not visible output themselves. -->
# pickup
- [3] &pickup-common <- player: $playerName, count: $itemCount
- [1] {mood is tense}
  &pickup-alert <-
    player: $playerName
    count: $itemCount

<!-- An emitting capture selects once, emits once, then makes $hero reusable. -->
# local-intro
- @{common.name as hero} arrived. $hero looked tired.

<!-- Braced clauses are silent. Guards come before bindings. -->
# localized-arrival
- {common.name as hero}
  &arrival <- hero: $hero

# localized-encounter
- {common.name as hero}
  {common.name as companion}
  {common.place as destination}
  &encounter <- $hero, $companion, $destination

# title-suffix
- [3] ""
- [1] " the "@common.title

# multiline-example
- |
  First line.
  Second line.

# tense-arrival
- [1] {mood is tense}
  {common.name as hero}
  &arrival <- $hero

# tense-arrival-with-companion
- [1] {mood is tense}
  {common.name as hero}
  {common.name as companion}
  &arrival <- hero: $hero, $companion

<!-- Basic composition and public rules. -->
# greeting
- [3] @salutation, @person!
- [1] @person, @observation.

<!-- The header declares a parameter; <- at the call site supplies it. -->
# greetings <- name: text
- Hello, $name!
- Welcome back, $name!

# player-greeting
- @greetings <- name: $playerName

# warning
- Attention, @person: @observation.

# salutation
- Hello
- Good morning
- Welcome

# person
- traveller
- neighbour
- old friend

# observation
- the weather has changed
- the market is unusually quiet
- today feels promising

<!-- A minimal subject-predicate grammar. -->
# sentence
- @subject @predicate.

# subject
- The pilot
- A maintenance drone

# predicate
- is waiting
- found the missing tool

<!-- Ordinary unweighted alternatives. -->
# temperature
- cold
- mild
- uncomfortably warm

<!-- References embedded in terminal text. -->
# report
- The @device is @condition.

# device
- air recycler
- navigation console

# condition
- offline
- making a strange noise

<!-- Integer and decimal relative weights. An omitted weight is 1. -->
# weighted-mood
- [6] calm
- [3] tired
- [1] furious
- [0.5] cautiously optimistic

<!-- Dynamic weights use bare numeric names and are evaluated before selection. -->
# urgency-reaction
- [urgency] The alarm is spreading.
- [weight = urgency, id = urgent-alarm] The alarm is spreading quickly.
- [1] Everything is quiet.

<!-- Compact expressions, ID-only metadata, and full metadata may be mixed. -->
# weighted-by-context <- parameterValue: number
- [weight = inputValue + parameterValue * 20] explicit expression
- [inputValue + parameterValue * 20] compact expression
- [id = stable-default] default weight with a stable ID
- [weight = 3, id = static-three] static weight with a stable ID

<!-- Empty output, optional text, and an explicitly delimited adjacent reference. -->
# titled-greeting
- Welcome, @{name}@title-option.

# title-option
- [3] ""
- [1] " the "@title

# name
- Ada
- Tomas

# title
- Captain
- Doctor

<!-- A delimited reference separates the rule name from a literal suffix. -->
# creature-count
- Several @{creature}s arrived.

# creature
- traveller
- maintenance drone

<!-- Productive recursion with a strongly preferred terminating production. -->
# inventory
- [5] @item
- [1] @item, @inventory

# item
- a coil of wire
- a repair kit
- an empty canister

# calm-line
- Everything is under control.

<!-- Escape a sigil when it should be emitted literally. -->
# contact
- Send a message to pilot\@example.invalid.

<!-- A raw string does not interpret sigils or escape sequences. -->
# raw-contact
- r"Send a message to pilot@example.invalid."

<!-- A raw block keeps every sigil as literal text and strips its final newline. -->
# raw-sigils
- |raw-
  @person, $playerName, and &pickup-alert are literal text.

<!-- Split incompatible concepts into semantically coherent branches. -->
# incident
- @repair-incident
- @travel-incident

# repair-incident
- @technician @repair-action @broken-device.

# travel-incident
- @traveller @travel-action @destination.

# technician
- The mechanic
- A service drone

# repair-action
- inspected
- repaired

# broken-device
- the air recycler
- the navigation console

# traveller
- The courier
- A survey team

# travel-action
- departed for
- finally reached

# destination
- the northern outpost
- the orbital terminal

The .meco format

.meco is the canonical source extension used by repository fixtures, imports, examples, editor integration, and generated documentation. The portable parser still consumes host-supplied source independently of its filename.

Front matter, modules, and entries

Every module declares meco: 1.0 and a module name. The header is a strict Mecojoni schema, not general-purpose YAML: unknown or duplicate fields are errors, as are YAML tags, anchors, aliases, merges, and implicit values.

The root may additionally declare:

Field Purpose
entry Optional default public rule for generation requests that do not name one.
sampler Optional authoring recommendation such as diverse/1.
types Named finite types, for example Mood: [calm, tense].
inputs Typed data supplied by the host.
imports Module paths mapped to aliases.
exports The rules that callers may select.

Rules are private unless exported. Imported references use their alias:

imports:
  common: "./common.meco"

# line
- @common.name arrived.

Rules, references, and visible text

A # heading defines a rule and each - item is a weighted alternative. Rule references expand inline and emit their result:

Initial identifiers are case-sensitive ASCII; terminal text may contain any valid UTF-8. Unicode identifiers are deferred so the portable core does not carry normalization tables before a real authoring requirement justifies them.

# report
- The @device is @condition.

Use @{name} when a suffix would otherwise make the reference ambiguous:

- Several @{creature}s arrived.

Use an emitting capture when the selected output must be reused later in the same production:

- @{common.name as hero} arrived. $hero looked tired.

The capture selects common.name once, emits it once, and binds that same value as $hero. Captures are local to their production and candidate; nested rules receive values only through declared parameters.

Silent clauses: guards and bindings

Leading braces describe work that does not itself produce visible text:

- {mood is tense}
  {common.name as hero}
  &arrival <- $hero

{mood is tense} is a guard. It determines whether the production is eligible. {common.name as hero} is a binding. It expands a rule once and stores the value without emitting it. Guards must come before bindings: eligibility is decided before the runtime selects a production and evaluates its bindings.

A later binding may call a parameterized rule with values from earlier bindings:

- {common.name as hero}
  {common.companion <- owner: $hero as companion}
  $hero arrived with $companion.

The as companion suffix names the silent result; the arguments between <- and as follow the same named and punned rules as an emitting call.

The first non-braced item is the visible body. This is why a normal textual body does not need a separator:

- {mood is tense}
  {common.name as hero}
  $hero, I see you are with your companion.

Typed rule parameters and calls

Rule headers declare typed parameters with <-:

# greetings <- name: text
- Hello, $name!

# player-greeting
- @greetings <- name: $playerName

The value after <- is an argument list, not output. The call expands greetings, passing the host input as its name parameter, then emits the chosen rule result. The same syntax calls a localized message:

&arrival <- hero: $hero, companion: $companion

Within an argument list, $hero is shorthand for hero: $hero. Therefore:

&encounter <- $hero, $companion, $destination

means:

&encounter <- hero: $hero, companion: $companion, destination: $destination

<- is only a call-argument operator after @rule or &message; it is never a general assignment form.

Branching and persistent NPC memory

Rule calls form a branching content graph. During one generation, Mecojoni follows the selected branch and any rules it calls, so a response can continue into a reusable sub-path:

# encounter
- {npcPath is cautious} @cautious-encounter
- {npcPath is trusted} @trusted-encounter

# cautious-encounter
- "Keep your voice down. "@cautious-decision

# cautious-decision
- I will show you the way.
- We should wait for daylight.

# trusted-encounter
- "Good to see you again. "@trusted-decision

# trusted-decision
- Let us start with the old signal tower.
- I saved something for you.

This traversal is local to that generation. Mecojoni does not mutate a character or story variable after returning a result. Persistent NPC memory belongs to the host application: store the character's state there, then provide it as a typed input on the next generation.

types:
  NpcPath: [unmet, cautious, trusted]

inputs:
  npcPath: NpcPath

For example, the host can begin with npcPath = unmet, update its saved NPC record to cautious after a player action, and pass npcPath = cautious on the next call. Guards then select the appropriate branch. This keeps saves, quest logic, and multiplayer authority in the host while keeping the grammar deterministic for a given input and seed.

diverse/1 is separate from narrative memory: it remembers recent selections and text only to avoid repetition. It does not decide or persist an NPC's story path.

Complete localized messages

&message resolves a stable external message through the configured formatter. It must be the complete visible body of a production, rather than a fragment inside English prose. This lets each locale control word order, agreement, plurality, and inflection.

- {common.name as hero}
  &arrival <- $hero

The binding contributes data but no text. The formatter owns the rendered result. A message-valued rule cannot be captured, suffixed, or wrapped in another visible rule fragment.

Compilation receives a formatter manifest, so missing IDs, missing or extra arguments, and type drift fail before generation. A generated message crosses the host boundary as one ordered request containing its stable ID, typed values, requested locale, and explicit fallback chain. The formatter returns the complete text plus actual locale, environment identity, diagnostics, deterministic work units, and whether the result is replayable. It is synchronous and operates only over resources the host has already loaded; the core performs no locale I/O and contains no built-in plural engine.

A filesystem-backed Rust integration test proves this generic boundary against the real fluent-bundle crate. Its .meco package passes text, exact integral number, and enum arguments into English and Italian .ftl resources; Fluent then selects grammatical gender and the one and other cardinal categories. The same test checks ordered locale fallback, formatter provenance, and Fluent's default bidirectional-isolation marks. fluent-bundle is strictly a test-only dependency, so the production core remains dependency-free and #![no_std] + alloc:

cargo test -p mecojoni-core --test fluent_integration

See the test adapter and its Meco/Fluent fixtures for the complete executable example. This validates how a standards-based adapter fits the interface; a reusable production Fluent adapter package remains a separate deferred deliverable.

The Rust API exposes compile_package_with_manifest, generate_weighted_structural, and generate_weighted_with_formatter. The browser-neutral TypeScript wrapper accepts the same contract:

const compiled = meco.compilePackage(packageDescription, {
  messages: [{
    id: "arrival",
    arguments: [
      { name: "hero", type: { kind: "text" } },
      { name: "count", type: { kind: "number" } },
    ],
  }],
});
if (!compiled.ok) throw new Error(compiled.error.message);

const result = meco.generateWeighted(compiled.value, {
  seed: 7n,
  locale: "pl",
  fallbackLocales: ["en"],
  data: { itemCount: { kind: "number", numerator: 2n, denominator: 1n } },
  formatter: (request) => formatFromPreloadedCatalog(request),
});

See js/README.md and INTERFACES.md for the concrete callback and wire contracts.

Weights, empty output, and recursion

Static weights are positive relative base weights. Omitted weights are 1, and decimals are valid:

# mood
- [6] calm
- [3] tired
- [1] furious
- [0.5] cautiously optimistic

A dynamic weight may use an immutable numeric input or a numeric parameter of the current rule. It is evaluated after guards but before selection; zero makes that production ineligible. Use the bare value name inside weight metadata—urgency, not $urgency:

inputs:
  urgency: number

# reaction
- [urgency] The alarm is spreading.
- [1] Everything is quiet.

[urgency] is shorthand for [weight = urgency]. Use the long form when the production also needs a stable authored identity:

- [weight = urgency, id = urgent-alarm] The alarm is spreading.

All supported metadata forms are:

- [weight = inputValue + parameterValue * 20] explicit expression
- [inputValue + parameterValue * 20] compact expression
- [id = stable-default] default weight (`1`) with a stable ID
- [weight = 3, id = static-three] explicit weight with a stable ID

The same expression works with a declared rule parameter, such as # reaction <- urgency: number. Expressions may use decimal literals, numeric inputs/parameters, parentheses, +, -, and *; they cannot use captures, bindings, generated rules, messages, callbacks, clocks, or ambient state. A negative or overflowing evaluated value is a generation error, and if every guard-eligible production evaluates to zero, generation returns E_NO_ELIGIBLE_PRODUCTION. This keeps dynamic selection deterministic and replayable.

Version rational/1 evaluates those values exactly as reduced signed fractions: the absolute numerator and positive denominator are each at most 2^63 - 1. Decimal literals contain at most 18 digits and an optional exponent from -18 to 18; an operation or per-rule scaled total outside the budget is an error rather than a floating-point approximation. Version splitmix64/1 supplies the seeded random stream, whose fixed vectors are shared by Rust and the Deno/WASM wrapper.

An entire production containing "" emits nothing:

# title-suffix
- [3] ""
- [1] " the "@title

Recursive rules are valid only when there is a productive route back to terminal text. Terminating alternatives should normally carry most of the weight:

# inventory
- [5] @item
- [1] @item, @inventory

Whitespace, strings, blocks, comments, and escapes

Visible text preserves its authored characters. Put intentional leading or trailing whitespace inside a quoted segment:

- " the "@title

Double-quoted segments interpret escapes. r"..." is a raw single-line literal; |raw, |raw-, and |raw+ are raw block forms. | retains one final newline, |- removes it, and |+ preserves trailing blank lines.

- Send a message to pilot\@example.invalid.
- r"Send a message to pilot@example.invalid."
- |raw-
  @person and $playerName are literal text here.

Outside literals, Markdown comments are ignored:

<!-- This production is for calm conditions. -->
# calm-line
- Everything is under control.

Sampling and reproducibility

Mecojoni exposes two runtime modes: independent random and repetition-resistant varied. Mecojoni makes the policy names and versions explicit:

Mecojoni policy Corresponding behavior Use
weighted/1 random Exact independent weighted CFG draws.
diverse/1 varied Stateful repetition resistance for player-facing text.

Under weighted/1, weights are exact relative probabilities. Under diverse/1, they remain authorial priors but may be adjusted by bounded structural cooldown, subtree diversity, candidate search, and surface-novelty scoring. Nullable and recursion-sensitive rules retain their termination and optionality weights.

sampler: diverse/1 is a recommendation stored with the grammar, not an unchangeable semantic property. A host may explicitly override it. The effective sampler version, settings, grammar hash, seed, input, locale, and requested entry must be recorded for reproducible sessions.

The initial diverse/1 profile, location/1, uses 12 candidate attempts, an immediate-reuse gap of one selection, a four-selection soft cooldown horizon, 3–8 word edge fragments, 300 retained edge records, and 50,000 retained exact records. The edge and exact histories also have 4 MiB and 16 MiB canonical UTF-8 logical-byte caps. These are versioned profile values, not hidden tuning constants. The default resource profile preserves a depth limit of 80 and expansion limit of 2,000 per candidate while also bounding output, sampling work, and rendered bytes. The complete profile and limit tables are normative in SPECIFICATION.md.

The executable API keeps mutable state explicit. SamplerSession owns the parent PRNG and call order; RepetitionStore owns structural, exact-output, and edge histories and may be shared deliberately. DiverseGenerationRequest contains no seed because the session is the sole random source. Each successful location/1 call reserves 12 substream seeds, commits one winner and one store revision, and reports its attempt and novelty score. Any failure leaves both objects unchanged.

let mut session = SamplerSession::new(42);
let mut repetition = RepetitionStore::new_location();
let result = session.generate(
    &grammar,
    &mut repetition,
    &DiverseGenerationRequest::default(),
)?;

Successful diverse results include a ReplayReceipt containing grammar and state hashes, the request digest, fixed PRNG reservation, stable derivation hash, winner, final-text hash, and post-commit revision. SamplerSession::snapshot() and RepetitionStore::snapshot() capture the pre-call continuation; restoring both after nonempty history reproduces the next result. In-memory repetition snapshots share an immutable copy-on-write root. Serialized snapshots use snapshot/1, are bounded to 64 MiB at the decoder, and validate their profile windows and logical byte declarations before allocating live state.

Repetition snapshots contain resolved lines and fragments and must therefore be treated as sensitive data. SnapshotPolicy makes capture consent, logical budget, pinning, and revision-relative expiry explicit. A pin is ordinary caller ownership: dropping the snapshot releases it, so the core never grows an ambient retention registry. Durable storage and encryption remain host responsibilities.

Production identities are not list positions. An explicit id remains stable across weight, order, and prose edits; otherwise the compiler derives a content- addressed artifact-local ID from the qualified rule and canonical body, excluding weight. Duplicate unlabeled alternatives and all within-rule ID collisions are compile errors.

Compilation, generation, and diagnostics

The proposed compiler validates source before any generation. Its checks include:

  • front-matter shape, module identity, imports, exports, and visibility;
  • duplicate or undefined rules, invalid references, parameters, and call arguments;
  • input and parameter types, guard expressions, duplicate bindings, shadowing, forward references, and unused bindings;
  • message-effect placement: a localized message must own the complete visible body;
  • productive reachable rules, nullable paths, recursive components, and risk;
  • weights, strings, escapes, comments, blocks, and source spans.

Generation uses an explicit expansion stack and configured limits rather than the host language call stack. Failed, losing, cancelled, or over-budget diverse candidates roll back their bindings and sampler state. Successful generations can return a trace that identifies selected rules, production identities, binding events, source locations, sampler adjustments, and formatter/message work. With trace_provenance, every visible byte/scalar range is linked to authored text, host or bound values, emitting captures, or a coarse complete message. Non-emitting bindings retain derivation links but no output range. Structural and rendered repetition audits operate on these retained traces; rendered findings attribute a fragment only to nodes whose output ranges overlap it.

composition/1 is an optional, deliberately strict audit heuristic. It warns when a sentence-ending locally composed production has fewer than three direct emitting grammar references or an authored literal run longer than two words. Complete &message bodies are exempt because their structure belongs to the formatter. It is a signal for review, never a prose-quality verdict.

Authoring guidance

The number of possible outputs is less important than whether each combination makes sense. Organize a grammar around semantic contracts:

# incident
- @repair-incident
- @travel-incident

# repair-incident
- @technician @repair-action @broken-device.

# travel-incident
- @traveller @travel-action @destination.

This prevents repair actions from being combined with travel destinations merely because both were placed in global pools. Use weights for intentional world texture, keep recursion termination-biased, and use localized messages when the sentence must adapt to locale-specific grammar.

Authoring CLI and editor grammar

The optional mecojoni-cli crate provides check, generate, trace, lint, audit, manifest, fmt, and bench. It recursively resolves imports from an explicit root while the portable core continues to perform no I/O.

cargo +1.85.0 run -p mecojoni-cli -- check npc.meco
cargo +1.85.0 run -p mecojoni-cli -- \
  generate npc.meco --seed 7 --data playerName=Rin
cargo +1.85.0 run -p mecojoni-cli -- lint npc.meco --deny-warnings

Human output and cli/1 JSONL have fixed stdout/stderr and exit-status contracts. Generation batches are buffered before output, so errors never leave partial success records. The initial formatter validates then preserves source byte for byte; this proves comments, edge spaces, and block chomp semantics cannot change while style-changing rules remain deliberately unspecified. Full CLI details are in the CLI guide.

editors/vscode/ supplies an initial TextMate grammar and language configuration. Semantic diagnostics use meco check; an LSP transport is deferred until real editor synchronization requirements justify it.

Tooling and implementation

The primary implementation is Rust. Its core is #![no_std] plus alloc, with no filesystem, network, clock, thread-runtime, environment, or operating-system randomness assumptions. Hosts provide source modules, seeds, data, formatter results, and persistence explicitly. The core has no external dependencies and forbids unsafe code; the WASM adapter isolates the target allocator.

JavaScript support targets wasm32-unknown-unknown through a dependency-light, handwritten linear-memory ABI and JavaScript/TypeScript wrapper for Deno and browsers. The WASM adapter supplies its global allocator. A C API is not part of the initial scope.

The implementation provides a parser with precise spans, an immutable compiled representation, typed Rust APIs, deterministic seeded sessions, structured errors, traces, corpus audits, and a formatter boundary. An LSP transport remains deferred until editor synchronization requirements are known. The runtime separates immutable grammar content from mutable sampler history so nearby NPCs can share repetition memory without making every generator globally stateful.

The production core remains no_std + alloc, while unit-test harnesses and integration tests may use std. Integration tests load checked-in .meco packages from the filesystem, exercise real imports, and compare exact diagnostics and deterministic seeded results. Deno and browser harnesses test the compiled WASM interface.

The full rationale, semantic contract, validation plan, localization boundary, performance constraints, and implementation phases are in SPECIFICATION.md. The formal lexical and strict front-matter grammar is in SYNTAX.md. The host package, WASM ownership/handle, JavaScript error, and CLI stream contracts are in INTERFACES.md. The implementation order and completion gates are tracked in ROADMAP.md.

Project structure

README.md                    Mecojoni overview and canonical syntax corpus
SPECIFICATION.md             Detailed specification and implementation plan
SYNTAX.md                    Normative lexical and complete source grammar
INTERFACES.md                Package, WASM, JavaScript, and CLI contracts
ROADMAP.md                   Phased implementation plan and completion gates
COMPATIBILITY.md             Frozen language/runtime/ABI compatibility policy
CONFORMANCE.md               Cross-runtime fixture and release test index
BENCHMARKS.md                Native/WASM workloads and optimization evidence
RELEASE.md                   Distribution release checklist
BYTECODE_FORMAT.md           Normative frozen bytecode/1 layout and policy
BYTECODE_FORMAT_PLAN.md      Completed compiled-artifact and embedding plan
Cargo.toml                   Rust 2024 workspace (MSRV 1.85)
crates/
  mecojoni-benchmarks/       Native operation/allocation workload harness
  mecojoni-cli/              Optional std authoring CLI
  mecojoni-core/             Safe, dependency-free no_std + alloc core
    tests/fixtures/          Filesystem-backed integration corpus
  mecojoni-wasm/             Handwritten WASM ABI and target allocator
js/
  mecojoni.ts                Browser-neutral TypeScript wrapper
  mecojoni_test.ts           Normative Deno integration suite
  browser_smoke.*            Same-artifact browser integration harness
editors/vscode/              TextMate grammar and editor configuration
examples/                    Checked single- and multi-module examples
docs/decisions/              Evidence-backed architecture decisions
assets/                      Mecojoni logo

Current limitations

The implementation currently provides owned UTF-8 sources, dual byte/scalar spans, ordered multi-error diagnostics, complete source parsing including cooked block interpolation, immutable multi-module compilation, exact weighted/1 generation, iterative graph analysis/expansion, deterministic work limits, versioned profile records, the composition audit, a version-discovery WASM ABI, and target-spanning tests. The WASM ABI now includes allocation, package, compilation, generation, result-copy, diagnostic, opaque-handle, disposal, and lifecycle-counter operations; the dependency-free TypeScript wrapper is tested in Deno and Chrome against the same artifact. The executable subset now supports static and dynamic exact weights, typed scalar/enum request data, guards, typed rule calls, emitting captures, ordered silent bindings, ordinary references, all literal/block forms, empty output, public entries, productive recursion, and opt-in binding/selection traces with exact evaluated weights. Stable external messages, typed message/input manifests, transitive complete-message effects, explicit locale fallback, formatter provenance, and synchronous Rust/JS formatter boundaries are also executable across the shared Rust/Deno/Chrome corpus. Transactional diverse/1 sessions, hard/soft cooldown, bounded exact/edge histories, winner-only commit, and cross-target deterministic sequences are executable as well. Stable production IDs, exact output provenance, overlap-only structural/rendered audits, copy-on-write snapshots, and replay receipts are executable through Rust and the Deno-tested WASM wrapper. The std CLI, conservative formatter, subprocess contract suite, and initial editor grammar are executable as well. Static trace-off weighted rules use precomputed cumulative indexes, and production-ID collision checks are O(n log n); both optimizations have committed native/WASM evidence. A production Fluent adapter and compound value records are explicitly deferred extensions, not unimplemented release promises. The generic boundary is nevertheless exercised against real Fluent resources by a Rust-only dev-dependency integration test. See COMPATIBILITY.md, CONFORMANCE.md, RELEASE.md, the frozen bytecode format, and its completed implementation plan.

Name

“Mecojoni” is Roman slang loosely conveying “wow.” It was chosen because it is memorable and fun to say.

About

A readable context-free grammar format for generative dialogue and text.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages