From 9bbf69870ff0e857156ae2cfa36915cb21f90938 Mon Sep 17 00:00:00 2001 From: JJ Date: Mon, 20 May 2024 15:26:39 -0700 Subject: docs: add EXAMPLES.md, minor updates --- README.md | 13 +- docs/EXAMPLES.md | 774 ++++++++++++++++++++++++++++++++++++++++++++++++ docs/METAPROGRAMMING.md | 6 + docs/OVERVIEW.md | 64 +++- docs/SUMMARY.md | 53 ++-- docs/SYNTAX.md | 2 +- docs/TYPES.md | 2 +- docs/book/highlight.js | 4 +- 8 files changed, 871 insertions(+), 47 deletions(-) create mode 100644 docs/EXAMPLES.md diff --git a/README.md b/README.md index d3e9757..19283ac 100644 --- a/README.md +++ b/README.md @@ -129,11 +129,11 @@ Puck is primarily a testing ground and should not be used in any important capac Don't use it. Everything is unimplemented and it will break underneath your feet. That said: in the future, once somewhat stabilized, reasons why you *would* use it would be for: -- The **syntax**, aiming to be flexible, predictable, and succinct, through the use of *uniform function call syntax* and significant whitespace +- The **syntax**, aiming to be flexible, predictable, and succinct, through the use of *uniform function call syntax*, significant whitespace, and consistent scoping rules - The **type system**, being modern and powerful with a strong emphasis on safety, algebraic data types, optional and result types, first-class functions, generics, interfaces, and modules - The **memory management system**, implementing a model of strict ownership with an optimized reference counting escape hatch - The **metaprogramming**, providing integrated macros capable of rewriting the abstract syntax tree before or after typechecking -- The **interop system**, allowing foreign functions to be usable with native semantics from a bevy of languages +- The **interop system**, allowing foreign functions to be usable with native syntax/semantics from a bevy of other languages This is the language I keep in my head. It sprung from a series of unstructured notes I kept on language design, that finally became something more comprehensive in early 2023. The overarching goal is to provide a language capable of elegantly expressing any problem, and explore ownership and interop along the way. @@ -141,10 +141,11 @@ This is the language I keep in my head. It sprung from a series of unstructured - The [basic usage](docs/BASIC.md) document lays out the fundamental semantics of Puck. - The [syntax](docs/SYNTAX.md) document provides a deeper and formal look into the grammar of Puck. -- The [type system](docs/TYPES.md) document gives an in-depth analysis of Puck's extensive type system. +- The [type system](docs/TYPES.md) document gives an in-depth analysis of Puck's extensive type system. - The [modules](docs/MODULES.md) document provides a more detailed look at the first-class module system. -- The [memory management](docs/MEMORY_MANAGEMENT.md) document gives an overview of Puck's memory model. -- The [metaprogramming](docs/METAPROGRAMMING.md) document explains how using metaprogramming to extend the language works. +- The [error handling](docs/ERRORS.md) document gives a look at the various kinds of error handling available. +- The [memory management](docs/MEMORY_MANAGEMENT.md) document gives an overview of Puck's memory model. +- The [metaprogramming](docs/METAPROGRAMMING.md) document explains how using metaprogramming to extend the language works. - The [asynchronous](docs/ASYNC.md) document gives an overview of Puck's colourless asynchronous support. - The [interop](docs/INTEROP.md) document gives an overview of how the first-class language interop system works. - The [standard library](docs/STDLIB.md) document provides an overview and examples of usage of the standard library. @@ -154,7 +155,7 @@ These are best read in order. Note that all of these documents (and parts of this README) are written as if everything already exists. Nothing already exists! You can see the [roadmap](docs/ROADMAP.md) for an actual sense as to the state of the language. I simply found writing in the present tense to be an easier way to collect my thoughts. -This language does not currently integrate ideas from the following areas of active research: effects systems, refinement types, and dependent types. It plans to integrate refinement types in the future as a basis for `range[]` types, and to explore safety and optimizations surrounding integer overflow. +This language does not currently integrate ideas from the following areas of active research: effects systems, refinement types, and dependent types. It plans to base (un)safety tracking, exception handling, and async/await on a future effects system. It plans to integrate refinement types in the future as a basis for `range[]` types, and to explore safety and optimizations surrounding integer overflow. ## Primary References - [The Rust I wanted had no future](https://graydon2.dreamwidth.org/307291.html) diff --git a/docs/EXAMPLES.md b/docs/EXAMPLES.md new file mode 100644 index 0000000..592a006 --- /dev/null +++ b/docs/EXAMPLES.md @@ -0,0 +1,774 @@ +# Example Programs + +These are taken directly from the (work-in-progress) stdlib. + +## std.options + +```puck +## std.options: Optional types. +## This module is imported by default. + +use std.format + +## The `Option` type. +## A type that represents either the presence or absence of a value. +pub type Option[T] = union + Some(T) + None + +## Syntactic sugar for optional type declarations. +pub macro ?(T: type) = + quote Option[`T`] + +## Directly accesses the inner value. Throws an exception if None. +pub func ![T](self: T?): T = + if self of Some(x) then x + else raise "empty" + +## Indirect access. Propagates `None`. +pub macro ?[T](self: Option[T]) = + quote + match `self` + of Some(x) then x + of None then return None + +## Checks if a type is present within an `Option` type. +pub func is_some[T](self: T?): bool = + self of Some(_) +## Checks if a type is not present within an `Option` type. +pub func is_none[T](self: T?): bool = + self of None + +## Converts an `Option[T]` to a `Result[T, E]` given a user-provided error. +pub func err[T, E](self: T?, error: E): Result[T, E] = + if self of Some(x) then + Okay(x) + else + Error(error) + +## Applies a function to `T`, if it exists. +pub func map[T, U](self: T?, fn: T -> U): U? = + if self of Some(x) then + Some(fn(x)) + else + None +## Converts `T` to a `None`, if `fn` returns false and it exists. +pub func filter[T](self: T?, fn: T -> bool): T? = + if self of Some(x) and fn(x) then + Some(x) + else + None + +## Applies a function to T, if it exists. Equivalent to `self.map(fn).flatten`. +pub func flatmap[T, U](self: T?, fn: T -> U?): U? = + if self of Some(x) then + fn(x) + else + None +## Converts from Option[Option[T]] to Option[T]. +pub func flatten[T](self: T??): T? = + if self of Some(Some(x)) then + Some(x) + else + None + +## Returns the inner value or a default. +pub func get_or[T](self: T?, default: T): T = + if self of Some(x) then x + else default + +## Overloads the `==` operation for use on Options. +pub func ==[T](a, b: T?): bool = + if (a, b) of (Some(x), Some(y)) then + x == y + else + false + +## Overloads the `str()` function for use on Options. +pub func str[T: Display](self: T?): str = + if self of Some(x) then + "Some({})".fmt(x.str) + else + "None" + +# references: +# https://nim-lang.github.io/Nim/options.html +# https://doc.rust-lang.org/std/option/enum.Option.html +``` + +## std.results + +```puck +## std.results: Result types. +## This module is imported by default. + +use std.[options, format] + +## The Result type. Represents either success or failure. +pub type Result[T, E] = union + Okay(T) + Error(E) + +## The Err class. Useful for dynamically dispatching errors. +pub type Err = class + str(Self): str + dbg(Self): str + +## A `Result` type that uses dynamically dispatched errors. +## The `Error` may be any type implementing `Err`. +pub type Result[T] = Result[T, ref Err] +## A `Result` type that only checks for success. +## Does not contain a value. +# pub type Success[E] = Result[void, E] +## A `Result` type that only checks for success. +## Does not contain a value. Dynamically dispatched. +# pub type Success = Result[void] + +## Syntactic sugar for dynamic result type declarations. +pub macro !(T: type) = + quote Result[`T`] + +## Indirect access. Propagates `Error`. +pub macro ?[T, E](self: Result[T, E]) = + quote + match `self` + of Okay(x) then x + of Error(e) then return Error(e) + +## Checks if a `Result` type was successful. +pub func is_ok[T, E](self: Result[T, E]): bool = + self of Okay(_) +## Checks if a `Result` type was not successful. +pub func is_err[T, E](self: Result[T, E]): bool = + self of Error(_) + +## Converts from a `Result[T, E]` to an `Option[T]`. +pub func ok[T, E](self: Result[T, E]): T? = + if self of Okay(x) then + Some(x) + else + None +## Converts from a `Result[T, E]` to an `Option[E]`. +pub func err[T, E](self: Result[T, E]): E? = + if self of Error(x) then + Some(x) + else + None + +## Applies a function to `T`, if self is `Okay`. +pub func map[T, E, U](self: Result[T, E], fn: T -> U): Result[U, E] = + match self + of Okay(x) then + Okay(fn(x)) + of Error(e) then + Error(e) +## Applies a function to `E`, if self is `Error`. +pub func map_err[T, E, F](self: Result[T, E], fn: E -> F): Result[T, F] = + match self + of Error(e) then + Error(fn(e)) + of Okay(x) then + Okay(x) + +## Applies a function to `T`, if it exists. Equivalent to `self.map(fn).flatten`. +pub func flatmap[T, E, U](self: Result[T, E], fn: T -> Result[U, E]): Result[U, E] = + match self + of Okay(x) then + fn(x) + of Error(e) then + Error(e) +## Converts from a `Result[Result[T, E], E]` to a `Result[T, E]`. +pub func flatten[T, E](self: Result[Result[T, E], E]): Result[T, E] = + match self + of Okay(Okay(x)) then + Okay(x) + of Okay(Error(e)), Error(e) then + Error(e) + +## Transposes a `Result[Option[T], E]` to an `Option[Result[T, E]]`. +pub func transpose[T, E](self: Result[T?, E]): Result[T, E]? = + match self + of Okay(Some(x)) then + Some(Okay(x)) + of Okay(None), Error(_) then + None +## Transposes an `Option[Result[T, E]]` to a `Result[Option[T], E]`. Takes a default error. +pub func transpose[T, E](self: Result[T, E]?, error: E): Result[T?, E] = + match self + of Some(Okay(x)) then Okay(Some(x)) + of Some(Error(e)) then Error(e) + of None then Error(error) + +## Returns the inner value or a default. +pub func get_or[T, E](self: Result[T, E], default: T): T = + if self of Okay(x) then x + else default + +## Directly accesses the inner value. Throws an exception if `Error`. +pub func ![T, E](self: Result[T, E]): T = + match self + of Okay(x) then x + of Error(e) then raise e +## Directly accesses the inner error. Throws an exception of type T if `Okay`. +pub func get_err[T, E](self: Result[T, E]): E = + match self + of Error(e) then e + of Okay(x) then raise x + +## Overloads the `==` operation for use on Results. +pub func ==[T, E, F](a: Result[T, E], b: Result[T, F]): bool = + if (a, b) of (Okay(x), Okay(y)) then + x == y + else + false + +## Overloads the `str()` function for use on Results. +pub func str[T: Display, E: Display](self: Result[T, E]): str = + match self + of Some(x) then + "Okay({})".fmt(x.str) + of Error(e) then + "Error({})".fmt(e.str) + +# references: +# https://doc.rust-lang.org/std/result/enum.Result.html +# https://github.com/arnetheduck/nim-results +# https://github.com/codex-storage/questionable +``` + +## std.format + +```puck +## std.format: Niceties around printing and debugging. +## This module is imported by default. + +## The Display class. Any type implementing `str` is printable. +## Any type that is Display must necessarily also implement Debug. +pub type Display = class + str(Self): str + dbg(Self): str + +## The Debug class. Broadly implemented for every type with compiler magic. +## Types can (and should) override the generic implementations. +pub type Debug = class + dbg(Self): str + +## Prints all of its arguments to the command line. +pub func print(params: varargs[Display]) = + stdout.write(params.map(x => x.str).join(" "), "\n") + +## Prints all of its arguments to the command line, in Debug form. +## +## Note: this function is special! It does not count as a side effect. +## This breaks effect tracking, of course: but `dbg` is for debugging. +## It will produce a warning in code compiled for release. +@[pure] +pub func dbg(params: varargs[Debug]) = + stdout.write(params.map(x => x.dbg).join(" "), "\n") + +## A dummy implementation of the Display class for strings. +pub func str(self: str): str = self +## An implementation of the Debug class for strings. +pub func dbg(self: str): str = "\"" & self & "\"" + +## An implementation of the Debug class for all structs. +## Uses the special `struct` typeclass. +pub func dbg[T: Debug](self: struct[T]): str = + "{{}}".fmt(self.fields.map((key, val) => key & ":" & val.dbg)) + +## An implementation of the Debug class for all tuples. +## Uses the special `tuple` typeclass. +pub func dbg[T: Debug](self: tuple[T]): str = + "({})".fmt(self.fields.map((key, val) => + key.map(x => x & ":").get_or("") & val.dbg).join(", ")) + +## An implementation of the Debug class for all arrays and lists. +pub func dbg[T: Debug](self: Iter[T]): str = + "[{}]".fmt(self.map(x => x.dbg).join(", ")) + +## The fmt macro. Builds a formatted string from its arguments. +pub macro fmt(self: const str, args: varargs[Display]): str = + let parts = self.split("{}") + if parts.len != args.len + 1 then + macro_error("wrong number of arguments") + use std.ast + var res = parts.get(0)! + for i, arg in args do + res &= quote(`parts` & str(`arg`) &) # fixme + res &= parts.last()! + res +``` + +## std.debug + +```puck +## std.debug: Useful functions for debugging. +## This module is imported by default. + +## The `assert` macro checks that a provided assertation is true, +## and panics and dumps information if it is not. +## Asserts remain in release builds. If not desired, see `dbg_assert` +pub macro assert(cond: bool) = + quote + if not `cond` then + panic "assertation failed!\n {}".fmt(dbg(`cond`)) + +## The `dbg_assert` function provides an assert that is compiled out in release builds. +## This is useful for debugging performance-critical code. +pub macro dbg_assert(cond: bool) = + quote + when debug then # fixme: where is this constant coming from? + assert `cond` + +## The `discard` function consumes an object of any type. +## Useful for throwing away the result of a computation. +pub func discard[T](self: T) = + return + +## The `panic` function prints a message to `stderr` and quits. +pub func panic(message: str): never = + stderr.write(message, "\n") + std.os.exit(1) + +## The special ... syntax is used to mark unimplemented parts of code. +## Such code will compile, but panic upon being called at runtime. +## It is usable almost anywhere, including in type declarations, thanks to compiler magic. +@[magic] +pub func ...: never = + panic("unimplemented") +``` + +## std.lists + +```puck +## std.lists: Dynamic arrays. +## This module is imported by default. + +## The fundamental list type. Heap-allocated. +## Equivalent to Vec in other languages. +@[opaque] # opaque on a struct tells us raw field access breaks invariants. +pub type list[T] = struct + data: ptr T + capacity: uint + length: uint + +## A transparent, common alias for a list of bytes. +pub type bytes = list[byte] + +## Initialize and return an empty list with inner type T. +pub func init[T]: list[T] = + { data = nil, capacity = 0, length = 0 } # fixme: nil!!!!! + +## Gets the length of a list. +@[inline] # idk what to do with attributes +pub func len[T](self: lent list[T]): uint = + self.length + +pub func empty[T](self: lent list[T]): bool = + self.length == 0 + +## Gets the internal capacity of a list. +func cap[T](self: lent list[T]): uint = + self.capacity + +## Expands the capacity of a list. +@[safe] +func grow[T](self: mut list[T]) = + self.capacity = max(self.length + 1, self.capacity * 2) + self.data = self.data.realloc(self.capacity * sizeof(T)) + +## Pushes a new element to the end of a list. +@[safe] +pub func push[T](self: mut list[T], val: T) = + if self.capacity == self.length then self.grow() + self.data.set(val, offset = self.length) + self.length += 1 + +## Takes ownership of and pushes all the values of a list into another list. +pub func push[T](self: mut list[T], values: list[T]) = + for val in values do + self.push(val) + +## Removes & returns an element from the end of a list, if it exists. +@[safe] +pub func pop[T](self: mut list[T]): T? = + if self.length == 0 then + None + else + self.length -= 1 + Some(self.data.get(offset = self.length)) + +## Returns a reference to an element of a list, if in range. +@[safe] +pub func get[T](self: lent list[T], i: uint): lent T? = + if i > self.length then + None + else # fixme: interior mutability + Some(lent self.data.get(offset = i)) +## Returns a mutable reference to an element of a list, if in range. +@[safe] +pub func get[T](self: mut list[T], i: uint): mut T? = + if i > self.length then + None + else # fixme: interior mutability + Some(mut self.data.get(offset = i)) + +## Sets the element of a list to a value. +@[safe] +pub func set[T](self: mut list[T], i: uint, val: T) = + assert i <= self.length, "index out of bounds" + Okay(self.data.set(offset = i, val)) + +## Inserts a value at a location and shifts elements of the list accordingly. +@[safe] +pub func insert[T](self: mut list[T], i: uint, val: T) = + assert i <= self.length, "index out of bounds" + if self.capacity == self.length then self.grow() + self.data.offset(i).copy(self.data.offset(i + 1), self.length - i) + self.data.set(i, val) + self.length += 1 +## Inserts a list of values at a location and shifts elements of the list accordingly. +pub func insert[T](self: mut list[T], i: uint, vals: list[T]) = + for val in vals.rev: # inserting backwards avoids counting + self.insert(val, i) + +## Removes a value at a location and shifts elements of the list accordingly. +@[safe] +pub func remove[T](self: mut list[T], i: uint): T? = + if index < self.length then None + else + self.length -= 1 + let res = self.data.get(i) + self.data.offset(i + 1).copy(self.data.offset(i), self.length - i) + res + +## Gets the last element of a list, if it exists. +pub func last[T](self: lent list[T]): lent T? = + self.get(self.len - 1) +## Gets the last element of a list mutably, if it exists. +pub func last[T](self: mut list[T]): mut T? = + self.get(self.len - 1) + +# reference: https://doc.rust-lang.org/nomicon/vec/vec.html +``` + +## std.strings + +```puck +## std.strings: The standard implementation of strings. +## This module is imported by default. + +## A primitive string type. +## +## We do not want methods defined on `list[byte]` to carry over, +## so we define `str` as a newtype. +@[opaque] +pub type str = struct + data: list[byte] + +## Initialize and return an empty string. +pub func init: str = { data = [] } + +## Gets the length of a string. +## This is an O(n) operation, due to UTF-8 encoding. +pub func len(self: lent str): uint = + var res: uint + for _ in self do + res += 1 + res + +## Pushes a character to the end of a mutable string. +pub func push(self: mut str, val: char) = + self.data.push(val.byte) # todo: obsolete by from/to conversion?? + +## Pushes an owned string to the end of a mutable string. +pub func push(self: mut str, val: str) = + self.data.push(val.bytes) # todo: obsolete by from/to conversion?? + +## Removes and returns the last character of a string, if it exists. +## +## SAFETY: We return early upon an empty string. +## And decrement by one char for a non-empty string. +@[safe] +pub func pop(self: mut str): char? = + let char = self.chars.rev.next? + self.data.set_len(self.len - char.len) # this is normally unsafe. + Some(char) + +## Returns the character at the provided index, if it exists. +pub func get(self: str, i: uint): char? = + ... + +## Sets the character at the provided index, if it exists. +## As strings are packed, this may call str.grow and reallocate. +## oh fuck we have to insert + remove anyway +pub func set(self: mut str, i: uint, val: char) = + ... + +## Inserts a character at an arbitrary position within a string. +## Panics on failure. (todo: can we do better?) +pub func insert(self: mut str, i: uint, val: char) = + ... + +## Removes and returns a character at an arbitrary position within a string. +## Panics on failure. (todo: can we do better?) +pub func remove(self: mut str, i: uint): char? = + ... + +## Syntactic sugar for string appending. +pub func &=(a: mut str, b: str) = + a.push(b) + +## The concatenation operator. Consumes two strings. +pub func &(a: str, b: str): str = + a.push(b) + a + +## Conversion from a string to a list of bytes. Zero-cost. +pub func to(self: str): list[byte] = self.data +## Conversion from a str to a list[char]. Reallocates. +pub func to(self: str): list[char] = + var res: list[char] + for char in self do res.push(char) + res +## Conversion from a char to an array of bytes. Zero-cost. +@[safe] # possibly unsafe?? depends on repr of arrays +pub func to(self: char): array[byte, 4] = + self.cast[array[byte, 4]] + +# reference: https://doc.rust-lang.org/std/string/struct.String.html +``` + +## std.compare + +```puck +## std.compare: Classes for comparable types. + +## The Eq class. For types with some notion of equivalence. +pub type Eq = class + ==(Self, Self): bool + +## A blanket implementation of a corresponding not-equal function. +pub !=[T: Eq](a: T, b: T): bool = + not(a == b) + +## The Compare class. For a type comparable with itself. +pub type Compare = class + <(a: Self, b: Self): bool + +## A blanket implementation of a corresponding greater-than function. +## Note to self: do NOT inline! +pub func >[T: Compare](a: T, b: T): bool = + b < a + +## The Ord class. For types with some notion of equivalence and comparision. +## +## Note: This is *not* a mathematical notion of an order! +## No invariants on `<` nor `==` are guaranteed to hold, as classes +## are implicitly implementable. +pub type Ord = class + <(a: Self, b: Self): bool + ==(a: Self, b: Self): bool + +## A blanket implementation of a corresponding less-than-or-equal function. +pub func <=[T: Ord](a: T, b: T): bool = + a < b or a == b + +## A blanket implementation of a corresponding greater-than-or-equal function. +pub func >=[T: Ord](a: T, b: T): bool = + a > b or a == b + +# reference: https://doc.rust-lang.org/std/cmp +``` + +## std.convert + +```puck +## std.convert: Classes for type coersion and conversion. +## This module is imported by default. + +## The Coerce class is used for type conversion that will not fail. +## Its associated methods, `from` and `into`, are used internally +## by the compiler for implicit type conversion (coersion). +pub type Coerce[T] = class + to(Self): T + # from(T): Self + +## The `from` function is automatically implemented for all types that +## implement `to`: that is, all types T that are convertable to U. +pub func from[T: Coerce[U], U](self: U): T = + to(self) + +## The Convert class is used for type conversion that may fail. +# We'll see what this breaks. +pub type Convert[T, E] = class + to(Self): Result[T, E] +``` + +## std.ranges + +```puck +## std.ranges: Ranges of integers and other things. For iteration. +## This module is imported by default. + +type Range[T] = struct + start: T + end: T + +type RangeIncl[T] = struct + start: T + end: T + done: bool + +## Exclusive ranges. Useful for iteration. +## Includes `from`, does not include `to`. +pub func ..(from: int, to: int): Range[int] = { from, to } + +## Inclusive ranges. Useful for ranges. +## Includes `from` and `to`. +pub func ..=(from: int, to: int): RangeIncl[int] = { from, to, done = false } + +# todo: implement for all types that can increment or smth idk +pub func next[T: int](self: mut Range[T]): T? = + if self.start < self.end then + self.start += 1 + Some(self.start - 1) + else + None + +# todo: We don't need a mutable Range here to peek. +# How does this interact with classes? +pub func peek[T: int](self: mut Range[T]): T? = + self.peek_nth(0) + +pub func peek_nth[T: int](self: mut Range[T], i: uint): T? = + let res = self.start + i + if res < self.end then + Some(res) + else + None + +pub func next[T: int](self: mut RangeIncl[T]): T? = + if self.done then + None + elif self.start < self.end then + let res = self.start + self.start += 1 + Some(res) + elif self.start == self.end then + self.done = true + Some(self.start) + else + self.done = true + None + +pub func peek[T: int](self: mut RangeIncl[T]): T? = + self.peek_nth(0) + +pub func peek_nth[T: int](self: mut RangeIncl[T], i: uint): T? = + let res = self.start + i + if res <= self.end + then Some(res) + else None +``` + +## std.ast + +```puck +## std.ast: Exposes the AST for building and operating on with macros. + +## The `Expr` type represents the abstract syntax tree of Puck itself. +## It notably lacks type information. It is also not necessarily syntactically +## correct-by-construction: Cond, Try, and Match expressions must have at least +## one branch in their branches (yet this is not expressible here). +pub type Expr = union + # Terms + Ident(str) + Number(int) + Float(float) + Char(char) + String(str) + Struct(list[(field: str, value: Expr)]) # {...} + Tuple(list[(field: str?, value: Expr)]) # (...) + List(list[Expr]) # [...] + # Bindings + Let(id: Pattern, kind: Type?, value: ref Expr) + Var(id: Pattern, kind: Type?, value: ref[Expr]?) + Constant(public: bool, id: Pattern, kind: Type?, value: ref Expr) + FuncDecl( + public: bool, + id: str, + generics: list[(id: str, kind: Type?)], + params: list[(id: str, kind: Type)], + kind: Type?, + body: list[Expr]) + MacroDecl( + public: bool, + id: str, + generics: list[(id: str, kind: Type?)], + params: list[(id: str, kind: Type?)], + kind: Type?, + body: list[Expr]) + TypeDecl( + public: bool, + id: str, + generics: list[str], + body: Type) + Module( + public: bool, + id: str, + generics: list[str], # always empty for now + body: list[Expr]) + Use(modules: list[(path: str, alias: str?)]) + # Control Flow + Call(id: str, params: list[Expr]) + Cond( + branches: list[(cond: Expr, body: list[Expr])], + else_body: list[Expr]) + Try( + try_body: list[Expr], + catches: list[(exceptions: list[str], body: list[Expr])], + finally_body: list[Expr]) # todo: throw this out + Match( + item: ref Expr, + branches: list[(pattern: Pattern, guard: Expr?, body: list[Expr])]) + Block(id: str?, body: list[Expr]) + Static(body: list[Expr]) + For(binding: Pattern, range: ref Expr, body: list[Expr]) + While(cond: ref Expr, body: list[Expr]) + Loop(body: list[Expr]) + Attribute(on: ref Expr) + Quote(body: ref Expr) + Unquote(body: ref Expr) + +pub type Type = ref union + Never + Int(size: uint) + Dec(size: uint) + Float(size: uint) + Func(from: list[Type], to: Type) + Struct(list[(id: str, kind: Type)]) + Tuple(list[(id: str?, kind: Type)]) + Union(list[(id: str, kind: Type)]) + Class(list[(id: str, from: list[Type], to: Type?)]) + Array(size: uint, kind: Type) + List(Type) + Slice(Type) # todo: plus ownership + Alias(str) # todo: params?? huh? + Const(Type) + Lent(Type) + Mut(Type) + Ref(Type) + Refc(Type) + Ptr(Type) + +pub type Pattern = union + Ident(str) + Number(int), Float(float), Char(char), String(str) + Struct(name: str, params: list[Pattern]) + Tuple(list[Pattern]) + List(list[Pattern]) + +@[magic] +pub func quote(body): Expr +``` diff --git a/docs/METAPROGRAMMING.md b/docs/METAPROGRAMMING.md index 17e65a0..191e53a 100644 --- a/docs/METAPROGRAMMING.md +++ b/docs/METAPROGRAMMING.md @@ -6,6 +6,8 @@ Macros take in fragments of the AST within their scope, transform them with arbi By keeping an intentionally minimal AST, some things not possible to express in literal code may be expressible in the AST: in particular, bindings can be injected in many places they could not be injected in ordinarily. (A minimal AST also has the benefit of being quite predictable.) +## Scope + Macros may not change Puck's syntax: the syntax is flexible enough. They have the same scope as other routines, that is: **function scope**: takes the arguments within or following a function call @@ -39,6 +41,8 @@ macro +=(a, b) = a += b ``` +## Usage + Macros typically take a list of parameters *without* types, but they optionally may be given a type to constrain the usage of a macro. Regardless: as macros operate at compile time, their parameters are not instances of a type, but rather an `Expr` expression representing a portion of the *abstract syntax tree*. Similarly, macros always return an `Expr` to be injected into the abstract syntax tree despite the usual absence of an explicit return type, but the return type may be specified to additionally typecheck the returned `Expr`. @@ -58,6 +62,8 @@ func meow: Result[bool, ref Err] = let a = stdin.get()? ``` +## Quoting + The `quote` macro is special. It takes in literal code and returns that code **as the AST**. Within quoted data, backticks may be used to break out in order to evaluate and inject arbitrary code: though the code must evaluate to an expression of type `Expr`. Thus, quoting is *structured*: one cannot simply quote any arbitrary section. Quoting is very powerful: most macros are implemented using it. ```puck 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[]` 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[...]`. +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 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. diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md index 8a4a530..d26c2aa 100644 --- a/docs/SUMMARY.md +++ b/docs/SUMMARY.md @@ -4,18 +4,24 @@ - [Basic Usage](OVERVIEW.md) - [Variables and Comments]() + - [Functions and Indentation]() + - [Uniform Function Call Syntax]() - [Basic Types]() - - [Functions and Calls]() - - [Boolean and Integer Operations]() - - [Conditionals and Control Flow]() + - [Conditionals and Pattern Matching]() - [Error Handling]() - - [Loops and Iterators]() - - [Modules]() + - [Blocks and Loops]() + - [Module System]() - [Compile-time Programming]() - - [Async and Threading]() - - [Advanced Types]() + - [Async System and Threading]() + - [Memory Management]() + - [Types System]() + - [Structs and Tuples]() + - [Unions and Enums]() + - [Classes]() - [Syntax](SYNTAX.md) - - [Indentation Rules [todo]]() + - [Call Syntax]() + - [Indentation Rules]() + - [Expression Rules]() - [Reserved Keywords]() - [A Formal Grammar]() - [Type System](TYPES.md) @@ -25,25 +31,30 @@ - [Abstract Types]() - [Advanced Types]() - [Module System](MODULES.md) - - [Using Modules]() - - [Implicit Modules]() - - [Defining Module Interfaces [todo]]() - - [Defining an External API [todo]]() + - [What are modules?]() + - [Using modules]() + - [Implicit modules]() + - [Defining interfaces [todo]]() + - [Defining an external API [todo]]() +- [Memory Management [todo]](MEMORY_MANAGEMENT.md) +- [Metaprogramming](METAPROGRAMMING.md) + - [Scope]() + - [Usage]() + - [Quoting [todo]]() - [Error Handling](ERRORS.md) - - [Errors as Monads]() - - [Errors as Catchable Exceptions]() - - [Errors and Void Functions [todo]]() + - [Errors as monads]() + - [Errors as checked exceptions]() + - [Errors as effects [todo]]() - [Unrecoverable Exceptions]() - [Async System](ASYNC.md) + - [Effects System [todo]]() - [Threading [todo]]() -- [Metaprogramming](METAPROGRAMMING.md) -- [Memory Management [todo]]() - - [Reference Counting Optimizations]() - - [Annotations and Ownership]() - [Language Interop [draft]](INTEROP.md) - - [Rust, Swift, Nim]() + - [Rust]() + - [Swift, Nim]() - [Java, Kotlin]() - [Python, Racket, C]() +- [Effects System [draft]]() - [Refinement Types [draft]]() - [Dependent Types [draft]]() -- [Effects System [draft]]() +- [Examples](EXAMPLES.md) diff --git a/docs/SYNTAX.md b/docs/SYNTAX.md index 249d6e3..d88a666 100644 --- a/docs/SYNTAX.md +++ b/docs/SYNTAX.md @@ -248,7 +248,7 @@ 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` `i[\d+]` `u[\d+]` +- types: `int` `uint` `float` `i[\d]+` `u[\d]+` - `f32` `f64` `f128` - `dec64` `dec128` - types: `bool` `byte` `char` `str` diff --git a/docs/TYPES.md b/docs/TYPES.md index 5d97db5..510c0c1 100644 --- a/docs/TYPES.md +++ b/docs/TYPES.md @@ -10,7 +10,7 @@ Basic types can be one-of: - `bool`: internally an enum. - `int`: integer number. x bits of precision by default. - `uint`: same as `int`, but unsigned for more precision. - - `i[\d+]`, `u[\d+]`: arbitrarily sized integers + - `i[\d]+`, `u[\d]+`: arbitrarily sized integers - `float`: floating-point number. - `f32`, `f64`: specified float sizes - `decimal`: precision decimal number. diff --git a/docs/book/highlight.js b/docs/book/highlight.js index 2352580..715d346 100644 --- a/docs/book/highlight.js +++ b/docs/book/highlight.js @@ -5270,6 +5270,7 @@ var hljs = (function () { "const", "lent", "mut", + "varargs", "func", "macro", "type", @@ -5298,8 +5299,7 @@ var hljs = (function () { "return", "raise", "in", - "is", - "type" + "is" ]; const BUILT_INS = [ "stdin", -- cgit v1.2.3-70-g09d2