A first step towards Spec Driven Design.
LQL is a compact, strongly typed DSL that lets you write powerful filters and validations on JSON‑like data. By emphasizing clear syntax, explicit type handling, and an extensible library system, LQL ensures reliable, unambiguous expressions that work consistently across different environments and languages.
LQL strictly enforces typing rules—no implicit conversions—ensuring consistent behavior across different implementations.
Additionally, LQL is an experiment for Spec Driven Design, using Generative AI technologies to convert formal specification documents into useful code. 99% of the content in this repository was generated by an LLM, with the goal of pioneering a technique to split the job of software engineer into two components, the systems design aspect and the code writing aspect.
-
Age & Country Check:
($user.age >= 18 && $user.country == "US")
-
Conditional:
cond.ifExpr($user.isActive, "allowed", "blocked")
-
Time Comparison:
time.isBefore(time.parse("2025-01-01","dateOnly"), time.now())
-
Regex:
regex.match("^[A-Z]{3}-\\d+$", $ticket.code)
-
String Concatenation:
string.concat($user.firstName, " ", $user.lastName)
- Clone or Download the repository containing the LQL CLI.
- Ensure Go 1.24+ (or later) is installed on your machine.
- Build:
go build -o lql
- Test:
You should see usage details for the LQL command-line interface.
./lql --help
The LQL CLI now supports the following subcommands:
compile
exec
repl
test
validate
highlight
analyze
export-contexts
Run lql <subcommand> --help
to see extra flags for each.
Compiles a single LQL expression into a binary bytecode file. You can supply the expression via -expr
or via -in
(if both are provided, the file takes precedence). Optionally, the output may be signed using an RSA private key.
lql compile [OPTIONS]
Key options:
-expr "<expression>"
: Inline expression to compile.-in <filename>
: Path to a file containing the expression.-out <filename>
(required): Output file for bytecode.-signed
: Indicate signing is desired.-private <keyfile>
: RSA private key (PKCS#1, PEM format) for signing (required if-signed
).
Examples:
-
From Inline Expression:
lql compile -expr "$user.age >= 18 && $user.country == \"US\"" -out policy.lql
Outputs the compiled bytecode into
policy.lql
. -
From File:
lql compile -in rules.txt -out compiled.lql
Reads the expression from
rules.txt
. -
Signed:
lql compile -in rules.txt -out compiled.lqlx -signed -private private.pem
Signs the generated bytecode with
private.pem
.
Executes either a raw LQL expression or a compiled bytecode file. You can pass context data (as JSON or YAML) via stdin.
lql exec [OPTIONS]
Options:
-expr "<expression>"
: Evaluate an inline expression directly.-in <filename>
: Load a compiled bytecode file.-signed
: Indicates the bytecode is signed (only valid if-in
is used).-public <keyfile>
: RSA public key file (PKCS#1, PEM) to verify signed bytecode.-format=json|yaml
: How to parse the context data from stdin (default isyaml
).
Examples:
-
Raw Expression:
echo "{user: {age: 20, country: \"US\"}}" | lql exec -expr "$user.age >= 18 && $user.country == \"US\""
Reads context from stdin (YAML) and evaluates the expression.
-
Compiled Bytecode:
echo "{user: {age: 20, country: \"US\"}}" | lql exec -in policy.lql
Loads
policy.lql
for the DSL logic. -
Signed Bytecode:
echo "{user: {age: 20, country: \"US\"}}" | lql exec -in compiled.lqlx -signed -public public.pem
Verifies the signature via
public.pem
before execution.
The REPL (Read-Eval-Print Loop) subcommand lets you interactively evaluate an LQL expression against different context objects. The DSL expression is provided on the command line via -expr
, and context data can be supplied via stdin—either by piping a stream of JSON or YAML objects or by entering them interactively.
lql repl [OPTIONS]
Options:
-expr "<expression>"
(Required): The DSL expression to evaluate (e.g.,\$a + \$b
).
Examples:
-
Piped Input:
echo '{"a": 5, "b": 3}' | lql repl -expr "\$a + \$b"
Evaluates the JSON context, outputting
8
. -
Interactive Mode:
lql repl -expr "\$a + \$b"
Then, at the prompt, enter:
{"a": 10, "b": 15}
The REPL will output:
25
An empty line exits the REPL.
Validates a DSL expression by processing it through the lexer and parser. The expression can be provided via the -expr
flag or through a file using -in
(if both are provided, the file takes precedence). If the expression is valid, the command exits with code 0; otherwise, it prints the error and exits with code 1.
lql validate [OPTIONS]
Key options:
-expr "<expression>"
: Inline DSL expression to validate.-in <filename>
: File containing the DSL expression to validate.
Examples:
-
Validating an Inline Expression:
lql validate -expr "\$a + \$b"
If the expression is valid, it exits with code 0.
-
Validating Expression from a File:
lql validate -in expression.lql
Reads the expression from
expression.lql
, validates it, and prints the result.
Runs a series of DSL tests to quickly validate expressions against contexts. The test files contain small definitions specifying expressions, contexts, and expected outcomes (error or value). This command is mainly used for testing LQL itself.
lql test [OPTIONS]
Notable options:
--test-file=FILENAME
(default:testcases.yml
)--fail-fast
: Stop on the first test failure.--verbose
: Toggle verbose mode.--output=text|yaml
: Choose output format (default is text).--benchmark
: When enabled, each test expression is run 1,000 times and benchmark information (elapsed time and operations per second) is printed (only applicable for function call expressions).
Example:
lql test --test-file=example_tests.yml --fail-fast --output=text --benchmark
Parses an LQL expression to confirm validity, then prints out a colorized version (based on one of the available themes). This is useful for visually checking the expression’s structure.
lql highlight [OPTIONS]
Key options:
-expr "<expression>"
(required): Inline LQL expression to parse and highlight.-theme mild|vivid|dracula|solarized
: Which color theme to use (default is mild).
Examples:
-
Basic Highlight:
lql highlight -expr "$user.age >= 18 && $user.country == \"US\"" -theme dracula
Displays a colorized version using the Dracula theme.
-
No Theme Specified:
lql highlight -expr "(1 + 2) * 3"
Defaults to the mild palette.
Parses a DSL expression, runs a static analyzer on the resulting AST, and prints any warnings or errors found. This command is useful for detecting potential semantic issues before execution.
lql analyze [OPTIONS]
Options:
-expr "<expression>"
: Inline DSL expression to analyze.-in <filename>
: File containing the DSL expression to analyze.
Example:
lql analyze -expr "$user.age >= 18 && $user.country == \"US\""
If issues are detected, errors are printed in red and warnings in yellow. If there are no issues, it prints "No issues found."
Extracts and prints context identifiers (e.g. variable names starting with $
) from a DSL expression. This is useful when you want to know which context fields your expression depends on.
lql export-contexts [OPTIONS]
Options:
-expr "<expression>"
: Inline DSL expression to extract context identifiers from.-in <filename>
: File containing the DSL expression.
Example:
lql export-contexts -expr "$user.age >= 18 && $user.country == \"US\""
This command prints each context identifier (like user
) on a separate line.
If you wish to sign your compiled bytecode (-signed
) or verify it in lql exec
, you’ll need an RSA key pair in PKCS#1 format. Here’s how to generate it with OpenSSL:
# Generate a 2048-bit RSA private key (PKCS#1 format):
openssl genrsa -out private.pem -traditional 2048
# Extract the matching public key:
openssl rsa -in private.pem -RSAPublicKey_out -out public.pem
Use:
private.pem
when runninglql compile -signed -private private.pem
public.pem
when runninglql exec -signed -public public.pem
1) Testing:
lql test --test-file=example_tests.yml --fail-fast --output=text --benchmark
Runs all tests in example_tests.yml
, stopping on the first failure and printing benchmark info.
2) Compiling:
lql compile -expr "math.abs(-42)" -out abs_rule.lql
Creates abs_rule.lql
containing the bytecode for math.abs(-42)
.
3) Executing Raw Expression:
echo '{user:{name:"Alice"}}' | lql exec -expr "$user.name == 'Alice'"
Evaluates to true
if the context’s $user.name
is "Alice"
.
4) Executing Compiled Bytecode:
echo '{user:{age:17,country:"US"}}' | lql exec -in policy.lql
If policy.lql
expresses $user.age >= 18 && $user.country == "US"
, the output is false
for age=17.
5) Signed Bytecode:
echo '{order:{items:[{price:10},{price:20}]}}' | \
lql exec -in compiled.lqlx -signed -public public.pem
Checks the signature before evaluation.
6) REPL Mode:
-
Piped Input:
echo '{"a": 5, "b": 3}' | lql repl -expr "\$a + \$b"
Outputs
8
. -
Interactive:
lql repl -expr "\$a + \$b"
At the prompt, enter:
{"a": 10, "b": 15}
The REPL will output:
25
(An empty line exits the REPL.)
7) Validating an Expression:
-
Inline Expression:
lql validate -expr "\$a + \$b"
Exits with code 0 if valid.
-
From a File:
lql validate -in expression.lql
Reads, validates, and prints the expression result.
8) Analyzing an Expression:
lql analyze -expr "$user.age >= 18 && $user.country == \"US\""
Runs static analysis on the expression and prints warnings or errors if found.
9) Exporting Context Identifiers:
lql export-contexts -expr "$user.age >= 18 && $user.country == \"US\""
Prints out each context identifier (e.g. user
).
Below is a quick overview of the LQL language itself—its syntax, data types, and operators. If you are just using lql exec, you can embed these expressions in files or inline strings.
- Context References: Must start with
$
, e.g.,$user
,$order.items[0]
. - Operators:
- Arithmetic:
+
,-
,*
,/
- Relational:
<
,<=
,>
,>=
- Equality:
==
,!=
- Logical:
AND
,OR
,NOT
(or&&
,||
,!
)
- Arithmetic:
- Literals:
- Numbers (
123
,2.5e3
), strings ("hi"
,'hello'
), booleans (true
,false
),null
.
- Numbers (
- Comments: Lines starting with
#
.
-
int (64-bit)
- Examples:
42
,-100
. - No automatic float conversion.
- Examples:
-
float (64-bit)
- Examples:
3.14
,1e10
. - No automatic int conversion.
- Examples:
-
string
- Enclosed in single or double quotes, with escape sequences.
-
boolean
- Lowercase
true
orfalse
only.
- Lowercase
-
null
- Explicitly the literal
null
.
- Explicitly the literal
-
object
- JSON-like:
{ key: <expr>, "anotherKey": <expr> }
. - No duplicate keys, no trailing commas.
- JSON-like:
-
array
- Square-bracketed:
[expr1, expr2, ...]
. - No trailing commas or empty slots.
- Square-bracketed:
-
time
- Must be created/manipulated via the time library (e.g.,
time.parse(...)
).
- Must be created/manipulated via the time library (e.g.,
Operator | Description | Example |
---|---|---|
+ , - , * , / |
Arithmetic (numbers only) | ($a + $b) / 2 |
== , != |
Equality/inequality (num, str, bool) | $value != null |
< , <= , > , >= |
Relational (num or string) | $str < "Smith" |
AND , OR , NOT or && , ` |
, !` |
|
unary - |
Negation (numbers only) | -($score + 5) |
$order?.items?[0]?.price
- If
$order
or.items
is missing or not an array, yieldsnull
instead of erroring. - If
[0]
is out of range, yieldsnull
.
- Array:
[1, 2, (3+4)]
→[1, 2, 7]
. - Object:
{ name: "Alice", "home-city": "NYC" }
.
Below is an expanded Section 5 (originally Section 4 in older docs) that details every function from the DSL specification—complete with signatures, return types, error conditions, and example usages. You can replace your existing library reference with this updated version.
Time values in LQL are opaque objects that store date/time internally as a 64‑bit integer (epochMillis
since 1970‑01‑01T00:00:00Z).
Important: You cannot apply <
, >
, etc. directly to time values. Instead, use the time library functions.
- Signature:
time.now()
- Return Type: Time
- Arguments: none
- Errors:
- Generally none at the DSL level; system clock issues could trigger an implementation-specific error.
- Example:
time.now() # returns a Time object for the current system time
- Signature:
time.parse(string, string [, string])
- Return Type: Time
- Arguments:
inputString
: The date/time string (e.g.,"2025-01-01T12:00:00Z"
).format
: Format identifier—"iso8601"
,"dateOnly"
,"epochMillis"
,"rfc2822"
, or"custom"
.formatDetails
(optional): Required ifformat == "custom"
(e.g.,"yyyy-MM-dd HH:mm:ss"
).
- Errors:
- Runtime/Semantic Error if
inputString
does not match the format or if an unknown format is specified.
- Runtime/Semantic Error if
- Examples:
# Parse ISO8601 time.parse("2025-01-01T12:00:00Z", "iso8601") # Parse a date-only time.parse("2025-01-01", "dateOnly") # Parse epochMillis time.parse("1735732800000", "epochMillis") # Parse with a custom format time.parse("2025-02-13 10:00:00", "custom", "yyyy-MM-dd HH:mm:ss")
- Signature:
time.add(Time, numeric)
- Return Type: Time
- Arguments:
timeVal
: A valid Time object.durationMillis
: A numeric value (int or float) representing milliseconds to add.
- Errors:
- Runtime Error if
timeVal
is not Time ordurationMillis
is not numeric.
- Runtime Error if
- Example:
# Adds 24h (86400000 ms) to the given date time.add(time.parse("2025-01-01", "dateOnly"), 86400000)
- Signature:
time.subtract(Time, numeric)
- Return Type: Time
- Arguments: Same as
time.add
, except the duration is subtracted. - Errors:
- Runtime Error if
timeVal
is not Time ordurationMillis
is not numeric.
- Runtime Error if
- Example:
time.subtract(time.parse("2025-01-02", "dateOnly"), 86400000) # => Time corresponding to 2025-01-01
- Signature:
time.diff(Time, Time)
- Return Type: numeric (milliseconds)
- Arguments: Two valid Time objects.
- Errors:
- Runtime Error if either argument is not Time.
- Example:
time.diff( time.parse("2025-02-13", "dateOnly"), time.parse("2025-02-01", "dateOnly") ) # => 12 days * 86400000 = 1036800000
- Signature:
time.isBefore(Time, Time)
- Return Type: boolean
- Errors:
- Runtime Error if either argument is not Time.
- Example:
time.isBefore( time.parse("2025-01-01T00:00:00Z", "iso8601"), time.now() )
- Signature:
time.isAfter(Time, Time)
- Return Type: boolean
- Errors: Same as
time.isBefore
. - Example:
time.isAfter(time.parse("2025-12-31T23:59:59Z","iso8601"), time.now())
- Signature:
time.isEqual(Time, Time)
- Return Type: boolean
- Errors: Same as
time.isBefore
. - Example:
time.isEqual( time.parse("2025-01-01T00:00:00Z","iso8601"), time.parse("2025-01-01T00:00:00Z","iso8601") ) # => true
- Signature:
time.format(Time, string)
- Return Type: string
- Arguments:
timeVal
: A valid Time object.formatString
: A pattern specifying how to format the date/time (e.g."yyyy-MM-dd HH:mm"
).
- Errors:
- Runtime Error if
timeVal
is not Time orformatString
is invalid.
- Runtime Error if
- Example:
time.format( time.parse("2025-01-01T12:00:00Z","iso8601"), "yyyy/MM/dd HH:mm" ) # => "2025/01/01 12:00"
- Signature:
time.toEpochMillis(Time)
- Return Type: numeric (64-bit integer)
- Errors:
- Runtime Error if
timeVal
is not a Time object.
- Runtime Error if
- Example:
time.toEpochMillis(time.parse("2025-01-01", "dateOnly")) # => 1735689600000 (depending on the time zone used)
These functions extract components from a Time object. Implementations may add more (like time.getHour
, time.getMinute
, etc.).
- Signature:
time.getYear(Time) -> numeric time.getMonth(Time) -> numeric time.getDay(Time) -> numeric
- Errors:
- Runtime Error if argument is not Time.
- Example:
time.getYear(time.parse("2025-01-01", "dateOnly")) # => 2025
- Signature:
time.startOfDay(Time) -> Time time.endOfDay(Time) -> Time
- Errors:
- Runtime Error if argument is not Time.
- Example:
time.startOfDay(time.parse("2025-01-01T12:34:56Z","iso8601")) # => Time object for 2025-01-01T00:00:00Z
- Signature:
time.withZone(Time, string) -> Time
- Errors:
- Runtime Error if
timeVal
is not Time orzoneName
is invalid.
- Runtime Error if
- Behavior:
Returns a new Time object representing the same instant in the specified time zone. (Internally,epochMillis
usually doesn’t change; formatting does.) - Example:
time.withZone(time.parse("2025-01-01T12:00:00Z","iso8601"), "America/New_York")
All math functions operate on numeric (int or float) arguments. Using them on non-numeric types raises a Runtime Error.
- Signature:
math.abs(numeric)
- Return Type: numeric
- Example:
math.abs(-42) # => 42
- Signature:
math.floor(numeric)
- Return Type: numeric
- Behavior: Rounds down to the nearest integer.
- Example:
math.floor(3.9) # => 3
- Signature:
math.ceil(numeric)
- Return Type: numeric
- Behavior: Rounds up to the nearest integer.
- Example:
math.ceil(3.1) # => 4
- Signature:
math.round(numeric)
- Return Type: numeric
- Behavior: Rounds to the nearest integer (details of half-up vs. half-to-even are implementation-specific).
- Example:
math.round(3.5) # => 4 (assuming half-up)
- Signature:
math.sqrt(numeric)
- Return Type: numeric
- Errors:
- Runtime Error if
x < 0
(unless imaginary is supported).
- Runtime Error if
- Example:
math.sqrt(16) # => 4
- Signature:
math.pow(numeric, numeric)
- Return Type: numeric
- Example:
math.pow(2, 3) # => 8
- Signature:
math.sum(array [, string, numeric])
- Return Type: numeric
- Behavior:
- If
subfield
is given, each element ofarr
is assumed to be an object, and the function sumselement[subfield]
. - If
defaultVal
is given, that is used when a subfield is missing or if the array is empty.
- If
- Errors:
- Runtime Error if
arr
is not an array or if elements are not numeric (and no defaultVal).
- Runtime Error if
- Example:
# Summation of array elements math.sum([1, 2, 3]) # => 6 # Summation of subfield "price" in an array of objects math.sum($order.items, "price", 0)
- Signature:
math.min(array [, string, numeric])
- Return Type: numeric
- Behavior: Similar to
math.sum
, but returns the minimum value. - Example:
math.min([5,2,7]) # => 2
- Signature:
math.max(array [, string, numeric])
- Return Type: numeric
- Behavior: Similar to
math.min
, but returns the maximum value. - Example:
math.max([5,2,7]) # => 7
- Signature:
math.avg(array [, string, numeric])
- Return Type: numeric (float or int, depending on implementation)
- Behavior: Computes average of numeric elements (or
subfield
values). - Example:
math.avg([5,5,5]) # => 5
All string functions require string arguments unless otherwise specified. Non-string arguments produce Runtime Errors.
- Signature:
string.toLower(string)
- Return Type: string
- Example:
string.toLower("HELLO") # => "hello"
- Signature:
string.toUpper(string)
- Return Type: string
- Example:
string.toUpper("Hello") # => "HELLO"
- Signature:
string.trim(string)
- Return Type: string
- Behavior: Removes leading and trailing whitespace.
- Example:
string.trim(" hello ") # => "hello"
- Signature:
string.startsWith(string, string) -> boolean
- Example:
string.startsWith("Hello World", "Hell") # => true
- Signature:
string.endsWith(string, string) -> boolean
- Example:
string.endsWith("Hello World", "World") # => true
- Signature:
string.contains(string, string) -> boolean
- Example:
string.contains("Hello World", "lo Wo") # => true
- Signature:
string.replace(string, string, string [, numeric]) -> string
- Behavior: Replaces all occurrences of
old
withnew
by default. Iflimit
is provided, replace up to that many occurrences. - Example:
string.replace("abc-123-xyz", "-", ":") # => "abc:123:xyz"
- Signature:
string.split(string, string) -> array of strings
- Example:
string.split("one,two,three", ",") # => ["one", "two", "three"]
- Signature:
string.join(array, string) -> string
- Behavior: Joins elements of
arrOfStrings
(which must be an array of strings) into a single string, separated bysep
. - Example:
string.join(["one","two","three"], ",") # => "one,two,three"
- Signature:
string.substring(string, numeric, numeric) -> string
- Behavior: Returns a substring from index
start
ofs
with the specifiedlength
. - Example:
string.substring("Hello", 1, 3) # => "ell"
- Signature:
string.indexOf(string, string [, numeric]) -> numeric
- Behavior: Returns the zero-based index of the first occurrence of
sub
ins
at or afterfromIndex
. If not found, returns-1
. - Example:
string.indexOf("banana", "ana") # => 1 string.indexOf("banana", "ana", 2)# => 3
- Signature:
string.concat(string, string, ...)
- Return Type: string
- Behavior: Concatenates multiple strings in order.
- Example:
string.concat("Hello", " ", "World") # => "Hello World"
For advanced pattern matching. Unlike string.replace
, these take regex patterns that may include anchors, groups, etc.
- Signature:
regex.match(string, string) -> boolean
- Behavior: Returns
true
if any substring ofs
matchespattern
, orfalse
otherwise. - Example:
regex.match("^[A-Z]{3}-\\d+$", "ABC-123") # => true
- Signature:
regex.replace(string, string, string) -> string
- Behavior: Replaces all occurrences of
pattern
ins
withreplacement
. Supports capturing groups inreplacement
(e.g.$1
) depending on implementation. - Example:
regex.replace("abc-123", "(\\d+)", "[$1]") # => "abc-[123]"
- Signature:
regex.find(string, string) -> string
- Behavior: Returns the first substring of
s
that matchespattern
. If none, returns""
. - Example:
regex.find("\\d+", "abc-123-xyz") # => "123"
All functions here require the first argument to be an array. Mismatched types raise Runtime Errors.
- Signature:
array.contains(array, any) -> boolean
- Example:
array.contains([100, 200, 300], 200) # => true
- Signature:
array.find(array, string, any [, anyObject])
- Return Type: object (or
defaultObj
if not found) - Behavior: Searches
arr
(an array of objects) for an element whosesubfield
equalsmatchVal
.- If found, returns the first matching object.
- If not found, returns
defaultObj
if provided; otherwise raises a runtime error.
- Example:
array.find($users, "id", 42, {name: "Unknown"}) # Returns the object in $users whose .id == 42, or {name:"Unknown"}
- Signatures:
array.first(array [, any]) -> any array.last(array [, any]) -> any
- Behavior:
- Returns the first (or last) element of
arr
. - If
arr
is empty, returnsdefaultVal
if provided, else raises an error.
- Returns the first (or last) element of
- Example:
array.first([1,2,3]) # => 1 array.last([1,2,3]) # => 3
- Signature:
array.extract(array, string [, any]) -> array
- Behavior:
- Maps each element of
arr
(an array of objects) to that element’ssubfield
. - If an element lacks
subfield
, usesdefaultVal
if provided, else raises an error.
- Maps each element of
- Example:
# Suppose $order.items = [ {price:10}, {price:20}, {price:15} ] array.extract($order.items, "price") # => [10, 20, 15]
- Signature:
array.sort(array [, boolean]) -> array
- Behavior:
- Sorts
arr
in ascending order by default, or descending ifascending == false
. - Elements must be comparable (all numeric or all strings).
- Sorts
- Example:
array.sort([3,1,2]) # => [1,2,3] array.sort([3,1,2], false)# => [3,2,1]
- Signature:
array.flatten(array) -> array
- Behavior:
- Flattens one level of sub-arrays into a single array.
- Example:
array.flatten([1, [2,3], [4], 5]) # => [1,2,3,4,5]
-
Signature:
array.filter(array [, string [, any]])
-
Return Type: array
-
Potential Errors:
- Runtime Error if
collection
is not an array. - Runtime Error if
subfield
is provided but not a string.
- Runtime Error if
-
Behavior:
- No
subfield
argument: Returns a new array containing all elements ofcollection
that are notnull
. subfield
provided, but nomatchVal
: Returns a new array of elements whosesubfield
is present and notnull
.subfield
andmatchVal
both provided: Returns a new array of elements whosesubfield
strictly equalsmatchVal
.
- No
These functions help with conditional logic or presence checks.
- Signature:
cond.ifExpr(boolean, any, any) -> any
- Behavior:
- If
condition
istrue
, returnsthenVal
; else returnselseVal
.
- If
- Example:
cond.ifExpr($user.age >= 18, "allowed", "blocked")
- Signature:
cond.coalesce(any, any, ...) -> the first non‑null
- Behavior:
- Returns the first non‑null among
expr1, expr2, ...
. - If all are
null
, raises a runtime error.
- Returns the first non‑null among
- Example:
cond.coalesce($user.middleName, "N/A") # returns "N/A" if $user.middleName is null or missing
- Signature:
cond.isFieldPresent(object, string) -> boolean
- Behavior:
- Returns
true
ifobject
has the specifiedfieldPath
(direct key) present (even if it’snull
), else false.
- Returns
- Example:
cond.isFieldPresent($user, "country")
Used for type checks (predicates) and explicit conversions (no implicit conversions occur in LQL).
type.isNumber(x)
type.isString(x)
type.isBoolean(x)
type.isArray(x)
type.isObject(x)
type.isNull(x)
Each returns true
or false
depending on whether x
is of that type.
Example:
type.isNumber($val) # => true or false
-
type.string(x)
- Converts
x
to a string. - If
x
isnull
, returns""
. - If
x
is numeric, returns its decimal string representation. - If
x
is a Time, raises an error unless your implementation chooses to define a special string representation. - Example:
type.string(123) # => "123" type.string(null) # => ""
- Converts
-
type.int(x)
- Converts
x
to a 64‑bit integer. - If
x
isnull
, returns0
. - If
x
is a float, it truncates (implementation-specific). - If
x
is a string, it must be a valid integer literal, else error. - Example:
type.int("42") # => 42 type.int(null) # => 0
- Converts
-
type.float(x)
- Converts
x
to a 64‑bit float. - If
x
isnull
, returns0.0
. - If
x
is an int, it becomes float. - If
x
is a string, it must parse correctly, else error. - Example:
type.float(3) # => 3.0 type.float("3.14") # => 3.14
- Converts
-
type.boolean(x)
- Converts
x
to a boolean. - If
x
isnull
, an empty string (""
), the string"0"
, the number0
, or the booleanfalse
, returnsfalse
. - If
x
is the string"1"
, the number1
, or the booleantrue
, returnstrue
. - For any other value, returns
false
and raises an error indicating that the value cannot be converted to a boolean. - Example:
type.boolean("1") # => true type.boolean(1) # => true type.boolean(true) # => true type.boolean("0") # => false type.boolean("") # => false type.boolean(0) # => false type.boolean("yes") # => error: "type.bool: 'yes' cannot be converted to bool"
- Converts
LQL reports errors in four main categories:
- Lexical Errors (e.g., unclosed string, malformed numeric).
- Syntax Errors (e.g., misplaced operators, mismatched parentheses).
- Semantic Errors (e.g.,
+
on non‑numeric,<
on booleans). - Runtime Errors (e.g., missing fields without optional chaining, out-of-bounds array indexes).
Examples:
LexicalError: Unclosed string literal at line 1, column 16
SyntaxError: Bare identifier 'True' is not allowed at line 1, column 16
SemanticError: '+' operator used on non-numeric type at line 1, column 5
RuntimeError: field 'user' not found at line 1, column 1
While LQL allows you to reference context fields directly (e.g. $user.age
), doing so is unsafe because the field’s type is not explicitly verified. This may lead to unpredictable behavior or runtime errors if the actual data type does not match what your expression expects. It is strongly recommended to wrap context fields using explicit conversion functions to enforce type safety.
-
Avoid Ambiguity:
A bare context reference (e.g.$user.age
) has an “unknown” type at analysis time. Without an explicit conversion, it’s unclear whether the value is numeric, string, or another type. This ambiguity increases the risk of type mismatches in operations such as arithmetic, comparisons, and function calls. -
Prevent Runtime Errors:
Operations like arithmetic or relational comparisons require operands of a specific type. If a context field is used without conversion and its type is not what is expected (for example, a string value in an arithmetic operation), the DSL may produce errors at runtime. -
Improve Code Clarity:
Explicitly wrapping context fields documents your intention. It makes it clear what type you expect, easing maintenance and reducing bugs when the data source changes.
When your DSL expression uses a context field, wrap it with the appropriate type conversion function:
-
Numeric Values:
Usetype.int()
for integers ortype.float()
for floating‑point numbers.
Example:type.int($user.age) >= 18
Instead of:
$user.age >= 18
Explanation: This explicitly converts
$user.age
to a 64‑bit integer. If the field is not a valid integer, a clear error is raised. -
String Values:
Usetype.string()
to ensure a context field is treated as a string.
Example:string.concat("Hello, ", type.string($user.firstName))
Instead of:
string.concat("Hello, ", $user.firstName)
Explanation: This guarantees that the first name is converted to a string before concatenation.
-
Boolean Values:
Usetype.boolean()
when a context field is expected to be boolean.
Example:cond.ifExpr(type.boolean($user.isActive), "allowed", "blocked")
Instead of:
cond.ifExpr($user.isActive, "allowed", "blocked")
Explanation: This explicitly converts
$user.isActive
to a boolean, ensuring correct evaluation. -
Arrays and Objects:
While less common, you may also need to wrap context fields when they are expected to be arrays or objects usingtype.intArray()
,type.stringArray()
, etc., to enforce uniformity within collection elements.
-
Arithmetic Comparison Without Conversion (Unsafe):
$user.age >= 18
Risk: If
$user.age
is not numeric (for example, if it’s a string or missing), the expression may produce a type mismatch or runtime error. -
Arithmetic Comparison With Conversion (Safe):
type.int($user.age) >= 18
Benefit: This ensures that
$user.age
is explicitly converted to an integer. If the conversion fails (e.g. the value is not a valid number), a meaningful error is raised. -
Function Call on a Context Field Without Conversion (Unsafe):
math.abs($user.balance)
Risk: If
$user.balance
is not numeric,math.abs
may fail or produce an incorrect result. -
Function Call With Conversion (Safe):
math.abs(type.float($user.balance))
Benefit: This explicitly converts
$user.balance
to a float, ensuring thatmath.abs
operates on a proper numeric value. -
Logical Expression Using a Context Field (Unsafe):
cond.ifExpr($user.isActive, "enabled", "disabled")
Risk: Without conversion, the type of
$user.isActive
is ambiguous. An unexpected type (e.g. string"true"
) could lead to an error. -
Logical Expression With Conversion (Safe):
cond.ifExpr(type.boolean($user.isActive), "enabled", "disabled")
Benefit: This ensures that
$user.isActive
is interpreted as a boolean, avoiding potential type conflicts.
LQL is not only available as a CLI tool but also as a WebAssembly (WASM) module that can be run directly in the browser. This version allows you to experiment with the LQL DSL through a convenient web interface. The WASM version provides functionality for all major commands, including:
- Compile: Generate bytecode from an LQL expression.
- Exec: Execute an expression or a compiled bytecode with provided context.
- Validate: Check the correctness of an LQL expression.
- Highlight: Get a colorized version of the expression for easier reading.
- Export Contexts: Extract context identifiers from an expression.
- Create, Eval, and Delete AST: Create, evaluate and delete abstract syntax trees in memory for re-use.
The WASM version is delivered as a compiled WebAssembly module (lql.wasm
), accompanied by a JavaScript shim (wasm_exec.js
) provided by Go. A sample HTML interface (see below) demonstrates how the WASM module can be integrated into a web page to offer interactive testing of LQL commands.
The WASM module is loaded using the Go-provided wasm_exec.js
. Once loaded, the Go runtime initializes the LQL functions and exposes them as JavaScript commands (such as compileCmd
, execCmd
, etc.) which are then used by the HTML interface.
For now, you must download a wasm release from https://github.com/SpecDrivenDesign/lql-go/releases and include it manually in your project. No CDN versions are available at this moment.
Example snippet from index.html
:
<script src="wasm_exec.js"></script>
<script>
// Create a new Go instance.
const go = new Go();
// Polyfill for browsers that don't support instantiateStreaming.
if (!WebAssembly.instantiateStreaming) {
WebAssembly.instantiateStreaming = async (resp, importObject) => {
const source = await (await resp).arrayBuffer();
return await WebAssembly.instantiate(source, importObject);
};
}
// Load the WebAssembly module.
WebAssembly.instantiateStreaming(fetch("lql.wasm"), go.importObject)
.then((result) => {
go.run(result.instance);
document.getElementById("status").textContent = "WebAssembly Loaded.";
document.getElementById("interface").style.display = "block";
})
.catch((err) => {
document.getElementById("status").textContent = "Failed to load WebAssembly: " + err;
});
</script>