The pint° programming language
I started making a toy language that’s supposed to compile to Dart and have interoperability with it.
The main objectives of the language are:
- Have seamless1 interoperability with stable Dart;
- Generate legible Dart code that can be used as is;
- Be terser and more expressive than Dart;
- Provide a powerful macro system with great ergonomy;
- Try to solve the greatest pain points when using Dart, especially with Flutter.2
This post will be an introduction to the language specification and what we have now.
Identifiers #
Identifiers are exactly as in Dart. As the language compiles to Dart, it’s easier if we do it this way3.
|
|
Some examples of valid identifiers:
myVariable
_privateVar
totalAmount
calculateSum
UserName
MAX_VALUE
isValid123
And some invalid identifiers:
123start
(cannot start with a digit)my-var
(hyphens are not allowed)my/function
(operators can’t be used)
Type definitions #
The first construct I’m working on for the language is type definition.
|
|
The type
keyword defines an opaque type with structural identity.
|
|
Generated Dart code
|
|
You can also define sum types with the operator +
:
type LogLevel = Debug + Info + Warn + Error + Fatal
Generated Dart code
|
|
Types can also be parameterized:
type Option(T) = Some(T value) + None
Generated Dart code
|
|
Why compile to Dart instead of the Dart Kernel? #
When ClojureDart was announced, 3 years ago, I asked the same thing.
Why don't you compile to dill instead of Dart?
— Mateus Felipe (@mateusfccp) June 9, 2021
I never got a response from Cristophe (or anyone), but it’s actually not viable to compile to Dart Kernel.
The truth is: even though Dart is an open-source project, som things weren’t made to be consumed by others besides the Dart team itself. The Dart VM, for instance. The Dart VM interprets Dart Kernel, an IR that is generated by the language’s CFE.
It’s only logical that a language that compiles to Dart should compile to run in the VM directly. However, as said before, this is not what the Dart teams intend.
The Dart Kernel “documentation” states the following:
The APIs in this package are in an early state; developers should be careful about depending on this package. In particular, there is no semver contract for release versions of this package. Please depend directly on individual versions.
The thing is, the Dart Kernel never stabilized, since it was born, almost a decade ago. The last update to the README was 6 years ago. So I decided to ask directly in Dart’s discord channel, where many (if not all) members of the Dart team are present.
Egorov gave a direct answer:
No. It is not going to be stable. It changes all the time and is not intended for external use.
So, the situation with Dart Kernel is:
- It will never be stable;
- It changes all the time;
- There are no guarantees;
- There’s barely any documentation (we have to look the source code to understand how it works);
- It’s not supposed to be used by anyone except the Dart team itself.
Considering all this, I see why ClojureDart didn’t want to target the Dart VM, and I decided that I also shouldn’t try messing with this.
What’s next? #
For now, I have a parse that can parse the aforementioned grammar and a transpiler that generates the exact code that was shown. Nothing more, nothing less.
There are two approaches I could take:
- Design the language from the ground up, build a parser for it, then a resolver, and then a transpiler;
- Design a single feature/aspect, implement it in the parser, then the resolver, and then the transpiler. Repeat.
I’m taking the 2nd approach, for two reasons:
- By doing so, it’s easier to test and get feedback from anyone (if someone) using it;
- I want to tackle what I think will be the hardest part of this task, at least in the initial versions: interoperability.
Currently, as I have the parser, the compiler does not check for semantic issues. For instance:
type Option(T) = Some(U value) + None
This code is invalid, because the declared type parameter T
was not used,
and U
, which does not exist in this context, is used instead. This code will
generate a Dart program that is syntatically correct, but will fail to compile.
This problem is not too hard when you think in the language isolated, but as my language is supposed to interoperate with Dart, I have to consider which identifiers were defined or not.
type Id = Id(int idNumber)
This should work, even if we didn’t define int
before, as dart:core
should
be available in the default environment, just like in a Dart program.
This task also requires the ability to import other Dart or pint° files.
By doing so, I will have a minimally usable language that may be used alongside Dart, and then I can work on adding more relevant features until it’s good enough to replace Dart completely if one decides to.
By seamless, I mean that you should be able to work on a 100% pint° project by importing Dart packages, or a mix between Dart and pint° files without any problem. Also, there shouldn’t be surprises when dealing with language differences. For this, we have to make some compromises, like the identifier grammar. ↩︎
This language is intended to support Flutter as first-class. This means that, if we have to choose between an approach that is better for Flutter but worse for general programming, we are doing what is better for Flutter. One of the key factors for this to work is to have a good and ergonomic macro system. ↩︎
Particularly, I would prefer to support full Unicode for identifiers. Why can’t I define my variable as
final String 🔑
? Doing so, however, would harm interoperability. ↩︎