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.