QCL is a small expression language for ACL-style checks over structured contexts.
English version. Chinese version: LANG.zh.md.
- Whitespace is ignored.
//starts a single-line comment./* */starts a block comment (can be nested).- Strings can be wrapped in either
"or'. - Supported escapes are
\\,\",\',\n,\r,\t,\0, and\uXXXX(Unicode code point). - Numeric literals may have a leading
+or-. - Integer literals support decimal, hexadecimal (
0x/0X), and octal (0o/0O) bases. - Integers are
i64; floats aref64. - Outside
@, bare identifiers are treated as string values, not variables. - Inside
@paths, the same identifier tokens are treated as field names. - Exact keywords are
true,false,nil, andin.
Examples:
foo == "foo"
123
0xFF
0o77
-1
"hello\nworld"
"\u0041lice" // "Alice"
// single-line comment
/* block comment */
QCL values include:
StringIntFloatBoolNilListMap
Nil is an explicit null-like value.
"Hello"
'Hello'
17
-1
3.14
true
false
nil
[1, 2, "three", true]
[]
[1, 2, 3,]
Map keys are evaluated like normal expressions first; if the result is a primitive value, it is converted to a string.
Allowed key result types are String, Int, Float, and Bool.
{
"name": "Alice",
42: "answer",
true: "yes",
name: "literal key"
}
Nested List and Map values are allowed, and list/map elements can contain full expressions.
[1 + 2, @user.age, {"name": @user.name}]
{"sum": 2 + 3, "user": {"id": @req.user.id}}
@ starts a context lookup.
@req.user.role
@record.granted
@users.0.name
Path segments may be:
- an identifier
- a quoted string
- an integer index
- a parenthesized expression
- another
@access
The first path segment selects a top-level context key. List indices are zero-based.
Negative integer indices count from the end: -1 is the last element, -2 the second-to-last, etc.
Out-of-bounds access and missing map keys return nil.
Examples:
@req."user-data"."is-active"
@data.'special-field'
@users.(@index)
@req.user."name"
@list.-1 // last element
@list.-2 // second-to-last
Primary expressions can be followed by .field or .index. Use parentheses when you want to access the result of a compound expression.
[1, 2, 3].1
{"name": "Alice"}.name
({"users": [1, 2]}).users.1
Operator precedence, from high to low:
- Postfix access:
. - Unary:
! - - Multiplicative:
* / % - Additive:
+ - - Comparison:
== != < > <= >= in - Logical AND:
&& - Logical OR:
|| - Nullish coalesce:
?? - Ternary:
? :
Notes:
- Binary operators are left-associative.
&&and||short-circuit.??returns the left operand if it is notnil, otherwise the right operand.? :requires aBoolcondition;cond ? true_expr : false_expr.!only acceptsBool.-(unary) negatesIntandFloat.inmeans:- substring when both sides are strings
- membership when the right side is a list and the left side is a single value
- subset when both sides are lists
- key membership when the right side is a map and the left side is a string
Examples:
@req.user.role == "admin"
@req.user.id in @record.granted
@user.age > 18 && @user.active
!@user.disabled
-@price
"name" in {"name": "Alice"}
@nickname ?? "anonymous"
@active ? "yes" : "no"
- Failed access returns
nil. @nonexistent == nilistrue.@nonexistent != nilisfalse.@nonexistent != 1istrue.
Default features:
jsonsem_arithstd(provides expression cache and deserialization)
Optional features:
yamltomladv_arithwasm(WebAssembly bindings viawasm-bindgen)ffi(C-compatible FFI)python(Python bindings via PyO3)
The library supports no_std (with alloc) when the std feature is disabled.
sem_arith changes integer division so exact results stay integers and inexact results become floats.
adv_arith enables extra arithmetic:
String + Int/FloatList + ListList + Valueappends one elementList - Valueremoves the first matching elementList - Listremoves values present in the right listMap + Map(right-hand keys win)Map - Mapremoves keys present in the right mapMap - Stringremoves one key
Contexts are parsed from JSON, YAML, or TOML depending on enabled features.
In the default build, JSON is the default parser.
The CLI reads the context from stdin and the expression from argv.
CLI flags:
--check/-c: exit with code 0 fortrue, 1 forfalse--ast: print the parsed AST instead of evaluating--version/-V: print version--help/-h: print usage--json/--yaml/--toml: force input format
echo '{"req": {"user": {"role": "admin"}}}' | cargo run -- '@req.user.role == "admin"'
echo 'name: test' | cargo run --features yaml -- --yaml '@name == "test"'
echo 'name = "test"' | cargo run --features toml -- --toml '@name == "test"'
echo '{"x": 1}' | cargo run -- --check '@x == 1' && echo ok
echo '{"x": 1}' | cargo run -- --ast '@x + 1'@record.published || @record.owner == @req.user.id
@req.user.role == "admin" || @req.user.id in @record.granted
@config."debug-mode" && {"name": @user.name}.name == "lk"
Informal grammar:
exp ::= ternary
ternary ::= coalesce [ "?" ternary ":" ternary ]
coalesce ::= or { "??" or }
or ::= and { "||" and }
and ::= cmp { "&&" cmp }
cmp ::= add { ("==" | "!=" | "<" | ">" | "<=" | ">=" | "in") add }
add ::= mul { ("+" | "-") mul }
mul ::= unary { ("*" | "/" | "%") unary }
unary ::= "!" unary | "-" unary | postfix
postfix ::= primary { "." segment }
primary ::= nil | bool | number | string | id | list | map | at | "(" exp ")"
list ::= "[" [ exp { "," exp } [ "," ] ] "]"
map ::= "{" [ pair { "," pair } [ "," ] ] "}"
pair ::= exp ":" exp
at ::= "@" segment { "." segment }
segment ::= id | string | int | "(" exp ")" | at
number ::= [ "+" | "-" ] ( digit+ [ "." digit+ ] | ("0x" | "0X") hex+ | ("0o" | "0O") oct+ )Notes:
- Outside
@, bareidtokens evaluate as strings. stringmeans a quoted string token.Mapkeys must finally resolve to a primitive value.- Ternary is right-associative; coalesce (
??) is left-associative.