compile time casting

compile time casting
Photo by Ludomił Sawicki / Unsplash

Currently there are three different kinds of string:

  1. raw strings: `a raw string`
  2. escaping strings: 'an escaping string'
  3. templating strings: "an ~{string-variable} string"

On top of that you can prefix any of those quote characters with a string that becomes part of the delimiter so that if you need single quotes inside single quotes or double quotes inside double quotes you can simply change which kind of quote you're using by making it something like mydelim"my "string" mydelim"

This is too much. The facilities are good but as far as language design goes this is a bad approach. What I want is one syntax for strings but to use the type system to change how it should be interpreted.

This is already true of numbers, eg:
age: Integer = 60
age: Float = 60

So why not for strings?
foo: String = "bar"
foo: EscapeString = "bar:\tbaz\n"
foo: TemplateString = "an {string-variable} string"

What this comes down to is compile time capabilities. Right now the type part of an expression is executed in the compilation scope to resolve itself. What we'd need to make different kinds of string interpretations be zero-cost at runtime would be to alter the parse tree based on the type.

This really comes down to how we treat literals. We could allow the type to be given the token stream to interpret it as a literal. That would allow just about anything that is syntactically valid to be used to create any kind of literal:
foo: emoticon-string: ";)"

(1) identifier : type = expression or literal
(2) identifier
:= expression or literal
(3) identifier
: type

Variation 1 is straight forward. We know the type and we could allow the type to interpret the literal; if it's an expression then there's nothing special we need to do.

Variation 2 determines the type based on the literal or the return value of the expression. That doesn't leave us any room to re-intepret things. But it does tell us a lot about writing efficient code, eg:

(a) room-temperature := 20 celsius
(b) room-temperature : Celsius = 20

In scenario (a) we're doing a runtime call and that is not something we re-interpret at compile time. We resolve what method that's being called but unless we add macros to the language, which I am loath to do considering how powerful the meta facilities already are, we can do nothing more with this.

In scenario (b) though we're effectively doing a compile time cast. In this case the transformation we want to do is {Celsius | value: 20}. Not terribly complicated but it tells us that the integers, floats, and strings being primitive literals can be transformed if the type is defined differently from what it is. Let's attempt to use that:

[number cast] [Integer -> Celsius | {value: number}]

That's shockingly simple. The only gotcha here is it needs to exist in the compilation scope below the one we're currently executing. But with that the compiler can lookup a method called cast that maps from the literal number to the class type celsius.

// a variant of String that escapes its characters as a literal
EscapeString :: {...String}.

// the escapes
Escapes :: {Character, Character | "t": 8, "n": 10}.

// casting integers to characters
[integer cast] [UnsignedInteger -> Character | {value: integer}].

// casting strings to escaped strings
[string cast] [string: String -> EscapeString |
  input: ReadStream[Character] = string.
  output: WriteStream[Character].
  input escape-into: output.
  output contents].

// i/o escape a string
[input escape-into: output]
  [    input: &ReadStream[Character],
      output: &WriteStream[Character]
   -> return: ø |
    input at-end then: [return].
    character = stream next.
    character = $\
      then: [output write: Escapes[character]]
      else: [output write: character].
    input escape-into: output].

And done. Assuming the compiler will attempt to resolve immediates to types at compile time by calling the method cast, this will now transform a string literal in to an escaped string such that runtime has no cost.

The same can then be done for templated strings; though that gets more complicated because what it generates is a new piece of code that glues all the strings together and calls code. It is a-kin to a macro at that point. We might try and tackle that in a future post.

What I should do is avoid implicit casting though, eg:

[foo bar: baz] [Integer, String -> Temperature | ...].

temp := 50.5 bar: "thing"

Here we should throw an error. You're not explicitly stating you want the Float to be an Integer here. Either the method should have used a Number instead of an Integer, or you should explicitly cast the 50.5 to an Integer using floor/ceiling/round.

And with that we have now removed all the different string syntaxes. I'm going to keep all three versions of string literals to allow single quotes in double quotes in back quotes so escaping them isn't required a lot of the time. Consider this: `"foo": 'bar'`.

Since we don't have implicit casting we may want some kind of inline casting syntax. But we don't want it to be too onerous - we could allow type to be a part of a subexpression, eg: (EscapedString | "Hello\tWorld\n").

Now if there's a method with more than one parameter variation you can explicitly pass the right type.