The Mesa programming language
mesa doc language.md
75 kB · 2618 lines
Markdown
at main

Mesa Language Manual #

Table of Contents #

  1. Introduction
  2. Principles
  3. Lexical Structure
  4. Values
  5. Procs
  6. Types
  7. Protocols
  8. Expressions and Operators
  9. Bindings and Scope
  10. Control Flow
  11. Error Handling
  12. Modules
  13. Packages

1. Introduction #

This document describes the design & behavior of the Mesa programming language. Mesa is a hobby programming language designed primarily for writing personal software including web applications and command-line utilities. It is inspired by Ruby, JavaScript, and Swift (among others). It is dynamically & strongly typed but features a subset of compile-time semantic analysis which can achieve some of the correctness assurances of more sophisticated static type systems. It features a pragmatic data model taking cues from object-oriented as well as functional approaches.

2. Principles #

Mesa's design is guided by a set of principles. No one principle dominates over any other, and they are traded off against one another where appropriate. More importantly, they are not to be understood as a set of axioms from which the entire design follows; rather, they are guidance to be used for evaluating points of tension or directions of development. A language which is axiomatic may possess a satisfying simplicity, but this satisfaction comes at the cost of its expressiveness. These principles are furthermore not to be taken as ultimate or universal, but particularly suited to the design of personal software systems.

Systems are built from the coherent organization of concepts and relationships. Programs represent discrete understandings of problems and solutions in a particular knowledge space. Thus, a programming language must provide the tools for constructing a vocabulary and grammar appropriate to the problem domain. In Mesa, systems are composed of a foundation of nouns and verbs which are combined in distinct forms of relationships, making the task of describing a particular problem the same task as converting it to an appropriate meta-language.

Prioritizing local reasoning results in clarity and encourages composability. Whereas some systems value flexibility above all else, Mesa prioritizes the ability to examine a program in relative isolation and, with a set of rules & restrictions in mind, understand what it means. Every name binding is typically locatable within the current source file. Standard control flow constructs and bounded points of extensibility provide specific ways to describe a problem space. The result is that reading and understanding a particular program consists of converting a limited set of language constructs into a defined set of domain constructs and exploring the resulting space.

Modeling complexity necessitates navigating it rather than refusing it. Mesa does not claim to be a 'simple' language but nevertheless attempts to be an understandable one. Programming languages, like natural languages, are tools for expressing thought and denoting reasoning, and for this reason must bend to the facts of the material world. While there is a satisfying reassurance in the belief that a minimal system can emphasize only the relevant aspects of a particular problem—while the language used to denote the problem regresses into the background—the reality is that the problem itself ultimately resolves into language. The world is not, despite our efforts to make it so, a coherent system, and so our tools for describing it must not resign to this assumption.

Optimizing for small scale yields less generality: A concept which provides affordances at a small scale may not necessarily provide the same affordances at a larger scale, and vice-versa. In particular, industrial systems tend to require robustness or universality that is not applicable at the scale of personal systems. By optimizing for one end of this tradeoff as opposed to attempting to satisfy both, programs written in Mesa can satisfy the hobbyist without burdening them with constraints which may be legitimate in an industrial context.

Visual cohesion is not separate from conceptual cohesion. Programs are both text files on disk and the reasoning which those files denote. Just as coherent concepts establish relations to one another with specific meanings and patterns, so must the text representing those concepts possess harmonious relationships. A resonant Mesa program strives to possess a distinctive style, rhythm, and rhyme just as a resonant piece of writing does.

Understanding and trust are the foundations for learning and empowerment. A programming language designed for building personal software ultimately serves both those who use it and those in their community with whom the software is shared. Mesa provides features for introspection and limits its available forms of access control so as to make the software explainable and understandable to anyone who can access it. Additionally, Mesa itself is free software and its implementation is documented so as to make building it and extending it as easy as possible.

3. Lexical Structure #

All Mesa source is UTF-8 encoded text. This source text is broken up into primitive elements, called tokens.

Identifiers #

An identifier begins with an ASCII letter or _ and is followed by any number of ASCII letters, digits, and _s. It may end with a single ? or !.

byte
read_all
_init
Account
JSON
empty?
sort!

Capitalization carries no requirements to the language. An identifier with a capitalized first letter conventionally names a type or protocol.

A trailing ? or ! in an identifier carries no requirements to the language. A trailing ? is conventionally used to name a boolean predicate. A trailing ! is conventionally used to distinguish a mutating, raising, or otherwise distinct variant of a non-suffixed name.

A trailing ! is treated as part of the preceding identifier unless it is immediately followed by an =. In this latter case, it is considered part of the != operator. Mesa has no bare = operator, so this is unambiguous.

Keywords #

The following symbols are reserved words and cannot be used as identifiers:

and     break   case    def     do      each    else
end     export  extern  false   impl    import  in
loop    module  nil     not     or      proto   raise
rescue  return  self    then    true    type    when

Literals #

A number literal is a sequence of digits with an optional fractional part separated by a .. The fractional part must be followed by at least one digit.

A number literal is never negative; -5 is unary - applied to 5. There is no digit separator, exponent notation, or alternate radix.

17
64.2
3.0

A string literal is a sequence of characters delimited by double quotes. It may span multiple lines.

A character literal is a single character delimited by single quotes.

Both string and character literals permit the escapes \n, \t, \", \', and \\. There is no string interpolation.

"Hello, world!"
"Multi
line"
"Multi\nline"

'a'
'é'
'\t'

A boolean literal is either true or false.

The nil literal is nil.

Comments and Whitespace #

Comments begin with a # and run until the end of the line. There is no multi-line comment form.

# Add the numbers.
5 + 2.3 # These are numbers.

Almost all whitespace in a Mesa program is insignificant. Newlines are an exception; they are used to delimit expressions. There is no explicit statement separator or terminator.

Operators and Punctuation #

The following tokens are operators or otherwise meaningful punctuation:

:=  ==  !=  <   >   <=  >=  +   -   *  /
<<  &   :   ,   .   (   )   [   ]   {  }

The assignment operator is spelled :=. A bare = is a syntax error.

4. Values #

Values are the fundamental unit of composition in Mesa. Every name in a Mesa program is bound to an inspectable value with some set of behaviors.

Like other dynamically typed languages, values have types, and any name can be bound to a value of any type. A value's type functions as the primary descriptor for that value's behavior. A type provides a structure for otherwise arbitrary data, and from this structure emerges a coherent concept which affords composing abstractions. Types have members which are properties about values of that type or behaviors which values of that type exhibit.

One can say that values are 'instances of' a particular type, and this will be familiar from object-oriented vocabulary. Instances of a particular type behave in similar ways.

Built-in Types #

The following types are built in to the language:

  • Num: A 64-bit floating-point number, e.g. 64.2, 17.
  • Bool: A boolean, true or false.
  • Char: A single Unicode scalar, e.g. 'a', 'é'.
  • Str: An immutable sequence of Unicode scalars, e.g. "Hello, world!".
  • Digest: An immutable, opaque blob of bytes, used for hashing.
  • List: A mutable, heterogeneous, insertion-ordered sequence of values, e.g. [1, "apple", nil].
  • Dict: A mutable, heterogeneous, insertion-ordered mapping of keys to values, e.g. {"id": 1024}.
  • Proc: An invocable unit of behavior. A method bound to its receiver is also a Proc.
  • Type: A meta-unit of related data and functionality.
  • Proto: A unit of behaviors common to multiple types.
  • Module: A unit of related functionality.
  • Nil: An absent value, nil.

Value and Reference Semantics #

Mesa supports value semantics for some built-in types. The types which follow value semantics are: Num, Bool, Char, Str, Digest, and Nil. All other types, built-in and user-defined, follow reference semantics. The consequences of this design for equality are mitigated by defaulting to structural equality for all types, overridable in cases where it is appropriate. The consequences of this design for mutability are not mitigated; Mesa instead encourages mutable algorithms and data sharing as the natural paradigm for expressing domain relationships and the change which occurs over a program's lifetime.

Truthiness, nil, and Absence #

Every value other than nil and false is truthy. When a truthy value is used in a conditional or with a logical operator, it is equivalent to true.

An empty list [] and an empty string "" are both truthy. Truthiness denotes presence, not fullness.

nil, on the other hand, is used to denote absence.

5. Procs #

A proc is a subroutine which computes a value and hands it back to the caller.

def balance(transactions)
	total := 0
	each transaction in transactions do
		...
	end
	total
end

As with all blocks, the value of calling a proc is the value of its last expression. If there is an early return, the value is instead the value provided to the return, or nil if the return has no value.

def find(id, query)
	record := find_by_id(id)
	when record == nil then
		return nil
	end
	...
end

Parameters and Arguments #

Procs can be declared with or without parameters, values which must be provided by the caller to specify the behavior of the proc. The caller provides arguments which bind to these parameters.

Without parameters, the proc's parentheses are optional. An incorrect number of provided arguments will raise an error.

Parameters can either be required or optional. Required parameters must be provided by a caller while optional parameters can be omitted. Placing optional parameters before required ones is an error at declaration time. An optional parameter can reference any parameter declared before it, and the expression is evaluated every time the proc is called.

def new
	...
end

def credit(account, amount: 0)
	...
end

new
new()
credit(account)
credit(account, 127.25)

When provided by a caller, arguments can either be positional or keyword. Positional arguments are not accompanied by a label while keyword parameters are. The labels provided by the caller are the names of the parameters, so these names are part of the proc's interface. Placing keyword arguments before positional ones is an error.

def debit(account, amount)
end

debit(account, amount: 50.13)

Invocation and Mentioning #

A declared proc invokes when it is referenced rather than resolving to the value of the proc itself.

def amount
	100
end

amount # => 100

Invocation is a property of the declaration, not of the value. A name introduced by an assignment or by a parameter never invokes; it reads its value as it is, even when that value is a proc.

def notify
	...
end

def apply(action)
	action # => def notify; a parameter never invokes
end

handler := &notify
handler   # => def notify; a local never invokes
handler() # calls notify

If invocation would leave a required parameter unfilled, it raises an error.

def credit(account)
	...
end

credit # => runtime error: missing arg 'account'

When used with explicit call syntax, automatic invocation is suppressed. This supports using parentheses in all cases without knowing the type of the value that the proc returns.

def make
	...
end

make     # invokes with no arguments
make()   # calls with no arguments
make()() # calls the result of make() with no arguments

To take an invocable as a value, the mention operator & asserts that what it names is invocable and yields it without invoking. Anything that is not invocable raises an error.

def make
	...
end

make  # invokes make
&make # takes make as a value
&100  # => runtime error: type Num is not invocable

On a name that would not have invoked, & yields the value unchanged, though the assertion still holds. It can therefore be written wherever the author wants to be explicit, without needing to know whether the name would have invoked.

handler := &notify
&handler # => def notify; the local would not have invoked either way

Mentioning can be used to pass a proc as an argument to another proc.

listeners := []

def add_listener(listener)
	listeners << listener
end

def on_credit
	...
end

add_listener(&on_credit)

Types are not invocable and must instead always use explicit call syntax. This syntactic necessity contains semantic information: creating an instance of a type and exercising a behavior of that instance are emphasized as two different operations.

Proc Literals #

A proc defined in an expression context is a proc literal and evaluates to the callable proc. Literals can be used to store procs in variables or pass procs to other procs. Though such composition is typically accomplished via types, instances, and protocols, passing procs as values can achieve a form of composition and delegation useful for simple situations.

add := def (a, b) a + b end
add(4, 2) # => 6

A proc literal can be used to pass a proc as an argument to another proc.

amounts := transactions.collect(def (txn) txn.amount end)

Unlike a regular proc, a proc literal does not automatically invoke. This follows the rule that only declared procs invoke, while locals do not.

make := def ... end
make # => def ...

Capturing #

Proc literals can capture variables bound in the scope in which they are defined. Locals referenced in the body of a proc literal are captured by value, including self. Module-level and prelude names are not captured and resolve at call time.

def adder(lhs)
	def (rhs) lhs + rhs end
end

add := adder(5)
add(2) # => 7
add(3) # => 8

Locals capture the value of the binding, not the value itself. Assigning to a captured name inside a proc literal only changes the literal's own copy of the value.

This means that for mutable types whose value is a reference, mutating them inside a literal works as expected. This is the case for List, Dict, and all user-defined types. Num, Bool, Char, and Str are not mutable, however, so assigning to a name bound to them will not mutate the captured value.

def stream(items: [])
	next := def
		when items.empty? then
			nil
		else
			items.shift()
		end
	end
	next
end

next := stream([1, 2])
next() # => 1
next() # => 2
next() # => nil

def broken
	count := 0
	counter := def
		count := count + 1
		count
	end
	counter
end

counter := broken
counter() # => 0
counter() # => 0

6. Types #

Types are a foundational tool for abstraction in Mesa along with procs. If a proc defines a verb reusable across contexts, a type defines a noun reusable across contexts and more easily transferrable as a value.

Constructors #

All types feature a single constructor invoked via call syntax. Because constructors follow normal call syntax rules, they can declare defaults for their parameters and callers can use keyword syntax. There are no additional construction or object initialization concepts. Any alternative construction behavior should go in a type-level method.

Str() # => ""
Num() # => 0.0

type Amount(cents)
end

Amount(105) # => Amount(cents: 105)

As opposed to procs and methods, types are not invocable. Referencing the name of a type does not automatically invoke its constructor. One always needs to distinguish a type from its instances, whereas one only rarely needs to distinguish an invocable from the result of its invocation.

User-defined Types #

While the built-in types provide a foundation for describing computational use-cases, most applications and libraries will want to define their own types specific to their problem domain. User-defined types provide a mechanism for declaring types, their behaviors, and their invariants in a coherent, consistent manner.

Fields #

Every type has a statically known set of fields, pieces of data that compose the type. Fields can be defined either by the constructor or in the type body. Constructor fields are supplied by the caller while body fields are not.

type Task(label, priority: 0)
end

Task("Update documentation")

Fields defined in the body can reference constructor fields or other body fields declared before themselves. They are evaluated every time a value of the type is constructed.

type Group(label)
	labels := [label]
end

Only constructor fields are printed by the output of Display, while body fields are not. All fields are printed by the output of Inspect.

type Group(label)
	items := []
end

Group("Urgent").display # => "Group(label: \"Urgent\")"
Group("Urgent").inspect # => "Group(label: \"Urgent\", items: [])"

Methods #

Procs attached to a type are its methods. Methods are procs bound to an instance of a type, and they follow all the same rules: they can define parameters, are automatically invoked when referenced, and can be mentioned to suppress invocation.

type Account(transactions)
	def balance
		each transaction in transactions do
			...
		end
	end
end

balance := &account.balance

In the body of a method, self is bound to the instance on which the method was called.

type Task(label)
	def clone
		self
	end
end

Task("Refactor").clone # => Task(label: "Refactor")
Type-Level Methods #

A type can define methods which are bound to the type object itself rather than instances of it. This can be done by defining a method with the prefix self. in the type body. In the body of a type-level method, self is bound to the type object itself. There is no equivalent for type-level fields.

type Amount(cents)
	def self.zero
		self(0)
	end
end

Members and Access #

Fields, methods, local types, and variants (discussed below) are all uniformly members of a type. Accessing a member uses syntax dependent on context: inside the type body, the bare name of a member accesses the member; outside the type body, the member operator spelled . accesses the member of the instance on the left hand side of the dot.

type Task(label, priority: 0)
	def description
		label
	end
end

Task("Documentation").label # => "Documentation"
Task("Documentation").description # => "Documentation"

Whether or not a member is automatically invoked when referenced follows the normal rules of invocation, which read the declaration and not the value. A method is declared, and invokes. A field is not, and is read as it is even when it holds a proc.

type Task(label, on_done)
	def description
		label
	end
end

def archive
	...
end

task := Task("Documentation", &archive)
task.description  # => "Documentation"; a method invokes
task.on_done      # => def archive; a field never invokes
task.on_done()    # calls archive
&task.description # takes the bound method as a value
&task.on_done     # => def archive; the field would not have invoked either way

Fields are therefore uniform with one another: a field is read the same way whatever it holds. Uniform access between a field and a method taking no arguments is preserved, since both still yield a value.

Mesa has no access control mechanism within types. Every member of a type—field, method, local type, variant—is public and introspectable, even if it is not providable by the caller or has no meaning outside of the type's internal implementation.

Mesa also has no mutability control mechanism. Every field is readable and writable from every part of the program. Methods are the one exception to this rule because they cannot be reassigned.

Local Types #

Some types are more related than shared module membership or source code adjacency would imply. In the case of certain domain types, several internal types may be related implementations of similar behavior. For these cases, local types provide a form of namespacing within a particular module.

type LinkedList
	head := nil

	type Node(data)
	end
end

Local types are not related to the enclosing type in any way other than their name. They cannot automatically see into fields of instances of the enclosing type and cannot call methods on it without explicitly holding an instance. In all ways except for name lookup they function exactly as a sibling type at the module level with a name prefix.

Case Types #

Case types are Mesa's form of sum types. They are used to represent values which can come in one of a limited number of forms, called variants. A variant is declared inside the body of the case type and can declare fields and methods of its own.

A variant cannot declare its own variants; there is a single level of nesting. In situations where you need multiple levels of nesting, you can nest instances of further case types as fields of the variants.

type Transaction
	case Credit(amount) end
	case Debit(amount) end
end

References to variants always use the variant's fully-qualified name. This applies in every context: constructing a variant, matching one in when...case, and naming one in the case type's own body.

def absolute
	when transaction
	case Transaction.Credit then
		transaction.amount
	case Transaction.Debit then
		-1 * transaction.amount
	end
end

Case types support a limited form of behavioral sharing. When a member name cannot be resolved against a case type's variant, the member is looked up against the case type itself. This is suited for situations in which a common behavior can be implemented in terms of a shared behavior.

type File
	case Virtual(contents)
		def read ... end
	end
	case Disk(fd)
		def read ... end
	end

	def read_all
		each byte in read do
			...
		end
	end
end

Fallback applies only to methods, not fields; a case type cannot have fields.

7. Protocols #

Protocols define behaviors shared across different types. Protocols provide a mechanism for types to explicitly declare that they share a common interface across all implementers. A type's conformance to a protocol is statically checked at declaration time.

Protocols are named after the primary verb which they provide, rather than with a capability suffix like '-able.'

proto Draw
	def draw end
end

type Square
	impl Draw

	def draw
		...
	end
end

type Circle
	impl Draw

	def draw
		...
	end
end

A protocol can declare both required and provided methods. A required method must be implemented by the implementing type. A provided method may be implemented by the implementing type, but otherwise falls back to the implementation provided by the protocol.

A protocol may not declare, but may possess, derived methods. A derived method has the same fallback mechanism as a provided method; however, instead of the default implementation being provided in the protocol declaration, it is provided by the language itself. The protocols which have derived methods are Equal, Hash, Access, and Inspect (see below).

A protocol must either be empty, contain only derived methods, or contain at least one required method. A protocol with only provided methods is not allowed.

proto Handle
end

proto Read
	def read end
	def read_all
		each byte in read do
			...
		end
	end
end

type File
	impl Handle, Read

	def read
		...
	end
end

type Socket
	impl Handle, Read

	def read
		...
	end
end

A protocol's provided methods can only access members the protocol explicitly declares. This is statically checked at declaration time.

Built-in Behaviors #

Some protocols are provided which offer a limited mechanism for user types to hook into built-in behaviors, including some operators. The protocols and the behaviors they govern are as follows:

Protocol Governs Status
Equal == != Automatic, overridable
Hash Use as a Dict key Opt-in
Order < <= > >= Opt-in
Access a[k] and a[k] := v Opt-in
Append << Opt-in
Iterate Use in each1 Opt-in
Display User-facing output Opt-in
Inspect Developer-facing output Automatic, overridable

1. Iterate pairs with the Advance protocol, not listed here.

The details of these protocols, including what they require, how they behave, and what they guarantee can be found in Appendix B.

8. Expressions and Operators #

Mesa is an expression-oriented language. Everything in an expression position evaluates to a value. Not every piece of syntax can be used in expression position, however; declarations like module and type are not expressions and do not evaluate to a value. Additionally, not every expression evaluates to a useful value. For example, each evaluates to nil unless it breaks with a value, and when evaluates to nil if there is no matching branch and no else clause.

Expression Termination #

Expressions are terminated at a newline when two things are true at once. First, the token preceding the newline must be able to end an expression. Second, the token following the newline must not be able to continue one.

The tokens that can end an expression are: an identifier; the keywords self and end; a string, character, number, boolean, or nil literal; and the characters ), ], and }.

The consequence of this rule is that almost any expression can be wrapped across lines by breaking either before or after an operator.

transactions.
	collect(def (transaction) transaction.amount end).
	select(def (amount) amount.cents > 0 end)

# or

transactions
	.collect(def (transaction) transaction.amount end)
	.select(def (amount) amount.cents > 0 end)
when
	shape.visible?
	and not shape.color.transparent?
then
	...
end

# or

when
	shape.visible? and
	not shape.color.transparent?
then
	...
end

The only time one must specifically break before an operator is with -, [, and (.

5 -
4   # evaluates to 1

5   # evaluates to 5
- 4 # evaluates to 4; ignored
items[0] # evaluates to items[0]

items # evaluates to items
[0]   # evaluates to [0]; ignored
make(1) # evaluates to the result of calling make(1)

make # evaluates to the result of invoking make
(1)  # evaluates to 1; ignored

return and break are the exceptions to this rule. These constructs always terminate at a newline despite the fact that they cannot end an expression. This means a return or a break is always bare unless its operand starts on the same line.

return 5.2 # returns 5.2

return # returns nil
5.2    # ignored

Additionally, because procs have optional parameter lists, a newline immediately after the name of the proc always indicates an empty parameter list, even if that line begins with a (. This is to distinguish parameter lists from grouping expressions with (.

# draw takes 1 parameter, 'shape'
def draw(
	shape
)
	...
end

# draw takes no parameters
def draw
	(shape)
end

Block Results #

A block of expressions always evaluates to the value of its last expression. The only exception to this is if the block exits early.

Operators #

Mesa supports the following operators, grouped by function:

Arithmetic: + - * / and unary - on Num. Not overloadable by user types.

Concatenation: + on Str. Not overloadable by user types.

Mixing: + on Digest. Combines two hashes in an order-sensitive manner, i.e. a.hash + b.hash and b.hash + a.hash are not necessarily equivalent. Not overloadable by user types.

Ordering: < <= > >= on any type implementing Order. Num compares numerically, Str and Char compare lexicographically by scalar value. User types can overload by implementing Order.

Equality: == and != on any type implementing Equal. Compares by structural equality over all fields; there is no reference equality operator. Automatic for every user type, but can be overloaded by explicitly implementing Equal. Implemented on every built-in type.

Logical: and and or on any type short-circuit and return the selected operand. not on any type always returns a Bool. Every value is truthy except for nil and false, including e.g. an empty List. Not overloadable by user types.

Append: << on a type implementing Append appends the operand to the end and returns the receiver so it can be chained. User types can overload by implementing Append. Implemented on built-in List.

Mention: & on an invocable takes its value instead of automatically invoking it. Only applicable to Procs, which includes a method bound to its receiver.

Call, Member, and Access #

Call syntax uses ( and ) to denote arguments passed to a Proc, bound method, or type constructor. Calls can pass both positional and keyword arguments, and parameters can be declared required or optional.

Member access syntax uses . to denote accessing a field, method, or local type / variant of the left-hand value.

Access syntax uses [ and ] to denote keyed or indexed access into a collection implementing Access.

Precedence and Grouping #

Expressions may be wrapped in ( ) to form explicit groups. Otherwise, expressions are parsed according to the following precedence table ordered from highest to lowest precedence:

Operator Description Form Associativity
() [] . Call, access, member Postfix Left-to-right
& Invocable mention Prefix Right-to-left
- Arithmetic negation Prefix Right-to-left
* / Multiplication, division Infix Left-to-right
+ - Addition, subtraction Infix Left-to-right
<< Append Infix Left-to-right
== != < <= > >= Comparison Infix Left-to-right
not Logical negation Prefix right-to-left
or and Logical or, and Infix Left-to-right
:= Assignment Infix, place on left Right-to-left

This table can also be found in Appendix D.

9. Bindings and Scope #

Every name is introduced by a binding which associates that name with a value. The constructs which introduce bindings are type declarations, proc declarations, imports, proc and constructor parameters, each loop variables, rescue bindings, and assignment.

Every source location has a scope which consists of the bindings introduced both at that point and by the constructs enclosing it. The constructs which introduce a scope are each, loop, when, do, rescue, and proc bodies.

Two kinds of name are introduced by none of these constructs. self is a keyword which denotes a value, but it is not a binding and cannot be reassigned. The prelude's names are bound before any program runs.

A binding does not outlive the scope which introduced it. A name first bound inside a block is unavailable after it, while an assignment to a name bound in an enclosing scope rebinds that binding rather than introducing a new one.

count := 0
do
	count := count + 1 # rebinds the outer binding
	extra := 5         # a new local binding
end
count # => 1
extra # => error: name 'extra' is not defined

Resolution #

A name is always resolved in the following order, falling back to a later tier only if it is not found in an earlier one.

locals → self's members → module bindings → prelude

Locals are the bindings of the current scope and of the scopes enclosing it. Resolution walks outward, beginning from the innermost scope; if the name is not found in any local scope, resolution continues to the next tier rather than raising.

self's members are reached through the self value which a method implicitly possesses: in an instance method, this is the instance; in a type-level method, this is the type. Each of these values has a fixed set of members against which a name is looked up. This lookup happens even for bare names, so e.g. name in an instance method on type Account(name) refers to the field. Code outside a method has no self and skips this tier.

Module bindings are a module's top-level declarations, top-level assignments, and the names its imports bind.

Prelude names are available in every module without being imported. The prelude consists of the built-in type names and nothing else.

If a name is not found in any tier, an error is raised.

Assignment #

Mesa features a single assignment operator, :=. The first assignment to a name binds it, and subsequent assignments rebind it.

sum := 4 + 5
sum := sum + 1
sum # => 10

The := operator always evaluates to the assigned value.

old := 4
new := old := 2
old # => 2
new # => 2

Places #

The left-hand side of the := operator designates a place. A place is either an identifier, a member expression, or an access expression; no other syntax is valid as a place for assignment.

shape := Square(5.2)
shape.color := "red"
shapes[0] := shape

A place is resolved in the same order as any other name. Assigning to a bare name writes to self's member before it creates a local, which is how a method assigns to a field of its instance with no receiver.

type Account(balance)
	def deposit(amount)
		balance := balance + amount
	end
end

A name which resolves in no tier is bound as a local in the current scope. There is no variable declaration syntax. Reading an undefined name raises, while assigning to one defines it.

color # runtime error: name 'color' is not defined.
color := "red"
color # => "red"

Not every place accepts assignment. Methods and read-only members cannot be reassigned.

type Group(items: [])
	def size
		items.size
	end
end

Group().size := def 5 end # runtime error: member 'size' on type Group is read-only.

A module-level declaration claims its name for the whole of the module that declares it, so an assignment cannot displace one. Re-declaring a member is an error at declaration time.

def make
	...
end

make := make # semantic error: duplicate member 'make'

Shadowing #

There is no explicit syntax for shadowing. Only the binding forms which bind a name in a new scope shadow: proc and constructor parameters, each loop variables, and rescue bindings.

num := 5
each num in [1, 2, 3] do
	num # => 1, 2, 3
end
num # => 5

Assignment shadows only a prelude name. Where a name resolves to a local, to a member of self, or to a module binding, assignment rebinds it in place; where it resolves to a prelude name or to nothing at all, assignment introduces a new binding in the current scope.

Num # => type Num
Num := 5
Num # => 5

10. Control Flow #

Mesa features two looping constructs (each and loop), a conditional construct (when), a grouping construct (do), two exit constructs (return and break), and error handling constructs (raise and rescue).

Control enters at the top of a construct, transfers to subroutines, proceeds sequentially through expressions, and leaves the construct either at its end or through a construct that transfers out of it.

Loops #

each #

each loops over each item in any type implementing Iterate, binding a single name for each yielded item. The iterate method is called on the value provided, and the resulting Advance type is called until it is exhausted. Iterate is implemented for the built-in List, Dict, and Str types, yielding items, keys, and characters respectively. An each expression evaluates to nil unless exited with a break value.

each item in [1, "apple", nil] do
	item # => 1, "apple", nil
end

each key in {"id": 100} do
	key # => "id"
end

loop #

loop loops infinitely until explicitly exited with break. This can replace any alternative loop construct, like while, until, etc. A loop expression evaluates to nil unless exited with a break value.

count := 0
loop do
	when count == 10 then break end
	count := count + 1
end
count # => 10

Conditionals #

when...else #

when...else evaluates conditions in the order they are listed until one matches. Alternative conditions are specified using else when branches, and a catch-all branch using else. An else branch is not required, in which case the entire construct evaluates to nil; otherwise, it evaluates to the value of the branch which matched. The entire construct is closed by a single end.

A condition matches when the relevant expression evaluates to a truthy value.

name := when 4 / 2 == 2 then
	"four"
else when 8 / 2 == 4 then
	"eight"
else
	"other"
end

name # => "four"

when...case #

when...case matches against the type of a value. Each branch names a type, and the branches are tried in order.

There is no destructuring, no binding form, and no type narrowing; once a branch matches, member access in the body proceeds by normal rules.

type Expr
	case Ident(name)
	case Lit(lit)
end

when expr
case Expr.Ident then
	expr.name
case Expr.Lit then
	expr.lit
end

The branches need not name variants of a case type. when...case performs general type matching against any type.

def encode(val)
	when val
	case Str then ...
	case Num then ...
	else ...
	end
end

The branches cannot match against a case type itself, given that it can never be constructed. Such a branch will never match.

type Transaction
	case Credit(amount) end
	case Debit(amount) end
end

result := when transaction
case Transaction then
	"transaction"
else
	"other"
end

result # => "other"

when...case is not a general pattern matching construct. For conditions more complex than type dispatch, when...else should be used instead.

Exhaustiveness Checking #

Exhaustive coverage of a when...case construct is checked at declaration time against the construct's branches. If the branches all name variants of the same case type, they are checked for exhaustiveness. If an else is provided in this circumstance, the branches must not exhaustively cover the variants; this would swallow later additions to the case type. If the branches do not cover the variants, however, an else is permitted.

If the branches name unrelated types, whether they be case types or not, an else is always permitted. If it is absent, the when...case evaluates to nil if no branch matches.

do #

do groups a sequence of expressions and evaluates to the value of the last one in its body. do introduces a new scope, so it can introduce variables that are not visible after its accompanying end.

sum := do
	total := 0
	each num in [1, 2, 3] do
		total := total + num
	end
	total
end

sum # => 6

do can be paired with rescue to guard a series of expressions from a raised error.

do
	window.draw(shape)
rescue
case DrawError.OutOfBounds then ...
end

return #

return exits a proc early and causes it to evaluate to the value provided to the return. If return is not provided a value, the proc evaluates to nil. In the absence of a return, a proc or block evaluates to the value of its last expression.

There is no special-cased guard clause; guarding a particular section of a block can be accomplished by a return inside a when.

def draw(shape)
	when not shape.visible? then
		return
	end

	...

	return dimensions
end

return inside a proc literal returns from the literal, not the defining proc.

def visible(shapes)
	shapes.select(def (shape) return shape.visible? end)
end

break #

break terminates the containing each or loop. If provided a value, the loop evaluates to the value, otherwise nil.

break is statically checked to never occur outside of a loop body; doing so is an error at declaration time.

first := each num in [1, 2, 3] do
	break num
end

first # => 1

break inside a proc literal breaks from the inner loop.

each nums in [[1], [2], [3]] do
	first := def
		each num in nums do
			break num
		end
	end
	first # => 1, 2, 3
end

raise and rescue #

raise propagates a value through calls until a rescue handles it, or until the program exits. A rescue is always paired with a do, and it guards the expressions in that do's body.

11. Error Handling #

Mesa represents both programming and system faults using errors. Errors are propagated with raise and handled with rescue. An unhandled error terminates the program and reports the location it was raised from.

There are no result types, no error return values, and no error propagation operator.

Built-in Errors #

Errors raised by the language itself or by the core library are variants of the Error type provided by the core library. The variants of this type, as well as their purposes, are listed in Appendix C. They are used throughout this section whenever Error is referenced.

raise #

raise aborts the currently executing construct and propagates the raised value up the call stack until it is handled.

raise Error.KeyError("key \"id\" not found")

Although it is most useful to raise a value of a purpose-built error type, a value of any type can be raised.

raise "An error occurred."

rescue #

rescue stops an error from propagating if one of its branches matches the error value. If a branch matches, its corresponding do evaluates to the last expression in that branch.

config := {}
port := do
	config["port"]
rescue
case Error.KeyError then
	4000
end

port # => 4000

Branches #

rescue matches against the type of the raised value. Like when...case, each branch names a type, and the branches are tried in order.

type LedgerError
	case InsufficientFunds(account) end
	case InvalidRecipient(id) end
end

do
	account.transfer(amount, to: other)
rescue
case LedgerError.InsufficientFunds then ...
case LedgerError.InvalidRecipient then ...
end

An else branch matches any error that no preceding branch matches.

do
	account.transfer(amount, to: other)
rescue
case LedgerError.InsufficientFunds then ...
else ...
end

Propagation #

Unlike when...case, the branches of a rescue are not checked for exhaustiveness; they need only name the types they explicitly handle. An unhandled error propagates through calls until a rescue handles it, or until the program exits.

type DrawError
	case OutOfBounds(shape) end
	case InvalidDimensions(shape) end
end

do
	raise DrawError.OutOfBounds(shape)
rescue
case DrawError.InvalidDimensions then ...
end

Binding #

If provided a name, rescue binds a variable to the raised error. This is unlike when...case, which has no binding form.

do
	account.transfer(amount, to: other)
rescue err
case LedgerError.InsufficientFunds then
	err.account # => Account(...)
case LedgerError.InvalidRecipient then
	err.id # => ID(...)
end

Hierarchies #

Mesa does not support subtyping. Instead, an error hierarchy is constructed by composing case types.

type DrawError
	case GeometryError(cause) end
end

type GeometryError
	case OutOfBounds(shape) end
	case InvalidDimensions(shape) end
end

To handle the composed error variant, rescue matches against the outer type while when...case matches against the inner type. Whereas rescue is not checked for exhaustiveness, when...case is.

do
	window.draw(shape)
rescue err
case DrawError.GeometryError then
	when err.cause
	case GeometryError.OutOfBounds then ...
	else ...
	end
end

An example of a set of errors defined using this structure is the predefined set of errors, described in Appendix C.

12. Modules #

Every Mesa program is composed of modules. A module is declared in a single file which contains its name, imports, declarations, and top-level expressions. This file contains the entire contents of the module; it may never be reopened or extended.

module Draw

origin := 0

type Shape
	...
end

def draw(shape)
	...
end

A module's declarations are order-independent.

module Draw

def draw(shape: Circle(radius: 5))
	...
end

type Circle(radius)
	...
end

A module may contain top-level expressions in addition to declarations. They are evaluated when the module is loaded.

module Draw

unit := scale(1)

def scale(n)
	...
end

Unlike declarations, top-level expressions are order-dependent.

module Draw

double := unit * 2 # runtime error: name 'unit' is not defined

unit := scale(1)

Children #

A module may have children which are declared in files within a directory that sits beside the module's own file and shares its name. Child modules use a dot . to indicate their relationship, akin to member access. A child module declares its name as the full dotted path.

# draw/screen.ms
module Draw.Screen

A module's filename carries no requirements to the language; only the module's prefix and its position are checked. A module is conventionally declared in a file named for its leaf, lowercased with underscores between words.

# draw.ms
module Draw

# draw/printer.ms
module Draw.Printer

# draw/screen.ms
module Screen # semantic error: module 'Screen' must be declared under 'Draw'

Module nesting is a fact about names rather than lookup. Parent and child modules do not implicitly share names. A child module must import anything it uses from its parent or from a sibling. A parent module must import anything it uses from its children.

# draw.ms
module Draw

type Shape
	...
end

# draw/screen.ms
module Draw.Screen

import Draw.Shape

Imports #

A module may import members of other modules, including other modules themselves. An import names a member by a dotted path and binds its last segment to a name in the module's scope.

There is no wildcard or renaming import form.

module Draw

import Draw.Screen
import Draw.Screen.Window

Screen # => module Draw.Screen
Window # => type Window

An import of a module binds the module itself, so it may be passed to a proc, stored, or otherwise used as a value.

import Draw.Screen
import Draw.Printer

def render(shape, target: Screen)
	target.draw(shape)
end

render(shape)
render(shape, target: Printer)

Cycles #

Modules may not form an import cycle. A cycle happens when a module imports another module that imports it, directly or indirectly. An import cycle is an error at declaration time.

# draw/screen.ms
module Draw.Screen

import Draw.Printer # semantic error: import cycle: Draw.Printer → Draw.Screen → Draw.Printer

# draw/printer.ms
module Draw.Printer

import Draw.Screen

Members #

A module's declarations, its top-level assignments, and its child modules are its members, in the same sense as a type's, and are accessed by the same member operator ..

# draw.ms
module Draw

origin := 0

type Shape
	...
end

def draw(shape)
	...
end

# draw/screen.ms
module Draw.Screen

import Draw

Draw.origin # => 0
Draw.Shape  # => type Shape
&Draw.draw  # => def draw(shape)

An imported module is not a member of the importing module. There is no implicit re-export.

# draw/screen.ms
module Draw.Screen

import Draw.Shape

Shape # => type Shape

# draw/printer.ms
module Draw.Printer

import Draw.Screen

Screen.Shape # runtime error: module Draw.Screen has no such member 'Shape'

A module's members share a single namespace. Two members may not share a name.

# draw.ms
module Draw

type Screen
	...
end

# draw/screen.ms
module Draw.Screen # semantic error: duplicate member 'Screen'

A module's members are read-only from outside the module. To mutate a member of another module, go through a proc call.

# draw.ms
module Draw

origin := 0

# draw/screen.ms
module Draw.Screen

import Draw

Draw.origin := origin # runtime error: member 'origin' on module Draw is read-only

13. Packages #

A package is a tree of modules. It is the unit of compilation, evaluation, and distribution of a Mesa program. When building an executable, it is evaluated as a package; when sharing a library, it is distributed as a package.

A package has a root module, which is declared in src/package.ms. Every other module in the package descends from the root.

src/ is a build root rather than a module; it has no name in the language. The root module is the one module whose declaring file sits inside the directory holding its children rather than beside it.

tracker/
	package.toml
	src/
		package.ms      # module Tracker
		groups.ms       # module Tracker.Groups
		tags.ms         # module Tracker.Tags
		tags/
			label.ms    # module Tracker.Tags.Label

A directory within src/ must have a declaring file beside it.

Like all other modules, children of the root module must have a prefix that matches their parent.

# src/package.ms
module Tracker

# src/groups.ms
module Groups # semantic error: module 'Groups' must be declared under 'Tracker'

Exports #

Within a single package there is no access control mechanism. Just as no type hides its members, every module and its members are reachable from every other module. Between packages, exports declare which names are reachable.

A module exports all of its members by default. An export list at the head of a module narrows this to just the members it names.

# src/package.ms
module Tracker

export Task, complete

type Task
	...
end

def complete(task)
	...
end

Every module has its own export list, and the lists compose. A name is reachable from outside a package when every member on its path is exported, from the root module's own members down to the member itself.

A module's children are among its members, so an export list may name them. A module exports a child without importing it.

# src/package.ms
module Tracker

export Tags

# src/tags.ms
module Tracker.Tags

export tag

def tag(task)
	...
end

# in another package
import Tracker.Tags.tag

Where any module on the path does not export the next, the name is unreachable, whatever the modules below it export.

# src/package.ms
module Tracker

export Task

type Task
	...
end

# src/tags.ms
module Tracker.Tags

export tag

def tag(task)
	...
end

# in another package
import Tracker.Tags.tag # semantic error: module 'Tracker' does not export 'Tags'

Exports govern the reachability of names. They never govern what may be seen of a value which is already named. An export list may only name module members, not members of the module's types themselves; an importer holding an exported value has access to the whole of it.

export Task.status # syntax error: unexpected token DOT

Manifest #

A package's manifest declares the package's name and its dependencies. The manifest file is package.toml in the package's root directory beside src/.

name = "tracker"

[dependencies]
ledger = "https://github.com/acme/ledger"

A package's name identifies the package for distribution. It is never written in Mesa source. A package's root module name is declared in src/package.ms, like every other module's, and is the name source code uses. The two are conventionally related but the correspondence is not checked.

Dependencies #

A package may depend on other packages, which it declares in its manifest by package name. Every module in a package may import any of the package's dependencies.

Packages may not form a dependency cycle. A package may not depend on itself, directly or indirectly.

# tracker/package.toml
name = "tracker"

[dependencies]
ledger = "https://github.com/acme/ledger" # semantic error: dependency cycle: ledger → tracker → ledger

# ledger/package.toml
name = "ledger"

[dependencies]
tracker = "https://github.com/acme/tracker"

An import names a dependency by its root module name, which the dependency declares in its own src/package.ms, not by the package name under which the manifest declares it.

# src/tags.ms
module Tracker.Tags

import Ledger.Account

An import does not distinguish a dependency's module from a module of the importing package's own tree.

# src/tags.ms
module Tracker.Tags

import Ledger.Account
import Tracker.Groups

Because imports do not distinguish them and there is no renaming import form, the root module names of a package and its dependencies share one space. A package depending on another package whose root module shares a name with its own, or on two packages whose root modules share a name with each other, is an error.

[dependencies]
ledger = "https://github.com/acme/ledger"
account-book = "https://github.com/acme/account-book" # semantic error: packages 'ledger' and 'account-book' both declare root module 'Ledger'

Only a dependency's exported names are reachable.

import Ledger.Table # semantic error: module 'Ledger' does not export 'Table'

The core library is a dependency of every package and is not declared. Its root module is Core. This module shares the one namespace with every other dependency's root, so no package may declare a root module named Core.

import Core.Behaviors.Order

Appendix A - Grammar #

program ::= { module-item }

module-item ::= module-decl
              | import-decl
              | export-decl
              | type-decl
              | extern-decl
              | proto-decl
              | proc-decl
              | expr

module-decl ::= "module" dotted-path
import-decl ::= "import" dotted-path
dotted-path ::= IDENT { "." IDENT }

export-decl ::= "export" IDENT { "," IDENT }

type-decl ::= "type" IDENT [ "(" params ")" ]
              [ impl-clause ]
              { case-decl }
              { field-decl }
              { type-decl | method-decl }
              "end"

case-decl ::= "case" IDENT [ "(" params ")" ]
              [ impl-clause ]
              { field-decl }
              { type-decl | method-decl }
              "end"

field-decl ::= IDENT ":=" expr

method-decl ::= "def" [ "self" "." ] IDENT [ "(" params ")" ] { expr } "end"

extern-decl ::= "extern" "type" IDENT
                [ impl-clause ]
                { method-decl }
                "end"

proto-decl ::= "proto" IDENT { proc-decl } "end"

impl-clause ::= "impl" dotted-path { "," dotted-path }

proc-decl ::= "def" IDENT [ "(" params ")" ] { expr } "end"

params ::= [ param { "," param } ]
param  ::= IDENT [ ":" expr ]

expr ::= each-expr
       | loop-expr
       | when-expr
       | do-expr
	   | proc-expr
       | return-expr
       | break-expr
       | raise-expr
       | assign-expr
       | binary-expr
       | unary-expr
       | mention-expr
       | call-expr
       | member-expr
       | access-expr
       | primary

each-expr ::= "each" IDENT "in" expr "do" { expr } "end"

loop-expr ::= "loop" "do" { expr } "end"

when-expr ::= "when" expr ( "then" { expr } ( "end" | else-clause )
                           | case-arms )
else-clause ::= "else" ( when-expr | { expr } "end" )

case-arms ::= { case-arm } [ "else" { expr } ] "end"
case-arm  ::= "case" dotted-path "then" { expr }

do-expr ::= "do" { expr } ( "end" | "rescue" [ IDENT ] case-arms )

proc-expr ::= "def" [ "(" params ")" ] { expr } "end"

return-expr ::= "return" [ expr ]

break-expr ::= "break" [ expr ]

raise-expr ::= "raise" expr

assign-expr ::= place ":=" expr
place       ::= IDENT | member-expr | access-expr

binary-expr ::= expr bin-op expr
bin-op ::= "or" | "and"
         | "==" | "!=" | "<" | ">" | "<=" | ">="
		 | "<<"
         | "+" | "-" | "*" | "/"

unary-expr   ::= ( "not" | "-" ) expr
mention-expr ::= "&" expr

call-expr ::= expr "(" args ")"
args      ::= [ arg { "," arg } ]
arg       ::= [ IDENT ":" ] expr

member-expr ::= expr "." IDENT
access-expr ::= expr "[" expr "]"

primary ::= "self"
          | IDENT
          | BUILTIN "(" expr ")"
          | STR | CHAR | NUM | BOOL | NIL
          | "[" [ expr { "," expr } ] "]"
          | "{" [ expr ":" expr { "," expr ":" expr } ] "}"
          | "(" expr ")"

IDENT   ::= ( letter | "_" ) { letter | digit | "_" } [ "?" | "!" ]
NUM     ::= digit { digit } [ "." digit { digit } ]
STR     ::= '"' { any | escape } '"'
CHAR    ::= "'" ( any | escape ) "'"
escape  ::= "\n" | "\t" | "\"" | "\'" | "\\"
BOOL    ::= "true" | "false"
NIL     ::= "nil"
BUILTIN ::= "$" IDENT
COMMENT ::= "#" { any except "\n" }

Appendix B - Built-in Behaviors #

Some protocols are provided which offer a limited mechanism for user types to hook into built-in behaviors, including some operators. The protocols and the behaviors they govern are as follows:

Protocol Governs Status
Equal == != Automatic, overridable
Hash Use as a Dict key Opt-in
Order < <= > >= Opt-in
Access a[k] and a[k] := v Opt-in
Append << Opt-in
Iterate Use in each1 Opt-in
Display User-facing output Opt-in
Inspect Developer-facing output Automatic, overridable

1. Iterate pairs with the Advance protocol, detailed below.

These protocols are all defined in the Core.Behaviors module of the core library.

Equal #

Equal indicates that values of a type can be tested against one another for (in)equality. equal should return true when self is equivalent to other and false otherwise. By implementing Equal, values of a type can be used with the equality operators == !=.

Equal is implemented by default for all types, both built-in and user-defined. It is structural over all fields, including body fields. For a value of a given type, another value of that same type is equal to the first if all the values of its fields are equal. For a value of a variant, another value of that same variant is equal to the first both if the fields are equal and if the values are of the same variant.

"Hello, world!" == "Hello, world!" # => true

type Task(priority)
end

type Group(tasks: [])
end

Task(5) == Task(5) # => true
Task(5) == Task(2) # => false
Group([Task(2)]) == Group([Task(2)]) # => true

Equality is guaranteed to terminate in the case of referential cycles. Structural equality is intended to determine semantic equivalence rather than referential identity. Two graphs of cyclic values are equal if every path traversal through their fields yields the same sequence of values.

type Node(data)
end

a := Node(nil)
a.data := a

b := Node(nil)
c := Node(b)
b.data := c

a == b # => true

Equal is implemented for all user-defined types, so impl Equal on any type is redundant. Consequently, any type implementing an equal method will override the behavior of ==.

type Account(id, name)
	def equal(other)
		id == other.id
	end
end

Account(1, "Checking") == Account(1, "Savings") # => true

Hash #

Hash indicates that values of a type can deterministically provide a representation of their identity. hash should return a Digest that deterministically identifies the value. By implementing Hash, values of a type can be used as a key in a Dict.

Hash is implemented for Num, Bool, Char, Str, Digest, Proc, Type, Module, and Nil. List and Dict do not implement Hash.

Hash requires that Equal be implemented. Two values which are equal should hash to the same value. A mutable value with an unstable identity should not implement Hash, because a mutation after its insertion into a Dict would cause future lookups to hash to a different value.

import Core.Behaviors.Hash

type Account(id, name, transactions)
	impl Hash

	def equal(other)
		id == other.id and name == other.name
	end

	def hash
		id.hash + self.name.hash
	end
end

A type which implements Hash and overrides equal must also override hash. Not doing so is an error at declaration time.

Order #

Order indicates that values of a type can be compared against one another to produce a coherent ordering. order should return -1 when self < other, 1 when self > other, and 0 otherwise. By implementing Order, values of a type can be used with the comparison operators < <= > >=.

Order is implemented for Num, Str, and Char. Num sorts by numeric value. Str and Char sort by scalar value, not locale collation.

import Core.Behaviors.Order

type Task(priority)
	impl Order

	def order(other)
		priority.order(other.priority)
	end
end

Task(0) < Task(1)  # => true
Task(1) >= Task(0) # => true
Task(3) < Task(1)  # => false
Task(2) <= Task(2) # => true

The ordering produced by a type implementing Order must be total, transitive, and reflexive. It need only be antisymmetric with respect to the sorted equivalence of the values.

order returning 0 operates independently of structural equality. For example, a user-defined type might implement an ordering in terms of a single field, resulting in two structurally distinct objects evaluating as equivalent under Order.

The ordering produced by a type implementing Order can be thought of as one over a projection of the values of the type rather than the values themselves.

import Core.Behaviors.Order

type Task(name, priority)
	impl Order

	def order(other)
		priority.order(other.priority)
	end
end

Task("Documentation", 1).order(Task("Refactor", 1)) # => 0
Task("Documentation", 1) == Task("Refactor", 1) # => false

Access #

Access indicates that values of a type can have their elements read from or written to using access syntax. access should return the item corresponding to the given key. store should set the given key to the given item.

Implementing access is required, but implementing store is optional. Implementing access without implementing store indicates that values of the type cannot be mutated using access syntax. In this case, an error will be raised when calling store.

Access is implemented for List and Dict. Accessing a list by an out-of-bounds index will raise an error. Accessing a dict by a key which does not implement Hash will raise an error.

import Core.Behaviors.Access

type Collection(items: [])
	impl Access

	def access(key)
		items[key]
	end

	def store(key, item)
		items[key] := item
	end
end

collection := Collection(["Item 1", "Item 2"])
collection[0] # => "Item 1"
collection[1] := "Item 3" # => "Item 3"

Append #

Append indicates that values of a type can be appended to. append should append the given item and return self to support chaining. By implementing Append, values of a type can be used with the << operator.

Append is implemented for List, and appends the item to the list.

import Core.Behaviors.Append

type Collection(items: [])
	impl Append

	def append(item)
		items << item
		self
	end
end

Collection() << "Item" # => Collection(items: ["Item"])

Iterate #

Iterate indicates that values of a type can be iterated using the built-in each construct. iterate should return an Advance which yields the successive values until it is exhausted.

Iterate is implemented for List, Dict, and Str. List yields items, Dict yields keys, and Str yields characters.

import Core.Behaviors.Iterate

type Group(tasks: [])
	impl Iterate

	def iterate
		tasks.iterate
	end
end

group := Group([Task(...)])
each task in group do
	...
end

Advance #

Advance indicates that values of a type represent an exhaustable iterator. advance should advance the iterator and return true if the iterator has more items, or false otherwise. item should return the current item, or nil if the iterator is exhausted.

each calls advance then item repeatedly until advance returns false.

import Core.Behaviors.Advance
import Core.Behaviors.Iterate

type LinkedList
	impl Iterate

	head := nil

	type Node(data, next)
	end

	type Iter(node)
		impl Advance

		started? := false

		def advance
			when not started? then
				started? := true
			else when node != nil then
				node := node.next
			end
			node != nil
		end

		def item
			when node != nil then
				node.data
			end
		end
	end

	def iterate
		LinkedList.Iter(head)
	end
end

Display #

Display indicates that values of a type can be converted to user-facing output. display should return a Str which is suitable to display to a user.

Display is implemented for Str and Char, and returns the contents of the respective string or character without quotes and with escape sequences translated to their literal equivalents.

Display requires that Inspect be implemented. A value which cannot be printed in a user-facing format prints using a developer-facing format. No value is unprintable.

"\"Hello\nworld!\"".display # => "\"Hello\nworld!\""

Inspect #

Inspect indicates that values of a type can be inspected by a developer for runtime debugging. inspect should return a Str which provides information about the value. In general, inspect should return something which would be valid syntax for creating that value, but this is not a requirement.

Inspect is implemented for all types, both built-in and user-defined. Num returns the number's decimal representation using the least amount of precision necessary. Bool returns "true" or "false". Nil returns "nil". Char returns the single-quoted character, escapes included. Str returns the double-quoted string, escapes included. List returns the comma-separated items surrounded by square brackets. Dict returns the comma separated key-value pairs separated by curly braces.

Inspect is implemented for all user-defined types and returns a constructor invocation including both constructor and body fields and their values.

type Account(name)
	transactions := []
end

Account("Checking").inspect # => "Account(name: \"Checking\", transactions: [])"

Inspect is implemented for all user-defined types, so impl Inspect on any type is redundant.

Appendix C - Built-in Errors #

Case Type Variant Description
Error ProtocolError A value did not conform to a particular protocol.
Error ArgumentError The arguments provided to a proc or constructor were invalid.
Error TypeError A value of an incorrect type was used.
Error MemberError A member was incorrectly accessed.
Error IndexError An invalid List index was provided.
Error NameError An undefined name was read.
Error KeyError An invalid Dict key was provided.
ProtocolError NotIterable A value that does not implement Iterate was used with each.
ProtocolError NotAccessible A value that does not implement Access was used with [].
ProtocolError NotAppendable A value that does not implement Append was used with <<.
ProtocolError NotOrderable A value that does not implement Order was used with < <= > >=.
ProtocolError NotImplemented A method was called on a type that only implements part of a protocol.
ArgumentError Missing A required argument was not provided.
ArgumentError TooMany Too many positional arguments were provided.
ArgumentError Unknown An unknown keyword argument was provided.
ArgumentError Duplicate A duplicate keyword argument was provided.
TypeError IndexNonNum A List index that is not a Num was provided.
TypeError ArithNonNum A value that is not a Num was used with + - * /.
TypeError ConcatNonStr A value that is not a Str was used with +.
TypeError NotCallable A value that is not callable was used with ().
TypeError NotConstructible A case type, rather than a variant, was constructed.
TypeError NotInvocable A value that is not invocable was used with &.
TypeError CaseNonType A value that is not a type was used in a when...case branch.
MemberError Missing A member was read from that does not exist.
MemberError ReadOnly A member was written to that is read-only.
IndexError OutOfRange A List index that is outside the bounds of the list was provided.
IndexError NonIntegral A List index that is a non-integral Num was provided.
type Error
	case ProtocolError(cause) end
	case ArgumentError(cause) end
	case TypeError(cause) end
	case MemberError(cause) end
	case IndexError(cause) end
	case NameError(message) end
	case KeyError(message) end
end

type ProtocolError
	case NotIterable(message) end
	case NotAccessible(message) end
	case NotAppendable(message) end
	case NotOrderable(message) end
	case NotImplemented(message) end
end

type ArgumentError
	case Missing(message) end
	case TooMany(message) end
	case Unknown(message) end
	case Duplicate(message) end
end

type TypeError
	case IndexNonNum(message) end
	case ArithNonNum(message) end
	case ConcatNonStr(message) end
	case NotCallable(message) end
	case NotConstructible(message) end
	case NotInvocable(message) end
	case CaseNonType(message) end
end

type MemberError
	case Missing(message) end
	case ReadOnly(message) end
end

type IndexError
	case OutOfRange(message) end
	case NonIntegral(message) end
end

Appendix D - Operator Precedence #

Operator Description Form Associativity
() [] . Call, access, member Postfix Left-to-right
& Invocable mention Prefix Right-to-left
- Arithmetic negation Prefix Right-to-left
* / Multiplication, division Infix Left-to-right
+ - Addition, subtraction Infix Left-to-right
<< Append Infix Left-to-right
== != < <= > >= Comparison Infix Left-to-right
not Logical negation Prefix right-to-left
or and Logical or, and Infix Left-to-right
:= Assignment Infix, place on left Right-to-left