diff options
-rw-r--r-- | README.md | 39 | ||||
-rw-r--r-- | docs/BASIC.md | 2 | ||||
-rw-r--r-- | docs/MODULES.md | 6 | ||||
-rw-r--r-- | docs/TYPES.md | 40 |
4 files changed, 76 insertions, 11 deletions
@@ -2,12 +2,47 @@ A place where I can make some bad decisions. -Puck is an experimental, memory safe, strongly typed, imperative and functional programming language. +Puck is an experimental, memory safe, structurally typed, imperative and functional programming language. It aims to be clean and succinct while performant: having the flexibility/metaprogramming of [Nim](https://nim-lang.org/) with the error handling of [Swift](https://www.swift.org/) and the performance/safety guarantees of [Rust](https://www.rust-lang.org/). You may judge for yourself if Puck meets these ideals. -```puck +```nim +import std/tables + +module AST: + pub type Value = interface + func repr(self: Self): string + pub type Ident = string + pub type Expr = ref union + Literal: Value + Variable: Ident + Abstraction: struct[param: Ident, body: Expr] + Application: struct[body, arg: Expr] + Conditional: struct + cond, then_case, else_case: Expr + + pub func eval(expr: Expr, context: var HashTable[Ident, ref Value]): Result[ref Value] + match expr + of Literal(value): + Okay(value) + of Variable(ident): + context.get(ident) + of Application{body, arg}: + if body of Abstraction{param, body as inner_body}: + context.set(param, eval(arg)) + inner_body.eval(context) + else: + Error(InvalidExpr) + of Conditional{cond, then_case, else_case}: + if eval(cond, context): + then_case.eval(context) + else: + else_case.eval(context) + of _: + Error(InvalidExpr) + +AST.eval((λx.x) 413) ``` ## Why Puck? diff --git a/docs/BASIC.md b/docs/BASIC.md index e266b29..9bba09f 100644 --- a/docs/BASIC.md +++ b/docs/BASIC.md @@ -85,7 +85,7 @@ Exhaustive structural pattern matching is available and particularly useful for ```puck ``` -I am undecided on how the import/module system will work and particularly how it will play into the type system. UFCS *will* be supported. todo +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). diff --git a/docs/MODULES.md b/docs/MODULES.md index d387e0a..911d207 100644 --- a/docs/MODULES.md +++ b/docs/MODULES.md @@ -1,6 +1,8 @@ # Modules and Namespacing -Puck has a rich module system, inspired by such expressive systems in the ML family of languages, most notably including Rust. 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. Puck cleans up such "namespace pollution" by **type-based disambiguation**. <!-- Puck allows for the usage of such unqualified identifiers by **type-based disambiguation**. --> +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. Puck cleans up such "namespace pollution" by **type-based disambiguation**. <!-- Puck allows for the usage of such unqualified identifiers by **type-based disambiguation**. --> + +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, modularity features are written directly inline with their declaration, and the file system structure is reused to form an implicit module system for internal use. ```puck import std/[ascii, unicode] @@ -38,7 +40,7 @@ The file structure forms an implicit, internal API, addressable by import statem 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. -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. In support of encapsulation, fields of types, too, are considered separate from the type itself and also must be `pub` to be accessible. Identifiers from imported modules, of course, are not considered part of the current module unless explicitly exported. +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. diff --git a/docs/TYPES.md b/docs/TYPES.md index 59cfd2f..b16c508 100644 --- a/docs/TYPES.md +++ b/docs/TYPES.md @@ -226,27 +226,55 @@ Unions are *tagged* type unions. They provide a high-level wrapper over an inner They are declared with `union[Variant: Type, ...]` and initialized with the name of a variant followed by its inner type constructor in brackets: `Square(side: 5)`. Tuple and struct types are special-cased to eliminate extraneous parentheses. ```puck +type Value = uint64 type Ident = string -type Expr = union - Literal: Term +type Expr = ref union + Literal: Value Variable: Ident - Abstraction: struct[param: Ident, func: Expr] - Application: struct[func, arg: Expr] + Abstraction: struct[param: Ident, body: Expr] + Application: struct[body, arg: Expr] Conditional: struct - if, then, else: Expr + cond, then_case, else_case: Expr ``` They take up as much space in memory as the largest variant, plus the size of the tag (typically one byte). #### pattern matching -todo... +Unions abstract over differing types. In order to *safely* be used, their inner types must be accessed via *pattern matching*: leaving no room for type confusion. Pattern matching in Puck relies on two syntactic constructs: the `match` statement, forcing qualification and handling of all possible types of a variable, and the `of` statement, querying type equality while simultaneously binding new identifiers to underspecified portions of variables. ```puck +import std/tables + +func eval(expr: Expr, context: var HashTable[Ident, Value]): Result[Value] + match expr + of Literal(value): + value + of Variable(ident): + context.get(ident) + of Application{body, arg}: + if body of Abstraction{param, body as inner_body}: + context.set(param, eval(arg)) + inner_body.eval(context) + else: + Error(InvalidExpr) + of Conditional{cond, then_case, else_case}: + if eval(cond, context): + then_case.eval(context) + else: + else_case.eval(context) + of _: + Error(InvalidExpr) ``` +The match statement takes exclusively a list of `of` sub-expressions, and checks for exhaustivity; but the `variable of Type(x)` syntax can be reused as a conditional, in `if` statements and elsewhere. + The `of` operator is similar to the `is` operator in that it queries type equality. However, unbound identifiers within `of` expressions are bound to appropriate values (if matched) and injected into the scope. +When matching unions with an inner product type (structs or tuples), external extraneous parenthesis are elided. + +todo: guards, either `where` or `if` + ### interfaces Interfaces can be thought of as analogous to Rust's traits, without explicit `impl` blocks and without need for the `derive` macro. |