Contents

Packages (v0.1)

Mere's package system is intentionally small. A project is a directory that contains a .mere_modules/ subdirectory; every top-level entry in that subdir is a "package", every .mere file inside a package is a "module". Imports of the form import "<package>/<module>.mere"; resolve by walking up from the importing file toward the filesystem root until a .mere_modules/ directory is found, then looking up <package>/<module>.mere inside it.

Layout

my_app/ main.mere ← your code .mere_modules/ mere-http/ router.mere ← a package's module session.mere mere-db/ pg.mere README.md

Inside main.mere:

import "mere-http/router.mere"; import "mere-db/pg.mere";

The resolver walks from my_app/main.mere up to my_app/, finds .mere_modules/, and reads .mere_modules/mere-http/router.mere and .mere_modules/mere-db/pg.mere.

Nested imports

When a vendored module imports another vendored module, the resolver walks up from that module's directory. Since it's still inside your project tree, it finds the same top-level .mere_modules/.

my_app/main.mere → imports "mere-http/router.mere" → walks up from .mere_modules/mere-http/ to my_app/ → finds .mere_modules/, resolves nested imports there → imports "mere-cookie/cookie.mere" → resolves to .mere_modules/mere-cookie/cookie.mere

This mirrors Node.js's node_modules walk semantics. Cross-package imports Just Work as long as everything lives under one project root.

How to vendor a package

Manual vendoring always works — the compiler only cares about the on-disk layout, not how the files got there — and mere install (below) automates it from a manifest. There is no central registry. Suitable manual methods:

git clone (recommended for tracked deps):

cd my_app mkdir -p .mere_modules git clone https://github.com/<owner>/<pkg-name> .mere_modules/<pkg-name>

git submodule (recommended when the app itself is a git repo):

cd my_app git submodule add https://github.com/<owner>/<pkg-name> \ .mere_modules/<pkg-name>

tarball drop (for one-shot bundling):

curl -L https://example.com/<pkg>.tar.gz | tar xz -C .mere_modules/

All three produce the same on-disk layout. The compiler doesn't care how the files got there.

Precedence

Import resolution tries paths in this order:

  1. <current_file_dir>/<path> — historical behaviour for

same-directory / relative imports (import "./util.mere" still works exactly as before).

  1. <nearest_.mere_modules_up>/path — the new v0.1 rule.
  2. Each directory in -I (CLI) plus MERE_PATH (env var), in that

order.

Absolute paths (starting with /) skip all of the above and resolve literally.

Global module dirs

For a shared cache across projects — e.g. ~/mere-modules/ — use MERE_PATH. It's colon-separated (: on POSIX). Set it in your shell rc:

export MERE_PATH=~/mere-modules

Then import "hello/greet.mere"; in any project also picks up ~/mere-modules/hello/greet.mere as a fallback after the project- local .mere_modules/ is exhausted.

v0.2 (experimental): mere.toml + mere install

Manual vendoring works, but mere install automates it from a manifest. Write a mere.toml next to your entry file:

[package] name = "my_app" version = "0.1.0"

[dependencies] http = { git = "https://github.com/merelang/mere", subdir = "contrib/http", rev = "<commit>" } db = { git = "https://github.com/merelang/mere", subdir = "contrib/db", rev = "<commit>" }

then:

mere install # or: mere install <dir>

This fetches each dependency at its pinned rev (a monorepo subdir is supported, so you can depend on one package inside a larger repo) and writes it under its module path. Each fetched package's own mere.toml [package] path decides where it lands — .mere_modules/github.com/owner/repo/ — so a Go-style full-path import (import "github.com/owner/repo/x.mere") resolves directly. A package with no declared path falls back to its bare source-directory name (legacy contrib layout), and ../ relative imports inside a monorepo are still followed.

Transitive dependencies across repos are pulled in automatically: the installer reads each fetched package's own [dependencies] and follows them, so an app that depends on A (which depends on B in another repo) gets B too, even though the app never names it.

A mere.lock records every resolved package (including transitive) with its full commit sha and a content hash. On a later install, that hash is verified: if a pinned (git, rev) coordinate ever produces different content — a moved tag, a force-push, a corrupt cache — the install fails loudly (go.sum-style integrity), rather than silently building against something else.

Still minimal: rev is an exact git commit (no version ranges), there's no central registry, and installs are whole-package.

Running without a compiler checkout: [host] + mere serve

A compiled .wasm needs a Node host to supply its extern imports (puts, read_file, http_serve, redis_*, sse_*, …). Those live in the compiler repo, so an app couldn't run without a checkout. Add a [host] section to fetch them into a self-contained .mere_host/:

[host] git = "https://github.com/merelang/mere" rev = "<commit>"

mere install then vendors scripts/*.js + contrib/**/*.glue.js into .mere_host/, flattening every require("…/x.js") to require("./x.js") so the bundle stands alone. Run a compiled server with:

mere serve app.wasm # = node .mere_host/run_http_server.js app.wasm

So a released mere binary + mere install is enough to build and run an app — no compiler source tree required at runtime.

The stack a program needs


[package]
name = "deep"
version = "0.1.0"
stack = "512MB"         # optional; also `0x20000000` or a plain byte count

A program built from the C or LLVM backend runs its work on a thread sized to this, so the request travels with the source instead of with the command that runs it. Without it, a deeply recursive program dies the way it always did — it still SAYS stack overflow (recursion too deep) rather than exiting 139 in silence, but the only cure lived outside the build and was spelled differently on every host: -Wl,-stack_size on Darwin, ulimit -s on Linux, --stack-size for node.

It costs address space, not memory: a trivial program asking for 512 MB holds about 1.5 MiB resident.

Every thread gets it, not only `main`'s (v0.1.540). A thread started with spawn is created with the same size, so a recursion that finishes on the main thread also finishes one spawn away; without a request, spawned threads keep the host's default (512 KiB on macOS). A spawned thread's overflow is named the same way as main's, and 64 threads under a 512 MB request hold about 2.6 MiB. So does every coroutine (coro_new, v0.1.543; C and LLVM): its stack is the requested size, or 8 MiB without a request, reserved below a guard page.

The Wasm and RV32IM backends refuse a file that asks. There the stack is not the program's to size — Node takes --stack-size when it runs the module, and a bare-metal image gets its stack from the linker script — so asking is an error rather than something quietly dropped. A size the compiler cannot read is refused too: a request nobody honours is worse than no request, because the program looks like it asked.

The compiler a package needs


[package]
name = "thing"
version = "0.1.0"
mere = ">= 0.1.480"     # optional; `x.y.z` means the same as `>= x.y.z`

Checked against the compiler that is running — by mere install, for this package and for every dependency it fetches, and by every build of a file inside the package. Without it the failure still happens, just later and mute: the newer syntax reaches an older parser and comes back as a syntax error in somebody else's file.

A manifest with no mere line makes no claim and is never refused. A line the compiler cannot read (^1.2, a range) is refused by name rather than ignored: a constraint nobody reads is worse than no constraint, because it looks like one.

mere fix — make the declared floor true


mere fix app.mere
# fix: ./mere.toml -- set `mere = ">= 0.1.422"`
#       (128-bit SIMD lane types (`f64x2` / `u8x16`) needs it)

Parses the file, asks which of the compiler's dated features it actually uses, and writes the highest into the nearest mere.toml. It writes that line and nothing else — a tool that "fixes" a file is one people stop reading the diff of.

mere --features prints the table it computes from (name, version, and the word scripts/version_floor_check.sh re-derives the version from in docs/changelog.md, so a row invented by hand fails the gate). The table is short on purpose: a row earns its place only for a feature that old compilers in the wild do not have.

The API surface, as data


mere --decls --json app.mere

{
  "mere": "0.1.504",
  "requires": ">= 0.1.422",
  "values": [ { "name": "pick", "type": "(color -> int)", "status": "ok", "note": "",
                "line": 3, "doc": "Which number a colour stands for." } ],
  "types":  [ { "name": "color", "kind": "variant", "params": [], "line": 1,
                "constructors": [ { "name": "Red", "payload": null } ] } ]
}

The same surface mere --decls prints, plus the type declarations and the package's declared floor — the promise and the surface in one document, which is how Gleam's package-interface carries its version constraint too.

What it is for: the diff between two versions of a package. mere fix writes a floor from the features a file uses; it cannot say whether the API a downstream repository depends on has changed. That question is a diff of this.

status is ok, duplicate or shadows-builtin: the last two are the declarations --decls prints commented out, because pasting them back would change the program. scripts/decls_json_check.sh rebuilds the text from this JSON and compares it byte for byte, so the two outputs cannot drift.

The same surface, for a reader


mere doc app.mere

TYPES

  color
    The three we actually use.

VALUES

  pick : (color -> int)
    Which number a colour stands for.

  helper : (int -> int)

The comment block written directly above a definition is its documentation — the same block mere lsp shows on hover, so the two cannot drift apart. A name with nothing written above it is still listed, with no block under it: otherwise "this file does not export that" and "nobody wrote a comment about it" would come back as the same answer.

⚠ mere doc lists only the file's OWN names. import splices declarations in, so --decls sees the imported names as well and prints them on purpose — that output is for pasting, and they are in scope. Documentation is a different question.

How a package is checked: [test] + mere test


[test]
run = ["sh verify.sh", "sh test/zlib_check.sh"]

mere test (v0.1.552) runs the run list of the nearest mere.toml, in order, from that file's directory. Without a list it runs verify.sh there -- the convention most packages already follow -- and with neither it says so and exits 2. It is not a test framework: the checks are whatever the package already has, and mere test is where they are found and what their exit statuses mean.

exit statusclassmeaning
0PASS
2CANNOTthe check could not answer (a tool it needs is missing)
3SKIPthe check is optional and was not run here
201TIMEOUTthe bound in scripts/bounded.sh
anything elseFAIL

mere test itself exits 1 if any check failed or timed out, 2 if any could not answer, 0 otherwise. Each command sees MERE and MERE_BIN (the compiler running it -- the variable most verify.sh files run) and, when that compiler is a checkout's build, MERE_ROOT (the checkout).

A list and not a glob, for the reason test/contrib_ctests.txt gives: a check that should have been found and was not says nothing. To run many packages at once, or every gate of a CI workflow, and to compare a run with the last one, there is a gate runner written in Mere (mgate).

Deliberate non-goals (for now)

No central registry. merelang.org-hosted registry is planned for v0.3+; the design work is in the project's internal notes.

No version resolution. rev pins an exact commit. If two packages pin different revs of a shared dependency, whichever wins the walk-up wins the import. Semver ranges are a v0.3+ concern.

No version resolution. If two vendored packages both bundle a different version of mere-http, whichever wins the walk-up wins the import. Sort this out at the deployment layer for now.

Demo

examples/pkg_demo/ is a self-contained test — one entry file and one vendored module in .mere_modules/hello/greet.mere. Try:

dune exec ./bin/mere.exe -- -w examples/pkg_demo/main.mere \ > /tmp/pkg.wat wat2wasm --enable-tail-call /tmp/pkg.wat -o /tmp/pkg.wasm node scripts/run_wasm.js /tmp/pkg.wasm # → hello, world!

Full-path imports (Go-style, identity in the path)

To make a package's identity unambiguous, an import path may include the package's origin, mirroring its on-disk location:

import "github.com/owner/http/router.mere";

vendored as .mere_modules/github.com/owner/http/router.mere. Because the owner is part of the path, two packages named http from different owners (github.com/a/http vs github.com/b/http) live at different paths and never collide — identity is the path, not a bare name.

In-repo resolution via a module path

A project can declare its own module path in mere.toml:

[package] name = "mere" path = "github.com/merelang/mere"

An import whose path starts with that module path resolves to local files, relative to the mere.toml directory — so in-repo code and an external consumer write the exact same import, the former resolving to the working tree and the latter to a vendored .mere_modules/ copy:

# inside github.com/merelang/mere, contrib/http/router.mere: import "github.com/merelang/mere/contrib/log/log.mere"; # → ./contrib/log/log.mere

Resolution order for a relative import path is: (1) module-path-local (if it matches the declared path), (2) importer-relative, (3) nearest .mere_modules/ walk-up, (4) -I / MERE_PATH. A project with no path declared is unaffected — full-path imports simply resolve via .mere_modules/.