aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJJ2024-01-28 09:05:03 +0000
committerJJ2024-01-28 09:07:00 +0000
commitba8ce3875d09b88463da76148ba5d563049b089f (patch)
tree73868472e6ba7a948fadf49e12362c500cef1843
parent6da57c3a2c5c222591b0994acce79183b81d7f99 (diff)
docs: complete MODULES.md. minor updates elsewhere. prepare for hosting
-rw-r--r--book.toml6
-rw-r--r--docs/ASYNC.md6
-rw-r--r--docs/ERRORS.md42
-rw-r--r--docs/MODULES.md43
-rw-r--r--docs/OVERVIEW.md35
-rw-r--r--docs/SUMMARY.md23
-rw-r--r--docs/SYNTAX.md47
7 files changed, 117 insertions, 85 deletions
diff --git a/book.toml b/book.toml
new file mode 100644
index 0000000..f69aa8b
--- /dev/null
+++ b/book.toml
@@ -0,0 +1,6 @@
+[book]
+authors = ["JJ"]
+language = "en"
+multilingual = false
+src = "docs"
+title = "The Puck Programming Language"
diff --git a/docs/ASYNC.md b/docs/ASYNC.md
index ec610ca..87c602d 100644
--- a/docs/ASYNC.md
+++ b/docs/ASYNC.md
@@ -29,11 +29,15 @@ pub func await[T](self: Future[T]): T =
This implementation differs from standard async/await implementations quite a bit.
In particular, this means there is no concept of an "async function" - any block of computation that resolves to a value can be made asynchronous. This allows for "anonymous" async functions, among other things.
+This (packaging up blocks of code to suspend and resume arbitrarily) is *hard*, and requires particular portable intermediate structures out of the compiler. Luckily, Zig is doing all of the R&D here. Some design decisions to consider revolve around *APIs*. The Linux kernel interface (among other things) provides both synchronous and asynchronous versions of its API, and fast code will use one or the other, depending if it is in an async context. Zig works around this by way of a known global constant that low-level functions read at compile time to determine whether to operate on synchronous APIs or asynchronous APIs. This is... not great. But what's better?
+
<!-- Asynchronous programming is hard to design and hard to use. Even Rust doesn't do a great job. It *shouldn't* need built-in language support - we should be able to encode it as a type and provide any special syntax via macros. Note that async is not just threading! threading is solved well by Rust's rayon and Go's (blugh) goroutines. -->
## Threading
-How threads work deserves somewhat of a mention. todo
+It should be noted that async is *not* the same as threading, *nor* is it solely useful in the presence of threads...
+
+How threads work deserves somewhat of a mention...
References:
- [What color is your function?](https://journal.stuffwithstuff.com/2015/02/01/what-color-is-your-function/)
diff --git a/docs/ERRORS.md b/docs/ERRORS.md
index 4f99e7c..33c15d5 100644
--- a/docs/ERRORS.md
+++ b/docs/ERRORS.md
@@ -1,23 +1,6 @@
# Error Handling
-Puck's error handling is shamelessly stolen from Swift.
-It uses a combination of Option/Result types and try/catch/finally statements, and leans somewhat on Puck's metaprogramming capabilities.
-
-```puck
-func get_debug[T](): T =
- let value: Option[T] = self.unsafe_get(413)
- try:
- let value = value!
- catch Exception(e)
-
-
-try:
- ..
-catch:
- ..
-finally:
- print "No such errors"
-```
+Puck's error handling is shamelessly stolen from Swift. It uses a combination of `Option`/`Result` types and `try`/`catch` statements, and leans somewhat on Puck's metaprogramming capabilities.
There are several ways to handle errors in Puck. If the error is encoded in the type, one can:
1. `match` on the error
@@ -25,10 +8,9 @@ There are several ways to handle errors in Puck. If the error is encoded in the
3. propagate the error with `?`
4. throw the error with `!`
-If an error is thrown, one must explicitly handle (or disregard) it with a `try/catch` block.
-This method of error handling may feel more familiar to Java programmers.
+If an error is thrown, one **must** explicitly handle (or disregard) it with a `try/catch` block or risk runtime failure. This method of error handling may feel more familiar to Java programmers.
-## Errors as monads
+## Errors as Monads
Puck provides [`Option[T]`](std/default/options.pk) and a [`Result[T, E]`](std/default/results.pk) types, imported by default. These are `union` types and so must be pattern matched upon to be useful: but the standard library provides [a bevy of helper functions](std/default/results.pk).
Two in particular are of note. The `?` operator unwraps a Result or propagates its error up a function call (and may only be used in type-appropriate contexts). The `!` operator unwraps an Option or Result directly or throws an exception in the case of None or Error.
@@ -56,12 +38,12 @@ The utility of the provided helpers in [`std.options`](std/default/options.pk) a
A notable helpful type is the aliasing of `Result[T]` to `Result[T, ref Err]`, for when the particular error does not matter. This breaks `try`/`catch` exhaustion (as `ref Err` denotes a reference to *any* Error), but is particularly useful when used in conjunction with the propagation operator.
-## Errors as catchable exceptions
+## Errors as Catchable Exceptions
Errors raised by `raise`/`throw` (or subsequently the `!` operator) must be explicitly caught and handled via a `try`/`catch`/`finally` statement.
If an exception is not handled within a function body, the function must be explicitly marked as a throwing function via the `yeet` prefix (name to be determined). The compiler will statically determine which exceptions in particular are thrown from any given function, and enforce them to be explicitly handled or explicitly ignored.
-Errors are 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.
+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:
@@ -76,7 +58,19 @@ This creates a distinction between two types of error handling, working in sync:
<!-- [nullable types](https://en.wikipedia.org/wiki/Nullable_type)?? -->
-## Unrecoverable exceptions
+## Errors and Void Functions
+
+Some functions do not return a value but can still fail: for example, setters.
+This can make it difficult to do monadic error handling elegantly: one could return a `Result[void, E]`, but...
+
+```puck
+pub func set[T](self: list[T], i: uint, val: T) =
+ if i > self.length:
+ raise IndexOutOfBounds
+ self.data.raw_set(offset = i, val)
+```
+
+## Unrecoverable Exceptions
There exist errors from which a program can not reasonably recover. These are the following:
- `Assertation Failure`: a call to an `assert` function has returned false at runtime.
diff --git a/docs/MODULES.md b/docs/MODULES.md
index 4f5bb70..bbce909 100644
--- a/docs/MODULES.md
+++ b/docs/MODULES.md
@@ -1,50 +1,37 @@
# Modules and Namespacing
-Puck has a rich module system, inspired by such expressive systems in the ML family of languages, notably including Rust and OCaml. Unlike these systems, however, opening modules i.e. unqualified imports is *encouraged* - even in the global scope - which at first would appear to run contrary to the point of a module system. Such "namespace pollution" is made a non-issue by **type-based disambiguation** (and can be avoided regardless with qualified imports anyway).
+Puck has a first-class module system, inspired by such expressive designs in the ML family.
-A major goal of Puck's module system is to allow the same level of expressiveness as the ML family - while cutting down on the extraneous syntax and boilerplate needed to do so. As such, access modifiers are written directly inline with their declaration, and the file system structure is reused to form an implicit module system for internal use.
+## Using Modules
```puck
-import std/[ascii, unicode]
-
-todo
```
-Type-based disambiguation allows for the simultaneous seamless existence of functions with the same name and different type signature: and further, the usage of multiple functions with identical signatures and multiple types with identical names through explicit disambiguation. This is nothing fancy and follows some simple scoping rules:
-- A module cannot define two constants with the same name.
-- A module cannot define two functions with the same type signature.
-- A module cannot define two types with the same name and generic parameter count.
-- A module cannot define a constant and a zero-parameter function with the same name.
-- A module cannot define an enum and a function with the same name. (note: to be weakened)
+Modules package up code for use by others. Identifiers known at compile time may be part of a *module signature*: these being constants, functions, macros, types, and other modules themselves. They may be made accessible to external users by prefixing them with the `pub` keyword. Files are modules, named with their filename. The `mod` keyword followed by an identifier and an indented block of code explicitly defines a module, inside of the current module. Modules are first class: they may be bound to constants (having the type `: mod`) and publicly exported, or bound to local variables and passed into functions for who knows what purpose.
-These unqualified imports by default may seem like madness to a Python or C programmer: but because Puck is *statically typed*, there is no ambiguity, and because of *uniform function call syntax*, it is usually clear what overloaded function is being called. Puck is also written with the modern tooling approach of language servers in mind: and so, navigating to the function declaration, in any IDE, should be two clicks or a keystroke away.
+The `use` keyword lets you use other modules. The `use` keyword imports public symbols from the specified module into the current scope *unqualified*. This runs contrary to expectations coming from most other languages: from Python to Standard ML, the standard notion of an "import" usually puts the imported symbols behind another symbol to avoid "polluting the namespace". As Puck is strongly typed and allows overloading, however, the author sees no reason for namespace pollution to be of concern. These unqualified imports have the added benefit of making uniform function call syntax more widely accessible. It is inevitable that identifier conflicts will exist on occasion, of course: when this happens, the compiler will force qualification (this then does restrict uniform function call syntax).
```puck
```
-Multiple modules can be imported in the same scope, and so conflicts may arise on imports. In the pursuit of *some* explicitness, no attempt is made to guess the proper identifier from usage. These must be disambiguated by prefixing the module name, followed by a dot. This disambiguation breaks uniform function call syntax on functions: yet because functions only conflict when both their name and entire function signature overlap, this is a rare occurrence. If so desired, an import followed by `/[]` will force full qualification of all identifiers in the module - yet this is an antipattern and not recommended.
-
-Unrelated to the module system - but note that functions only differing in return type are allowed, albeit discouraged. Extra type annotations may be needed for the compiler to properly infer in such cases.
+Nonetheless, if qualification of imports is so desired, an alternative approach is available - binding a module to a constant. Both the standard library and external libraries are available behind identifiers without use of `use`: `std` and `lib`, respectively. (FFI and local modules will likely use more identifiers, but this is not hammered out yet.) A submodule - for example, `std.net` - may be bound in a constant as `const net = std.net`, providing all of the modules' public identifiers for use, as fields of the constant `net`. We will see this construction to be extraordinarily helpful in crafting high-level public APIs for libraries later on.
-```puck
-```
+Multiple modules can be imported at once, i.e. `use std.[logs, tests]`, `use lib.crypto, lib.http`. The standard namespaces (`std`, `lib`) deserve more than a passing mention. There are several of these: `std` for the standard library, `lib` for all external libraries, `crate` for the top-level namespace of a project (subject to change), `this` for the current containing module (subject to change)... In addition: there are a suite of *language* namespaces, for FFI - `rust`, `nim`, and `swift` preliminarily - that give access to libraries from other languages. Recall that imports are unqualified - so `use std` will allow use of the standard library without the `std` qualifier (not recommended: several modules have common names), and `use lib` will dump every library it can find into the global namespace (even less recommended).
-This document has talked quite a lot about modules so far - with no mention so far of what modules actually *are*.
+## Implicit Modules
-At their basic level: modules are a scope, defined by the `module` keyword followed by a label, a colon, and the module body. As such, modules may contain other modules, and further identifiers within a nested module are not imported by importing the outer module by default. The `module` keyword itself, however, will often go fairly unused: because an implicit form of modules are **files** in the **filesystem structure**.
+A major goal of Puck's module system is to allow the same level of expressiveness as the ML family, while cutting down on the extraneous syntax and boilerplate needed to do so. As such, access modifiers are written directly inline with their declaration, and the file system structure is reused to form an implicit module system for internal use. This - particularly the former - *limits* the structure a module can expose at first glance, but we will see later that interfaces recoup much of this lost specificity.
-The file structure forms an implicit, internal API, addressable by import statements using `..` and the previously-seen `module/submodule` syntax. Paths may be relative with `..` or absolute from the project root with `/`. Moving and renaming files will break imports, of course, so some care must be taken while refactoring. This will hopefully be a non-issue with the aid of lightweight tooling.
+We mentioned that the filesystem forms an implicit module structure. This begets a couple of design choices. Module names **must** be lowercase, for compatibility with case-insensitive filesystems. Both a file and a folder with the same name can exist. Files within the aforementioned folder are treated as submodules of the aforementioned file. This again restricts the sorts of module structures we can build, but we will again see later that this restriction can be bypassed.
-```puck
-```
+The `this` and `crate` modules are useful for this implicit structure...
-When linked as a library, however, the file structure is not visible - and so one must define an "external API", in the main file of the library. This is aided by the `export` statement which can be used to re-export imported modules as submodules of the existing module. Such an external API allows for significant flexibility in defining the structure of a library. Exports may be of a full module (`export module`), effectively merging it with the current module, or of individual items within the external module `export module/[one, two, three]`. Such exports may be placed within a new `module` scope for even more flexibility.
+## Defining Interfaces
-Modules do not export everything in their scope: indeed, all identifiers within a module are **private by default**, and must be explicitly marked public by use of the `pub` keyword. Some languages allow for individual fields of structs to be marked public, in better support of encapsulation. Unfortunately, this fundamentally breaks structural typing for a variety of reasons, and so is not supported. Only a binary distinction between fully visible and fully opaque types is allowed. Identifiers from imported modules are not considered part of the current module unless explicitly exported, of course.
+...
-Modules and identifiers from modules may be imported and exported `as` a different name.
+## Defining an External API
-```puck
-```
+The filesystem provides an implicit module structure, but it may not be the one you want to expose to users.
-todo: explore the relation between module systems and typeclasses. are hot-swappable types in modules (i.e. self Self) worth it, or would such a thing be better accomplished by `int`, `string`, etc being typeclasses?
+...
diff --git a/docs/OVERVIEW.md b/docs/OVERVIEW.md
index df4b22f..f6d95b7 100644
--- a/docs/OVERVIEW.md
+++ b/docs/OVERVIEW.md
@@ -1,6 +1,10 @@
# An Overview of Puck
-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 cross-language interop. Primarily, however, this is the language I keep in my head. It reflects the way I think and reason about code.
+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.
+
+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.
@@ -35,17 +39,6 @@ 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 `#;`.
```puck
-func reverse(s: str): str =
- let half_len = s.len div 2
- s.get(half_len, s.len)!.reverse() & s.get(half_len, s.len)!.reverse()
-
-pub func...
-
-# May fail! `yeet` denotes functions that can throw
-pub yeet func pretty_print[T](value: T) =
- # the ! converts optionals into throwing exceptions
- print!(typeof(value))
- 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 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_.
@@ -55,6 +48,12 @@ Functions are declared with the `func` keyword. They take an (optional) list of
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).
```puck
+func inc(self: list[int], by: int): list[int] =
+ self.map(x => x + by)
+
+print inc([1, 2, 3], len("four")) # 5, 6, 7
+print [1, 2, 3].inc(1) # 2, 3, 4
+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.)
@@ -116,7 +115,12 @@ More details may be found in [error handling overview](ERRORS.md).
```puck
loop:
- break
+ print "This will never normally exit."
+ break
+
+for i in 0 .. 3: # exclusive
+ for j in 0 ..= 3: # 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.
@@ -161,10 +165,6 @@ Compile-time programming may be done via the previously-mentioned `const` keywor
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).
```puck
-func await(promise: Promise)
-pub async func
-
-await
```
The async system is *colourblind*: the special `async` macro will turn any function *call* returning a `T` into an asynchronous call returning a `Future[T]`. The special `await` function will wait for any `Future[T]` and return a `T` (or an error). Async support is included in the standard library in `std.async` in order to allow for competing implementations. More details may be found in the [async document](ASYNC.md).
@@ -196,6 +196,7 @@ type MyTuple = tuple[str, b: str]
let a: MyTuple = ("hello", "world")
print a.1 # world
+print a.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. Tuples may be constructed with `()` parenthesis.
diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md
index 35374e8..8a4a530 100644
--- a/docs/SUMMARY.md
+++ b/docs/SUMMARY.md
@@ -15,7 +15,7 @@
- [Async and Threading]()
- [Advanced Types]()
- [Syntax](SYNTAX.md)
- - [Indentation Rules]()
+ - [Indentation Rules [todo]]()
- [Reserved Keywords]()
- [A Formal Grammar]()
- [Type System](TYPES.md)
@@ -24,21 +24,26 @@
- [Reference Types]()
- [Abstract Types]()
- [Advanced Types]()
-- [Module System [todo]](MODULES.md)
+- [Module System](MODULES.md)
+ - [Using Modules]()
- [Implicit Modules]()
- - [Namespacing]()
- - [Interfaces, Again]()
+ - [Defining Module Interfaces [todo]]()
+ - [Defining an External API [todo]]()
- [Error Handling](ERRORS.md)
+ - [Errors as Monads]()
+ - [Errors as Catchable Exceptions]()
+ - [Errors and Void Functions [todo]]()
+ - [Unrecoverable Exceptions]()
- [Async System](ASYNC.md)
- - [Threading]()
+ - [Threading [todo]]()
- [Metaprogramming](METAPROGRAMMING.md)
-- [Memory Management [todo]](MEMORY_MANAGEMENT.md)
+- [Memory Management [todo]]()
- [Reference Counting Optimizations]()
- [Annotations and Ownership]()
- [Language Interop [draft]](INTEROP.md)
- [Rust, Swift, Nim]()
- [Java, Kotlin]()
- [Python, Racket, C]()
-- [Refinement Types [draft]](REFINEMENTS.md)
-- [Dependent Types [draft]](DEPENDENT_TYPES.md)
-- [Effects System [draft]](EFFECTS.md)
+- [Refinement Types [draft]]()
+- [Dependent Types [draft]]()
+- [Effects System [draft]]()
diff --git a/docs/SYNTAX.md b/docs/SYNTAX.md
index a1c8bfb..1bd3331 100644
--- a/docs/SYNTAX.md
+++ b/docs/SYNTAX.md
@@ -1,5 +1,7 @@
# Syntax: A Casual and Formal Look
+> ! This section is **incomplete**. Proceed with caution.
+
## Reserved Keywords
The following keywords are reserved:
@@ -15,13 +17,46 @@ The following keywords are reserved:
- types: `type` `distinct` `ref`
- types: `struct` `tuple` `union` `enum` `interface`
- reserved:
- - `impl` `object` `class` `concept` `auto` `empty` `effect` `case` `nil`
+ - `impl` `object` `class` `concept` `auto` `empty` `effect` `case`
- `suspend` `resume` `spawn` `pool` `thread` `closure`
- `cyclic` `acyclic` `sink` `move` `destroy` `copy` `trace` `deepcopy`
+The following identifiers are in use by the standard prelude:
+- logic: `not` `and` `or` `xor` `shl` `shr` `div` `mod` `rem`
+- logic: `+` `-` `*` `/` `<` `>` `<=` `>=` `==` `!=` `is`
+- async: `async` `await`
+- types: `int` `uint` `float`
+ - `i8` `i16` `i32` `i64` `i128`
+ - `u8` `u16` `u32` `u64` `u128`
+ - `f32` `f64` `f128`
+ - `dec64` `dec128`
+- types: `bool` `byte` `char` `str`
+- types: `void` `never`
+- strings: `&` (string append)
+
+The following punctuation is taken:
+- `=` (assignment)
+- `.` (chaining)
+- `,` (params)
+- `;` (statements)
+- `:` (types)
+- `#` (comment)
+- `_` (unused bindings)
+- `|` (generics)
+- `\` (string/char escaping)
+- `()` (params, tuples)
+- `{}` (scope, structs)
+- `[]` (generics, lists)
+- `""` (strings)
+- `''` (chars)
+- ``` `` ``` (unquoting)
+- unused: `~` `@` `$` `%`
+
## A Formal Grammar
-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.
+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.
### Identifiers
```
@@ -144,7 +179,7 @@ Body ::= Expr | ('{' Expr (';' Expr)* '}')
---
References:
-- https://www.joshwcomeau.com/javascript/statements-vs-expressions/
-- https://pgrandinetti.github.io/compilers/
-- https://docs.swift.org/swift-book/ReferenceManual/LexicalStructure.html
-- https://nim-lang.github.io/Nim/manual.html
+- [Statements vs. Expressions](https://www.joshwcomeau.com/javascript/statements-vs-expressions/)
+- [Swift's Lexical Structure](https://docs.swift.org/swift-book/ReferenceManual/LexicalStructure.html)
+- [The Nim Programming Language](https://nim-lang.github.io/Nim/manual.html)
+- [Pietro's Notes on Compilers](https://pgrandinetti.github.io/compilers/)