Beluga Syntax

We discuss on a high-level the idea behind Beluga's concrete syntax. Beluga is a two-level system: 1) it supports specifications of formal systems in the logical framework LF 2) it supports programming with LF specifications. Hence, Beluga programs consist of two kinds of declarations: LF constant declarations and computation-level declarations.

sig ::=                   % Empty signature
     | lf_decl sig        % LF constant declaration
     | c_decl  sig        % Computation-level constant declaration

Warning:

LF declarations

LF declarations in Beluga follow closely the syntax used in the Twelf system. There are however a few differences which we will mention below.

lf_decl ::= id : term.         % a : K  or  c : A
       | %name id id.          % name preference declaration

term ::= type                  % type
       | id                    % variable x or constant a or c
       | term -> term          % A -> B
       | term <- term          % A <- B, same as B -> A
       | {id : term} term      % Pi x:A. K  or  Pi x:A. B
       | \id . term            % lambda x. M 
       | term term             % A M  or  M N
       | _                     % hole, to be filled by term reconstruction
     

The constructs {x:U} V and \x. V bind the identifier x in V, which may shadow other constants or bound variables. As usual in type theory, U -> V is treated as an abbreviation for {x:U} V where x does not appear in V.

In the order of precedence, we disambiguate the syntax and impose restrictions as follows:

Next, we give a few simple examples illustrating what terms Beluga parses and which ones it does not. For example, the following are parsed identically:

   d : a <- b <- {x:term} c x -> p x.
   d : ({x:term} c x -> p x) -> b -> a.
   d : ((a <- b) <- ({x: term} ((c x) -> (p x)))).

The following term parses:

      d : p x <- a <- b <-  c x.  % against our convention but parses
      d : p X <- a <- b <-  c X.  % Better code: uses convention 

      P:term -> type              % Parse error

The following will not parse:

  d: p ((\x.x) a) .    % this will give an error since there is a redex
  d: p a.              % this will parse.

More differences to Twelf syntax: We summarize a few more differences here. These restrictions may be addressed in the future.

Computation-level declarations

In addition to LF declarations, we support three additional kinds of declarations: schema declarations, which describe the structure of contexts, declarations of recursive functions (including mutual recursive functions), and value declarations.

c_decl ::= 
       | schema ctx_schema;            % schema declaration
       | rec_prog ;                    % recursive programs
       | let id = exp ;                % computation-level variable declaration


rec_prog ::= rec id : c_typ = exp      % recursive program id 
       | rec_prog and id : c_typ = exp 

To embed LF terms into computations, we extend the LF term language to support contextual variables and projections on bound variables and contextual variables. These extensions cannot be used when declaring an LF signature, but only when we use LF objects within computation-level programs and declarations.

% Terms
term ::=  .. 
        | id . k               % k-th projection of a bound variable x
        | cvar sigma           % contextual variable    
        | block s_decl . term  % Sigma x:A.B

s_decl ::= id:term | s_decl , id_term

% Contextual variable

cvar   ::= 
        | id                % context variable, meta-variable 
        | #id               % parameter variable 
        | #id . k           % proj_k 

% Substitutions 

sigma ::= 
      |                     % Nothing denotes the empty substitution
      | ..                  % Identity substitution
      | sigma term          % substitution sigma , M                      

We impose the following restrictions:

Schemas and Contexts

Schemas classify context. We allow contexts to be built up not only by single declarations, but also by Sigma-types, i.e. dependent tuples. Logically, this means we can have not only atoms in our context, but also conjunctions of atoms (where the conjunction corresponds to the Sigma-type type-theoretically).

% Schema
ctx_schema  ::= term | some [some_decls] block decls
some_decls  ::=                    % NOTHING
            | id:term              % Single declaration
            | id:_term, some_decls % Multiple declarations
decls       ::= . term             % Single declaration
            | {id:term}decls       % Multiple declarations  

% Context
ctx         ::=                    % NOTHING denoting the empty context
            | id                   % Context variable
            | x:block_type         % Context with one declaration x:block_type
            | ctx, id:block_type   % Psi, x:block_type  

block_type ::= term                % Single type
            | block decls          % Sigma-type.

A ctx_hat is a context where we erase the type declarations.

ctx_hat ::= 
        | id                      % Context variable or variable name
        |                         % NOTHING denoting the empty context
        | ctx_hat, id             % psihat, x 

Computation-level types

% Computation-level Types
c_typ   ::= 
        | term [ctx]               % Contextual type A[Psi]
        | c_typ -> c_typ           % Functions
        | {id::term[ctx]} c_typ    % Dependent function type U::A[Psi]} tau
        | {id:ctx_schema} c_typ    % Implicit quantification over context variables 
        | {id:(ctx_schema)*} c_typ % Explicit quantification over context variables 

In the order of precedence, we disambiguate the syntax:

Computation-level Expressions

exp    ::= 
       | [ctx_hat] term                % Contextual object: [g] M
       | [ctx] term                    % Contextual object where we provide
                                       % the full context as a type annotation
       | FN id => exp                  % Context abstraction FN g => e
       | fn id => exp                  % Computation-level functions: fn x => e 
       | mlam id => exp                % Abstraction over dependent type argument
       | exp exp                       % Application: e1 e2
       | exp [ctx]                     % Context application: e [g]
       | exp A < ctx_hat . term >      % Application of a dependent argument
       | exp A < ctx . term >          % Application of a dependent argument
                                       % with full context as type annotation
       | let [ctx] term = exp in exp   % Let expression
       | case exp of branch | .. | branch


% Branches
branch ::= 
       | delta  [ctx] term => exp
       | delta  [ctx] term : term[ctx] => exp   
                                       % with explicit type annotation on pattern

% Declarations of contextual variables
delta  ::= 
       |                               % NOTHING
       | {id::block_type[ctx]}         % Single contextual declaration
       | {id:(schema)*}                % Single context variables declaration
       | delta, {id::block_type[ctx]}  % Contextual declaration
       | delta, {id:(schema)*}         % Quantification over context variables

The constructs {X::A[Psi]} tau the identifier X must be an upper-case letter. It may overshadow other contextual variables in tau. Note that A[Psi] -> tau is NOT just an abbreviation for {X::A[Psi]} tau . where X does not appear in tau. On the computation-level we retain the difference between quantification and implication. It is however worth noting that there is a simple translation between programs of these two types which involves lifting universally quantified elements to become proper stand-alone computation-level expressions.

In the order of precedence, we disambiguate the syntax and impose restrictions as follows:

Emacs mode

There is an emacs mode for Beluga, which is distributed with the Beluga code and example base. This will display the the source-syntax in a nicer format.