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) # Returns 8
| math::mul(4, 2) # Returns 8
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
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
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:
"My Project" | md::h(1)
# equivalent to md::h("My Project", 1)
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"
| md::reference_index(nodes)
# => {"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"
| md::resolve_references(nodes)
| 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 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::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.
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--allowed-domain. This is a separate permission from--allow-net, which only gates thehttp()/http_request()builtins, 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 | Enable HTTP module imports. Disabled by default; import/include of a github.com/... or https://... URL fails without this. |
--refresh-modules | Discard cached mutable-ref modules and re-fetch them, updating their mq.lock entries. |
--allowed-domain <domain> | Allow HTTP imports from an additional domain beyond the default (raw.githubusercontent.com/harehare). Repeat to add multiple domains. Has no effect unless --allow-http-import (or --allow-all) is also passed. |
--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 --allowed-domain example.com 'self' file.md
# Allow multiple domains
mq --allow-http-import --allowed-domain example.com --allowed-domain 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) and write_file(path, content) 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_get(url, headers = {}), http_post(url, body, headers = {}),
http_put(url, body, headers = {}), http_patch(url, body, headers = {}),
http_delete(url, headers = {}), and http_head(url, headers = {}) are convenience wrappers
around http(:get, url, headers), http(:post, url, body, headers), and so on, for the most
common cases — headers defaults to {} and can be omitted.
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 |