Skip to content

Latest commit

 

History

History
272 lines (219 loc) · 11.3 KB

SYNTACTIC_GRAMMAR.md

File metadata and controls

272 lines (219 loc) · 11.3 KB

ProScript syntactic grammar

This compiler-facing EBNF summarizes the admitted P-374/S-205 surface. Static restrictions called out below are normative even where EBNF alone cannot express them. The authoritative syntax rationale is retained in the S-numbered decision record.

Notation: quoted text is a terminal; ?, *, and + mean optional, zero-or-more, and one-or-more; parentheses group; | separates alternatives; comma-list(X) means zero or more X separated by exactly one comma with one optional trailing comma.

Compilation unit

source-file   = shebang? package-declaration import-declaration* declaration* EOF
package-declaration = "package" identifier
import-declaration  = "import" import-path import-tail?
import-path          = identifier ("." identifier)*
import-tail          = "as" identifier | "{" comma-list(identifier) "}"

The package header is mandatory. Imports precede other declarations. as is valid only for a whole-package import. Package and import declarations have no semicolon.

Declarations

declaration = const-declaration | struct-declaration | enum-declaration
            | resource-declaration | type-declaration | function-declaration
            | native-function-declaration | native-type-declaration
            | protocol-declaration | conformance-declaration

visibility        = "pub"
generic-parameters = "<" comma-list1(generic-parameter) ">"
generic-parameter  = identifier (":" bound-list)?
bound-list         = type ("+" type)*

const-declaration = visibility? "const" identifier (":" type)?
                    "=" constant-expression ";"

struct-declaration = visibility? "struct" identifier generic-parameters?
                     directive-group? "{" comma-list(struct-field) "}"
struct-field       = visibility? identifier ":" type directive-group?

enum-declaration = visibility? "enum" identifier generic-parameters?
                   directive-group? "{" comma-list(enum-variant) "}"
enum-variant     = identifier ("(" comma-list1(type) ")")? directive-group?

resource-declaration = visibility? "resource" identifier generic-parameters?
                       "{" resource-field-list cleanup-clause "}"
resource-field-list  = (resource-field ",")*
resource-field       = identifier ":" type
cleanup-clause       = "cleanup" "(" "mut" "self" ")" block

type-declaration = visibility? "type" "opaque"? identifier type ";"
                 | visibility? "type" identifier generic-parameters?
                   "=" type ";"

function-declaration = visibility? "fn" function-head generic-parameters?
                       parameter-list result-type? block
function-head        = identifier | owner-head "::" identifier
owner-head           = type-path | type-path "<" comma-list1(owner-parameter) ">"
owner-parameter      = identifier (":" bound-list)?
parameter-list       = "(" comma-list(parameter) ")"
parameter            = "self" | "mut" "self"
                     | "mut"? identifier ":" type
result-type          = type

native-function-declaration = visibility? "native" "offload"? "fn" identifier
                              parameter-list result-type? ";"
native-type-declaration     = visibility? "native" "type" identifier ";"

protocol-declaration = visibility? "protocol" identifier generic-parameters?
                       "{" comma-list(protocol-requirement) "}"
protocol-requirement = "fn" identifier parameter-list result-type?
conformance-declaration = "conform" owner-head ":" named-type ";"

const exists only at package scope. A receiver, when present, occupies the first parameter position. Generic owner heads repeat the owner's complete parameter vector; P-363 permits only positive applicability bounds there. Resources do not admit pub fields or structural directives. Native functions have no owner, receiver, body, or generic parameters. A native declaration may have at most one writable parameter, normalized exactly to Array<byte>.

Structural directives

directive-group  = "#[" comma-list1(directive) "]"
directive        = directive-path ("(" comma-list(directive-option) ")")?
directive-path   = identifier ("::" identifier)*
directive-option = identifier | identifier ":" constant-expression

Directive groups are trailing attachments on struct/enum heads, struct fields, and enum variants only. The admitted paths and option vocabularies are closed; this grammar is not an open attribute system.

For Debug, an ordinary struct field admits exactly Debug(redact) or Debug(skip). They are marker directives, are mutually exclusive, and cannot be duplicated. Debug(deny) is invalid at every attachment position. No positive Debug, derives, visibility-based, enum-variant, whole-type, or resource-field Debug directive exists.

Types

type          = named-type | callable-type | "(" type ")"
named-type    = type-path type-arguments?
type-path     = identifier ("." identifier)*
type-arguments = "<" comma-list1(type) ">"
callable-type = "fn" "(" comma-list(callable-parameter-type) ")" result-type?
callable-parameter-type = "mut"? type

Primitive, predeclared, imported, and declared names share named-type syntax. Angle-bracket token splitting in nested types is parser-owned. A callable's mut parameter effect is part of its exact type.

Blocks and statements

block       = "{" statement* tail-expression? "}"
statement   = let-statement | assignment-statement | return-statement
            | expression-statement | if-statement | if-let-statement
            | for-statement | arena-statement | "break" ";" | "continue" ";"

let-statement = "let" "mut"? identifier (":" type)?
                ("=" expression)? ";"
              | "let" struct-let-pattern "=" expression ";"
assignment-statement = assignment-target assignment-operator expression ";"
assignment-target    = "_" | place
assignment-operator  = "=" | "+=" | "-=" | "*=" | "/=" | "%="
                     | "&=" | "|=" | "^=" | "&^=" | "<<=" | ">>="
return-statement     = "return" expression? ";"
expression-statement = expression ";"

Only value-producing blocks owned by an expression admit a final expression without a semicolon. There are no empty statements or standalone bare blocks. Assignment is statement-only.

Control flow

if-statement     = "if" expression block ("else" (if-statement | block))?
if-let-statement = "if" "let" pattern "=" expression block
                   ("else" block)?
for-statement    = "for" for-header? block
for-header       = expression | for-pattern "in" expression
                 | map-for-binding "," map-for-binding "in" expression
map-for-binding  = "mut"? identifier | "_"
arena-statement  = "arena" block

if-expression    = "if" expression block "else" (if-expression | block)
match-expression = "match" expression "{" comma-list1(match-arm) "}"
match-arm        = pattern "=>" (expression | block)
select-expression = "select" "{" comma-list1(select-branch) "}"
select-branch     = "case" select-target? "="? expression "=>" (expression | block)
                  | "default" "=>" (expression | block)
recover-expression = "recover" block "with" identifier block

The three loop forms are infinite for {}, condition for condition {}, and iteration. Arrays, sets, receivers, ranges, and user iterators use for pattern in source {}. A map uses exactly for key_binding, value_binding in source {}; each binding is one identifier, mut plus one identifier, or _. A map rejects the single-binding form and every non-map rejects the two-binding form. There is no MapEntry type, C-style three-clause loop, loop value, label, while, named arena, match guard, or select guard. Select target punctuation is constrained by the selected operation's result and the rules in the consolidated specification.

Expressions

Primary expressions include literals, paths, self, collection literals, anonymous functions, struct construction, and the control-flow expressions above. Postfix operations bind more tightly than prefix or binary operators.

expression = primary-expression
           | expression postfix
           | prefix-operator expression
           | expression binary-operator expression
           | expression (".." | "..=") expression

postfix = "(" comma-list(call-argument) ")"
        | "<" comma-list1(type) ">" "(" comma-list(call-argument) ")"
        | "." identifier
        | "::" identifier
        | "[" expression "]"
        | "?"

call-argument = expression | "mut" place
place         = identifier (("." identifier) | ("[" expression "]"))*

array-literal = "[" comma-list(expression) "]"
map-literal   = "map" "{" comma-list(expression ":" expression) "}"
set-literal   = "set" "{" comma-list(expression) "}"
struct-construction = type-path type-arguments?
                      "{" comma-list(identifier ":" expression) "}"
anonymous-function = "fn" anonymous-parameter-list result-type? block
spawn-expression   = "spawn" call-expression

mut place is a complete call argument, not a prefix expression. spawn selects a call-shaped operation, looking through grouping only. Generic struct and enum construction requires a complete explicit owner argument vector when the owner is generic; pattern heads do not write owner arguments.

Precedence

From tightest to loosest:

  1. Member/type-associated access, indexing, calls, and postfix ?
  2. Prefix !, -, and ^; call-only spawn
  3. *, /, %, <<, >>, &, &^
  4. +, -, |, ^
  5. ==, !=, <, <=, >, >=
  6. &&
  7. ||
  8. finite range .. and ..=

Binary arithmetic, bitwise, shift, and logical operators are left-associative. Comparisons do not create a general chaining facility. Range expressions require both endpoints and cannot chain.

Patterns

pattern = alternative-pattern | bound-pattern
alternative-pattern = "|"? bound-pattern ("|" bound-pattern)+
bound-pattern = ("mut"? identifier "@")? atomic-pattern

atomic-pattern = "_" | identifier | qualified-value | literal
               | prelude-variant-pattern | enum-variant-pattern
               | struct-pattern | array-pattern | range-pattern
               | "(" pattern ")"

enum-variant-pattern = type-path "::" identifier
                       ("(" comma-list(pattern) ")")?
struct-pattern = type-path "{" comma-list(struct-pattern-field | "..") "}"
struct-pattern-field = identifier | identifier ":" pattern
array-pattern = "[" comma-list(pattern | "..") "]"
range-pattern = range-bound? (".." | "..=") range-bound?

Alternative branches must bind identical names, types, and mutability. An array pattern has at most one rest element. Struct rest is final. Range-pattern bounds use the restricted literal/constant grammar. let and for admit only their specified irrefutable subsets.

Interpolation slots

interpolation = "${" interpolation-path "}"
interpolation-path = (identifier | "self") ("." identifier)*

Calls, indexing, conversion, operators, blocks, and nested interpreted strings are not admitted in a slot.

Parser obligations

A conforming parser must preserve original byte spans, recover with bounded work on invalid input, and never use name or type lookup to change the parse. Ambiguous call/member/type-owned forms remain lookup-neutral syntax until resolution. The formatter and editor grammar must consume this same admitted surface; a permissive highlighting grammar is not normative.