stz modules and importing
We know how to export things and we know during compilation we're running in the compilation-scope-context. So how do we pull things in to that from other compilation-scope-contexts?
First we should side-bar and talk about source code management. Smalltalk has always had a long history of doing its own thing. It has incredible development environments with reflection and ease of navigation and powerful source code management solutions like ENVY and Store.
But this is a systems level language we're designing here. We don't get to start with a big fancy development environment. We're down in the gutter looking up at the stars. So we're going to use files and store the stuff with git. For those of you not versed in this Smalltalk centric attitude and wonder why I make it sound like files+git is a compromise. It is, but it's a pretty good compromise. One we may never need to revisit.
When we compile we specify one source file we're going to compile. It will pull in anything and everything else of interest through its import instructions. Therefore importing references a resource that is most like a file. But we want files to work in concert with each other if they're part of the same module. Therefore the first thing we need to define is what module we're in.
We also need to pick some terminology. Module is a cool word but feels overloaded. In store you have a Package and in many other languages you have packages too. And various synonyms for packages... so let's just go with package:
package: 'my-stuff'
This tells us that the compiler scans all the files in its compilation scope looking for things that send package: a-name. That'd be problematic because we'd have to both compile and execute the code to know that. It might be enough to compile it and look for a package: send but let's be real here - it'd still require compilation and that is time wasted if you don't need to include calendar/julian in your program.
This forces us to come up with another way of specifying the package a file belongs to. The dumbest and simplest way I can think of to do that is to dictate how to name files: package-whateveryouwant.stz so let's just go with the dumb solution for now. We only need to parse filenames.
packages tend to exist in collections in file based languages. Your LIB directory might be somewhere separate from your SRC directory and you might want to reference your libraries with a name like lib:opengl.
That's certainly a way to do it. If we really need namespacing for packages then namespace-package-whateveryouwant.stz would also suffice as a naming convention. Let's just stick with that for now and the compiler is told every directory it should scan, eg:
stz build src/ lib/
Now let's talk about importing. The basics:
opengl := import: 'opengl'
Now we can reference opengl classes and methods that are exported via opengl.blahblah. What is in the opengl object anyway? and what kind of class is it? It's an opengl-package class which is constructed by the import: method.
We know from our experiments with reference that some types are methods. We need a way to to call them. They will be visible on the opengl-package which we now have an instance of:
person-ref = reference-package reference-of: person
That's a mouthful so we need a way to install stuff in reference-package on to our compilation-context-scope. First off let's name our package:
person.stz
person = { name: string, height: distance }
[ self init | person |
name = 'Unnamed'
height = 0 metres
]
We now have a person module and we're telling it to import something. import really should mean 'copy stuff from that other package in to me' which suggests when we want to use another package we should use a different method. Perhaps using:
reference-package = using: 'core/reference'
person-ref = reference-package reference-of: person
Now the importing variant needs a few fancy tricks. Other module importing systems allow you to skip naming the package in to a local variable and have your scope pick up the name of the package as a local variable for you. Import should do that. Using should do that too. If we rename reference-of: to just of: then something magical happens:
using: 'core/reference'
person-ref = reference of: person
Other module systems let you import everything or only some things. Some things tends to amount to writing out long lists of methods you want to be able to call which is somehow supposed to be a good idea. I find it onerous. If we really want to import some things in to our context and/or rename them you can write a method:
using: 'core/reference'
[ self reference-of: thing | reference-package, $T -> $R | reference of: thing ]
That might seem like a grumpity response but it's an honest one. Packages should be small and to the point so that you don't pollute other peoples contexts. You also have to explicitly say you're exporting something which helps too. Keep in mind that everything has a receive and what you're really importing are the package-level methods. So with that - import v2:
opengl-ui.stz
using: 'core/reference'
import: 'graphics/window'
import: 'graphics/opengl'
export:
[ main |
main-window = { reference of: window | new init, title: 'My Window' }
glc = { reference of: opengl.context | new init }
glc bind: main-window
]
The obvious problem with this is its too wordy to make a reference. References are going to be super common. They're so common most other systems languages use a piece of syntax to describe them. Usually & and * or ^.
For now let's add a method to class called ref returns the reference:
core-reference.stz
using: 'core/memory'
reference = { address: memory address, allocator: memory allocator }
export:
[ a-class ref | class -> $T | reference variant: $T ]
export:
[ self deref | $T ref -> $T | memory get: address as: $T ]
export:
[ self new: heap-allocator | $T ref, memory allocator -> self |
address = heap-allocator new: $T
allocator: heap-allocator
]
export:
[ self new | $T ref -> self | new: memory default-allocator ]
opengl-ui.stz
using: 'core/reference'
import: 'graphics/widgets'
import: 'graphics/opengl'
export:
[ main |
main-window = { widgets.window ref | new init, title: 'My Window' }
glc = { opengl.context ref | new init }
glc bind: main-window
]
Suddenly we're starting to look like a real programming language. Exports and imports, instantiations, code blocks, a smattering of flow logic...
There's plenty of holes to be confused over still. This blog post came about because of the desire for reference to act as a proxy. So.. what if it's as simple as this:
export:
[ self variant: another-class | reference, class -> $C |
variant = { class | }
variant import: another-class
variant import: reference
variant
]
This also tells us how we make variants in general. We make a new class and import the base. What this doesn't tell us is how the type system avoids creating numerous versions of the same class. We'll have to work on that. Perhaps in the next blog post.