Skip to content

Latest commit

 

History

History
279 lines (215 loc) · 6.55 KB

File metadata and controls

279 lines (215 loc) · 6.55 KB

QCL Language

QCL is a small expression language for ACL-style checks over structured contexts.

English version. Chinese version: 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:

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

"Hello"
'Hello'
17
-1
3.14
true
false
nil

Lists

[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.

{
  "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}}

Access

Context access

@ 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

Postfix access

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

Operators

Operator precedence, from high to low:

  1. Postfix access: .
  2. Unary: ! -
  3. Multiplicative: * / %
  4. Additive: + -
  5. Comparison: == != < > <= >= in
  6. Logical AND: &&
  7. Logical OR: ||
  8. Nullish coalesce: ??
  9. 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:

@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
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'

Examples

@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"

Grammar

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 @, 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.