# QCL Language
QCL is a small expression language for ACL-style checks over structured contexts.
English version. Chinese version: [LANG.zh.md](LANG.zh.md).
## Lexical Rules
- 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 are `f64`.
- 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`, and `in`.
Examples:
```text
foo == "foo"
123
0xFF
0o77
-1
"hello\nworld"
"\u0041lice" // "Alice"
// single-line comment
/* block comment */
```
## Values
QCL values include:
- `String`
- `Int`
- `Float`
- `Bool`
- `Nil`
- `List`
- `Map`
`Nil` is an explicit null-like value.
## Literals
### Basic
```text
"Hello"
'Hello'
17
-1
3.14
true
false
nil
```
### Lists
```text
[1, 2, "three", true]
[]
[1, 2, 3,]
```
### Maps
`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`.
```text
{
"name": "Alice",
42: "answer",
true: "yes",
name: "literal key"
}
```
Nested `List` and `Map` values are allowed, and list/map elements can contain full expressions.
```text
[1 + 2, @user.age, {"name": @user.name}]
{"sum": 2 + 3, "user": {"id": @req.user.id}}
```
## Access
### Context access
`@` starts a context lookup.
```text
@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:
```text
@req."user-data"."is-active"
@data.'special-field'
@users.(@index)
@req.user."name"
@list.-1 // last element
@list.-2 // second-to-last
```
### Postfix access
Primary expressions can be followed by `.field` or `.index`. Use parentheses when you want to access the result of a compound expression.
```text
[1, 2, 3].1
{"name": "Alice"}.name
({"users": [1, 2]}).users.1
```
## Operators
Operator precedence, from high to low:
1. Postfix access: `.`
1. Unary: `! -`
1. Multiplicative: `* / %`
1. Additive: `+ -`
1. Comparison: `== != < > <= >= in`
1. Logical AND: `&&`
1. Logical OR: `||`
1. Nullish coalesce: `??`
1. Ternary: `? :`
Notes:
- Binary operators are left-associative.
- `&&` and `||` short-circuit.
- `??` returns the left operand if it is not `nil`, otherwise the right operand.
- `? :` requires a `Bool` condition; `cond ? true_expr : false_expr`.
- `!` only accepts `Bool`.
- `-` (unary) negates `Int` and `Float`.
- `in` means:
- 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:
```text
@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"
```
## Special Semantics
- Failed access returns `nil`.
- `@nonexistent == nil` is `true`.
- `@nonexistent != nil` is `false`.
- `@nonexistent != 1` is `true`.
## Feature Flags
Default features:
- `json`
- `sem_arith`
- `std` (provides expression cache and deserialization)
Optional features:
- `yaml`
- `toml`
- `adv_arith`
- `wasm` (WebAssembly bindings via `wasm-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/Float`
- `List + List`
- `List + Value` appends one element
- `List - Value` removes the first matching element
- `List - List` removes values present in the right list
- `Map + Map` (right-hand keys win)
- `Map - Map` removes keys present in the right map
- `Map - String` removes one key
## Input Formats
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 for `true`, 1 for `false`
- `--ast`: print the parsed AST instead of evaluating
- `--version` / `-V`: print version
- `--help` / `-h`: print usage
- `--json` / `--yaml` / `--toml`: force input format
```bash
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'
```
## Examples
```text
@config."debug-mode" && {"name": @user.name}.name == "lk"
```
## Grammar
Informal grammar:
```ebnf
exp ::= ternary
ternary ::= coalesce [ "?" ternary ":" ternary ]
coalesce ::= or { "??" or }
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 `@`, bare `id` tokens evaluate as strings.
- `string` means a quoted string token.
- `Map` keys must finally resolve to a primitive value.
- Ternary is right-associative; coalesce (`??`) is left-associative.