Pika 0.10.1 - Programming Guide

This software is provided 'as-is', without any express or implied warranty. In no event will the authors be held liable for any damages arising from the use of this software.

Permission is granted to anyone to use this software for any purpose, including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions:

1. The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation would be appreciated but is not required.

2. Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software.

3. This notice may not be removed or altered from any source distribution.


Table of Contents

1. Language Basics
Identifiers
Keywords
Comments
Single Line Comments
Multi Line Comments
Literals
Null
Boolean
Integer
Real
String
Array
Dictionary
Object
2. Expressions
Operators
Operator Precedence
Arithmetic
Shift
Bitwise
Logical
Concatenation
Comparison
Equality
is
has
Null Coalesce
Conditional Operators
Slice
Dot Operator
Index Operator
Bind Expression
3. Statements
Variable Assignment
Augmented Assignment
Unpacking
Swapping Values
Statement Blocks
begin
using
Control Statements
if, elseif, else
loop
while
for to, for downto
for in
break
continue
labels
Exception Handling
raise
try catch
re-raise
finally
catch is
else
4. Declarations
Storage Specifiers
local
global
member
Functions
Function Declaration
Calling a function
Return Values
Closures
Default Argument Values
The Variable Argument
The Keyword Argument
Argument Application
Redirect self
Packages
Declaring a Package
Super Packages
Accessing Current Package
world
Classes
Declaring Classes
Specifying the base class
Creating instances
Initializing instances
Class variables
Instance Methods
ClassMethod
super
Properties
Using Properties
Declaring Properties
When to use Properties
Annotations
Using Annotations
Creating your own Annotations
5. Advanced Topics
Coroutines
Context
Operator Overrides
Override Names
Binary Overrides
Unary Overrides
Conversion Overrides
Unpack Override
Get and Set Overrides

List of Tables

2.1. Precedence of All Operators

Chapter 1. Language Basics

This chapter deals with basics of the Pika programming language.

Identifiers

Identifiers in Pika start with a letter or underscore followed by any number of letters, digits or underscores and optional ending with a single ? or !.

toString
__package
valid?
set!
var1234

The back-tick can be used for identifiers that contain characters that are not allowed in identifiers.

`Id With Spaces`
`1234Id`
`Id-With-Dashes`

Keywords

The following are keywords and cannot be used as identifiers.

and         begin       bind        break       by
catch       class       continue    do          downto
else        elseif      end         false       finally
for         function    global      has         if
in          is          local       locals      loop
member      mod         not         null        of
or          package     property    raise       return
self        super       then        to          true
try         using       when        while       xor
yield

In addition the following have special meaning depending on the where they occur.

get         set

Comments

Single Line Comments

Single line comments start with a # (octothorp) and run to the end of the line.

# single line comments
x = 4 # set x

Multi Line Comments

Multi line comments are enclosed inside (* *). Multi line comments in Pika, unlike most other languages, can be nested.

(* Multiline -
       comment (* Yes, 
    They can be nested *)
*)

Literals

Null

Null is the absence of a value. By default all variables and missing function arguments are set to null. A null value is converted to false when ever a boolean is needed, however null does not convert implicitly to any numerical value.

null

Boolean

Booleans represent a true/false, on/off, yes/no or present/absent state. There are only two boolean values one called true and one called false. All objects are implicitly converted to a boolean value when used in a conditional statement, expression or loop. To override the default conversion implement the toBoolean method.

true
false

Integer

Integers represent a finite subset of all positive and negative whole numbers.

4
23303
23211

You can also specify the radix of an integer.

Hex and binary have special forms.

0xFF44FFAA
0b101011011

However any radix from 2 to 36 can be used.

36rPIKALANG
8r654501

An underscore can be used as a place separator. However, you cannot start an integer with an underscore. That would make it an identifier.

1_000_345
36rPIKA_LANG

Real

Real numbers represent a subset of the set of all real numbers. A 64bit double is the C++ data-type used.

120.0
0.233421
123.00
1e200  # exponent
42e-4

Reals, like integers, can use separators and different radices. The only restriction is you cannot mix exponents and a radix specification since the e or E would be counted as a digit.

36rPIKA.LANG
0b10011.10011
1_234_234.000_1

String

In Pika strings can contain any byte value including nulls. This means strings can contain arbitrary binary data including utf-8 characters. Strings can be declared using either single or double quotes. A single quotes string can contain double quotes and vice versa.

'Hello World!'
"Goodbye for now"
"That's a good idea!" # ' inside a " string
'She said "hello".'  # " inside a ' string

The following are valid escape codes usable in strings

  • \a Bell
  • \b Backspace
  • \f Form Feed
  • \n New Line
  • \r Carriage Return
  • \t Tab
  • \v Vertical Tab
  • \s Space
  • \\ Back Slash
  • \" Single Quote
  • \' Double Quote
  • \x and \X Hexadecimal byte
  • \{ Left curly brace
  • \} right curly brace
  • A slash followed by digits is an ASCII character code (decimal)
  • A slash followed by any other character means skip the character in question

Array

Arrays can contain any values of all types and are enclosed in []'s.

# empty Array
[]
# Arrays with elements
[ 1, 2, 3, 4 ]

a = [ 1, "abc", ['a', 'b', 'c'], null ] 
print a[1]

Dictionary

Dictionaries are associative arrays of key:value pairs. You don't have to put quotes on keys whose value is also a valid identifier. However when indexing you need to always use quotes on string key values.

# empty Dictionary
{}
# Dictionaries with elements. 
{ a:1, b:2, c:null }

d = { a:'alpha', { name: 'Bob', age: 99 } }
print d['a']

Object

Object literals are made much like Dictionary literals, the difference is you specify a type for the Object.

obj = { type: Object, name: 'Bob', age: 99, job: 'Programmer' }

Chapter 2. Expressions

Operators

Operator Precedence

Table 2.1. Precedence of All Operators

call ++ -- if/else [] . **postfix operators
~ ! not + - ++ -- bindprefix operators
=>redirect self
* / // % modmultiplication and division
+ -Addition and subtraction
<< >> >>>bit-shift
< > <= >=comparison
== != === !== isequality
&bit and
^bit xor
|bit or
and &&logical and
xor ^^logical xor
or ||logical or
.. ...concatenation
??null coalesce
?:conditional

Arithmetic

Pika has the standard arithmetic operators. Of note is that regular division, /, will return a Real if the result is not integral, for integer only division use //.

a + b   # addition
a - b   # subtraction
a * b   # multiplication
a / b   # division
a // b  # integer division
a % b   # modulo
a mod b # modulo
-a      # negative
+b      # positive
++b     # prefix increment
--b     # prefix decrement
b++     # postfix increment
b--     # postfix decrement

Shift

Bit shift operators, including an unsigned shift.

a << b    # left shift
a >> b    # right shift, maintains the sign of the integer
a >>> b   # unsigned right shift, sign bit not propagated.

Bitwise

Bitwise operators.

a & b   # bit and
a | b   # bit or
a ^ b   # bit xor
~a      # bit not

Logical

Logical operators, include keyword alternatives. Both operators and and or short circuit when possible. It because of this the operators cannot be overridden. What you can do is override toBoolean.

# logical and
a && b
a and b

# logical or
a | b
a or b

# logical xor
a ^ b
a xor b 

# logical not
!a
not a

Concatenation

There are two types of concatenation operators. The standard one that concats two strings and one that inserts a space between two strings. Of course arrays can be concatenated, as well as any object that overrides the operator.

a..b    # concat
a...b   # concat with a space

Comparison

No big surprises with the comparison operators. If you've programmed before these should all be familiar.

a > b     # greater than
a < b     # less than
a >= b    # greater than or equal
a <= b    # less than or equal

Equality

Pika uses the C family's equality operators (and JS's same operator).

a == b    # equal to
a != b    # not equal to
a === b   # same as
a !=== b  # not same as

is

You can test if an Object is an instance of a given type with the is operator.

a is Object

has

You can test if an Object or collection has an instance variable or element with the has operator.

a has 'length'

Null Coalesce

Null coalesce will return the left operand if it is not null. The right operand is returned otherwise.

a ?? b    # null coalesce

This is equivalent to the following conditional expression.

a if a != null else b

Conditional Operators

Pika has two conditional operators. The first is C's tertiary operator ?:. The second is if-else.

# both equal to: if a then b else c
a ? b : c
b if a else c

Slice

Returns a range of elements.

a = [ 1, 2, 3, 4, 5 ]
slice = a[ 2 to 4 ]

Dot Operator

Used for member access.

world.Object.type
Type.base

Index Operator

Used to access elements.

dict['name']
foo.array[2]

Bind Expression

Binds, usually, an object to a function. Must be applied to a dot.expression or index[expression]. Perfect for making callback functions. Like most other operators it can be overridden

Warning

There is problem in version 0.10.1 where certain native functions are improperly bound. It is fixed in the repository and new releases should be up soon.

fn = bind a.foo   # bind object 'a' to method 'foo'
fn()

Chapter 3. Statements

Statements in Pika are terminated by a newline.

print 'statement 1'
x = 'statement 2'

If you want to place multiple statements on the same line you can use a semicolon to separate them.

print 'statement 1' ; x = 'statement 2'

Variable Assignment

Variables in Pika are created when you assign a value to them. See storage for information on controlling where variables are stored.

The value stored inside a variable can be changed at any time by assigning it a new value.

x = 2
a, b, c = 1, 2, 3

Augmented Assignment

Pika supports the following augmented assignment statements. For example a += b is equivalent to a = a + b.

+=    -=    *=    /=    //=
%=    <<=   >>=   >>>=  |=
^=    &=    ..=   ...=

Unpacking

Unpacking happens with you assign multiple variables a single value that is a collection or an object that overrides opUnpack.

a = [ 1, 2, 3 ]
x, y, z = a

Swapping Values

You can swap the values stored in variables by taking advantage of the assignment statement. You can juggle the values of any number of variables as well.

a = 55
b = 'alpha'
a, b = b, a   # swap the a & b

# rotate the values to the left
w, x, y, z = 1, 2, 3, 4
w, x, y, z = x, y, z, w

Statement Blocks

begin

The most basic kind of block. All it does is at another local variable scope.

begin
  x = 'something'
end

using

A special kind of block where the subject becomes the self object. In addition before entering the block subject.onUse is called and at exit subject.onDispose is called. If onUse returns successfully (no exceptions) onDispose is guaranteed to be called, even if an exception is raised. This make using ideal for handling resources that must be cleaned up, such as files and sockets.

The subject is not required to implement either onUse or onDispose however.

using File.new()
  self.open("foo.txt", "w")
  self.write("Hello File System!")
  # close is called automagically!
end

Control Statements

if, elseif, else

Executes a block of code only if the condition is meet.

if x
  print "x passed"
end

The elseif block will be tested when all previous tests failed.

if x
  print "x passed"
elseif y
  print "y passed, x failed"  
end

The else block must be specified last an is executed only if all if and elseif tests failed.

if x
  print "x passed"
elseif y
  print "y passed, x failed"
else
  print "y & x failed"
end

You can put the test expression and the block's body, of if and elseif statements, on the same line using the keyword then

if x then  print "x is valid" end

loop

The loop statement is the most basic kind of loop, an infinite one. The only way to exit is via break, return and raise.

loop
  print 'looping...'
end

while

Loops until a condition is met.

x = 0

while x < 20
  x += 5
  print x
end

for to, for downto

Loops on an range of integer values. The start and ending range can be any expression that evaluates to an Integer.

# count up to 6
for i = 0 to 6
  print i
end

# now count down to 0
for i = 6 downto 0
  print i
end

By default the loop variable is incremented by 1. You can change that with by.

for i = 6 to 0 by -2
  print i
end

for in

Loops on the contents of an object or collection.

dict = { name:'Bob', age:35, sex:'male' }
for x in dict
  print x
end

The keyword of allow you to enumerate a set of values instead of the default.

The most common enumerable sets include (with the default listed first):

  • Dictionary: keys, elements
  • Array: indices, elements
  • String: indices, elements
  • Object: names, values
  • Type: names, values, methods, classmethods, properties
for x in 'elements' of dict
  print x
end

break

Exits the loop immediately.

loop
  print 'about to break'
  break
  print 'never executed'
end

continue

Immediately starts the next run through the loop.

for i = 1 to 10
  if i mod 2 != 0
    continue
  end
  print '{i} is even'
end

labels

Below the outer most loop is labeled 'outer' and the inner most is labeled 'inner'. There can only be whitespace and comments between a label and the loop you are applying the label to.

outer:
for i = 4 to 0 by -1
  inner: for j = 0 to 4       
    if i == j
      print 'i==j breaking...'
      break outer
    end
  end
end

Exception Handling

raise

A raise statement is used to raise an exception. The value raised can be any valid value. However only using Error derived exceptions is recommended.

function oops()
  raise Error.new "oops something went wrong."
end

try catch

Try blocks are used when you want to handle the exceptions that might come from a block of code. Each try block must have at least one catch statement. Below the catch statement will catch all exceptions, it's called a catch-all.

try
  oops()
catch e
  print "{e.type} {e}"
end

re-raise

Inside a catch block the caught exception can be raised again by using raise without a value.

try
  try
    oops()
  catch e
    print 'catch #1 {e}.'
    # raise the exception again.
    raise
  end
catch e
  print 'catch #2 {e}.'
end

finally

Finally blocks will always be executed, even if an exception is raised.

try
  oops()
catch e
  print "{e.type} {e}"
finally
  print "finally block was executed."
end

A finally block can be used as a finalizer for other types of blocks. Since there is no catch block the exception will be automatically re-raised after the finally block is executed.

# block with a finally statement
begin
  print 'begin'
finally
  print 'finally'
end

class, function, using, while, for and loop statements can also have a finally block.

# Function finally block
function()
finally
end

catch is

Used to only catch exception of a certain type.

try
  oops()
catch e is String
  print 'string exception: "{e}" caught'
catch e is Error
  print 'Error exception: "{e}" caught'
catch e
  print 'Generic catch statement'
end

As you can see above, multiple 'catch is' statements can be used. Also the catch-all statement must be the last catch block specified.

else

Executed immediately after the try block when no exception is raised. Must be declared after all catch blocks and before the finally block.

try
  x = 5
catch e is String
  print 'Error caught: {e}'
else
  print 'no exception raised'
end

Chapter 4. Declarations

Storage Specifiers

Storage specifiers allow you to specify where a declaration will be stored.

By default, in the main scope of a script, variable declarations are stored locally. While function, class, package and property declarations are stored globally. Inside a function body everything is local by default. Inside a class or package everything is global by default.

local

The declaration will be stored on the stack. Local is faster but is limited to its block's and sub-block's lifetime. See Closures to learn how locals can live beyond their block, sort-of.

global

The declaration is stored in the global package. Is accessible anywhere the package is. Each script has their own global package derived from Script to help reduced name clashes.

member

The declaration is stored in the current self. If self does not support writing or is null an exception is raised.

To declare storage without setting its values prefix it with the storage specifier

local x
global printer
member msg

Otherwise just prefix the declaration with a storage specifier. The specifier must come after all Annotations.

global x, y, z = 1, 2, 3
local function fn() 
end

member function foo()
end

local class A
end

member package utils
end

global property state
  get: function()
  end
end

Functions

Function Declaration

All function statements in Pika are identified by the keyword function. Followed by the function's name, which may be a dot.expression or an index[expression]. The arguments, if any, will be enclosed in parentheses. Finally an optional list of statements ending with the keyword end.

# basic no parameters
function f()
end

# multiple parameters and a body
function foo(a, b)
  c = a + b
  print "foo:  {a} {b} {c}"
end

Calling a function

foo(1, 2)

You can omit () if the first argument is on the same line as the function.

foo 3, 4 # OK: all arguments are on same line
foo 5,   # OK: because of ,
    6

Return Values

If no return statement is present either the last expression evaluated or null will be returned. Below no_ret has no expression to return so null will always be returned when it is called.

function no_ret()
end

The function adder, however, will return the result of a + b implicitly.

function adder(a,b)
  a + b
end

You return values from a function using a return statement.

# explicitly return a + b.
function adder(a,b)
  return a + b
end

You can specify multiple return values by separating them with a comma.

function ret_vals(multiple?)
  if not multiple?
    return 1
  else
    return 1, 2, 3
  end
end

x = ret_vals(false)
print x

x, y, z = ret_vals(true)
print x, y, z

Closures

In Pika functions can be nested. When local variables of a parent function are accessed by a child a Closure is formed. The function outer's variables will live on even after outer returns because inner needs access to them.

function outer(a)
  function inner(b)    
    # Local variables of parent scopes can be accessed by
    # inner scopes including functions.        
    return a + b
  end    
  # Since functions are 1st class primitives they can passed 
  # to and returned from other functions.
  return inner 
end

x = outer(3)
print x(3)

Default Argument Values

Unspecified arguments are set to null, you can override that behavior by specifying a default value for any regular argument. That default value will be used instead of null when the caller leaves the argument unspecified. All arguments with a default value must come after all other regular arguments, you cannot mix them, and before the two special arguments, variable and keyword, which you will learn about next.

function greeting(a = "hello", b = "world!")
  print '{a}, {b}'
end

greeting()
greeting('goodbye')
greeting('hello', 'universe!!')

The Variable Argument

The Variable Argument is an Array filled with unspecified positional arguments at the time the function was called. The Variable Argument must appear after all regular arguments and be prefixed with a : (colon).

function bar(:c)
  print c
end

bar(1,2,3,4,5)
bar(1,3)

Modifying bar to accept regular arguments in addition to the Variable Argument. After running it notice how the first two arguments passed to the function are not in the Array, thats because a and b were set to those first two values.

function bar(a, b, :c)
  print c
end

bar(1,2,3,4,5)
bar(1,3)

The Keyword Argument

The Keyword Argument is a Dictionary filled with unspecified named arguments at the time the function was called. It must always be the last argument declared and be prefixed by :: (two colons).

function key_test(:va, ::ka)
  print 'key_test called with vararg: {va} and kwarg: {ka}.'
end

Any function call can specify argument : value pairs. Even if the function does not have a Keyword Argument.

function location_test(city, state, zip)
  print 'location is: {city}, {state} {zip}'
end

location_test(zip:78701, city:"Austin", state:"TX")

Argument Application

You can apply the arguments of the current function call to another function call, this is called argument application. For regular arguments you pass them by name, while the Variable Argument's name needs to be prefixed with : (colon) and the Keyword Argument with :: (double colon).

The example below helps illustrate the concept. Here B calls A with Variable Arguments [1,2,3] and Keyword Arguments {a:'alligator', b:'bumblebee'}.

function A(:va, ::ka)
  print 'A called with vararg: {va} and kwarg: {ka}.'
end

function B(:va, ::ka)
  A(:va, ::ka)
end

B(1, 2, 3, a:'alligator', b:'bumblebee')

Redirect self

One potential problem with argument application is how to apply the same self to the function call. The problem will most likely creep up when creating Annotations for InstanceMethods and ClassMethods. If you don't know what those are come back after reading the section on Classes and Annotations.

Luckily Pika has an operator that will redirect the self object for a given function call. You can do this using =>, with the self object as the left hand operand and the function call as the right hand operand.

function redirect()
  print "self is:" ... self
end

obj = Object.new()
obj=>redirect()

Packages

Packages are an essential part of the Pika programming language. They prevent name clashes, two global variables in different scripts with the same name. They are the basic importable unit, Type, Script and Module are all derived from Package.

Declaring a Package

package A
  x = 'hi from A'
end

print A.x

Super Packages

Each package has a super package whose variables it can access in the global scope.

package X
  foo = "foo"
  package Y
    bar = "bar"
    # access foo from package X
    foobar = foo..bar 
  end
end

print X.Y.foobar

The super package is stored in the __parent property, which you can only access from the package directly.

print A.__parent

Accessing Current Package

A script is also a package. To access the current package use the __package variable.

print __package
print __package.A

world

The package world is the top most package in the global scope. All other packages are children of world, intentional or not. Inside world are the types, functions and other components common to all scripts.

Usually you can access the members of world without explicitly naming it.

print Object
print Function

Above print, Object and Function are all members of world. However sometimes you need to overwrite one of the global variables in world while still accessing the original. Consider the following problem, but do not run it.

# don't run this...infinite recursion
function print(a,b,c)
  print(a,b,c)
end

Can you see the problem? Since we created a function called print any time we call print in the same package the version above will be called. In this case causing an infinite loop. Note that this doesn't apply to packages outside print's declaration scope. Meaning we never actually wrote over the original.

So how do we fix it? By explicitly telling Pika to use the print function located inside world.

function print(a,b,c)
    world.print(a,b,c)
end

Classes

Classes are the blueprints used to describe objects from their creation to features and use. In Pika the result of a class statement is a Type object. All values in Pika are objects (though not necessary derived from Object) and have a Type. Here we will show the basics on how to create and manipulate new Types via the class statement.

Declaring Classes

Classes are declared with the keyword class followed by the name and the body.

class A
end

Specifying the base class

You can specify a base class by adding, : base_class, after the class name for some Type base_class.

Below A is the base class for B.

class B : A
end

Creating instances

You can create new instances of a given Type via the method new.

obj = Object.new(arg0,arg1)

Initializing instances

new creates a new instance and then passes the arguments to init, this makes init the best place to initialize objects. Of course instance variables can be added at anytime, just one of the advantages of dynamic languages.

class A
  function init(a)
    # create instance variable a.
    self.a = a 
  end
end

It's important to note that you must use self when attempting to read/write an instance variable or call a instance method. If you don't the variable will not be found and an exception will be raised.

class A
  function init(a)
    # create instance variable a.
    self.a = a 
  end
  
  function print()
    # access a
    # also notice since we overrode print we need to specify which one we want to
    # call.
    world.print self.a 
  end
  
  function call_print()
    # call another method
    self.print()
  end
end

a = A.new("foo")
a.call_print()

Class variables

You can create class variables inside the class body or by modifying the class directly.

class Counter
  # create an class variable
  num_instances = 0
  
  function init()
    # modify it each time an instance is created
    Counter.num_instances++
  end
end

print "Before: {Counter.num_instances}"
# make a couple of instances
c0, c1 = Counter.new(), Counter.new()
print "After {Counter.num_instances}"

Instance Methods

When a function is declared in a class body it becomes a instance method. Instance methods are methods used by instances of a class. The corresponding Type in Pika is called InstanceMethod.

The differences between a regular function and an instance method is that, when called, self must be of the correct type. This prevents instances of one two accidentally calling a method of another type. If you do want to do something like that use a regular function, since the type of self will not be checked.

Below both init and name? are instance methods.

class InstTest
  function init(name)
    self.name = name
  end
  
  function name?()
    print self.name
  end
end

i = InstTest.new("Bob")
i.name?()

You can also add an instance method after a class is created via the function addMethod.

Integer.addMethod(
  function abs()
    if self < 0
      return -self
    else
      return self
    end
  end
)
x = -5
print x.abs()   # >>> 5

ClassMethod

Class methods are methods that can only be used by a certain class. When called self is set to the value of the class it is bound to.

In a class body you can create a class method by a dot expression with the class on left and the name of the method on the right. You can employ the same technique to add a class method after the class is created as well.

class B
    var = "i'm a class var"
    function B.bar()
      print "{self} {self.var}"
    end
end

B.bar()

One final to add a class method is addClassMethod function.

String.addClassMethod(
  function do_nothing()
    print "do_nothing called with {self}"
  end
)

String.do_nothing()

super

Sometimes a derived class needs to call the same method but from its base class. The keyword super allows you to do just that.

class A
  function init(a)
    self.a = a
    print "A.init called with {a}"
  end
end

class B : A
  function init(a, b)
    super(a)
    self.b = b
    print "B.init called with {b}"
  end
end

b = B.new(33, 44)

Properties

Using Properties

Properties are somewhere between functions and variables. On the outside they behave like a variable, but under the hood a read or write operation causes an accessor function to be called.

One example of a property is Array.length. It is a read write property, but when used it acts like an instance variable of the array object.

a = [1, 2, 3]
print a.length  # read, calls getLength()
a.length = 5    # write, calls setLength(5)

Declaring Properties

Properties are declared with the keyword property followed by the name and the properties body. The body must contain a get or set label (or both), followed by either a function declaration or a expression whose result is a function. A function inside a property behaves the same as a function outside of one. This means a property's functions, when inside a class, are made into InstanceMethods and become part of the Type.

A read only property:

property full_name
  get: function()
    return self.first_name ... self.last_name
  end
end

A write only property:

property msg
  set: function(m)
      self.__msg__ = m...self.signature
  end
end

A read/write property:

property val
  get: function()
      return self.__val
  end
  set: function(m)
      self.__val = val.toInteger()
  end
end

When to use Properties

Although the choice is entirely up to you, properties should be reserved for values that need to manipulated in some way before they are written or read. Remember there is performance hit when compared to instance variables (since its really a function call in disguise). Don't let performance factor into your decision unless you really need the extra speed. Ease of use and clarity of the code should be paramount.

Annotations

Using Annotations

Annotations can be applied to functions, classes, packages and properties. You specify them with the character @ followed by the name and any, optional, arguments.

@strict
function bar()
end

Annotations can take arguments since under the hood they are just functions. The arguments can optionally be enclosed in parentheses and must be on the same line as the annotation's declaration.

@doc "I'm an annotated doc string!"
function foo()
end

print "{foo} - {foo.__doc}"

You can also pass Keyword Arguments.

@attr author:"Foo Bar", email:"foo.bar@example.com"
class Widget
end

print Widget.author
print Widget.email

More than one annotation can be applied to a declaration. The first one listed is the last applied, while the one closest to the function, class, package or property is the first applied.

@abstract
@doc "A basic widget"
@attr author:"Foo Bar", email:"foo.bar@example.com"
class Widget
end

print Widget.author
print Widget.email

The above code is equivalent to the following.

class Widget
end

Widget = abstract(doc(attr(Widget, author:"Foo Bar", email:"foo.bar@example.com"), "A basic widget"))

Creating your own Annotations

As discussed before, at their core annotations are just functions calls. All annotations must take at least one arguments, the object being annotated. We will refer to it as the subject for here on. Any other arguments must come after the subject. The annotation can take keyword arguments as well, like the annotation attr.

function donothing(subj)
  print 'donothing called with subject "{subj}"'
  return subj
end

@donothing
class FooBar
end

And finally a more complicated example:

# resultis: checks that the subject returns the correct type.
function resultis(subj, type)
  
  # See functions.pika if you don't know whats going on here.
  # Create a function that will take the place of 'subj'
  
  function __inner__(:v, ::k) 
    res = self=>subj(:v, ::k) # call the subj with the current self object and arguments        
    assert(res is type)       # check that the result is the right type
    return res                # return the result
  end
  return __inner__
end

@resultis Boolean
function lessthan?(x, y)
  return x < y
end


print lessthan?(0, 3)

Chapter 5. Advanced Topics

Coroutines

Coroutines, also known as Cooperative Threads, are an integral part of the Pika VM. Each script loaded has its own coroutine that executes the script, handle function calls, returns and yields. You can also programmatically create coroutines at runtime.

Unlike normal asynchronous threads, only one cooperative thread will run at a time. When that thread is finished it will yield or return to its parent thread. The parent can then continue execution or hand off to another cooperative thread. If a thread yields it can pick up where it left off when its called again.

Context

Coroutines in Pika are handled by the class called Context. You can create a new Context like you would any other class. The initialization function takes one argument, the function that will be executed. That root function, or any called functions, can use the keyword yield> to send values back to its parent. If you want to exit the context completely the root function must return.

function iter(n)
  for x = 0 to n
    yield x
  end
end

ctx = Context.new(iter)

Now that we have created a context lets setup the function call. What we are doing is supplying the arguments that will used to call the root function, in this case iter.

ctx.setup(5)

Now we can call iter.

print ctx.call()   # should print 0

Since Context overrides toBoolean we can know when the context returns.

Warning

There is a bug in Pika 0.10.1 where an objects toBoolean is sometimes not called. It is fixed in the repository and new releases should be up soon.
(* iterate the last few values. 
 * The last value, null, is the result of a return. *)
while ctx
  print ctx.call()
end

Thats the basics on using Pika's coroutine class Context. From here you should be able to handle more complex examples.

Operator Overrides

In Pika you can override the default behavior of object by implementing the correct override functions.

Override Names

Below are a list of all valid override names. This include the left hand and right hand variants. Notice that the right hand variants end with _r, such as opAdd_r and opMul_r. For left hand and postfix operators self is the left hand operand. For right hand and prefix operators self is the right hand operand.

opAdd         opSub         opMul         opDiv         opIDiv        
opMod         opPow         opEq          opNe          opLt          
opGt          opLte         opGte         opBitAnd      opBitOr       
opBitXor      opLsh         opRsh         opURsh        opNot         
opBitNot      opIncr        opDecr        opPos         opNeg         
opCatSp       opCat         opBind        opUnpack      opCall        
opGet         opSet         opGetAt       opSetAt       opAdd_r
opSub_r       opMul_r       opDiv_r       opIDiv_r      opMod_r
opPow_r       opEq_r        opNe_r        opLt_r        opGt_r
opLte_r       opGte_r       opBitAnd_r    opBitOr_r     opBitXor_r
opLsh_r       opRsh_r       opURsh_r      opCatSp_r     opCat_r
opBind_r      toBoolean     toInteger     toReal        toString
init          new

Binary Overrides

Binary overrides take exactly one argument, with self being either the left operand or right operand (for overrides ending in _r). There is no guarantee on the type of the other operand and you should test it a proceed from there.

class A
  function init(val)
    self.val = val
  end
  function opSub(rhs)
    if rhs is A
      return A.new(self.val - rhs.val)
    else
      return self.val - rhs
    end
  end

  function opSub_r(lhs)
    if lhs is A
      return A.new(lhs.val - self.val)
    else
      return lhs - self.val
    end
  end
  
  function toString()
    return self.val.toString()
  end
end

a = A.new(3)
b = A.new(6)
c = a - b
d = 3 - c
print c
print d

Unary Overrides

Unary overrides take no arguments, self is the operand. This hold true for both prefix and postfix operators.

class A
  function init(val)
    self.val = val
  end
  function opNeg(rhs)
    return A.new( -self.val )
  end
    
  function toString()
    return self.val.toString()
  end
end

a = A.new(3)
print( -a )

Conversion Overrides

Conversion functions are called to convert between Boolean, Integer, Real and String types. By default all types override toBoolean and toString.

class A
  function init(val)
    if not (val is Integer)
      raise TypeError.new "A.init expects an Integer value."      
    end
    self.val = val
  end
  
  function toBoolean()
    return self.val != 0
  end
  function toInteger()
    return self.val
  end
  
  function toReal()
    return self.val.toReal()
  end
  
  function toString()
    return self.val.toString()
  end
end

a = A.new(3)
print a

Unpack Override

The unpack override, opUnpack, takes an integer value which is the number of expected return values.

Get and Set Overrides

The member opGet, opSet and elements opGetAt, opSetAt are used for dot operator and index operator access respectively. They are also the most complicated override function because its easy to get caught in a infinite loop if your not careful.

Warning

Inside the override function be careful not to perform a dot or index access operation on the self object. This will lead to the override function being called continuously in an infinite loop. Instead use Object's class methods for accessing members.
class A
  # here we don't really do anything. its just to show the correct way to do things.
  function opGet(name)
      # must return a value.
      return Object.rawDotRead(self, name, val)
  end
  
  function opSet(name, val)
      Object.rawDotWrite(self, name, val)
  end
end