Skip to content

§4 Syntactic grammar

This chapter defines Bynk’s phrase structure: how tokens (§3) combine into declarations, types, expressions, patterns, and statements. Each production is generated from the grammar (§2.1) and embedded by name.

A production states what parses. Every constraint beyond parsing — name resolution, typing, exhaustiveness, refinement admission, the effect discipline, and all other well-formedness — is a static-semantics rule, specified normatively in §5 and not repeated here. Where a construct carries such constraints, this chapter forward-references §5 rather than restating them.

The chapters mirror the construct groupings of the friendly grammar reference; the productions are shared, the register here is the normative definition.

A source file is a commons, a context, an adapter, or test declarations.

source_file ::= (commons_decl | context_decl | adapter_decl | integration_decl | test_decl)+ | item_fragment+ | expr_fragment

A whole file: one or more top-level declarations, or a single fragment used by editor tooling.

item_fragment ::= context_body_item | handler | store_field | key_decl

A tooling entry point: a single body item parsed in isolation. Not written by hand.

expr_fragment ::= statement+ expression? | expression

A tooling entry point: statements and/or an expression parsed in isolation. Not written by hand.

commons_decl ::= "commons" qualified_name ("{" commons_body_item* "}" | commons_body_item*)

A commons module. The body braces are optional at file scope; with no braces the body items run to the end of the file.

context_decl ::= "context" qualified_name ("{" context_body_item* "}" | context_body_item*)

A context. As with commons, the body braces are optional at file scope. Well-formedness: §5.

adapter_decl ::= "adapter" qualified_name ("{" adapter_body_item* "}" | adapter_body_item*)

An adapter — the host boundary: a capability contract co-located with a named TypeScript binding. As with commons, the body braces are optional at file scope. An adapter’s providers are external (bodiless, §4.3.8) and it may not declare services or agents; those placement rules, the binding requirement, and the reserved bynk namespace are well-formedness: §5.

test_decl ::= "test" qualified_name ("{" test_body_item* "}" | test_body_item*)

A test block naming the commons or context it targets. Well-formedness: §5.

integration_decl ::= "test" "integration" string_literal ("{" wires_decl integration_body_item* "}" | wires_decl integration_body_item*)

A test integration block: the keyword test integration, a name, a wires clause, and integration body items. Well-formedness: §5.

wires_decl ::= "wires" qualified_name ("," qualified_name)*

The comma-separated list of contexts an integration test wires together. Well-formedness: §5.

integration_body_item ::= uses_decl | test_case

What may appear in an integration test: uses declarations and test cases.

commons_body_item ::= uses_decl | type_decl | fn_decl | capability_decl | provider_decl | service_decl | agent_decl | actor_decl

The declaration forms admitted in a commons body.

context_body_item ::= uses_decl | consumes_decl | exports_decl | type_decl | fn_decl | capability_decl | provider_decl | service_decl | agent_decl | actor_decl

The declaration forms admitted in a context body, including consumes and exports.

adapter_body_item ::= binding_decl | uses_decl | consumes_decl | exports_decl | type_decl | fn_decl | capability_decl | provider_decl | service_decl | agent_decl | actor_decl

The declaration forms admitted in an adapter body: the binding clause, capability and type declarations, pure helpers and uses, consumes, exports, and providers. The grammar is deliberately permissive — service and agent parse here so the placement error can be precise; their rejection is well-formedness: §5.

test_body_item ::= uses_decl | consumes_decl | mocks_decl | test_case

The declaration forms admitted in a test body, including mocks and test cases.

qualified_name ::= identifier ("." identifier)*

A dotted sequence of identifiers, e.g. shop.orders. A dotted name is a single flat identifier, not a hierarchy: bynk and bynk.time are independent names that merely share a leading segment.

uses_decl ::= "uses" qualified_name

uses followed by a qualified name. Well-formedness: §5.

consumes_decl ::= "consumes" qualified_name ("as" identifier | "{" (identifier ("," identifier)*)? ","? "}")?

consumes a unit, in one of three forms: the whole unit (consumes b), the whole unit under an alias (consumes b as Alias), or a capability selection (consumes b { Cap, … }), which flattens the named capabilities into the consumer’s local capability namespace under their bare names. The target may be a context or an adapter; which forms each consumer kind admits, and the flattening and clash rules, are well-formedness: §5.

exports_decl ::= "exports" ("opaque" | "transparent" | "capability") "{" (identifier ("," identifier)*)? ","? "}"

exports, one of opaque / transparent / capability, and a brace-delimited identifier list. Well-formedness: §5.

binding_decl ::= "binding" string_literal ("requires" "{" (binding_requirement ("," binding_requirement)*)? ","? "}")?

An adapter’s binding clause: the TypeScript module supplying its external provider classes, as a string-literal path resolved relative to the adapter’s source file, with an optional requires { … } map of npm dependencies. Well-formedness: §5.

binding_requirement ::= string_literal ":" string_literal

One "package": "range" entry in a binding’s requires map. Ranges MUST be pinned; well-formedness: §5.

Type declarations and the type references that appear in signatures.

type_decl ::= "type" identifier "=" type_body

type, a name, =, and a type body. Well-formedness: §5; the type system: §6.

type_body ::= opaque_type | refined_type | record_type | sum_type | enum_type

The right-hand side of a type: one of the five type forms.

opaque_type ::= "opaque" base_type ("where" refinement)?

opaque, a base type, and an optional where refinement.

refined_type ::= base_type ("where" refinement)?

A base type with an optional where refinement. Well-formedness: §5; admission: §6.

record_type ::= "{" (record_field ("," record_field)*)? ","? "}"

A brace-delimited, comma-separated list of record fields, with an optional trailing comma.

record_field ::= identifier ":" type_ref ("where" refinement)? ("=" expression)?

A field name, :, a type, an optional inline where refinement, and an optional = default expression. Well-formedness: §5.

sum_type ::= sum_variant+

One or more |-prefixed variants.

sum_variant ::= "|" constant_name ("(" (variant_payload_field ("," variant_payload_field)*)? ","? ")")?

A |, a constant name, and an optional parenthesised payload.

variant_payload_field ::= identifier ":" type_ref

A named field in a sum-variant payload: an identifier, :, and a type.

enum_type ::= "enum" "{" (constant_name ("," constant_name)*)? ","? "}"

enum and a brace-delimited list of constant names — a sum type whose variants all carry no payload.

refinement ::= refinement_pred ("and" refinement_pred)*

One or more predicates joined by and. Well-formedness: §5.

refinement_pred ::= pred_call | predicate_name

A single predicate: a predicate call or a bare predicate name.

pred_call ::= predicate_name "(" (pred_arg ("," pred_arg)*)? ")"

A predicate name applied to parenthesised arguments, e.g. InRange(1, 100).

predicate_name ::= "Matches" | "InRange" | "MinLength" | "MaxLength" | "Length" | "NonNegative" | "Positive" | "NonEmpty"

The set of built-in refinement predicates. Well-formedness: §5.

pred_arg ::= number_literal | float_literal | string_literal

An argument to a predicate: a number or string literal.

base_type ::= "Int" | "String" | "Bool" | "Float" | "Duration" | "Instant"

The primitive types Int, String, and Bool. Well-formedness: §5.

type_ref ::= function_type_ref | base_type | unit_type | validation_error_type | generic_type_ref | identifier

A type as it appears in a signature: a function type, a base type, the unit type, the validation-error type, a generic application, or a named type.

function_type_ref ::= (base_type | unit_type | validation_error_type | generic_type_ref | identifier | "(" type_ref ("," type_ref)* ","? ")") "->" type_ref

A function type (v0.20a): Int -> Int, (Int, String) -> Bool, () -> Int. The arrow is right-associativeA -> B -> C is A -> (B -> C) — and a parenthesised list before -> is a parameter list (a single parenthesised type without an arrow is a grouping; the empty () without an arrow stays the unit type). A function type is effectful exactly when its return type is Effect[_] — the structural rule of §6. Function types are confined to non-boundary positions; well-formedness: §5.

unit_type ::= "(" ")"

The unit type ().

validation_error_type ::= "ValidationError"

ValidationError, the error type produced when refined-type validation fails.

generic_type_ref ::= ("Result" | "Option" | "Effect" | "HttpResult" | "List" | "Map" | "Stream" | "Query" | "Connection") "[" type_ref ("," type_ref)* "]"

A generic constructor — Result, Option, Effect, HttpResult, or (v0.20b) List, Map — applied to bracketed type arguments. Well-formedness: §5 (Map keys are value-keyable, §5.10); the type system: §6.

Pure functions and methods, capability interfaces, and the providers that implement them.

fn_decl ::= "fn" (method_name | identifier) ("[" identifier ("," identifier)* "]")? "(" params? ")" "->" type_ref block

fn, a function name or a Type.method name, an optional [A, B] type-parameter list (v0.20a — free functions only; a type parameter is an unconstrained, bound-free name scoped to the signature and body), a parameter list, ->, a return type, and a block body. Well-formedness: §5.

method_name ::= identifier "." identifier

A Type.method name, defining a method on a named type.

params ::= (self_param | param) ("," param)* ","?

A parameter list: an optional self receiver followed by named parameters, with an optional trailing comma.

self_param ::= "self"

The self receiver of a method or handler.

param ::= identifier ":" type_ref

One parameter: an identifier, :, and a type. Well-formedness: §5.

capability_decl ::= "capability" identifier "{" capability_op* "}"

capability, a name, and a brace-delimited list of operation signatures. Well-formedness: §5.

capability_op ::= "fn" identifier "(" (param ("," param)*)? ","? ")" "->" type_ref

One operation in a capability: fn, a name, parameters, ->, and a return type — no body. Well-formedness: §5.

provider_decl ::= "provides" identifier "=" identifier given_clause? ("{" provider_op* "}")?

provides, the capability name, =, an implementation name, an optional given clause, and an optional brace-delimited list of operation implementations. The presence of the brace block distinguishes the two provider kinds: with a block the provider is implemented in Bynk (context-only); with no block it is external — its implementation is the named class exported by the enclosing adapter’s binding module (§4.1.19). The absence of the block, not an empty one, is the signal. Placement and wiring rules: well-formedness, §5.

provider_op ::= "fn" identifier "(" (param ("," param)*)? ","? ")" "->" type_ref block

One operation implementation: a capability operation signature with a block body. Well-formedness: §5.

given_clause ::= "given" qualified_name ("," qualified_name)*

given and a comma-separated list of the capabilities a handler or provider may use. Well-formedness: §5.

A service groups the handlers that respond to calls and external triggers.

service_decl ::= "service" identifier service_protocol? "{" handler* "}"

service, a name, an optional from <protocol> header clause, and a brace-delimited list of handlers. One protocol per service. Well-formedness: §5.

service_protocol ::= "from" ("http" | "cron" | "queue" "(" string_literal ")" | "WebSocket" "(" "in" ":" type_ref "," "out" ":" type_ref ","? ")")

The from <protocol> clause: from http, from cron, from queue("name") (v0.44), or from WebSocket(in: I, out: O) (v0.103). Absent ⇒ the contract-mediated default, which admits only on call. Well-formedness: §5.

handler ::= call_handler | http_handler | cron_handler | queue_handler | ws_open_handler | ws_close_handler

A handler: a call, HTTP, cron, or queue entry point, matching the service’s protocol. Well-formedness: §5.

call_handler ::= "on" "call" identifier? by_clause? "(" (param ("," param)*)? ","? ")" "->" type_ref given_clause? block

on call, an optional name, parameters, ->, a return type, an optional given clause, and a block body.

http_handler ::= "on" http_method "(" string_literal ")" by_clause? "(" (param ("," param)*)? ","? ")" "->" type_ref given_clause? block

on <Method>("route") — an HTTP method-builder (the verb collapses verb+route into one config expression in the handler-config slot), then parameters, ->, a return type, an optional given clause, and a block body. Valid only in a from http service. Well-formedness: §5.

http_method ::= "GET" | "POST" | "PUT" | "PATCH" | "DELETE"

The HTTP verbs a route may handle. Well-formedness: §5.

cron_handler ::= "on" "schedule" "(" string_literal ")" by_clause? "(" (param ("," param)*)? ","? ")" "->" type_ref given_clause? block

on schedule("expr"), parameters, ->, a return type, an optional given clause, and a block body. Valid only in a from cron service. Well-formedness: §5.

queue_handler ::= "on" "message" by_clause? "(" (param ("," param)*)? ","? ")" "->" type_ref given_clause? block

on message(message) — the bound queue lives on the service’s from queue("name") header. Parameters, -> Effect[QueueResult], an optional given clause, and a block body. Well-formedness: §5.

A from WebSocket(in: I, out: O) service declares the connection-lifecycle handlers on open, on message, and on close. Each is a handler head — on, the lifecycle keyword, a required by_clause naming the actor, parameters, -> Effect[()], an optional given clause, and a block body — and is valid only in a from WebSocket service:

  • on open — the upgrade handshake; the body sees an owned connection binding of type Connection[O].
  • on message — parameters end with the decoded inbound frame of type I.
  • on close — the connection ended.

Well-formedness — exactly one on open, edge authentication, held-resource disposal: §5. (The rendered grammar productions for these handler heads land with the tree-sitter grammar; see Reference — grammar.)

by_clause ::= "by" (identifier ":")? identifier ("|" identifier)*

by (<binder>:)? <Actor> ("|" <Actor>)* — the actor(s) a handler consumes, positioned after the protocol config and before the parameters (on schedule("…") by s: Scheduler () -> …). The binder is optional (v0.50): by <name>: <Actor> captures the verified identity (read as <name>.identity); by <Actor> declares-and-verifies the contract without capturing it (anonymous or verify-and-discard). Omitting by entirely inherits the protocol’s default actor; on a from http handler by is required (the binder still optional).

A |-separated list of actors (v0.52) is an ordered sum of peer actors (by who: User | Visitor): the boundary tries each peer’s scheme in declared order and binds the first that verifies; the body matches on the resolved actor (the binder is required for a sum). Well-formedness: §5.

An actor is a nominal boundary contract — a closed, compiler-known authentication scheme plus an optional sealed identity — consumed by a handler’s by clause (§4.4.8). Actors are context-only.

actor_decl ::= "actor" identifier ("{" "auth" "=" scheme scheme_config? ("," "identity" "=" type_ref)? "}" | "=" identifier "where" refinement)

actor <Name> { auth = <Scheme> }, optionally , identity = <Type>. The refinement form actor <Name> = <Base> where <predicate> (v0.53) declares an authorisation invariant over a Bearer base; the predicate is a closed set of claim predicates (hasClaim, claimEquals). Well-formedness: §5.

scheme ::= "None" | "Internal" | "Bearer" | "Signature"

The closed authentication-scheme set. None, Internal, Bearer, and Signature (v0.51) are supported. The authenticated schemes carry a keyed-args config — Bearer(secret = "<ENV>") and Signature(secret = "<ENV>", header = "<Header>", (timestamp = "<Header>", tolerance = <seconds>)?) — parsed by the scheme_config production (string- or integer-valued args; the checker validates which keys each scheme admits). Well-formedness: §5.

An agent is a keyed, stateful entity whose state lives in store fields that handlers read by name and write with :=.

agent_decl ::= "agent" identifier "{" key_decl store_field* invariant_decl* handler* "}"

agent, a name, and a body holding a key declaration, store fields, zero or more invariants, and handlers — in that fixed order. Well-formedness: §5.

key_decl ::= "key" identifier ":" type_ref

key, an identifier, :, and a type — the agent’s identity.

store_field ::= "store" identifier ":" store_kind store_annotation* ("=" expression)?

store, a name, :, a store_kind over its type parameters, zero or more store_annotations, and an optional = initialiser — a persistent field of the agent. Well-formedness — required initial value, kind validity: §5 (ADR 0108).

store_kind ::= identifier ("[" type_ref ("," type_ref)* "]")?

The closed catalogue of storage kinds: Cell, Map, Set, Cache, Log. The catalogue is closed — there is no Queue storage kind (ADR 0122).

store_annotation ::= "@" identifier ("(" (annotation_arg ("," annotation_arg)*)? ","? ")")?

A @name(args) annotation between the kind and the initialiser — @ttl (on Cache), @retain (on Log), @indexed (on Map), @bounded. Arguments are compile-time literals. Well-formedness — kind match, known name: §5 (ADR 0111).

invariant_decl ::= "invariant" identifier ":" expression

invariant, a name, :, and a predicate expression — a universally-quantified property that must hold of every committed state. Invariants form a phase between the store fields and the handlers; one after a handler is a parse error (bynk.parse.invariant_after_handler). The predicate references the agent’s store fields by bare name. Well-formedness — purity, Bool type, agent-locality: §5 (ADR 0107).

Bynk is expression-oriented: a block’s value is its final expression. Operator precedence is fixed by the binary_expr production (§4.6.7).

expression ::= if_expr | match_expr | is_expr | assert_expr | binary_expr | unary_expr | primary

Any expression: control flow, a refinement check, an operator expression, or a primary.

primary ::= lambda_expr | paren_expr | method_call | field_access | call | record_construction | record_spread | question_expr | ok_expr | err_expr | some_expr | none_expr | effect_pure_expr | mock_expr | list_literal | block | number_literal | float_literal | string_literal | boolean_literal | unit_literal | self_expr | identifier

The atomic and postfix expressions: literals, names, calls, field and method access, constructors, and parenthesised expressions.

if_expr ::= "if" expression block "else" (if_expr | block)

if, a condition, a block, else, and either a further if or a block. The else arm is not optional. Well-formedness: §5.

match_expr ::= "match" expression "{" match_arm* "}"

match, a scrutinee, and a brace-delimited list of match arms. Well-formedness — including exhaustiveness: §5.

is_expr ::= expression "is" pattern

An expression, is, and a pattern. Well-formedness — including the narrowing it introduces: §5.

binary_expr ::= expression "implies" expression | expression "||" expression | expression "&&" expression | expression ("==" | "!=") expression | expression ("<" | "<=" | ">" | ">=") expression | expression ("+" | "-") expression | expression ("*" | "/") expression

The binary operators, listed from lowest precedence (implies, then ||) to highest (*, /); the production order is the precedence order. implies (v0.80) is logical implication, right-associative, P implies Q!P || Q. Well-formedness: §5.

unary_expr ::= ("!" | "-") expression

Logical negation ! and numeric negation -, prefixed to an expression.

method_call ::= primary "." identifier ("[" type_ref ("," type_ref)* "]")? "(" (expression ("," expression)*)? ","? ")"

A receiver, ., a method name, and parenthesised arguments. Well-formedness: §5.

v0.22a: the numeric base-type keywords Int and Float are admitted in static-receiver positionInt.parse(s) / Float.parse(s) — but only when immediately followed by .; a bare Int in expression position remains a parse error. (List.empty() needs no such rule: List is lexically an ordinary identifier.)

v0.22b: a method call accepts explicit type argumentsJson.decode[Order](s) — under the same same-line-[ rule as call type application (0039): a [ opening a new line is a list literal. In v0.22b only the Json.decode static consumes them; type arguments on any other method are bynk.generics.type_arg_mismatch (generic user methods remain deferred). The bare name[T] value form stays reserved.

field_access ::= primary "." identifier

A receiver, ., and a field name. Well-formedness: §5.

lambda_expr ::= "(" (lambda_param ("," lambda_param)*)? ")" "=>" (expression | block)

A lambda (v0.20a): (o) => o.paid, (acc, t) => acc + t, () => 0, or with a block body (o) => { … }. Always parenthesised; => is the value arrow, shared with match arms — -> stays the type arrow. Well-formedness (contextual parameter typing, the unannotated rule, bottom-up effectfulness): §5.

lambda_param ::= identifier (":" type_ref)?

One lambda parameter with an optional type annotation — optional because an expected function type supplies it; required in unconstrained positions (§5).

call ::= identifier ("[" type_ref ("," type_ref)* "]")? "(" (expression ("," expression)*)? ","? ")"

A name, optional bracketed type arguments (v0.20a, name[T](…) — the explicit-instantiation form; a bare name[T] without an argument list is a reserved parse error), and parenthesised arguments — a function call, a variant construction, an agent instantiation, or (v0.20a) the application of a function-typed value in scope. Well-formedness: §5.

record_construction ::= identifier "{" (field_init ("," field_init)*)? ","? "}"

A type name and a brace-delimited list of field initialisers. Well-formedness: §5.

field_init ::= identifier ":" expression | identifier

One field of a record construction: name: value, or the shorthand name.

record_spread ::= identifier "{" "..." expression ("," field_init)* ","? "}" | "{" "..." expression ("," field_init)* ","? "}"

A ... spread of an existing record, optionally overriding fields, with or without a leading type name. Well-formedness: §5.

question_expr ::= expression "?"

An expression followed by ?. Well-formedness: §5.

ok_expr ::= "Ok" "(" expression ")"

Ok(…) — the success constructor of Result or HttpResult. Well-formedness: §5.

err_expr ::= "Err" "(" expression ")"

Err(…) — the failure constructor of Result. Well-formedness: §5.

some_expr ::= "Some" "(" expression ")"

Some(…) — the present constructor of Option. Well-formedness: §5.

none_expr ::= "None"

None — the absent constructor of Option.

effect_pure_expr ::= "Effect" "." "pure" "(" expression ")"

Effect.pure(…) — lifts a pure value into an Effect.

mock_expr ::= "Mock" "[" type_ref "]" mock_arg?

Mock[T] with an optional pin argument. Well-formedness — including that it is valid only in test bodies: §5.

mock_arg ::= "(" expression ("," expression)* ","? ")" | "{" (field_init ("," field_init)*)? ","? "}"

The pin to a Mock[T]: positional arguments or a brace-delimited record of field pins.

list_literal ::= "[" (expression ("," expression)* ","?)? "]"

(v0.20b) [a, b, c], with an optional trailing comma — a leading [ in expression position. It does not collide with explicit type application (name[T](…), §4.6.10): that [ is a postfix form on a callee identifier and MUST sit on the same line as it — a [ opening a new line starts a list literal. There is no Map literal ({ } is records and blocks) and no indexing form (get(i) returns Option[T]). Well-formedness — including empty-literal element-type inference: §5.10.

paren_expr ::= "(" expression ")"

A parenthesised expression, for grouping.

self_expr ::= "self"

self — the receiver inside a method or agent handler. Well-formedness: §5.

The patterns used in match arms and is checks.

match_arm ::= pattern "=>" expression ","?

A pattern, =>, a result expression, and an optional trailing comma — arm separators are optional. Well-formedness: §5.

pattern ::= wildcard_pattern | variant_pattern

A pattern: a wildcard or a variant pattern.

variant_pattern ::= (identifier ".")? identifier ("(" (pattern_binding ("," pattern_binding)*)? ","? ")")?

A constant name, optionally qualified, with an optional parenthesised list of bindings. Well-formedness: §5.

wildcard_pattern ::= "_"

_ — matches anything and binds nothing.

pattern_binding ::= named_binding | positional_binding

A binding within a variant pattern: named or positional.

named_binding ::= identifier ":" (identifier | "_")

Binds a payload field by name: field: name, or field: _ to ignore it.

positional_binding ::= identifier | "_"

Binds a payload field by position, or _ to ignore it.

A block is a sequence of statements ending in an optional value expression.

block ::= "{" statement* expression? "}"

A brace-delimited sequence of statements with an optional trailing expression, which is the block’s value.

statement ::= let_stmt | effect_let_stmt | effect_send_stmt | assign_stmt | assert_expr

A statement: a let, an effectful let, an asynchronous send (~>), a := store write, or an assertion.

let_stmt ::= "let" binding_name (":" type_ref)? "=" expression

let, a binding name, an optional type annotation, =, and an expression. Well-formedness: §5.

effect_let_stmt ::= "let" binding_name (":" type_ref)? "<-" expression

let, a binding name, an optional type annotation, <-, and an effect expression. Well-formedness: §5.

effect_send_stmt ::= "~>" expression

~> and an effect expression — an asynchronous send. Unlike an effect_let_stmt it carries no binder: the reply is not awaited and nothing is bound. Well-formedness — including the requirement that the reply be Effect[()] (the error gate): §5.

assign_stmt ::= identifier ":=" expression

An identifier, :=, and an expression — a Cell store write. Well-formedness — including that the target is a store Cell field and the right-hand side does not read it: §5 (ADR 0108).

assert_expr ::= "assert" expression

assert and a condition. Well-formedness: §5.

binding_name ::= identifier | "_"

The name bound by a let: an identifier, or _ to discard the value.

Test cases and mocks. See also the top-level test_decl (§4.1.6) and integration_decl (§4.1.7).

test_case ::= "test" string_literal block

test, a description string, and a block body. Well-formedness: §5.

mocks_decl ::= "mocks" identifier "=" identifier "{" provider_op* "}"

mocks, a capability name, =, an implementation name, and a brace-delimited list of operation implementations. Well-formedness: §5.