542 lines
17 KiB
Plaintext
Executable File
542 lines
17 KiB
Plaintext
Executable File
/* example.im - An example Ivy Implementation source file */
|
|
|
|
package net.doorstuck.test
|
|
|
|
use std.io
|
|
|
|
-- Anything after a double hypen is ignored by the compiler
|
|
/*
|
|
Multi
|
|
Line
|
|
Comment
|
|
*/
|
|
|
|
/**
|
|
classes also double up as protocols.
|
|
|
|
any class can "pretend" to be another class by responding to the same
|
|
messages that the base class understands.
|
|
|
|
for example, by implementing all the messages that the Package class
|
|
understands, your class is said to implement the Package protocol,
|
|
allowing package-specific syntax (i.e. indexing and dot syntax)
|
|
to be used with your class.
|
|
|
|
there are two standard messages that complement this behaviour:
|
|
-is: takes a Class parameter, and returns true if the recipient
|
|
is an instance of the given Class.
|
|
-implements: takes a Class parameter, and returns true if the recipient
|
|
understands all of the messages that the parameter class
|
|
understands.
|
|
**/
|
|
class Person
|
|
/**
|
|
every message handler can have one or both of the following:
|
|
- a message name (in this case, 'init')
|
|
- a set of one or more parameters (wrapped in parentheses if the message
|
|
has a name)
|
|
|
|
each parameter has an internal name and an (optional) label, specified
|
|
as a pair of identifiers separated by a colon (:). the label comes
|
|
first, and is the name used by whoever is sending the message the
|
|
internal name comes second, and is used by the message handler itself as
|
|
a variable name.
|
|
|
|
you can omit the label for a parameter by specifying an underscore (_)
|
|
as the label, but this is not recommended. it looks weird, it obscures
|
|
the meaning of the message, and is really only supported for
|
|
compatibility with lambdas.
|
|
**/
|
|
- init(name:name age:age)
|
|
self::name = name.
|
|
self::age = age!
|
|
|
|
- test(param:data _:extra) | cout put:'Received {data}, {extra}'.
|
|
|
|
- name | ^self::name.
|
|
|
|
- age | ^self::age.
|
|
|
|
- ageInMonths | ^self::age * 12.
|
|
|
|
- setName:name | self::name = name.
|
|
|
|
- setAge:age | self::age = age.
|
|
|
|
- setAge:age inUnit:units
|
|
match units in
|
|
#years => self::age = age,
|
|
#months => self::age = age / 12,
|
|
#days => self::age = age / 365,
|
|
_ => self::age = 0
|
|
end!
|
|
|
|
- getAgeInUnit:units
|
|
^match units in
|
|
#years => self::age,
|
|
#months => self::age / 12,
|
|
#days => self::age / 365,
|
|
_ => 0
|
|
end!
|
|
|
|
/* Properties are defined using the $ symbol.
|
|
They accomplish two things:
|
|
1) they make it easy to synthesize the -get: and -put:at: messages
|
|
that the package dot syntax requires.
|
|
2) they allow you to implement getters and setters that are used
|
|
when someone attempts to access your object's data.
|
|
|
|
The contents of a property looks a bit like a package, but it has a few
|
|
special rules:
|
|
1) every value must have an associated key; and
|
|
2) the only keys that can be used are `get` and `set` (this is the only
|
|
instance where reserved keywords can be used as keys in a package).
|
|
|
|
In this case, the getter has been set to a variable. To facilitate this
|
|
functionality, the compiler converts this to a lambda of the form
|
|
[ ^self::val ]
|
|
that will be evaluated when the getter is invoked
|
|
|
|
Similarly, the compiler synthesizes a lambda for the setter as well.
|
|
Here, the expression
|
|
self::val = value
|
|
is converted to the lambda
|
|
[ :x | self::val = x ]
|
|
*/
|
|
$ exampleProperty | get => self::val, set => self::val = value.
|
|
|
|
/* Without the lambda synthesis, the property would look like this:
|
|
Note that this is the only time it is legal to access private fields
|
|
via `self` from a lambda. */
|
|
$ exampleProperty |
|
|
get => [ ^self::val ],
|
|
set => [ :x | self::val = x ].
|
|
|
|
/* The `get` element of a property doesn't have to be a lambda, it can
|
|
be any value. When the property is accessed, the value provided will
|
|
be returned.
|
|
|
|
If either the `set` or `get` elements are not provided, the property
|
|
becomes read-only or write-only respectively */
|
|
$ exampleProperty2 | get => 42.
|
|
|
|
/* A property can also be configured to act just like a regular variable,
|
|
by setting the getter and setters to default implementations.
|
|
|
|
The default getter will return the value of a private member variable
|
|
named by prepending two underscores to the property name
|
|
(__exampleProperty3 in this case).
|
|
|
|
Similarly, the default setter will set the value of a private member
|
|
variable named by prepending two underscores to the property name
|
|
(__exampleProperty3 in this case) to the value provided to the setter. */
|
|
$ exampleProperty3 (get, set)
|
|
|
|
/* Just like in the earlier examples, either `get` or `set` can be omitted
|
|
to create a write-only or read-only property respectively */
|
|
$ exampleProperty4 (get)
|
|
end
|
|
|
|
p1 = Person new(name:'John Doe' age:34).
|
|
p1 setAge:100 inUnit:#months.
|
|
|
|
|
|
/**
|
|
when sending a message with unlabeled parameters, the preceding colon (:) is
|
|
still required. this is part of the reason why unlabeled parameters are
|
|
weird and not recommended.
|
|
**/
|
|
p1 test(param:'Hello' :'World').
|
|
|
|
/******************************************************************************/
|
|
|
|
i = 0.
|
|
while i < 100 do
|
|
cout put:'Count is {i}'.
|
|
i += 2
|
|
end
|
|
|
|
for i in 0 to:100 step:2 do
|
|
cout put:'Count is {i}'
|
|
end
|
|
|
|
/**
|
|
a lambda's parameters are *always* unlabeled, and the underscore (_) before
|
|
the label colon (:) is unnecessary.
|
|
**/
|
|
0 to:100 step:2 do:[ :i | cout put:'Count: {i}' ].
|
|
|
|
/******************************************************************************/
|
|
|
|
/**
|
|
lambdas can capture state from the context in which they are created.
|
|
|
|
a lambda can reference any local variable that is accessible at the point
|
|
it is created. if a variable from such a context is referenced, its value
|
|
is captured at the point the lambda is constructed. the value of the
|
|
variable is copied into the lambda.
|
|
|
|
this means that, if a lambda references a variable in the outer context,
|
|
and the value of that variable changes between the lambda being created
|
|
and the lambda being executed, that change will not be reflected within
|
|
the lambda.
|
|
|
|
because the value of captured variables is copied into the lambda, it is
|
|
safe for a method to return a lambda that references variables in its
|
|
local context.
|
|
**/
|
|
q = 32.
|
|
l = [ cout put:'Value of q is {q}' ]. /* value of `q` is captured here */
|
|
q = 64.
|
|
_ = l call. /* prints 'Value of q is 32' */
|
|
|
|
/******************************************************************************/
|
|
|
|
j = 32.
|
|
|
|
/**
|
|
expressions are terminated with a newline.
|
|
an expression can be written across multiple lines by using
|
|
the _ (underscore) line continuation character.
|
|
**/
|
|
|
|
/**
|
|
keyword messages have a lower precedence than all non-assignment
|
|
binary operators, so (i < j) will be evaluated first, and
|
|
if:else: will be sent to the result.
|
|
**/
|
|
|
|
i < j
|
|
if:[ cout put:'True!' ]
|
|
else:[ cout put:'False!' ].
|
|
|
|
if i < j then
|
|
cout put:'True!'
|
|
end
|
|
|
|
/******************************************************************************/
|
|
|
|
pkg = {}.
|
|
|
|
/**
|
|
Packages can be indexed via integer or string. If a package is index with a
|
|
variable, the value of the variable is used as the index.
|
|
**/
|
|
|
|
pkg[0] = 16.
|
|
|
|
/* All of these accesses are equivalent */
|
|
pkg['x'] = 32.
|
|
pkg->x = 32.
|
|
pkg put:32 at:'x'.
|
|
|
|
index = 'x'.
|
|
pkg[index] = 32.
|
|
pkg put:32 at:index.
|
|
|
|
/**
|
|
this syntax, and the pkg.* instructions that it translates to, is
|
|
implemented behind-the-scenes by sending messages to the package.
|
|
|
|
if your custom object implements this protocol, package syntax can be used
|
|
on it too.
|
|
**/
|
|
|
|
/**
|
|
these are the same packages used to store Classes and global variables.
|
|
for example, the std package is a regular package object, and you can use
|
|
package syntax on it or even send it messages.
|
|
**/
|
|
|
|
/******************************************************************************/
|
|
|
|
/* integer constants can be written in all sorts of interesting formats */
|
|
i = 100.
|
|
i = 0x100.
|
|
i = 0200.
|
|
i = 0b1010101.
|
|
i = 1_000_000_000.
|
|
i = 100.25.
|
|
|
|
/* tuples can be used to create a set of values */
|
|
|
|
tuple = (32, 'a string').
|
|
|
|
/**
|
|
they are similar to packages, except they only support consecutive integer
|
|
indices, and you cannot specify the indices when creating a tuple.
|
|
|
|
the main purpose of tuples is to allow unwrapping of iterator variables in
|
|
for loops.
|
|
**/
|
|
|
|
for (key, val) in pkg do
|
|
cout put:'{key} -> {val}'
|
|
end
|
|
|
|
/**
|
|
any object can be the target of a for-loop, as long as it meets one of the
|
|
following criteria:
|
|
a) is an Iterator object; or
|
|
b) implements the Iterator protocol; or
|
|
c) understands the -iterator message, and returns an object that itself
|
|
conforms to a) or b)
|
|
**/
|
|
|
|
/******************************************************************************/
|
|
|
|
/**
|
|
underscore (_) is used as a placeholder during assignment.
|
|
it can only appear on the left of an assignment, and anything
|
|
assigned to it is discarded.
|
|
*/
|
|
_ = 3 * 2.
|
|
|
|
/* it is mostly used with pattern-matching and destructuring. */
|
|
|
|
a = (32, 64).
|
|
(_, v) = a.
|
|
|
|
/* v is now equal to 64 */
|
|
|
|
|
|
/******************************************************************************/
|
|
|
|
try
|
|
v = Int parse:'342'
|
|
catch (#err:number_format, err) in
|
|
cout put:'Cannot parse integer string ({err})'
|
|
catch (_, err) in
|
|
cout put:'Unknown error occurred ({err})'
|
|
end
|
|
|
|
/* equivalent 'pure' syntax */
|
|
|
|
[ v = Int parse:'342' ]
|
|
on:#err:number_format do:[ :err :data |
|
|
cout put:'Cannot parse integer string ({err})'
|
|
];
|
|
onError:[ :err :data |
|
|
cout put:'Error {err} occurred ({data})'
|
|
];
|
|
call.
|
|
|
|
v = 5
|
|
squared
|
|
squared
|
|
squared.
|
|
|
|
/**
|
|
below are some examples of throwing errors. when an error is thrown,
|
|
the runtimes unwinds the stack looking for the nearest error handler.
|
|
if no error handlers are found, the exception information is written
|
|
to cerr and exection stops.
|
|
|
|
the exact behaviour of a throw statement depends on what is thrown:
|
|
1) a string is thrown: a generic Error object is created.
|
|
Sending -msg to the Error returns the thrown string.
|
|
Sending -data to the Error returns null.
|
|
2) an object that implements the Error protocol is thrown: the
|
|
thrown object becomes the error object that is passed to the
|
|
error handler (or that is written to cerr when execution stops).
|
|
3) an object that doesn't implement the Error protocol is thrown:
|
|
a generic Error object is created.
|
|
sending -msg to the Error returns null.
|
|
sending -data to the Error returns the thrown object.
|
|
**/
|
|
|
|
/* example 1) */
|
|
throw 'an error occurred'.
|
|
|
|
/* example 2) */
|
|
throw { i => 32, s => 'error' }.
|
|
|
|
/******************************************************************************/
|
|
|
|
x = 32.
|
|
/* a whole bunch of ways of doing conditionals using lambdas and messages */
|
|
[ cout put:'Hello, world!' ] if:x > 10.
|
|
[ cout put:'Hello, world!' ] unless:x <= 10.
|
|
|
|
/* and the equivalent 'fancy' syntax */
|
|
|
|
cout put:'Hello, world!' if x > 10.
|
|
cout put:'Hello, world!' unless x <= 10.
|
|
|
|
/**
|
|
NOTE that keywords (if, end, try, and so on) can still be used as message
|
|
parameter names. however, they cannot be used as message names.
|
|
**/
|
|
|
|
/******************************************************************************/
|
|
|
|
/**
|
|
Order of execution (precedence) for expression elements (listed from highest
|
|
to lowest precedence). elements that appear on the same list item have equal
|
|
precedence. in these cases, their associativity determines the order of
|
|
execution.
|
|
|
|
- binary operators
|
|
- package-access (->)
|
|
- self-access (::)
|
|
- unary operators
|
|
- boolean NOT (not)
|
|
- bitwise NOT (~)
|
|
- binary operators
|
|
- Type check operator (is, is not)
|
|
- Protocol check operator (understands)
|
|
- unary messages, complex messages
|
|
- binary operators
|
|
- multiplication (*)
|
|
- division (/)
|
|
- modulo (%)
|
|
- addition (+)
|
|
- subtraction (-)
|
|
- bitwise shift (<<, >>)
|
|
- comparison (<, >, <=, >=)
|
|
- (in)equality (==, !=)
|
|
- bitwise AND (&)
|
|
- bitwise XOR (^)
|
|
- bitwise OR (|)
|
|
- boolean AND (and)
|
|
- boolean OR (or)
|
|
- cascade (;)
|
|
- inline if-else
|
|
- keyword messages
|
|
- binary operators
|
|
- assignment by product, quotient, and remainder (*=, /=, %=)
|
|
- assignment by sum and difference (+=, -=)
|
|
- assignment by bitwise shift (<<=, >>=)
|
|
- assignment by bitwise AND, XOR, and OR (&=, ^=, |=)
|
|
- assignment (=)
|
|
**/
|
|
|
|
p1
|
|
setAge:2 squared squared + 4 squared multiply(by:2 add:4 + 1 * 3)
|
|
in:'mon' + 'ths'.
|
|
(p1
|
|
setAge:(((2 squared) squared) + ((4 squared) multiply(by:2 add:(4 + (1 * 3)))))
|
|
in:('mon' + 'ths')).
|
|
|
|
/******************************************************************************/
|
|
|
|
/* how about package comprehension? */
|
|
|
|
/**
|
|
these two lines both construct a package containing all even numbers between
|
|
0 and 100 incremented by 1
|
|
|
|
the first one uses fancy list/package comprehension syntax.
|
|
the second one uses pure message/lambda syntax.
|
|
**/
|
|
data = { x + 1 for x in 0 to:100 if (x % 2) == 0 }.
|
|
data = (0 to: 100) select:[ :x | ^(x % 2) == 0 ] collect:[ :x | ^x + 1 ].
|
|
|
|
/**
|
|
for a package comprehension that follows this template:
|
|
EXPRESSION for VARIABLE in COLLECTION if CONDITION
|
|
the equivalent -select:collect: message would look something like this:
|
|
COLLECTION select:[ VARIABLE | CONDITION ] collect:[ VARIABLE | EXPRESSION ]
|
|
|
|
-select:collect: is used to create a new package by filtering and
|
|
transforming an existing package.
|
|
|
|
To apply a filter without a subsequent transform, you can use -select:
|
|
To apply a transformation with a filter beforehand, you can use -map:
|
|
**/
|
|
|
|
/******************************************************************************/
|
|
|
|
/**
|
|
packages also support a -map: message for constructing a new package by
|
|
manipulating an existing one.
|
|
**/
|
|
|
|
/**
|
|
this example creates a package of all ints between 1 and 5 using a regular
|
|
collection literal, and then uses -map: to create a second package by
|
|
doubling each of the numbers in the fist package.
|
|
**/
|
|
pkg1 = { 1, 2, 3, 4, 5 }.
|
|
pkg2 = { x * 2 for x in pkg1 }.
|
|
pkg2 = pkg1 map:[ :x | ^x * 2 ].
|
|
|
|
/******************************************************************************/
|
|
|
|
/**
|
|
semicolon (;) is the cascade operator. it returns the recipient of the
|
|
previously sent message. it can be used to send multiple messages to a
|
|
single recipient.
|
|
**/
|
|
|
|
age = Person new(name:'John Doe' age:34);
|
|
setAge:144 inUnit:#months;
|
|
ageInMonths.
|
|
|
|
-- age now has the value 144
|
|
-- the same behaviour can be achieved by using do..end
|
|
|
|
age = do
|
|
x = Person new(name:'John Doe' age:34).
|
|
x setAge:144 inUnit:#months.
|
|
x ageInMonths
|
|
end
|
|
|
|
/**
|
|
this allows messages to be easily chained, without relying on the message
|
|
handler returning `self`.
|
|
**/
|
|
|
|
/**
|
|
NOTE that cascade is a binary operator, and cannot be used without a
|
|
right-hand operand. otherwise, simply adding a semicolon to the end of an
|
|
expression would change the behaviour (and result) of the expression.
|
|
|
|
however, because cascade is a binary operator, it also supports implicit
|
|
line continuations.
|
|
|
|
for situations where you want to return the recipient after a chain of
|
|
cascaded messages, you can use the -yourself message, which is understood by
|
|
all objects by default and always returns the object itself::
|
|
**/
|
|
|
|
p1 = Person new(name:'John Doe' age:34);
|
|
setAge:100 inUnit:#months;
|
|
yourself::
|
|
|
|
/* p1 now contains the Person object */
|
|
/* again, with do..end */
|
|
|
|
p1 = do
|
|
x = Person new(name:'John Doe' age:34).
|
|
x setAge:100 inUnit:#months.
|
|
x
|
|
end.
|
|
|
|
|
|
v = "Hello".
|
|
if v is String then
|
|
cout put:"v is a string!"
|
|
end
|
|
|
|
/******************************************************************************/
|
|
|
|
/**
|
|
Blocks are a bit like lambdas, except they are part of the context of the
|
|
code that uses them. They are essentially a way of evaluating multiple
|
|
statements in a place where a single statement is usually expected.
|
|
**/
|
|
|
|
/* Blocks are delimited by do..end, and the return operator can be used within
|
|
them. When it is, the value that is 'returned' will become the value that
|
|
the block as a whole evaluates to. also, the result of the last expression
|
|
evaluated in a block will be taken as its overall value. */
|
|
val = do
|
|
x = 3.
|
|
y = 2.
|
|
x * y
|
|
end.
|
|
|
|
/* after this statement is executed, `val` will be equal to 6. */
|
|
|