aboutsummaryrefslogtreecommitdiff
path: root/docs/OVERVIEW.md
diff options
context:
space:
mode:
Diffstat (limited to 'docs/OVERVIEW.md')
-rw-r--r--docs/OVERVIEW.md64
1 files changed, 48 insertions, 16 deletions
diff --git a/docs/OVERVIEW.md b/docs/OVERVIEW.md
index 344f72e..97f481c 100644
--- a/docs/OVERVIEW.md
+++ b/docs/OVERVIEW.md
@@ -8,13 +8,13 @@ This is the language I keep in my head. It reflects the way I think and reason a
I do hope others enjoy it.
-## Declarations and Comments
+## Variables and Comments
```puck
let ident: int = 413
# type annotations are optional
var phrase = "Hello, world!"
-const compile_time = when linux then "linux" else "windows"
+const compile_time = std.os.file_name
```
Variables may be mutable (`var`), immutable (`let`), or compile-time evaluated and immutable (`const`).
@@ -22,7 +22,7 @@ Type annotations on variables and other bindings follow the name of the binding
Variables are conventionally written in `snake_case`. Types are conventionally written in `PascalCase`.
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
- - `i[\d+]`, `u[\d+]`: arbitrary fixed-size counterparts
+ - `i[\d]+`, `u[\d]+`: arbitrary fixed-size counterparts
- `float`, `decimal`: floating-point numbers
- `f32`/`f64`/`f128`: their fixed-size counterparts
- `dec64`/`dec128`: their fixed-size counterparts
@@ -63,7 +63,7 @@ print [1].len # 1
Puck supports *uniform function call syntax*: and so any function may be called using the typical syntax for method calls, that is, the first parameter of any function may be appended with a `.` and moved to precede it, in the style of a typical method. (There are no methods in Puck. All functions are statically dispatched. This may change in the future.)
-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.
+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 object-oriented classes more bearable. UFCS is implemented in shockingly few languages, and so Puck joins the tiny club that previously consisted of just D, Nim, Koka, and Effekt.
## Basic Types
@@ -75,7 +75,7 @@ Boolean logic and integer operations are standard and as one would expect out of
- integer division is expressed with the keyword `div` while floating point division uses `/`
- `%` is absent and replaced with distinct modulus and remainder operators
- boolean operators are bitwise and also apply to integers and floats
-- more operators are available via the standard library (`exp` and `log`)
+- more operators are available via the standard library (`std.math.exp` and `std.math.log`)
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.
@@ -117,11 +117,11 @@ type Result[T] = Result[T, ref Err]
func may_fail: Result[T] = ...
```
-Error handling is done via a fusion of functional monadic types and imperative exceptions, with much syntactic sugar. Functions may `raise` exceptions, but by convention should return `Option[T]` or `Result[T, E]` types instead: these may be handled in `match` or `if`/`of` statements. The compiler will track functions that `raise` errors, and warn on those that are not handled explicitly via `try`/`with` statements.
+Error handling is done via a fusion of functional monadic types and imperative exceptions, with much syntactic sugar. Functions may `raise` exceptions, but by convention should return `Option[T]` or `Result[T, E]` types instead: these may be handled in `match` or `if`/`of` statements. The effect system built into the compiler will track functions that `raise` errors, and warn on those that are not handled explicitly via `try`/`with` statements anywhere on the call stack.
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`/`with` error handling style to do so with ease, with only the need to add one additional character to a function call.
+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 `with` statements. This allows for users used to a `try`/`with` 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).
@@ -139,7 +139,7 @@ for i in 0 .. 3 do # exclusive
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]` 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.
+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.
@@ -147,7 +147,7 @@ The `break` keyword immediately breaks out of the current loop, and the `continu
block
statement
-let x = block:
+let x = block
let y = read_input()
transform_input(y)
@@ -176,9 +176,28 @@ More details may be found in the [modules document](MODULES.md).
## Compile-time Programming
```puck
+## Arbitrary code may execute at compile-time.
+const compile_time =
+ match std.os.platform # known at compile-time
+ of Windows then "windows"
+ of MacOS then "darwin"
+ of Linux then "linux"
+ of Wasi then "wasm"
+ of _ then "unknown platform"
+
+## The propagation operator is a macro so that `return` is injected into the function scope.
+pub macro ?[T](self: Option[T]) =
+ quote
+ match `self`
+ of Some(x) then x
+ of None then return None
+
+## Type annotations and overloading allow us to define syntactic sugar for `Option[T]`, too.
+pub macro ?(T: type) =
+ quote Option[`T`]
```
-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 macros: 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/).
+Compile-time programming may be done via the previously-mentioned `const` keyword and `when` statements: or via macros. Macros operate directly on the abstract syntax tree at compile-time: taking in syntax objects, transforming them, and returning them to be injected. They are *hygenic* and will not capture identifiers not passed as parameters. While parameters are syntax objects, they can be annotated with types to constrain applications of macros and allow for overloading. Macros are written in ordinary Puck: there is thus no need to learn a separate "macro language", as syntax objects are just standard `unions`. Additionally, support for *quoting* removes much of the need to operate on raw syntax objects. A full description may be found in the [metaprogramming document](METAPROGRAMMING.md/).
## Async System and Threading
@@ -193,6 +212,7 @@ Threading support is complex and also regulated to external libraries. OS-provid
```puck
# Differences in Puck and Rust types in declarations and at call sights.
+# note: this notation is not valid and is for illustrative purposes only
func foo(a:
lent T → &'a T
mut T → &'a mut T
@@ -212,7 +232,7 @@ foo( # this is usually elided
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).
+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 unnecessary counts. Details on memory safety, references and pointers, and deep optimizations may be found in the [memory management overview](MEMORY_MANAGEMENT.md).
## Types System
@@ -226,14 +246,14 @@ 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]
+ # so it implicitly carries over: and is defined on Foo
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
+Foo([4, 5, 6]).foo_dbg # prints: Foo: 4 5 6
```
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.
@@ -261,19 +281,25 @@ Types, like functions, can be *generic*: declared with "holes" that may be fille
## Structs and Tuples
```puck
+# standard alternative syntax to inline declarations
type MyStruct = struct
a: str
b: str
+
+# syntactic sugar for tuple[str, b: str]
type MyTuple = (str, b: str)
let a: MyTuple = ("hello", "world")
print a.1 # world
print a.b # world
+
+let c: MyStruct = {a = a.0, b = a.1}
+print c.b # 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 (`foo.0`, `bar.1`, ...). Tuples are constructed with `()` parentheses: and also may be *declared* with such, as syntactic sugar for `tuple[...]`.
+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 are both constructed and optionally *declared* with parentheses.
-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?). 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)]`.
## Unions and Enums
@@ -291,6 +317,12 @@ 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
+type Opcode = enum
+ BRK INC POP NIP SWP ROT DUP OVR EQU NEQ GTH LTH JMP JCN JSR STH JCI JMI
+ LDZ STZ LDR STR LDA STA DEI DEO ADD SUB MUL DIV AND ORA EOR SFT JSI LIT
+
+print Opcode.BRK # 0
+...
```
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.
@@ -311,6 +343,6 @@ Class types function much as type classes in Haskell or traits in Rust do. They
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, classes similarly make no such distinction. They *do* distinguish borrowed/mutable/owned 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. Structs may be described with their fields written as methods. They *do* distinguish borrowed/mutable/owned parameters, those being part of the type signature.
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.