From d3c91164aff6a620348c81776fdae37fa41b81c3 Mon Sep 17 00:00:00 2001 From: JJ Date: Thu, 9 May 2024 18:18:15 -0700 Subject: docs: update for previous commit accordingly --- docs/ASYNC.md | 4 +- docs/ERRORS.md | 8 +-- docs/METAPROGRAMMING.md | 12 ++-- docs/OVERVIEW.md | 154 ++++++++++++++++++++++++++++++++++++------------ docs/SYNTAX.md | 98 +++++++++++++++--------------- 5 files changed, 180 insertions(+), 96 deletions(-) (limited to 'docs') diff --git a/docs/ASYNC.md b/docs/ASYNC.md index 28a69da..4fa6fa3 100644 --- a/docs/ASYNC.md +++ b/docs/ASYNC.md @@ -23,8 +23,8 @@ pub macro async(self): Future[T] = ```puck pub func await[T](self: Future[T]): T = - while not self.ready: - block + while not self.ready do + # block self.value! # apply callbacks? ``` diff --git a/docs/ERRORS.md b/docs/ERRORS.md index 33c15d5..cce29c1 100644 --- a/docs/ERRORS.md +++ b/docs/ERRORS.md @@ -46,11 +46,11 @@ If an exception is not handled within a function body, the function must be expl Despite functioning here as exceptions: errors remain types. An error thrown from an unwrapped `Result[T, E]` is of type `E`. `catch` statements, then, may pattern match upon possible errors, behaving similarly to `of` branches. ```puck -try: +try ... -catch "Error": +catch "Error" then ... -finally: +finally ... ``` @@ -65,7 +65,7 @@ This can make it difficult to do monadic error handling elegantly: one could ret ```puck pub func set[T](self: list[T], i: uint, val: T) = - if i > self.length: + if i > self.length then raise IndexOutOfBounds self.data.raw_set(offset = i, val) ``` diff --git a/docs/METAPROGRAMMING.md b/docs/METAPROGRAMMING.md index b6a4165..a8675b2 100644 --- a/docs/METAPROGRAMMING.md +++ b/docs/METAPROGRAMMING.md @@ -10,7 +10,7 @@ Macros may not change Puck's syntax: the syntax is flexible enough. Code is synt **function scope**: takes the arguments within or following a function call ```puck macro print(params: varargs) = - for param in params: + for param in params do result.add(quote(stdout.write(`params`.str))) print(1, 2, 3, 4) @@ -21,7 +21,7 @@ print "hello", " ", "world", "!" ```puck macro my_macro(body) -my_macro: +my_macro 1 2 3 @@ -31,7 +31,7 @@ my_macro: **operator scope**: takes one or two parameters either as a postfix (one parameter) or an infix (two parameters) operator ```puck macro +=(a, b) = - quote: + quote `a` = `a` + `b` a += b @@ -47,10 +47,10 @@ As macros operate at compile time, they may not inspect the *values* that their ```puck macro ?[T, E](self: Result[T, E]) = - quote: + quote match self - of Okay(x): x - of Error(e): return Error(e) + of Okay(x) then x + of Error(e) then return Error(e) func meow: Result[bool, ref Err] = let a = stdin.get()? diff --git a/docs/OVERVIEW.md b/docs/OVERVIEW.md index f6d95b7..1c19000 100644 --- a/docs/OVERVIEW.md +++ b/docs/OVERVIEW.md @@ -1,18 +1,20 @@ # An Overview of Puck -Puck is an experimental, high-level, memory-safe, statically-typed, whitespace-sensitive, interface-oriented, imperative programming language with functional underpinnings. +Puck is an experimental, high-level, memory-safe, statically-typed, whitespace-sensitive, interface-oriented, imperative programming language with functional underpinnings. -It attempts to explore designs in making functional programming paradigms comfortable to those familiar with imperative and object-oriented languages, as well as deal with some more technical problems along the way, such as integrated refinement types and typesafe interop. +It attempts to explore designs in making functional programming paradigms comfortable to those familiar with imperative and object-oriented languages, as well as deal with some more technical problems along the way, such as integrated refinement types and typesafe interop. This is the language I keep in my head. It reflects the way I think and reason about code. I do hope others enjoy it. +## Declarations and Comments + ```puck let ident: int = 413 # type annotations are optional var phrase = "Hello, world!" -const compile_time = when linux: "linux" else: "windows" +const compile_time = when linux then "linux" else "windows" ``` Variables may be mutable (`var`), immutable (`let`), or compile-time evaluated and immutable (`const`). @@ -38,14 +40,18 @@ Documentation comments are declared with `##` and may be parsed by language serv Multi-line comments are declared with `#[ ]#` and may be nested. Taking cues from the Lisp family of languages, any expression may be commented out with a preceding `#;`. +## Functions and Indentation + ```puck ``` -Functions are declared with the `func` keyword. They take an (optional) list of generic parameters (in brackets), an (optional) list of parameters (in parentheses), and **must** be annotated with a return type if they return a type. Every function parameter must be annotated with a type. Their type may optionally be prefixed with either `mut` or `static`: denoting a *mutable* type (types are copied into functions and thus immutable by default), or a *static* type (known to the compiler at compile time, and usable in `const` exprs). Generic parameters may each be optionally annotated with a type functioning as a _constraint_. +Functions are declared with the `func` keyword. They take an (optional) list of generic parameters (in brackets), an (optional) list of parameters (in parentheses), and **must** be annotated with a return type if they return a type. Every function parameter must be annotated with a type. Their type may optionally be prefixed with either `lent`, `mut` or `static`: denoting an immutable or mutable borrow (more on these later), or a *static* type (known to the compiler at compile time, and usable in `const` exprs). Generic parameters may each be optionally annotated with a type functioning as a _constraint_. -Whitespace is significant but flexible: functions may be declared entirely on one line if so desired. A new level of indentation after certain tokens (`:`, `=`) denotes a new level of scope. There are some places where arbitrary indentation and line breaks are allowed - as a general rule of thumb, after operators, commas, and opening parentheses. The particular rules governing indentation may be found in the [syntax guide](SYNTAX.md#indentation-rules). +Whitespace is significant but flexible: functions may be declared entirely on one line if so desired. A new level of indentation after certain tokens (`=`, `do`, `then`) denotes a new level of scope. There are some places where arbitrary indentation and line breaks are allowed - as a general rule of thumb, after operators, commas, and opening parentheses. The particular rules governing indentation may be found in the [syntax guide](SYNTAX.md#indentation-rules). + +## Uniform Function Call Syntax ```puck func inc(self: list[int], by: int): list[int] = @@ -60,6 +66,8 @@ Puck supports *uniform function call syntax*: and so any function may be called This allows for a number of syntactic cleanups. Arbitrary functions with compatible types may be chained with no need for a special pipe operator. Object field access, module member access, and function calls are unified, reducing the need for getters and setters. Given a first type, IDEs using dot-autocomplete can fill in all the functions defined for that type. Programmers from object-oriented languages may find the lack of classes more bearable. UFCS is implemented in shockingly few languages, and so Puck joins the tiny club that previously consisted of just D and Nim. +## Basic Types + ```puck ``` @@ -85,6 +93,8 @@ print phrase.last() # ✨ String concatenation uses a distinct `&` operator rather than overloading the `+` operator (as the complement `-` has no natural meaning for strings). Strings are unified, mutable, internally a byte array, externally a char array, and are stored as a pointer to heap data after their length and capacity (fat pointer). Chars are four bytes and represent a Unicode character in UTF-8 encoding. Slices of strings are stored as a length followed by a pointer to string data, and have non-trivial interactions with the memory management system. More details can be found in the [type system overview](TYPES.md). +## Conditionals and Pattern Matching + ```puck ``` @@ -99,53 +109,60 @@ Exhaustive structural pattern matching is available with the `match`/`of` statem -The `of` statement also stands on its own as an operator for querying subtype equality. Used as a conditional in `if` statements or `while` loops, it retains the variable injection properties of its `match` counterpart. This allows it to be used as a compact alternative to `if let` statements in other languages. +The `of` statement also stands on its own as an operator for querying subtype equality. Used as a conditional in `if` statements or `while` loops, it retains the variable injection properties of its `match` counterpart. This allows it to be used as a compact and coherent alternative to `if let` statements in other languages. + +## Error Handling ```puck -func may_fail: Result[T, ref Err] +type Result[T] = Result[T, ref Err] +func may_fail: Result[T] = ... ``` -Error handling is done via a fusion of imperative `try`/`catch` statements and functional `Option`/`Result` types, with much syntactic sugar. Functions may `raise` errors, but should return `Option[T]` or `Result[T, E]` types instead by convention. The compiler will note functions that `raise` errors, and force explicit qualification of them via `try`/`catch` statements. +Error handling is done via a fusion of functional `Option`/`Result` types and imperative `try`/`catch` statements, with much syntactic sugar. Functions may `raise` errors, but by convention should return `Option[T]` or `Result[T, E]` types instead. The compiler will note functions that `raise` errors, and force explicit qualification of them via `try`/`catch` statements. -A bevy of helper functions and macros are available for `Option`/`Result` types, and are documented and available in the `std.options` and `std.results` modules (included in the prelude by default). Two in particular are of note: the `?` macro accesses the inner value of a `Result[T, E]` or propagates (returns in context) the `Error(e)`, and the `!` accesses the inner value of an `Option[T]` / `Result[T, E]` or raises an error on `None` / the specific `Error(e)`. Both operators take one parameter and so are postfix. (There is additionally another `?` postfix macro, taking in a type, as a shorthand for `Option[T]`) +A bevy of helper functions and macros are available for `Option`/`Result` types, and are documented and available in the `std.options` and `std.results` modules (included in the prelude by default). Two in particular are of note: the `?` macro accesses the inner value of a `Result[T, E]` or propagates (returns in context) the `Error(e)`, and the `!` accesses the inner value of an `Option[T]` / `Result[T, E]` or raises an error on `None` / the specific `Error(e)`. Both operators take one parameter and so are postfix. The `?` and `!` macros are overloaded and additionally function on types as shorthand for `Option[T]` and `Result[T]` respectively. The utility of the `?` macro is readily apparent to anyone who has written code in Rust or Swift. The utility of the `!` function is perhaps less so obvious. These errors raised by `!`, however, are known to the compiler: and they may be comprehensively caught by a single or sequence of `catch` statements. This allows for users used to a `try`/`catch` error handling style to do so with ease, with only the need to add one additional character to a function call. More details may be found in [error handling overview](ERRORS.md). +## Blocks and Loops + ```puck -loop: +loop print "This will never normally exit." break -for i in 0 .. 3: # exclusive - for j in 0 ..= 3: # inclusive +for i in 0 .. 3 do # exclusive + for j in 0 ..= 3 do # inclusive print "{} {}".fmt(i, j) ``` Three types of loops are available: `for` loops, `while` loops, and infinite loops (`loop` loops). For loops take a binding (which may be structural, see pattern matching) and an iterable object and will loop until the iterable object is spent. While loops take a condition that is executed upon the beginning of each iteration to determine whether to keep looping. Infinite loops are infinite are infinite are infinite are infinite are infinite are infinite and must be manually broken out of. -There is no special concept of iterators: iterable objects are any object that implements the `Iter[T]` interface (more on those in [the type system document](TYPES.md)), that is, provides a `self.next()` function returning an `Option[T]`. As such, iterators are first-class constructs. For loops can be thought of as while loops that unwrap the result of the `next()` function and end iteration upon a `None` value. While loops, in turn, can be thought of as infinite loops with an explicit conditional break. +There is no special concept of iterators: iterable objects are any object that implements the `Iter[T]` class (more on those in [the type system document](TYPES.md)), that is, provides a `self.next()` function returning an `Option[T]`. As such, iterators are first-class constructs. For loops can be thought of as while loops that unwrap the result of the `next()` function and end iteration upon a `None` value. While loops, in turn, can be thought of as infinite loops with an explicit conditional break. The `break` keyword immediately breaks out of the current loop, and the `continue` keyword immediately jumps to the next iteration of the current loop. Loops may be used in conjunction with blocks for more fine-grained control flow manipulation. ```puck -block: +block statement let x = block: let y = read_input() transform_input(y) -block foo: - for i in 0 ..= 100: - block bar: - if i == 10: break foo +block foo + for i in 0 ..= 100 do + block bar + if i == 10 then break foo print i ``` Blocks provide arbitrary scope manipulation. They may be labelled or unlabelled. The `break` keyword additionally functions inside of blocks and without any parameters will jump out of the current enclosing block (or loop). It may also take a block label as a parameter for fine-grained scope control. +## Module System + ```puck ``` @@ -153,16 +170,20 @@ Code is segmented into modules. Modules may be made explicit with the `mod` keyw A module can be imported into another module by use of the `use` keyword, taking a path to a module or modules. Contrary to the majority of languages ex. Python, unqualified imports are *encouraged* - in fact, are idiomatic (and the default) - type-based disambiguation and official LSP support are intended to remove any ambiguity. -Within a module, functions, types, constants, and other modules may be *exported* for use by other modules with the `pub` keyword. All such identifiers are private by default and only accessible module-locally without. Modules are first-class and may be bound, inspected, modified, and returned. As such, imported modules may be *re-exported* for use by other modules by binding them to a public constant, i.e. `use my_module; pub const my_module = my_module`. +Within a module, functions, types, constants, and other modules may be *exported* for use by other modules with the `pub` keyword. All such identifiers are private by default and only accessible module-locally without. Modules are first-class and may be bound, inspected, modified, and returned. As such, imported modules may be *re-exported* for use by other modules by binding them to a public constant. More details may be found in the [modules document](MODULES.md). +## Compile-time Programming + ```puck ``` Compile-time programming may be done via the previously-mentioned `const` keyword and `when` statements: or via `const` *blocks*. All code within a `const` block is evaluated at compile-time and all assignments and allocations made are propagated to the compiled binary as static data. -Further compile-time programming may be done via metaprogramming: compile-time manipulation of the abstract syntax tree. The macro system is complex, and a description may be found in the [metaprogramming document](METAPROGRAMMING.md). +Further compile-time programming may be done via metaprogramming: compile-time manipulation of the abstract syntax tree. The macro system is complex, and a description may be found in the [metaprogramming document](METAPROGRAMMING.md). + +## Async System and Threading ```puck ``` @@ -171,39 +192,91 @@ The async system is *colourblind*: the special `async` macro will turn any funct Threading support is complex and also regulated to external libraries. OS-provided primitives will likely provide a `spawn` function, and there will be substantial restrictions for memory safety. I really haven't given much thought to this. +## Memory Management + ```puck +# Differences in Puck and Rust types in declarations and at call sights. +func foo(a: + lent T → &'a T + mut T → &'a mut T + T → T +): + lent T → &'a T + mut T → &'a mut T + T → T + +let t: T = ... +foo( # this is usually elided + lent t → &t + mut t → &mut t + t → t +) ``` -Details on memory safety, references and pointers, and deep optimizations may be found in the [memory management overview](MEMORY_MANAGEMENT.md). -The memory model intertwines deeply with the type system. +Puck copies Rust-style ownership near verbatim. `&T` corresponds to `lent T`, `&mut T` to `mut T`, and `T` to `T`: with `T` implicitly convertible to `lent T` and `mut T` at call sites. A major goal of Puck is for all lifetimes to be inferred: there is no overt support for lifetime annotations, and it is likely code with strange lifetimes will be rejected before it can be inferred. (Total inference, however, *is* a goal.) Another major difference is the consolidation of `Box`, `Rc`, `Arc`, `Cell`, `RefCell` into just two (magic) types: `ref` and `refc`. `ref` takes the role of `Box`, and `refc` both the role of `Rc` and `Arc`: while `Cell` and `RefCell` are disregarded. The underlying motivation for compiler-izing these types is to make deeper compiler optimizations accessible: particularly with `refc`, where the existing ownership framework is used to eliminate counts. Details on memory safety, references and pointers, and deep optimizations may be found in the [memory management overview](MEMORY_MANAGEMENT.md). + +## Types System ```puck +# The type Foo is defined here as an alias to a list of bytes. +type Foo = list[byte] + +# implicit conversion to Foo in declarations +let foo: Foo = [1, 2, 3] + +func fancy_dbg(self: Foo) = + print "Foo:" + # iteration is defined for list[byte] + # so self is implicitly converted from Foo to list[byte] + for elem in self do + dbg(elem) + +# NO implicit conversion to Foo on calls +[4, 5, 6].foo_dbg # this fails! + +Foo([4, 5, 6]).foo_dbg # prints: Foo:\n 4\n\ 5\n 6\n +``` + +Finally, a few notes on the type system are in order. Types are declared with the `type` keyword and are aliases: all functions defined on a type carry over to its alias, though the opposite is not true. Functions defined on the alias *must* take an object known to be a type of that alias: exceptions are made for type declarations, but at call sites this means that conversion must be explicit. + +```puck +# We do not want functions defined on list[byte] to carry over, +# as strings function differently (operating on chars). +# So we declare `str` as a struct, rather than a type alias. +pub type str = struct + data: list[byte] + +# However, the underlying `empty` function is still useful. +# So we expose it in a one-liner alias. +# In the future, a `with` macro may be available to ease carryover. +pub func empty(self: str): bool = self.data.empty + +# Alternatively, if we want total transparent type aliasing, we can use constants. +pub const MyAlias: type = VeryLongExampleType ``` -Finally, a few notes on the type system are in order. +If one wishes to define a new type *without* previous methods accessible, the newtype paradigm is preferred: declaring a single-field `struct`, and manually implementing functions that carry over. It can also be useful to have *transparent* type aliases, that is, simply a shorter name to refer to an existing type. These do not require type conversion, implicit or explicit, and can be used freely and interchangeably with their alias. This is done with constants. -Types are declared with the `type` keyword and are transparent aliases. -That is, `type Foo = Bar` means that any function defined for `Bar` is defined for `Foo` - that is, objects of type `Foo` can be used any time an object of type `Bar` is called for. -If such behavior is not desired, the `distinct` keyword forces explicit qualification and conversion of types. `type Foo = distinct Baz` will force a type `Foo` to be wrapped in a call to the constructor `Baz()` before being passed to such functions. +Types, like functions, can be *generic*: declared with "holes" that may be filled in with other types upon usage. A type must have all its holes filled before it can be constructed. The syntax for generics in types much resembles the syntax for generics in functions, and generic *constraints* and the like also apply. -Types, like functions, can be *generic*: declared with "holes" that may be filled in with other types upon usage. A type must have all its holes filled before it can be constructed. The syntax for generics in types much resembles the syntax for generics in functions, and *constraints* and the like also apply. +## Structs and Tuples ```puck type MyStruct = struct a: str b: str -type MyTuple = tuple[str, b: str] +type MyTuple = (str, b: str) let a: MyTuple = ("hello", "world") print a.1 # world print a.b # world ``` -Struct and tuple types are declared with `struct[]` and `tuple[]`, respectively. Their declarations make them look similar at a glance, but they differ fairly fundamentally. Structs are *unordered*, and every field must be named. They may be constructed with `{}` brackets. Tuples are *ordered* and so field names are optional - names are just syntactic sugar for positional access. Tuples may be constructed with `()` parenthesis. +Struct and tuple types are declared with `struct[]` and `tuple[]`, respectively. Their declarations make them look similar at a glance, but they differ fairly fundamentally. Structs are *unordered*, and every field must be named. They may be constructed with `{}` brackets. Tuples are *ordered* and so field names are optional - names are just syntactic sugar for positional access (`foo.0`, `bar.1`, ...). Tuples are constructed with `()` parentheses: and also may be *declared* with such, as syntactic sugar for `tuple[...]`. -I am undecided whether to allow *structural subtyping*: that is, `{a: Type, b: Type, c: Type}` being valid in a context expecting `{a: Type, b: Type}`. This has benefits (multiple inheritance with no boilerplate) but also downsides (obvious). +It is worth noting that there is no concept of `pub` at a field level on structs - a type is either fully transparent, or fully opaque. This is because such partial transparency breaks with structural initialization (how could one provide for hidden fields?). However, the `@[opaque]` attribute allows for expressing that the internal fields of a struct are not to be accessed or initialized: this, however, is only a compiler warning and can be totally suppressed with `@[allow(opaque)]`. -It is worth noting that there is no concept of `pub` at a field level on structs - a type is either fully transparent, or fully opaque. This is because such partial transparency breaks with structural initialization (how could one provide for hidden fields?). An idiomatic workaround is to model the desired field structure with a public-facing interface. +## Unions and Enums ```puck type Expr = union @@ -219,19 +292,26 @@ Union types are the bread and butter of structural pattern matching. Composed wi This is often useful as an idiomatic and safer replacement for inheritance. ```puck -pub type Iter[T] = interface +``` + +Enum types are similarly composed of a list of *variants*. These variants, however, are static values: assigned at compile-time, and represented under the hood by a single integer. They function similarly to unions, and can be passed through to functions and pattern matched upon, however their underlying simplicity and default values mean they are much more useful for collecting constants and acting as flags than anything else. + +## Classes + +```puck +pub type Iter[T] = class next(mut Self): T? -pub type Peek[T] = interface +pub type Peek[T] = class next(mut Self): T? peek(mut Self): T? peek_nth(mut Self, int): T? ``` -Interface types function much as type classes in Haskell or traits in Rust do. They are not concrete types, and cannot be constructed - instead, their utility is via indirection, as parameters or as `ref` types, providing constraints that some concrete type must meet. They consist of a list of function signatures, implementations of which must exist for the given type in order to compile. +Class types function much as type classes in Haskell or traits in Rust do. They are not concrete types, and cannot be constructed - instead, their utility is via indirection, as parameters in functions or as `ref` types in structures, providing constraints that some concrete type must meet. They consist of a list of function signatures, implementations of which must exist for the given type passed in in order to compile. -Their major difference, however, is that Puck's interfaces are *implicit*: there is no `impl` block that implementations of their associated functions have to go under. If functions for a concrete type exist satisfying some interface, the type implements that interface. This does run the risk of accidentally implementing an interface one does not desire to, but the author believes such situations are few and far between, well worth the decreased syntactic and semantic complexity, and mitigatable with tactical usage of the `distinct` keyword. +Their major difference, however, is that Puck's classes are *implicit*: there is no `impl` block that implementations of their associated functions have to go under. If functions for a concrete type exist satisfying some class, the type implements that class. This does run the risk of accidentally implementing a class one does not desire to, but the author believes such situations are few and far between and well worth the decreased syntactic and semantic complexity. As a result, however, classes are entirely unable to guarantee any invariants hold (like `PartialOrd` or `Ord` in Rust do). -As the compiler makes no such distinction between fields and single-argument functions on a type when determining identifier conflicts, interfaces similarly make no such distinction. They *do* distinguish mutable and immutable parameters, those being part of the type signature. +As the compiler makes no such distinction between fields and single-argument functions on a type when determining identifier conflicts, classes similarly make no such distinction. They *do* distinguish borrowed/mutable/owned parameters, those being part of the type signature. -Interfaces are widely used throughout the standard library to provide general implementations of such conveniences like iteration, debug and display printing, generic error handling, and much more. +Classes are widely used throughout the standard library to provide general implementations of such conveniences like iteration, debug and display printing, generic error handling, and much more. diff --git a/docs/SYNTAX.md b/docs/SYNTAX.md index 1bd3331..6561acb 100644 --- a/docs/SYNTAX.md +++ b/docs/SYNTAX.md @@ -6,18 +6,18 @@ The following keywords are reserved: - variables: `let` `var` `const` -- control flow: `if` `elif` `else` +- control flow: `if` `then` `elif` `else` - pattern matching: `match` `of` -- loops: `loop` `while` `for` `in` -- blocks: `block` `break` `continue` `return` -- functions: `func` `mut` `static` `varargs` -- modules: `pub` `mod` `use` `as` - error handling: `try` `catch` `finally` -- metaprogramming: `macro` `quote` `when` -- types: `type` `distinct` `ref` -- types: `struct` `tuple` `union` `enum` `interface` +- loops: `while` `do` `for` `in` +- blocks: `loop` `block` `break` `continue` `return` +- modules: `pub` `mod` `use` `as` +- functions: `func` `varargs` +- metaprogramming: `macro` `quote` `static` `when` +- ownership: `lent` `mut` `ref` `refc` +- types: `type` `distinct` `struct` `tuple` `union` `enum` `class` - reserved: - - `impl` `object` `class` `concept` `auto` `empty` `effect` `case` + - `impl` `object` `interface` `concept` `auto` `empty` `effect` `case` - `suspend` `resume` `spawn` `pool` `thread` `closure` - `cyclic` `acyclic` `sink` `move` `destroy` `copy` `trace` `deepcopy` @@ -37,24 +37,25 @@ The following identifiers are in use by the standard prelude: The following punctuation is taken: - `=` (assignment) - `.` (chaining) -- `,` (params) +- `,` (parameters) - `;` (statements) - `:` (types) - `#` (comment) +- `@` (attributes) - `_` (unused bindings) - `|` (generics) - `\` (string/char escaping) -- `()` (params, tuples) -- `{}` (scope, structs) +- `()` (parameters, tuples) - `[]` (generics, lists) +- `{}` (scope, structs) - `""` (strings) - `''` (chars) - ``` `` ``` (unquoting) -- unused: `~` `@` `$` `%` +- unused: `~` `$` `%` ## A Formal Grammar -We now shall take a look at a more formal description of Puck's syntax. +We now shall take a look at a more formal description of Puck's syntax. Syntax rules are described in [extended Backus–Naur form](https://en.wikipedia.org/wiki/Extended_Backus–Naur_form) (EBNF): however, most rules surrounding whitespace, and scope, and line breaks, are modified to how they would appear after a lexing step. @@ -105,61 +106,64 @@ Struct ::= '{' Ident ':' Expr (',' Ident ':' Expr)* '}' ### Variables ``` Decl ::= Let | Var | Const | Func | Type -Let ::= 'let' Pattern Annotation? '=' Expr -Var ::= 'var' Pattern Annotation? ('=' Expr)? -Const ::= 'pub'? 'const' Pattern Annotation? '=' Expr +Let ::= 'let' Pattern (':' Type)? '=' Expr +Var ::= 'var' Pattern (':' Type)? ('=' Expr)? +Const ::= 'pub'? 'const' Pattern (':' Type)? '=' Expr Pattern ::= Char | String | Number | Float | Ident | '(' Pattern (',' Pattern)* ')' Ident '(' Pattern (',' Pattern)* ')' ``` ### Declarations ``` -Func ::= 'pub'? 'func' Ident Generics? Parameters? Annotation? '=' Body -Macro ::= 'pub'? 'macro' Ident Generics? Parameters? Annotation? '=' Body -Generics ::= '[' Ident Annotation? (',' Ident Annotation?)* ']' -Parameters ::= '(' Ident Annotation? (',' Ident Annotation?)* ')' -Annotation ::= ':' Type +Func ::= 'pub'? 'func' Ident Generics? Parameters? (':' Type)? '=' Body +Macro ::= 'pub'? 'macro' Ident Generics? Parameters? (':' Type)? '=' Body +Generics ::= '[' Ident (':' Type)? (',' Ident (':' Type)?)* ']' +Parameters ::= '(' Ident (':' Type)? (',' Ident (':' Type)?)* ')' ``` +All arguments to functions must have a type. This is resolved at the semantic level, however. +(Arguments to macros may lack types. This signifies a generic node.) + ### Types ``` TypeDecl ::= 'pub'? 'type' Ident Generics? '=' Type -Type ::= StructType | TupleType | EnumType | UnionType | Interface | - (('distinct' | 'ref' | 'ptr' | 'mut' | 'static') (Type | ('[' Type ']'))?) -StructType ::= 'struct' ('[' Ident ':' Type (',' Ident ':' Type)* ']')? -UnionType ::= 'union' ('[' Ident ':' Type (',' Ident ':' Type)* ']')? -TupleType ::= 'tuple' ('[' (Ident ':')? Type (',' (Ident ':')? Type)* ']')? -EnumType ::= 'enum' ('[' Ident ('=' Expr)? (',' Ident ('=' Expr)?)* ']')? -Interface ::= 'interface' ('[' Signature (',' Signature)* ']')? -Signature ::= Ident Generics? ('(' Type (',' Type)* ')')? Annotation? +Type ::= TypeStruct | TypeTuple | TypeEnum | TypeUnion | TypeClass | + (Modifier* (Type | ('[' Type ']'))) +TypeStruct ::= 'struct' ('[' Ident ':' Type (',' Ident ':' Type)* ']')? +TypeUnion ::= 'union' ('[' Ident ':' Type (',' Ident ':' Type)* ']')? +TypeTuple ::= 'tuple' ('[' (Ident ':')? Type (',' (Ident ':')? Type)* ']')? +TypeEnum ::= 'enum' ('[' Ident ('=' Expr)? (',' Ident ('=' Expr)?)* ']')? +TypeClass ::= 'class' ('[' Signature (',' Signature)* ']')? +Modifier ::= 'distinct' | 'ref' | 'refc' | 'ptr' | 'lent' | 'mut' | 'static' +Signature ::= Ident Generics? ('(' Type (',' Type)* ')')? (':' Type)? ``` ## Control Flow ``` -If ::= 'if' Expr ':' Body ('elif' Expr ':' Body)* ('else' ':' Body)? -When ::= 'when' Expr ':' Body ('elif' Expr ':' Body)* ('else' ':' Body)? -Try ::= 'try' ':' Body - ('except' Ident ('as' Ident)? (',' Ident ('as' Ident)?)*) ':' Body)* - ('finally' ':' Body)? -Match ::= 'match' Expr ('of' Pattern (',' Pattern)* ('where' Expr)? ':' Body)+ -Block ::= 'block' Ident? ':' Body -Block ::= 'static' ':' Body -Loop ::= 'loop' ':' Body -While ::= 'while' Expr ':' Body -For ::= 'for' Pattern 'in' Expr Body +If ::= 'if' Expr 'then' Body ('elif' Expr 'then' Body)* ('else' Body)? +When ::= 'when' Expr 'then' Body ('elif' Expr 'then' Body)* ('else' Body)? +Try ::= 'try' Body + ('except' Ident ('as' Ident)? (',' Ident ('as' Ident)?)*) 'then' Body)+ + ('finally' Body)? +Match ::= 'match' Expr ('of' Pattern (',' Pattern)* ('where' Expr)? 'then' Body)+ +While ::= 'while' Expr 'do' Body +For ::= 'for' Pattern 'in' Expr 'do' Body +Loop ::= 'loop' Body +Block ::= 'block' Ident? Body +Static ::= 'static' Body +Quote ::= 'quote' QuoteBody ``` ## Modules ``` -Mod ::= 'pub'? 'mod' Ident ':' Body -Use ::= 'use' Ident ('/' Ident)* ('/' ('[' Ident (',' Ident)* ']'))? +Mod ::= 'pub'? 'mod' Ident Body +Use ::= 'use' Ident ('.' Ident)* ('.' ('[' Ident (',' Ident)* ']'))? ``` ### Operators ``` Operator ::= 'and' | 'or' | 'not' | 'xor' | 'shl' | 'shr' | - 'div' | 'mod' | 'rem' | 'is' | 'in' | - Opr+ + 'div' | 'mod' | 'rem' | 'is' | 'in' | Opr+ Opr ::= '=' | '+' | '-' | '*' | '/' | '<' | '>' | '@' | '$' | '~' | '&' | '%' | '|' | '!' | '?' | '^' | '.' | ':' | '\\' @@ -170,10 +174,10 @@ Opr ::= '=' | '+' | '-' | '*' | '/' | '<' | '>' | Call ::= Ident ('[' Call (',' Call)* ']')? ('(' (Ident '=')? Call (',' (Ident '=')? Call)* ')')? | Ident Call (',' Call)* | Call Operator Call? | - Call ':' Body + Call Body Expr ::= Let | Var | Const | Func | Type | Mod | Use | Block | Static | For | While | Loop | If | When | Try | Match | Call -Body ::= Expr | ('{' Expr (';' Expr)* '}') +Body ::= Expr | (Expr (';' Expr)*) ``` --- -- cgit v1.2.3-70-g09d2