Petal compiler, specification, and tools 3.9KB

Open questions

  • How can we manage a shared system that allows thread-local GC, or is that even a goal?
  • How do we want to manage foreign function interfaces?

Should we include builtin rational numbers?

Rational numbers are really nice for some things. If you’re using rational numbers for time, for instance, you can represent times from ½^63 seconds to 2^63 seconds exactly. You can represent a third of a second. Or a fifth.

Unfortunately, you have two dimensions of overflow, not just one.

Answered questions

Do we need a character type?

No. A character type is misleading. It makes you think that ‘é’ should be an 8-bit value. Or if the only character type is 32-bit, it makes you think that ‘é’ should be one character, just like ‘é’. It makes you think you should be able to get the character at position n in constant time, and that’s just not possible.

string instead has explicit functions for each, but tends to work on a code unit basis:

  • string.bytes is a uint8[] slice of the same memory.
  • string[start -> end] is a shorthand for string(string.bytes[start -> end]).
  • string.byCluster iterates through character clusters with type string. This is the default iteration method.
  • string.byCodepoint iterates through codepoints with type uint32.
  • string.byCodeUnit iterates through code units with type uint8.

Will we want multiple builtin string types?

No. The runtime will pretend that the entire world is on UTF-8. The standard library will include an encoding section. This will support other encodings.

Will we have a separation between runtime and standard library, like D?

No. We want to share code (such as Unicode data) between the standard library and the runtime.

Do we want unions?

No. Unions are an advanced, unsafe feature. They let you do some useful things, but on the whole, we want to leave unions for a compiler-provided any type.

Do we need null?

No. We will use a library-provided type. Something like:

struct Maybe(T)
  null { value = Unit(). }
  set T v { value = v. }
  get -> T { assert value !is Unit. return value as T. }
  private: let any value = Unit().

I suspect that, with a little practice, the need to use Maybe will be minimal in many applications.

Do we want builtin typedefs?

We will have aliases. What about a typedef? A typedef is an exact duplicate of a type. It’s a type that differs from its input type only in name. They’re typically mutually convertible, but only with an explicit cast.

These are only rarely useful. D’s std.typecons.Typedef handles this by making the typedef’d type entirely opaque; a Typedef!int can’t be incremented, a Typedef!MyType has none of the members of MyType exposed; etc. This sucks.

A more thorough treatment would be to treat it as if you’d copy/pasted the source code and swapped the names around.

class Foo base Stream
    int i.
    this this:i.
    next -> Foo { return Foo(i + 1). }
    same Foo f -> bool { return this is f. }
typedef Bar = Foo.
# expands to:
class Bar base Stream
    int i.
    this this:i.
    next -> Bar { return Bar(i + 1). }
    same Bar f -> bool { return this is f. }

With sufficient metaprogramming, we could make this happen:

# Alternate expansion
struct Bar
    private let Foo _self.
    private this this:self.
    this int i { _self = Foo(i). }
    next -> Bar { return Bar(_self:next). }
    same Bar f -> bool { return _self:same(f:_self). }

I’m not that sold on typedefs, so we’re going with “no” for now.

Do we need interfaces? Multiple interfaces per type?


Let’s say we want some objects to opt into advanced formatting. Like I have a Currency type that should be formattable with similar options to floating point numbers. It needs to inherit from Formattable. But that shouldn’t affect its base class options. Or Serializable likewise.

And it’s not hard to imagine something that’s both formattable and serializable.