Modules and Imports
mq provides several ways to organize and reuse code: module, import, and include.
Module
Defines a module to group related functions and prevent naming conflicts using the syntax module name: ... end.
module module_name:
def function1(): ...
def function2(): ...
end
Functions within a module can be accessed using qualified access syntax:
module_name::function1()
Examples
# Define a math module
module math:
def add(a, b): a + b;
def sub(a, b): a - b;
def mul(a, b): a * b;
end
# Use functions from the module
| math::add(5, 3)
# => 8
Scope
A module, inline or imported, sees only:
- Its own functions and
let/varvalues. - Built-in functions.
- Modules it imports itself, and inline modules defined earlier (by qualified name).
It does not see the query’s own let, var, or def bindings, modules imported outside it, or runtime values such as __FILE__ and --arg. Referring to them is an error.
module csv_tools:
import "csv"
def rows(text): csv::csv_parse(text, true);
end
| csv_tools::rows("a,b\n1,2")
# => [{"a": "1", "b": "2"}]
Module-level let values are computed once per run.
A module may bind each name only once across its let, var and def declarations. Declaring module m again extends the existing module with the new members. A member name already bound by an earlier declaration is a compile error:
module m: def a(): 1; def a(): 2; end # error: module binds "a" more than once
module m: def a(): 1; end | module m: def b(): 2; end # ok: m has a and b
module m: def a(): 1; end | module m: def a(): 2; end # error: module binds "a" more than once
Import
Loads a module from an external file using the syntax import "module_path".
The imported module is available with its defined name and can be accessed using qualified access syntax.
The import directive searches for .mq files in the following locations:
$HOME/.mq- User’s home directory mq folder$ORIGIN/../lib/mq- Library directory relative to the source file$ORIGIN/../lib- Parent lib directory relative to the source file$ORIGIN- Current directory relative to the source file
import "module_name"
Examples
math.mq:
def add(a, b): a + b;
def sub(a, b): a - b;
main.mq:
# Import the math module
import "math"
# Use functions with qualified access
| math::add(10, 5) # Returns 15
| math::sub(10, 5) # Returns 5
Import Aliases
Use import "module_path" as alias to bind the module under a different name, useful for
shortening long module paths or avoiding naming conflicts. Only the alias is bound; the
module’s original name is not available.
import "math" as m
| m::add(10, 5) # Returns 15
| m::sub(10, 5) # Returns 5
Include
Loads functions from an external file directly into the current namespace using the syntax include "module_name".
Unlike import, functions are available without a namespace prefix.
The include directive searches for .mq files in the same locations as import.
include "module_name"
Examples
math.mq:
def add(a, b): a + b;
def sub(a, b): a - b;
main.mq:
# Include math functions
include "math"
# Functions are available directly
| add(2, 3) # Returns 5
| sub(10, 4) # Returns 6
Circular dependencies
A module that imports or includes itself, directly or through other modules, is an error:
Circular module dependency: a -> b -> a
Loading the same module from several places is fine as long as it does not depend back on itself.
Built-in modules
mq ships several built-in modules for parsing common structured data formats.
They are available via import without any additional installation.
| Module | Parse function | Description |
|---|---|---|
json | json::json_parse() | Parses a JSON string |
yaml | yaml::yaml_parse() | Parses a YAML string |
toml | toml::toml_parse() | Parses a TOML string |
xml | xml::xml_parse() | Parses an XML string |
toon | toon::toon_parse() | Parses a Toon string |
csv | csv::csv_parse(has_header) | Parses CSV (, delimiter) |
csv | csv::tsv_parse(has_header) | Parses TSV (\t delimiter) |
csv | csv::psv_parse(has_header) | Parses PSV (| delimiter) |
These modules are also used automatically when you process a file whose extension matches (see CLI auto-parsing).
Example
import "json"
| json::json_parse()
JSON Pointer
The json module also supports JSON Pointer (RFC 6901) references, built on get_path/set_path/del_path.
| Function | Description |
|---|---|
json::json_pointer_get(data, ptr) | Returns the referenced value, or None if it does not exist |
json::json_pointer_has(data, ptr) | Returns true if the reference exists, even when its value is None |
json::json_pointer_set(data, ptr, value) | Sets the value; missing containers become dicts and - appends to arrays |
json::json_pointer_del(data, ptr) | Deletes the value |
json::json_pointer_to_path(data, ptr) | Converts a pointer to a path array for get_path/set_path/del_path |
json::json_pointer_from_path(path) | Converts a path array to a pointer, escaping ~ and / |
import "json"
| let doc = {"a/b": [10, 20]}
| json::json_pointer_get(doc, "/a~1b/1")
# => 20
JSON Patch
The json module can compare two values and apply the result as a JSON Patch (RFC 6902). Values from YAML, TOML and front matter work the same way.
| Function | Description |
|---|---|
json::json_diff(a, b, options) | Returns the patch that turns a into b. A key removed and re-added with an equal value becomes a move; pass {moves: false} to turn that off |
json::json_patch(doc, ops) | Applies add, remove, replace, move, copy and test operations, raising an error if one cannot be applied |
import "json"
| let a = {"name": "mq", "tags": ["md"]}
| let b = {"title": "mq", "tags": ["md", "cli"]}
| json::json_diff(a, b)
# => [{"op": "add", "path": "/tags/1", "value": "cli"}, {"op": "move", "from": "/name", "path": "/title"}]
Dicts are compared by key and arrays by index, so an element inserted at the front of an array shows up as a run of replace operations.
JSON Merge Patch and 3-way merge
| Function | Description |
|---|---|
json::json_merge_patch(doc, patch) | Applies a JSON Merge Patch (RFC 7396): null members remove keys, dict members merge recursively, anything else replaces the value wholesale |
json::merge3(base, ours, theirs) | 3-way merges ours and theirs against their common base, returning {value, conflicts}. Favors ours on a conflicting leaf or a deletion-vs-edit clash; conflicts lists every diverging path as {path, base, ours, theirs} |
import "json"
| json::json_merge_patch({"a": 1, "b": {"c": 2, "d": 3}}, {"a": None, "b": {"c": 9}})
# => {"b": {"c": 9, "d": 3}}
import "json"
| json::merge3({"a": 1, "b": 1}, {"a": 2, "b": 1}, {"a": 1, "b": 3})
# => {"value": {"a": 2, "b": 3}, "conflicts": []}
merge3 only merges nested dicts key-by-key; arrays and scalars are compared as a whole leaf, so a change to any element of an array both sides touched is one conflict on the whole array. A key added independently by both sides with different values is also a conflict, even though neither base had it.
Combine with paths to list a pointer for every leaf: paths(doc) | map(json::json_pointer_from_path).
Markdown Builder (md)
The md module provides functions for constructing markdown nodes from scratch, rather than
filtering or transforming existing ones. Each function returns a markdown value that can be
combined with others using md::doc(), which merges nodes into a single markdown value.
md::doc() accepts either a variable number of arguments or a single array, and flattens nested
arrays automatically, so the result of map() (or any function returning a plain array of nodes)
can be spliced in directly as children, and None entries (e.g. from conditional branches) are
dropped.
Note: This module is under development. APIs and behavior may change without notice.
import "md"
| md::doc(
md::h("My Project", 1),
md::text("Run `cargo install mq`."),
md::code("cargo install mq", "bash"),
map(["fast", "composable", "jq-like"], fn(x): md::list(x);),
)
Since the current value (self) is automatically passed when a call is missing an argument,
builder calls also read naturally in pipeline position:
import "md"
| "My Project" | md::h(1) # equivalent to md::h("My Project", 1)
# => "# My Project"
Lists and tables are built the same way:
import "md"
| md::doc(
# List
md::list("Plain item"),
md::list("Nested item", 1),
md::list("Ordered item", 0, true),
md::list("Checked item", 0, false, true),
# Table
md::table_row(["Name", "Age"]),
md::table_align(["left", "right"]),
md::table_row(["Alice", "30"]),
)
Inline and heading helpers cover the rest of the common formatting cases:
import "md"
| md::doc(
md::h2("Notes"), # md::h1 .. md::h6
md::highlight("key"), # ==key==
md::comment("todo"), # <!-- todo -->
md::wikilink("Page", "alias"), # [[Page|alias]]
md::embed("image.png"), # ![[image.png]]
md::escape("a*b_c"), # a\*b\_c
md::hard_break("a\nb"), # single newlines become hard line breaks
)
Reference indexing and resolution
md::reference_index(md_nodes) and md::resolve_references(md_nodes, index = None) index and
resolve reference-style links ([text][ident], including the shortcut form [ident]), reference
images (![alt][ident]), and footnotes ([^ident]) against their [ident]: url / [^ident]: ...
definitions. Both need every document node at once: pass -A on the CLI, or pipe through nodes
in an inline query/script.
reference_index groups every link and footnote definition by identifier:
import "md"
| nodes
| md::reference_index()
# => {"links": {"ident": [{ident, label, url, title, location}, ...]}, "footnotes": {"ident": [{ident, location}, ...]}}
Each identifier maps to an array of every definition found for it, so a duplicate definition
(more than one [ident]: line for the same ident) shows up as an array with more than one entry.
resolve_references walks every reference node and looks it up in the index, returning one dict
per reference with its type, ident, label, location, whether it resolved to a definition,
whether that definition is duplicate, the definition picked (CommonMark’s first-match-wins, or
None if unresolved), and every candidate definitions:
import "md"
| nodes
| md::resolve_references()
| filter(fn(r): !r[:resolved];)
# => reference nodes with no matching definition (for building linkcheck-style diagnostics)
index defaults to md::reference_index(md_nodes), resolving references against their own
document. Pass an explicit index built from a different set of nodes to resolve references that
got separated from their definitions, for example when a document is split or a section is moved
into another file, which is exactly the case where a reference legitimately comes back unresolved.
Property-Based Test Generators (gen)
The gen module is bundled with mq-test (it is not available to the plain mq command) and
provides generator combinators for mq-test’s
# @property(count, generators) annotation (see the mq-test crate’s README for the annotation
itself). A generator is a plain function fn(seed): value; built on top of the seeded rand/rand_int/
random_string builtins, so the same seed always produces the same value. Combinators build
bigger generators out of smaller ones, deriving an independent sub-seed for each part so nested
generators don’t correlate with one another.
import "gen"
| (gen::int(1, 10))(42)
| Function | Description |
|---|---|
gen::int(min, max) | A uniformly random integer in [min, max] |
gen::float() | A uniformly random float in [0, 1) |
gen::bool() | A random boolean |
gen::string(charset, len) | A random string of len characters drawn from charset |
gen::const(value) | Always value, ignoring the seed |
gen::element(values) | A uniformly random element of values |
gen::one_of(gens) | Picks one of gens and runs it |
gen::array(gen, len) | A fixed-length array of len values from gen |
gen::array_of(gen, min_len, max_len) | A variable-length array (min_len to max_len) of values from gen |
gen::transform(gen, mapper) | Transforms the value gen produces through mapper |
gen::tuple(gens) | One value per generator in gens, independently seeded |
gen::markdown_heading(min_depth, max_depth) | An ATX heading line with a depth in [min_depth, max_depth] (1 to 6) |
gen::markdown_block(max_depth = 6) | One Markdown block: heading, paragraph, bullet list, fenced code block or blockquote |
gen::markdown(min_blocks, max_blocks, max_depth = 6) | A Markdown document of min_blocks to max_blocks blocks |
The Markdown generators produce source text (a string), so a failing document shrinks by
truncation. Parse it inside the property with to_markdown(source).
gen::tuple is what # @property(...) calls under the hood to turn an array of per-parameter
generators into one argument list per iteration; a failing iteration’s reported index doubles as
the seed to reproduce it with, by calling gen::tuple(generators)(seed) again with the same
generators array.
Shrinking (gen::shrink_value, gen::shrink)
When a # @property(...) case fails, mq-test automatically searches for a smaller, simpler
failing example before reporting it. The failure message shows both the shrunk arguments and
the original ones, so the report stays easy to reason about even when the raw seed produced a
large or convoluted value:
| Function | Description |
|---|---|
gen::shrink_value(value) | One step of “smaller” candidates for value (numbers toward 0, strings/arrays toward shorter, dicts toward simpler values) |
gen::shrink(args, is_interesting) | Searches the array args for a smaller array that’s still is_interesting (a fn(args): bool) |
import "gen"
| gen::shrink([10], fn(a): a[0] > 3;)
# => [4]
Both are also usable directly (not just through # @property(...)) by hand-written property
tests.
Small-scope tests (enumerate)
# @small_scope(max_size, enumerators, options) runs a test on every input of size <= max_size,
smallest first, instead of on random inputs. The first failure is a smallest counterexample, so no
shrinking is needed. A clean run means no counterexample was found within the listed scope; it says
nothing about larger inputs. The enumerate module is bundled with mq-test (not available to the
plain mq command) and imported automatically. An enumerator is a dict whose tier(k) returns every
value of size exactly k.
| Enumerator | Values |
|---|---|
enumerate::element(values) | Each of values, all of size 0 |
enumerate::one_of(enums) | The values of any of enums |
enumerate::tuple(enums) | One value per enumerator; sizes add |
enumerate::array(en, max_len) | Arrays of 0 to max_len values of en; size is the length plus the items’ sizes |
enumerate::array_of(en, min_len, max_len) | Like array, with a minimum length |
enumerate::transform(en, mapper) | mapper applied to each value, same size |
enumerate::node(en, mapper) | Like transform, one size larger |
enumerate::markdown(options) | Markdown source text; size is the number of blocks plus list items |
enumerate::int(min, max) | Integers in [min, max]; size is abs(n) |
enumerate::string(charset, max_len) | Strings of 0 to max_len characters of charset; size is the length |
enumerate::bool() | true and false |
enumerate::dict_of(key_en, value_en, max_len) | Dicts of 0 to max_len entries, each listed once |
enumerate::json(options) | JSON-like values (none, booleans, small integers, short strings, nested arrays and dicts) |
# @small_scope(3, [enumerate::markdown({"kinds": ["heading", "paragraph"], "min_blocks": 1})])
def test_first_block(src):
assert_true(is_markdown(to_markdown(src)[0]))
end
Every enumerated input is checked; build inputs that satisfy a precondition with the enumerator
options (here min_blocks). Enumeration stops with an error when max_cases (default 100000) is
exceeded. See the mq-test crate’s README for the options and the full semantics.
HTTP Imports
When mq is built with the http-import feature, import and include accept HTTP/HTTPS URLs
in addition to local file names.
Security note: HTTP imports are disabled by default and must be enabled with
--allow-http-import. Once enabled, only URLs undergithub.com/harehare(resolved toraw.githubusercontent.com/harehare) are allowed; importing from any other domain also requires--allow-http-import=DOMAIN. This is a separate permission from--allow-net, which only gates thehttp()builtin, not module resolution.
Plain URL
import "https://example.com/mymod.mq"
GitHub shorthand
The scheme can be omitted for GitHub repositories.
mq automatically maps the path to raw.githubusercontent.com.
github.com/{owner}/{path}[@{version}]
| Shorthand | Resolved URL |
|---|---|
github.com/alice/mymod | raw.githubusercontent.com/alice/mymod/HEAD/mymod.mq |
github.com/alice/mymod.mq | raw.githubusercontent.com/alice/mymod.mq/HEAD/mymod.mq |
github.com/alice/[email protected] | raw.githubusercontent.com/alice/mymod/v1.0/mymod.mq |
github.com/alice/repo/lib/[email protected] | raw.githubusercontent.com/alice/repo/v2.0/lib/util.mq |
Example:
mq --allow-http-import 'import "github.com/harehare/kdl.mq" | kdl::kdl_parse("title \"Hello, World!\"")'
Caching
Fetched modules are cached in {system_cache_dir}/mq/ as {md5(url)}.mq files.
- Versioned URLs (e.g.
@v0.1.0): cached indefinitely: the tag content is immutable. - Mutable refs (
HEAD,main,master, or no version): cached on first fetch. Pass--refresh-moduleson the command line to discard the cache and re-fetch.
Lock file (mq.lock)
- First use of a URL (fetched or served from the cache): recorded in
mq.lock. - Later use, content unchanged: succeeds silently.
- Later use, content changed: fails with an error explaining the mismatch. Re-run with
--refresh-modulesto accept the new content and updatemq.lock(for a versioned/tagged URL, whose cache--refresh-modulesdoesn’t touch, use--clear-cacheinstead). --refresh-modules/--clear-cachealso drop the corresponding entries frommq.lock(mutable-ref entries only, and all entries respectively), matching their disk-cache behavior.- Pass
--no-lockfileto disable the check entirely (no file is read or written). - Pass
--lockfile <path>to use a different location instead of./mq.lock. Missing parent directories are created automatically. Mutually exclusive with--no-lockfile.
Commit mq.lock alongside scripts that use HTTP imports so CI and teammates fetch the exact
content you locked, the same way package-lock.json/deno.lock work.
CLI options
| Flag | Description |
|---|---|
--allow-http-import[=DOMAIN,...] | Enable HTTP module imports. Disabled by default; import/include of a github.com/... or https://... URL fails without this. =DOMAIN also allows an additional domain beyond the default (raw.githubusercontent.com/harehare); repeat the flag or comma-separate to add more. Use github.com/{user}/{repo} for one repository. |
--refresh-modules | Discard cached mutable-ref modules and re-fetch them, updating their mq.lock entries. |
--no-lockfile | Disable the mq.lock integrity check/update. |
--frozen | Fail instead of recording a new mq.lock entry. Use in CI once mq.lock is committed. Mutually exclusive with --no-lockfile. |
--lockfile <path> | Use <path> instead of ./mq.lock. |
Examples:
# Enable HTTP imports, restricted to the built-in default domain
mq --allow-http-import 'self' file.md
# Force re-fetch of any HEAD/branch modules, accepting new content into mq.lock
mq --allow-http-import --refresh-modules 'self' file.md
# Only allow imports from example.com (in addition to the built-in default)
mq --allow-http-import=example.com 'self' file.md
# Allow multiple domains
mq --allow-http-import=example.com,raw.githubusercontent.com 'self' file.md
# Use a different lock file location
mq --allow-http-import --lockfile config/mq.lock 'self' file.md
# Skip the mq.lock check entirely
mq --no-lockfile 'self' file.md
# CI: fail if a script tries to import a URL not already recorded in the committed mq.lock
mq --allow-http-import --frozen 'self' file.md
Network and File-Write Capabilities
http(method, url) / http(method, url, body) / http(method, url, headers) /
http(method, url, body, headers), write_file(path, content) and mkdir(path) are disabled by default and
must be explicitly enabled with --allow-net / --allow-write. Calling them without the
corresponding flag raises a runtime error explaining how to opt in.
method is a string or symbol ("post" or :post) and accepts any HTTP method (get, post,
put, delete, patch, head, and so on). The optional body argument is sent as the request
body regardless of method. The optional headers argument is a dict of string to string
(e.g. {"Content-Type": "application/json"}) applied to the request.
http_all([...requests]) issues a batch of HTTP requests and returns an array of response
bodies in the same order. Each request is a dict with a required url and optional method
(defaults to get), body, and headers keys, e.g.:
mq --allow-net 'http_all([{"url": "https://example.com/a"}, {"url": "https://example.com/b", "method": "post", "body": "{}", "headers": {"Content-Type": "application/json"}}])'
Requests in the batch run concurrently when the runtime supports it, so fanning out to multiple endpoints is faster than calling http() in a loop. The same URL/domain policy applies as for http(): HTTPS only, and --allow-net (with or without a domain allowlist) is required.
Security note:
httponly acceptshttps://URLs and is routed through the same SSRF-hardened client used for HTTP imports (no automatic redirects), and DNS results are filtered to publicly routable addresses, so a loopback/private/link-local address can’t be reached even with--allow-netset.
# Blocked by default
mq 'http(:get, "https://example.com")'
# Enabled explicitly
mq --allow-net 'http(:get, "https://example.com")'
mq --allow-net 'http(:delete, "https://example.com/resource/1")'
mq --allow-net 'http(:post, "https://example.com", "{}", {"Content-Type": "application/json"})'
mq --allow-net 'http(:post, "https://example.com", "{}", {"Content-Type": "application/json"})'
mq --allow-write 'write_file("out.md", "# Hello")'
--allow-net also accepts a domain allowlist, restricting http/http_* calls to just those domains (and any path under them) instead of granting unrestricted network access:
# Restrict to just example.com
mq --allow-net=example.com 'http(:get, "https://example.com")'
# Repeat the flag, or comma-separate, to allow more than one domain
mq --allow-net=example.com,api.example.org 'http(:get, "https://api.example.org")'
Comparison
| Feature | module | import | include |
|---|---|---|---|
| Purpose | Define a module | Load external module | Load external functions |
| Access | Qualified access (module::func) | Qualified access (module::func) | Direct access (func) |
| Use case | Organize code within a file | Reuse modules across files | Simple function sharing |