PackageTrack
Sign in Get early access

apollovm

Portable multi-language VM: parse, run and translate Dart, Java, Kotlin, Go, C#, JavaScript, TypeScript, Lua and Python — with on-the-fly Wasm compilation and MCP/LSP servers.

2.30.0 4.4K downloads/mo #314 most downloaded on pub.dev ApolloVM/apollovm_dart

What this package is like to depend on

Last release 7 days ago

17 Aug 2026

Ships unpredictably

gaps range from 8 days to 2.1 years

Nearly every release is documented

notes for 157 of 158 stable releases

Nothing withdrawn

no release was ever pulled

5 years old

158 releases · first in 2021

105 releases in the last 12 months

see the full history below

Release timeline

158 releases · Apr 2021 to Aug 2026
2022 2023 2024 2025 2026
Release Pre-release

Releases

latest 60 of 158
  1. 2.30.0 17 Aug 2026
    Release notes

    Dart: multiple variables per declaration

    A declaration could only introduce one variable, so the ordinary Dart form of declaring several at once was a syntax error:

    void main() {
      num nr = 0.96, ng = 0.24, nb = 0.56;  // was: [SyntaxError] ";" expected
    
      print(nr);
      print(ng);
      print(nb);
    }

    Every declarator is now accepted, in a function body and at the top level, sharing the declared type and the final / const / late modifier of the first one. A declarator may omit its initializer (int a, b = 5;) and may read the ones before it (int p = 1, q = p + 1;), matching Dart's left-to-right initialization.

    The declarators are expanded into one declaration each as the code is parsed, so translation emits the form every target already supports:

    num nr = 0.96;
    num ng = 0.24;
    num nb = 0.56;
    nr: float = 0.96
    ng: float = 0.24
    nb: float = 0.56

    A for initializer still takes a single declarator: a for header has no place for the extra declarations the expansion produces, and the comma-separated form does not exist in every target language.

    Full changelog: v2.29.0...v2.30.0

    Open source →
    Release notes

    Dart: multiple variables per declaration

    A declaration could only introduce one variable, so the ordinary Dart form of declaring several at once was a syntax error:

    void main() {
      num nr = 0.96, ng = 0.24, nb = 0.56;  // was: [SyntaxError] ";" expected
    
      print(nr);
      print(ng);
      print(nb);
    }
    

    Every declarator is now accepted, in a function body and at the top level, sharing the declared type and the final / const / late modifier of the first one. A declarator may omit its initializer (int a, b = 5;) and may read the ones before it (int p = 1, q = p + 1;), matching Dart's left-to-right initialization.

    The declarators are expanded into one declaration each as the code is parsed, so translation emits the form every target already supports:

    num nr = 0.96;
    num ng = 0.24;
    num nb = 0.56;
    
    nr: float = 0.96
    ng: float = 0.24
    nb: float = 0.56
    

    A for initializer still takes a single declarator: a for header has no place for the extra declarations the expansion produces, and the comma-separated form does not exist in every target language.

    Open source →
    Release notes

    v2.30.0 - Multiple variables per Dart declaration Latest

    Latest

    Compare

    Choose a tag to compare

    Open source →
  2. 2.29.0 17 Aug 2026
    Release notes

    int and double now expose the same num interface

    Each primitive only carried the conversion that changed its type: int had
    toDouble() and double had toInt(), so the identity half of the pair was
    missing on both. 3.toInt() and 1.5.toDouble() — both valid Dart, since
    num.toInt() / num.toDouble() return this — failed with
    Bad state: Can't find core function: int.toInt(...).

    The same gap covered the rest of the num interface on int, which had none
    of the members that only double had been given:

    on int before now
    toInt() ✓ (identity)
    round(), floor(), ceil(), truncate() ✓ (identities)
    toStringAsFixed(), toStringAsExponential(), toStringAsPrecision()
    toDouble()

    and on double, toDouble() is added as the matching identity.

    This mattered most for a num-typed variable. ApolloVM has no num core
    class, so num n = ... dispatches on the runtime value's class — meaning
    n.toInt() worked or failed depending on whether n happened to hold a
    double or an int. Both now work either way:

    num n = 3;      // was: Can't find core function: int.toInt
    n.toInt();      // 3
    n.toDouble();   // 3.0

    toNum() remains unsupported, as it is not a method on num, int or
    double in Dart.

    Open source →
    Release notes

    int and double now expose the same num interface

    Each primitive only carried the conversion that changed its type: int had toDouble() and double had toInt(), so the identity half of the pair was missing on both. 3.toInt() and 1.5.toDouble() — both valid Dart, since num.toInt() / num.toDouble() return this — failed with Bad state: Can't find core function: int.toInt(...).

    The same gap covered the rest of the num interface on int, which had none of the members that only double had been given:

    on int before now
    toInt() ✓ (identity)
    round(), floor(), ceil(), truncate() ✓ (identities)
    toStringAsFixed(), toStringAsExponential(), toStringAsPrecision()
    toDouble()

    and on double, toDouble() is added as the matching identity.

    This mattered most for a num-typed variable. ApolloVM has no num core class, so num n = ... dispatches on the runtime value's class — meaning n.toInt() worked or failed depending on whether n happened to hold a double or an int. Both now work either way:

    num n = 3;      // was: Can't find core function: int.toInt
    n.toInt();      // 3
    n.toDouble();   // 3.0
    

    toNum() remains unsupported, as it is not a method on num, int or double in Dart.

    Open source →
    Release notes

    v2.29.0 - int and double share the same num interface

    Compare

    Choose a tag to compare

    Open source →
  3. 2.28.1 15 Aug 2026
    Release notes

    Fixed: compile <file> --target=ast silently compiled to Wasm

    An option written after the source file was not parsed at all. The
    source-file commands shared a parser built with allowTrailingOptions: false,
    so apollovm compile foo.dart --target=ast left the flag sitting in the
    leftover positional arguments, --target kept its wasm default, and the
    command compiled to WebAssembly without saying that it had ignored anything.

    It surfaced as an unrelated Wasm codegen crash — UnimplementedError: Wasm maps with value type dynamic are not supported yet — for a program that was
    never meant to reach the Wasm backend. 2.28.0's own compile --help showed
    that exact ordering.

    Trailing options are now parsed for compile and translate, whose only
    positional argument is the source file, so both orderings work. run keeps
    them unparsed, because everything after the file there belongs to the program
    being executed and must reach it untouched — including arguments that look
    like apollovm's own flags.

    A leftover positional argument is now reported rather than ignored:
    apollovm compile foo.dart oops fails with Unexpected argument after the source file: oops.

    compile: target inferred from the output extension, and -t

    --output now selects the target when --target is omitted, so naming the
    file is enough:

    apollovm compile foo.dart -o foo.avma   # binary AST
    apollovm compile foo.dart -o foo.wasm   # WebAssembly

    .avma means the AST target and .wasm means Wasm; any other extension falls
    back to the wasm default as before. An explicit --target always wins, so
    -t ast -o out.bin still writes an image under that name. The extension is
    read from the final path segment, so a . in a parent directory
    (build.v2/out) is not mistaken for it.

    --target also gained the abbreviation -t, on both compile and
    translate.

    Open source →
    Release notes

    Fixed: compile <file> --target=ast silently compiled to Wasm

    An option written after the source file was not parsed at all. The source-file commands shared a parser built with allowTrailingOptions: false, so apollovm compile foo.dart --target=ast left the flag sitting in the leftover positional arguments, --target kept its wasm default, and the command compiled to WebAssembly without saying that it had ignored anything.

    It surfaced as an unrelated Wasm codegen crash — UnimplementedError: Wasm maps with value type dynamic are not supported yet — for a program that was never meant to reach the Wasm backend. 2.28.0's own compile --help showed that exact ordering.

    Trailing options are now parsed for compile and translate, whose only positional argument is the source file, so both orderings work. run keeps them unparsed, because everything after the file there belongs to the program being executed and must reach it untouched — including arguments that look like apollovm's own flags.

    A leftover positional argument is now reported rather than ignored: apollovm compile foo.dart oops fails with Unexpected argument after the source file: oops.

    compile: target inferred from the output extension, and -t

    --output now selects the target when --target is omitted, so naming the file is enough:

    apollovm compile foo.dart -o foo.avma   # binary AST
    apollovm compile foo.dart -o foo.wasm   # WebAssembly
    

    .avma means the AST target and .wasm means Wasm; any other extension falls back to the wasm default as before. An explicit --target always wins, so -t ast -o out.bin still writes an image under that name. The extension is read from the final path segment, so a . in a parent directory (build.v2/out) is not mistaken for it.

    --target also gained the abbreviation -t, on both compile and translate.

    Open source →
    Release notes

    v2.28.1 - Binary AST CLI fixes

    Compare

    Choose a tag to compare

    Open source →
  4. 2.28.0 15 Aug 2026
    Release notes

    Binary AST serialization

    A parsed AST can now be saved as a compact binary image and loaded back without
    running a parser. Parsing dominates the cost of loading code, so an application
    can parse once — at build time, or on first run — and afterwards load the same
    code unit by decoding bytes.

    Such an image is an .avma file: an Apollo Virtual Machine
    Archive. It holds one parsed code unit, or a whole VM's worth of them.

    Measured on a ~4 KB Dart program: decoding is about 11× faster than parsing
    and the image is about two thirds the size of the source. For a very small
    unit the fixed header and pools cost more than the source is worth; the saving
    appears once there is a program to speak of.

    import 'package:apollovm/apollovm_serialization.dart';
    
    var image = vm.saveCodeUnitAST(codeUnit);   // Uint8List
    await ApolloVM().loadCodeUnitAST(image);    // no parser involved

    vm.saveAllAST() / vm.loadAllAST() do the same for a whole VM, bundling every
    loaded code unit into one archive. The CLI gained
    apollovm compile --target=ast, and apollovm run recognizes an image by its
    magic bytes — not its extension — taking the language from the image itself.

    Everything is Uint8List in and Uint8List out, so file access stays with the
    caller and the whole feature works unchanged on the web. The Chrome suite covers
    it, including the places where a JavaScript double behaves unlike a VM int.

    Loading a decoded unit needs no new path: ApolloVM.loadCodeUnit only reaches
    for a parser when a unit has no AST yet, so namespace registration, the
    null-safety check and incremental-resolution invalidation all behave exactly as
    they do for parsed source. Nothing derivable is stored — parent links,
    scope-variable resolution, superclass and extension targets and the this.field
    constructor-parameter promotion are re-established by one resolveNode call
    after decoding, exactly as every grammar does after a parse.

    Nodes holding live Dart state are refused with an error naming where in the
    program they were found: external functions and getters, which carry a closure,
    and runtime values such as a class instance or a pending future. All of them are
    injected by the VM at run time rather than produced by a parser, so a parsed AST
    never contains them and the same bindings are re-injected after a binary load.

    Coverage is enforced by a test that scans lib/src/ast/ and requires every
    concrete AST* class to be registered, pooled as a type, encoded inline by a
    parent, or listed as refused with a written reason — so adding a node kind and
    forgetting its codec fails the build rather than silently dropping a field.

    Binary AST integrity

    Every image carries a CRC-32, verified on load. It detects corruption, not
    tampering:
    anyone who can modify a file can recompute the checksum in
    microseconds. Only a signature made with a key the attacker does not have makes
    an image tamper-evident, and an unsigned image deserves exactly as much trust as
    the source it came from — loading one and running it is equivalent to running
    arbitrary code from that source.

    Signing is optional and pluggable (ASTBinarySigner / ASTBinaryVerifier), so
    an HMAC, a public-key signature or a hardware key store all fit;
    HmacSha256Signer is built in, which is why crypto becomes a direct
    dependency — it was already present transitively. The signature covers
    everything up to and including the CRC, so an attacker who edits a section and
    recomputes the checksum still fails verification.

    Binary AST compatibility

    An image records two version numbers: the container revision that wrote it, and
    the oldest revision that can decode it correctly. Every section is
    length-prefixed, so a reader skips any section it does not recognize, and every
    section is decoded from a bounded view, so fields appended by a newer writer are
    ignored rather than misread. A newer ApolloVM's output therefore keeps loading
    in an older ApolloVM for as long as the new information is purely additive, and
    an older image keeps loading in every future ApolloVM — the reader retains the
    decode path for every format version it has ever supported. When a change
    genuinely cannot be understood by an older reader, the writer raises the minimum
    reader version and that older reader fails immediately with an
    ASTBinaryException naming both versions, rather than silently producing a
    wrong AST. Raising it is a breaking change and will only ever ship in a major
    release, announced here.

    A real image written by format version 1 is committed in the test suite and must
    keep loading; it is the only check that can catch an accidental incompatible
    change, since an image synthesized by the current writer would move with it.

    Requires data_serializer 1.2.3

    The dependency is raised to ^1.2.3, and this is a requirement rather than a
    preference: earlier versions decode LEB128 incorrectly, which silently
    corrupts a binary AST image.

    Building this format surfaced four bugs there, fixed in 1.2.3:

    • BytesBuffer.readLeb128SignedInt sign-extended from the wrong byte on every
      platform, so -2 decoded as 126 and 64 as -16320. The binary AST format
      sidesteps it anyway by writing a form byte plus an unsigned magnitude — which
      also removes any sign-extension difference between the VM and dart2js — but
      the remaining three could not be sidestepped.
    • On the web, shiftLeftInt/shiftRightInt fell back to a 32-bit <</>>,
      so any shift of 32 or more produced 0.
    • On the web, the LEB128 accumulator used |=, also a 32-bit operation, and
      dropped the high bits of any value past 2^32.
    • On the web, signed decoding lost precision above roughly 2^49, because a
      negative's unsigned intermediate is about 2^shift — the range
      DateTime.microsecondsSinceEpoch occupies.

    Together those meant an integer literal of 2^32 or more decoded to the wrong
    number on the web: 4294967296 came back as 0, and 1000000000000000 as
    2764472320. A test now covers literals from 2^28 upwards, positive and
    negative, and it runs under --platform chrome.

    Open source →
    Release notes

    Binary AST serialization

    A parsed AST can now be saved as a compact binary image and loaded back without running a parser. Parsing dominates the cost of loading code, so an application can parse once — at build time, or on first run — and afterwards load the same code unit by decoding bytes.

    Such an image is an .avma file: an Apollo Virtual Machine Archive. It holds one parsed code unit, or a whole VM's worth of them.

    Measured on a ~4 KB Dart program: decoding is about 11× faster than parsing and the image is about two thirds the size of the source. For a very small unit the fixed header and pools cost more than the source is worth; the saving appears once there is a program to speak of.

    import 'package:apollovm/apollovm_serialization.dart';
    
    var image = vm.saveCodeUnitAST(codeUnit);   // Uint8List
    await ApolloVM().loadCodeUnitAST(image);    // no parser involved
    

    vm.saveAllAST() / vm.loadAllAST() do the same for a whole VM, bundling every loaded code unit into one archive. The CLI gained apollovm compile --target=ast, and apollovm run recognizes an image by its magic bytes — not its extension — taking the language from the image itself.

    Everything is Uint8List in and Uint8List out, so file access stays with the caller and the whole feature works unchanged on the web. The Chrome suite covers it, including the places where a JavaScript double behaves unlike a VM int.

    Loading a decoded unit needs no new path: ApolloVM.loadCodeUnit only reaches for a parser when a unit has no AST yet, so namespace registration, the null-safety check and incremental-resolution invalidation all behave exactly as they do for parsed source. Nothing derivable is stored — parent links, scope-variable resolution, superclass and extension targets and the this.field constructor-parameter promotion are re-established by one resolveNode call after decoding, exactly as every grammar does after a parse.

    Nodes holding live Dart state are refused with an error naming where in the program they were found: external functions and getters, which carry a closure, and runtime values such as a class instance or a pending future. All of them are injected by the VM at run time rather than produced by a parser, so a parsed AST never contains them and the same bindings are re-injected after a binary load.

    Coverage is enforced by a test that scans lib/src/ast/ and requires every concrete AST* class to be registered, pooled as a type, encoded inline by a parent, or listed as refused with a written reason — so adding a node kind and forgetting its codec fails the build rather than silently dropping a field.

    Binary AST integrity

    Every image carries a CRC-32, verified on load. It detects corruption, not tampering: anyone who can modify a file can recompute the checksum in microseconds. Only a signature made with a key the attacker does not have makes an image tamper-evident, and an unsigned image deserves exactly as much trust as the source it came from — loading one and running it is equivalent to running arbitrary code from that source.

    Signing is optional and pluggable (ASTBinarySigner / ASTBinaryVerifier), so an HMAC, a public-key signature or a hardware key store all fit; HmacSha256Signer is built in, which is why crypto becomes a direct dependency — it was already present transitively. The signature covers everything up to and including the CRC, so an attacker who edits a section and recomputes the checksum still fails verification.

    Binary AST compatibility

    An image records two version numbers: the container revision that wrote it, and the oldest revision that can decode it correctly. Every section is length-prefixed, so a reader skips any section it does not recognize, and every section is decoded from a bounded view, so fields appended by a newer writer are ignored rather than misread. A newer ApolloVM's output therefore keeps loading in an older ApolloVM for as long as the new information is purely additive, and an older image keeps loading in every future ApolloVM — the reader retains the decode path for every format version it has ever supported. When a change genuinely cannot be understood by an older reader, the writer raises the minimum reader version and that older reader fails immediately with an ASTBinaryException naming both versions, rather than silently producing a wrong AST. Raising it is a breaking change and will only ever ship in a major release, announced here.

    A real image written by format version 1 is committed in the test suite and must keep loading; it is the only check that can catch an accidental incompatible change, since an image synthesized by the current writer would move with it.

    Requires data_serializer 1.2.3

    The dependency is raised to ^1.2.3, and this is a requirement rather than a preference: earlier versions decode LEB128 incorrectly, which silently corrupts a binary AST image.

    Building this format surfaced four bugs there, fixed in 1.2.3:

    • BytesBuffer.readLeb128SignedInt sign-extended from the wrong byte on every platform, so -2 decoded as 126 and 64 as -16320. The binary AST format sidesteps it anyway by writing a form byte plus an unsigned magnitude — which also removes any sign-extension difference between the VM and dart2js — but the remaining three could not be sidestepped.
    • On the web, shiftLeftInt/shiftRightInt fell back to a 32-bit <</>>, so any shift of 32 or more produced 0.
    • On the web, the LEB128 accumulator used |=, also a 32-bit operation, and dropped the high bits of any value past 2^32.
    • On the web, signed decoding lost precision above roughly 2^49, because a negative's unsigned intermediate is about 2^shift — the range DateTime.microsecondsSinceEpoch occupies.

    Together those meant an integer literal of 2^32 or more decoded to the wrong number on the web: 4294967296 came back as 0, and 1000000000000000 as 2764472320. A test now covers literals from 2^28 upwards, positive and negative, and it runs under --platform chrome.

    Open source →
    Release notes

    v2.28.0 - Binary AST serialization

    Compare

    Choose a tag to compare

    Open source →
  5. 2.27.0 14 Aug 2026
    Release notes

    Dart setters

    set name(T value) { … } is implemented. It was the last accessor with no
    support at all — no grammar rule, no AST node, no dispatch. 2.26.0 turned it
    from a silent misparse (into a method named name returning a type named set)
    into a clean parse error; it now works.

    Parsed on classes and extensions, with a typed or untyped parameter and an arrow
    or block body:

    class Box {
      int _v = 0;
      int get value => _v;
      set value(int x) { _v = x; }
      set scaled(v) => _v = v * 10;
    }

    A setter runs on every write position — obj.x = v, this.x = v, and an
    unqualified x = v inside the class, the write-side mirror of an unqualified
    getter read. A real variable in scope still wins, so a setter's own parameter
    cannot re-enter it. Compound assignment (obj.x += 1) reads through the getter
    when there is one and falls back to the backing field; ??= short-circuits, so
    the setter does not run when the current value is non-null. Inherited and
    overridden setters resolve through the superclass chain, and extension setters
    work like extension getters.

    Only Dart generates setters. Every other target refuses with
    UnsupportedSyntaxError, and Wasm refuses rather than letting an assignment
    silently write the backing field instead of running the setter body.

    An arrow-bodied setter regenerates as an arrow: emitting it as a block would
    produce set x(v) { return …; }, which Dart rejects because a setter returns
    void. The generated output is verified with the real dart analyze.

    Fixed: getters silently dropped when translating to Python, Go and Lua

    Those three generators never iterated the class's accessors, so a class with a
    getter translated to any of them lost the accessor entirely while the method
    bodies kept referencing the property — broken code that looked fine, rather than
    the honest UnsupportedSyntaxError Java/C#/JS/TS already produced. They now
    refuse, like the others.

    Fixed: ASTBlock.set dropped setters

    It copied functions, getters and statements, so every class member survived the
    temporary parse block except the new ones.

    Still missing

    For getters and setters alike: static accessors, top-level accessors, and an
    unqualified read of a getter (return value; inside a method — this.value
    works). Kotlin generates getters but not setters.

    Full changelog: https://github.com/ApolloVM/apollovm_dart/blob/v2.27.0/CHANGELOG.md

    Open source →
    Release notes

    Dart setters

    set name(T value) { … } is implemented. It was the last accessor with no support at all — no grammar rule, no AST node, no dispatch. 2.26.0 made it a clean parse error instead of a silent misparse into a method named name returning a type named set; it now works.

    Parsed on classes and extensions, with a typed or untyped parameter and an arrow or block body:

    class Box {
      int _v = 0;
      int get value => _v;
      set value(int x) { _v = x; }
      set scaled(v) => _v = v * 10;
    }
    

    A setter runs on every write position: obj.x = v, this.x = v, and an unqualified x = v inside the class — the write-side mirror of an unqualified getter read. A real variable in scope still wins, so a setter's own parameter cannot re-enter it. Compound assignment (obj.x += 1) reads through the getter when there is one and falls back to the backing field; ??= short-circuits, so the setter does not run when the current value is non-null. Inherited and overridden setters resolve through the superclass chain, and extension setters work like extension getters.

    Only Dart generates setters. Every other target refuses with UnsupportedSyntaxError, and Wasm refuses rather than letting an assignment silently write the backing field instead of running the setter body.

    An arrow-bodied setter regenerates as an arrow: emitting it as a block would produce set x(v) { return …; }, which Dart rejects because a setter returns void. The generated output is checked with the real dart analyze.

    Fixed: getters silently dropped when translating to Python, Go and Lua

    Those three generators never iterated the class's accessors, so a class with a getter translated to any of them lost the accessor entirely while the method bodies kept referencing the property — broken code that looked fine, rather than the honest UnsupportedSyntaxError Java/C#/JS/TS already produced. They now refuse, like the others.

    Fixed: ASTBlock.set dropped setters

    It copied functions, getters and statements, so every class member survived the temporary parse block except the new ones.

    Still missing

    For getters and setters alike: static accessors, top-level accessors, and an unqualified read of a getter (return value; inside a method — this.value works). Kotlin generates getters but not setters.

    Open source →
    Release notes

    v2.27.0 - Dart setters

    Compare

    Choose a tag to compare

    Open source →
  6. 2.26.0 14 Aug 2026
    Release notes

    Control-flow bodies no longer require braces

    for (var e in l) print('- $e'); is ordinary Dart, and it did not parse. Only
    the plain if accepted an unbraced single-statement body — every loop, every
    if/else and every else if demanded { }.

    All seven remaining rules now accept either form, in Dart, Java 11, Kotlin,
    C#, JavaScript and TypeScript
    : for, for-in/for-each, while,
    do/while, if/else, the else if chain, and the final else. A braced
    body is still tried first, so nothing about existing sources changes.

    singleLineStatement was also far too narrow — return …; or an expression
    statement, nothing else — so even the if that already supported it rejected
    if (x) break;, if (x) throw e; and a nested if. It is now each language's
    full statement set minus declarations and bare blocks.

    A dangling else binds to the nearest if, matching every target
    language. Python gains the equivalent construct, the inline suite
    (if x: return 1, def f(): pass, while c: i += 1; j += 1), at every
    suite() position.

    Go is deliberately excluded — its spec defines
    Block = "{" StatementList "}" — and Lua has no such form. A single-statement
    body translated to either is emitted braced / doend.

    Not supported, on purpose: the empty statement as a body (while (c) ;),
    for (;;), and ;-separated statements on an ordinary (non-suite) Python line.

    A latent else-prefix miscompile, fixed first

    BaseGrammarLexer.token() is a prefix matcher with no word-boundary guard, so
    string('else') matches the start of an identifier like elseCount. That was
    unreachable while every else arm followed a mandatory braced block, and would
    have become a silent miscompile the moment bodies could be unbraced:

    if (a) x();
    elseCount = 1;   // `else` matches; `Count = 1;` becomes the else arm

    The branch and loop rules of all six C-style grammars now use whole-word
    keyword tokens. Also covers Kotlin's when entry labels and if expression.

    New Dart syntax

    • Interpolation inside triple-quoted strings'''Hello $name''' yielded
      the literal text $name. Wrong output, not an error.
    • assert(c) / assert(c, m) as a real statement (it parsed as a call to a
      user function named assert). Each target emits its own idiom: Java
      assert c : m;, Python assert c, m, Kotlin assert(c) { m }, C#
      Debug.Assert, Lua's built-in, JS/TS/Go lowered to an explicit check. Wasm
      refuses to compile it rather than mis-compile it.
    • required named parameters on plain, constructor-typed and this.
      forms. ({required int a}) was a hard parse failure.
    • Annotations (@override, @Deprecated('x'), @pragma(...), @a.B(1))
      at every position Dart allows. There was no @ in any grammar in the repo,
      so a single @override broke the whole class body — which matters beyond
      hand-written sources, since lib/src/pub loads real pub packages. Parsed and
      discarded for now.
    • late on locals and fields (accepted and dropped).
    • const at use sites: const Foo(), const [], const {}.
    • Arrow and async bodies on local and anonymous functions.
    • catch (e, s) now binds the stack trace — it was parsed and thrown away,
      so any handler referencing s failed.
    • Compound assignment %= &= |= ^= <<= >>=.
    • Untyped getters (get twice => …) now parse.

    Other fixes

    • Java and C#: if (a) {} else if (b) {} with no trailing else failed to
      parse — both made the final else non-optional, unlike every other language.
    • Python: a do/while translated to Python emitted literal
      do { … } while (c);. Now lowers to while True: … if not (c): break.
    • Lua: compound assignment was emitted verbatim (a += 1), which Lua does
      not have. Now lowers to a = a + 1.
    • const [1] silently misparsed as an index read on a variable named const.
    • set value(int v) {} silently became a method named value returning a
      type named set. Now a clean parse error at the set. Full setter support
      remains unimplemented.

    Full changelog: https://github.com/ApolloVM/apollovm_dart/blob/v2.26.0/CHANGELOG.md

    Open source →
    Release notes

    Control-flow bodies no longer require braces

    for (var e in l) print('- $e'); is ordinary Dart, and it did not parse. Only the plain if accepted an unbraced single-statement body — every loop, every if/else and every else if demanded { }.

    All seven remaining rules now accept either form, in Dart, Java 11, Kotlin, C#, JavaScript and TypeScript: for, for-in/for-each, while, do/while, if/else, the else if chain, and the final else. A braced body is still tried first, so nothing about existing sources changes.

    singleLineStatement was also far too narrow — return …; or an expression statement, nothing else — so even the if that already supported it rejected if (x) break;, if (x) throw e; and a nested if. It is now the language's full statement set minus declarations and bare blocks.

    A dangling else binds to the nearest if, matching every target language. Python gets the equivalent construct, the inline suite (if x: return 1, def f(): pass, while c: i += 1; j += 1), for every suite() position: if/elif/else, for, while, try, def, class and case.

    Go is deliberately excluded — its spec defines Block = "{" StatementList "}" and every control-flow statement takes a Block — and Lua has no such form. A single-statement body translated to either is emitted braced / doend.

    Not supported, on purpose: the empty statement as a body (while (c) ;), for (;;), and ;-separated statements on an ordinary (non-suite) Python line.

    Fixed: a else-prefix miscompile, latent until now

    BaseGrammarLexer.token() is a prefix matcher with no word-boundary guard, so string('else') matches the start of an identifier such as elseCount. That was unreachable while every else arm was preceded by a mandatory braced block, and would have become a silent miscompile the moment bodies could be unbraced:

    if (a) x();
    elseCount = 1;   // `else` matches; `Count = 1;` becomes the else arm
    

    The branch and loop rules of all six C-style grammars now use whole-word keyword tokens. Also covers Kotlin's when entry labels and if expression.

    Other grammar fixes

    • Java and C#: if (a) {} else if (b) {} with no trailing else failed to parse — both made the final else non-optional, unlike every other language.
    • Python: a do/while translated to Python emitted literal do { … } while (c);. It now lowers to while True: … if not (c): break.
    • Lua: compound assignment was emitted verbatim (a += 1), which is not valid Lua. It now lowers to a = a + 1.
    • Go: removed a dead codeBlockOrSingleLineBlock cluster that was defined but never referenced.

    New Dart syntax

    • Interpolation inside triple-quoted strings. '''Hello $name''' yielded the literal text $name — wrong output, not an error. Raw r'''…''' is unaffected.
    • assert(c) / assert(c, m) as a real statement. It previously parsed as a call to a user function named assert and failed later with a confusing message. A failed assertion throws and is catchable. Every target emits its own idiom (Java assert c : m;, Python assert c, m, Kotlin assert(c) { m }, C# Debug.Assert, Lua's built-in, JS/TS/Go lowered to an explicit check). Wasm refuses to compile it rather than mis-compile it.
    • required named parameters on plain, constructor-typed and this. forms. ({required int a}) was a hard parse failure. Dart output keeps the modifier; other targets express required-ness by the absence of a default.
    • Annotations@override, @Deprecated('x'), @pragma(...), @a.B(1) — at every position Dart allows. There was no @ in any grammar in the repo, so one @override broke the whole class body; this matters beyond hand-written sources, since lib/src/pub loads real pub packages. Parsed and discarded for now.
    • late on locals and fields (accepted and dropped).
    • const at use sites: const Foo(), const [], const {}. const [1] used to silently misparse as an index read on a variable named const.
    • Arrow and async bodies on local and anonymous functions: int f(int x) => x * 2; inside a function, and (x) async => ….
    • catch (e, s) now binds the stack trace. It was parsed and thrown away, so any handler that referenced s failed. ApolloVM has no stack traces, so it binds to an empty string; targets whose catch header has no second variable declare it as the handler's first statement.
    • Compound assignment %= &= |= ^= <<= >>=. In JS/TS %= was already in the grammar but missing from the shared operator enum, so it surfaced as a SyntaxError.
    • Untyped getters (get twice => …) now parse. They failed because type().optional() greedily ate get and petitparser's optional() cannot backtrack.

    Fixed: set no longer misparses into a method

    simpleType() accepted any identifier, so set value(int v) {} silently became a method named value returning a type named set, failing much later with a confusing error. get/set are now rejected in a type position, turning that into a parse error at the set.

    Full setter support remains unimplemented: it would mirror the entire getter subsystem plus assignment dispatch, and getters are generated for only 2 of 9 targets. This change converts a silent misparse into a clear error.

    Open source →
    Release notes

    v2.26.0 - Control-flow bodies no longer require braces

    Compare

    Choose a tag to compare

    Open source →
  7. 2.25.1 02 Aug 2026
    Release notes

    apollovm_wasm 1.2.0: the runtime now tracks the core it decodes

    No change to this package's code — this release exists to carry the apollovm_wasm bump, the way 2.23.2 carried apollovm_wasm 1.1.0.

    apollovm_wasm declared apollovm: ^2.0.0, but it is not a loosely-coupled consumer: it decodes what this package's Wasm generator encodes. The boxed-Object cell layout and its _boxTag* values are a contract, and both wasm_runner.dart and wasm_generator.dart carry a comment saying the constants must match.

    That constraint let pub pair the runtime with any 2.x, including releases whose box encoding it was never built against — a mismatch that shows up as a wrong value or a trap at run time, not as a resolution failure. It is now apollovm: ^2.25.0, widened deliberately rather than by default. Its wasm_run also moves to ^0.2.0+2 (patch, no API change).

    Open source →
  8. 2.25.0 01 Aug 2026
    Release notes

    Wasm: ?. on a boxed slot no longer refuses to compile

    var x = null; x?.length was an UnimplementedError"Wasm getter .length
    on Null is not supported yet"
    . That was reachable from ordinary code, and the
    advice the neighbouring error gives ("declare the variable as var / Object?
    / dynamic") led straight into it.

    A boxed slot is the only Wasm representation that can hold null, so it is also
    the only place ?. has anything to do — on a concrete slot the receiver cannot
    be null and the null-awareness is vacuous. .length / .isEmpty / .isNotEmpty
    on a boxed receiver now dispatch on the box tag at runtime:

    var missing = null;
    var n = missing?.length;   // -> null
    return n ?? -1;            // -> -1

    Only a boxed String carries these members — List/Map have no boxed form
    at all, and the int/double/bool/instance tags have no length — so any other tag
    traps rather than reading a length word out of a payload that is not a string
    pointer. A plain . on a null box traps too, matching the interpreter's
    ApolloVMNullPointerException.

    The null-aware result is itself boxed, because it can be null and a nullable
    value has no unboxed encoding. That is what lets it flow into ?? and
    == null, which already understand boxes. The plain form still yields an
    unboxed int/bool.

    Known gap this exposed

    An explicitly-declared Object? local initialized from a concrete value keeps
    the initializer's type instead of being boxed, so Object? s = '' makes s a
    String slot. s?.isEmpty then takes the concrete-String path and stores its i32
    boolean into a slot sized i64, producing a module that compiles but fails Wasm
    validation. That reproduces byte-identically on 2.24.0, so it is a separate
    defect in the declaration path rather than a consequence of this change; it is
    pinned as a skipped test in test/wasm/apollovm_wasm_boxed_member_test.dart.

    Full Changelog: v2.24.0...v2.25.0

    Open source →
    Release notes

    Wasm: ?. on a boxed slot no longer refuses to compile

    var x = null; x?.length was an UnimplementedError"Wasm getter .length on Null is not supported yet". That was reachable from ordinary code, and the advice the neighbouring error gives ("declare the variable as var / Object? / dynamic") led straight into it.

    A boxed slot is the only Wasm representation that can hold null, so it is also the only place ?. has anything to do — on a concrete slot the receiver cannot be null and the null-awareness is vacuous. .length / .isEmpty / .isNotEmpty on a boxed receiver now dispatch on the box tag at runtime:

    var missing = null;
    var n = missing?.length;   // -> null
    return n ?? -1;            // -> -1
    

    Only a boxed String carries these members — List/Map have no boxed form at all, and the int/double/bool/instance tags have no length — so any other tag traps rather than reading a length word out of a payload that is not a string pointer. A plain . on a null box traps too, matching the interpreter's ApolloVMNullPointerException.

    The null-aware result is itself boxed, because it can be null and a nullable value has no unboxed encoding. That is what lets it flow into ?? and == null, which already understand boxes. The plain form still yields an unboxed int/bool.

    Known gap this exposed

    An explicitly-declared Object? local initialized from a concrete value keeps the initializer's type instead of being boxed, so Object? s = '' makes s a String slot. s?.isEmpty then takes the concrete-String path and stores its i32 boolean into a slot sized i64, producing a module that compiles but fails Wasm validation. That reproduces byte-identically on 2.24.0, so it is a separate defect in the declaration path rather than a consequence of this change; it is pinned as a skipped test in test/wasm/apollovm_wasm_boxed_member_test.dart.

    Open source →
    Release notes

    v2.25.0 - Wasm: ?. on a boxed slot no longer refuses to compile

    Compare

    Choose a tag to compare

    Open source →
  9. 2.24.0 01 Aug 2026
    Release notes

    Null-aware access is now really checked on every target, not dropped

    a?.b was emitted as a plain a.b on Java, C#, JavaScript, Lua and Python: the
    generated code compiled, but the null check was gone — it threw on exactly the
    input ?. exists to handle. The README's null-safety matrix marked those cells
    ⚠️ lossy.

    Java, Lua and Python now lower the access to an explicit guard, and the guard
    wraps the whole chain rather than the null-aware link alone:

    // Dart source
    int chain(A? a) { return a?.m().toInt(); }
    // Java, before: threw when `a` was null
    return a.m().toInt();
    // Java, now
    return (a != null ? a.m().toInt() : null);
    # Python
    return (a.m().toInt() if a is not None else None)

    C# and JavaScript were lossy for a simpler reason — both have had the operator
    all along (C# 6, ES2020) and merely never declared it — so they now emit a?.b
    natively. JavaScript uses ?.[i] for element access, and its postfix ! is
    handled separately: JavaScript has no null-assertion operator (a postfix ! is
    logical NOT), so a! is emitted as plain a rather than negating the value.

    Go continues to report ?. / ?[ as unsupported: it represents a nullable T?
    as *T, so a degraded access would both skip the nil check and yield the wrong
    type.

    ??, &&, || and x == null are AST nodes of their own

    These four were shapes encoded on the generic binary-operation node and
    special-cased ahead of its operator switch, because each of them short-circuits.
    Every consumer had to re-detect them by inspecting operands — the null-safety
    analyzer reconstructed x != null to promote a variable, the Go backend probed
    for a null literal to compare the pointer instead of dereferencing it, and the
    Wasm backend re-tested the operator — and the enum's exhaustiveness forced
    throw StateError('unreachable') arms in three places.

    ASTExpressionNullCoalesce, ASTExpressionLogicalAnd, ASTExpressionLogicalOr
    and ASTExpressionNullCheck now carry those shapes, built by the new
    astExpressionOperation() factory that the shared grammar reduction uses, so
    all nine front-ends get them. Consumers dispatch on the node type instead of
    pattern-matching, and evaluating x == null no longer concretizes both operands
    and runs them through equality dispatch.

    This is internal structure — the generated source is unchanged, except where a
    target has a better idiom that the dedicated node makes reachable. Python is the
    one such case:

    # before
    if a == None:
    # now
    if a is None:

    == dispatches through __eq__, which a class can redefine to return True
    for None; is cannot be intercepted, and is the form PEP 8 mandates.

    The Python grammar learned is None / is not None to match, so the
    generated source still round-trips — ApolloVM can now read back what it writes.
    This is deliberately limited to the None comparison: general a is b is
    identity, and mapping it to == would silently turn it into equality, so it
    stays unparsed as before.

    Building a binary operation directly with ASTExpressionOperation still works
    for the ordinary operators. Constructing one with ??, && or || now throws
    rather than silently evaluating both operands, which would no longer be a
    short-circuit.

    Full Changelog: v2.23.3...v2.24.0

    Open source →
    Release notes

    Null-aware access is now really checked on every target, not dropped

    a?.b was emitted as a plain a.b on Java, C#, JavaScript, Lua and Python: the generated code compiled, but the null check was gone — it threw on exactly the input ?. exists to handle. The README's null-safety matrix marked those cells ⚠️ lossy.

    Java, Lua and Python now lower the access to an explicit guard, and the guard wraps the whole chain rather than the null-aware link alone:

    // Dart source
    int chain(A? a) { return a?.m().toInt(); }
    
    // Java, before: threw when `a` was null
    return a.m().toInt();
    // Java, now
    return (a != null ? a.m().toInt() : null);
    
    # Python
    return (a.m().toInt() if a is not None else None)
    

    C# and JavaScript were lossy for a simpler reason — both have had the operator all along (C# 6, ES2020) and merely never declared it — so they now emit a?.b natively. JavaScript uses ?.[i] for element access, and its postfix ! is handled separately: JavaScript has no null-assertion operator (a postfix ! is logical NOT), so a! is emitted as plain a rather than negating the value.

    Go continues to report ?. / ?[ as unsupported: it represents a nullable T? as *T, so a degraded access would both skip the nil check and yield the wrong type.

    ??, &&, || and x == null are AST nodes of their own

    These four were shapes encoded on the generic binary-operation node and special-cased ahead of its operator switch, because each of them short-circuits. Every consumer had to re-detect them by inspecting operands — the null-safety analyzer reconstructed x != null to promote a variable, the Go backend probed for a null literal to compare the pointer instead of dereferencing it, and the Wasm backend re-tested the operator — and the enum's exhaustiveness forced throw StateError('unreachable') arms in three places.

    ASTExpressionNullCoalesce, ASTExpressionLogicalAnd, ASTExpressionLogicalOr and ASTExpressionNullCheck now carry those shapes, built by the new astExpressionOperation() factory that the shared grammar reduction uses, so all nine front-ends get them. Consumers dispatch on the node type instead of pattern-matching, and evaluating x == null no longer concretizes both operands and runs them through equality dispatch.

    This is internal structure — the generated source is unchanged, except where a target has a better idiom that the dedicated node makes reachable. Python is the one such case:

    # before
    if a == None:
    # now
    if a is None:
    

    == dispatches through __eq__, which a class can redefine to return True for None; is cannot be intercepted, and is the form PEP 8 mandates.

    The Python grammar learned is None / is not None to match, so the generated source still round-trips — ApolloVM can now read back what it writes. This is deliberately limited to the None comparison: general a is b is identity, and mapping it to == would silently turn it into equality, so it stays unparsed as before.

    Building a binary operation directly with ASTExpressionOperation still works for the ordinary operators. Constructing one with ??, && or || now throws rather than silently evaluating both operands, which would no longer be a short-circuit.

    Open source →
    Release notes

    v2.24.0 - Null-aware access is now really checked on every target, not dropped

    Compare

    Choose a tag to compare

    Open source →
  10. 2.23.3 27 Jul 2026
    Release notes

    A signature mismatch is no longer reported as a missing entry function

    apollovm.execute returned the same diagnostic whether the entry name was absent or existed but rejected the passed arguments, so a caller that guessed the parameters was told to fix the name:

    $ apollovm mcp call apollovm.execute --language dart --args '[1,2,3]' \
        --source 'int main(int a, String b){ return 1; }'
    {
      "diagnostics": [
        { "severity": "error", "message": "Entry function not found: main" }
      ],
      "isError": true
    }
    

    ApolloRuntime.execute now separates the two causes. When the name exists but no overload matches, the diagnostic shows the call that was attempted — with the passed argument types — and every declared signature of that name:

    No entry function matching the passed arguments: `main(int, int, int)`.
    A function named `main` exists, but with a different signature:
    `int main(int a, String b)`. Adjust the arguments to match a declared signature.
    

    Several declarations are all listed, so an overload set or a name shared across classes shows the full menu:

    No entry function matching the passed arguments: `run(double, double, double)`.
    2 functions named `run` exist, but with different signatures:
    `int A.run(int a)`, `int B.run(bool x, bool y)`.
    

    Signatures are qualified with their class, including for a method reached through the auto-discovery order (no className given) — the case where the caller cannot otherwise tell which class answered.

    An explicit className that does not exist is now reported as a missing class instead of blaming the method: Entry class not found: Bar (looking for the method `run`). A name that is genuinely absent from the source still reports Entry function not found, unchanged.

    The extra lookup is read-only and runs only on the failure path, so successful execution is untouched.

    Full Changelog: v2.23.2...v2.23.3

    Open source →
    Release notes

    A signature mismatch is no longer reported as a missing entry function

    Calling apollovm.execute with arguments that no declaration of the entry name accepts reported the same thing as a typo in the name — Entry function not found: main — leaving the caller with nothing to correct:

    $ apollovm mcp call apollovm.execute --language dart --args '[1,2,3]' \
        --source 'int main(int a, String b){ return 1; }'
    {
      "diagnostics": [
        { "severity": "error", "message": "Entry function not found: main" }
      ],
      "isError": true
    }
    

    ApolloRuntime.execute now separates the two causes. When the name exists but no overload matches, the diagnostic shows the call that was attempted and every declared signature of that name (qualified with the class when it is a method, including one reached by the auto-discovery order):

    No entry function matching the passed arguments: `main(int, int, int)`.
    A function named `main` exists, but with a different signature:
    `int main(int a, String b)`. Adjust the arguments to match a declared signature.
    

    An explicit className that does not exist now reports Entry class not found: Bar (looking for the method run) instead of blaming the method. A name that is genuinely absent from the source still reports Entry function not found.

    Open source →
    Release notes

    v2.23.3 - Signature mismatch is no longer reported as a missing entry function

    Compare

    Choose a tag to compare

    Open source →
  11. 2.23.2 26 Jul 2026
    Release notes

    apollovm_wasm 1.1.0 — wasm_run ^0.2.0+1

    wasm_run 0.2.0 is a breaking release. It is absorbed inside apollovm_wasm, so consumers keep the same WasmRuntimeIO API and gain one thing: there is no install step any more.

    dart run wasm_run:setup is gone. The SDK's build hooks download the native library into .dart_tool/lib/ during dart run / dart test / dart compile, so that directory is now searched first — walking up to every enclosing package root, so a nested package finds a library fetched by its workspace — along with .dart_tool/wasm_run/ (the wasm_run:build_binaries output).

    Three further breaks needed handling:

    • The bindings moved to flutter_rust_bridge 2.x, so the symbol that identifies a genuine wasm_run library changed: wire_compile_wasmfrb_get_rust_content_hash. Validating the old one rejected every 0.2 library.
    • WasmRunLibrary.isReachable() became Future<bool>.
    • wasm_run 0.2 initializes its Rust bindings asynchronously, and no longer locates its own library in a pure-Dart application. WasmRunLibrary.setUp() is now awaited once, lazily, on the first module compile — ensureBooted() and isSupported stay synchronous, as WasmRuntime requires, and only probe for the library.

    WASM_RUN_DART_DYNAMIC_LIBRARY (the variable wasm_run itself reads) now overrides the library path. The older WASM_RUN_LIB_PATH is still honored, and is finally read as a path: it used to be consulted only on platforms with no known library name, and then joined with candidate directories as if it were a file name.

    apollovm_wasm now requires Dart >= 3.10, matching wasm_run 0.2.

    apollovm 2.23.2

    Nothing changes for package:apollovm itself — it still compiles Wasm everywhere and pulls in no native toolchain. This release only corrects the places that told people to run a command that no longer exists: mcp doctor, the Wasm test suite and WASM_BACKEND_PLAN.md. CI drops its three wasm_run:setup steps.

    Known issue: macOS on Apple Silicon

    The upstream aarch64-apple-darwin 0.2.0 binary is killed by macOS (SIGKILL, Code Signature Invalid) the moment wasmtime executes JIT-compiled Wasm inside the JIT Dart VM — i.e. under dart run and dart test. Compiling and instantiating modules is fine; only the call into generated code trips it, and wasm_run 0.1.0+2 does not hit it on the same machine.

    Ahead-of-time compilation is unaffected, so on macOS arm64 run Wasm through dart test --compiler exe / dart compile exe. It is a macOS code-signing enforcement, so other platforms are not expected to be affected — Linux is verified in CI (2541 tests, JIT and AOT).

    Full Changelog: v2.23.1...v2.23.2

    Open source →
    Release notes

    The native Wasm runtime no longer has an install step

    apollovm_wasm 1.1.0 moves to wasm_run 0.2, which dropped dart run wasm_run:setup in favor of the SDK's build hooks: the native library is downloaded automatically by dart run, dart test and dart compile. mcp doctor and the Wasm test suite said otherwise, so they now just point at package:apollovm_wasm.

    Nothing changes for package:apollovm itself — it still compiles Wasm everywhere and pulls in no native toolchain. See the apollovm_wasm changelog for the details, including a macOS/Apple-Silicon issue in the upstream 0.2.0 binary.

    Open source →
    Release notes

    v2.23.2 - apollovm_wasm 1.1.0: wasm_run 0.2, no install step

    Compare

    Choose a tag to compare

    Open source →
  12. 2.23.1 25 Jul 2026
    Release notes

    A blank className/function no longer breaks execution

    A client that fills in every field rather than omitting the optional ones sends "" — and "" was taken as a real name to look up, so nothing matched:

    $ apollovm mcp call apollovm.execute --language dart --class-name "" \
        --source 'int main(List a){ return 7; }'
    {
      "result": null,
      ...
      "diagnostics": [
        { "severity": "error", "message": "Entry function not found: .main" }
      ],
      "isError": true
    }
    

    ApolloRuntime.execute now normalizes both entry names: they are trimmed, and a name that is empty after trimming means "not specified" — className falls back to the full discovery order (top-level function, then any class method) and function falls back to main. Trimming also makes " main " resolve, which previously did not. A name that is genuinely absent from the source still reports Entry function not found, now with the trimmed name in the message.

    Class lookup is guarded at the source as well, so an empty name can never match an entry regardless of the caller: LanguageNamespaces.getClass, CodeNamespace.getClass/containsClass, ASTRoot.getClass and ApolloRunner.getClassMethod return early for a blank class name.

    Dart tooling ignores the LSP fixtures

    lsp/example_workspace/broken.dart is intentionally unparseable (it is what makes the language server emit a parse diagnostic), which made dart format . fail with exit 65 and made the analyzer report it whenever it was opened in an editor. The root analyzer: exclude: did not cover either case, and the formatter has no exclude option at all.

    The fixture is now lsp/example_workspace/.broken.dart: both dart format and dart analyze skip dot-prefixed paths during a directory walk, while the editor still sees a .dart file and hands it to the ApolloVM language server, so the demo is unchanged. A local analysis_options.yaml silences the remaining fixture diagnostics. No published code is affected — lsp/ is .pubignored.

    Open source →
  13. 2.23.0 25 Jul 2026
    Release notes

    nullSafetyChecks is reachable from the CLI and MCP

    2.22.0 added ApolloVM(nullSafetyChecks: true), but the only way to turn it on was to construct the VM in Dart. It is now exposed at every entry point that actually loads code.

    CLI--null-safety on run, translate and compile (added once on the shared CommandSourceFileBase, like --pub):

    $ apollovm run --null-safety foo.dart
    ** NULL SAFETY: Can't load `foo.dart` (dart): 1 null-safety error(s).
       - The operand 'b' can be 'null', so it can't be used in an operation
         unconditionally. Use '??', '!' or a null check.
    $ echo $?
    1
    

    The rejection is printed as a report rather than escaping as an unhandled error with a stack trace, and it is not restated as a parse failure. main now maps a command result of false to exit status 1, so a check can gate a build; no existing command path returns false.

    MCP — every source tool takes an optional nullSafety argument, and --null-safety on both apollovm mcp serve and apollovm mcp call sets the default (a per-call value always wins). The two tool kinds behave differently, by design:

    • the tools that loadapollovm.execute, apollovm.translate, apollovm.wasm — reject the source, returning the findings as diagnostics rather than throwing;
    • the parse-based tools — apollovm.parse, apollovm.ast, apollovm.symbols, apollovm.types — never load anything, so a "fail the load" flag would be meaningless. They add the findings to diagnostics and still succeed, which is the useful form for inspection.

    The server default is merged into the tool arguments at the single dispatch point shared by the in-process and isolate paths. That matters: _IsolateJob carries only the arguments and limits, so a field on the server object would never reach a spawned isolate — and apollovm.execute runs in one by default. It is deliberately not stored in McpLimits, which is documented as resource and security limits.

    Adds mcpCoerceBool beside mcpCoerceInt, so a client sending "true" or 1 is handled rather than crashing the tool on a hard cast — the same class of bug 2.16.0 fixed for the integer arguments.

    Not changed

    The LSP was checked and deliberately left alone: its Analyzer uses its VM only for getParser and never loads a code unit, and it already reports null-safety findings as diagnostics. An editor must report, not refuse to open a file.

    Open source →
  14. 2.22.0 25 Jul 2026
    Release notes

    The null-safety analyzer had never seen a class method

    NullSafetyAnalyzer.analyze walked root.descendantChildren looking for invocables. But ASTBlock.children is only [functions, statements], and ASTRoot keeps its classes in a separate map — so the traversal never entered a class body. ASTClass compounds it by keeping constructors and getters outside children too.

    The effect: every method, constructor and getter of every class went unanalyzed, including in the LSP/Problems panel. Only top-level functions were ever checked, which is also why the existing tests — all written against top-level functions — never caught it.

    class Foo {
      static void main(int a, int? b) {
        var c = a + b;   // reported no diagnostic at all
      }
    }
    

    analyze now walks each class and extension explicitly, plus their constructors and getters. The reported case produces unchecked-nullable-operand with no rule changes — the rules were fine, nothing was reaching them.

    Opt-in: fail the load instead of failing mid-run

    ApolloVM(nullSafetyChecks: true) makes loadCodeUnit throw the new NullSafetyError when the AST has null-safety errors, before the unit is registered:

    var vm = ApolloVM(nullSafetyChecks: true);
    await vm.loadCodeUnit(unit); // throws — nothing printed, nothing executed
    

    Without it, the snippet above prints 5 and null and then throws ApolloVMNullPointerException from the +, having already produced output.

    • Off by default, so existing behaviour is unchanged unless you opt in.
    • Only NullSafetySeverity.error blocks; warnings (e.g. null!) and info stay diagnostic-only, exactly as in the LSP.
    • A BinaryCodeUnit (Wasm) carries no AST and is skipped.
    • The analysis is best-effort: an internal analyzer failure never becomes a load failure.
    • The thrown NullSafetyError carries the offending findings.

    No language gate is needed — only the Dart grammar parses ?, so every other language's types are non-nullable and produce no findings. There is a test asserting that rather than assuming it.

    Open source →
  15. 2.21.0 25 Jul 2026
    Release notes

    Two invalid-output fixes found while documenting null safety

    Writing the README's null-safety matrix meant generating every construct into every target and reading the result. Two cells were emitting source that is not valid in the target language:

    • Kotlin's null-aware index was xs?[0]. Kotlin has no ?[ operator — its null-aware element access is the call xs?.get(0). The shared nullAwareIndexOpen hook gained a matching nullAwareIndexClose, so a target can close with ) instead of ].
    • Lua's null literal was null. Lua's is nil, so the generated code referenced an undefined global: a == null was always false rather than a nil test. Both null-rendering hooks are now overridden for Lua.

    README: the null-safety surface is documented

    The feature tables covered control flow, operators and OOP, but nothing of the null-safety work from 2.16.0 onwards. A new Null safety section carries a per-language table for nullable types, ?? / ??=, ?. / ?[, !, cascades, == null and the static analyzer — each cell verified by generating the construct and reading the output, not from memory.

    It adds a ⚠️ lossy marker for targets that emit a form which compiles but drops the null check (a?.xa.x), distinguishing those from a faithful idiom (🧩) such as Java's ternary for ?? or Go's pointer dereference for !. A Member-access chains section covers the 2.17.0 chain support, the null row in the operators table now points at the Wasm limits, and the Wasm status paragraph mentions the boxed-null domain.

    Open source →
  16. 2.20.0 25 Jul 2026
    Release notes

    Go: a nullable T? is generated as a pointer *T

    Go had no representation for nullability at all. A nullable local was emitted as var s string = nil — source that does not compile — and ?? was reported as unsupported because a plain Go value type cannot be compared to nil. 2.19.0 turned the broken output into an explicit error; this release implements the representation.

    A nullable T? now becomes a Go pointer *T, which is the one Go form that can hold nil:

    • Declarations and parameters carry the pointer type — int? a is a *int, a String? name field is name *string.
    • Reads deref ((*a)), including a bare return x, which takes its own generation path.
    • Null checks compare the pointerx == null is x == nil, not a deref.
    • A non-null value takes its address through a generated func goPtr[T any](v T) *T helper, since Go cannot write &5. The helper is emitted only in modules that need it.
    • a ?? b lowers to an inline function that nil-checks the pointer: func() int { if a != nil { return *a }; return b }(). Go has no conditional expression, so this is the only correct form.
    • Types already nilable in Go are left alone — a List<int>? stays []int, not *[]int, and likewise for maps and interfaces.

    Every shape above is verified by compiling the generated source with a real Go toolchain (go build), not by asserting on text — which is what let var s string = nil ship in the first place.

    Two constructs remain unsupported in Go, and both now report rather than emit something wrong:

    • ?. — the shared fallback degrades it to ., which was merely lossy when a nullable was a plain T. Against a *T it would skip the nil check and yield a value where a pointer is expected. Lowering it needs the accessed member's type at generation time, which the generator does not resolve yet.
    • ??= — lowering it to t = t ?? v needs the target's element type to build the inline function's return type, and the text-level assignment hook does not have it. Plain ?? is unaffected.
    Open source →
  17. 2.19.0 25 Jul 2026
    Release notes

    x == null no longer throws

    x == null — the most common null check in Dart — threw for any typed left operand:

    int? a = 1;  if (a == null) { … }
    // _TypeError: type 'Null' is not a subtype of type 'FutureOr<int>' in type cast
    

    No ternary, nullable slot or ?. was needed; String s = 'x'; s == null failed the same way. Only the reversed null == x worked, because ASTValueNull.equals type-tests instead of casting.

    ASTValue.equals read the other operand through _getValue, which casts it to this value's T. That is right for arithmetic — a mismatched operand there is a real error — but wrong for equality, where a different type must compare false. Equality now reads both operands uncast, so x == null is false, x == 'other type' is false, and neither throws. The three equals overrides (ASTValueStatic, ASTValuePrimitive, ASTValueNum) had the same cast and were fixed with it.

    This predates the null-safety work — it is in the base ASTValue — but 2.16.0 made x == null the idiom people reach for, so it went from obscure to prominent.

    Wasm: == null / != null against the null box

    With null representable since 2.18.0, the equality paths had to learn about it. A comparison against a null literal is now recognised before the String and numeric paths:

    • a boxed operand compares its pointer against the null box, so a[0] == null on a List<Object> answers correctly (it previously took the __streq route and compared contents against address 0, reporting a non-null String as null);
    • a concrete operand (int, double, String, an instance) can never be the null box, so the result is a constant — and, importantly, a valid module. String s; s == null previously pushed two i32 handles into an i64.eq and produced a module that failed to validate.

    Grammar: (expr).m().field

    A group invocation followed by member access did not parse — the group-invocation rule chains only further invocations, so a trailing .field had nothing to match it. A new rule reuses that rule for the head and folds the trailing segments. It requires at least one trailing segment, so (expr).m() keeps its own rule and parenthesized arithmetic is untouched (reordering the rules instead changes how the operation chain groups and breaks round-trip generation).

    Go: no more var s string = nil

    The Go generator emitted var s string = nil for a nullable local — source that does not compile. It now reports an UnsupportedSyntaxError naming the type, consistent with how ?? is already handled. A List/Map still accepts nil, since those become a Go slice/map, which are nilable.

    Go's nullable representation remains the open item: supporting T? properly means generating *T throughout — declarations, zero values, every dereference, and parameter/return types.

    Open source →
  18. 2.18.0 24 Jul 2026
    Release notes

    Wasm: null in the boxed-Object domain

    Compiling a null literal to Wasm threw a bare UnimplementedError: generateASTExpressionNullValue — a leftover TODO — so an ordinary Dart idiom such as var a = args.length > 0 ? args[0] : null; could not be compiled at all.

    null is now a real value in the backend's boxed domain: the boxed-Object pointer 0. The heap never allocates at address 0, so it needs no cell, costs no allocation, and is distinguishable from every real box.

    • Ternary arms unify. Both arms of a conditional are now coerced to the block's result type, and an arm that is null forces that type to a boxed Object — otherwise c ? 1 : null mixed an i64 with an i32 and produced a module that failed to validate.
    • ?? tests a boxed operand. It previously always yielded its left operand ("a Wasm value is never null"), which remains correct in the numeric domain. A boxed operand is now compared against the null box, so a ?? 99 falls back when a is null.
    • String interpolation prints null. The box-to-string helper checks the null box before dereferencing a tag.
    • __alloc look-ahead. Boxing allocates, but the alloc export is decided before the Code section while the boxing is only discovered while writing it — so [1, null, 3] and a ?? 99 aborted at run time with No exported Wasm function __alloc. The generator now scans function bodies for a null literal up front.
    • An Object? return decodes to null instead of the raw pointer 0. Two causes: the return path had no Object case, and _typeTag's fallback gave tag 5 to both Object and a class instance, so the runner could not tell a box from a bare instance pointer. A class instance now carries tag 8, leaving 5 unambiguously "boxed value".

    Where null genuinely has no representation — a slot whose Wasm type is concrete, such as int (i64) or String (a string pointer) — the compiler now reports an UnsupportedSyntaxError naming the type and suggesting var / Object? / dynamic, rather than emitting a module that fails to validate.

    Null-safety analyzer: operands and nullable-to-non-nullable assignment

    The analyzer checked unconditional member/method/index access on a nullable, and the null literal assigned to a non-nullable slot. Two adjacent mistakes went unreported:

    • A nullable operand in an operation (x + (y ?? 0) where x is int?) now reports unchecked-nullable-operand. ??, == and != are exempt — a nullable operand is exactly what they exist to handle — and ! or a preceding null check still suppress it.
    • A nullable value assigned to a non-nullable slot (int x = a; where a is int?) now reports nullable-to-non-nullable. Previously only a literal null was caught, so the same error one step removed passed silently.
    Open source →
  19. 2.17.0 24 Jul 2026
    Release notes

    Null-safety fixes: nullable parameters, null-aware typing, access chains

    Four gaps found by exercising the 2.16.0 null-safety surface end-to-end.

    • A T? parameter now accepts a T argument. Calling a method with a String? parameter and a non-null String failed with parameters signature not compatible, though passing null worked. Argument passing is an assignment, so ASTFunctionParameters.parameterAcceptsType now uses ASTType.acceptsAssignment instead of acceptsType. (int?/double? only appeared to work because StrictType ignores nullable in ==, while ASTTypeString compares it — so String? and String were unequal and ASTTypeString.acceptsType rejected the argument.) A non-nullable parameter still rejects null.
    • A null-aware access now reports a nullable static type. Storing a short-circuited result in a local failed: var v = s?.length on a null receiver threw Class not set for type: Null, and var v = xs?[0] threw Can't cast initial (null) value to type: int. ?. (getter and method) and ?[ now resolve to T?, and resolving the type of an access whose receiver is null reports Null instead of trying to find a class for it. Using the result in a return, inline with ??, or in a string interpolation already worked and is unchanged.
    • Member-access chains parse. a.b.c, a.b?.c, a.b!.c, a.b.m(), a.b?.m(x) and chained writes (a.b.c = v) previously failed — the grammar accepted only a single identifier receiver, so a.next?.value reported SyntaxError: digit expected (the ? was read as the start of a number) and even plain a.next.value reported "(" expected. A new chain rule folds each segment onto the previous one, wrapping it in the new ASTExpressionVariable (an ASTVariable backed by an expression) so the existing object-access nodes supply the runtime, null-aware and code-generation behaviour. Chains of any depth work, in any mix of ., ?. and !. Single-segment access keeps its own rules — the chain rule requires two or more segments — so enum entries, static fields and import prefixes resolve exactly as before. A field read off a parenthesized receiver ((a).v, (a)?.v) also parses now; only (expr).m().field remains unsupported.
    • ?? and ??= no longer leak into targets that cannot compile them. Java, Lua, Python and Go emitted a ?? b verbatim, and ??= leaked into every target — including Kotlin, which had a correct ?: for ?? but no hook for the assignment form. ?? now goes through an overridable renderNullCoalesce, and ??= is lowered to t = t ?? v wherever supportsNullCoalesceAssignment is false, so each target defines only one desugaring:
      • Java: (a != null ? a : b)
      • Python: (a if a is not None else b) — an is not None test, so 0/''/ False are preserved
      • Lua: an immediately-invoked function with an explicit nil test, because both a or b and the a ~= nil and a or b idiom return b for a non-nil false; it also binds a to a local, so it is evaluated once
      • Kotlin: ?: for ??, and t = t ?: v for ??=
      • Dart, C#, JavaScript, TypeScript: unchanged, they have both operators
      • Go: reports an UnsupportedSyntaxError. Go has neither a null-coalescing operator nor a conditional expression, and this generator maps a nullable T? onto a plain Go T, which for a value type cannot be compared to nil — any rendering would be code that does not compile. Representing T? as *T throughout the Go generator is separate work.

    Also adds an overridable resolveASTAssignmentOperatorText, which lets the Python generator drop its whole generateASTExpressionVariableAssignment override (it existed only to spell integer division //=).

    Open source →
  20. 2.16.0 24 Jul 2026
    Release notes

    Dart null-safety support

    • Nullable type syntax (T?). The Dart grammar now parses the ? suffix on simple, generic, collection and function types (String?, List<String?>, Map<String, User?>, Future<User?>, void Function()?, String? Function(int)). ASTType carries an isNullable/nullable flag (via asNullable() / withoutNullability()), and the Dart/Kotlin generators round-trip the ? suffix (TypeScript renders nullable types without the suffix — T? on a member isn't valid TS — and other targets drop it, best-effort).
    • Null-aware operators. Added full parse + runtime + Dart round-trip support for ?? (null-coalescing), ??= (null-coalescing assignment), ?. (null-aware getter and method invocation), ?[ (null-aware indexing), postfix ! (null assertion — standalone x! and before access: x!.f, x!.m(), x![i] — throwing ApolloVMNullPointerException on null), and cascades .. / ?.. (null-aware). ?? is wired into the operator-precedence reducer as the loosest binary operator (relational/equality/logical tiers were made explicit at the same time).
    • Assignability is nullability-aware. ASTType.acceptsAssignment (used by the runtime declaration/instance-of checks) accepts null only for nullable slots and lets a T? slot accept a T value.
    • Static null-safety analysis pass. A new pragmatic, flow-aware analyzer (NullSafetyAnalyzer) reports assigning null to a non-nullable declaration/parameter and unconditional member/method/index access on a nullable local, with flow promotion for if (x != null) { … } / if (x == null) … else { … } and suppression via ?./!. Its diagnostics are surfaced through the LSP analyzer for Dart documents.
    • Cross-language generation. Kotlin emits the Elvis operator ?: for ?? and !! for !; TypeScript emits ??, ?., ?.[ (element access) and !. Java, JavaScript, C#, Go, Python and Lua are best-effort (C#/JS keep native ??; the rest render the closest form without failing).
    • Wasm compilation. The Wasm backend lowers null-safety syntax within its non-null numeric domain: x! compiles to x, a ?? b to its left operand, ?./?[ to plain access, and a nullable T? to the underlying numeric type. A null literal stays an explicit unsupported-construct error (Wasm has no null value) rather than a silent miscompilation.

    LSP & MCP correctness fixes

    • LSP textDocument/references now honors context.includeDeclaration. The flag was plumbed through every layer but discarded, so the declaration occurrence was always returned. When includeDeclaration is false, the occurrence that coincides with the symbol's declaration is now excluded.
    • LSP field/variable documentSymbol range covers the whole declaration. A field/variable range previously stopped at the name; it now extends to the terminating ; (skipping over any (/[/{ } in an initializer), matching how methods and enum members already cover their full entry.
    • MCP tools coerce client arguments instead of hard-casting them. Decoded JSON numbers may arrive as double (e.g. 1000.0) or as strings, so timeoutMs/maxDepth/args/className were coerced via new mcpCoerceInt/_strOrNull/_listOrEmpty helpers rather than an unsafe as int?/as List? that threw a raw TypeError and crashed the tool. Applies to apollovm.execute/apollovm.ast and both isolate executors.
    • MCP HTTP/SSE transport answers CORS preflight. HttpSseTransport now responds to OPTIONS with Access-Control-Allow-Origin/Methods/Headers (204) instead of a 404, unblocking cross-origin browser POSTs.

    Go & Lua generator correctness fixes

    • Go &^ (AND NOT / bit clear) now computes a & (~b). It was mapped to a plain bitwise-AND (a & b), silently producing wrong results and corrupting Go→Go round-trips. It is now desugared at parse time to a bitwise-AND whose right operand is the bitwise complement of the right-hand side, at the same precedence as &.
    • Lua string-variable concatenation emits ... A + whose operands are statically String-typed (e.g. s1 + s2, with no string literal to trip the old heuristic) now generates Lua .. instead of an invalid +.
    • Lua list indexing is shifted to Lua's 1-based convention. A list index coming from a 0-based source language (list[0]) now generates list[1] (list[i]list[(i) + 1]). Map/table key access is left unchanged. The Lua grammar does not parse index-access expressions, so no Lua→Lua round-trip is double-shifted.
    • Lua string-interpolation subexpressions are parenthesized. An interpolated expression that binds looser than .. (e.g. "${a < b}") is now wrapped in parentheses ("x" .. (a < b)) so it no longer mis-associates under Lua's concatenation precedence.
    Open source →
  21. 2.15.0 19 Jul 2026

    Nothing published for this version

  22. 2.14.0 19 Jul 2026
    Release notes

    Wasm: String == / String != content equality

    The on-the-fly WebAssembly compiler now compiles String == String and String != String to content equality via the __streq synth helper (the same byte-comparison already used for switch cases and Map<String, …> key lookups), instead of leaving the two String handles to flow into a numeric comparison — which tested pointer identity and, when the handles reached an i64.eq, produced invalid Wasm. The result is a proper bool, so it can be returned directly, used as an if/&&/|| condition, or otherwise combined logically. != inverts the helper's result with i32.eqz. Covers literal/variable operands and the empty string.

    Open source →
  23. 2.13.0 19 Jul 2026
    Release notes

    Wasm: generic-class fields (Box<T>) + aggregate-return coverage

    The on-the-fly WebAssembly compiler now supports a generic class with a type-parameter field (class Box<T> { T value; ... }). The field is stored boxed (the constructor boxes the argument), and reading it back at the instantiation type unboxes to the concrete representation. Two fixes closed this: the return-expression path now threads its context into the value-conversion helper (it previously hit a module-less path and threw), and that helper gained an Object → String / bool / instance unbox case alongside the existing numeric one. Covers Box<int|double|String|bool>, generic fields in arithmetic, and multi-parameter classes such as Pair<int, String>.

    Also adds regression tests confirming that returning a List/Map across the module boundary works (List<int|double|String> via literal, arrow, or built-with-.add, and Map<String,int>).

    With this, every lettered gap in WASM_BACKEND_PLAN.md (A–G) is closed.

    Open source →
  24. 2.12.0 18 Jul 2026
    Release notes

    String index s[i] (interpreter + Wasm)

    Adds String[i] — Dart's index operator, which returns the character at i as a length-1 String — to the AST interpreter core (which was missing it, throwing a null-pointer error) and to the on-the-fly WebAssembly compiler. Both backends now agree. This restores functionality briefly dropped in 2.11.0, where s[i] had been mistaken for invalid Dart; it is valid, so it is now supported end to end.

    Open source →
  25. 2.11.0 18 Jul 2026
    Release notes

    Wasm: String split

    The on-the-fly WebAssembly compiler now supports String.split(sep), returning a List<String>. It is compiled as two passes over the [len][utf8] layout: the first counts the separators to size the list (pieces = count + 1), the second allocates each piece as a fresh String and stores its pointer in the list buffer. Handles multi-char separators and leading/trailing empty pieces; an empty sep yields a single whole-string piece (Dart's char-split for '' is a follow-up).

    With this, the String method surface in Wasm is broadly complete (case, length, slice/search, trim/pad, replace, compareTo, split). The main remaining Wasm backend gaps are value-representation ones: generic-typed fields and returning a List/Map across the module boundary.

    Open source →
  26. 2.10.0 18 Jul 2026
    Release notes

    String compareTo (interpreter + Wasm)

    Adds String.compareTo(other) to the AST interpreter core (which was missing it) and to the on-the-fly WebAssembly compiler, so it now works on both backends with matching results. The Wasm implementation is a lexicographic byte comparison over the [len][utf8] layout, returning -1 / 0 / 1 (a shorter string that is a prefix of the other sorts first).

    Still to come for String in Wasm: split (returns a List) and index [].

    Open source →
  27. 2.9.0 18 Jul 2026
    Release notes

    Wasm: String replaceAll / replaceFirst

    The on-the-fly WebAssembly compiler now supports replaceAll(from, to) and replaceFirst(from, to) over its [len][utf8] layout. Each is compiled as two passes — the first counts non-overlapping matches to size the output buffer, the second builds it (copying to for a match, else one byte). Handles a replacement that grows, shrinks, or removes the match, and matches at either end; an empty from returns a copy (avoiding a non-terminating scan).

    Still to come for String: split (returns a List), index [], and compareTo (which also needs String.compareTo in the interpreter core first).

    Open source →
  28. 2.8.0 18 Jul 2026
    Release notes

    Wasm: String trim & pad methods

    The on-the-fly WebAssembly compiler now supports the String trim and pad methods over its [len][utf8] layout: trim() / trimLeft() / trimRight() (ASCII whitespace stripping) and padLeft(width, [pad]) / padRight(width, [pad]) (single-byte pad, default space). These join the slice/search methods added in 2.7.0. Byte-indexed, so exact for ASCII text.

    Still to come: split, replaceAll / replaceFirst, index [], and compareTo (which also needs String.compareTo in the interpreter core first).

    Open source →
  29. 2.7.0 18 Jul 2026
    Release notes

    Wasm: String slice & search methods

    The on-the-fly WebAssembly compiler now supports the common String slice/search methods over its [len][utf8] layout: substring(start, [end]) (a fresh-buffer memory.copy slice), codeUnitAt(i), startsWith / endsWith, indexOf, and contains (byte scans, guarded against out-of-bounds reads). These join the already-supported length / isEmpty / isNotEmpty getters and toUpperCase / toLowerCase. Methods are byte-indexed, so results are exact for ASCII text.

    Still to come: trim, split, replaceAll / replaceFirst, padLeft / padRight, compareTo, and index []; chaining a method onto a method result (the receiver must be a named local).

    Open source →
  30. 2.6.0 18 Jul 2026
    Release notes

    Wasm: nested collections & chained indexing (m[0][1])

    The on-the-fly WebAssembly compiler now supports collections nested inside collections and chained subscript access. A nested collection is stored as a pointer to the inner header, so a List/Map literal can hold List/Map elements ([[1, 2], [3, 4]], {'a': {'b': 5}}), and chained subscripts read and write through every level (m[0][1], m['a']['b'], m[0]['x'], m['k'][1]), including compound assignment (m[0][1] += 5) and multi-level nesting. Nested literals use depth-offset scratch locals so an inner literal never clobbers the enclosing one's buffers; single-level collections stay byte-identical.

    Writing into an innermost Map (list[0]['k'] = v) — reads of that shape already work — and a subscript/method on a non-variable receiver (getList()[0], m[1].length) remain follow-ups.

    Open source →
  31. 2.5.0 18 Jul 2026
    Release notes

    Wasm: custom instance getters

    The on-the-fly WebAssembly compiler now supports user-declared instance getters (int get x { ... }). A getter is synthesized as a zero-argument instance method, so an access via a receiver (c.x) lowers to a 0-arg method call — reusing the whole instance-method path (argument marshalling, return conversion) and the superclass-chain resolution, so inherited and overridden getters resolve just like methods. Covers int/double/bool/String getters, a computed-expression getter body, and a getter used inside an expression (read once or twice), plus inherited and overridden getters.

    Bare getter access inside a method body (x resolving to this.x, no receiver) and setters remain follow-ups (bare access is not resolved by the interpreter yet either).

    Open source →
  32. 2.4.0 18 Jul 2026
    Release notes

    Wasm: class inheritance (extends / super)

    The on-the-fly WebAssembly compiler now supports single inheritance, matching the interpreter. A subclass instance carries its superclass's fields first in its heap layout, so an inherited field sits at the same offset as on the superclass — a superclass method, compiled once, reads/writes the correct slot when invoked on a subclass instance. Method resolution walks the extends chain (an override on the subclass wins, otherwise the inherited superclass method), the subclass constructor runs inherited field initializers, and super.method(args) keeps the current instance as the receiver while dispatching to the superclass (skipping the override). Covers inherited method calls (bare and via a receiver, with arguments), inherited field read/write (including double), own+inherited fields at distinct offsets, override-wins, super.method()/super.method(args), and multi-level extends chains.

    Dispatch is static (by the receiver's declared type); virtual dispatch through an upcast receiver, super.field/super.getter, and inherited user-getters remain follow-ups. Static members are intentionally not inherited (matching Dart).

    Open source →
  33. 2.3.0 18 Jul 2026
    Release notes

    Wasm: static class fields

    The on-the-fly WebAssembly compiler now supports static class fields (bringing it toward parity with the interpreter, which gained them in 2.1.1). Each static field becomes a typed, mutable module global seeded with its literal int/double/bool initializer; a bare reference inside a static method reads and writes it (including compound assignment like c += 1), and values persist across calls. Qualified Class.field from another class, inherited static fields, and non-literal initializers remain follow-ups.

    Open source →
  34. 2.2.0 18 Jul 2026
    Release notes

    Nested / chained index access (m[0][1])

    Indexing into a nested collection now works for both reads and writes: m[0][1], m['a']['b'], m['a'][0], and deeper chains, including compound assignment (m[1][0] += 5). Previously only a single [...] on a bare variable was supported. Single-index access is unchanged.

    Class inheritance (extends) is now functional

    extends was parsed but had no runtime effect — inherited methods and fields were invisible and super was unresolved. Now:

    • Inherited methods resolve through the superclass chain, for both bare calls (base() inside a subclass) and receiver calls (obj.base()); a subclass override still wins over the inherited method.
    • Inherited fields are initialized onto instances (superclass-first) and are readable/writable from a subclass.
    • Inherited getters resolve through the superclass chain (a subclass getter override still wins), and inherited static fields read/write the base class that declares them (Sub.staticField).
    • super dispatches to the parent: super.method() calls the overridden parent method (resolved relative to the class where the call is written, so it is correct for multi-level hierarchies), super.getter reads the parent getter, and super.field reads/writes the inherited field.
    • The Java (extends), C# (: Base) and JavaScript (extends) grammars now record the superclass they were previously dropping, so inheritance works across those languages (and TypeScript / Python) too. (Kotlin's class B : A() base clause is not parsed yet.)

    Not yet supported: constructor initializer lists with an explicit super-constructor call (B(v) : super(v)) — inherited fields are still set from a constructor body or a this.param.

    Open source →
  35. 2.1.1 18 Jul 2026
    Release notes

    static class fields are now supported

    static fields previously had no class-level storage — they were initialized onto every instance, and reading one (ClassName.field, or a bare reference inside a static method) threw at runtime. Now each class has a lazily initialized static-field store, shared by qualified ClassName.field access and bare references inside the class's own static methods, for read, write and compound assignment. The Java and C# grammars also now record the static field modifier (they were dropping it), so this works across those languages too.

    Correctness fixes (parser & interpreter)

    • Compound assignment ~/= no longer crashes the parser. ~/= (Dart) and //= (Python) matched the grammar but had no handler, so parsing threw an uncaught error out of loadCodeUnit instead of building the AST. ~/=///= are now fully supported, and any other still-unsupported compound operator (e.g. JavaScript/TypeScript %=) now surfaces as a clean SyntaxError instead of an uncaught crash.
    • Enum.values can be assigned to a typed or inferred list. It was built with a dynamic element type while its declared type is List<Enum>, so var v = Color.values; / List<Color> v = Color.values; failed the declaration cast. It now carries the enum's own element type.
    • ASTValue.fromValue<int> on a non-whole double now throws a clean ApolloVMCastException instead of a raw Dart TypeError.

    Division semantics are unchanged and remain intentionally per-language (Dart/JS/TS/Python / yields a double; Java/C#/Go/Kotlin / on ints is truncating integer division); regression guards now pin both.

    Open source →
  36. 2.1.0 17 Jul 2026
    Release notes

    Wasm backend — loop increment fix and initial String methods

    • Fixed: a ++/-- statement inside a while/do-while body produced an invalid module. A bare increment/decrement (i++, ++i, i--, --i) used as a statement left the operator's value on the operand stack; inside a loop's void block that value reached the block end and the compiled module failed WebAssembly validation ("values remaining on stack at end of block"). Only explicitly int-typed counters written as i = i + 1 happened to avoid it. The discarded value is now dropped in statement position; the expression form (x = i++) still yields its value, and for-header updates are unchanged.
    • New: String.length, .isEmpty, .isNotEmpty compile to Wasm. They read the string's [len:i32][utf8] header length word.
    • New: String.toUpperCase() / .toLowerCase() compile to Wasm for ASCII text — a fresh buffer is allocated and each ASCII letter is shifted by the case bit; other bytes are copied unchanged.

    The stored String length is the UTF-8 byte count, so these match Dart semantics for ASCII input. See WASM_BACKEND_PLAN.md for the re-verified backend status (8 of the 9 previously documented gaps already work) and the remaining gaps.

    Tests

    • Substantially expanded coverage: AST values/types/variables/annotations, the core library, external-function/getter mapping, runner tryExecute* fallbacks, in-memory code storages, cross-language translation and generator emit paths, native parse+execute across every non-Dart language, and a transpile-then-execute integration matrix (Dart → Java/JS/TS/C#).
    Open source →
  37. 2.0.1 17 Jul 2026
    Release notes

    LSP — an enum constant now selects its whole entry, not just its name

    A Dart enum constant is a constructor invocation, but the language server's declaration scanner recorded its range as the name alone: earth(5.97, 6371) selected only earth, dropping the arguments. Every other declaration kind has its range extended past the name — a class member's covers its parameters and body — so enum constants were the one exception, and folding, hover and the document outline all saw a truncated span.

    • An enum constant's range now covers the whole entry: a .named constructor, <T> type arguments, the argument list, or an = value (Red = 1). Its name span is unchanged, so go-to-definition and rename still target the name alone.
    • Named arguments are no longer reported as extra constants. The scan that skipped past a constant stopped at the first , even when nested inside the argument list, so scanning resumed mid-arguments and read the argument label as another constant: earth(mass: 5.97, radius: 6371) emitted a phantom radius enum member into the outline. Entry consumption is now depth-aware — only a ,/;/} outside brackets ends an entry — which fixes both the truncated range and the phantom symbols.

    Constructors declared in an enum body (const Planet(this.mass);) were already correct and are unchanged.

    Open source →
  38. 2.0.0 13 Jul 2026
    Release notes

    apollovm no longer drags a native/FFI toolchain into every consumer

    Executing a compiled Wasm module on the Dart VM needs a native engine (package:wasm_run), which brings an FFI/Rust toolchain and a long-abandoned flutter_rust_bridge 1.x with it. That dependency was paid by every consumer of apollovm — including the many that only parse, translate or generate code and never execute Wasm at all. Worse, flutter_rust_bridge 1.x pins shelf_web_socket ^1.0.2 and web_socket_channel ^2.2.0, so anything depending on apollovm (however indirectly) could not also use a modern shelf/WebSocket stack.

    The native runtime now lives in its own package, apollovm_wasm. Nothing else moved: Wasm compilation is unchanged and still in core.

    Breaking

    • package:wasm_run is no longer a dependency. On the Dart VM, WasmRuntime() now returns an unsupported runtime (isSupported == false) unless the native engine is registered. Wasm still compiles; it just cannot be executed out of the box.

      To execute Wasm on the VM, add apollovm_wasm and register it once:

      import 'package:apollovm/apollovm.dart';
      import 'package:apollovm_wasm/apollovm_wasm.dart';
      
      void main() {
        registerApolloVMWasmRuntime();
      
        final runtime = WasmRuntime()..ensureBooted();
        print(runtime.isSupported); // true
      }
      

      Browsers are unaffected — they have a Wasm engine built in, and the browser runtime remains in core.

    • createWasmRuntime() is no longer part of the VM conditional-import chain. The platform default is now resolved through WasmRuntime.registerProvider(), which apollovm_wasm calls.

    Added

    • WasmRuntime.registerProvider() — installs the implementation that WasmRuntime() instantiates, so an engine can be supplied from outside the package.
    Open source →
  39. 1.10.0 11 Jul 2026
    Release notes

    Parse errors now point at the real mistake, not the top of the file

    Every ApolloVM grammar's top rule is compilationUnit().star().trim().end(). When a syntax error appeared deep in a file, the last top-level definition failed deep in the input, but star() treated that as "stop, success" and discarded the failure, so end() reported a generic end of input expected at offset 0 (line 1) — technically correct, useless in an editor. petitparser's plain parse() does not track the farthest failure, so the deep position was lost.

    Parsers now record the farthest point the grammar actually reached and report the error there, at or immediately adjacent to the real mistake:

    • New opt-in ApolloSourceCodeParser.trackFarthestFailure. When a parse fails, the source is re-parsed with a copy of the grammar (built via petitparser's transformParser + callCC) in which every parser records each Failure it produces if it is the deepest seen. This captures failures that star() / optional() would otherwise swallow. The deeper of the two positions is used, so the reported position never gets worse. The happy path is untouched — the fast plain parser still runs first; the tracking re-parse only runs on failure.
    • Enabled for Dart, Java 11, Kotlin, Go, C#, JavaScript, TypeScript and Lua. For an invalid token mid-expression on line N, all eight now report the error exactly at that token on line N instead of at line 1.
    • Python is intentionally excluded. Its source is rewritten by the indentation preprocessor before the grammar sees it, so grammar offsets are in preprocessed coordinates — dropped blank/comment lines shift the reported line and INDENT/DEDENT markers would leak into messages. Enabling it needs a source map from preprocessed offsets back to the original (future work).

    LSP

    • locateParseError now runs its structural heuristics (bracket-imbalance, missing ;) before trusting the raw parser position, then falls back to the now-precise parser position. Editor-friendly locations (a missing ; reported at the end of the offending value; '(' is never closed) are preserved, while the fallback for every other failure is far better than the old offset 0.
    Open source →
  40. 1.9.2 10 Jul 2026
    Release notes

    Go — a class with a field-initializing constructor now survives Dart -> Go

    Three defects made Dart -> Go -> parse -> run fail (or, worse, silently produce the wrong answer) for programs the 1.9.1 fixtures did not cover.

    • A field-initializing constructor parameter was dropped. Dart's Point(this.x, this.y) shorthand generated func NewPoint(x int, y int) *Point { o := &Point{}; return o } — the arguments were never assigned, so every field stayed zero-valued and the program ran to a wrong result rather than failing. The factory now emits o.x = x for each this. parameter, after the field's declared initial value so an explicit argument wins. The long-hand form (Point(int x, int y) { this.x = x; }) was already correct.
    • A source identifier colliding with a Go keyword emitted invalid Go. A Dart var map = ... or a field named type became map := ... / o.type, which does not parse — map and type are two of Go's 25 reserved words. Such a name is now escaped with a trailing _ (map_, type_) at every site: declarations, reads, parameters, struct fields, methods and the members written after a ..
    • Structs were emitted after the functions that call them. ApolloVM's Go parser resolves NewPoint(...) against the declarations it has already seen, so a top-level function emitted first could not resolve the struct. generateASTRoot now emits structs (and their factories) before the top-level function block.

    Supporting change:

    • New ApolloGenerator.normalizeIdentifier(String) hook, applied by the base generator to the member names written after a . (fields, getters, setters and methods). It is the identity for every language except Go, which uses it to escape reserved words, so a declaration and its uses always agree.

    Known gap

    ApolloVM's Go parser remains order-dependent: hand-written Go that calls NewPoint(...) before declaring type Point struct still fails to resolve, though Go itself is order-independent. The generator no longer produces that shape.

    Open source →
  41. 1.9.1 10 Jul 2026
    Release notes

    Core-library, Java 11 and Go fixes found by a coverage pass

    Line coverage of lib/ went from 83.4% to 84.7%. The number is the least interesting part: writing the tests surfaced seven real defects, all fixed here.

    Core library

    • String.length, String.isEmpty, String.isNotEmpty and the sign of int/double only worked when called. s.length() worked and s.length threw — the opposite of how Dart, Kotlin and C# source reads. List and Map already exposed theirs as getters. They are now getters and methods, so Java-style s.length() keeps working.

    Java 11

    • The grammar crashed on the diamond form of a collection literal. new HashMap<>(){{ put("a", 1); }} — exactly what the Java generator emits for a Dart map literal — handed the '>' character to a cast expecting an ASTType and threw a raw TypeError. A separate typo made the map literal's diamond alternative match << instead of <>. Generic type arguments are now picked out by type rather than by position, in all four list/map literal rules. A Dart map literal now round-trips through Java.

    Go — constructors did not work at all, in either direction

    • The grammar had no &T{} composite literal, so any Go source containing a func NewFoo() *Foo { o := &Foo{} ... } factory — the exact shape the Go generator emits — failed to parse. The zero-valued form now parses as the struct's no-argument constructor. Field-initializing literals (&Foo{x: 1}) remain unsupported.
    • A NewFoo(...) call site resolved to a top-level function that no longer existed, since the declaration had already been folded into the struct's constructor. It now resolves to the constructor.
    • Inside a factory, o was a plain local, so o.x = x failed with "Can't find variable: 'o'". It is now bound as the receiver before the body parses, which the factory-to-constructor conversion already assumed.
    • The generator rewrote a shadowing parameter as a field. Point(int x) { this.x = x; } emitted o.x = o.x, silently assigning the field to itself. A parameter now shadows a same-named field.
    • The generator emitted p := Point(a, 1), which is not Go. Instantiation now emits the factory call p := NewPoint(a, 1), and every struct gets a factory (a field-less struct used to get none) so the call always resolves. This is the only change to the expected output of the go_*.test.xml fixtures.

    Dart -> Go -> parse -> run now works end to end.

    Cleanup

    • ApolloGenerator carried ~140 lines of dead dispatch (generateASTNode, generateASTRoot, generateASTType, generateASTStatement, generateASTBranch, generateASTExpression, generateASTValue). Every one was shadowed by both subclasses, and they had already drifted: the base generateASTRoot never learned to emit extensions, so a future third generator would have silently dropped them. They are now abstract, or removed where nothing dispatches through the base.

    Tests

    • The core runtime library: dart:math, and every String, int, double, List and Map member, in both getter and method form where both exist.
    • Go constructors: the factory and its composite literal, running a factory, the shadowing fix, and a full Dart -> Go -> parse -> run round-trip.
    • A generator matrix asserting that a program exercising most AST node kinds generates in all nine languages and that the generated source parses back. This is what found the Java 11 and Go defects.

    Known gaps, documented but not fixed

    Several grammars parse only a subset of what their own generator emits: the Lua generator emits a + b for string concatenation and s.m() for method calls, where Lua wants a .. b and s:m() (Lua classes are already marked unsupported in the README matrix, so that generator is outside the stated contract); Python cannot parse the lambda it generates; JS and TS cannot parse the map literal they generate. Each is listed in test/unit/apollovm_generator_matrix_test.dart.

    Open source →
  42. 1.9.0 10 Jul 2026
    Release notes

    Extensions — add methods and getters to an existing type

    Three of the supported languages have a native extension construct, and they now share one: parse in any of them, run, and translate to the others.

    • Dart: extension NumExt on int { int doubled() { return this * 2; } int get twice { … } } (the name is optional).
    • Kotlin: top-level fun Int.doubled(): Int { … } and val Int.twice: Int get() = …; members are grouped by receiver into one extension.
    • C#: static class NumExt { public static int Doubled(this int self) { … } }; the self-parameter and this are translated in both directions. C# has no extension property, so an extension with a getter cannot be emitted as C#.

    Extensions apply to core types (int, String, …) as well as user classes, support overloads, and may call one another (this.doubled() or plain doubled()). A class member always wins over an extension member. Extensions are module-local: they are not carried across import. Java, JavaScript, TypeScript, Python, Go and Lua have no equivalent construct, so asking for one throws UnsupportedSyntaxError instead of emitting a shim that would mean something else.

    Supporting changes:

    • Dart now parses instance getters (int get x { … } / => …) in class bodies too; the ASTClassGetterDeclaration node existed but no grammar reached it.
    • New ASTExtension node and ASTRoot.extensions registry, consulted as a fallback when a receiver's own class lookup misses.
    • Fixed: expressions interpolated into a string ('${a.doubled()}') were never linked into the AST parentNode chain, so they could not resolve anything reached through it.
    • Extensions appear in LSP document symbols and in the MCP AST serialization.
    • MCP: an ASTGetterDeclaration now serializes its name, return type and modifiers instead of appearing as an anonymous node.
    Open source →
  43. 1.8.0 07 Jul 2026
    Release notes

    Remote repository backend — edit and version-control a project from the browser

    The 1.7.0 repository features left "room for a remote/web backend"; this release adds it, so a web IDE (or any Dart host) can drive a real project over the wire.

    • New RemoteRepositoryAdapter (web-safe, in package:apollovm/apollovm_repository.dart): a RepositoryAdapter that talks to a repository server over HTTP, implementing the full interface (filesystem, search, git). Connect with RemoteRepositoryAdapter.connect('http://host:port'); it reports RepoCapabilities.isRemote. Backed by package:http, so it runs in the browser.
    • New RepositoryRpc (web-safe): a transport-agnostic JSON dispatcher that maps {op, ...args} requests to a RepositoryService. A shared Op constant set is the single source of truth for the wire contract, so server and client can't drift.
    • fromJson on every repository value type (RepoFile, RepoEntry, RepoStat, RepoEdit, TextMatch, RepoCapabilities, and the git types), completing the JSON round-trip alongside the existing toJson.
    • New tool/repository_server.dart: a lightweight HTTP server exposing a local checkout (via LocalRepositoryAdapter) over POST /rpc, with reflected, permissive CORS so a browser app on another origin can reach it. Flags: --workspace, --allow-write, --allow-git-write, --port, --address. The server remains the authority on permissions regardless of what a client requests.
    Open source →
  44. 1.7.0 07 Jul 2026
    Release notes

    Repository features — read, search, navigate, edit and version-control a codebase

    • New standalone libraries package:apollovm/apollovm_repository.dart (web-safe) and apollovm_repository_io.dart (adds the on-disk adapter). The repository features are not MCP-exclusive — a web IDE, a desktop editor, an agent or a test can use them directly via RepositoryService, a typed façade returning typed results (no MCP/JSON). It bundles the filesystem/search/ git operations of a pluggable RepositoryAdapter with language-aware code navigation (outline/definition/references/hover/diagnostics/ workspaceSymbols/searchSymbols) powered by ApolloVM's parsers via an in-process LspService.
    • Pluggable backend. RepositoryAdapter implementations: LocalRepositoryAdapter (dart:io + git), InMemoryRepositoryAdapter (web-safe), plus room for a remote/web backend — enabling file edits and git commands from the browser. A PermissionGuard decorator enforces a RepoConfig uniformly across backends.

    MCP — the same features as agent tools

    • New apollovm.fs.*, apollovm.search.*, apollovm.code.* and apollovm.git.* tools are a thin JSON layer over RepositoryService, letting an agent read, search, navigate, edit and version-control real files instead of shelling out to POSIX (cat, ls, find, grep, sed, git). Enabled by serving with --workspace <dir> (or by passing a RepositoryAdapter to ApolloMcpServer/serveStdio/HttpSseTransport); with no workspace the server stays inline-source-only exactly as before.
    • Security. Read-only by default; filesystem writes and git mutations are opt-in (--allow-write / --allow-git-write). Paths are confined to the workspace root (../absolute rejected), git is invoked with an argument list (never a shell) pinned to the root, and fs.edit accepts an atLine safety anchor (made mandatory by --require-line-match).
    • The repository tools are surfaced only when a workspace/adapter is configured: the server registers them only when given a RepositoryAdapter, and the apollovm mcp list/schema/info CLI advertise them only with --workspace. mcp call invokes them via --json-args + --workspace.
    Open source →
  45. 1.6.3 07 Jul 2026
    Release notes

    Language Server — body-less constructor/method ranges

    • A member with no { ... } body is now fully covered by its documentSymbol range. A ;-terminated constructor (const Foo(this.x);, a redirecting Foo.zero() : this(0);) or an abstract method previously stopped its range at the name, so selecting it excluded the parameters. The range now extends to the terminating ;, covering the whole signature — parameters and any initializer list. Applies to class and enum constructors alike; bodied and expression-bodied members are unchanged.
    Open source →
  46. 1.6.2 06 Jul 2026
    Release notes

    Language Server — expression-body member ranges

    • Members with an expression body are now fully covered by their documentSymbol range. A => expr; member (Dart/C#) previously stopped at its name, and a brace-less = expr member (Kotlin) even swallowed the next member because the scanner never recovered. The token indexer now consumes the expression: the => form extends to its terminating ;, and the brace-less = form is bounded to the end of its line (so the following member is still recognized). Balanced ()/[]/{} inside the expression (e.g. a map literal body => {1: 2}) are handled without corrupting brace tracking. Works for both class and enum members.
    Open source →
  47. 1.6.1 06 Jul 2026
    Release notes

    Language Server — enum members after the constant list

    • Enum members declared after the constant list are now recognized as real members. Everything after the ; that ends an enum's constant list (fields, constructors, methods) was being swallowed as bogus enum constants (named int, const, final, …), so documentSymbol gave enum methods no body range and did not recognize enum constructors at all. The token indexer now tracks the constant-list terminator per enum: after it, identifiers in the enum body are parsed like class members — real kinds and full-body ranges. Pure-constant enums (enum E { a, b, c }) are unaffected; class constructors were already handled correctly.
    Open source →
  48. 1.6.0 06 Jul 2026
    Release notes

    Language Server — member-aware completion and full-body symbol ranges

    • documentSymbol ranges now span a member's whole body, not just its signature. Previously only class/enum declarations had their range extended to the closing }; a function/method/constructor stopped at the end of its name, so an editor "select symbol" or outline action only highlighted the signature. The token indexer now tracks member bodies too — skipping the parameter list first, so Dart named-parameter braces ({ ... }) are not mistaken for the body — and extends fullEnd to the body's closing }. Bodyless members (abstract/;-terminated) are left unchanged. The narrower selectionRange (the name) is untouched.
    • Completion on this. / super. proposes the enclosing type's members. A member-access position is now recognized and answered with just that type's fields and methods (with their real kinds), instead of the full grab-bag of every identifier plus keywords.
    • Completion keeps real symbol kinds even when the buffer does not parse. The this. case leaves the source unparseable, which had emptied the AST symbol table and degraded every proposal to a plain identifier. Completion now also draws on the token-index declarations (which survive a failed parse), so methods and fields keep their proper kinds mid-edit. Local variables and parameters continue to be surfaced from the raw token stream.
    Open source →
  49. 1.5.0 06 Jul 2026
    Release notes

    Language Server — better completion and parse-error locations

    • Completion now surfaces in-scope identifiers, and works while the buffer does not parse. Previously textDocument/completion only offered AST symbols (top-level/members) plus keywords — so mid-edit, when the parse fails (the common case), it fell back to keywords only. It now also harvests identifiers from the raw token stream, which survives a failed parse and includes local variables and parameters the symbol table omits. Results are de-duplicated and ranked: local-scope symbols, then other symbols, then harvested identifiers, then keywords. The partial token under the cursor is skipped.
    • Parse-error diagnostics locate a missing ;. ApolloVM's PEG parser reports a generic "end of input expected" at offset 0 for most structural mistakes; the LSP layer already recovered the real position for bracket imbalances, and now also for a missing statement terminator in ;-required languages (Dart, Java, C#). A balanced-but-unterminated statement is pinned to the end of the offending value (with an expected ';' hint) instead of defaulting to the top of the file. The heuristic is conservative — it skips continuation shapes (operators, ., ?, :, ,, open brackets, continuation keywords like return, and annotations) and does not apply to languages where ; is optional/absent (Kotlin, JavaScript, Lua, Python).
    Open source →
  50. 1.4.2 06 Jul 2026
    Release notes
    • Docs: fix the ApolloVM Web Demo link — the live playground is served at the site root (https://apollovm.github.io/apollovm_web_example/), not the old /www/ path.
    Open source →
  51. 1.4.1 06 Jul 2026
    Release notes

    MCP — web compatibility

    • package:apollovm/apollovm_mcp.dart is now fully web-safe — it imports no dart:io and no dart:isolate, so the MCP server and every apollovm.* / apollovm.lsp.* tool compile and run in a browser (dart2js / DDC). Construct ApolloMcpServer on any StreamChannel<String> (e.g. a web MessageChannel) and drive all tools in-process. (In 1.4.0 the apollovm.lsp.* tools were documented as web-safe, but apollovm_mcp.dart still transitively imported dart:io/dart:isolate, so a web build failed to compile.)
    • The dart:io-only pieces moved to a new package:apollovm/apollovm_mcp_io.dart (which re-exports apollovm_mcp.dart): serveStdio, HttpSseTransport and the CommandMcp CLI group. Native embedders and the apollovm CLI import this. Migration: if you imported serveStdio / HttpSseTransport / CommandMcp from apollovm_mcp.dart, switch that import to apollovm_mcp_io.dart (a one-line change).
    • The per-tool timeout executor is now platform-selected (mirroring wasm_runtime.dart): a killable-isolate executor on native, and an in-process executor with a cooperative timeout on the web (no dart:isolate). Tools in McpLimits.isolateTools therefore still run, degrading to a soft timeout on the web.
    • New cross-platform test test/mcp/web_compat_test.dart runs the tools, the in-process LspClient and the server under dart test --platform chrome, guarding the web-safe surface against future dart:io/dart:isolate leaks.
    Open source →
  52. 1.4.0 05 Jul 2026
    Release notes

    MCP — code-inspection tools (LSP)

    • The MCP server now exposes ApolloVM's LSP features as apollovm.lsp.* tools, so an AI agent can inspect code the way an editor does. Results carry precise LSP line/character ranges.
      • apollovm.lsp.diagnostics — errors/warnings with ranges.
      • apollovm.lsp.symbols — document outline (nested symbols + ranges).
      • apollovm.lsp.hover — signature, type and documentation at a position.
      • apollovm.lsp.definition — declaration location at a position.
      • apollovm.lsp.references — all references at a position.
      • apollovm.lsp.completion — completion proposals at a position.
      • apollovm.lsp.workspaceSymbols — symbol search across multiple in-memory files (codebase-wide lookup).
    • Each tool is stateless and web-safe (backed by the in-process LspService; no socket, no dart:io), runs in-process or inside an isolate like the other bounded tools, and honors the existing maxSourceChars limit. The parser is selected from the language argument, so a mismatched URI cannot change it.
    • New public API in package:apollovm/apollovm_mcp.dart: LspRuntime, buildLspTools, computeLspTool, isLspTool, lspToolNames. The CLI apollovm mcp call gained --line, --character and --query flags.
    • New example example/apollovm_example_mcp_lsp.dart.

    Language Server Protocol — in-process API (no socket)

    • New LspService in package:apollovm/apollovm_lsp.dart: a document-oriented facade that embeds an in-process LspServer and exposes the language features as plain typed Dart calls — no transport, no socket and no initialize/initialized handshake to run by hand. Being dart:io-free it runs unchanged in a browser (compiled to JS) or an AI agent.
      • One-shot analyze(uri, text) returns the buffer's diagnostics directly; open/change/close manage documents (versions tracked automatically); every query resolves against the current buffer.
      • Typed helpers: hover, definition, documentSymbols, completion, references, documentHighlight, prepareRename, rename, workspaceSymbols, plus a diagnostics stream and a ready (InitializeResult) future.
      • LspService.wrap(client) layers the same convenience over an existing LspClient (e.g. one connected to a remote server).
    • New example example/apollovm_example_lsp_api.dart drives the whole flow through LspService with no transport wiring.
    Open source →
  53. 1.3.0 05 Jul 2026
    Release notes

    Language Server Protocol — new server features

    • textDocument/documentHighlight — highlights every occurrence of the identifier under the cursor, marking the declaration site as Write and other uses as Read (documentHighlightProvider capability).
    • textDocument/prepareRename — validates a rename target, returning its range and placeholder, or null when the cursor is not on an identifier (advertised via the rename provider's prepareProvider).

    Language Server Protocol — client

    • New LspClient in package:apollovm/apollovm_lsp.dart: a JSON-RPC client that consumes the ApolloVM LspServer. It correlates responses to the requests it sends, streams server-pushed diagnostics (Stream<PublishDiagnosticsParams> get diagnostics), and exposes typed helpers — initialize, didOpen/didChange/didClose, hover, definition, documentSymbol, completion, references, documentHighlight, prepareRename, rename, workspaceSymbol, shutdown/exit — plus low-level sendRequest/sendNotification.
    • LspClient.inProcess() pairs a client with a fresh LspServer over a linked MessageLspEndpoint pair, all in one isolate — no subprocess, no dart:io. The client works over any LspEndpoint, so an out-of-process server can be driven with LspClient(StreamLspEndpoint(out, in)).
    • LspEndpoint now routes JSON-RPC responses (previously ignored) to a new onResponse hook, enabling the client role. Pure-server usage is unchanged.
    • Protocol data types gained fromJson factories (Hover, Location, Diagnostic, DocumentSymbol, CompletionItem, TextEdit, WorkspaceEdit), and new types were added: InitializeResult, ServerInfo, PublishDiagnosticsParams, WorkspaceSymbol, CompletionList, DocumentHighlight/DocumentHighlightKind, PrepareRenameResult.
    • New example example/apollovm_example_lsp.dart drives a full session (handshake, diagnostics, symbols, hover, definition, completion, highlight, prepare-rename, rename) through LspClient.inProcess().
    Open source →
  54. 1.2.1 03 Jul 2026
    Release notes
    • Fix broken 1.2.0 package on pub.dev: the .pubignore pattern lsp/ was unanchored, so besides the intended root lsp/ dev-tooling directory it also stripped lib/src/lsp/ (the LSP implementation) from the published archive, making dart pub global activate apollovm fail to compile. The pattern is now anchored as /lsp/.
    • .pubignore now also repeats the relevant .gitignore exclusions (*.exe, *.iml, etc.), since a .pubignore file replaces .gitignore for publishing — 1.2.0 accidentally shipped a 7 MB bin/apollovm.exe.
    Open source →
  55. 1.2.0 03 Jul 2026
    Release notes

    Language Server Protocol (LSP 3.17) server

    • A Dart-first language server is now part of the apollovm package, exposed as a separate library package:apollovm/apollovm_lsp.dart (the existing package:apollovm/apollovm.dart exports are unchanged). Source lives in lib/src/lsp/.
    • Runnable two ways. Locally over stdio via a new CLI subcommand apollovm lsp; and embedded / web — the library imports no dart:io, so a browser IDE or an AI agent can drive it with decoded JSON-RPC messages via MessageLspEndpoint (no byte framing). StreamLspEndpoint provides Content-Length framing for stdio/sockets. Both share a transport-agnostic LspEndpoint.
    • The server keeps the ApolloVM core read-only: because the AST carries no source positions and the parser discards comments, a small self-contained scanner re-scans raw text for identifier/declaration positions and correlates them back to the AST (the source of truth for semantics). Four strictly separated layers keep LSP logic out of the parser — transport, protocol (LSP 3.17 types), analysis (parse/index/resolve), and server (handlers).
    • Implemented: initialize/shutdown, incremental diagnostics (parse + unresolvable core imports), documentSymbol, hover (kind/signature/type/ documentation), definition; plus single-file references/rename and a basic ranked completion.
    • Parse-error diagnostics are located precisely: since the core parser reports a generic "end of input expected" at offset 0 for most structural mistakes, the server runs a bracket-balance analysis to underline the real culprit (e.g. an unclosed (/{) with a hint, instead of pointing at the top of the file.
    • Companion assets live under lsp/ (excluded from the published package via .pubignore): a VS Code client (lsp/vscode), an example workspace (lsp/example_workspace), and a latency benchmark (lsp/benchmark).
    • Verified with dart analyze (clean), 17 passing tests in test/lsp/ (including full stdio and message-level protocol sessions), and a benchmark comfortably under its latency targets (open, hover, completion).

    Optional Dart package importer (pub.dev / pubspec-compatible)

    • package: imports can now be resolved against real pub packages, via an optional importer exposed at package:apollovm/apollovm_pub.dart (kept out of the web-safe apollovm.dart).
      • Pluggable PackageProvider: PackageConfigProvider (default, VM-only, zero extra deps — resolves through .dart_tool/package_config.json, exact pub semantics) and PubDevProvider (web-compatible) — downloads archives from pub.dev or a configurable/private/mirror host, extracts them in memory, caches them (MemoryPackageCache by default, FilePackageCache on the VM), and honors pubspec version constraints. Built on web-safe libraries only (http, archive, pub_semver, yaml — no dart:io), so it runs on the VM and in the browser.
      • Web/CORS: PubDevProvider accepts an injectable http.Client, a custom host, and a rewriteUrl hook to route requests through a CORS proxy — a ready-made proxy ships in tool/pub_cors_proxy.dart.
      • DartPackageLoader + DartPackageImporter.provision() fetch each reachable package: import transitively and load its source into the VM; injected via the new settable ApolloVM.moduleLoader. A generic CompositeModuleLoader chains loaders.
      • CLI: apollovm run/translate --pub (with --pub-host / --pub-cache) resolves package: imports before executing.
      • Promotes http, archive, pub_semver, yaml to direct dependencies (all web-safe); only the filesystem members (PackageConfigProvider, FilePackageCache) are behind conditional imports with web stubs.
      • See doc/module_resolution.md and example/apollovm_example_pub_importer.dart.

    Language-agnostic package/module import system

    • Cross-module imports now resolve and execute. A source file can import symbols (classes, functions, enums, type aliases) from other loaded modules, normalized into a single canonical AST regardless of language.
      • Enriched ASTStatementImport (named/show/hide, wildcard, whole-module prefix alias, per-symbol alias) plus new ASTStatementExport and ASTTypeAlias nodes.
      • New web-safe resolution layer (lib/src/resolution/): pluggable ModuleLoader (in-memory VMModuleLoader), four-level SymbolTables + ImportScope, ModuleResolver, a DependencyGraph (Tarjan cycle detection, Kahn topological order, incremental affectedBy invalidation), structured ImportDiagnostics (missing module/symbol, duplicate symbol, circular import, invalid export), a ResolutionCache, and the ModuleResolutionEngine facade.
      • ApolloVM.resolve() returns aggregated diagnostics; resolution is triggered lazily by the runner and invalidated incrementally on loadCodeUnit.
      • Parse + generate wired for Dart, TypeScript, and Python (named/show/ hide/wildcard/alias/re-export/typedef); other languages keep basic imports and compile unchanged against the additive AST.
      • Golden-test harness extended for multi-<source> (cross-module) tests.
      • See doc/module_resolution.md and example/apollovm_example_imports.dart.

    New language: Go

    • Added first-class Go support — ApolloVM can now parse, execute, and translate Go source (.go / go, alias golang) through the shared AST, bidirectionally with every other supported language (and on-the-fly Wasm).
    • Implemented under lib/src/languages/go/ (go_grammar_lexer.dart, go_grammar.dart, go_generator.dart, go_parser.dart, go_runner.dart) and wired into ApolloVM (getParser/createRunner/createCodeGenerator and the .go file-extension mapping).
    • Supported: top-level and struct receiver methods (func (o *Name) m(...)), struct types with fields and factory constructors (func NewName(...) *Name), var/:= type inference, if/else if/else, the four for forms (C-style, condition-only as while, range as for-each, infinite / do-while), Go switch (no fall-through), slices/maps ([]T{…}, map[K]V{…}), closures, all arithmetic/comparison/logical/bitwise operators, string + concatenation, and fmt.Println (normalized to the VM's print).
    • Go has no classes: a class is modeled as a struct + receiver methods (the same idiom Lua uses for tables), so OOP code round-trips across all languages. See the README feature tables for the full per-feature matrix; try/catch/throw, inheritance/interfaces, rich enums and generics are not yet implemented for Go.

    MCP-native runtime: expose ApolloVM to AI agents over the Model Context Protocol

    • New apollovm mcp command group exposes ApolloVM as an MCP (Model Context Protocol) server and tools, turning it into a programmable, sandboxed execution engine for AI agents. Built on the official dart_mcp SDK. Subcommands: mcp serve (run the server over stdio or HTTP/SSE via --http <port>), mcp list (tool definitions), mcp call <tool> (one-shot tool invocation for scripting/CI), mcp info, mcp schema, and mcp doctor.
    • Seven tools: apollovm.parse, apollovm.execute, apollovm.translate, apollovm.ast, apollovm.symbols, apollovm.types, apollovm.wasm — parse, run, translate, compile to Wasm, and inspect AST / symbol graph / type table across all supported languages.
    • Security model: file/network access denied by construction (only print is exposed; inputs are inline source only); apollovm.execute runs in a killable isolate so a hard timeout is enforced even against runaway synchronous loops (per-tool configurable via --isolate-tools); input/output size caps (--max-source-chars, --max-output-chars); best-effort process-level memory.
    • New public library package:apollovm/apollovm_mcp.dart (ApolloMcpServer, serveStdio, HttpSseTransport, McpLimits, computeTool).
    • Added dependency dart_mcp: ^0.5.2 (and stream_channel). New mcp-tagged integration tests under test/mcp/. See doc/MCP.md.
    Open source →
  56. 0.1.48 30 Jun 2026
    Release notes

    CLI: run executes .wasm files through the Wasm runtime

    • apollovm run foo.wasm now runs the binary module via the Wasm runtime (ApolloRunnerWasm) instead of trying to decode it as UTF-8 source. The file is loaded as a BinaryCodeUnit, parsed for its exported functions by ApolloParserWasm, and its entry function (e.g. main) is invoked — closing the compilerun loop from the command line.
    • The .wasm file extension now maps to the wasm language in ApolloVM.parseLanguageFromFilePathExtension.
    Open source →
  57. 0.1.47 28 Jun 2026
    Release notes

    Wasm backend: integer division semantics (/ on ints) + division-by-zero

    • / on integer operands is now integer (truncating) division in Java/Kotlin/C# — where the expression resolves to int — instead of an f64 quotient (so 10 / 3 is 3, not 3.33). Dart's / keeps its double result; ~/ is unchanged.
    • Integer division by zero raises a catchable exception whose message matches the interpreter (IntegerDivisionByZeroException for /, Unsupported operation: Infinity or NaN toInt for ~/). Applies to both ~/ and integer /.
    • A print whose argument is built from a value that just raised (e.g. the quotient in print('q = ${a ~/ b}')) is skipped when an exception is pending, matching the interpreter, which never reaches the print.

    These fix the Dart, Java and Kotlin exception (try/catch/finally) examples.

    Open source →
  58. 0.1.46 27 Jun 2026
    Release notes

    Wasm backend: num (TypeScript/JS number) + switch on a boxed scrutinee

    • num (a TypeScript/JavaScript number) is now supported in the Wasm backend. A plain num has no fixed width; the VM treats integer-valued numbers as int, so num is represented as i64. This fixes string interpolation/concatenation of a num (e.g. "sum=" + (a + b)), switch on a num scrutinee, and num arithmetic — unblocking the TypeScript Class, Conditional, Exceptions and Switch examples.
    • switch on a boxed dynamic/Object scrutinee (e.g. a List<Object> element) now compiles: the scrutinee is unboxed to a concrete i64 to drive the integer branch table.
    • Scalar Object/dynamic entry-point parameters are now marshalled. An untyped parameter (e.g. a JavaScript/Python main(a, b), or an explicit Dart dynamic parameter) is passed as a host-allocated box instead of a raw scalar the module would read as a garbage pointer — fixing the JavaScript, Lua and Python Class/Conditional/Switch examples, which previously ran with all arguments seen as 0. (The apollovm_sig section is now emitted whenever a public function has an Object/dynamic parameter, and a plain num is tagged as int so it is passed as a raw i64 rather than boxed.)
    • Anonymous functions with an untyped parameter (n => n * 2 from C#/Lua/Python) now compile: the parameter type is inferred from its body. A nested closure's return no longer makes the enclosing (void) function non-void.
    • Named nested function declarations (let twice = (n) => …, which JavaScript/TypeScript parse as a function declaration rather than a var) are hoisted and lowered to a direct call.
    • A boxed value flows into and out of an Object/dynamic slot. A concrete value passed to a generic T field/parameter (represented as a boxed Object) is boxed; a boxed value used in arithmetic or passed to a typed numeric parameter is unboxed. This makes generic Box<T> (Dart, Java, Kotlin, C#, TypeScript) work.
    • More boxed-operand operations. A var whose initializer is a boxed-operand expression (e.g. var s = a + b where a/b are Object[]/List<Object> elements) is refined to the result's concrete type, and the == 0 fast path (i64.eqz) unboxes a boxed operand first. Fixes the Java Class example and the JavaScript try/catch example.
    Open source →
  59. 0.1.45 27 Jun 2026
    Release notes

    Wasm backend: collection-to-String + dynamic arithmetic on boxed values

    • Map/ListString coercion in print(...) / string interpolation (e.g. print('Map: $m'), '$list'). Renders Dart's {k: v, …} / [e, …] form by scanning the runtime key/value (or element) buffers and coercing each entry through the existing string-coercion path. (Nested collections inside a Map/List toString still throw a clear UnimplementedError.)
    • Arithmetic and comparison on boxed Object/dynamic operands, such as values read from a List<Object> (args[1] + 5, args[2] ~/ 2, args[3] * 3, c > 120). These carry a box pointer, not a number; they are now unboxed to a concrete numeric value (dispatching on the runtime box tag: int→i64, double→f64) before the operation, instead of feeding the pointer into i64.add/f64.div (which produced invalid Wasm).
    • A boxed Object value flowing into a typed numeric Map/List slot (e.g. <String,int>{'a': a} where a is dynamic) is unboxed to match the slot's i64/f64 width.

    Wasm backend: anonymous functions assigned to a var and called directly

    • Lambdas stored in a var and invoked by name now compile (e.g. var twice = (int n) => n * 2; … twice(x)). The return type is inferred from the body when no typed call context provides it, and the variable adopts the closure's concrete function signature so the call resolves.
    • Fixed a latent bug where anonymous functions were exported with an empty name; two closures then collided on the same "" export name, producing an invalid module. Anonymous functions are internal (table-dispatched) and are no longer exported.
    • Optimization: a capture-free closure assigned to a var that is only ever called (never used as a value, reassigned, or captured) is lowered to a direct call — no environment heap allocation, no call_indirect, and the function-table / element sections are omitted entirely. Closures used as first-class values or that capture variables keep the environment + table path.
    Open source →
  60. 0.1.44 27 Jun 2026
    Release notes

    Wasm backend: rich-enum field/method reads in a print context

    • Fixed garbage values when a rich-enum instance field or method result was passed through print(...) / string interpolation (e.g. print(p.gravity) or print('${p.mult(2)}')). The lazily-generated enum-entry initializer baked its constructor call index during an early discovery pass, before the print/double_to_str host imports were registered; those imports then shifted every function index, so the cached call landed on a host import instead of the enum constructor. Enum-entry initializer bodies are now generated lazily (after the import count is final), so the call indices are correct. Reading the same field/method outside a print (e.g. return p.gravity) was already correct.
    Open source →

Every package, every release, already written down.

The archive is open and free. Watching your own project is what we are building next.

Browse the archive