README
¶
mace
Mace is a typed configuration language and Go toolkit for producing deterministic object data. Mace is a new language created in 2026, designed to make configuration contracts explicit, deterministic, and easy to validate.
This repository contains:
- a parser, evaluator, and validator for
.macefiles - a CLI for inspecting, formatting, and evaluating Mace documents
- a language server for editor integrations
- a public Go package for parsing, unmarshalling, and marshalling Mace data
Status
Mace is actively implemented in this repository. The current language contract is documented in the formal specification.
Features
- Typed script declarations for
alias,schema, and variables - Literal
choice[...]types for user-selectable value domains - Choice-aware editor completions for literal domains and variants
- Deterministic expression evaluation
- Output validation against local schemas or external schema files in implicit or explicit data outputs
- Relative imports between Mace files and remote imports over HTTP(S)
- Schema-validated runtime input through
parse = <Schema>andparse_file = '<path>'in data outputs, including remote schema files over HTTP(S); parsed fields are exposed as$-prefixed variables,parseselects an already-available schema, andparse_fileloads schema declarations and can infer the schema when the referenced file exports exactly one schema - Canonical source formatting
- Language Server Protocol support over stdio
- a Go codec package for parsing, unmarshalling, marshalling, and format conversion
Language overview
A Mace file can contain:
- an optional script block
- exactly one output block
Imports use from ... import ...; and must appear at the top of the script
block before other declarations. Imported names may optionally define a
local alias with Name:Alias. Use from './schema.mace' bind Name; to bind an output schema file as a single schema or an output data file as a single record variable.
Example:
|===|
from './shared.mace' import User:ProfileUser;
alias Environment: choice["dev", "prod"];
Environment env = "prod";
ProfileUser current = {
name: "Ada",
age: 27
};
|===|
[output = 'data']
{
env: env,
current: current
}
Aliases only rename the local reference inside the importing file. They do not rename the exported key in the imported file.
Mace supports:
:for alias declarations (alias,schema)=for variable initializers- primitive types:
string,int,float,hex_int,hex_float,boolean - arrays:
array<T> - open records:
record<T>for arbitrary keys whose values must matchT - fusions:
fusion[T1, T2, ...] - variants:
variant[T1, T2, ...] - choices:
choice["a", 1, true, OtherChoice] - named type aliases
- schemas, including recursive named-schema references such as
array<Node> - literal
choice[...]aliases with mixed scalar members, reusable choice aliases, and variant-friendly autocomplete - record, array, arithmetic, logical, and conditional expressions
- record and data output field shorthand:
{ name, }expands to{ name: name, }, and it works for strings, numbers, arrays, nested records, and output blocks - output fields evaluate expressions directly; parentheses may group any expression for precedence, associativity, or readability
- commas separate record, schema, and output fields; semicolons terminate declarations and statements
$selfreferences inside output evaluation- hexadecimal integer and fractional numeric types with canonical string JSON output
Fusion and variant types are first-class across the language, including named aliases, output schema validation, imports, formatter output, and editor tooling.
Mace treats variants as closed alternatives: values must match exactly one member, record members reject unknown fields, and record values may not combine fields from different variant branches.
|===|
alias Identity: variant[string, int];
alias Values: variant[array<string>, array<int>];
Identity primary = "Ada";
Identity fallback = 42;
Values tags = ["api", "web"];
|===|
[output = 'data']
{
primary: primary,
fallback: fallback,
tags: tags
}
Mace treats fusions as composition: schema members are combined into one closed record shape.
Unicode identifiers
Mace accepts international Unicode identifiers and interprets every identifier using Unicode NFC. Canonically equivalent spellings therefore refer to the same name, while the formatter emits the canonical NFC spelling:
|===|
string naïve = "ok";
|===|
[output = 'data'] { value: naïve, }
Declaring both naïve and naïve is a duplicate declaration. NFC does not
normalize strings or paths, and it is not case folding or compatibility
normalization; visually confusable characters can still be distinct.
|===|
schema Profile: { name: string };
schema Audit: { created_at: string };
alias User: fusion[Profile, Audit];
User value = {
name: "Ada",
created_at: "2026-04-08"
};
|===|
[output = 'data']
{
value: value
}
Choices define finite literal domains directly in the type system.
Choice aliases can be merged with fusion[...] and embedded inside variants.
|===|
alias Access: choice["read", "write"];
alias Feature: choice["write", "execute"];
alias Permission: fusion[Access, Feature];
Permission value = "execute";
|===|
[output = 'data']
{
value: value
}
Hexadecimal values stay distinct from decimal numerics. When emitted through
mace json, hex_int and hex_float values are serialized as strings such as
"0xFF" and "0x2.8" so their hexadecimal spelling is preserved.
hex_int is signed 64-bit; arithmetic and overflowing left shifts fail rather
than wrap, and its minimum value is written -0x8000000000000000.
hex_float accepts arbitrarily long fixed-point hexadecimal components and is
serialized as an exact, uppercase, fixed-point binary64 expansion with a
required fractional component. This makes every finite value round-trip
without precision loss. The largest finite literal is reproducibly constructed
as "0x" + strings.Repeat("F", 256) + ".0" and represents
math.MaxFloat64.
For the exact rules and currently supported syntax, see the formal specification.
Installation
Build locally
go build ./cmd
Install the CLI
go install github.com/louiss0/mace/cmd@latest
Package managers are supported through Homebrew, Winget, and Nix.
Mace 1.0.0 is the first stable release of this new language, created in 2026. It defines a deterministic, strongly typed configuration format with a CLI, editor tooling, and a public Go codec.
If you are working on this repository directly, you can also run:
go run ./cmd --help
CLI
The root command is mace.
mace json <path>
mace import <path>
mace check <path>
mace nodes <path>
mace output <path>
mace lsp
mace json <path>
Evaluates a Mace file and prints the computed output block as JSON.
mace json ./config.mace
You can provide runtime parse input with --input using a Mace record literal:
mace json ./config.mace --input '{ env: "prod", token: "abc" }'
Example input:
|===|
schema Runtime: { env: string; };
int base = 2 + 2;
|===|
[output = 'data', parse = Runtime]
{
env: $env,
base: base
}
Example output:
{
"base": 4,
"env": "prod"
}
mace import <path> [path...]
Converts JSON, YAML, and TOML files into .mace files.
- input format is determined from each file extension
- generated files are written next to the source files by default
- JSON files with a
$schemakey are converted into Mace output schema blocks - other JSON, YAML, and TOML files are converted into Mace output data blocks
- JSON Schema
nullmaps to field optionality during schema conversion - JSON Schema
anyOfandoneOfalternatives can be emitted as Macevariant[...]types during import - JSON Schema
allOfschema composition can be emitted as Macefusion[...]types during import - imported
variant[...]types use Mace's closed variant semantics rather than preserving a distinctanyOfversusoneOfbehavior - imported
fusion[...]types represent schema composition and require schema members only - imported
variant[...]andfusion[...]types remain regular Mace types that work in scripts, schema validation, formatting, and LSP tooling - when multiple files are imported, successful files are still written even if some files fail
mace import ./config.yaml
mace import ./config.toml
mace import ./config.json
mace import ./config.json ./config.yaml ./config.toml
Use --output-dir to write generated files to a different directory:
mac
e import ./config.json --output-dir ./generated
mace check <path> [path...]
Checks JSON, YAML, and TOML files for Mace compatibility issues and prints a Mace record report.
- input format is determined from the file extension when available
- JSON can fall back to content detection when no supported extension is present
- syntax problems are reported under
syntax - incompatible keys are reported under
key_incompatibility nullvalues and YAML scalar/tag mismatches are reported undertype_incompatibility- duplicate keys, YAML multi-document files, comments, block scalar style loss,
and structural mismatches such as non-record JSON roots are reported under
structure_incompatibility - multiple files are emitted as a
filesarray of per-file reports
mace check ./config.json
mace check ./config.yaml ./config.toml
Example output:
{
syntax: [],
key_incompatibility: [{
path: "$[\"foo-bar\"]",
reason: "key is not a valid Mace identifier",
format: "json",
key: "foo-bar"
}],
type_incompatibility: [],
structure_incompatibility: []
}
mace nodes <path>
Parses a file and prints its AST-like node structure. This is useful when working on the language itself.
mace nodes ./config.mace
mace output <path>
Parses a file and prints canonical Mace source.
This command does not evaluate the file into runtime JSON output.
mace output ./config.mace
This is useful for inspecting how the formatter normalizes script delimiters, records, choice aliases, and expressions.
mace lsp
Starts the Mace language server over stdio.
mace lsp
The server currently supports:
- diagnostics
- completions
- hover
- go to definition
- document symbols
- code actions
- document formatting
Go package usage
The public Go API lives in ./codec.
Parse Mace into generic Go data
package main
import (
"fmt"
"github.com/louiss0/mace/codec"
)
func main() {
result, err := codec.Parse(`[output = 'data']
{
name: "Ada",
enabled: true
}`)
if err != nil {
panic(err)
}
fmt.Println(result.Data["name"])
}
Parse with runtime input
result, err := codec.ParseWithInput(`|===|
schema Runtime: { env: string; };
|===|
[output = 'data', parse = Runtime]
{
env: $env
}`, map[string]any{
"env": "prod",
})
Unmarshal into a struct
type Config struct {
Name string `json:"name"`
Enabled bool `json:"enabled"`
}
var config Config
err := codec.Unmarshal(`[output = 'data']
{
name: "Ada";
enabled: true;
}`, &config)
Marshal Go values back to Mace
source, err := codec.Marshal(map[string]any{
"name": "Ada",
"enabled": true,
"scores": []int{1, 2, 3},
})
Import JSON, YAML, or TOML into Mace
source, err := codec.ImportYAML(`name: Ada
enabled: true
profile:
level: 2
`)
schemaSource, err := codec.ImportJSONSchema(`{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"name": { "type": "string" }
},
"required": ["name"]
}`)
For schema output, codec.Parse also returns structured schema metadata in
Result.Schema.
Development
Run tests
go test ./...
Repository layout
cmd/- CLI entrypoints and the LSP server commandcodec/- public Go API for parsing, marshalling, and format conversioninternal/lexer/- tokenizationinternal/parser/- parsing and AST constructioninternal/processor/- validation, imports, evaluation, and schema checksinternal/analyzer/- editor analysis, diagnostics, hover, completion, definitions, symbols, code actions, and formatting helpersinternal/formatter/- canonical source formattingdocs/src/content/docs/reference/spec.mdx- current language specificationmace.ebnf- grammar reference
Notes
A few language areas are intentionally still in progress. At the time of writing, the specification lists these as not yet implemented:
- explicit export declarations
License
Mace is distributed under the MIT License.
Optional chaining
Use ?. for optional schema properties and record keys that may be absent.
Resolve an optional access with ?? before placing it in output.
city: user ? user.profile.address?.city ?? "" : "",
packages: record<record<string>>,
value: packages?.codefixer?.cn_efs ?? "",
Each nested record lookup requires a corresponding nested record type. For
example, packages.codefixer.cn_efs requires record<record<string>>; it is
invalid for record<string>.
For a record variant, the permitted chain depth is the common record depth of
every variant member. For example,
variant[record<string>, record<record<string>>] permits one optional lookup
but rejects a second because the first member is already a string.
Accessing an optional schema field with . reports
mace.type.optional-field-access.