Files
Decodal/doc/manual/source/language/operators.md
T

7.4 KiB

Operators

This chapter defines the meaning of Decodal's operators.

Operator reference

Operator Form Kind Operands Result / meaning
. object.field field reference object / abstract object field value
call fn(arg) function call function function result
! !expr unary logical concrete Bool concrete Bool
- -expr unary arithmetic concrete Int / Float negated number
* lhs * rhs arithmetic concrete Int / Float numeric product
/ lhs / rhs arithmetic concrete Int / Float Float quotient
+ lhs + rhs arithmetic concrete Int / Float numeric sum
- lhs - rhs arithmetic concrete Int / Float numeric difference
++ lhs ++ rhs array concat concrete arrays concatenated array
== lhs == rhs equality concrete scalar concrete Bool
!= lhs != rhs equality concrete scalar concrete Bool
< lhs < rhs ordering concrete Int / Float concrete Bool
<= lhs <= rhs ordering concrete Int / Float concrete Bool
> lhs > rhs ordering concrete Int / Float concrete Bool
>= lhs >= rhs ordering concrete Int / Float concrete Bool
> > value comparison constraint numeric constraint value abstract constraint
>= >= value comparison constraint numeric constraint value abstract constraint
< < value comparison constraint numeric constraint value abstract constraint
<= <= value comparison constraint numeric constraint value abstract constraint
&& lhs && rhs logical concrete Bool short-circuit AND
` ` `lhs
& lhs & rhs composition value / constraint / object constraint-preserving composition
// lhs // rhs patch object / value right-biased structural patch
default base default fallback default abstract value materialization fallback
as narrower as wider range refinement value / constraint / structure narrower result plus untouched right-only ranges

A concrete scalar is a String, Bool, Int, or Float value.

Precedence

Precedence is highest first:

  1. Function calls and field references
  2. Unary ! and -
  3. * and /
  4. + and -
  5. ++
  6. ==, !=, <, <=, >, and >=
  7. &&
  8. ||
  9. &
  10. //
  11. default
  12. as

Binary operators at the same precedence are left-associative. default is right-associative.

Arithmetic operators

+, -, *, and / perform arithmetic on concrete Int and Float values. See Arithmetic Expression for details.

Array concatenation operator

++ concatenates two concrete arrays. It does not transform the elements; elements from the right side follow elements from the left.

["read", "write"] ++ ["admin"]

Logical and comparison operators

!, &&, and || operate on concrete Bool values. && and || short-circuit.

== and != compare concrete scalar values. <, <=, >, and >= compare concrete numeric values. See Logical and Comparison Expressions for details.

&: constraint-preserving composition

& composes values, constraints, and structures.

A & B

The basic rules are:

  • Two constraints become a constraint that satisfies both.
  • A constraint and a concrete value require the concrete value to satisfy the constraint.
  • Two identical concrete values produce that value.
  • Two different concrete values conflict.
  • Two objects are composed field by field.
  • A field present on both sides is composed with &.
  • A field present on only one side is retained unchanged.
  • Any contradiction produces an error.

For example:

Port = Int & >= 1 & <= 65535;
NarrowedPort = Port & > 443;

MyConfig = {
    port = NarrowedPort default 8080;
};

Config = MyConfig & {
    port = 8000;
};

Conceptually, Config.port becomes:

NarrowedPort & 8000

This succeeds because 8000 satisfies NarrowedPort. The following fails:

BadConfig = MyConfig & {
    port = 80;
};

80 does not satisfy > 443.

//: patch composition

// is a right-biased structural patch operator. Where & preserves constraints, // changes or overrides configuration and schemas.

A // B

The basic rules are:

  • Two objects are patched recursively field by field.
  • The same field containing object/object is patched recursively.
  • The same field containing anything other than object/object is replaced by the right side.
  • Fields present only on the left are retained.
  • Fields present only on the right are added.
  • Arrays are replaced by the right side by default.
  • Function values are replaced by the right side by default.

Thus, // is a deep patch rather than a shallow merge.

Base = {
    feature_hoge = {
        enable = Bool default true;
        fuga = Int default 10;
    };
};

Patched = Base // {
    feature_hoge = {
        enable = false;
    };
};

Patched is equivalent to:

{
    feature_hoge = {
        enable = false;
        fuga = Int default 10;
    };
}

The same patch can be written with a dot path:

Patched = Base // {
    feature_hoge.enable = false;
};

Replacing an entire object

Under //, object/object is always patched deeply. The core language does not include a special replace(...) syntax or built-in function for replacing an entire object field.

To use an entirely different object structure, construct the value outside the object being patched.

as: range refinement

as is an asymmetric refinement operator that verifies the left side is more concrete and narrower than the right side.

Refined = {
    port = 8000;
} as {
    port = Int & >= 1 & <= 65535;
    host = String;
    enabled = Bool default true;
};

port becomes the concrete value 8000. host and enabled, which exist only on the right, remain abstract in the result. Whether a field has a default does not affect preservation of a right-only field. as does not select defaults; normal default rules apply only if the result is later materialized.

A field present only in the left object lies outside the right-hand field domain and produces an error. Nested objects, array constraints, and map constraints present on both sides are refined recursively using the same rules.

Containment can also be checked between abstract ranges.

Int & > 10 as Int & > 0 # succeeds and returns Int & > 10
Int as Int & > 0        # fails

& is symmetric and retains object fields found on only one side. Do not use it in place of directional refinement. See Range Refinement for details.

Choosing between &, //, and as

Use & for symmetric composition that preserves constraints and partial structure.

Combined = MyConfig & {
    port = 8000;
};

Use // to override or reshape an existing structure.

ModifiedSchema = MyConfig // {
    port = Int default 9000;
};

Because // is a right-biased patch, it does not always preserve constraints from the left. Use & when those constraints must remain.

Use as to compose while verifying that the left side is narrower than the right.

RefinedConfig = NarrowConfig as WideConfig;