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.