The data templating language
Jsonnet
\[ \newcommand{\array}[1]{[ #1 ]} \newcommand{\assign}[2]{ #1\texttt{ = }#2} \newcommand{\binary}[3]{ #1\texttt{ }#2\texttt{ }#3} \newcommand{\error}[1]{\texttt{error }#1} \newcommand{\false}{\texttt{false}} \newcommand{\function}[2]{\texttt{function(}#1\texttt{)} #2} \newcommand{\if}[3]{\texttt{if }#1\texttt{ then }#2\texttt{ else }#3} \newcommand{\import}[1]{\texttt{import }#1} \newcommand{\importstr}[1]{\texttt{importstr }#1} \newcommand{\index}[2]{#1\texttt{[}#2\texttt{]}} \newcommand{\local}[2]{\texttt{local }#1\texttt{ ; }#2} \newcommand{\null}{\texttt{null}} \newcommand{\object}[1]{\{ #1 \}} \newcommand{\ocomp}[4]{\object{[#1]:#2\texttt{ for }#3\texttt{ in }#4}} \newcommand{\self}{\texttt{self}} \newcommand{\super}{\texttt{super}} \newcommand{\true}{\texttt{true}} \newcommand{\unary}[2]{\texttt{ }#1\texttt{ }#2} \newcommand{\rule}[3]{\frac{#2}{#3}\textrm{(#1)}} \]

Specification

This page is the authority on what Jsonnet programs should do. It defines what input is / is not a syntactically valid Jsonnet program, and precisely how they are parsed. It describes which programs should be rejected statically (i.e. before execution). Finally, it specifies the manner in which the program is executed, i.e. the JSON that is output, or the dynamic error if there is one.

The specification is intended be terse but precise. The intention is to illuminate various subtleties and edge cases in order to allow fully-compatible reimplementations of the language, refactoring tools, etc. The specification employs some standard theoretical computer science techniques, namely type systems and big step operational semantics. If that's not your cup of tea, then see the more discussive description of Jsonnet behavior in tutorial.

Abstract Syntax

In this notation, x★ defines a comma-separated possibly zero-length list of x (either a terminal or non-terminal) with an optional comma after the last element. The regular * and + have their usual meaning, i.e. repetition without commas.

expr ∈ Expr::= 'null' | 'true' | 'false' | 'self' | 'super' | '$' | string | number
| '{' ('local' bind | field)★ '}'
| '{' ('local' bind ',')* '[' expr ']' ':' expr 'for' id 'in' expr '}'
| '[' expr★ ']'
| '[' expr 'for' id 'in' expr [ 'if' expr ] ']'
| expr '.' id
| expr '[' expr ']'
| expr '(' expr★ ')'
| id
| 'local' bind [ ',' bind ]* ';' expr
| 'if' expr 'then' expr
| 'if' expr 'then' expr 'else' expr
| expr symbol expr
| symbol expr
| expr '{' field★ '}'
| 'function' '(' id★ ')' expr
| 'import' string
| 'importstr' string
| 'error' expr
field::= fieldname ':' [ ':' ] e
| fieldname '(' id★ ')' ':' [ ':' ] e
fieldname::= id | string | '[' e ']'
bind::= id '=' e
| id '(' id★ ')' '=' e
symbol::= ( '+' | '-' | '*' | '/' | '%' | '&' | '|' | '^' | '=' | '<' | '>' )+

Additionally, id is defined by regular expression: [a-zA-Z_][a-zA-Z0-9_]*. The definition of string is equivalent to the JSON string, including escape characters. Finally, number is equivalent to the JSON number, but without the leading -.

Associativity and Operator Precedence

The parsing of the concrete syntax into abstract syntax can be controlled by adding parentheses in order to resolve ambiguities explicitly. If parentheses are not given, the ambiguity is resolved according to the following rules:

Everything is left associative. In the case of local, if, function, and error, ambiguity is resolved by consuming as many tokens as possible on the right hand side. For example the parentheses are redundant in local x = 1; (x + x). All remaining ambiguities are resolved according to the following order of precedence:

  1. f() arr[e] obj[e] obj.f   (application and indexing)
  2. + - ! ~   (the unary operators)
  3. * / %   (these, and the remainder below, are binary operators)
  4. + -
  5. << >>
  6. < > <= >=
  7. == !=
  8. &
  9. ^
  10. |
  11. &&
  12. ||

While the abstract syntax reserves arbitrary sequences of symbol characters for future use, only the operators in the above list are currently parsed.

Core Language Subset

In order to build a simpler model of Jsonnet's behavior, many of the language features are represented as syntax sugar. Below, the core syntax is defined, and the desugaring function. Both the static checking rules and the operational semantics are defined at the level of the core language, so it is reasonable to implement the desugaring in the parser.

Core Syntax

e ∈ Core::= 'null' | 'true' | 'false' | 'self' | 'super' | string | number
| '{' ( '[' e ']' ':' [ ':' ] e )★ '}'
| '{' '[' e ']' ':' e 'for' id 'in' e '}'
| '[' e★ ']'
| e '[' e ']'
| e '(' e★ ')'
| id
| 'local' id '=' e [ ',' id '=' e ]* ';' e
| 'if' e 'then' e 'else' e
| e symbol e
| symbol e
| 'function' '(' id★ ')' e
| 'error' e

In the core language, the set of identifiers now includes $, which is no-longer a special keyword.

Additionally, the != operator is desugared to !(a == b) and the unary + is stripped (it is a no-op).

Also removed in the core language are import and importstr. The semantics of these constructs is that they are replaced with either the contents of the file, or an error construct if importing failed (e.g. due to I/O errors). In the first case, the file is parsed, desugared, and subject to static checking before it can be substituted. In the latter case, the file is substituted in the form of a string, so it merely needs to contain valid UTF-8.

A given Jsonnet file can be recursively imported via import. Thus, the implementation loads files lazily (i.e. during execution) as opposed to via static desugaring. The imported Jsonnet file is parsed and statically checked in isolation. Therefore, the behavior of the import is not affected by the environment into which it is imported. The files are cached by filename, so that even if the file changes on disk during Jsonnet execution, referential transparency is maintained.

Desugaring

Desugaring is defined as the function desugar: (Expr × Boolean) → Core. The second parameter of the function tracks whether we are within an object.

desugar( { field₁, ... fieldₙ, binds }, b) = where binds = local bind₁, ... local bindₙ
let binds' = desugarbinds(binds, true)
let binds'' = { binds if b = true,
binds, local $ = self otherwise
in { desugarfield(field₁, binds'', b), ... desugarfield(fieldₙ, binds'', b)}
desugar({ binds, [ expr ] : expr' for id in expr'' }, b) = where binds = local bind₁, ... local bindₙ
let binds' = desugarbinds(binds, true)
let binds'' = { binds if b = true,
binds, local $ = self otherwise
let expr''' = local binds''; desugar(expr', true)
{ [ desugar(expr, b) ] : expr''' for id in desugar(expr'', b) }
desugar([ expr for id in expr' ], b) = std.map(function(id)desugar(expr', b), desugar(expr, b))
desugar([ expr for id in expr' if expr'' ], b) = where ffunc = function(id)desugar(expr'', b)
where mfunc = function(id)desugar(expr, b)
where arr = desugar(expr', b)
std.filterMap(ffunc, mfunc, arr)
desugar(expr . id, b) = desugar(expr, b)["id"]
desugar(if e₁ then e₂, b) = if desugar(e₁, b) then desugar(e₂, b) else null
desugar(expr { ... }, b) = desugar(expr + { ... }, b)
Other cases invoke structural recursion.

desugarfield(fname :: expr, binds, b) = desugarfname(fname, b) :: local binds ; desugar(expr, true)
desugarfield(fname : expr, binds, b) = desugarfname(fname, b) : local binds ; desugar(expr, true)
desugarfield(fname ( params ):: expr, b) = desugarfname(fname, b) :: local binds ; function ( params ) desugar(expr, true)
desugarfield(fname ( params ): expr, b) = desugarfname(fname, b) : local binds ; function ( params ) desugar(expr, true)

desugarfname(id, b) = ["id"]
desugarfname(string, b) = [string]
desugarfname([expr], b) = [desugar(expr, b)]

desugarbinds(local id = expr, b) = local id = desugar(expr, b)

Static Checking

After the Jsonnet program is parsed and desugared, a syntax-directed algorithm is employed to reject programs that contain certain classes of errors. This is presented like a static type system, except that there are no static types. Programs are only rejected if they use undefined variables, or if self, super or $ are used outside the bounds of an object. In the core language, $ has been desugared to a variable, so its checking is implicit in the checking of bound variables.

The static checking is described below as a judgement \(Γ ⊢ e\), where \(Γ\) is the set of variables in scope of \(e\). The set \(Γ\) initially contains only std, the implicit standard library. In the case of imported files, each jsonnet file is checked independently of the other files.

\[\rule{chk-lit} { } { \_ ⊢ \null, \true, \false, string, number } \]
\[\rule{chk-self} { \self ∈ Γ } { Γ ⊢ \self } \]
\[\rule{chk-super} { \super ∈ Γ } { Γ ⊢ \super } \]
\[\rule{chk-object} { Γ ⊢ e_1 \ldots e_n \\ Γ ∪ \{\self,\super\} ⊢ e'_1 \ldots e'_n \\ ∀ i,j: e_i ∈ string ∧ e_j = e_i ⇒ i = j } { Γ ⊢ \object{[e_1]:e'_1 \ldots [e_m]:e'_m, [e_{m+1}]::e'_{m+1} \ldots [e_n]::e'_n } } \]
\[\rule{chk-object-comp} { Γ ∪ \{x\} ⊢ e_1 \\ Γ ∪ \{x,\self,\super\} ⊢ e_2 \\ Γ ⊢ e_3 } { Γ ⊢ \ocomp{e_1}{e_2}{x}{e_3} } \]
\[\rule{chk-array} { Γ ⊢ e_1 \ldots e_n } { Γ ⊢ \array{e_1, \ldots e_n} } \]
\[\rule{chk-array-index} { Γ ⊢ e \\ Γ ⊢ e' } { Γ ⊢ e[e'] } \]
\[\rule{chk-apply} { Γ ⊢ e \\ ∀ i∈\{1\ldots n\}: Γ ⊢ e_i } { Γ ⊢ e(e_1\ldots e_n) } \]
\[\rule{chk-var} { x ∈ Γ } { Γ ⊢ x } \]
\[\rule{chk-local} { Γ ∪ \{x_1 \ldots x_n\} ⊢ e_1 \ldots e_n, e \\ ∀ i,j: x_i = x_j ⇒ i = j } { Γ ⊢ \local{\assign{x_1}{e_1} \ldots \assign{x_n}{e_n}}e } \]
\[\rule{chk-if} { Γ ⊢ e_1, e_2, e_3 } { Γ ⊢ \if{e_1}{e_2}{e_3} } \]
\[\rule{chk-binary} { Γ ⊢ e_L, e_R } { Γ ⊢ \binary{e_L}{sym}{e_R} } \]
\[\rule{chk-unary} { Γ ⊢ e } { Γ ⊢ \unary{sym}{e} } \]
\[\rule{chk-function} { Γ ∪ \{x_1 \ldots x_n\} ⊢ e \\ ∀ i,j: x_i = x_j ⇒ i = j } { Γ ⊢ \function{x_1\ldots x_n}{e} } \]
\[\rule{chk-import} { } { Γ ⊢ \import{s} } \]
\[\rule{chk-importstr} { } { Γ ⊢ \importstr{s} } \]
\[\rule{chk-error} { Γ ⊢ e } { Γ ⊢ \error{e} } \]

Operational Semantics

We present two sets of operational semantics rules. The first defines the judgement \(e ↓ v\) which represents the execution of Jsonnet expressions into Jsonnet values. The other defines the judgement \(v ⇓ j\) which represents manifestation, the process by which Jsonnet values are converted into JSON values.

We model both explicit runtime errors (raised by the error construct) and implicit runtime errors (e.g. array bounds errors) as stuck execution. Errors can occur both in the \(e ↓ v\) judgement and in the \(v ⇓ j\) judgement (because it is defined in terms of \(e ↓ v\)).

Jsonnet Values

Jsonnet values are yielded by executing Jsonnet expressions. These are not the same as JSON values, although there is some overlap. In particular, they contain functions which are not representable in JSON, and both object fields and array elements have yet to be executed to yield values.

vValue= { null, true, false } ∪ StringDoubleObjectFunctionArray
oObject= String ⇀ (Hidden × Core)
Hidden= { true, false }
Function::= function ( id* ) e
aArray::= [ e₀ ... eₙ ]

Objects are represented as finite partial functions, hence the use of the harpoon notation ⇀. If an object does not define a particular field, the function is undefined. Otherwise, the field is bound to a tuple containing a boolean indicating whether or not the field is hidden, and an expression to be evaluated when the field is accessed.

To construct a partial function, we use a bounded lambda notation, e.g. the object {a: 5, b: 5} would be represented by λx∈{"a", "b"}. (false, 5). In order to allow substituting objects into expressions e, we use the following function to convert:

\[\begin{array}{l} exp(o) = \object{[e_1]:o(f_1) \ldots [e_m]:o(f_m), [e_{m+1}]::f_{m+1} \ldots [e_n]::e'_n } \\ \textrm{where }\\ \hspace{20pt}\{(f_1,e_1) \ldots (f_m,e_m)\} = \{ (f,e) | f ∈ dom(o), o(f) = (\false, e) \} \\ \hspace{20pt}\{(f_{m+1}, f_{m+1}) \ldots (f_n, e_n)\} = \{ (f,e) | f ∈ dom(o), o(f) = (\true, e) \} \\ \end{array} \]

Capture-Avoiding Substitution

The rules for capture-avoiding variable substitution e [ e' / id ] are an extension of those in the lambda calculus. In the following, assume that y ≠ x.

self [ e' / x ] = self
super [ e' / x ] = super
x [ e' / x ] = e'
y [ e' / x ] = y
{...[e]: e''...} [ e' / x ] = {...[e[e' / x]]: e''[e' / x]...}
{[e]: e'' for x in e'''} [ e' / x ] = {[e]: e'' for x in e''' [ e' / x ]}
{[e]: e'' for y in e'''} [ e' / x ] = {[e[ e' / x ]]: e''[ e' / x ] for y in e''' [ e' / x ]}
(let ... x=e... ; e'') [ e' / x ] = let ... x=e... ; e'' If any variable matches.
(let ... y=e... ; e'') [ e' / x ] = let ... y=e[ e' / x ]... ; e''[ e' / x ] If no variable matches.
(function (...x...) e) [ e' / x ] = function (...x...) e If any variable matches.
(function (...y...) e) [ e' / x ] = function (...x...) e[ e' / x ] If no variable matches.
Otherwise, e [ e' / x ] proceeds via syntax-directed recursion into subterms of e.

The rules for keyword substitution ee' / kw ⟧ for kw ∈ { self, super } avoid substituting keywords that are captured by nested objects:

selfe' / self ⟧ = e'
supere' / super ⟧ = e'
selfe' / super ⟧ = self
supere' / self ⟧ = super
{...[e]: e''...}e' / kw ⟧ = {...[ee' / kw]: e''...}
{[e]: e'' for x in e'''}e' / kw ⟧ = {[ee' / kw ⟧]: e'' for x in e'''e' / kw}
Otherwise, ee' / kw ⟧ proceeds via syntax-directed recursion into subterms of e.

Execution

The following big step operational semantics rules define the execution of Jsonnet programs, i.e. the reduction of a Jsonnet program e into its Jsonnet value v via the judgement \(e ↓ v\).

\[\rule{value} { v ∈ \{\null, \true, \false\} ∪ String ∪ Number ∪ Function ∪ Array } { v ↓ v } \]
\[\rule{object} { ∀i∈\{1\ldots n\}: e_i ↓ f_i ∈ String \\ ∀i,j∈\{1\ldots n\}: f_i = f_j ⇒ i = j \\ o = λf∈\{ f_1 \ldots f_n \}.\textrm{let }i\textrm{ such that } f=f_i \\ \hspace{20pt} \left\{\begin{array}{ll} (\false, e'_i) & \textrm{if }i≤m \\ (\true, e'_i) & \textrm{otherwise} \\ \end{array}\right. \\ } { \object{[e_1]:e'_1 \ldots [e_m]:e'_m, [e_{m+1}]::e'_{m+1} \ldots [e_n]::e'_n } ↓ o } \]
\[\rule{object-comp} { e_3 ↓ [ e'_1 \ldots e'_n ] \\ ∀i∈\{1\ldots n\}: e_1[e'_i/x] ↓ f_i ∈ String \\ ∀i,j∈\{1\ldots n\}: f_i = f_j ⇒ i = j \\ o = λf∈\{ f_1 \ldots f_n \}. e_2[e'_i/x]\textrm{ if }f_i = f } { \ocomp{e_1}{e_2}{x}{e_3} ↓ o } \]
\[\rule{array-index} { e ↓ \array{e_0 \ldots e_n} \\ e' ↓ i ∈ \{ 0 \ldots n \} \\ e_i ↓ v } { \index{e}{e'} ↓ v } \]
\[\rule{object-index} { e ↓ o \\ e' ↓ f ∈ dom(o) \\ o(f) = (\_, e_{body}) \\ e_{body} ⟦ exp(o) / \self, \{\} / \super ⟧ ↓ v } { \index{e}{e'} ↓ v } \]
\[\rule{apply} { e_0 ↓ \function{x_1\ldots x_n}{e_b} \\ e_b [ e_1/x_1 \ldots e_n/x_n ] ↓ v } { e_0(e_1 \ldots e_n) ↓ v } \]
\[\rule{local} { binds = \assign{x_1}{e_1} \ldots \assign{x_n}{e_n} \\ \local{binds}{e[\local{binds}{e_1} / x_1 \ldots \local{binds}{e_n} / x_n ]} ↓ v } { \local{binds}e ↓ v } \]
\[\rule{if-true} { e_1 ↓ \true \hspace{15pt} e_2 ↓ v } { \if{e_1}{e_2}{e_3} ↓ v } \]
\[\rule{if-false} { e_1 ↓ \false \hspace{15pt} e_3 ↓ v } { \if{e_1}{e_2}{e_3} ↓ v } \]
\[\rule{object-inherit} { e_L ↓ o_L \hspace{15pt} e_R ↓ o_R \hspace{15pt} y, z \textrm{ fresh} \\ e_s = \super + exp(λf∈dom(o_L). o_L(f)⟦y / \self, z / \super ⟧) \\ o = λf∈dom(o_L, o_R). \\ \begin{array}{ll} \hspace{20pt} (h, \local{y = \self, z = \super}{e_{body} ⟦e_s / \super⟧}) & \textrm{if }o_R(f) = (h, e_{body}) \\ \hspace{20pt} o_L(f) & \textrm{otherwise} \\ \end{array} \\ } { e_L \texttt{ + } e_R ↓ o } \]
\[\rule{string-concat} { e_L ↓ v_L \hspace{15pt} e_R ↓ v_R \\ v_L ∈ String \vee v_R ∈ String } { e_L \texttt{ + } e_R ↓ stringconcat(tostring(v_L), tostring(v_R)) } \]
\[\rule{equality} { e_L ↓ v_L ⇓ j_L \\ e_R ↓ v_R ⇓ j_R } { e_L \texttt{ == } e_R ↓ j_L = j_R } \]
\[\rule{boolean-and-shortcut} { e_L ↓ \false } { e_L \texttt{ && } e_R ↓ \false } \]
\[\rule{boolean-and-longcut} { e_L ↓ \true \\ e_R ↓ \true } { e_L \texttt{ && } e_R ↓ \true } \]
\[\rule{boolean-or-shortcut} { e_L ↓ \true } { e_L \texttt{ || } e_R ↓ \true } \]
\[\rule{boolean-or-longcut} { e_L ↓ \false \\ e_R ↓ b } { e_L \texttt{ || } e_R ↓ b } \]

String concatenation will implicitly convert one of the values to a string if necessary. This is similar to Java. The referred function \(tostring\) returns its argument unchanged if it is a string. Otherwise it will manifest its argument as a JSON value \(j\) and unparse it as a single line of text.

The semantics of unary operators - ~ and ! are not given above. Negation has its usual double precision floating point meaning. Unary ~ first converts the value to a 64 bit signed integer, then does the unary negation, then casts back to double precision floating point. The unary operator ! operates only on booleans and has its usual meaning.

Binary operators can be divided into those that implement boolean arithmetic (short cut semantics, see above rules), floating point arithmetic (standard semantics, rules not given above), and the various special cases (rules given above): string concatenation, array concatenation, manifest equality, and object inheritance. The floating point arithmetic operations are standard, except in the case of bitwise operations, where the number is first cast to a 64 bit signed integer for the operation, then cast back to a double.

Equality has a subtle definition in Jsonnet, due to the desire for e.g. {x:1, y:self.x} to compare equal to {x:1, y: 1}. To permit this, equality is performed after manifesting the values being compared into JSON values, which binds self and fully evaluates all the fields. One effect of this is that hidden fields are ignored when testing for equality. There is another more fundamental reason why, even if two Jsonnet expressions e₁ and e₂ compare equal with ==, they cannot be substituted in an arbitrary context. For example in the above case, e₁ + {x: 2} will yield {x: 2, y: 2}, whereas e₂ + {x: 2} will yield {x: 2, y: 1}. Note that such caveats are actually quite common in programming languages, particularly those that perform automatic conversions on operands of the == operator.

The error operator cannot be expressed in our rules, as we model errors as stuck execution. The semantics of error are that its subterm is evaluated to a Jsonnet value. If this is a string, then that is the error that is raised. Otherwise, an error is raised complaining that the error message was not a string. This specification does not specify how the error is presented to the user, and whether or not there is a stack trace. Error messages are meant for human inspection, and there is therefore no need to standardize them.

JSON Values

JSON values are unparsed to become the ultimate output of Jsonnet programs. Hidden fields in Jsonnet objects are lost on the conversion to JSON. Arrays elements and Object fields are fully evaluated. Functions cannot be represented.

jJValue= { null, true, false } ∪ StringDoubleJObjectJArray
JObject= StringJValue
JArray::= [ j₀ ... jₙ ]

Note that JValueValue.

JObjects are represented as partial functions, hence the use of the harpoon notation ⇀. If a JSON object does not define a particular field, the function is undefined. This partial function representation makes it clear that there can be no duplicate fields, and also the fields are unordered.

Partial functions are constructed with the bounded lambda notation described earlier.

Manifestation

Manifestation is the conversion of a Jsonnet value into a JSON value. It is represented with the judgement \(v⇓j\). The process requires executing arbitrary Jsonnet code fragments, so the two semantic judgements represented by \(↓\) and \(⇓\) are mutually recursive. Hidden fields are ignored during manifestation. Functions cannot be manifested, so an error is raised in that case.

\[\rule{manifest-value} { j ∈ \{\null, \true, \false\} ∪ String ∪ Number } { j ⇓ j } \]
\[\rule{manifest-object} { visible = \{\ f\ |\ f∈dom(o) ∧ o(f) = (\false, \_)\ \} \\ j = λf∈visible. j'\textrm{ where }(\_, e_{body}) = o(f)\textrm{, and }e_{body}⟦exp(o)/self,\{\}/super⟧↓v⇓j' } { o ⇓ j } \]
\[\rule{manifest-array} { ∀i∈\{1\ldots n\}: e_i↓v_i⇓j_i } { \array{e_1 \ldots e_n} ⇓ \array{j_1 \ldots j_n} } \]

Properties of Jsonnet Inheritance

Let D, E, F range over arbitrary expressions. Let ≡ mean contextual equivalence, i.e if D ≡ E then C[D] and C[E] will manifest to the same JSON, for any context C. If D yields an error, and E yields an error, then D ≡ E (regardless of what the exact text of the message is).

Associativity (D + E) + F   ≡   D + (E + F)
Idempotence D + D   ≡   D (if D does not contain super)
Commutativity D + E   ≡   E + D (if D, E do not contain super and have no common fields)
Identity D + { }   ≡   D
{ } + D   ≡   D

Standard Library Reflection Functions

The functions in the standard library that provide reflection capabilities are formalized below. Note the lexical sorting of the fields in the object-fields rule. This is because the fields have no order within the object value, but the resulting list does define an order. To avoid ambiguity, we therefore require the list of fields to be sorted alphabetically.

\[\rule{type-null} { e ↓ \texttt{null} } { \texttt{std.type(}\textit{e}\texttt{)} ↓ \texttt{"null"} } \]
\[\rule{type-boolean} { e ↓ v ∈ Boolean } { \texttt{std.type(}\textit{e}\texttt{)} ↓ \texttt{"boolean"} } \]
\[\rule{type-number} { e ↓ v ∈ Number } { \texttt{std.type(}\textit{e}\texttt{)} ↓ \texttt{"number"} } \]
\[\rule{type-string} { e ↓ v ∈ String } { \texttt{std.type(}\textit{e}\texttt{)} ↓ \texttt{"string"} } \]
\[\rule{type-object} { e ↓ v ∈ Object } { \texttt{std.type(}\textit{e}\texttt{)} ↓ \texttt{"object"} } \]
\[\rule{type-function} { e ↓ v ∈ Function } { \texttt{std.type(}\textit{e}\texttt{)} ↓ \texttt{"function"} } \]
\[\rule{type-array} { e ↓ v ∈ Array } { \texttt{std.type(}\textit{e}\texttt{)} ↓ \texttt{"array"} } \]
\[\rule{object-has} { e ↓ o \\ b = ∃f∈dom(o). o(f) = (\false, \_) } { \texttt{std.objectHas(}\textit{e}\texttt{)} ↓ b } \]
\[\rule{object-fields} { e ↓ o \\ \{f_1 \ldots f_n\} = \{\ f\ |\ f∈dom(o) ∧ o(f) = (\false, \_)\ \} \\ ∀i,j∈\{1 \ldots n\}: i≤j ⇒ f_i≤f_j } { \texttt{std.objectFields(}\textit{e}\texttt{)} ↓ \array{ f_1 \ldots f_n } } \]