blaise/docs/grammar.ebnf
Graeme Geldenhuys 6a22aa7828 feat(lang): default parameter values + metaclass refs uncovered by testpunit2
Three compiler additions, exercised end-to-end by testpunit2.pas:

* Default parameter values — single-name, non-var, non-open-array params
  may carry '= literal-or-named-constant'. Overload resolution accepts
  any arity in [MinArity, ParamCount]; the resolver tie-breaks toward
  the candidate that needs fewer defaulted slots. AnalyseProcCall and
  AnalyseFuncCallExpr clone the default expression into the call's Args
  list. Defaults declared on a unit-interface forward decl are
  ownership-transferred to the matching impl param at reconciliation.
* Metaclass references — a bare class type identifier in a value
  position (e.g. 'EError' as an argument or in 'Pointer(EError)') is
  now retyped to Pointer with codegen emitting '$typeinfo_<Name>' as
  an immediate. Matches the value held by vtable[0], so 'A.ClassType =
  EError' is true exactly when A is an instance of that exact class.
* Numeric widening at call sites — the existing CoerceArg helper now
  covers Integer→Double (swtof/sltof), Single→Double (exts) and
  Integer→Single, in addition to the pre-existing w→l. Inherited-,
  constructor-, method-, and function-call sites all route through it.

punit.pas gains DefaultDoubleDelta, two Double AssertEquals overloads,
and '= ''' defaults on the four-arity AddTest declarations.

Docs: grammar.ebnf extends ParamGroup with the optional default; a new
DefaultValue rule lists the permitted forms. language-rationale.adoc
adds 'Default Parameter Values' and 'Metaclass References' sections
recording the decision, alternatives rejected, and the overload
tie-break formula.

All 1194 existing compiler tests pass; testpunit2 compiles, assembles,
links and runs (31 tests; the expected pass/fail/inactive pattern).
2026-05-05 17:08:51 +01:00

703 lines
25 KiB
EBNF

(* Blaise Language Grammar EBNF
*
* This grammar reflects the language as implemented through Phase 3 of the
* Blaise compiler. It is derived directly from uParser.pas and serves as
* the authoritative reference for the parser, language tooling, and future
* extensions.
*
* Notation
* --------
* { x } zero or more repetitions of x
* [ x ] optional x (zero or one)
* ( x | y ) alternation
* "..." terminal literal token text (case-insensitive keywords)
* UPPER terminal token class defined in the Tokens section
*
* The Tokens section defines the terminal vocabulary produced by the lexer.
* The Grammar section defines the syntactic structure.
*)
(*
* TOKENS
* *)
(* Literals *)
INT_LIT = digit { digit } ;
STRING_LIT = "'" { char | "''" } "'" ;
(* Identifiers (case-insensitive) *)
IDENT = letter { letter | digit | "_" } ;
(* Keywords all case-insensitive *)
PROGRAM = "program" ;
UNIT = "unit" ;
INTERFACE = "interface" ;
IMPLEMENTATION = "implementation" ;
USES = "uses" ;
TYPE = "type" ;
RECORD = "record" ;
CLASS = "class" ;
VAR = "var" ;
BEGIN = "begin" ;
END = "end" ;
IF = "if" ;
THEN = "then" ;
ELSE = "else" ;
WHILE = "while" ;
DO = "do" ;
FOR = "for" ;
TO = "to" ;
DOWNTO = "downto" ;
TRY = "try" ;
EXCEPT = "except" ;
FINALLY = "finally" ;
RAISE = "raise" ;
NIL = "nil" ;
IS = "is" ;
AS = "as" ;
DIV = "div" ;
VIRTUAL = "virtual" ;
OVERRIDE = "override" ;
PROCEDURE = "procedure" ;
FUNCTION = "function" ;
CONSTRUCTOR = "constructor" ;
DESTRUCTOR = "destructor" ;
(* Context-sensitive soft keywords IDENT that is treated as a keyword in a
* specific position but may be used as an identifier elsewhere *)
PROPERTY = "property" ; (* only in class bodies *)
READ = "read" ; (* only in property declarations *)
WRITE = "write" ; (* only in property declarations *)
(* Operators and punctuation *)
ASSIGN = ":=" ;
EQUALS = "=" ;
NOT_EQUALS = "<>" ;
LESS = "<" ;
GREATER = ">" ;
LESS_EQ = "<=" ;
GREATER_EQ = ">=" ;
COLON = ":" ;
PLUS = "+" ;
MINUS = "-" ;
STAR = "*" ;
SLASH = "/" ;
LPAREN = "(" ;
RPAREN = ")" ;
COMMA = "," ;
SEMICOLON = ";" ;
DOT = "." ;
CARET = "^" ; (* pointer dereference / pointer type prefix *)
(* Ignored by the lexer *)
WHITESPACE = { " " | "\t" | "\r" | "\n" } ;
LINE_COMMENT = "//" { char } newline ;
BLOCK_COMMENT = "{" { char } "}" | "(*" { char } "*)" ;
DIRECTIVE = "{$" { char } "}" ; (* compiler directives skipped *)
(*
* COMPILATION UNITS
* *)
(* A source file is either a program or a unit. *)
Program
= PROGRAM IDENT SEMICOLON
[ UsesClause ]
Block
DOT
;
Unit
= UNIT IDENT SEMICOLON
InterfaceSection
ImplementationSection
DOT
;
InterfaceSection
= INTERFACE
[ UsesClause ]
{ ConstSection | ProcSignature }
;
ImplementationSection
= IMPLEMENTATION
[ UsesClause ]
{ ConstSection | StandaloneDecl }
;
UsesClause
= USES UnitName { COMMA UnitName } SEMICOLON
;
UnitName
= IDENT { DOT IDENT }
;
(* Dotted unit names map to path-separated files: Generics.Collections
* Generics/Collections.pas. The dot is part of the name, not a field
* access operator; it is only valid in the UsesClause position. *)
(*
* BLOCK
* *)
Block
= [ TypeSection ]
{ ConstSection | VarSection | StandaloneDecl }
BEGIN StmtList END
;
(* VarSection and StandaloneDecl may be interleaved with each other. *)
(*
* TYPE SECTION
* *)
TypeSection
= TYPE TypeDecl { TypeDecl }
;
TypeDecl
= IDENT EQUALS TypeDef SEMICOLON
| IDENT LESS TypeParamList GREATER EQUALS TypeDef SEMICOLON
;
(* The second form declares a generic type template. TypeDef must be either
* ClassDef ( TGenericTypeDef) or InterfaceDef ( TGenericInterfaceDef).
* The parser rejects other TypeDef kinds at that position. *)
TypeDef
= RecordDef
| ClassDef
| GenericClassDef
| InterfaceDef
| GenericInterfaceDef
| EnumDef
| SetType
| ProceduralType
;
RecordDef
= RECORD FieldList END
;
ClassDef
= CLASS [ LPAREN GenericName { COMMA GenericName } RPAREN ]
{ ConstSection | FieldList }
MethodDeclList
END
;
GenericName
= IDENT [ LESS TypeArgList GREATER ] (* plain name or generic specialisation *)
;
(* The parser stores the first name in the paren list as ParentName. During
* semantic analysis, if ParentName contains '<' it is resolved as a generic
* interface and silently moved to the ImplementsNames list. Subsequent names
* are always treated as implements entries.
* Example: class(TObject, IFoo, IBar<Integer>) *)
GenericClassDef
= CLASS LESS TypeParamList GREATER (* generic template, e.g. class<T> *)
[ LPAREN IDENT RPAREN ]
{ ConstSection | FieldList }
MethodDeclList
END
;
InterfaceDef
= INTERFACE
[ LPAREN IDENT RPAREN ] (* optional parent interface *)
MethodSignatureList
END
;
GenericInterfaceDef
= INTERFACE
[ LPAREN IDENT RPAREN ] (* optional parent interface *)
MethodSignatureList
END
;
(* GenericInterfaceDef is syntactically identical to InterfaceDef; the distinction
* is that it appears after "IDENT < TypeParamList > =" in a TypeDecl. The parser
* creates a TGenericInterfaceDef AST node wrapping the TInterfaceTypeDef body.
* Type parameters are substituted at instantiation time (e.g. IFoo<Integer>). *)
FieldList
= { FieldDecl }
;
FieldDecl
= IdentList COLON TypeName SEMICOLON
;
MethodDeclList
= { MethodDecl | PropertyDecl }
;
MethodDecl
= ( PROCEDURE | FUNCTION | CONSTRUCTOR | DESTRUCTOR ) IDENT
[ LESS TypeParamList GREATER ] (* optional generic type params *)
[ LPAREN ParamList RPAREN ]
[ COLON TypeName ] (* return type for functions *)
SEMICOLON
{ MethodDirective SEMICOLON }
[ Block SEMICOLON ] (* body absent for forward, external, or abstract decls *)
;
MethodDirective
= VIRTUAL
| OVERRIDE
| ExternalDirective
| "overload" (* required on every overload of a same-named proc/func/method *)
| IDENT (* inline, stdcall, cdecl, abstract, reintroduce, *)
;
ExternalDirective
= EXTERNAL [ NAME StrLit ]
;
(* Standalone procedure/function/constructor/destructor top-level or within a block. *)
StandaloneDecl
= ( PROCEDURE | FUNCTION | CONSTRUCTOR | DESTRUCTOR ) IDENT
[ DOT IDENT ] (* TTypeName.MethodName non-generic class impl *)
[ LESS TypeParamList GREATER DOT IDENT ] (* TName<T>.MethodName generic class impl *)
[ LESS TypeParamList GREATER ] (* function Identity<T> generic standalone *)
[ LPAREN ParamList RPAREN ]
[ COLON TypeName ]
SEMICOLON
{ MethodDirective SEMICOLON }
[ Block SEMICOLON ] (* body absent for external declarations *)
;
(* Disambiguation of the three IDENT forms above:
*
* IDENT DOT IDENT non-generic class method impl (TFoo.Bar)
* IDENT LESS TypeParamList GREATER DOT IDENT generic class method impl (TList<T>.Add)
* IDENT LESS TypeParamList GREATER generic standalone function (Identity<T>)
*
* The parser reads the IDENT, then checks LESS IDENT (GT|COMMA) to enter the
* type-param list. After consuming the closing GREATER it inspects the next
* token: DOT generic class method impl; anything else generic standalone.
* For the non-generic DOT form, DOT is checked before any LESS test. *)
ProcSignature
= ( PROCEDURE | FUNCTION ) IDENT
[ LPAREN ParamList RPAREN ]
[ COLON TypeName ]
SEMICOLON
;
MethodSignatureList
= { ProcSignature }
;
PropertyDecl
= "property" IDENT
[ LBRACKET IDENT COLON TypeName RBRACKET ] (* optional index parameter *)
COLON TypeName
[ "read" IDENT ]
[ "write" IDENT ]
SEMICOLON
;
(* If the index parameter is present, the property is indexed:
* property Items[Index: Integer]: Pointer read Get write Put;
* Indexed reads emit a getter call with one extra argument (the index).
* Indexed writes emit a setter call with two extra arguments (index, value).
* Only a single index parameter is supported; multiple-index properties
* (as in Delphi) are not. *)
(* "property", "read", "write" are contextual soft keywords; they may be used
* as identifiers outside a class body. *)
ParamList
= ParamGroup { SEMICOLON ParamGroup }
;
ParamGroup
= [ VAR | CONST ] IdentList COLON ParamType [ EQUALS DefaultValue ]
;
ParamType
= ARRAY OF TypeName
| TypeName
;
DefaultValue
= IntLiteral
| FloatLiteral
| StrLiteral
| NIL
| IDENT (* must resolve to a named constant *)
;
(* VAR marks a by-reference parameter; CONST marks a read-only parameter.
* ARRAY OF TypeName denotes an open-array parameter using the two-register
* ABI: a data pointer and a high-index value passed as separate arguments.
*
* A DefaultValue is permitted only on a single-name ParamGroup that is not
* VAR/OUT and not an open-array. A call site may omit any trailing
* argument whose corresponding ParamGroup carries a DefaultValue; the
* compiler fills the missing arguments with a clone of the default
* expression. Where two overloads resolve equally, the candidate
* requiring fewer defaulted slots wins (defaulting penalty in
* ResolveStandaloneOverload / ResolveMethodOverload). *)
TypeParamList
= IDENT { COMMA IDENT }
;
(*
* CONSTANT SECTION
* *)
ConstSection
= CONST ConstDecl { ConstDecl }
;
ConstDecl
= IDENT EQUALS ConstExpr SEMICOLON
;
ConstExpr
= [ MINUS ] IntLit
| StrLit
;
(* ConstExpr covers the constant initialisers currently supported:
* - integer literals, optionally negated
* - string literals
* ConstSection may appear in: program/unit Block, InterfaceSection,
* ImplementationSection, standalone procedure/function/method bodies,
* and class declaration bodies (class-level constants). *)
(*
* VARIABLE SECTION
* *)
VarSection
= VAR VarDecl { VarDecl }
;
VarDecl
= IdentList COLON TypeName SEMICOLON
;
(*
* SHARED HELPERS
* *)
IdentList
= IDENT { COMMA IDENT }
;
TypeName
= ArrayType (* static array: array[L..H] of T *)
| SetType (* bit-set over an enum base type *)
| CARET TypeName (* pointer-to type: ^Integer, ^T, ^^T *)
| IDENT LESS TypeArgList GREATER (* generic instantiation: TList<Integer> *)
| IDENT (* simple type or built-in *)
;
ArrayType
= ARRAY LBRACKET IntLit DOTDOT IntLit RBRACKET OF TypeName
;
SetType
= SET OF IDENT
;
(* Bare procedural type function or procedure pointer. Stored as a
* single 8-byte code pointer (QBE 'l'). Not 'of object' (method pointer)
* and not 'reference to' (anonymous method / closure); both deferred
* until a use case requires them. Bare-function pointers are sufficient
* for callback APIs such as the punit test framework's TTestRun /
* TTestSetup hooks. Example:
* type
* TIntFn = function: Integer;
* TStrFn = function(const S: string): Integer;
* TLogProc = procedure(Level: Integer; const Msg: string);
*)
ProceduralType
= ( FUNCTION [ LPAREN ParamList RPAREN ] COLON TypeName )
| ( PROCEDURE [ LPAREN ParamList RPAREN ] )
;
(* Set literals use the same bracket notation as array literals.
The semantic pass distinguishes them by assignment-target type context:
when the LHS is a set type, LBRACKET ExprList RBRACKET is a set literal;
otherwise it is an array literal. An empty LBRACKET RBRACKET is valid
only as a set literal (the empty set). *)
SetLiteral
= LBRACKET [ Expr { COMMA Expr } ] RBRACKET
;
TypeArgList
= IDENT { COMMA IDENT }
;
(*
* STATEMENTS
* *)
StmtList
= { Stmt SEMICOLON } [ Stmt ]
;
Stmt
= IfStmt
| WhileStmt
| ForStmt
| TryFinallyStmt
| TryExceptStmt
| RaiseStmt
| CompoundStmt
| PointerWriteStmt
| StaticSubscriptAssign
| FieldAssignment
| MethodCall
| Assignment
| ProcCall
| (* empty *)
;
StaticSubscriptAssign
= IDENT LBRACKET Expr RBRACKET ASSIGN Expr
;
IfStmt
= IF Expr THEN Stmt [ ELSE Stmt ]
;
WhileStmt
= WHILE Expr DO Stmt
;
ForStmt
= FOR IDENT ASSIGN Expr ( TO | DOWNTO ) Expr DO Stmt
| FOR IDENT IN Expr DO Stmt
;
TryFinallyStmt
= TRY StmtList FINALLY StmtList END
;
TryExceptStmt
= TRY StmtList EXCEPT ExceptBody END
;
ExceptBody
= OnClause { ";" OnClause } [ ";" ] [ ELSE StmtList ]
| StmtList
;
OnClause
= "on" [ IDENT ":" ] TypeRef DO Stmt
;
RaiseStmt
= RAISE [ Expr ]
;
CompoundStmt
= BEGIN StmtList END
;
(* Pointer write: P^ := Expr store through pointer *)
PointerWriteStmt
= IDENT CARET ASSIGN Expr
;
FieldAssignment
= IDENT DOT IDENT [ LBRACKET Expr RBRACKET ] ASSIGN Expr
;
MethodCall
= IDENT DOT IDENT [ LPAREN [ ArgList ] RPAREN ]
;
(* Semantic disambiguation of IDENT DOT IDENT in Stmt:
* - Followed by ASSIGN or LBRACKET ASSIGN FieldAssignment (plain or indexed)
* - Otherwise MethodCall
*
* When IDENT DOT IDENT is followed by LBRACKET, the parser checks whether the
* member is an indexed property (symbol table lookup). If it is, LBRACKET Expr
* RBRACKET is consumed as the index; the result is an indexed property access.
* A following ASSIGN makes it an IndexedPropWrite; absence of ASSIGN is a read. *)
Assignment
= IDENT ASSIGN Expr
;
ProcCall
= IDENT [ LPAREN [ ArgList ] RPAREN ]
;
ArgList
= Expr { COMMA Expr }
;
(*
* EXPRESSIONS
* *)
Expr
= Comparison [ ( IS | AS ) IDENT ]
;
(* IS and AS have the lowest precedence. IDENT is the target type name. *)
Comparison
= Additive [ ( EQUALS | NOT_EQUALS | LESS | GREATER | LESS_EQ | GREATER_EQ ) Additive ]
;
Additive
= Term { ( PLUS | MINUS ) Term }
;
Term
= Factor { ( STAR | SLASH | DIV | MOD | AND | XOR | SHL | SHR ) Factor }
;
Factor
= INT_LIT
| STRING_LIT
| NIL
| ArrayLiteral
| AddrOf
| IDENT DOT IDENT [ LBRACKET Expr RBRACKET ] (* indexed property read *)
[ LPAREN [ ArgList ] RPAREN ] (* field/constructor/method *)
| IDENT LESS TypeArgList GREATER LPAREN [ ArgList ] RPAREN (* generic func call *)
| IDENT [ LPAREN [ ArgList ] RPAREN ] (* variable, func call, type cast *)
| LPAREN Expr RPAREN
;
AddrOf
= AT Factor
;
ArrayLiteral
= LBRACKET Expr { COMMA Expr } RBRACKET
;
(* ArrayLiteral is a stack-allocated temporary array passed to open-array formals.
* The element type is inferred from the first element; all elements must match.
* Empty literals [] are a semantic error (element type cannot be inferred).
* The postfix subscript Expr[N] is not available on an ArrayLiteral directly. *)
(* Pointer dereference P^ is a postfix on any Factor result:
*
* Deref
* = Factor CARET
* ;
*
* For simplicity this is shown as a postfix suffix on Factor above rather
* than a separate rule; the parser handles it by wrapping the parsed Factor
* in a TDerefExpr node when CARET follows.
*
* Type cast: TypeName LPAREN Expr RPAREN parsed as an IDENT followed by
* LPAREN, identical to a function call. The semantic pass distinguishes casts
* from calls by checking whether the symbol resolves to a type or a function.
*)
(*
* BUILT-IN PROCEDURES, FUNCTIONS, AND TYPES
* *)
(*
* Built-in procedures (recognised by the code generator, not the parser):
*
* WriteLn ( [ Expr ] ) print with trailing newline; calls libc printf
* Write ( [ Expr ] ) print without newline
* FreeMem ( Pointer ) calls libc free()
*
* Built-in functions (recognised by the code generator):
*
* GetMem ( Integer ) : Pointer calls libc malloc()
* ReallocMem( Pointer, Integer ) : Pointer calls libc realloc()
*
* Built-in types (pre-populated in the symbol table):
*
* Integer 32-bit signed integer (QBE type: w)
* Int64 64-bit signed integer (QBE type: l)
* UInt32 32-bit unsigned integer (QBE type: w)
* Boolean 32-bit boolean (QBE type: w; 0 = false, 1 = true)
* Byte 8-bit unsigned integer (QBE type: w)
* String ARC-managed UTF-8 string pointer (QBE type: l)
* Pointer untyped pointer (QBE type: l)
* ^T typed pointer to T; created on demand (QBE type: l)
*)
(*
* SEMANTIC DISAMBIGUATION NOTES
* *)
(*
* IDENT LPAREN ArgList RPAREN in Factor:
* - Symbol resolves to a function/procedure function call
* - Symbol resolves to a type type cast (e.g. Integer(x), Pointer(p))
*
* IDENT DOT IDENT in Factor:
* - Left IDENT resolves to a type symbol constructor call (TypeName.Create)
* - Left IDENT resolves to a class variable class field read
* - Left IDENT resolves to a record variable record field read
*
* IDENT DOT IDENT in Stmt:
* - Followed by ASSIGN FieldAssignment (record/class field write)
* - Otherwise MethodCall
*
* Generic instantiation vs. comparison:
* IDENT LESS ... is disambiguated by two-token lookahead. LESS is treated
* as opening a type-arg list only when both: (a) the token after LESS is an
* IDENT, and (b) the token after that is GREATER or COMMA. Otherwise LESS
* is a comparison operator (e.g. "if A < B then").
*
* Virtual dispatch vs. static call:
* Determined at semantic time from the VTableSlot field on the resolved
* method declaration. Grammar is identical for both cases.
*
* Interface method call:
* IDENT DOT IDENT where the left IDENT resolves to an interface variable.
* Generates an indirect call through the itab pointer at the appropriate slot.
*)
(*
* ARC IMPLICIT OPERATIONS
* *)
(*
* The compiler inserts reference-counting calls automatically.
* These are not visible in source syntax but are part of the language semantics.
*
* String assignment:
* _StringAddRef ( new_value )
* _StringRelease ( old_value )
* store new_value target
*
* Block exit for every String variable in scope:
* _StringRelease ( variable )
*
* Exception path (try/finally, try/except):
* _StringRelease called and slot zeroed on exception unwind.
*
* String literal layout: { w -1, w len, w cap, b "data", b 0 }
* refcount = -1 immortal (never released); char data starts at ptr+12.
*)