{ ... }
or begin ... end
keywords. All indentation in Converge must be in the form of spaces: no other form of whitespace is acceptable. Conventionally 4 space characters form one level of indentation, although you may use any number of space characters to indicate indentation provided you are consistent within your own code.
Hello world!
program:
import Sys func main(): Sys::println("Hello world!")As expected, when this program is run it prints out
Hello world!
.
Note how the body of the main
function is indented from its func main():
definition. The name of this function is also significant. When a program is run, Converge looks for a function called main
and executes that function. If no such function exists, an exception is thrown. Sys
is a built-in module. To lookup module definitions such as the println
function one uses the ::
operator.
hello.cv
, it can be compiled and run automatically as follows:
$ converge hello.cv Hello world! $In other words, when the VM is passed a source script, it automatically compiles dependencies, links, and executes the file. It also caches the results of the compilation so that subsequent runs are much quicker. It is often instructive to see what steps are being taken prior to the file being run. The
-v
switch to the VM shows the auto-make steps in operation:
$ converge -v hello.cv ===> Compiling /tmp/hello.cv... ===> Finished compiling /tmp/hello.cv. ===> Linking. Hello world! $Converge's auto-make facility ensures that all files in a project are upto date; its chief limitation is that it sometimes has to compile individual files multiple times to ensure that this is the case, even when a user may know that this is not strictly necessary. In such cases, it may be more efficient to use the individual compiler tools.
If you encounter strange
compilation problems (perhaps after moving files from one machine to another), converge can be instructed via the -f
switch to ignore all your cached bytecode files and perform compilation from scratch, updating all cached bytecode files. Although this is rarely needed, it can be useful on occasion.
func m(ai, ..., an, aj := <default value>j;, ... am := <default value>m, *v)
where i, j >= 0 and v
is optional. Put another way, this means that (optionally) normal arguments are (optionally) followed by arguments with default values and (optionally) completed by a final 'var args' variable.
The caller of a function must specify values for all normal arguments. Arguments with default values for which no value is specified by the caller will have their default value evaluated and assigned to that arg. All remaining arguments are put into a list and assigned to v
.
For example a printf
like function would be specified as follows:
func printf(format, *var_args): ...An
fprintf
style function which defaults to using standard out might look as follows:
func fprintf(format, stream := Sys::stdout, *var_args): ...Default values are evaluated each time a value is not passed to a particular argument. This is substantially different than the mechanism found in Python. Note that Converge has no concept of function overloading based on e.g. function arity.
0
then 1
then 1
.
func main(): x := 0 Sys::println(x) func f2(): Sys::println(x) x := 1 f2() Sys::println(x)Assignment of a variable in a block causes that variable to be made local to the block. The following program will thus print
0
then 0
then 0
.
func main(): x := 0 Sys::println(x) func f2(): x := 1 Sys::println(x) f2() Sys::println(x)In order to assign to a variable in an outer block, the
nonlocal
statement at the beginning of a function punches a hole up to an appropriate layer. The following program will thus print 0
then 0
then 1
.
func main(): x := 0 Sys::println(x) func f2(): nonlocal x x := 1 Sys::println(x) f2() Sys::println(x)Note that class elements are deliberately excluded from the standard scoping mechanism so that the following is invalid:
class Dog: type := "Dog" func get_type(self): return typeAs the above suggests, the first parameter of a bound function will always be set to the
selfobject. Conventionally this parameter is always called
self
. Via this variable the current objects slots can be accessed:
class Dog: type := "Dog" func get_type(self): return self.type
lambdas
small functionssuch as a specialised lambda form. However sometimes the indentation syntax of Converge can be inconvenient when passing functions containing only a few expressions as arguments to another function. In such cases one can use the alternative curly bracket syntax as follows:
Functional::map(func (x) { return x.to_str() }, list)
.
(dot) operator.
A simple definition of a class is as follows:
class Animal: func init(self, name): self.name := name func get_name(self): return self.name func set_name(self, name): self.name := nameNew objects can be created via the
new
slot of a class:
fido := Animal.new("Fido")The resulting object can then have its slots accessed and assigned in the standard fashion:
fido.set_name("Dido") Sys::println(fido.get_name()) fido.name := "Lido"When an object is first created, its
new
slot is called to set-up the object; it is common to override the default action provided by new
as in the above example.
Functions within a class are slightly different to normal top-level functions; when they are extracted from an object they are bound to that object. The distinguished variable self
is automatically assigned to the bound object. In all other respects bound and unbound functions are identical.
class Dog(Animal): ...If more than one superclass is specified, then any naming conflicts between two classes are resolved in favour of the class specified later in the superclass list.
C.instantiated(o)
succeeds if o
was created by C
's new
function. C.conformed_by(o)
succeeds if o
has a matching slot for every slot defined in C
(note that o
may have been created by a different class but still conform to C
).
For example whilst the following module is correct:
class Animal: ... class Dog(Animal): ...reversing the two classes would be incorrect because the creation of
Dog
would attempt to access the Animal
variable before it has had a class assigned to it.
However notice that it is only at the top-level that the ordering of elements is important. For example the following module is perfectly valid because by the time the new_dog
function can be called lexical scoping will have ensured that Dog
will have been assigned a value:
func new_dog(): return Dog() class Animal: ... class Dog(Animal): ...
::
operator to access definitions within a foreign module. It should be noted that modules are also normal objects and that a modules' normal slots can be accessed via the standard .
(dot) operator as any other object.
import
statement imports an element into a modules namespace. By default the imported element is assigned to a variable which has the name of the final part of the import
statement e.g. import A::B::C
will bring into existence a variable C
and assigns to it the value of the element A::B::C
. This behaviour can be altered by using the as X
suffix. e.g. import A::B::C as D
will bring into existence a variable D
and assigns to it the value of the element A::B::C
.
Converge needs to be able to determine at compile-time exactly which modules are imported by the import
statement. To facilitate this, the variable created by an import
statement can not be assigned to. In other words this is illegal:
import A::B::C as D ... D := 4
list := [1, 2, 3] set := Set{3, 2, 1} dict := Dict{"a" : 1, "c" : 3, "b" : 2} str := "123"
list := [1, 2, 3, 4] Sys::println(list[1]) // prints 1 Sys::println(list[0 : 2]) // prints [1, 2] Sys::println(list[-1]) // prints 4 Sys::println(list[1 : -1]) // prints [2, 3] list[3] := 10 Sys::println(list[-2]) // prints 10 list[1 : -1] := [5, 6, 7] Sys::println(list) // prints[1, 5, 6, 7, 4]Indexes start from zero. Negative indexes count n elements from the end of the list.
dict := Dict{"a" : 1, "c" : 3, "b" : 2} Sys::println(dict["a"]) // prints 1 dict["b"] := 10 Sys::println(dict["b"]) // prints 10
str := "1234" Sys::println(str[1]) // prints "1" Sys::println(str[0 : 2]) // prints "12" Sys::println(str[-1]) // prints "4" Sys::println(str[1 : -1]) // prints "23"
:=
operator to differentiate it from the equality operator ==
. Assignment returns the value of its right hand side. The following fragment prints 10
:
Sys::println(i := 10)Syntactic sugar is available for addition
+=
, multiplication *=
and division /=
e.g.:
i += 2The above is pure syntactic sugar for:
i := i + 2Since assignment is an expression, assignments can be chained. The following fragment assigns
10
to both x
and y
:
x := y := 10Assignment can be used to
10
then 20
:
x, y := [10, 20] Sys::println(x) Sys::println(y)The number of unpacking variables must equal exactly the size of the list being unpacked or a run-time exception will be raised.
Expressions in Converge can succeed or fail. When an expression succeeds it produces a value; when it fails, various outcomes are possible. A simple example of an expression that succeeds is:
x := 5 < 10This is in fact two expressions. Firstly the expression
5 < 10
succeeds and produces the value 10. The assignment of 10
to x
succeeds and similarly produces the value 10
. In the presence of an if statement the following results in the expected behaviour, printing Correct
:
if 5 < 10 then: Sys::println("Correct") else: Sys::println("Should never get here")If the expression in the
if
fails then the following fragment similarly produces the expected behaviour:
if 10 < 5 then: Sys::println("Should never get here.") else: Sys::println("Correct.")Essentially, the comparison
10 < 5
fails, which causes control to branch immediately to the else
branch of the if
statement.
Generators are generally used with the for
statement which will pump a generator for values until it produces no more. The following fragment prints all elements in the list l
:
l := [1, 3, 5, 8] for e := l.iter(): Sys::println(e)Functions can be made to produce multiple values (rather than simply
return
'ing a value) with the yield
expression. For example the following function will generate all values from m
up to but excluding n
:
func range(m, n): while m < n: yield m m += 1 failThe
fail
statement causes the function to both return and to transmit failure to its caller.
andand
or
&
operator can be used as the traditional andoperator and the alternation operator
|
as the traditional oroperator.
The following condition succeeds only if both comparisons succeed:
if i < j & i > 100: ...
Similarly the following succeeds if either the first comparison succeeds, if it is not, the second comparison succeeds (note that this evaluation is, as expected, lazy so that if the first comparison succeeds the second is not evaluated):
if i < j | i > 100: ...
if
statement is a bound expression, so the failure of the condition does not cause control to escape back beyond the if
statement. Significantly, each separate line in a source file is automatically a bound expression, so if the expression on one line fails, control does not backtrack to previous lines.
Alternation is a special kind of generator which successively generates each of its values. For example, the following prints 1
then 2
:
for i := 1 | 2: Sys::println(i)
==
, +
etc. The infix operators are simply syntactic sugar for calling these functions, and thus one can redefine their meaning by providing different definitions of the infix functions within objects. As well as the standard mathematic binary operators, the is
operator can be used to test two objects for identity equality.
if
control structureif <condition> then: ... elif <condition> then: ... ... else: ...Zero or more
elif
blocks can be specified. A maximum of one else
block can be specified.
ndif
control structurendif <condition> then: ... elif <condition> then: ... ...Zero or more
elif
blocks can be specified.
ndif
is the no default if
structure; in other words, a run-time exception is raised if none of the clauses' conditions succeeds. Since the default
action is therefore to raise an exception, ndif
statements can not contain an else
clause.
for
control structurefor
evaluates expression
and, if it is a generator, pumps it until it fails. If the loop terminates naturally through the exhaustion of expression
, the optional exhausted
block is executed. If the loop is terminated through a break
command, the optional broken
block is executed. Both exhausted
and broken
blocks may optionally be specified on a for
structure.
for <expression>: ... exhausted: ... broken: ...
while
control structurewhile
repeatedly evaluates expression
until it fails. If the loop terminates naturally through expression
failing, the optional exhausted
block is executed. If the loop is terminated through a break
command, the optional broken
block is executed. Both exhausted
and broken
blocks may be specified on a while
structure.
while <expression>: ... exhausted: ... broken: ...Note that
while
differs from for
in that expression
is reevaluated on each loop. This means that the following fragment will loop infinitely printing 1
:
l := [1, 2, 3, 4] while i := l.iter(): Sys::println(i)since the
iter
generator will be recreated anew on each loop which is probably not what was intended.
break
and continue
break
causes the innermost loop to be terminated immediately. continue
causes the innermost loop to attempt its next iteration immediately.
It is illegal to use break
or continue
outside of loops.
try
... catch
try: ... catch <Exception1> to <v1>: ... catch to <vn>: ...One or more
catch
's must be present. Zero or more catch
's catching specific exceptions may be specified. The final catch
may optionally not catch a specific exception at which point it will catch all exceptions that occur.
Object
is the base class from which all other classes ultimately extend. If a class does not specify a superclass, then Object
is automatically made its one and only superclass.
find_slot(name)
|
Returns the value of the slot name if it exists. Fails if name name does not exist.
|
get_slot(name)
|
Returns the value of the slot name . Throws an exception if name does not exist.
|
set_slot(name, value)
|
Sets the value of the slot name to value .
|
Class
or one of its subclasses. Thus the classes we have seen earlier in this document have elided the metaclass
which defaults to Class
:
class Animal metaclass Class: ...New metaclasses can be defined and used thus:
class Singleton(Class): func new(self): if not self.find_slot("instance"): self.instance := exbi Class.new() return self.instance class M metaclass Singleton: ...Sub-classes of
Class
generally need to override the new
method which is invoked when a class is instantiated, and is expected to return a new object.