Pen programming language
Pen is the programming language for scalable software development, focused on software maintainability and portability.
import Os'Context { Context }
import Os'File
import Os'Process
sayHello = \(ctx Context) none | error {
File'Write(ctx, File'StdOut(), "Hello, world!\n")?
none
}
main = \(ctx context) none {
e = sayHello(ctx.Os)
if _ = e as none {
none
} else if error {
Process'Exit(ctx.Os, 1)
}
}
Install
See Install.
Documentation
- Getting started
- Language reference
- Code examples
Vision
Pen aims to make large-scale software development efficient where a number of people develop software together for a long time. To realize that, it focuses on software maintainability and portability.
- Maintainability
- Simplicity: The language is small and easy to learn but also full featured.
- Testability: Unit tests are always fast and reliable.
- Modifiability: Developers can change application logic independently from implementation details.
- Portability
- Programs written in the language can be ported to different platforms including WebAssembly.
Features
Minimal language
- Its syntax and type system are small, simple, and easy to learn.
- Yet, the language supports all the modern features.
Concurrent/parallel computation
- The language and its runtime enables thread-safe concurrent/parallel computation.
- For more information, see Concurrency and parallelism.
System packages
- System packages encapsulate side effects as separate packages.
- No other packages have side effects unless injected them into explicitly.
Reliable testing
- Unit tests are always deterministic and fast.
- No flaky or slow tests bother developers.
Even more...
- Static typing
- Immutable values
- Pure functions by default
- Errors as values
- Asynchronous I/O
- Cross compile
- Rust/C Foreign Function Interface (FFI)
License
Pen is released under open source licenses. See its LICENSE file for more information.
Guides
This chapter contains how-to guides for software development in the language.
Getting started
Install
See Install.
Creating a package
To create your first package, run the following command.
pen create foo
Then, you should see a foo
directory under the current directory. When you go to the foo
directory, you should see a main.pen
source file and a pen.json
file for package configuration.
Building a package
To build the package, run the following command in the foo
directory.
pen build
Then, you will see an executable file named app
in the directory. Run it to see your first "Hello, world!"
./app
For more information...
Now, you can start editing *.pen
files and build your own application!
- For more code examples, see Examples.
- To know more about the language's constructs, see Syntax and Types.
- To know how to use the standard packages, see Standard packages.
- To know how to add more modules in your package, see Modules.
- To know how to import other packages, see Packages.
Install
Via Homebrew
On Linux, macOS and WSL, you can install Pen through Homebrew.
-
Install Homebrew.
-
Run the following command in your terminal.
brew install pen-lang/pen/pen
Now, you should be able to run a pen
command in your shell.
pen --version
Building from source
You can also build Pen from source on your local machine.
-
Install the following software using a package manager of your choice (e.g.
apt
for Ubuntu and Homebrew for macOS.) -
Clone the Git repository.
git clone https://github.com/pen-lang/pen
-
Run a
cargo
command in the repository's directory.cargo install --path cmd/pen
-
Set a
PEN_ROOT
environment variable to the directory.export PEN_ROOT=<directory>
Now, you are ready to use the pen
command built manually.
Testing
This page describes how to write and run unit tests for programs written in the language.
Testing codes consists of the following steps:
- Write tests as test functions in test modules.
- Run the tests with a
pen test
command.
Writing tests
You can write tests as test functions in test modules. All modules with the .test.pen
file extension are test modules. And, all public functions in test modules are test functions. The test functions need to have a type of \() none | error
and should return error
values when they fail.
For example, to test a Foo
function in a Foo.pen
module, write a Foo.test.pen
test module with the following contents.
import Test'Assert
import 'Foo
CheckFoo = \() none | error {
Assert'True(Foo'Foo() == 42)
}
The Test
package
The Test
standard package includes some utilities which helps you to write tests.
Running tests
To run tests, you can run a pen test
command in your package's directory. Then, you should see test results of test functions in test modules. The pen test
command exits with a non-zero status code if some tests fail.
Concurrency and parallelism
Concurrent programs use CPU time efficiently without blocking on I/O or synchronization. And parallel programs leverage multi-core CPUs to compute something in parallel for speed.
The language provides simple syntax for concurrent and parallel programming. Its go
expression runs a given function concurrently, and possibly in parallel. The expression is composed of the go
keyword followed by a function with no argument.
f = go \() number {
computeExpensive(x, y, z)
}
The go
expression returns a function of the type same as the given expression (a.k.a. futures or promises in other languages.) The function returns a resulting value of the function execution.
The go
expression may or may not run a given function immediately depending on its implementation. For example, the standard Os
system package runs the given function in parallel if multiple CPU cores are available.
Coding style
This page describes the common coding style used in programs written in the language.
Spacing
Use 2 space characters for indentation.
Foo = \(x number) number {
if x == 0 {
"Succeeded!"
} else {
"Failed..."
}
}
Naming convention
Naming is important to keep codes consistent. The language currently has the following naming conventions.
Kind | Case style | Examples |
---|---|---|
Variables | Camel case | fooBar , FooBar , i , x |
Functions | Camel case | fooBar , FooBar , f , g |
Types | Camel case | fooBar , FooBar |
Modules | Camel case | fooBar , FooBar |
Module directories | Camel case | fooBar , FooBar |
Packages | Upper camel case | FooBar |
Global and local names
You should use descriptive names for global functions and types. But, on the other hand, you are encouraged to use abbreviated names for local variables as long as that doesn't incur ambiguity. For example, you might use the following abbreviated names:
i
forindex
c
forrequestCount
sys
forsystem
ctx
forcontext
Acronyms
Acronyms are treated as single words.
Cpu
Ast
References
This chapter details language features, command line tools, and standard packages.
Language
This chapter describes the language's constructs and its type, module and package systems.
Syntax
This page describes syntactical constructs of the language. You can compose programs building up those constructs. See also Types about syntax for specific data types.
Module
Modules are sets of type and function definitions. Syntactically, a module consists of statements. See Modules about how modules themselves interact with each other.
Statements
Statements are constructs that declare functions and types in modules.
Import statement
It imports types and functions from another module from the same or another package.
See Modules for more details.
import Foo'Bar
Foreign import statement
It imports a function from a foreign language.
See Foreign Function Interface (FFI) for more details.
import foreign "c" foo \(number, number) number
Record type definition
It defines a record type.
See Records for more details.
type foo {
bar number
baz string
}
Type alias
It gives another name to a type.
type foo = number | none
Function definition
It defines a function with a given name. The right-hand side of =
signs must be function expressions.
foo = \(x number, y number) number {
x + y
}
Foreign function definition
It defines a function exported to foreign languages.
See Foreign Function Interface (FFI) for more details.
foreign "c" foo = \(x number, y number) number {
x + y
}
Block
A block consists of 1 or more expressions wrapped in {
and }
. Values of the last expressions are treated as resulting values of the blocks.
{
foo(ctx, z)
x + y + z
}
If you want to keep values of intermediate expressions for later use, you can define variables putting their names and =
operators in front of the expressions.
{
x = 42
...
}
Expressions
Expressions represent some computation. Expressions can be nested; expressions often contain other expressions inside.
Function call
It calls a function to evaluate it with given arguments returning its result value.
f(x, y)
Operators
Arithmetic
Arithmetic operators add, subtract, multiply, or divide a number with another.
1 + 1
1 - 1
1 * 1
1 / 1
Comparison
Equality
Equal (==
) and not-equal (!=
) operators compare two values and return a boolean value indicating if they are equal or not.
1 == 1
1 != 1
The operators can compare any types except functions and types containing them.
"foo" == "bar"
foo{x: 0} == foo{x: 1}
42 != none
Ordering
Order operators compare two numbers and return a boolean value indicating if the condition is correct or not.
1 < 1
1 <= 1
1 > 1
1 >= 1
Boolean
A not operator flips a boolean value.
!true
An and operator returns true
if both operands are true
, or false
otherwise.
true & false
An or operator returns true
if either operand is true
, or false
otherwise.
true | false
Error handling
?
suffix operators immediately exit the current functions with operands if they are of the error
type. Both the operands and result values of functions where the operators are used must be a union type containing the error
type.
x?
Function
It creates a function.
First, functions declare their argument names and types (x number
and y number
) and their result types (number
). After that, function bodies of blocks describe how the functions compute result values.
\(x number, y number) number {
x + y
}
Conditionals
If expression
It evaluates one of blocks depending on a condition of an expression of a boolean type.
- It evaluates the first block if a given boolean value is
true
. - Otherwise, it evaluates the second block.
if x {
...
} else {
...
}
If-type expression
It evaluates one of blocks depending on the type of a given expression. The expression (foo()
) needs to be bound to a variable (x
) and, in each block, the variable is treated as its specified type.
if x = foo() as number {
...
} else if string | none {
...
} else {
...
}
If-list expression
It deconstructs a list and evaluates one of blocks depending on if the list is empty or not.
- If a given list has 1 or more element, it evaluates the first block with a function that returns its first element (
x
) and rest of elements as a list (xs
). - If it has no element, it evaluates the second block.
if [x, ...xs] = ... {
...
} else {
...
}
Loop
List comprehension
It iterates over elements in a given list and generates a new list with elements of a given expression.
[number f(x) for x in xs]
Concurrency
go
expression
It executes a function in parallel. Its returned value is a future represented as a function that returns a result of the executed function.
f = go \() number {
...
}
Comment
Comments start with #
and end with new-line characters.
# This is a comment.
Types
This page describes different data types in the language.
Number
It represents a real number. It is implemented as a 64-bit floating point number of IEEE 754.
number
Literals
3.14
-42
Boolean
It is a boolean value of true
or false
.
boolean
Literals
true
false
None
It represents a missing value. It has only a single value of none
.
none
Literals
none
String
It is a byte array.
string
Literals
String literals are sequences of bytes. They are often used to represent texts encoded in UTF-8.
"foo"
Escape sequences
String literals can contain the following escape sequences.
Escape sequence | Name |
---|---|
\n | New line |
\r | Carriage return |
\t | Tab |
\" | Double quote |
\\ | Backslash |
\x9f | Byte |
Functions
A function represents reusable computation with arguments and a result.
Functions represent not only pure computation but may also execute side effects, such as I/O.
\(number, number) number
Literals
\(x number, y number) number {
x + y
}
Lists
It is a list of values. Its element type is put between [
and ]
.
[number]
Literals
A list literal contains its element type and elements as expressions.
[number]
[number 1]
[number 1, 2, 3]
You can create new lists from existing ones by spreading elements of the old ones prefixed by ...
into the new ones.
[number x, ...xs]
Note that expressions within list literals are evaluated lazily; they are evaluated only if their values are required.
Records
It combines multiple types into a single type. Each field of a record type is composed of its name and type.
Fields are not accessible outside modules where they are defined by default.
type person {
name string
age number
}
To expose fields as well as the type itself to other modules, you need to capitalize their names.
type Person {
Name string
Age number
}
Literals
Record values are constructed using record literals containing their field names and values separated by commas.
person{name: "foo", age: 42}
You can also create new records from existing ones spreading fields of the old ones into the new ones.
person{...one, name: "bar"}
You can access field values by appending their names with .
prefixes to expressions of record types.
john.name
Unions
It is a union of multiple types.
For example, the type below represents values that can be either number
or none
.
number | none
Any
Literally, it's an any type. Any values can be converted to the type.
any
Built-ins
Built-in types and functions are ones implicitly defined in every module.
Types
error
It is a special record type used for error handling. See also Error handling.
type error {
...
}
Functions
error
It creates an error with its source information.
\(info any) error
source
It extracts source information from an error.
\(e error) any
debug
It prints a debug message given as an argument if a PEN_DEBUG
environment variable is set.
Note that behavior of this function can change among system packages. You may not even see any messages with system packages whose systems do not have any consoles.
\(message string) none
Modules
Modules are sets of functions and types. Using modules, you can split large programs into smaller chunks.
Each source file suffixed with a .pen
file extension composes a module. Modules can import functions and types from other modules.
Exporting functions and types from modules
You can name functions and types in an upper camel case for them to be accessible from other modules.
type Foo {
...
}
type Bar = ...
Foo = \() number {
...
}
Importing functions and types from modules
In order to import functions and types from other modules, place import statements at the top of modules.
The first components in the statements are names of external packages you declare in package configuration files (Foo
.) They are omitted if imported modules are in the same packages. The rest of the components are directory names where the modules exist (Bar
) and the modules' filenames without their file extensions (Baz
for Baz.pen
.)
import Foo'Bar'Baz
Then, you can access exported members of the modules with their prefixes.
type foo = Baz'Type
bar = \(x number) number {
Baz'Function(x)
}
Module names
Modules in the same package
Modules in the same package are referenced by their paths relative to a root directory of the package.
For example, a module of a file at <package directory>/Foo/Bar.pen
is imported as below.
import 'Foo'Bar
Modules in other packages
Modules in other packages are referenced by their package names defined in package configuration files and module paths.
For example, a module of a file at <package directory>/Bar/Baz.pen
in a package Foo
is imported as below.
import Foo'Bar'Baz
Private modules
For modules to be private and not accessible from other packages, you can name them in lower camel case (e.g. fooBar
.)
Custom prefixes
Imported modules can have custom prefixes given different names after the as
keywords.
import Foo'Bar'Baz as Blah
Unqualified import
You can import functions and types without prefixes by putting their names between {
and }
in import statements. This is especially useful when module names and imported functions or types have the same names like import 'MyType { MyType }
.
import Foo'Bar { Foo, Bar }
type Baz {
foo Foo
}
Blah = \() number {
Bar()
}
Packages
Packages are sets of modules. Like modules, packages can import other packages specifying them in their configurations.
What composes a package?
The following entities compose packages.
- Standard packages bundled in installation of the language
- Remote repositories managed by version control systems (VCS)
- Currently, the language supports only Git as a VCS.
- Directories with package configuration files on file systems
During builds of packages, the language's build system automatically download and initialize their dependency packages based on their URLs.
Package types
There are 3 package types: application, library, and system. Those types are specified in package configuration files.
Application packages
Application packages build applications often as executable files. Every application package must have a main.pen
module file at its top directory. The main module has a main
function that receives an argument of a context
type and returns a none
type. The context
type is a record type containing context values of system packages with their field names of package names. For example, given system packages named Http
and Os
, a main function looks like the following.
main = \(ctx context) none {
s = fetch(ctx.Http, "https://pen-lang.org/")
print(ctx.Os, s)
none
}
Every application package must specify one and only one system package that links applications (e.g. the Os
standard system package) in its package configuration file. However, application packages can specify system packages that do not link applications (e.g. the Http
system package in the example above) as many as possible.
Library packages
Library packages contain functions and types that have no side effects. They are imported and used by other packages.
System packages
System packages contain functions and types that have side effects to provide system interfaces to application packages. The language currently provides the two standard system packages of Os
and OsSync
.
Although they can be imported by library packages as well as application packages, then they are expected not to cause any side effects.
If you want to write your own system packages, see Writing system packages.
Package configuration
Each package has its configuration file named pen.json
in a JSON format at its top directory. The JSON file has the following fields.
Name | Required | Description |
---|---|---|
type | Yes | Package type (either application , library , or system ) |
dependencies | Yes | Map of package names to their URLs |
Package URLs have different protocol schemes depending on where they are located.
- Standard packages:
pen
- Git repositories:
git
- Directories on file systems: none
Examples
Application
{
"type": "application",
"dependencies": {
"Os": "pen:///os",
"Core": "pen:///core",
"Foo": "git://github.com/foo/foo",
"Bar": "../bar"
}
}
Library
{
"type": "library",
"dependencies": {
"Core": "pen:///core",
"Foo": "git://github.com/foo/foo",
"Bar": "../bar"
}
}
System
{
"type": "system",
"dependencies": {
"Core": "pen:///core",
"Foo": "git://github.com/foo/foo",
"Bar": "../bar"
}
}
Command line tools
The pen
command has the following sub-commands.
For more information, see its help message by running pen --help
.
build
command
It builds a package in the current directory.
pen build
create
command
It creates a package of a given kind in a specified directory.
Creating an application package
pen create foo
Creating a library package
pen create --library foo
Standard packages
Standard packages provide common utility functions and system interfaces. They are installed together with the pen
command on your system. This chapter describes their APIs and how to use them.
Core
package
This package provides common algorithms and data structures.
Install
{
"dependencies": {
"Core": "pen:///core"
}
}
Core'Bit
module
This module provides bitwise operations.
Most functions defined in this module take arguments of 64-bit integers.
They can be converted from and into integers represented in IEEE-754 of a
number
type.
Types
No types are defined.
Functions
And
Calculate bitwise "and" given two 64-bit integers.
\(x number, y number) number
Or
Calculate bitwise "or" given two 64-bit integers.
\(x number, y number) number
Xor
Calculate bitwise exclusive-"or" given two 64-bit integers.
\(x number, y number) number
Not
Calculate bitwise "not" given two 64-bit integers.
\(x number) number
LeftShift
Calculate unsigned left shift given a 64-bit integer.
\(x number, n number) number
RightShift
Calculate unsigned right shift given a 64-bit integer.
\(x number, n number) number
ToInteger64
Convert an integer in IEEE-754 to a 64-bit integer.
\(x number) number
FromInteger64
Convert a 64-bit integer to an integer in IEEE-754.
\(x number) number
Core'Boolean
module
This module provides common boolean operations.
Types
No types are defined.
Functions
Any
Return true if any of given booleans are true or false otherwise.
\(bs [boolean]) boolean
All
Return true if all of given booleans are true or false otherwise.
\(bs [boolean]) boolean
Core'List
module
This module provides common list operations.
Types
No types are defined.
Functions
Length
Calculate a length of a list.
\(xs [any]) number
First
Get the first element in a list. If a list is empty, it returns a fallback value.
\(xs [any], fallback any) any
Last
Get the last element in a list. If a list is empty, it returns a fallback value.
\(xs [any], fallback any) any
ToNumbers
Convert a list of an any
type to one of a number
type skipping non-number
types.
\(xs [any]) [number]
ToStrings
Convert a list of an any
type to one of a string
type skipping non-string
types.
\(xs [any]) [string]
ToBooleans
Convert a list of an any
type to one of a boolean
type skipping non-boolean
types.
\(xs [any]) [boolean]
Core'Number
module
This module provides common number operations.
Types
No types are defined.
Functions
String
Convert a number into its string representation.
\(x number) string
Sum
Sum up numbers.
\(xs [number]) number
Remainder
Calculate a remainder.
\(dividend number, divisor number) number
Power
Calculate a power.
\(base number, exponent number) number
SquareRoot
Calculate a square root.
\(x number) number
Sequence
Create a list of numbers from 1 to a maximum value.
\(maximum number) [number]
Range
Create a list of numbers from a minimum to a maximum.
\(minimum number, maximum number) [number]
IsNan
Check if a number is NaN.
\(x number) boolean
Ceil
Calculate a ceil value.
\(x number) number
Floor
Calculate a floor value.
\(x number) number
Nan
NaN
\() number
Infinity
Infinity
\() number
Minimum
Calculate a minimum.
\(x number, y number) number
Maximum
Calculate a maximum.
\(x number, y number) number
Core'String
module
This module provides common string operations.
Types
No types are defined.
Functions
Concatenate
Concatenate strings.
\(strings [string]) string
Join
Join strings with a separator.
\(strings [string], separator string) string
HasPrefix
Check if a string has a prefix.
\(s string, prefix string) boolean
Core'String'Byte
module
This module provides operations for strings as byte arrays.
Types
No types are defined.
Functions
Length
Return a length of a string.
\(s string) number
Slice
Slice a string.
\(s string, start number, end number) string
Core'String'Byte'View
module
This module provides views of strings as byte arrays. By using those views, you can apply operations to strings without copying them.
Types
View
A view of a string
type View {
# ...
}
Functions
New
Create a view of a string.
\(s string) View
Viewee
Get an original string.
\(v View) string
Start
Get a start index of a view.
\(v View) number
End
Get an end index of a view.
\(v View) number
HasPrefix
Check if a string has a prefix.
\(v View, s string) boolean
Length
Get a length of a view.
\(v View) number
Seek
Move a start index forward.
\(v View, n number) View
Slice
Slice a view.
\(v View, start number, end number) View
ToString
Convert a view into a string.
\(v View) string
Core'String'Utf8
module
This module provides operations for strings encoded in UTF-8.
Types
No types are defined.
Functions
Length
Return a length of a string.
\(s string) number
Slice
Slice a string.
\(s string, start number, end number) string
Flag
package
This package provides command-line flag parsing.
Install
{
"dependencies": {
"Flag": "pen:///flag"
}
}
Flag'Flag
module
This module provides command-line flag parsing.
The flags have the following formats:
-foo bar
: a named flag of a keyfoo
with a valuebar
foo
: a positional flag of a valuefoo
Named flags and positional flags are separated by --
optionally.
Types
Set
A set of flags
type Set {
Named {string: string}
Positional [string]
}
Functions
Parse
Parse flags.
\(ss [string]) Set | error
Json
package
This package provides a JSON parser.
Install
{
"dependencies": {
"Json": "pen:///json"
}
}
Json'Decode
module
Types
No types are defined.
Functions
Decode
Decode a string into a JSON value.
\(s string) Value | error
Json'Encode
module
Types
No types are defined.
Functions
Encode
Encode a JSON value.
\(v Value) string
Json'Value
module
Types
Value
A JSON value
type Value {
# ...
}
Raw
A raw JSON value represented by built-in types
type Raw = boolean | none | number | string | [Value] | {string: Value}
Functions
New
Create a JSON value.
\(r Raw) Value
Raw
Get a raw value.
\(v Value) Raw
Http
package
This package provides HTTP client and server.
Install
{
"dependencies": {
"Http": "pen:///http"
}
}
Http'Client
module
This module provides an HTTP client.
Types
No types are defined.
Functions
Send
Send an HTTP request.
\(ctx Context, r Request) Response | error
Http'Context
module
Types
Context
An HTTP context
type Context = context'Context
Functions
No functions are defined.
Http'Request
module
Types
Request
An HTTP request
type Request {
Method string
Uri string
Headers {string: string}
Body string
}
Functions
No functions are defined.
Http'Response
module
Types
Response
An HTTP response
type Response {
Status number
Headers {string: string}
Body string
}
Functions
No functions are defined.
Http'Server
module
This module provides an HTTP server.
Types
No types are defined.
Functions
Serve
Run an HTTP service.
\(ctx Context, address string, callback \(Request) Response) none | error
Os
package
This package provides an interface for operating systems.
Install
{
"dependencies": {
"Os": "pen:///os"
}
}
Os'Context
module
Types
Context
A context of an operating system.
type Context = context'Context
Functions
No functions are defined.
Os'Directory
module
Types
No types are defined.
Functions
Read
Read a directory and return file paths it contains.
\(ctx Context, path string) [string] | error
Create
Create a directory.
\(ctx Context, path string) none | error
Remove
Remove a directory.
\(ctx Context, path string) none | error
Os'Environment
module
Types
No types are defined.
Functions
Arguments
Get command line arguments.
\(ctx Context) [string]
Variable
Get an environment variable.
\(ctx Context, name string) string | error
Os'File
module
Types
File
A file on a file system.
type File {
# ...
}
Functions
StdIn
A file of standard input.
\() File
StdOut
A file of standard output.
\() File
StdErr
A file of standard error.
\() File
Open
Open a file for read-only.
\(ctx Context, path string) File | error
OpenWithOptions
Open a file with options.
\(ctx Context, path string, opt OpenOptions) File | error
Read
Read a file.
\(ctx Context, file File) string | error
ReadLimit
Read a file until a size limit.
\(ctx Context, file File, limit number) string | error
Write
Write data to a file.
\(ctx Context, file File, data string) number | error
Copy
Copy a file to another path.
\(ctx Context, src string, dest string) none | error
Move
Move a file to another path.
\(ctx Context, src string, dest string) none | error
Remove
Remove a file.
\(ctx Context, path string) none | error
Metadata
Get metadata of a file at a path.
\(ctx Context, path string) Metadata | error
Os'File'Metadata
module
Types
Metadata
File metadata
type Metadata {
Size number
}
Functions
No functions are defined.
Os'File'OpenOptions
module
Types
OpenOptions
Options to open a file
Append
allows appending data to the file.Create
creates a new file if the file doesn't exist or opens it otherwise.CreateNew
creates a new file. If the file already exists, it emits an error.Read
allows reading data from the file.Truncate
truncates the file to zero byte.Write
allows writing data to the file.
type OpenOptions {
Append boolean
Create boolean
CreateNew boolean
Read boolean
Truncate boolean
Write boolean
}
Functions
Default
Get default options to open a file. They do not include any permission.
\() OpenOptions
Os'Process
module
Types
No types are defined.
Functions
Exit
Exit a current process.
\(ctx Context, code number) none
Os'Random
module
Types
No types are defined.
Functions
Number
Generate a random number in a range of [0, 1).
\(ctx Context) number
Os'Tcp
module
Types
No types are defined.
Functions
Bind
Create a listener bound to a server address.
\(ctx Context, address string) Listener | error
Connect
Create a stream connected to a peer address.
\(ctx Context, address string) Stream | error
Accept
Accept a client connection and create its stream.
\(ctx Context, l Listener) AcceptedStream | error
Receive
Receive data from a peer through a stream with a size limit in bytes.
\(ctx Context, s Stream, limit number) string | error
Send
Send data to a peer through a stream.
\(ctx Context, s Stream, data string) number | error
Os'Tcp'AcceptedStream
module
Types
AcceptedStream
A TCP stream accepted on a server with a client address
type AcceptedStream {
Stream Stream
Address string
}
Functions
No functions are defined.
Os'Tcp'Listener
module
Types
Listener
A TCP listener to listen for client connections
type Listener {
# ...
}
Functions
No functions are defined.
Os'Tcp'Stream
module
Types
Stream
A TCP stream
type Stream {
# ...
}
Functions
No functions are defined.
Os'Time
module
Types
No types are defined.
Functions
Now
Fetch a current system time in milliseconds.
\(ctx Context) number
Sleep
Pause a current execution context for a given amount of time.
\(ctx Context, milliseconds number) none
Os'Udp
module
Types
No types are defined.
Functions
Bind
Bind a socket with a server address.
\(ctx Context, address string) Socket | error
Connect
Connect a socket to a peer address.
\(ctx Context, s Socket, address string) none | error
Receive
Receive a datagram from a connected address.
\(ctx Context, s Socket) string | error
ReceiveFrom
Receive a datagram from any address.
\(ctx Context, s Socket) Datagram | error
Send
Send a datagram to a connected address.
\(ctx Context, s Socket, data string) number | error
SendTo
Send a datagram to a specified address.
\(ctx Context, s Socket, data string, address string) number | error
Os'Udp'Datagram
module
Types
Datagram
UDP datagram
type Datagram {
Data string
Address string
}
Functions
No functions are defined.
Os'Udp'Socket
module
Types
Socket
UDP socket
type Socket {
# ...
}
Functions
No functions are defined.
Sql
package
This package provides a SQL database client.
Install
{
"dependencies": {
"Sql": "pen:///sql"
}
}
Sql'Context
module
Types
Context
A SQL database context
type Context = context'Context
Functions
No functions are defined.
Sql'Pool
module
Types
Pool
A connection pool
type Pool = pool'Pool
Functions
New
Create a connection pool.
\(context Context, uri string, options Options) Pool | error
Query
Run a query and return its rows.
\(context Context, pool Pool, query string, arguments [Value]) [[Value]] | error
Execute
Run a query and return a number of affected rows.
\(context Context, pool Pool, query string, arguments [Value]) number | error
Sql'Pool'Options
module
Types
Options
Connection pool options
type Options {
MinConnections number
MaxConnections number
ConnectTimeout number
}
Functions
No functions are defined.
Sql'Value
module
Types
Value
A value in a column
type Value = boolean | none | number | string
Functions
No functions are defined.
Test
package
This package provides test utilities.
Install
{
"dependencies": {
"Test": "pen:///test"
}
}
Test'Assert
module
This module provides a collection of assertion logic for testing.
Types
No types are defined.
Functions
True
Assert that a condition is true.
\(x boolean) none | error
Error
Check if a value is an error.
\(x any) none | error
Fail
Fail with an error immediately. This function is useful to make unreachable codes fail.
\() error
Examples
These are examples on how to use the language, its command line tools, and libraries.
Types
These are examples on how to use types and their values in the language.
Number
Background
Given a file named "pen.json" with:
{
"type": "library",
"dependencies": {}
}
Use a number literal
Given a file named "Foo.pen" with:
f = \() number {
42
}
When I run pen build
Then the exit status should be 0.
Use arithmetic operators
Given a file named "Foo.pen" with:
f = \() number {
1 + 2 - 3 * 4 / 5
}
When I run pen build
Then the exit status should be 0.
Use equality operators
Given a file named "Foo.pen" with:
f = \() boolean {
0 == 0
}
g = \() boolean {
0 != 0
}
When I run pen build
Then the exit status should be 0.
Use order operators
Given a file named "Foo.pen" with:
f = \() boolean {
0 < 0
}
g = \() boolean {
0 <= 0
}
h = \() boolean {
0 > 0
}
i = \() boolean {
0 >= 0
}
When I run pen build
Then the exit status should be 0.
Boolean
Background
Given a file named "pen.json" with:
{
"type": "library",
"dependencies": {}
}
Use boolean literals
Given a file named "Foo.pen" with:
f = \() boolean {
true
}
g = \() boolean {
false
}
When I run pen build
Then the exit status should be 0.
Use an and operation
Given a file named "Foo.pen" with:
f = \() boolean {
true & false
}
When I run pen build
Then the exit status should be 0.
Use an or operation
Given a file named "Foo.pen" with:
f = \() boolean {
true | false
}
When I run pen build
Then the exit status should be 0.
Use a not operation
Given a file named "Foo.pen" with:
f = \() boolean {
!true
}
When I run pen build
Then the exit status should be 0.
Use an if expression
Given a file named "Foo.pen" with:
f = \() number {
if true {
1
} else {
0
}
}
When I run pen build
Then the exit status should be 0.
None
Background
Given a file named "pen.json" with:
{
"type": "library",
"dependencies": {}
}
Use a none literal
Given a file named "Foo.pen" with:
f = \() none {
none
}
When I run pen build
Then the exit status should be 0.
String
Background
Given a file named "pen.json" with:
{
"type": "library",
"dependencies": {}
}
Use a string literal
Given a file named "Foo.pen" with:
f = \() string {
"foo"
}
When I run pen build
Then the exit status should be 0.
Use equality operators
Given a file named "Foo.pen" with:
f = \() boolean {
"" == ""
}
g = \() boolean {
"" != ""
}
When I run pen build
Then the exit status should be 0.
Function
Background
Given a file named "pen.json" with:
{
"type": "library",
"dependencies": {}
}
Define a function
Given a file named "Foo.pen" with:
f = \(x number) number {
x
}
When I run pen build
Then the exit status should be 0.
Call a function with no argument
Given a file named "Foo.pen" with:
f = \() number {
f()
}
When I run pen build
Then the exit status should be 0.
Call a function with an argument
Given a file named "Foo.pen" with:
f = \(x number) number {
f(x)
}
When I run pen build
Then the exit status should be 0.
Call a function with two arguments
Given a file named "Foo.pen" with:
f = \(x number, y number) number {
f(x, y)
}
When I run pen build
Then the exit status should be 0.
Define a closure
Given a file named "Foo.pen" with:
f = \(x number) \(number) number {
\(y number) number {
x + y
}
}
When I run pen build
Then the exit status should be 0.
Record
Background
Given a file named "pen.json" with:
{
"type": "library",
"dependencies": {}
}
Create a record with a field
Given a file named "Foo.pen" with:
type r {
x number
}
f = \() r {
r{x: 42}
}
When I run pen build
Then the exit status should be 0.
Create a record with two fields
Given a file named "Foo.pen" with:
type r {
x number
y none
}
f = \() r {
r{x: 42, y: none}
}
When I run pen build
Then the exit status should be 0.
Create a record with no field
Given a file named "Foo.pen" with:
type r {}
f = \() r {
r{}
}
When I run pen build
Then the exit status should be 0.
Update a record
Given a file named "Foo.pen" with:
type r {
x number
y none
}
f = \(x r) r {
r{...x, y: none}
}
When I run pen build
Then the exit status should be 0.
Get a field in a record
Given a file named "Foo.pen" with:
type r {
x number
}
f = \(x r) number {
x.x
}
When I run pen build
Then the exit status should be 0.
Use an equal operator
Given a file named "Foo.pen" with:
type r {
x number
}
f = \(x r, y r) boolean {
x == y
}
When I run pen build
Then the exit status should be 0.
Use a not-equal operator
Given a file named "Foo.pen" with:
type r {
x number
}
f = \(x r, y r) boolean {
x == y
}
When I run pen build
Then the exit status should be 0.
Union
Background
Given a file named "pen.json" with:
{
"type": "library",
"dependencies": {}
}
Upcast a number into a union
Given a file named "Foo.pen" with:
f = \() number | none {
42
}
When I run pen build
Then the exit status should be 0.
Upcast a function into a union
Given a file named "Foo.pen" with:
f = \() (\() number) | none {
\() number {
42
}
}
When I run pen build
Then the exit status should be 0.
Upcast a list into a union
Given a file named "Foo.pen" with:
f = \() [number] | none {
[number 42]
}
When I run pen build
Then the exit status should be 0.
Downcast a union type
Given a file named "Foo.pen" with:
f = \(x number | none) number {
if x = x as number {
x
} else if none {
0
}
}
When I run pen build
Then the exit status should be 0.
Downcast a union type with an else block
Given a file named "Foo.pen" with:
f = \(x number | none) number {
if x = x as none {
0
} else {
x
}
}
When I run pen build
Then the exit status should be 0.
Downcast a union type to another union type
Given a file named "Foo.pen" with:
f = \(x number | boolean | none) number | none {
if x = x as number | none {
x
} else {
none
}
}
When I run pen build
Then the exit status should be 0.
List
Background
Given a file named "pen.json" with:
{
"type": "library",
"dependencies": {}
}
Create an empty list
Given a file named "Foo.pen" with:
f = \() [number] {
[number]
}
When I run pen build
Then the exit status should be 0.
Create a list with an element
Given a file named "Foo.pen" with:
f = \() [number] {
[number 1]
}
When I run pen build
Then the exit status should be 0.
Create a list with two elements
Given a file named "Foo.pen" with:
f = \() [number] {
[number 1, 2]
}
When I run pen build
Then the exit status should be 0.
Join lists
Given a file named "Foo.pen" with:
f = \(xs [number]) [number] {
[number ...xs, ...xs]
}
When I run pen build
Then the exit status should be 0.
Create a list of a union type
Given a file named "Foo.pen" with:
f = \() [number | none] {
[number | none 1, none]
}
When I run pen build
Then the exit status should be 0.
Coerce elements of a spread list
Given a file named "Foo.pen" with:
f = \(xs [number]) [number | none] {
[number | none ...xs]
}
When I run pen build
Then the exit status should be 0.
Use if-list expression
Given a file named "Foo.pen" with:
f = \(xs [number]) [number] {
if [y, ...ys] = xs {
[number y(), ...ys]
} else {
[number]
}
}
When I run pen build
Then the exit status should be 0.
Use list comprehension
Given a file named "Foo.pen" with:
f = \(xs [number]) [number] {
[number x() + 42 for x in xs]
}
When I run pen build
Then the exit status should be 0.
Error
Background
Given a file named "pen.json" with:
{
"type": "library",
"dependencies": {}
}
Call an error function
Given a file named "Foo.pen" with:
f = \() error {
error(none)
}
When I run pen build
Then the exit status should be 0.
Call a source function
Given a file named "Foo.pen" with:
f = \(e error) any {
source(e)
}
When I run pen build
Then the exit status should be 0.
Use a try operator
Given a file named "Foo.pen" with:
f = \(x number | error) number | error {
x? + 1
}
When I run pen build
Then the exit status should be 0.
Use a try operator with a union type
Given a file named "Foo.pen" with:
f = \(x number | none | error) number | error {
if x = x? as number {
x + 1
} else if none {
0
}
}
When I run pen build
Then the exit status should be 0.
Any
Background
Given a file named "pen.json" with:
{
"type": "library",
"dependencies": {}
}
Use an any type
Given a file named "Foo.pen" with:
f = \() any {
42
}
When I run pen build
Then the exit status should be 0.
Downcast an any type
Given a file named "Foo.pen" with:
f = \(x any) number {
if x = x as number {
x
} else {
0
}
}
When I run pen build
Then the exit status should be 0.
Polymorphism
Background
Given a file named "pen.json" with:
{
"type": "library",
"dependencies": {}
}
Use an equal operator
Given a file named "Foo.pen" with:
f = \(x number | none) boolean {
x == none
}
When I run pen build
Then the exit status should be 0.
Use a not-equal operator
Given a file named "Foo.pen" with:
f = \(x number | none) boolean {
x != none
}
When I run pen build
Then the exit status should be 0.
Compare unions
Given a file named "Foo.pen" with:
f = \(x number | none, y number | none) boolean {
x == y
}
When I run pen build
Then the exit status should be 0.
Compare a union and none
Given a file named "Foo.pen" with:
f = \(x number | none) boolean {
x == none
}
When I run pen build
Then the exit status should be 0.
Syntax
These are examples on how to compose programs in the language.
Block
Background
Given a file named "pen.json" with:
{
"type": "library",
"dependencies": {}
}
Define a variable
Given a file named "Foo.pen" with:
f = \(x number) number {
y = x
y
}
When I run pen build
Then the exit status should be 0.
Call a function
Given a file named "Foo.pen" with:
f = \() none {
none
}
g = \() none {
f()
none
}
When I run pen build
Then the exit status should be 0.
Use if expression
Given a file named "Foo.pen" with:
f = \() none {
none
}
g = \() none {
none
}
h = \(x boolean) none {
if x {
f()
} else {
g()
}
none
}
When I run pen build
Then the exit status should be 0.
Modules
Background
Given a file named "pen.json" with:
{
"type": "library",
"dependencies": {}
}
Import a function from a module
Given a file named "Foo.pen" with:
Foo = \() number {
42
}
And a file named "Bar.pen" with:
import 'Foo
Bar = \() number {
Foo'Foo()
}
When I run pen build
Then the exit status should be 0.
Import a type alias from a module
Given a file named "Foo.pen" with:
type Foo = number
And a file named "Bar.pen" with:
import 'Foo
type Bar = Foo'Foo
When I run pen build
Then the exit status should be 0.
Import a function from a nested module
Given a file named "Foo/Foo.pen" with:
Foo = \() number {
42
}
And a file named "Bar.pen" with:
import 'Foo'Foo
Bar = \() number {
Foo'Foo()
}
When I run pen build
Then the exit status should be 0.
Import a module with a custom prefix
Given a file named "Foo.pen" with:
Foo = \() number {
42
}
And a file named "Bar.pen" with:
import 'Foo as Bar
Bar = \() number {
Bar'Foo()
}
When I run pen build
Then the exit status should be 0.
Import a type definition with no prefix
Given a file named "Foo.pen" with:
type Foo {}
And a file named "Bar.pen" with:
import 'Foo { Foo }
type Bar = Foo
When I run pen build
Then the exit status should be 0.
Import a type alias with no prefix
Given a file named "Foo.pen" with:
type Foo = number
And a file named "Bar.pen" with:
import 'Foo { Foo }
type Bar = Foo
When I run pen build
Then the exit status should be 0.
Import a function with no prefix
Given a file named "Foo.pen" with:
Foo = \() number {
42
}
And a file named "Bar.pen" with:
import 'Foo { Foo }
Bar = \() number {
Foo()
}
When I run pen build
Then the exit status should be 0.
Packages
Background
Given a file named "foo/pen.json" with:
{
"type": "library",
"dependencies": {}
}
And a file named "foo/Foo.pen" with:
type Foo = number
Foo = \() number {
42
}
And a file named "foo/Foo/Foo.pen" with:
Foo = \() number {
42
}
And a directory named "bar"
And I cd to "bar"
And a file named "pen.json" with:
{
"type": "library",
"dependencies": {
"Foo": "../foo"
}
}
Import a function from a module
Given a file named "Bar.pen" with:
import Foo'Foo
Bar = \() number {
Foo'Foo()
}
When I run pen build
Then the exit status should be 0.
Import a type from a module
Given a file named "Bar.pen" with:
import Foo'Foo
type Bar = Foo'Foo
When I run pen build
Then the exit status should be 0.
Import a function from a nested module
Given a file named "Bar.pen" with:
import Foo'Foo'Foo
Bar = \() number {
Foo'Foo()
}
When I run pen build
Then the exit status should be 0.
Commands
These are examples on how to use command line tools of the language.
Building packages
Build an application package
Given a file named "pen.json" with:
{
"type": "application",
"dependencies": {
"Os": "pen:///os"
}
}
And a file named "main.pen" with:
import Os'Context { Context }
main = \(ctx context) none {
none
}
When I successfully run pen build
Then I successfully run ./app
.
Build a library package
Given a file named "pen.json" with:
{
"type": "library",
"dependencies": {}
}
And a file named "Foo.pen" with:
f = \(x number) number {
x
}
When I run pen build
Then the exit status should be 0.
Cross-build an application package
Given a file named "pen.json" with:
{
"type": "application",
"dependencies": {
"Os": "pen:///os-sync"
}
}
And a file named "main.pen" with:
import Os'Context { Context }
main = \(ctx context) none {
none
}
And I successfully run rustup target add <target>
When I run pen build --target <target>
Then the exit status should be 0.
Examples
target |
---|
i686-unknown-linux-musl |
x86_64-unknown-linux-musl |
aarch64-unknown-linux-musl |
wasm32-wasi |
Cross-build a library package
Given a file named "pen.json" with:
{
"type": "library",
"dependencies": {}
}
And a file named "Foo.pen" with:
f = \(x number) number {
x
}
And I successfully run rustup target add <target>
When I run pen build --target <target>
Then the exit status should be 0.
Examples
target |
---|
i686-unknown-linux-musl |
x86_64-unknown-linux-musl |
aarch64-unknown-linux-musl |
wasm32-wasi |
Creating packages
Create an application package
Given I successfully run pen create foo
And I cd to "foo"
When I successfully run pen build
Then I successfully run ./app
.
Create a library package
Given I successfully run pen create --library foo
And I cd to "foo"
When I run pen build
Then the exit status should be 0.
Create an application package in a current directory
Given I successfully run pen create .
When I successfully run pen build
Then I successfully run ./app
.
Create a library package in a current directory
Given I successfully run pen create --library .
When I run pen build
Then the exit status should be 0.
Testing packages
Background
Given a file named "pen.json" with:
{
"type": "library",
"dependencies": {
"Test": "pen:///test"
}
}
And a file named "Foo.pen" with:
Add = \(x number, y number) number {
x + y
}
Test a module
Given a file named "Foo.test.pen" with:
import Test'Assert
import 'Foo
Add = \() none | error {
Assert'True(Foo'Add(41, 1) == 42)
}
When I run pen test
Then the exit status should be 0
And the stdout should contain "OK".
Fail to test a module
Given a file named "Foo.test.pen" with:
import Test'Assert
import 'Foo
Add = \() none | error {
Assert'True(Foo'Add(41, 0) == 42)
}
When I run pen test
Then the exit status should not be 0
And the stdout should contain "FAIL".
Run no test
When I run pen test
Then the exit status should be 0.
Use a debug function in a test
Given a file named "Foo.test.pen" with:
Foo = \() none | error {
debug("hello")
}
And I append "1" to the environment variable "PEN_DEBUG"
When I run pen_test_on_linux.sh
Then the exit status should be 0.
Formatting module files
Background
Given a file named "pen.json" with:
{
"type": "library",
"dependencies": {}
}
Format module files
Given a file named "Foo.pen" with:
Foo = \() none {
none
}
When I successfully run pen format
Then a file named "Foo.pen" should contain exactly:
Foo = \() none {
none
}
Check if module files are formatted
Given a file named "Foo.pen" with:
Foo = \() none {
none
}
When I run pen format --checked
Then the exit status should not be 0.
Standard packages
These are examples on how to use standard packages.
Core
Background
Given a file named "pen.json" with:
{
"type": "library",
"dependencies": {
"Core": "pen:///core"
}
}
Convert a number to a string
Given a file named "Foo.pen" with:
import Core'Number
f = \() string {
Number'String(42)
}
When I run pen build
Then the exit status should be 0.
Sum numbers
Given a file named "Foo.pen" with:
import Core'Number
f = \() number {
Number'Sum([number 1, 2, 3])
}
When I run pen build
Then the exit status should be 0.
Join strings
Given a file named "Foo.pen" with:
import Core'String
f = \() string {
String'Join([string "hello", "world"], " ")
}
When I run pen build
Then the exit status should be 0.
Slice a string as bytes
Given a file named "Foo.pen" with:
import Core'String'Byte
f = \() string {
Byte'Slice("foo", 1, 2)
}
When I run pen build
Then the exit status should be 0.
Get a length of a list
Given a file named "Foo.pen" with:
import Core'List
f = \() number {
xs = [none]
List'Length([any ...xs])
}
When I run pen build
Then the exit status should be 0.
Os
Background
Given a file named "pen.json" with:
{
"type": "application",
"dependencies": {
"Os": "pen:///os",
"Core": "pen:///core"
}
}
Build an application
Given a file named "main.pen" with:
import Os'Context { Context }
import Os'File
import Os'Process
main = \(ctx context) none {
if _ = run(ctx.Os) as none {
none
} else {
Process'Exit(ctx.Os, 1)
}
}
run = \(ctx Context) none | error {
File'Write(ctx, File'StdOut(), "Hello, world!")?
none
}
When I successfully run pen build
Then I successfully run ./app
And the stdout from "./app" should contain exactly "Hello, world!".
Get arguments
Given a file named "main.pen" with:
import Core'String
import Os'Context { Context }
import Os'Environment
import Os'File
import Os'Process
main = \(ctx context) none {
if _ = run(ctx.Os) as none {
none
} else {
Process'Exit(ctx.Os, 1)
}
}
run = \(ctx Context) none | error {
File'Write(ctx, File'StdOut(), String'Join(Environment'Arguments(ctx), " "))?
none
}
When I successfully run pen build
Then I successfully run ./app foo bar
And stdout from "./app foo bar" should contain exactly "foo bar".
Get an environment variable
Given a file named "main.pen" with:
import Core'String
import Os'Context { Context }
import Os'File
import Os'Environment
import Os'Process
main = \(ctx context) none {
if _ = run(ctx.Os) as none {
none
} else {
Process'Exit(ctx.Os, 1)
}
}
run = \(ctx Context) none | error {
File'Write(ctx, File'StdOut(), Environment'Variable(ctx, "FOO")?)?
none
}
And I append "foo" to the environment variable "FOO"
When I successfully run pen build
Then I successfully run ./app
And stdout from "./app" should contain exactly "foo".
Open a file
Given a file named "main.pen" with:
import Os'Context { Context }
import Os'File { File }
import Os'Process
main = \(ctx context) none {
if _ = run(ctx.Os) as none {
none
} else {
Process'Exit(ctx.Os, 1)
}
}
run = \(ctx Context) none | error {
File'Open(ctx, "./foo.txt")?
none
}
And a file named "foo.txt" with ""
When I successfully run pen build
Then I successfully run ./app
.
Read a file
Given a file named "main.pen" with:
import Os'Context { Context }
import Os'File
import Os'File'OpenOptions
import Os'Process
main = \(ctx context) none {
if _ = run(ctx.Os) as none {
none
} else {
Process'Exit(ctx.Os, 1)
}
}
run = \(ctx Context) none | error {
f = File'Open(ctx, "foo.txt")?
d = File'Read(ctx, f)?
File'Write(ctx, File'StdOut(), d)?
none
}
And a file named "foo.txt" with "foo"
When I successfully run pen build
Then I successfully run ./app
And the stdout from "./app" should contain exactly "foo".
Read a file until a limit
Given a file named "main.pen" with:
import Os'Context { Context }
import Os'File
import Os'Process
main = \(ctx context) none {
if _ = run(ctx.Os) as none {
none
} else {
Process'Exit(ctx.Os, 1)
}
}
run = \(ctx Context) none | error {
f = File'Open(ctx, "foo.txt")?
d = File'ReadLimit(ctx, f, 5)?
File'Write(ctx, File'StdOut(), d)?
none
}
And a file named "foo.txt" with "Hello, world!"
When I successfully run pen build
Then I successfully run ./app
And the stdout from "./app" should contain exactly "Hello".
Write a file
Given a file named "main.pen" with:
import Os'Context { Context }
import Os'File
import Os'File'OpenOptions { OpenOptions }
import Os'Process
main = \(ctx context) none {
if _ = run(ctx.Os) as none {
none
} else {
Process'Exit(ctx.Os, 1)
}
}
run = \(ctx Context) none | error {
f = File'OpenWithOptions(
ctx,
"./foo.txt",
OpenOptions{...OpenOptions'Default(), Write: true},
)?
File'Write(ctx, f, "foo")?
none
}
And a file named "foo.txt" with ""
When I successfully run pen build
Then I successfully run ./app
And the file "foo.txt" should contain "foo".
Copy a file
Given a file named "main.pen" with:
import Os'Context { Context }
import Os'File
import Os'Process
main = \(ctx context) none {
if _ = File'Copy(ctx.Os, "foo.txt", "bar.txt") as none {
none
} else {
Process'Exit(ctx.Os, 1)
}
}
And a file named "foo.txt" with "foo"
When I successfully run pen build
Then I successfully run ./app
And the file "foo.txt" should contain "foo"
And the file "bar.txt" should contain "foo".
Move a file
Given a file named "main.pen" with:
import Os'Context { Context }
import Os'File
import Os'Process
main = \(ctx context) none {
if _ = File'Move(ctx.Os, "foo.txt", "bar.txt") as none {
none
} else {
Process'Exit(ctx.Os, 1)
}
}
And a file named "foo.txt" with "foo"
When I successfully run pen build
Then I successfully run ./app
And the file "foo.txt" does not exist
And the file "bar.txt" should contain "foo".
Remove a file
Given a file named "main.pen" with:
import Os'Context { Context }
import Os'File
import Os'Process
main = \(ctx context) none {
if _ = File'Remove(ctx.Os, "foo.txt") as none {
none
} else {
Process'Exit(ctx.Os, 1)
}
}
And a file named "foo.txt" with ""
When I successfully run pen build
Then I successfully run ./app
And the file "foo.txt" does not exist.
Read a directory
Given a file named "main.pen" with:
import Core'String
import Os'Context { Context }
import Os'File
import Os'Directory
import Os'Process
main = \(ctx context) none {
if _ = run(ctx.Os) as none {
none
} else {
Process'Exit(ctx.Os, 1)
}
}
run = \(ctx Context) none | error {
File'Write(
ctx,
File'StdOut(),
String'Join(Directory'Read(ctx, ".")?, "\n"),
)?
none
}
When I successfully run pen build
Then I successfully run ./app
And the stdout from "./app" should contain "main.pen"
And the stdout from "./app" should contain "pen.json".
Create a directory
Given a file named "main.pen" with:
import Os'Context { Context }
import Os'Directory
import Os'Process
main = \(ctx context) none {
if _ = Directory'Create(ctx.Os, "foo") as none {
none
} else {
Process'Exit(ctx.Os, 1)
}
}
When I successfully run pen build
Then I successfully run ./app
And a directory named "foo" should exist.
Remove a directory
Given a file named "main.pen" with:
import Os'Context { Context }
import Os'Directory
import Os'Process
main = \(ctx context) none {
if _ = Directory'Remove(ctx.Os, "foo") as none {
none
} else {
Process'Exit(ctx.Os, 1)
}
}
And a directory named "foo"
When I successfully run pen build
Then I successfully run ./app
And a directory named "foo" should not exist.
Get file metadata
Given a file named "main.pen" with:
import Os'Context { Context }
import Os'File
import Os'File'Metadata { Metadata }
import Os'Process
main = \(ctx context) none {
m = File'Metadata(ctx.Os, "foo")
c = if m = m as Metadata {
if m.Size == 3 {
0
} else {
1
}
} else {
1
}
Process'Exit(ctx.Os, c)
}
And a file named "foo" with:
foo
When I successfully run pen build
Then I successfully run ./app
.
Get os time
Given a file named "main.pen" with:
import Core'Number
import Os'Context { Context }
import Os'File
import Os'Process
import Os'Time
main = \(ctx context) none {
if m = run(ctx.Os) as none {
none
} else {
Process'Exit(ctx.Os, 1)
}
}
run = \(ctx Context) none | error {
File'Write(ctx, File'StdOut(), Number'String(Time'Now(ctx)))?
none
}
When I successfully run pen build
Then I successfully run ./app
.
Sleep
Given a file named "main.pen" with:
import Os'Context { Context }
import Os'Time
main = \(ctx context) none {
Time'Sleep(ctx.Os, 1)
}
When I successfully run pen build
Then I successfully run ./app
.
Generate a random number
Given a file named "main.pen" with:
import Core'Number
import Os'Context { Context }
import Os'File
import Os'Process
import Os'Random
main = \(ctx context) none {
if m = run(ctx.Os) as none {
none
} else {
Process'Exit(ctx.Os, 1)
}
}
run = \(ctx Context) none | error {
File'Write(ctx, File'StdOut(), Number'String(Random'Number(ctx)))?
none
}
When I successfully run pen build
Then I successfully run ./app
.
Exit a process
Given a file named "main.pen" with:
import Os'Context { Context }
import Os'Process
main = \(ctx context) none {
Process'Exit(ctx.Os, 42)
}
When I successfully run pen build
Then I run ./app
And the exit status should be 42.
Test
Background
Given a file named "pen.json" with:
{
"type": "library",
"dependencies": {
"Test": "pen:///test"
}
}
Check if a condition is true
Given a file named "foo.test.pen" with:
import Test'Assert
Foo = \() none | error {
Assert'True(true)
}
When I run pen test
Then the exit status should be 0.
Check if a value is an error
Given a file named "foo.test.pen" with:
import Test'Assert
Foo = \() none | error {
Assert'Error(error(none))
}
When I run pen test
Then the exit status should be 0.
Make a test fail
Given a file named "foo.test.pen" with:
import Test'Assert
Foo = \() none | error {
Assert'Fail()
}
When I run pen test
Then the exit status should be 1.
FFI
Background
Given a file named "pen.json" with:
{
"type": "library",
"dependencies": {}
}
Import a foreign function of native calling convention
Given a file named "Foo.pen" with:
import foreign g \(number) number
f = \(x number) number {
g(x)
}
When I run pen build
Then the exit status should be 0.
Import a foreign function of the C calling convention
Given a file named "Foo.pen" with:
import foreign "c" g \(number) number
f = \(x number) number {
g(x)
}
When I run pen build
Then the exit status should be 0.
Export a foreign function
Given a file named "Foo.pen" with:
foreign f = \(x number) number {
x
}
When I run pen build
Then the exit status should be 0.
Export a foreign function of the C calling convention
Given a file named "Foo.pen" with:
foreign "c" f = \(x number) number {
x
}
When I run pen build
Then the exit status should be 0.
Advanced features
This chapter lists up several advanced features of the language and describe when and how to use them.
Cross compile
The language's compiler supports cross compile. To compile applications and libraries for different targets, specify the --target
option of the pen build
subcommand.
For example, run the following command to compile a wasm32
binary for the WASI platform.
pen build --target wasm32-wasi
Note that we currently support those targets via Rust's cross compiler toolchain. Please install a Rust compiler through rustup
to enable installation of toolchains for different targets.
Supported targets
Run pen build --help
to see all supported targets.
System package support
Cross compile support of system packages are totally up to their developers. For example, the Os
standard system package supports most targets as long as their platforms expose OS-like APIs. However, some custom system packages might not support those targets because of their limited use cases.
Foreign Function Interface (FFI)
Using FFI, you can import or export functions in foreign languages, such as Rust and C.
Importing functions in foreign languages
You can import functions in foreign languages using foreign import statements. The statements specify the foreign functions' calling convention, names and types.
You might specify calling conventions of foreign functions in a format of string literals after import foreign
keywords optionally. Currently, only the C calling convention is supported as "c"
. If the options are omitted, the functions are imported with the native calling convention of the language.
import foreign "c" foo \(number, number) number
Caveat: You can import foreign functions that might cause side effects only in system packages. See also Writing system packages.
Exporting functions to foreign languages
You can export functions to foreign languages using foreign function definitions, which have foreign
keywords in front of normal function definitions.
You might specify calling conventions of exported foreign functions optionally after foreign
keywords as well as imported foreign functions.
foreign "c" foo = \(x number, y number) number {
...
}
Building libraries of foreign languages
During builds of your packages, you might want to build libraries of foreign languages so that you can use their functions. If that's your case, you can set up pen-ffi
scripts in your packages. The script files run on every build and output absolute paths to .a
archive files of your libraries in foreign languages built by the scripts. The script files may or may not have file extensions.
The pen-ffi
scripts should accept the following command line arguments.
Argument | Required | Description |
---|---|---|
-t <target> | No | Custom target triple of builds |
One of examples in practice is a pen-ffi.sh
file in the Core
library.
Native calling convention in Pen
WIP
Writing system packages
Using existing system packages covers most use cases in application development. However, by writing your own system packages, you can achieve the following:
- Define your own system interfaces of functions and types with side effects.
- Link applications in arbitrary file formats.
This page assumes that you have already read Packages.
Caveat: Providing bad system packages which do not conform to conventions can break the ecosystem of the language! In the worst cases, they might make applications malfunction. Please be careful to follow the conventions to keep applications maintainable and portable.
Functionalities of system packages
System packages have the following functionalities:
- Define context types.
- Provide system interfaces as functions and types.
- Link application files.
Defining context types
Every system package must have a module named Context
at the top level. The module defines a Context
type and an UnsafeNew
function that returns a Context
value with no argument.
For example, a system package for command line applications might have the following Context
module:
...
type Context {
print \(string) none
}
UnsafeNew = \() Context {
Context{
print: ...
}
}
The language's compiler uses these type and function to compose a context
type passed to main
functions in main
modules in application packages.
Providing system functions and types
System packages are the only places where you can define functions that have side effects. Thanks to system packages, applications can perform effects to interact with the world, such as:
- Console input/output
- File system operations
- Networking
- Random number generation
Note that system packages should never expose side effects directly through their functions; all public functions in system packages must be purely functional. Instead, you need to pass a Context
type to every effect-ful function for it to make side effects.
For example, a system package for command line applications might have the following types and functions:
# Define a foreign function to output a string in console.
import foreign _pen_cli_print \(string) none
type Context {
print: _pen_cli_print,
}
Print = \(ctx Context, s string) none {
ctx.print(s)
}
rather than:
import foreign _pen_cli_print \(string) none
Print = \(s string) none {
# Oh, no! We make side effects in a public function directly.
_pen_cli_print(s)
}
Linking application files (optional)
System packages might have optional script files named pen-link
at their top directories. On every build of application packages using the system packages, the script files run given object files specified as command line arguments to link application files. The script files may or may not have file extensions.
The scripts should accept the following command line arguments.
Argument | Required | Description |
---|---|---|
-t <target> | No | Target triple |
-o <application> | Yes | Path of an application file |
<archive>... | Yes | Paths of archive files sorted topologically from main packages |
At the liking phase, compiled main functions are available under a symbol named _pen_main
with the language's native calling convention.
Examples
The Os
standard package is an example of system packages.
Roadmap
Items are ordered by priority.
- Basic syntax
- CPS transformation
- Capability-based effect system
-
Performant GC
- Automatic reference counting
- Foreign function interface
- Basic OS interface
- WASM backend
- Stream-based list type
- Testing framework
- TCP/UDP sockets
- System time
- Asynchronous operations
- Parallel computation
- Binary operations
- List comprehension
- Multiple system packages
- Map type
- Code formatter
- Documentation generator
-
IDE/editor support
- Language server
- Reflection
- Serialization / deserialization
- Process management
-
Mutable state
- Thread safety
- Vector type
- Web browser interface
- Self-hosting!?
The Zen
- Integrated values over spontaneous values
- Beautiful is better than ugly.
- Explicit is better than implicit.
- Simple is better than complex.
- One way to do one thing
- Single solution to solve multiple problems
Language qualities
In the order of priority,
- Simplicity
- Readability
- Consistency
- Writability