Unicon packages
In large programs, the global name space becomes crowded. You can create
a disaster if one of your undeclared local variables uses the same name as
a built-in function, but at least you can memorize the names of all the built-in
functions and avoid them. Memorization is no longer an option after you add
in hundreds of global names from unfamiliar code libraries. You may accidentally
overwrite some other programmer's global variable, without any clue that it
happened.
Packages allow you to partition and protect the global name space. Every
global declaration (variables, procedures, records, and classes) is "invisible"
outside the package, unless imported explicitly.
The package declaration
A source file declares that its global declarations are to be placed in
a package using the package declaration :-
package <packagename>
<packagename> should be a valid identifier. There can be only
one package declaration in each source file. It needn't appear at the
top of the file, but that is conventional.
Here is an example source file which declares some global declarations and
adds them to a package.
# File pack1.icn
package first
procedure my_proc()
write("In my_proc")
end
class SomeClass()
method f()
write("In SomeClass.f()")
end
end
When this is compiled, the unicon compiler updates its database to record
the fact that the package "first" includes the global symbols "my_proc" and
"SomeClass", and is defined (at least in part) in "pack1.icn". The compiler
also applies a "name mangling" process to the global symbols. This means
that in the generated ucode file, "my_proc" and "SomeClass" actually appear
as "first__my_proc" and "first__SomeClass" respectively. This is how
the package system prevents global symbol clashes.
The import declaration
Having created a package, it is very simple to access its global symbols,
by using the "import" statement, which has the following syntax :-
import <packagename>
This causes the compiler to look up the package in its database. From
that information it can deduce which link statements are necessary, and which
symbols in the source file need to be mangled to correspond to the package's
imported symbols. For example :-
import first
procedure main()
local c
c := SomeClass()
c.f()
my_proc()
end
The compiler will automatically link the resulting object file to the file
"pack1" above, and will mangle the references to "SomeClass" and "my_proc"
so that they work as expected.
Explicit package references
It may happen that two imported packages define the same symbol. Or,
it may be that a symbol is imported from a package but is also defined in
the global namespace. To resolve these problems, it is possible to explicitly
specify which package a particular occurrence of a symbol refers to. For
example if packages "first" and "second" both defined a procedure named "write",
then
import first, second
procedure main()
first::write() # Calls write() in package first
second::write() # Calls write() in package second
::write() # Calls the normal write() method
end
As an aside, the use of the "::" operator on its own, as in "::write()",
is a useful way to refer to a top-level procedure from within a class with
a method of the same name :-
class Abc(x)
method write()
::write("Abc x=", x)
end
end
In this example, omitting the "::" would cause the write() method to repeatedly
call itself, eventually leading to stack overflow.
Compilation order
One complication of using packages is that compilation order becomes significant.
To see why this is the case consider three source files, as follows
:-
# File order1.icn
package demo
procedure first()
write("first")
end
# File order2.icn
package demo
procedure second()
write("second")
first()
end
# File order3.icn
import demo
procedure main()
second()
end
The files "order1.icn" and "order2.icn" create a package called "demo", with
two procedures, whilst "order3.icn" imports the package and uses one of the
procedures? What is the correct way to compile these three files? If
we compile "order3.icn" first, then the compilation will fail with the message
"Unable to import package demo". So, we should compile "order1.icn"
or "order2.icn" first. If we try "order2.icn" first, then at least
the code compiles, but it doesn't work as expected :-
$ unicon -c order2.icn
$ unicon -c order1.icn
$ unicon -c order3.icn
$ unicon -o order order1.u order2.u order3.u
$ ./order
second
Run-time error 106
File order2.icn; Line 7
procedure or integer expected
offending value: &null
Traceback:
main()
demo__second() from line 8 in order3.icn
&null() from line 7 in order2.icn
What has happened here is that by compiling "order2.icn" first, we have not
yet put the symbol "first" into the package demo (to do that we must compile
"order1.icn"). So, when compiling "order2.icn" the reference to "first"
is not mangled as it should be. Hence it refers to an non-existent
procedure at runtime.
The correct order of compilation is therefore "order1.icn", "order2.icn",
"order3.icn".
One particularly confusing point to note is that if we run the incorrect
compilation sequence a second time, then we find the program mysteriously
works! The reason for this is that the first attempt at compilation
has added "first" to the compiler's database, so on the second attempt at
compilation the compiler picks this up. But on any "clean" compilation
(where the database has been deleted) the problem will present itself again.
The unidep utility
As can be seen from the above example, even with a small example the compilation
order needs some thought. With a large library (such as the Unicon
gui library) determining the correct compilation order becomes a very difficult
task, in fact one which cannot practically be done manually. Unidep
is a utility program to automate this task. It takes as command line
parameters several source files, and produces a set of makefile dependencies
which will guarantee correct compilation order. These dependencies
are appended to an existing makefile, which for the above example might be
as follows :-
all: order
clean:
rm -f order *.u uniclass.dir uniclass.pag
deps:
unidep order1.icn order2.icn order3.icn
order: order1.u order2.u order3.u
unicon -o order order1.u order2.u order3.u
%.u: %.icn
unicon -c $*
Now running the command "make deps" will append the required additional
dependencies to the makefile. In this case these are :-
### Autogenerated dependencies
order1.u : order1.icn
order2.u : order2.icn order1.u
order3.u : order3.icn order2.u
With these dependencies appended, the makefile will now compile the files
in the correct order. Re-running "make deps" will simply update the
autogenerated dependencies. This may be necessary if any of the source
files change.