stz killing receiverless

stz killing receiverless
Photo by Rob Wicks / Unsplash

For many days now we've been embracing the syntax: { type | calls... } to create something on the stack. At compile time the type is computed and that tells the compiler how to construct the stack frame. But do we need it?

What is a variable? when it's on the stack it's a pointer relative to the stack pointer. But sometimes it's not on the stack. Sometimes it's in a register. When that happens the variable doesn't have an address but instead has an assigned register.

We can't use a 'stack allocator' to give us space for every variable because we want as many of them in registers as possible. That's where things run fastest. That is to say we can't, at runtime, 'allocate' on to the stack every variable without giving up on registers. I'm not going to give up on registers.

All that is to say we need a syntax to tell the compiler a variable exists and what it is. The 'what it is' tells us how big it is and allows us to put it either on the stack or in registers.

That syntax has been { type | calls... } but it's always felt a little awkward to me. It doesn't help that when we define closures and blocks we declare types differently to this too. We can't have three ways of declaring types for things. That's crazy!

Type 1: [ my-method | type1, type2 → return-type | ...method code... ]
Type 2: [ arg1: type1, arg2: type2 → return type | ...block code... ]
Type 3: foo = { type1 | ...initialisation code... }

We have a potential way of declaring default values for blocks, but not for methods. This is such a mess.

I asked the question 'could we use the Go/Jai/Odin approach' to solve this?
variable-name: type = value
variable-name := type-inferred-value
variable-name: type

I rejected this previously because variable-name: type is indistinguishable from a receiverless call. do-the-thing: type-parameter. May be the answer instead is to reject receiverless calls? The only area I think this impacts in any real respect is changing the scope for classes and methods: #scope: private.

Perhaps we could come up with some other way to do that bit. After all '#scope' could be a variable put in to our compilaction scope context and we can send a message to it: #scope private. Done.

Are there any other places where receiverless is a blessing? ... sort of yes - when we are in a method context the first argument because the 'default receiver' for messages. We could, instead, borrow a page from Smalltalk and dump the dot syntax: jane-person .name and only allow access to the fields of a structure from within a method that uses the object.

This would formally introduce 'variables' in to the language and some of those variables would be the members of the 'imported' parameters of a method.

[ person full-name | &person -> string | "~{last-name}, ~{first-name}" ]

We trade off the dot syntax and receiverless and in return we get the ability to define the type of a variable with a colon. It seems like an unfair trade but it might be the best choice. We now have the following ways to define arguments:

Type 1: [ my-object my-method: my-arg | type1, type2 → return-type | ...method code...]
Type 2: [ arg1: type1, arg2: type2 → return-type | ...block-code... ]
Type 3: jane-person: person

But we also gain the ability to define default parameters for Type 2 and 3.
Type 2: [ arg1: type1 = default1, arg2: type2 = default2 → return-type | ...block-code... ]
Type 3: jane-person: person = my-model make-person
(Type 3): jane-person := my-model make-person

The crazy thing here is we're back to Smalltalk like assignment syntax of := when we want it to infer the type of the variable for us. Cool!

This simplifies our syntax. Either you have a map of named types, or a list of types. But what if you wanted to list a whole bunch of data in your program? do you make a method?

si-unit-prefix := (name, symbol: string, base: int)
si-unix-prefixes := ( si-unit-prefix  |
  (something? name: 'kilo' symbol: 'k' base: 10 ** 3)
  ...)

The something? is bothering me. What is it and why do we need it. Smells like a 'factory' pattern to me. It seems receiverless and make blocks have a place after all:

si-unit-prefix := (name, symbol: string, base: int)
si-unix-prefixes := ( si-unit-prefix  |
  {name: 'kilo', symbol: 'k', base: 10 ** 3}
  ...)

Alright. So weirdness has just happened. We've come full circle in a single blog post because now you're back to being able to do this:

jane := {person | name: 'Jane'}

But you can also do this:

jane: person = {name: 'Jane'}

Did we achieve anything at all? kind of. The receiverless magic only happens within squiggle {} brackets. We have achieved a way to bind variables that is consistent too. We got rid of the dot syntax. All in all it's an improvement in our understanding of the syntax.

But just one more thing. If := is inferred type declaration and = is assignment of an already defined variable. Do we really need the :? Either you define the type or you don't. I personally think we should be able to use = anywhere we'd otherwise :=. Sorry Smalltalk, goodbye to the :=