aboutsummaryrefslogtreecommitdiff
path: root/docs/BASIC.md
diff options
context:
space:
mode:
Diffstat (limited to 'docs/BASIC.md')
-rw-r--r--docs/BASIC.md63
1 files changed, 44 insertions, 19 deletions
diff --git a/docs/BASIC.md b/docs/BASIC.md
index ee8e68f..df4b22f 100644
--- a/docs/BASIC.md
+++ b/docs/BASIC.md
@@ -14,7 +14,20 @@ const compile_time = when linux: "linux" else: "windows"
Variables may be mutable (`var`), immutable (`let`), or compile-time evaluated and immutable (`const`).
Type annotations on variables and other bindings follow the name of the binding (with `: Type`), and are typically optional.
Variables are conventionally written in `snake_case`. Types are conventionally written in `PascalCase`.
-The type system is comprehensive, and complex enough to warrant delaying covering until the end.
+The type system is comprehensive, and complex enough to warrant delaying full coverage of until the end. Some basic types are of note, however:
+- `int`, `uint`: signed and unsigned integers
+ - `i8`/`i16`/`i32`/`i64`/`i128`: their fixed-size counterparts
+ - `u8`/`u16`/`u32`/`u64`/`u128`: their fixed-size counterparts
+- `float`, `decimal`: floating-point numbers
+ - `f32`/`f64`/`f128`: their fixed-size counterparts
+ - `dec64`/`dec128`: their fixed-size counterparts
+- `byte`: an alias to `u8`, representing one byte
+- `chr`: an alias to `u32`, representing one Unicode character
+- `bool`: defined as `union[false, true]`
+- `array[T, S]`: primitive fixed-size (`S`) arrays
+- `list[T]`: dynamic lists
+- `str`: mutable strings. internally a `list[byte]`, externally a `list[chr]`
+- `slice[T]`: borrowed "views" into the three types above
Comments are declared with `#` and run until the end of the line.
Documentation comments are declared with `##` and may be parsed by language servers and other tooling.
@@ -35,9 +48,7 @@ pub yeet func pretty_print[T](value: T) =
print!(value)
```
-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 (non-generic) parameter must be annotated with a parameter. Generic parameters may be each optionally annotated with a type functioning as a _constraint_.
-
-Every function parameter must be explicitly annotated with a type. Their type may also be prefixed with `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).
+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, constants, types, and modules may be optionally prefixed with a `pub` modifier denoting visibility outside the current scope / module. More on the module system later. -->
@@ -62,9 +73,15 @@ Boolean logic and integer operations are standard and as one would expect out of
The above operations are performed with *operators*, special functions that take a prefixed first argument and (often) a suffixed second argument. Custom operators may be implemented, but they must consist of only a combination of the symbols `=` `+` `-` `*` `/` `<` `>` `@` `$` `~` `&` `%` `|` `!` `?` `^` `\` for the purpose of keeping the grammar context-free. They are are declared identically to functions.
-Term (in)equality is expressed with the `==` and `!=` operators. Type equality is expressed with `is`. Subtyping relations may be queried with `of`, which has the additional property of introducing new bindings in the current scope (more on this in the [types document](TYPES.md)). Membership of collections is expressed with `in`, and is overloaded for most types.
+Term (in)equality is expressed with the `==` and `!=` operators. Type equality is expressed with `is`. Subtyping relations may be queried with `of`, which has the additional property of introducing new bindings in the current scope (more on this in the [types document](TYPES.md)). <!-- Membership of collections is expressed with `in`, and is overloaded for most types. -->
```puck
+let phrase: str = "I am a string! Wheeee! ✨"
+for c in phrase:
+ stdout.write(c) # I am a string! Wheeee! ✨
+for b in phrase.bytes():
+ stdout.write(b.chr) # Error: cannot convert between u8 and chr
+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).
@@ -79,17 +96,17 @@ All values in Puck must be handled, or explicitly discarded. This allows for con
```puck
```
-Exhaustive structural pattern matching is available with the `match`/`of` statement, and is particularly useful for the `struct` and `union` types. Branches of a `match` statement take a *pattern*, of which the unbound identifiers within will be injected into the branch's scope. Multiple patterns may be used for one branch provided they all bind the same identifiers of the same type. Branches may be *guarded* with the `where` keyword, which takes a conditional, and will necessarily remove the branch from exhaustivity checks.
+Exhaustive structural pattern matching is available with the `match`/`of` statement, and is particularly useful for the `struct` and `union` types. `of` branches of a `match` statement take a *pattern*, of which the unbound identifiers within will be injected into the branch's scope. Multiple patterns may be used for one branch provided they all bind the same identifiers of the same type. Branches may be *guarded* with the `where` keyword, which takes a conditional, and will necessarily remove the branch from exhaustivity checks.
<!-- todo: structural matching of lists and arrays -->
-The `of` statement also stands on its own as a conditional for querying subtype equality. Used as a conditional in `if` statements, 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.
+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.
```puck
func may_fail: Result[T, ref Err]
```
-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. <!-- Those that `raise` errors or call functions that `raise` errors without handling them must additionally be explicitly marked as `yeet`. This is purely to encourage safe error handling, and is not absolute - there will likely be several builtins considered safe by compiler magic.--> <!-- todo -->
+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.
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]`)
@@ -104,7 +121,7 @@ loop:
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 Optional type. 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]` 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.
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.
@@ -172,26 +189,34 @@ If such behavior is not desired, the `distinct` keyword forces explicit qualific
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.
```puck
-let myStruct = struct
- a: int
- b: int
-let myTuple = tuple[int, b: int]
-print myTuple.1
+type MyStruct = struct
+ a: str
+ b: str
+type MyTuple = tuple[str, b: str]
+
+let a: MyTuple = ("hello", "world")
+print a.1 # world
```
Struct and tuple types are declared with `struct[<fields>]` and `tuple[<fields>]`, 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.
-Puck's type system is *structural*, and there is no better example of what this entails than with structs... todo. This allows for power at the cost of clarity, zero boilerplate multiple inheritance, etc
+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?). An idiomatic workaround is to model the desired field structure with a public-facing interface.
```puck
type Expr = union
- Variable(int)
- Abstraction()
- Application() # much better
+ Literal(int)
+ Variable(str)
+ Abstraction(param: str, body: ref Expr)
+ Application(body: ref Expr, arg: ref Expr)
```
+Union types are composed of a list of *variants*. Each variant has a *tag* and an *inner type* the union wraps over. Before the inner type can be accessed, the tag must be pattern matched upon, in order to handle all possible values. These are also known as *sum types* or *tagged unions* in other languages.
+
+Union types are the bread and butter of structural pattern matching. Composed with structs and tuples, unions provide for a very general programming construct commonly referred to as an *algebraic data type*.
+This is often useful as an idiomatic and safer replacement for inheritance.
+
```puck
pub type Iter[T] = interface
next(mut Self): T?
@@ -202,7 +227,7 @@ pub type Peek[T] = interface
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 a list of function signatures, implementations of which must exist for the given type in order to compile.
+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.
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.