Mesa Language Manual #
Table of Contents #
- Introduction
- Principles
- Lexical Structure
- Values
- Procs
- Types
- Protocols
- Expressions and Operators
- Bindings and Scope
- Control Flow
- Error Handling
- Modules
- Packages
- Appendix A - Grammar
- Appendix B - Built-in Behaviors
- Appendix C - Built-in Errors
- Appendix D - Operator Precedence
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,trueorfalse.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 aProc.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 := ¬ify
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 := ¬ify
&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 |