stz scoping variables and things

stz scoping variables and things
Photo by Azzedine Rouichi / Unsplash

We can scope methods easily enough:

public
[ a-public-method | -> ø ]

package
[ a-package-method | -> ø ]

private
[ a-private-method | -> ø ]

But when we want to create a class or a trait we suddenly lose that granularity at the variables and constants level. It is an array after all. But it's a special array. Well, two special arrays:

( key: value, key: value, key: value, )
( value, value, value, value, )

Without any typing the compile will infer the type and if the values are all types (and in the first version the keys are all unknown identifiers) it will create a structure or a tuple for you.

For a structure the keys are transformed in to member names on a structure object and their offsets computer. If we're talking about offsets then we're also talking about alignment. Where do we specify the alignment if we're not building the structure ourselves? (by which I mean { structure | ... }).

Suddenly it's not just the scope that we care about but also alignment. We also have no way of representing a "C union" right now - a type whose subtypes all overlap in memory.

// no alignment, everything overlaps, this is a C union:
foo1 = (
  #alignment: 0,
  name: string,
  status: foo-status, )

// alignment of 1 byte, everything is 'packed' with no padding:
foo2 = (
  #alignment: 1,
  name: string,
  status: foo-status, )

// custom alignment, forcing it to 32-bit instead of the platform padding:
foo3 = (
  #alignment: 4,
  name: string,
  status: foo-status, )

The # symbol is not a valid part of a signature. At least not at the start of the signature. As such we're stealing it here and adding a new syntax. This is a meta-property. There's also no restriction on repeating properties. The later version overwrites the prior version. That makes it possible to change the alignment mid-structure.

It also means we can specify the scope:

foo = (
  #scope: public,
  name: string,

  #scope: package,
  status: foo-status,

  #scope: private,
  is-banned: bool, )

To be consistent we can should now change the way we define scope for methods and classes in general too:

#scope: package
foo = ( name: string )

#scope: private
[ myugly-hacky-code: thing | foo -> ø | .. ]

I've been of two minds about whether structure == class in this system. The answer is still eluding me. But let's try and add all the meta-information to both classes and traits using our list-t example:

iterable-t = (
  #constants: (element-class: object-t,)
  #methods: (
    [ iterable length | iterable-t -> uint ],
    [ iterable[index] | iterable-t, uint -> iterable element-class ] ), )
list-t = (
 #constants: (element-class: object-t,),  
  length: uint,
  
 #scope: package,
  elements: &array of: element-class,
  allocator: &memory-allocator,)

Each entry in to the structure should be evaluated by the compile just like it would sequentially of any assignments:

a = 1
b = a + 2

That means by the time we hit elements: &array of: element-class the variable element-class will exist in the scope of the structure definition. It is effectively writing code for you at this point. With all that information it is building up the following objects and definitions:

#scope: public
iterable-t = { trait |
  constants: ('element-class': object-t,)
  methods: (
    [ iterable length | iterable-t -> uint ],
    [ iterable[index] | iterable-t, uint -> iterable element-class ] ) }

[ iterable-t-class of: element-class
| iterable-t class, object-t -> trait
| { trait |
    constants: ('element-class': element-class,)
    methods: iterable-t methods } ]

[ iterable-t-class element-class
| iterable-t class -> object-t
| iterable-t-class constants['element-class'] ]

list-t = { trait |
  constants: ('element-class': object-t,) }

[ list-t-class of: element-class
| list-t class, object-t -> trait
| { trait |
    constants: ('element-class': element-class,)
    methods: list-t methods } ]

[ list-t-class element-class
| list-t class -> object-t
| list-t-class constants['element-class'] ]

[ list-t-class of: element-class
| list-t class, object-t -> class
| { class
  | traits: (
      list-t of: element-class,
      iterable-t of: element-class, )
    variables: (
      length: uint,
      elements: &array of: element-class,
      allocator: &memory-allocator, ) } ]

[ list length | list-t -> uint | list .length ]

#scope: package
[ list elements | list-t -> &array of: list element-class | list .elements ]

[ list allocator | list-t -> &memory-allocator | list .allocator ]

Well that's a mouthful isn't it. I'm glad we've come full circle here and not only written out how it's really done but also created a nice short-hand for defining our classes. All of that generated code will be in the compilation scope context class to build these things for us. And based on a previous blog post it'll be heavily using string interpolation to generate it all too.

If I update the parser definition of what a signature is to allow # at the front then all of this is still unexceptional syntax. That's ideal to me - it does restrict what you can do with # though. You cannot have your own conflicting variable names in a structure that start with #. They should be considered a special reserved namespace.

The compilation scope context has been doing a lot of heavy lifting ever since we first introduced the idea way back at the beginning of this blog. Given it has to run in the interpreter (or does it?) it might be time to figure out just what it does to make al this magic work.