HN user

mraleph

852 karma

mrale.ph

Posts0
Comments286
View on HN
No posts found.
Elixir 1.19 9 months ago

We are done with NNBD transition for a few years now.

Dart 2.12 (released March 2021) introduced null-safety.

That started a 2 year transitionary period during which you could mix nullsafe (language versions 2.12 or above) and non-nullsafe (language versions below 2.12) code in one program.

Dart 3.0 (released May 2023) removed support for language versions prior to 2.12 - meaning that you can no longer opt out of null-safety.

A Walk with LuaJIT 2 years ago

At some point in my life (when I briefly worked on LuaJIT for DeepMind) I have written a stack walker which can stitch together native and Lua frames: for each native stack frame it checks if that is actually an interpreter frame or a trace frame - if that's the case it finds corresponding `lua_State` and unwinds corresponding Lua stack, then continues with native stack again.

This way you get a stack trace which contains all Lua and native frames. You can use it when profiling and you can use it to print hybrid stack traces when your binary crashes.

I was considering open-sourcing it, but it requires a bunch of patches in LJ internals so I gave up on that idea.

(There is also some amount of over-engineering involved, e.g. to compute unwinding information for interpreter code I run an abstract interpretation on its implementation and annotate interpreter code range with information on whether it is safe or unsafe to try unwinding at a specific pc inside the interpreter. I could have just done this by hand - but did not want to maintain it between LJ versions)

The fact that I can’t then immediately go

Who said you can't? :) This actually works in Dart:

    Dog? dog;
    bool isDog = dog is Dog;
    
    if (isDog) {
      dog.bark();
    }
i.e. boolean variables which serve as witnesses for type judgements are integrated into promotion machinery.

I do agree with you that

1. In general there is no limit to developers' expectations with respect to promotion, but there is a limit to what compiler can reasonably achieve. At some point rules become too complex for developers to understand and rely upon. 2. Creating local variables when you need to refine the type is conceptually simpler and more explicit, both for language designers, language developers and language users.

I don't hate excessive typing, especially where it helps to keep things simple and readable - so I would not be against a language that does not do any control flow based variable type promotion. Alas many developers don't share this view of the world and are vehemently opposed to typing anything they believe compiler can already infer.

It's all a matter of personal taste in the end.

The reason why languages promote variable types based on control flow is because developers en masse actually expect that to happen, e.g. facing the code like

    Dog? maybeDog;
    if (maybeDog != null) {
      maybeDog.bark();
    }
If compiler says "sorry, maybeDog might be null", developer (rightfully so) usually responds "but I have just checked and I know it is not, why do you bother me?". So languages chose to accommodate this.

What if I want to set the value back to nil if it is not-nil?

You can. The type of the variable does not actually change. You could say that the information about more precise type is simply propagated to the uses which are guarded by the control flow. The following will compile just fine:

    Dog? maybeDog;
    if (maybeDog != null) {
      maybeDog.bark();
      maybeDog = null;
    }
> Why should I have to wrap things back in an optional if I want to pass it along as such?

You don't, with a few exceptions. There is a subtype relationship between T and T?: T <: T?, so the following is just fine:

    void foo(Dog? maybeDog);

    Dog? maybeDog;
    if (maybeDog != null) {
      maybeDog.bark();
      foo(maybeDog);  // Dog can be used where Dog? is expected
    }
 
You might need to account for it in places where type inference infers type which is too precise, e.g.
    Dog? maybeDog;
    if (maybeDog != null) {
      // This will be List<Dog> rather than List<Dog?>. 
      final listOfDogs = [maybeDog];
    }
Though I don't think it is that bad of a problem in practice.

In Dart you use class hierarchies instead, rather than enums (which in Dart are way to define a compile time constant set of values). So the original example becomes:

    sealed class MySomething<T> {
    }

    final class One extends MySomething<Never> {
    }

    final class Two extends MySomething<Never> {
      final Child child; 
      
      Two(this.child);
    }

    final class Three<T> extends MySomething<T> {
      final T value;

      Three(this.value);
    }

    final class Four extends MySomething<Never> {
      final int Function() callback;

      Four(this.callback);
    }
And then you can exhaustively switch over values of MySomething:
    int foo(MySomething<String> foo) {
      return switch (foo) {
        One() => 1,
        Two(child: Child(:final value)) => value,
        Three(:final value) => int.parse(value),
        Four(:final callback) => callback(),
      };
    }
The declaration of a sealed family is considerably more verbose than Swift - and I really would like[1] us to optimized things for the case when people often declare such families. Macros and a primary constructors will likely provide reprieve from this verbosity.

But importantly it composes well with Dart's OOPy nature to achieve things which are not possible in Swift: you can mix and match pattern matching and classical virtual dispatch as you see fit. This, for example, means you can attach different virtual behavior to every member of the sealed class family.

[1] https://github.com/dart-lang/language/issues/3021

Dart+Flutter look promising until you notice that they run Skia WASM inside the dart VM which itself runs in WASM which runs inside a JS VM and performance is abysmal.

That's not how Flutter works on either of the platforms it supports.

When you run natively (e.g. mobile, desktop, etc) Skia or Impeller execute natively and all your Dart code is compiled ahead-of-time to native code as well. No JS or Wasm is involved anywhere in the stack. No JIT compilation in release binaries, only during development.

When you run on the Web - then naturally Skia is compiled to Wasm and Dart code is compiled to JS or Wasm, JS VM will end up running both of those. No Dart VM is involved anywhere here.

[disclaimer: I work on Dart, though not on the language team]

With primary constructors this will become something along the lines of:

    sealed class Message();

    class IncrementBy(final int value) extends Message;

    class DecrementBy(final int value) extends Message;

    class Set(final int value) extends Message;
Which is considerably less repetitive, though `extends Message` is still there. I am fairly optimistic that the next step would be to eliminate that[1], though I think we would need to gather a bit more feedback from users as people are getting more and more reps in with Dart 3.0 features. I personally would prefer something along the lines of:
    sealed class Message() {
      case IncrementBy(final int value);
      case DecrementBy(final int value);
      case Set(final int value);
    }
Current syntax is not all that bad if you are going to do OO and add various helper methods on `Message` and its subclasses, but if you just want to define your data and no behavior / helpers - then it is exceedingly verbose.

[1] https://github.com/dart-lang/language/issues/3021

it was built on top of v8

Was never built on top of V8 in any shape of form. Dart VM does not even share any code with V8.

Dart 1 was designed by people who originally started V8 (Lars Bak and Kasper Lund), that's the only connection.

It would be interesting to see 1-1 comparison with LuaJIT interpreter _without corresponding runtime changes_. That would given a meaningful baseline for how much you potentially loose/gain using this approach.

Current measurement presents comparison of two wildly different implementation strategies: LuaJIT Remake uses IC and hidden classes while LuaJIT proper does not. It's a well known fact that ICs+hidden classes can lead to major performance improvements. That insight originated in Smalltalk world and was brought to dynamic language mainstream by V8† - but unfortunately it muddles the comparison here.

† V8 was open sourced on September 2, 2008... Coincidentally (though probably not :)) JSC landed their first IC prototype on September 1, 2008.

The JIT still performs better, but the AOT code starts up faster.

I should update that side note because a lot of things has changed. It should probably say something like:

Theoretically, Dart VM JIT should have best peak performance, while Dart VM AOT has best startup time. In reality the comparison is quite complicated. AOT has seen a lot of work in the last few years, which made it faster than JIT in some aspects e.g. both direct and highly polymorphic method calls are usually faster in AOT. JIT's peak performance has been languishing in a state of neglect, but it still runs circles around AOT in some cases due to aggressive inlining.

Something like this.

The extension has recently reached Stage 2 - most of the questions around type system have been resolved. V8 has a working implementation. We have build `dart2wasm` compiler targeting this and it shows good numbers.

what's interesting is that static typing buys you somewhat minimal gains in performance.

There are a lot of caveats here. You could potentially claim "minimal gains in peak performance if you can apply adaptive JIT compilation techniques", but even that is stretching it somewhat.

Adaptive JITing comes at a price in terms of warmup, memory usage and implementation complexity. Fixed object shapes help somewhat to reduce the amount of checks needed but they don't take you all the way there. Optimising numerics remains challenging (e.g. think about optimising the case where a field always contains a `double` floating-point value or a field that always contains a 64-bit integer value). Knowing the shape of the container does not yield any information about the shape of elements which implies that some checks have to stay behind in the loops. Yes, monomorphic checks are usually simple (compare+branch) but polymorphic are not. And so on and so forth.

Yes, Dart 1 is easier to compile into efficient code compared to JavaScript. Dart 2 is even easier though - because it is more statically typed.

but fear that it painted itself into a corner by first targeting memory managed languages.

FWIW WASM GC is coming - and it looks great.

Flutter 2 5 years ago

We are planning to fix it some time in the future, we really only expected moderate usage of `dart2native` and only on Linux. Turns out that there is demand for using it all over the place, which includes Mac and Windows and requires things like signing support... And to make executable signable we need to make them real Mach-O / PE executables and not weird concatenations of bytes.

Flutter 2 5 years ago

[I work on Dart VM team]

`dart2native` just concatenates two binaries together: AOT runtime binary and AOT snapshot binary. AOT snapshot is an ELF binary which contains native code generated from your original Dart code.

The approach is not pretty but was chosen as an implementation shortcut.

That's why `strip` does not actually do good things to the binary.

There is no reason to run `strip` on such binaries - because they don't contain anything strippable.

The operator itself would throw an error.

Dart is planning to have a sound non-nullability which means that if something has static type `SomeType` then it is guaranteed to be a value that is actually `SomeType` and not something else.

Thus you can't take `SomeType?` and just cast it to `SomeType` because `SomeType?` can be null and `SomeType` can't be null.

This means this cast has to perform a check and throw if your value is null.

This also means that if you have `e1` and `e1` has non-nullable static type then `e1` would never be null in the NNBD world - which is a very good property (e.g. for optimizations).

[disclaimer: I work on Dart team]

Short answer: there are a lot of different editors in the world, but Dart team is not large enough and does not posses necessary expertise to develop top-notch integration with all of them. So it makes sense to focus on the very popular ones (and both VSCode and IDEA are very popular) and provide very good support for those.

We hope that community can take and support other editors. That is why we provide tools (e.g. dart analyzer which has an LSP wrapper) that make it easy to build Dart plugins for any editor you like to use.

There are plugins for both emacs (https://github.com/bradyt/dart-mode) and vim (https://github.com/dart-lang/dart-vim-plugin), which I think work relatively well, but they are not maintained by the Dart team.

In the two experiments that we did LLVM brought only marginal benefits so we could not warrant the huge dependency and associated maintenance costs. We already have a good compilation pipeline, which was developed for the JIT mode and we use that for AOT with good results. Adding LLVM on top increases complexity - suddenly to tweak things you need to be an expert both in our compilation pipeline and LLVM (which is probably 100x larger than the whole Dart VM source code).

A lot of optimizations which benefit Dart code size and performance require high level optimizations anyway, so having LLVM does not help you in any way.

That said there are obvious benefits for having LLVM as a backend - so we are planning to explore it yet again in the near future.

DDC stands for Dart Development Compiler. It is a fast modular compiler that you use for quick edit&refresh development in the browser. It does not do any global optimizations, unlike dart2js - which is what you use for deployment.

If you write something like:

    import 'package:flutter_web/animations.dart';

    void main() => print('Hello, World!');
DDC would faithfully compile animations.dart as whole and ship that to your browser.

dart2js would include 0 bytes of code from animations.dart into the output.

`webdev build` failed for me in examples/gallery

Maybe file a bug?

Here is what I get for Gallery:

    ╭─~/s/f/f/e/gallery ⟨master ⟩ ⟨9s449ms⟩
    ╰─» flutter packages pub global run webdev build
    ...
    Compiled 18,344,245 characters Dart to 1,914,077 characters JavaScript in 27.9 seconds
If I gzip the output I get around 500k.

Gallery uses a lot of Flutter so this is in some sense upper boundary for framework overhead.

Also it is still early days - I can clearly see this pushed down.

It's actually the very same main.dart.js (dart2js compiler does not have ability to target a specific browser, its output is supposed to work in every supported browser).

359kb is its compressed size, 990k is its uncompressed size.

Firefox shows compressed size in the "Transferred" column and uncompressed size in "Size" column.

In Chrome by default you see compressed size only, but if you click "Use large request rows" then you will see both compressed and uncompressed size.

but it also compiles to ARM binary via LLVM for iOS applications

[disclaimer: I am TL for Dart Native Compilers]

We currently don't use LLVM for compiling Dart to native and we actually never used it in production - though we previously built couple of prototypes to evaluate potential benefits of using LLVM.

We use our own AOT compilation toolchain which has roots in our JIT compiler for Dart.

meaning that compiling to WASM should be a trivial option

It's not really trivial because you still need to figure out some things - most importantly GC. On ARM you can scan the stack - on WASM you can't. They are working on GC support from WASM, but I don't think it is ready yet. And some things are just unfortunate (e.g. i31ref type which mimics V8's SMI - Dart SMI's are 63-bit on 64-bit platforms).

DBC is an interesting beast, the way it was implemented in Dart VM was not like a normal interpreter (e.g. you interpret and then you tier up into JIT which generates native code), but kinda like a pseudoarchitecture. DBC tiers up into... itself. An optimizing JIT in DBC mode would generate optimized DBC bytecodes, not native code. This all kinda makes sense in the environment for which DBC was created - iOS, where you can't JIT. However in environment where you can generate native code this makes no sense - and even prevents you from reaching peak performance.

KBC is an experiment in this space where you have a more classical interpreter which tiers up into native JIT. In KBC mode bytecodes are also given to VM rather than generated internally inside the VM like DBC does.

Theoretically KBC should have better startup latency than just JIT (because you can start executing code immediately, rather than spending time to generate native code) and peak performance that matches JITs - because it tiers up into JIT.

Dart was actually really opinionated right from the start. Type annotations that has no bearing on execution; reified generics and dynamic typing at the same time; mirrors and noSuchMethod... even having semicolon - as opposed to not having it. These are all strong opinions about language design. They might not align with what is considered "modern" these days, but they are all opinions nevertheless.

Dart VM is written primarily in C++.

There is bytecode, but the story here is kinda complicated - there are actually 2 different versions: one is called DBC and is used during development of Flutter applications on iOS, there is another called KBC - that one is more like a classical bytecode, it is currently in development.

And is there a place where more details are available?

You can get some high level information at https://mrale.ph/dartvm

Let me know if you want to know something special.