diff options
author | JJ | 2023-11-07 04:06:05 +0000 |
---|---|---|
committer | JJ | 2023-11-07 04:06:05 +0000 |
commit | 201ce572671d335e54f0f62c3e380d169449fe53 (patch) | |
tree | 1a3ae3387dd4605bbd41f79a81ce87b5b3d4f91a | |
parent | dc1640fd403649f6e54146754b30dbfa3145fba9 (diff) |
docs: rewrite BASIC.md
-rw-r--r-- | docs/BASIC.md | 114 |
1 files changed, 70 insertions, 44 deletions
diff --git a/docs/BASIC.md b/docs/BASIC.md index 9bba09f..36a2aa3 100644 --- a/docs/BASIC.md +++ b/docs/BASIC.md @@ -1,40 +1,53 @@ -# Basic Usage of Puck +# 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. I do hope others enjoy it. ```puck +let ident: int = 413 +# type annotations are optional +var phrase = "Hello, world!" +const compile_time = when linux: "linux" else: "windows" ``` -Mutable variables are declared with `var`. -Immutable variables are declared with `let`. -Compile-time evaluated immutable variables are declared with `const`. +Variables may be mutable (`var`), immutable (`let`), or evaluated at compile-time and immutable (`const`). +Type annotations on variables and other bindings follow the name of the binding (with `: Type`), and are typically optional. +Variables are conventionally written in `camelCase`. Types are conventionally written in `PascalCase`. +The type system is comprehensive, and complex enough to [warrant its own document](TYPES.md). -Comments are declared with `#`. -Documentation comments are declared with `##`. +Comments are declared with `#` and run until the end of the line. +Documentation comments may be declared with `##` and will be parsed by language servers and other tooling. Multi-line comments are declared with `#[ ]#` and may be nested. +Taking cues from the Lisp family of languages, a top-level expression may be commented out with `#;` preceding. -Type annotations on variable declarations follow the name with `: Type` and are typically optional. The compiler is quite capable of variable inference. +```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() -The type system is comprehensive, and complex enough to [warrant its own document](TYPES.md). +pub pure func... -```puck +# 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, followed by the function name, followed by an (optional) list of parameters surrounded in parenthesis, followed by a type annotation. Functions may be prefixed with one or more of the following modifiers: -- `pub`: exports the function for use by external files +Functions are declared with the `func` keyword. They take an (optional) list of generic parameters (in brackets), an (optional) list of parameters (in parentheses), and must be annotated with a return type if they return a type. Every (non-generic) parameter not annotated with a type takes its type from the next parameter. Generic parameters may be each optionally annotated with a type functioning as a *constraint*. + +Functions, constants, types, and modules may be optionally prefixed with a `pub` modifier denoting visibility outside the current scope (more specifically: module). Functions may also be prefixed with one or more of the following additional modifiers: - `pure`: denotes a function as a "pure function", lacking side effects, i.e. IO or nondeterminism or parameter mutability -- `yeet`: denotes a function as a "throwing function", meaning it may raise exceptions. -- `async`: marks a function as asynchronous which may only be called by other asynchronous functions or with the `await` keyword -<!-- - `total`: idk --> -<!-- - more?? converter?? idk todo --> +- `yeet`: denotes a function as a "throwing function", that may raise exceptions. +- `async`: marks a function as asynchronous which may only be called by other asynchronous functions or brought to a value with the `await` function +<!-- todo? more? --> -<!-- There is an explicit ordering of these prefixes: everything << `pub` << `pure`. `pub` and `pure` are idk what i'm going for here is `async` and everything else should be macros providable by libraries. so `async` functions can be rewritten to use cps, etc depending on what you do, `await` is just a function that converts an `async T` to a `T`, `suspend`, `resume` are functions, etc. --> +Whitespace is flexible, and 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. A list of parameters, surrounded by parentheses and separated by commas, may follow the function name. These are optional and a function with no parameters may be followed with `()` or simply nothing at all. More information on function parameters (and return types) is available in the [type system overview](TYPES.md). -Type annotations on function declarations follow the name and parameters (if any) with `: Type` and are typically required. The compiler is not particularly capable of function type inference (and it is good practice to annotate them anyway). - -Uniform function call syntax (UFCS) is supported: and so arbitrary functions with compatible types may be chained with no more than the `.` operator. +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.) -<!-- All assignments may be overridden, but overriding values outside of shadowing (assigning the value of an immutable parameter to a mutable variable) will be a compiler warning. --> +This allows for a number of syntactic cleanups. Arbitrary functions with compatible types may be chained with no need for a special pipe operator. Struct/tuple field access, module field 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. ```puck ``` @@ -46,63 +59,76 @@ Boolean logic and integer operations are standard and as one would expect out of - boolean operators are bitwise and also apply to integers and floats - more operators are available via the standard library -Term in/equality is expressed with `==` and `!=`. Type in/equality is expressed with `is` and `isnot` (more on this in the [types document](TYPES.md)). Set logic is expressed with `in` and `notin`, and is applicable to not just sets but collections of any sort. +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 declared like functions, with their name in backticks, and the restriction that they must be composed of the following punctuation tokens: todo. This restriction is to ensure the parser remains context free. + +Term in/equality is expressed with the `==` and `!=` operators. Type in/equality is expressed with `is` and `not (T is U)`, and subtyping may be queried with `of` (more on this in the [types document](TYPES.md)). Set logic is expressed with `in` and `not (x in Y)`, and is overloaded for not just sets but collections of any sort. -String concatenation uses `&` rather than overloading the `+` operator (as the complement `-` has no natural meaning for strings). Strings are also unified and mutable. More details can be found in the [type system overview](TYPES.md). +String concatenation uses a distinct `&` operator rather than overloading the `+` operator (as the complement `-` has no natural meaning for strings). Strings are unified, mutable, internally a byte array, externally a char array, and are stored as a pointer to heap data after their length and capacity (fat pointer). Slices of strings are stored as a length followed by a pointer to string data, and have non-trivial interactions with the memory management system. Chars are four bytes and represent a Unicode character in UTF-8 encoding. More details can be found in the [type system overview](TYPES.md). ```puck ``` -Basic conditional control flow is standard via `if`, `elif`, and `else` statements. +Basic conditional control flow is standard via `if/elif/else` statements. The `when` statement provides a compile-time `if`. It also takes `elif` and `else` branches and is syntactic sugar for an `if` statement within a `static` block (more on those later). Exhaustive structural pattern matching is available with the `match/of` statement, and is particularly useful for the `union` type. Branches of a `match` statement take a *pattern*, of which the unbound identifiers within will be injected into the branch's scope. Multiple patterns may be used for one branch provided they all bind the same identifiers of the same type. Branches may be *guarded* with the `where` keyword, which takes a conditional, and will necessarily remove the branch from exhaustivity checks. -There is a distinction between statements, which do not produce a value but rather only execute computations, and expressions, which evaluate to a value. Several control flow constructs - conditionals, block statements, and pattern matches - may be used as both statements and expressions. +The `of` statement also stands on its own as a conditional for querying subtype equality. It retains the variable injection properties of its counterpart within `match` statements. This allows it to be used as a compact and coherent alternative to `if let` statements in other languages. -The special `discard` statement allows for throwing an expression's value away. On its own, it provides a no-op. All (non-void) expressions must be handled: however, a non-discarded expression at the end of a scope functions as an implicit return. This allows for significant syntactic reduction. +All values in Puck must be handled, or explicitly discarded. This allows for conditional statements and many other control flow constructs to function as *expressions*, and evaluate to a value, when an unbound value is left at the end of each of their branches' scopes. Puck makes no attempt to determine this without context, and so expressions and statements look identical in syntax and semantics (AST). ```puck ``` -Three types of loops are available: `while` loops, `for` loops, and infinite loops (`loop` loops). While loops take a condition that is executed upon the beginning of each iteration to determine whether to keep looping. 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. Infinite loops are, well, infinite and must be manually broken out of. +Error handling is done with a fusion of imperative `try/catch` statements and functional `Option/Result` types, with much syntactic sugar. Functions may `raise` errors, but should return `Option[T]` or `Result[T, E]` types instead by convention. Those that `raise` errors or call functions that `raise` errors without handling them must additionally be explicitly marked as `yeet`. This is purely to encourage safe error handling, and is not absolute - there will likely be several builtins considered safe by compiler magic. (??? what are those?) -There is no special concept of iterators: iterable objects are any object that implements the Iterable interface (more on those in [the type system document](TYPES.md)), that is, provides a `self.next()` function returning an Optional type. For loops desugar to while loops that unwrap the result of the `next()` function and end iteration upon a `None` value. While loops, in turn, desugar to infinite loops with an explicit conditional break. +A bevy of helper functions and macros are available for `Option/Result` types, and are documented and available in the `std/options` module (imported 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]` or `Result[T, E]` or raises the `Error(e)` or a an error on `None` or `Error`. Both are operators taking one parameter and so are postfix. -The `break` keyword immediately breaks out of the current loop. -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. +The utility of the `?` macro is readily apparent to anyone who has written code in Rust or Swift. The utility of the `!` function is perhaps less so obvious. These errors raised by `!`, however, are known to the compiler: and they may be comprehensively caught by a single or sequence of `catch` statements. This allows for users used to a `try/catch` error handling style to do so with ease, with only the need to add one additional character to a function call. + +More details may be found in [error handling overview](ERRORS.md). ```puck +loop: + break ``` -Blocks provide arbitrary scope manipulation. They may be labelled or unlabelled. The `break` keyword additionally functions inside of blocks and without any parameters will jump out of the current enclosing block (or loop). It may also take a block label as a parameter for fine-grained scope control. +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, well, infinite and must be manually broken out of. -All forms of control flow ultimately desugar to continuations: https://github.com/nim-works/cps/tree/master/docs +There is no special concept of iterators: iterable objects are any object that implements the `Iter[T]` interface (more on those in [the type system document](TYPES.md)), that is, provides a `self.next()` function returning an Optional type. For loops can be thought of as while loops that unwrap the result of the `next()` function and end iteration upon a `None` value. While loops, in turn, can be thought of as infinite loops with an explicit conditional break. + +The `break` keyword immediately breaks out of the current loop, and the `continue` keyword immediately jumps to the next iteration of the current loop. Loops may be used in conjunction with blocks for more fine-grained control flow manipulation. ```puck -``` +block: + statement -Exhaustive structural pattern matching is available and particularly useful for tagged unions. This is frequently a better alternative to a series of `if` statements. +let x = block: + let y = read_input() + transform_input(y) -```puck +block foo: + block bar: + for i in 0..=100: + if i == 10: break foo ``` -Code is segmented into modules. A module can be imported into another module by use of the `import` keyword. Files form implicit modules, and explicit modules can be declared with the `module` keyword. Modules are declared in-line with implementations, and other modules can be re-exported with the `export` keyword. All identifiers are private by default within the module system and can explicitly be marked `pub`. - -More details may be found in the [modules document](MODULES.md). +Blocks provide arbitrary scope manipulation. They may be labelled or unlabelled. The `break` keyword additionally functions inside of blocks and without any parameters will jump out of the current enclosing block (or loop). It may also take a block label as a parameter for fine-grained scope control. ```puck ``` -Compile-time programming may be done via the previously-mentioned `const` keyword: or via `static` blocks. All code within a `static` block is evaluated at compile-time and all assignments made are propagated to the compiled binary. As a result, `static` blocks are only available in the global context (not within functions). +Code is segmented into modules. Modules may be made explicit with the `mod` keyword followed by a name, but there is also an implicit module structure in every codebase that follows the structure and naming of the local filesystem. For compatibility with filesystems, and for consistency, module names are exclusively lowercase (following the same rules as Windows). + +Within modules, constants, functions, types, and other modules may be *exported* for use by other modules with the `pub` keyword. All such identifiers are private by default within a module and only accessible locally. The imported modules, constants, functions, types, etc within imported modules may be *re-exported* for use by other modules with the `export` keyword. Modules are first-class and may be bound, inspected, modified, and returned. -Compile-time programming may also be intertwined in the codebase with the use of the `when` statement. It functions similarly to `if`, but may only take a static operation as its parameter, and will directly replace code accordingly at compile-time. The `else` statement is overloaded to complement this. +A module can be imported into another module by use of the `use` keyword, taking a path to a module or modules. Contrary to the majority of languages ex. Python, unqualified imports are *encouraged*: type-based disambiguation and official LSP support are intended to remove any ambiguity. -Further compile-time programming may be done via metaprogramming: compile-time introspection on the abstract syntax tree. -Two distinct language constructs of differing complexity are provided: templates for raw substitution, and macros for direct manipulation of the abstract syntax tree. These are complex, and more details may be found in the [metaprogramming document](METAPROGRAMMING.md). +More details may be found in the [modules document](MODULES.md). ```puck ``` -Error handling is typically done via explicitly matching upon Optional and Result values (with the help of the `?` operator), but such functions can be made to explicitly throw exceptions (which may then be caught via `try`/`catch`/`finally` or thrown with `raise`) with the help of the `!` operator. This is complex and necessarily verbose, although a bevy of helper functions and syntactic sugar are available to ease usage. More details may be found in [error handling overview](ERRORS.md). +Compile-time programming may be done via the previously-mentioned `const` keyword and `when` statements: or via `static` blocks. All code within a `static` block is evaluated at compile-time and all assignments made are propagated to the compiled binary. + +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 ``` @@ -111,7 +137,7 @@ Threading support is complex and regulated to external libraries (with native sy Async support is complex and relegated to external libraries (with native syntax via macros). More details may be found in the [async document](ASYNC.md). It is likely that this will look like Zig, with `async`/`await`/`suspend`/`resume`. -Effects are complex and relegated to external libraries (with native syntax via macros). More details may be found in the [effects document](EFFECTS.md). +Effects are complex and lack any sort of design structure. More details may be found in the [effects document](EFFECTS.md). ```puck ``` |