Copyright © 2006-2010 Russell J Kyle
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
List of Tables
Table of Contents
This chapter deals with basics of the Pika programming language.
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`
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
Single line comments start with a # (octothorp) and run to the end of the line.
# single line comments x = 4 # set x
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
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
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 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
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
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]
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']
Table of Contents
Table 2.1. Precedence of All Operators
call ++ -- if/else [] . ** | postfix operators |
~ ! not + - ++ -- bind | prefix operators |
=> | redirect self |
* / // % mod | multiplication and division |
+ - | Addition and subtraction |
<< >> >>> | bit-shift |
< > <= >= | comparison |
== != === !== is | equality |
& | bit and |
^ | bit xor |
| | bit or |
and && | logical and |
xor ^^ | logical xor |
or || | logical or |
.. ... | concatenation |
?? | null coalesce |
?: | conditional |
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
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.
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
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
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
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
You can test if an Object or collection has an instance variable or element with the has operator.
a has 'length'
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
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
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
fn = bind a.foo # bind object 'a' to method 'foo' fn()
Table of Contents
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'
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
Pika supports the following augmented assignment statements. For example a += b is equivalent to a = a + b.
+= -= *= /= //= %= <<= >>= >>>= |= ^= &= ..= ...=
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
The most basic kind of block. All it does is at another local variable scope.
begin x = 'something' end
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
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
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
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
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):
for x in 'elements' of dict print x end
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
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
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 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
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 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
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.
Table of Contents
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.
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.
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.
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
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
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
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
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)
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 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 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")
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')
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 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.
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
A script is also a package. To access the current package use the __package variable.
print __package print __package.A
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 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.
Classes are declared with the keyword class followed by the name and the body.
class A end
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
You can create new instances of a given Type via the method new.
obj = Object.new(arg0,arg1)
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()
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}"
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
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()
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 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)
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
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 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"))
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)
Table of Contents
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.
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.
(* 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.
In Pika you can override the default behavior of object by implementing the correct override functions.
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 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 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 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
The unpack override, opUnpack, takes an integer value which is the number of expected return values.
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.
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