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 2026Releases
latest 60 of 158-
2.30.017 Aug 2026Release notes
Open source →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/latemodifier 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
forinitializer still takes a single declarator: aforheader 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
Release notes
Open source →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/latemodifier 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.56A
forinitializer still takes a single declarator: aforheader has no place for the extra declarations the expansion produces, and the comma-separated form does not exist in every target language.Release notes
Open source →v2.30.0 - Multiple variables per Dart declaration Latest
Latest
Compare
Choose a tag to compare
-
2.29.017 Aug 2026Release notes
Open source →intanddoublenow expose the samenuminterfaceEach primitive only carried the conversion that changed its type:
inthad
toDouble()anddoublehadtoInt(), so the identity half of the pair was
missing on both.3.toInt()and1.5.toDouble()— both valid Dart, since
num.toInt()/num.toDouble()returnthis— failed with
Bad state: Can't find core function: int.toInt(...).The same gap covered the rest of the
numinterface onint, which had none
of the members that onlydoublehad been given:on intbefore 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 nonumcore
class, sonum n = ...dispatches on the runtime value's class — meaning
n.toInt()worked or failed depending on whethernhappened to hold a
doubleor anint. 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 onnum,intor
doublein Dart.Release notes
Open source →intanddoublenow expose the samenuminterfaceEach primitive only carried the conversion that changed its type:
inthadtoDouble()anddoublehadtoInt(), so the identity half of the pair was missing on both.3.toInt()and1.5.toDouble()— both valid Dart, sincenum.toInt()/num.toDouble()returnthis— failed withBad state: Can't find core function: int.toInt(...).The same gap covered the rest of the
numinterface onint, which had none of the members that onlydoublehad been given:on intbefore 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 nonumcore class, sonum n = ...dispatches on the runtime value's class — meaningn.toInt()worked or failed depending on whethernhappened to hold adoubleor anint. Both now work either way:num n = 3; // was: Can't find core function: int.toInt n.toInt(); // 3 n.toDouble(); // 3.0toNum()remains unsupported, as it is not a method onnum,intordoublein Dart.Release notes
Open source →v2.29.0 -
intanddoubleshare the samenuminterfaceCompare
Choose a tag to compare
-
2.28.115 Aug 2026Release notes
Open source →Fixed:
compile <file> --target=astsilently compiled to WasmAn option written after the source file was not parsed at all. The
source-file commands shared a parser built withallowTrailingOptions: false,
soapollovm compile foo.dart --target=astleft the flag sitting in the
leftover positional arguments,--targetkept itswasmdefault, 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 owncompile --helpshowed
that exact ordering.Trailing options are now parsed for
compileandtranslate, whose only
positional argument is the source file, so both orderings work.runkeeps
them unparsed, because everything after the file there belongs to the program
being executed and must reach it untouched — including arguments that look
likeapollovm's own flags.A leftover positional argument is now reported rather than ignored:
apollovm compile foo.dart oopsfails withUnexpected argument after the source file: oops.compile: target inferred from the output extension, and-t--outputnow selects the target when--targetis omitted, so naming the
file is enough:apollovm compile foo.dart -o foo.avma # binary AST apollovm compile foo.dart -o foo.wasm # WebAssembly
.avmameans the AST target and.wasmmeans Wasm; any other extension falls
back to thewasmdefault as before. An explicit--targetalways wins, so
-t ast -o out.binstill 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.--targetalso gained the abbreviation-t, on bothcompileand
translate.Release notes
Open source →Fixed:
compile <file> --target=astsilently compiled to WasmAn option written after the source file was not parsed at all. The source-file commands shared a parser built with
allowTrailingOptions: false, soapollovm compile foo.dart --target=astleft the flag sitting in the leftover positional arguments,--targetkept itswasmdefault, 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 owncompile --helpshowed that exact ordering.Trailing options are now parsed for
compileandtranslate, whose only positional argument is the source file, so both orderings work.runkeeps them unparsed, because everything after the file there belongs to the program being executed and must reach it untouched — including arguments that look likeapollovm's own flags.A leftover positional argument is now reported rather than ignored:
apollovm compile foo.dart oopsfails withUnexpected argument after the source file: oops.compile: target inferred from the output extension, and-t--outputnow selects the target when--targetis omitted, so naming the file is enough:apollovm compile foo.dart -o foo.avma # binary AST apollovm compile foo.dart -o foo.wasm # WebAssembly.avmameans the AST target and.wasmmeans Wasm; any other extension falls back to thewasmdefault as before. An explicit--targetalways wins, so-t ast -o out.binstill 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.--targetalso gained the abbreviation-t, on bothcompileandtranslate. -
2.28.015 Aug 2026Release notes
Open source →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
.avmafile: 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, andapollovm runrecognizes an image by its
magic bytes — not its extension — taking the language from the image itself.Everything is
Uint8Listin andUint8Listout, 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 VMint.Loading a decoded unit needs no new path:
ApolloVM.loadCodeUnitonly 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 thethis.field
constructor-parameter promotion are re-established by oneresolveNodecall
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
concreteAST*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;
HmacSha256Signeris built in, which is whycryptobecomes 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
ASTBinaryExceptionnaming 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_serializer1.2.3The 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.readLeb128SignedIntsign-extended from the wrong byte on every
platform, so-2decoded as126and64as-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 anddart2js— but
the remaining three could not be sidestepped.- On the web,
shiftLeftInt/shiftRightIntfell back to a 32-bit<</>>,
so any shift of 32 or more produced0. - 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 about2^shift— the range
DateTime.microsecondsSinceEpochoccupies.
Together those meant an integer literal of 2^32 or more decoded to the wrong
number on the web:4294967296came back as0, and1000000000000000as
2764472320. A test now covers literals from 2^28 upwards, positive and
negative, and it runs under--platform chrome.Release notes
Open source →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
.avmafile: 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 involvedvm.saveAllAST()/vm.loadAllAST()do the same for a whole VM, bundling every loaded code unit into one archive. The CLI gainedapollovm compile --target=ast, andapollovm runrecognizes an image by its magic bytes — not its extension — taking the language from the image itself.Everything is
Uint8Listin andUint8Listout, 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 VMint.Loading a decoded unit needs no new path:
ApolloVM.loadCodeUnitonly 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 thethis.fieldconstructor-parameter promotion are re-established by oneresolveNodecall 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 concreteAST*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;HmacSha256Signeris built in, which is whycryptobecomes 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
ASTBinaryExceptionnaming 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_serializer1.2.3The 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.readLeb128SignedIntsign-extended from the wrong byte on every platform, so-2decoded as126and64as-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 anddart2js— but the remaining three could not be sidestepped.- On the web,
shiftLeftInt/shiftRightIntfell back to a 32-bit<</>>, so any shift of 32 or more produced0. - 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 rangeDateTime.microsecondsSinceEpochoccupies.
Together those meant an integer literal of 2^32 or more decoded to the wrong number on the web:
4294967296came back as0, and1000000000000000as2764472320. A test now covers literals from 2^28 upwards, positive and negative, and it runs under--platform chrome. -
2.27.014 Aug 2026Release notes
Open source →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.0turned it
from a silent misparse (into a method namednamereturning a type namedset)
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
unqualifiedx = vinside 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
produceset x(v) { return …; }, which Dart rejects because a setter returns
void. The generated output is verified with the realdart 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 honestUnsupportedSyntaxErrorJava/C#/JS/TS already produced. They now
refuse, like the others.Fixed:
ASTBlock.setdropped settersIt 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:
staticaccessors, 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
Release notes
Open source →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.0made it a clean parse error instead of a silent misparse into a method namednamereturning a type namedset; 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 unqualifiedx = vinside 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 realdart 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
UnsupportedSyntaxErrorJava/C#/JS/TS already produced. They now refuse, like the others.Fixed:
ASTBlock.setdropped settersIt 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:
staticaccessors, top-level accessors, and an unqualified read of a getter (return value;inside a method —this.valueworks). Kotlin generates getters but not setters. -
2.26.014 Aug 2026Release notes
Open source →Control-flow bodies no longer require braces
for (var e in l) print('- $e');is ordinary Dart, and it did not parse. Only
the plainifaccepted an unbraced single-statement body — every loop, every
if/elseand everyelse ifdemanded{ }.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, theelse ifchain, and the finalelse. A braced
body is still tried first, so nothing about existing sources changes.singleLineStatementwas also far too narrow —return …;or an expression
statement, nothing else — so even theifthat already supported it rejected
if (x) break;,if (x) throw e;and a nestedif. It is now each language's
full statement set minus declarations and bare blocks.A dangling
elsebinds to the nearestif, 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 /do…end.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 firstBaseGrammarLexer.token()is a prefix matcher with no word-boundary guard, so
string('else')matches the start of an identifier likeelseCount. That was
unreachable while everyelsearm 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'swhenentry labels andifexpression.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 namedassert). Each target emits its own idiom: Java
assert c : m;, Pythonassert c, m, Kotlinassert(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.requirednamed parameters on plain, constructor-typed andthis.
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@overridebroke the whole class body — which matters beyond
hand-written sources, sincelib/src/publoads real pub packages. Parsed and
discarded for now. lateon locals and fields (accepted and dropped).constat use sites:const Foo(),const [],const {}.- Arrow and
asyncbodies on local and anonymous functions. catch (e, s)now binds the stack trace — it was parsed and thrown away,
so any handler referencingsfailed.- Compound assignment
%= &= |= ^= <<= >>=. - Untyped getters (
get twice => …) now parse.
Other fixes
- Java and C#:
if (a) {} else if (b) {}with no trailingelsefailed to
parse — both made the finalelsenon-optional, unlike every other language. - Python: a
do/whiletranslated to Python emitted literal
do { … } while (c);. Now lowers towhile True: … if not (c): break. - Lua: compound assignment was emitted verbatim (
a += 1), which Lua does
not have. Now lowers toa = a + 1. const [1]silently misparsed as an index read on a variable namedconst.set value(int v) {}silently became a method namedvaluereturning a
type namedset. Now a clean parse error at theset. Full setter support
remains unimplemented.
Full changelog: https://github.com/ApolloVM/apollovm_dart/blob/v2.26.0/CHANGELOG.md
Release notes
Open source →Control-flow bodies no longer require braces
for (var e in l) print('- $e');is ordinary Dart, and it did not parse. Only the plainifaccepted an unbraced single-statement body — every loop, everyif/elseand everyelse ifdemanded{ }.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, theelse ifchain, and the finalelse. A braced body is still tried first, so nothing about existing sources changes.singleLineStatementwas also far too narrow —return …;or an expression statement, nothing else — so even theifthat already supported it rejectedif (x) break;,if (x) throw e;and a nestedif. It is now the language's full statement set minus declarations and bare blocks.A dangling
elsebinds to the nearestif, 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 everysuite()position:if/elif/else,for,while,try,def,classandcase.Go is deliberately excluded — its spec defines
Block = "{" StatementList "}"and every control-flow statement takes aBlock— and Lua has no such form. A single-statement body translated to either is emitted braced /do…end.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 nowBaseGrammarLexer.token()is a prefix matcher with no word-boundary guard, sostring('else')matches the start of an identifier such aselseCount. That was unreachable while everyelsearm 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 armThe branch and loop rules of all six C-style grammars now use whole-word keyword tokens. Also covers Kotlin's
whenentry labels andifexpression.Other grammar fixes
- Java and C#:
if (a) {} else if (b) {}with no trailingelsefailed to parse — both made the finalelsenon-optional, unlike every other language. - Python: a
do/whiletranslated to Python emitted literaldo { … } while (c);. It now lowers towhile True: … if not (c): break. - Lua: compound assignment was emitted verbatim (
a += 1), which is not valid Lua. It now lowers toa = a + 1. - Go: removed a dead
codeBlockOrSingleLineBlockcluster 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. Rawr'''…'''is unaffected. assert(c)/assert(c, m)as a real statement. It previously parsed as a call to a user function namedassertand failed later with a confusing message. A failed assertion throws and is catchable. Every target emits its own idiom (Javaassert c : m;, Pythonassert c, m, Kotlinassert(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.requirednamed parameters on plain, constructor-typed andthis.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@overridebroke the whole class body; this matters beyond hand-written sources, sincelib/src/publoads real pub packages. Parsed and discarded for now. lateon locals and fields (accepted and dropped).constat use sites:const Foo(),const [],const {}.const [1]used to silently misparse as an index read on a variable namedconst.- Arrow and
asyncbodies 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 referencedsfailed. ApolloVM has no stack traces, so it binds to an empty string; targets whosecatchheader 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 aSyntaxError. - Untyped getters (
get twice => …) now parse. They failed becausetype().optional()greedily ategetand petitparser'soptional()cannot backtrack.
Fixed:
setno longer misparses into a methodsimpleType()accepted any identifier, soset value(int v) {}silently became a method namedvaluereturning a type namedset, failing much later with a confusing error.get/setare now rejected in a type position, turning that into a parse error at theset.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.
Release notes
Open source →v2.26.0 - Control-flow bodies no longer require braces
Compare
Choose a tag to compare
- Interpolation inside triple-quoted strings —
-
2.25.102 Aug 2026Release notes
Open source →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_wasmbump, the way2.23.2carriedapollovm_wasm 1.1.0.apollovm_wasmdeclaredapollovm: ^2.0.0, but it is not a loosely-coupled consumer: it decodes what this package's Wasm generator encodes. The boxed-Objectcell layout and its_boxTag*values are a contract, and bothwasm_runner.dartandwasm_generator.dartcarry 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. Itswasm_runalso moves to^0.2.0+2(patch, no API change). -
2.25.001 Aug 2026Release notes
Open source →Wasm:
?.on a boxed slot no longer refuses to compilevar x = null; x?.lengthwas anUnimplementedError— "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 asvar/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/Maphave 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
nulland 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
unboxedint/bool.Known gap this exposed
An explicitly-declared
Object?local initialized from a concrete value keeps
the initializer's type instead of being boxed, soObject? s = ''makessa
String slot.s?.isEmptythen 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 intest/wasm/apollovm_wasm_boxed_member_test.dart.Full Changelog: v2.24.0...v2.25.0
Release notes
Open source →Wasm:
?.on a boxed slot no longer refuses to compilevar x = null; x?.lengthwas anUnimplementedError— "Wasm getter.lengthon Null is not supported yet". That was reachable from ordinary code, and the advice the neighbouring error gives ("declare the variable asvar/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/.isNotEmptyon a boxed receiver now dispatch on the box tag at runtime:var missing = null; var n = missing?.length; // -> null return n ?? -1; // -> -1Only a boxed String carries these members —
List/Maphave 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'sApolloVMNullPointerException.The null-aware result is itself boxed, because it can be
nulland 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 unboxedint/bool.Known gap this exposed
An explicitly-declared
Object?local initialized from a concrete value keeps the initializer's type instead of being boxed, soObject? s = ''makessa String slot.s?.isEmptythen 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 intest/wasm/apollovm_wasm_boxed_member_test.dart.Release notes
Open source →v2.25.0 - Wasm:
?.on a boxed slot no longer refuses to compileCompare
Choose a tag to compare
-
2.24.001 Aug 2026Release notes
Open source →Null-aware access is now really checked on every target, not dropped
a?.bwas emitted as a plaina.bon 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 emita?.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), soa!is emitted as plainarather than negating the value.Go continues to report
?./?[as unsupported: it represents a nullableT?
as*T, so a degraded access would both skip the nil check and yield the wrong
type.??,&&,||andx == nullare AST nodes of their ownThese 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 reconstructedx != nullto 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
andASTExpressionNullChecknow 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 evaluatingx == nullno 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 returnTrue
forNone;iscannot be intercepted, and is the form PEP 8 mandates.The Python grammar learned
is None/is not Noneto match, so the
generated source still round-trips — ApolloVM can now read back what it writes.
This is deliberately limited to theNonecomparison: generala is bis
identity, and mapping it to==would silently turn it into equality, so it
stays unparsed as before.Building a binary operation directly with
ASTExpressionOperationstill 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
Release notes
Open source →Null-aware access is now really checked on every target, not dropped
a?.bwas emitted as a plaina.bon 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?.bnatively. JavaScript uses?.[i]for element access, and its postfix!is handled separately: JavaScript has no null-assertion operator (a postfix!is logical NOT), soa!is emitted as plainarather than negating the value.Go continues to report
?./?[as unsupported: it represents a nullableT?as*T, so a degraded access would both skip the nil check and yield the wrong type.??,&&,||andx == nullare AST nodes of their ownThese 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 != nullto 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 forcedthrow StateError('unreachable')arms in three places.ASTExpressionNullCoalesce,ASTExpressionLogicalAnd,ASTExpressionLogicalOrandASTExpressionNullChecknow carry those shapes, built by the newastExpressionOperation()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 evaluatingx == nullno 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 returnTrueforNone;iscannot be intercepted, and is the form PEP 8 mandates.The Python grammar learned
is None/is not Noneto match, so the generated source still round-trips — ApolloVM can now read back what it writes. This is deliberately limited to theNonecomparison: generala is bis identity, and mapping it to==would silently turn it into equality, so it stays unparsed as before.Building a binary operation directly with
ASTExpressionOperationstill 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.Release notes
Open source →v2.24.0 - Null-aware access is now really checked on every target, not dropped
Compare
Choose a tag to compare
-
2.23.327 Jul 2026Release notes
Open source →A signature mismatch is no longer reported as a missing entry function
apollovm.executereturned 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.executenow 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
classNamegiven) — the case where the caller cannot otherwise tell which class answered.An explicit
classNamethat 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 reportsEntry 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
Release notes
Open source →A signature mismatch is no longer reported as a missing entry function
Calling
apollovm.executewith 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.executenow 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
classNamethat does not exist now reportsEntry class not found: Bar (looking for the methodrun)instead of blaming the method. A name that is genuinely absent from the source still reportsEntry function not found.Release notes
Open source →v2.23.3 - Signature mismatch is no longer reported as a missing entry function
Compare
Choose a tag to compare
-
2.23.226 Jul 2026Release notes
Open source →apollovm_wasm1.1.0 —wasm_run^0.2.0+1wasm_run0.2.0 is a breaking release. It is absorbed insideapollovm_wasm, so consumers keep the sameWasmRuntimeIOAPI and gain one thing: there is no install step any more.dart run wasm_run:setupis gone. The SDK's build hooks download the native library into.dart_tool/lib/duringdart 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/(thewasm_run:build_binariesoutput).Three further breaks needed handling:
- The bindings moved to
flutter_rust_bridge2.x, so the symbol that identifies a genuinewasm_runlibrary changed:wire_compile_wasm→frb_get_rust_content_hash. Validating the old one rejected every 0.2 library. WasmRunLibrary.isReachable()becameFuture<bool>.wasm_run0.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()andisSupportedstay synchronous, asWasmRuntimerequires, and only probe for the library.
WASM_RUN_DART_DYNAMIC_LIBRARY(the variablewasm_runitself reads) now overrides the library path. The olderWASM_RUN_LIB_PATHis 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_wasmnow requires Dart >= 3.10, matchingwasm_run0.2.apollovm2.23.2Nothing changes for
package:apollovmitself — 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 andWASM_BACKEND_PLAN.md. CI drops its threewasm_run:setupsteps.Known issue: macOS on Apple Silicon
The upstream
aarch64-apple-darwin0.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. underdart runanddart test. Compiling and instantiating modules is fine; only the call into generated code trips it, andwasm_run0.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
Release notes
Open source →The native Wasm runtime no longer has an install step
apollovm_wasm1.1.0 moves towasm_run0.2, which droppeddart run wasm_run:setupin favor of the SDK's build hooks: the native library is downloaded automatically bydart run,dart testanddart compile.mcp doctorand the Wasm test suite said otherwise, so they now just point atpackage:apollovm_wasm.Nothing changes for
package:apollovmitself — it still compiles Wasm everywhere and pulls in no native toolchain. See theapollovm_wasmchangelog for the details, including a macOS/Apple-Silicon issue in the upstream 0.2.0 binary.Release notes
Open source →v2.23.2 - apollovm_wasm 1.1.0: wasm_run 0.2, no install step
Compare
Choose a tag to compare
- The bindings moved to
-
2.23.125 Jul 2026Release notes
Open source →A blank
className/functionno longer breaks executionA 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.executenow normalizes both entry names: they are trimmed, and a name that is empty after trimming means "not specified" —classNamefalls back to the full discovery order (top-level function, then any class method) andfunctionfalls back tomain. Trimming also makes" main "resolve, which previously did not. A name that is genuinely absent from the source still reportsEntry 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.getClassandApolloRunner.getClassMethodreturn early for a blank class name.Dart tooling ignores the LSP fixtures
lsp/example_workspace/broken.dartis intentionally unparseable (it is what makes the language server emit a parse diagnostic), which madedart format .fail with exit 65 and made the analyzer report it whenever it was opened in an editor. The rootanalyzer: exclude:did not cover either case, and the formatter has no exclude option at all.The fixture is now
lsp/example_workspace/.broken.dart: bothdart formatanddart analyzeskip dot-prefixed paths during a directory walk, while the editor still sees a.dartfile and hands it to the ApolloVM language server, so the demo is unchanged. A localanalysis_options.yamlsilences the remaining fixture diagnostics. No published code is affected —lsp/is.pubignored. -
2.23.025 Jul 2026Release notes
Open source →nullSafetyChecksis reachable from the CLI and MCP2.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-safetyonrun,translateandcompile(added once on the sharedCommandSourceFileBase, 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 $? 1The 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.
mainnow maps a command result offalseto exit status 1, so a check can gate a build; no existing command path returnsfalse.MCP — every source tool takes an optional
nullSafetyargument, and--null-safetyon bothapollovm mcp serveandapollovm mcp callsets the default (a per-call value always wins). The two tool kinds behave differently, by design:- the tools that load —
apollovm.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 todiagnosticsand 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:
_IsolateJobcarries only the arguments and limits, so a field on the server object would never reach a spawned isolate — andapollovm.executeruns in one by default. It is deliberately not stored inMcpLimits, which is documented as resource and security limits.Adds
mcpCoerceBoolbesidemcpCoerceInt, so a client sending"true"or1is 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
Analyzeruses its VM only forgetParserand never loads a code unit, and it already reports null-safety findings as diagnostics. An editor must report, not refuse to open a file. - the tools that load —
-
2.22.025 Jul 2026Release notes
Open source →The null-safety analyzer had never seen a class method
NullSafetyAnalyzer.analyzewalkedroot.descendantChildrenlooking for invocables. ButASTBlock.childrenis only[functions, statements], andASTRootkeeps its classes in a separate map — so the traversal never entered a class body.ASTClasscompounds it by keeping constructors and getters outsidechildrentoo.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 } }analyzenow walks each class and extension explicitly, plus their constructors and getters. The reported case producesunchecked-nullable-operandwith no rule changes — the rules were fine, nothing was reaching them.Opt-in: fail the load instead of failing mid-run
ApolloVM(nullSafetyChecks: true)makesloadCodeUnitthrow the newNullSafetyErrorwhen the AST has null-safety errors, before the unit is registered:var vm = ApolloVM(nullSafetyChecks: true); await vm.loadCodeUnit(unit); // throws — nothing printed, nothing executedWithout it, the snippet above prints
5andnulland then throwsApolloVMNullPointerExceptionfrom the+, having already produced output.- Off by default, so existing behaviour is unchanged unless you opt in.
- Only
NullSafetySeverity.errorblocks; 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
NullSafetyErrorcarries the offendingfindings.
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. -
2.21.025 Jul 2026Release notes
Open source →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 callxs?.get(0). The sharednullAwareIndexOpenhook gained a matchingnullAwareIndexClose, so a target can close with)instead of]. - Lua's null literal was
null. Lua's isnil, so the generated code referenced an undefined global:a == nullwas 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,== nulland 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?.x→a.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, thenullrow in the operators table now points at the Wasm limits, and the Wasm status paragraph mentions the boxed-nulldomain. - Kotlin's null-aware index was
-
2.20.025 Jul 2026Release notes
Open source →Go: a nullable
T?is generated as a pointer*TGo 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 tonil. 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 holdnil:- Declarations and parameters carry the pointer type —
int? aisa *int, aString? namefield isname *string. - Reads deref (
(*a)), including a barereturn x, which takes its own generation path. - Null checks compare the pointer —
x == nullisx == nil, not a deref. - A non-null value takes its address through a generated
func goPtr[T any](v T) *Thelper, since Go cannot write&5. The helper is emitted only in modules that need it. a ?? blowers 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 letvar s string = nilship 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 plainT. Against a*Tit 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 tot = t ?? vneeds 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.
- Declarations and parameters carry the pointer type —
-
2.19.025 Jul 2026Release notes
Open source →x == nullno longer throwsx == 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 castNo ternary, nullable slot or
?.was needed;String s = 'x'; s == nullfailed the same way. Only the reversednull == xworked, becauseASTValueNull.equalstype-tests instead of casting.ASTValue.equalsread the other operand through_getValue, which casts it to this value'sT. That is right for arithmetic — a mismatched operand there is a real error — but wrong for equality, where a different type must comparefalse. Equality now reads both operands uncast, sox == nullisfalse,x == 'other type'isfalse, and neither throws. The threeequalsoverrides (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 madex == nullthe idiom people reach for, so it went from obscure to prominent.Wasm:
== null/!= nullagainst the null boxWith
nullrepresentable since 2.18.0, the equality paths had to learn about it. A comparison against anullliteral is now recognised before the String and numeric paths:- a boxed operand compares its pointer against the null box, so
a[0] == nullon aList<Object>answers correctly (it previously took the__streqroute 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 == nullpreviously pushed two i32 handles into ani64.eqand produced a module that failed to validate.
Grammar:
(expr).m().fieldA group invocation followed by member access did not parse — the group-invocation rule chains only further invocations, so a trailing
.fieldhad 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 = nilThe Go generator emitted
var s string = nilfor a nullable local — source that does not compile. It now reports anUnsupportedSyntaxErrornaming the type, consistent with how??is already handled. AList/Mapstill acceptsnil, since those become a Go slice/map, which are nilable.Go's nullable representation remains the open item: supporting
T?properly means generating*Tthroughout — declarations, zero values, every dereference, and parameter/return types. - a boxed operand compares its pointer against the null box, so
-
2.18.024 Jul 2026Release notes
Open source →Wasm:
nullin the boxed-ObjectdomainCompiling a
nullliteral to Wasm threw a bareUnimplementedError: generateASTExpressionNullValue— a leftoverTODO— so an ordinary Dart idiom such asvar a = args.length > 0 ? args[0] : null;could not be compiled at all.nullis now a real value in the backend's boxed domain: the boxed-Objectpointer 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
nullforces that type to a boxedObject— otherwisec ? 1 : nullmixed 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, soa ?? 99falls back whenais null.- String interpolation prints
null. The box-to-string helper checks the null box before dereferencing a tag. __alloclook-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]anda ?? 99aborted at run time withNo exported Wasm function __alloc. The generator now scans function bodies for anullliteral up front.- An
Object?return decodes tonullinstead of the raw pointer0. Two causes: the return path had noObjectcase, and_typeTag's fallback gave tag5to bothObjectand a class instance, so the runner could not tell a box from a bare instance pointer. A class instance now carries tag8, leaving5unambiguously "boxed value".
Where
nullgenuinely has no representation — a slot whose Wasm type is concrete, such asint(i64) orString(a string pointer) — the compiler now reports anUnsupportedSyntaxErrornaming the type and suggestingvar/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
nullliteral assigned to a non-nullable slot. Two adjacent mistakes went unreported:- A nullable operand in an operation (
x + (y ?? 0)wherexisint?) now reportsunchecked-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;whereaisint?) now reportsnullable-to-non-nullable. Previously only a literalnullwas caught, so the same error one step removed passed silently.
- Ternary arms unify. Both arms of a conditional are now coerced to the
block's result type, and an arm that is
-
2.17.024 Jul 2026Release notes
Open source →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 aTargument. Calling a method with aString?parameter and a non-nullStringfailed withparameters signature not compatible, though passingnullworked. Argument passing is an assignment, soASTFunctionParameters.parameterAcceptsTypenow usesASTType.acceptsAssignmentinstead ofacceptsType. (int?/double?only appeared to work becauseStrictTypeignoresnullablein==, whileASTTypeStringcompares it — soString?andStringwere unequal andASTTypeString.acceptsTyperejected the argument.) A non-nullable parameter still rejectsnull. - A null-aware access now reports a nullable static type. Storing a
short-circuited result in a local failed:
var v = s?.lengthon anullreceiver threwClass not set for type: Null, andvar v = xs?[0]threwCan't cast initial (null) value to type: int.?.(getter and method) and?[now resolve toT?, and resolving the type of an access whose receiver isnullreportsNullinstead of trying to find a class for it. Using the result in areturn, 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, soa.next?.valuereportedSyntaxError: digit expected(the?was read as the start of a number) and even plaina.next.valuereported"(" expected. A new chain rule folds each segment onto the previous one, wrapping it in the newASTExpressionVariable(anASTVariablebacked 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().fieldremains unsupported. ??and??=no longer leak into targets that cannot compile them. Java, Lua, Python and Go emitteda ?? bverbatim, and??=leaked into every target — including Kotlin, which had a correct?:for??but no hook for the assignment form.??now goes through an overridablerenderNullCoalesce, and??=is lowered tot = t ?? vwhereversupportsNullCoalesceAssignmentis false, so each target defines only one desugaring:- Java:
(a != null ? a : b) - Python:
(a if a is not None else b)— anis not Nonetest, so0/''/Falseare preserved - Lua: an immediately-invoked function with an explicit
niltest, because botha or band thea ~= nil and a or bidiom returnbfor a non-nilfalse; it also bindsato a local, so it is evaluated once - Kotlin:
?:for??, andt = t ?: vfor??= - 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 nullableT?onto a plain GoT, which for a value type cannot be compared tonil— any rendering would be code that does not compile. RepresentingT?as*Tthroughout the Go generator is separate work.
- Java:
Also adds an overridable
resolveASTAssignmentOperatorText, which lets the Python generator drop its wholegenerateASTExpressionVariableAssignmentoverride (it existed only to spell integer division//=). - A
-
2.16.024 Jul 2026Release notes
Open source →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)).ASTTypecarries anisNullable/nullableflag (viaasNullable()/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 — standalonex!and before access:x!.f,x!.m(),x![i]— throwingApolloVMNullPointerExceptionon 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) acceptsnullonly for nullable slots and lets aT?slot accept aTvalue. - Static null-safety analysis pass. A new pragmatic, flow-aware analyzer
(
NullSafetyAnalyzer) reports assigningnullto a non-nullable declaration/parameter and unconditional member/method/index access on a nullable local, with flow promotion forif (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 tox,a ?? bto its left operand,?./?[to plain access, and a nullableT?to the underlying numeric type. Anullliteral stays an explicit unsupported-construct error (Wasm has no null value) rather than a silent miscompilation.
LSP & MCP correctness fixes
- LSP
textDocument/referencesnow honorscontext.includeDeclaration. The flag was plumbed through every layer but discarded, so the declaration occurrence was always returned. WhenincludeDeclarationisfalse, the occurrence that coincides with the symbol's declaration is now excluded. - LSP field/variable
documentSymbolrange 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, sotimeoutMs/maxDepth/args/classNamewere coerced via newmcpCoerceInt/_strOrNull/_listOrEmptyhelpers rather than an unsafeas int?/as List?that threw a rawTypeErrorand crashed the tool. Applies toapollovm.execute/apollovm.astand both isolate executors. - MCP HTTP/SSE transport answers CORS preflight.
HttpSseTransportnow responds toOPTIONSwithAccess-Control-Allow-Origin/Methods/Headers(204) instead of a404, unblocking cross-origin browser POSTs.
Go & Lua generator correctness fixes
- Go
&^(AND NOT / bit clear) now computesa & (~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 staticallyString-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 generateslist[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.
- Nullable type syntax (
-
2.15.019 Jul 2026Nothing published for this version
-
2.14.019 Jul 2026Release notes
Open source →Wasm:
String ==/String !=content equalityThe on-the-fly WebAssembly compiler now compiles
String == StringandString != Stringto content equality via the__streqsynth helper (the same byte-comparison already used forswitchcases andMap<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 ani64.eq, produced invalid Wasm. The result is a properbool, so it can be returned directly, used as anif/&&/||condition, or otherwise combined logically.!=inverts the helper's result withi32.eqz. Covers literal/variable operands and the empty string. -
2.13.019 Jul 2026Release notes
Open source →Wasm: generic-class fields (
Box<T>) + aggregate-return coverageThe 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 anObject → String/bool/ instance unbox case alongside the existing numeric one. CoversBox<int|double|String|bool>, generic fields in arithmetic, and multi-parameter classes such asPair<int, String>.Also adds regression tests confirming that returning a
List/Mapacross the module boundary works (List<int|double|String>via literal, arrow, or built-with-.add, andMap<String,int>).With this, every lettered gap in
WASM_BACKEND_PLAN.md(A–G) is closed. -
2.12.018 Jul 2026Release notes
Open source →String index
s[i](interpreter + Wasm)Adds
String[i]— Dart's index operator, which returns the character atias 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, wheres[i]had been mistaken for invalid Dart; it is valid, so it is now supported end to end. -
2.11.018 Jul 2026Release notes
Open source →Wasm: String
splitThe on-the-fly WebAssembly compiler now supports
String.split(sep), returning aList<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 emptysepyields 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/Mapacross the module boundary. -
2.10.018 Jul 2026Release notes
Open source →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 aList) and index[]. -
2.9.018 Jul 2026Release notes
Open source →Wasm: String
replaceAll/replaceFirstThe on-the-fly WebAssembly compiler now supports
replaceAll(from, to)andreplaceFirst(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 (copyingtofor a match, else one byte). Handles a replacement that grows, shrinks, or removes the match, and matches at either end; an emptyfromreturns a copy (avoiding a non-terminating scan).Still to come for String:
split(returns aList), index[], andcompareTo(which also needsString.compareToin the interpreter core first). -
2.8.018 Jul 2026Release notes
Open source →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) andpadLeft(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[], andcompareTo(which also needsString.compareToin the interpreter core first). -
2.7.018 Jul 2026Release notes
Open source →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-buffermemory.copyslice),codeUnitAt(i),startsWith/endsWith,indexOf, andcontains(byte scans, guarded against out-of-bounds reads). These join the already-supportedlength/isEmpty/isNotEmptygetters andtoUpperCase/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). -
2.6.018 Jul 2026Release notes
Open source →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/Mapliteral can holdList/Mapelements ([[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. -
2.5.018 Jul 2026Release notes
Open source →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. Coversint/double/bool/Stringgetters, 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 (
xresolving tothis.x, no receiver) and setters remain follow-ups (bare access is not resolved by the interpreter yet either). -
2.4.018 Jul 2026Release notes
Open source →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
extendschain (an override on the subclass wins, otherwise the inherited superclass method), the subclass constructor runs inherited field initializers, andsuper.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 (includingdouble), own+inherited fields at distinct offsets, override-wins,super.method()/super.method(args), and multi-levelextendschains.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). -
2.3.018 Jul 2026Release notes
Open source →Wasm:
staticclass fieldsThe on-the-fly WebAssembly compiler now supports
staticclass 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 literalint/double/boolinitializer; a bare reference inside astaticmethod reads and writes it (including compound assignment likec += 1), and values persist across calls. QualifiedClass.fieldfrom another class, inherited static fields, and non-literal initializers remain follow-ups. -
2.2.018 Jul 2026Release notes
Open source →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 functionalextendswas parsed but had no runtime effect — inherited methods and fields were invisible andsuperwas 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
staticfields read/write the base class that declares them (Sub.staticField). superdispatches 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.getterreads the parent getter, andsuper.fieldreads/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'sclass 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 athis.param. - Inherited methods resolve through the superclass chain, for both bare calls
(
-
2.1.118 Jul 2026Release notes
Open source →staticclass fields are now supportedstaticfields previously had no class-level storage — they were initialized onto every instance, and reading one (ClassName.field, or a bare reference inside astaticmethod) threw at runtime. Now each class has a lazily initialized static-field store, shared by qualifiedClassName.fieldaccess and bare references inside the class's ownstaticmethods, for read, write and compound assignment. The Java and C# grammars also now record thestaticfield 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 ofloadCodeUnitinstead of building the AST.~/=///=are now fully supported, and any other still-unsupported compound operator (e.g. JavaScript/TypeScript%=) now surfaces as a cleanSyntaxErrorinstead of an uncaught crash. Enum.valuescan be assigned to a typed or inferred list. It was built with adynamicelement type while its declared type isList<Enum>, sovar 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 cleanApolloVMCastExceptioninstead of a raw DartTypeError.
Division semantics are unchanged and remain intentionally per-language (Dart/JS/TS/Python
/yields adouble; Java/C#/Go/Kotlin/on ints is truncating integer division); regression guards now pin both. - Compound assignment
-
2.1.017 Jul 2026Release notes
Open source →Wasm backend — loop increment fix and initial String methods
- Fixed: a
++/--statement inside awhile/do-whilebody 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 explicitlyint-typed counters written asi = i + 1happened to avoid it. The discarded value is now dropped in statement position; the expression form (x = i++) still yields its value, andfor-header updates are unchanged. - New:
String.length,.isEmpty,.isNotEmptycompile 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.mdfor 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#).
- Fixed: a
-
2.0.117 Jul 2026Release notes
Open source →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 onlyearth, 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
.namedconstructor,<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 phantomradiusenum 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. - An enum constant's range now covers the whole entry: a
-
2.0.013 Jul 2026Release notes
Open source →apollovmno longer drags a native/FFI toolchain into every consumerExecuting a compiled Wasm module on the Dart VM needs a native engine (
package:wasm_run), which brings an FFI/Rust toolchain and a long-abandonedflutter_rust_bridge1.x with it. That dependency was paid by every consumer ofapollovm— including the many that only parse, translate or generate code and never execute Wasm at all. Worse,flutter_rust_bridge1.x pinsshelf_web_socket ^1.0.2andweb_socket_channel ^2.2.0, so anything depending onapollovm(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_runis 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_wasmand 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 throughWasmRuntime.registerProvider(), whichapollovm_wasmcalls.
Added
WasmRuntime.registerProvider()— installs the implementation thatWasmRuntime()instantiates, so an engine can be supplied from outside the package.
-
-
1.10.011 Jul 2026Release notes
Open source →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, butstar()treated that as "stop, success" and discarded the failure, soend()reported a genericend of input expectedat offset 0 (line 1) — technically correct, useless in an editor. petitparser's plainparse()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'stransformParser+callCC) in which every parser records eachFailureit produces if it is the deepest seen. This captures failures thatstar()/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
locateParseErrornow 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.
- New opt-in
-
1.9.210 Jul 2026Release notes
Open source →Go — a class with a field-initializing constructor now survives Dart -> Go
Three defects made
Dart -> Go -> parse -> runfail (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 generatedfunc 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 emitso.x = xfor eachthis.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 namedtypebecamemap := .../o.type, which does not parse —mapandtypeare 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.generateASTRootnow 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 declaringtype Point structstill fails to resolve, though Go itself is order-independent. The generator no longer produces that shape. - A field-initializing constructor parameter was dropped. Dart's
-
1.9.110 Jul 2026Release notes
Open source →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.isNotEmptyand thesignofint/doubleonly worked when called.s.length()worked ands.lengththrew — the opposite of how Dart, Kotlin and C# source reads.ListandMapalready exposed theirs as getters. They are now getters and methods, so Java-styles.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 anASTTypeand threw a rawTypeError. 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 afunc 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,
owas a plain local, soo.x = xfailed 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; }emittedo.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 callp := 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 thego_*.test.xmlfixtures.
Dart -> Go -> parse -> run now works end to end.
Cleanup
ApolloGeneratorcarried ~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 basegenerateASTRootnever 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 everyString,int,double,ListandMapmember, 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 + bfor string concatenation ands.m()for method calls, where Lua wantsa .. bands: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 intest/unit/apollovm_generator_matrix_test.dart. -
1.9.010 Jul 2026Release notes
Open source →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 { … }andval 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 andthisare 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 plaindoubled()). A class member always wins over an extension member. Extensions are module-local: they are not carried acrossimport. Java, JavaScript, TypeScript, Python, Go and Lua have no equivalent construct, so asking for one throwsUnsupportedSyntaxErrorinstead of emitting a shim that would mean something else.Supporting changes:
- Dart now parses instance getters (
int get x { … }/=> …) in class bodies too; theASTClassGetterDeclarationnode existed but no grammar reached it. - New
ASTExtensionnode andASTRoot.extensionsregistry, 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 ASTparentNodechain, so they could not resolve anything reached through it. - Extensions appear in LSP document symbols and in the MCP AST serialization.
- MCP: an
ASTGetterDeclarationnow serializes its name, return type and modifiers instead of appearing as an anonymous node.
- Dart:
-
1.8.007 Jul 2026Release notes
Open source →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, inpackage:apollovm/apollovm_repository.dart): aRepositoryAdapterthat talks to a repository server over HTTP, implementing the full interface (filesystem, search, git). Connect withRemoteRepositoryAdapter.connect('http://host:port'); it reportsRepoCapabilities.isRemote. Backed bypackage:http, so it runs in the browser. - New
RepositoryRpc(web-safe): a transport-agnostic JSON dispatcher that maps{op, ...args}requests to aRepositoryService. A sharedOpconstant set is the single source of truth for the wire contract, so server and client can't drift. fromJsonon every repository value type (RepoFile,RepoEntry,RepoStat,RepoEdit,TextMatch,RepoCapabilities, and the git types), completing the JSON round-trip alongside the existingtoJson.- New
tool/repository_server.dart: a lightweight HTTP server exposing a local checkout (viaLocalRepositoryAdapter) overPOST /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.
- New
-
1.7.007 Jul 2026Release notes
Open source →Repository features — read, search, navigate, edit and version-control a codebase
- New standalone libraries
package:apollovm/apollovm_repository.dart(web-safe) andapollovm_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 viaRepositoryService, a typed façade returning typed results (no MCP/JSON). It bundles the filesystem/search/ git operations of a pluggableRepositoryAdapterwith language-aware code navigation (outline/definition/references/hover/diagnostics/workspaceSymbols/searchSymbols) powered by ApolloVM's parsers via an in-processLspService. - Pluggable backend.
RepositoryAdapterimplementations:LocalRepositoryAdapter(dart:io+git),InMemoryRepositoryAdapter(web-safe), plus room for a remote/web backend — enabling file edits and git commands from the browser. APermissionGuarddecorator enforces aRepoConfiguniformly across backends.
MCP — the same features as agent tools
- New
apollovm.fs.*,apollovm.search.*,apollovm.code.*andapollovm.git.*tools are a thin JSON layer overRepositoryService, 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 aRepositoryAdaptertoApolloMcpServer/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),gitis invoked with an argument list (never a shell) pinned to the root, andfs.editaccepts anatLinesafety 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 theapollovm mcp list/schema/infoCLI advertise them only with--workspace.mcp callinvokes them via--json-args+--workspace.
- New standalone libraries
-
1.6.307 Jul 2026Release notes
Open source →Language Server — body-less constructor/method ranges
- A member with no
{ ... }body is now fully covered by itsdocumentSymbolrange. A;-terminated constructor (const Foo(this.x);, a redirectingFoo.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.
- A member with no
-
1.6.206 Jul 2026Release notes
Open source →Language Server — expression-body member ranges
- Members with an expression body are now fully covered by their
documentSymbolrange. A=> expr;member (Dart/C#) previously stopped at its name, and a brace-less= exprmember (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.
- Members with an expression body are now fully covered by their
-
1.6.106 Jul 2026Release notes
Open source →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 (namedint,const,final, …), sodocumentSymbolgave 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.
- Enum members declared after the constant list are now recognized as real
members. Everything after the
-
1.6.006 Jul 2026Release notes
Open source →Language Server — member-aware completion and full-body symbol ranges
documentSymbolranges now span a member's whole body, not just its signature. Previously only class/enum declarations had theirrangeextended 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 extendsfullEndto the body's closing}. Bodyless members (abstract/;-terminated) are left unchanged. The narrowerselectionRange(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.
-
1.5.006 Jul 2026Release notes
Open source →Language Server — better completion and parse-error locations
- Completion now surfaces in-scope identifiers, and works while the buffer does
not parse. Previously
textDocument/completiononly 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 offset0for 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 anexpected ';'hint) instead of defaulting to the top of the file. The heuristic is conservative — it skips continuation shapes (operators,.,?,:,,, open brackets, continuation keywords likereturn, and annotations) and does not apply to languages where;is optional/absent (Kotlin, JavaScript, Lua, Python).
- Completion now surfaces in-scope identifiers, and works while the buffer does
not parse. Previously
-
1.4.206 Jul 2026Release notes
Open source →- 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.
- Docs: fix the ApolloVM Web Demo link — the live playground is served at the
site root (
-
1.4.106 Jul 2026Release notes
Open source →MCP — web compatibility
package:apollovm/apollovm_mcp.dartis now fully web-safe — it imports nodart:ioand nodart:isolate, so the MCP server and everyapollovm.*/apollovm.lsp.*tool compile and run in a browser (dart2js / DDC). ConstructApolloMcpServeron anyStreamChannel<String>(e.g. a webMessageChannel) and drive all tools in-process. (In 1.4.0 theapollovm.lsp.*tools were documented as web-safe, butapollovm_mcp.dartstill transitively importeddart:io/dart:isolate, so a web build failed to compile.)- The
dart:io-only pieces moved to a newpackage:apollovm/apollovm_mcp_io.dart(which re-exportsapollovm_mcp.dart):serveStdio,HttpSseTransportand theCommandMcpCLI group. Native embedders and theapollovmCLI import this. Migration: if you importedserveStdio/HttpSseTransport/CommandMcpfromapollovm_mcp.dart, switch that import toapollovm_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 (nodart:isolate). Tools inMcpLimits.isolateToolstherefore still run, degrading to a soft timeout on the web. - New cross-platform test
test/mcp/web_compat_test.dartruns the tools, the in-processLspClientand the server underdart test --platform chrome, guarding the web-safe surface against futuredart:io/dart:isolateleaks.
-
1.4.005 Jul 2026Release notes
Open source →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, nodart:io), runs in-process or inside an isolate like the other bounded tools, and honors the existingmaxSourceCharslimit. The parser is selected from thelanguageargument, so a mismatched URI cannot change it. - New public API in
package:apollovm/apollovm_mcp.dart:LspRuntime,buildLspTools,computeLspTool,isLspTool,lspToolNames. The CLIapollovm mcp callgained--line,--characterand--queryflags. - New example
example/apollovm_example_mcp_lsp.dart.
Language Server Protocol — in-process API (no socket)
- New
LspServiceinpackage:apollovm/apollovm_lsp.dart: a document-oriented facade that embeds an in-processLspServerand exposes the language features as plain typed Dart calls — no transport, no socket and noinitialize/initializedhandshake to run by hand. Beingdart: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/closemanage documents (versions tracked automatically); every query resolves against the current buffer. - Typed helpers:
hover,definition,documentSymbols,completion,references,documentHighlight,prepareRename,rename,workspaceSymbols, plus adiagnosticsstream and aready(InitializeResult) future. LspService.wrap(client)layers the same convenience over an existingLspClient(e.g. one connected to a remote server).
- One-shot
- New example
example/apollovm_example_lsp_api.dartdrives the whole flow throughLspServicewith no transport wiring.
- The MCP server now exposes ApolloVM's LSP features as
-
1.3.005 Jul 2026Release notes
Open source →Language Server Protocol — new server features
textDocument/documentHighlight— highlights every occurrence of the identifier under the cursor, marking the declaration site asWriteand other uses asRead(documentHighlightProvidercapability).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'sprepareProvider).
Language Server Protocol — client
- New
LspClientinpackage:apollovm/apollovm_lsp.dart: a JSON-RPC client that consumes the ApolloVMLspServer. 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-levelsendRequest/sendNotification. LspClient.inProcess()pairs a client with a freshLspServerover a linkedMessageLspEndpointpair, all in one isolate — no subprocess, nodart:io. The client works over anyLspEndpoint, so an out-of-process server can be driven withLspClient(StreamLspEndpoint(out, in)).LspEndpointnow routes JSON-RPC responses (previously ignored) to a newonResponsehook, enabling the client role. Pure-server usage is unchanged.- Protocol data types gained
fromJsonfactories (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.dartdrives a full session (handshake, diagnostics, symbols, hover, definition, completion, highlight, prepare-rename, rename) throughLspClient.inProcess().
-
1.2.103 Jul 2026Release notes
Open source →- Fix broken 1.2.0 package on pub.dev: the
.pubignorepatternlsp/was unanchored, so besides the intended rootlsp/dev-tooling directory it also strippedlib/src/lsp/(the LSP implementation) from the published archive, makingdart pub global activate apollovmfail to compile. The pattern is now anchored as/lsp/. .pubignorenow also repeats the relevant.gitignoreexclusions (*.exe,*.iml, etc.), since a.pubignorefile replaces.gitignorefor publishing — 1.2.0 accidentally shipped a 7 MBbin/apollovm.exe.
- Fix broken 1.2.0 package on pub.dev: the
-
1.2.003 Jul 2026Release notes
Open source →Language Server Protocol (LSP 3.17) server
- A Dart-first language server is now part of the
apollovmpackage, exposed as a separate librarypackage:apollovm/apollovm_lsp.dart(the existingpackage:apollovm/apollovm.dartexports are unchanged). Source lives inlib/src/lsp/. - Runnable two ways. Locally over stdio via a new CLI subcommand
apollovm lsp; and embedded / web — the library imports nodart:io, so a browser IDE or an AI agent can drive it with decoded JSON-RPC messages viaMessageLspEndpoint(no byte framing).StreamLspEndpointprovidesContent-Lengthframing for stdio/sockets. Both share a transport-agnosticLspEndpoint. - 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-filereferences/renameand a basic rankedcompletion. - 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 intest/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 atpackage:apollovm/apollovm_pub.dart(kept out of the web-safeapollovm.dart).- Pluggable
PackageProvider:PackageConfigProvider(default, VM-only, zero extra deps — resolves through.dart_tool/package_config.json, exact pub semantics) andPubDevProvider(web-compatible) — downloads archives from pub.dev or a configurable/private/mirror host, extracts them in memory, caches them (MemoryPackageCacheby default,FilePackageCacheon the VM), and honors pubspec version constraints. Built on web-safe libraries only (http,archive,pub_semver,yaml— nodart:io), so it runs on the VM and in the browser. - Web/CORS:
PubDevProvideraccepts an injectablehttp.Client, a custom host, and arewriteUrlhook to route requests through a CORS proxy — a ready-made proxy ships intool/pub_cors_proxy.dart. DartPackageLoader+DartPackageImporter.provision()fetch each reachablepackage:import transitively and load its source into the VM; injected via the new settableApolloVM.moduleLoader. A genericCompositeModuleLoaderchains loaders.- CLI:
apollovm run/translate --pub(with--pub-host/--pub-cache) resolvespackage:imports before executing. - Promotes
http,archive,pub_semver,yamlto direct dependencies (all web-safe); only the filesystem members (PackageConfigProvider,FilePackageCache) are behind conditional imports with web stubs. - See
doc/module_resolution.mdandexample/apollovm_example_pub_importer.dart.
- Pluggable
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 newASTStatementExportandASTTypeAliasnodes. - New web-safe resolution layer (
lib/src/resolution/): pluggableModuleLoader(in-memoryVMModuleLoader), four-levelSymbolTables +ImportScope,ModuleResolver, aDependencyGraph(Tarjan cycle detection, Kahn topological order, incrementalaffectedByinvalidation), structuredImportDiagnostics (missing module/symbol, duplicate symbol, circular import, invalid export), aResolutionCache, and theModuleResolutionEnginefacade. ApolloVM.resolve()returns aggregated diagnostics; resolution is triggered lazily by the runner and invalidated incrementally onloadCodeUnit.- 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.mdandexample/apollovm_example_imports.dart.
- Enriched
New language:
Go- Added first-class Go support — ApolloVM can now parse, execute, and
translate Go source (
.go/go, aliasgolang) 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 intoApolloVM(getParser/createRunner/createCodeGeneratorand the.gofile-extension mapping). - Supported: top-level and struct receiver methods (
func (o *Name) m(...)),structtypes with fields and factory constructors (func NewName(...) *Name),var/:=type inference,if/else if/else, the fourforforms (C-style, condition-only aswhile,rangeas for-each, infinite /do-while), Goswitch(no fall-through), slices/maps ([]T{…},map[K]V{…}), closures, all arithmetic/comparison/logical/bitwise operators, string+concatenation, andfmt.Println(normalized to the VM'sprint). - 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 mcpcommand 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 officialdart_mcpSDK. 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, andmcp 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
printis exposed; inputs are inline source only);apollovm.executeruns 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(andstream_channel). Newmcp-tagged integration tests undertest/mcp/. Seedoc/MCP.md.
- A Dart-first language server is now part of the
-
0.1.4830 Jun 2026Release notes
Open source →CLI:
runexecutes.wasmfiles through the Wasm runtimeapollovm run foo.wasmnow runs the binary module via the Wasm runtime (ApolloRunnerWasm) instead of trying to decode it as UTF-8 source. The file is loaded as aBinaryCodeUnit, parsed for its exported functions byApolloParserWasm, and its entry function (e.g.main) is invoked — closing thecompile→runloop from the command line.- The
.wasmfile extension now maps to thewasmlanguage inApolloVM.parseLanguageFromFilePathExtension.
-
0.1.4728 Jun 2026Release notes
Open source →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 toint— instead of an f64 quotient (so10 / 3is3, not3.33). Dart's/keeps itsdoubleresult;~/is unchanged.- Integer division by zero raises a catchable exception whose message
matches the interpreter (
IntegerDivisionByZeroExceptionfor/,Unsupported operation: Infinity or NaN toIntfor~/). Applies to both~/and integer/. - A
printwhose argument is built from a value that just raised (e.g. the quotient inprint('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.
-
0.1.4627 Jun 2026Release notes
Open source →Wasm backend:
num(TypeScript/JSnumber) + switch on a boxed scrutineenum(a TypeScript/JavaScriptnumber) is now supported in the Wasm backend. A plainnumhas no fixed width; the VM treats integer-valued numbers asint, sonumis represented as i64. This fixes string interpolation/concatenation of anum(e.g."sum=" + (a + b)),switchon anumscrutinee, andnumarithmetic — unblocking the TypeScript Class, Conditional, Exceptions and Switch examples.switchon a boxeddynamic/Objectscrutinee (e.g. aList<Object>element) now compiles: the scrutinee is unboxed to a concrete i64 to drive the integer branch table.- Scalar
Object/dynamicentry-point parameters are now marshalled. An untyped parameter (e.g. a JavaScript/Pythonmain(a, b), or an explicit Dartdynamicparameter) 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 as0. (Theapollovm_sigsection is now emitted whenever a public function has anObject/dynamicparameter, and a plainnumis tagged asintso it is passed as a raw i64 rather than boxed.) - Anonymous functions with an untyped parameter (
n => n * 2from C#/Lua/Python) now compile: the parameter type is inferred from its body. A nested closure'sreturnno 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 avar) are hoisted and lowered to a directcall. - A boxed value flows into and out of an
Object/dynamicslot. A concrete value passed to a genericTfield/parameter (represented as a boxedObject) is boxed; a boxed value used in arithmetic or passed to a typed numeric parameter is unboxed. This makes genericBox<T>(Dart, Java, Kotlin, C#, TypeScript) work. - More boxed-operand operations. A
varwhose initializer is a boxed-operand expression (e.g.var s = a + bwherea/bareObject[]/List<Object>elements) is refined to the result's concrete type, and the== 0fast path (i64.eqz) unboxes a boxed operand first. Fixes the Java Class example and the JavaScript try/catch example.
-
0.1.4527 Jun 2026Release notes
Open source →Wasm backend: collection-to-String + dynamic arithmetic on boxed values
Map/List→Stringcoercion inprint(...)/ 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 aMap/ListtoStringstill throw a clearUnimplementedError.)- Arithmetic and comparison on boxed
Object/dynamicoperands, such as values read from aList<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 intoi64.add/f64.div(which produced invalid Wasm). - A boxed
Objectvalue flowing into a typed numericMap/Listslot (e.g.<String,int>{'a': a}whereais dynamic) is unboxed to match the slot'si64/f64width.
Wasm backend: anonymous functions assigned to a
varand called directly- Lambdas stored in a
varand 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
varthat is only ever called (never used as a value, reassigned, or captured) is lowered to a directcall— no environment heap allocation, nocall_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.
-
0.1.4427 Jun 2026Release notes
Open source →Wasm backend: rich-enum field/method reads in a
printcontext- Fixed garbage values when a rich-enum instance field or method result was
passed through
print(...)/ string interpolation (e.g.print(p.gravity)orprint('${p.mult(2)}')). The lazily-generated enum-entry initializer baked its constructorcallindex during an early discovery pass, before theprint/double_to_strhost 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 aprint(e.g.return p.gravity) was already correct.
- Fixed garbage values when a rich-enum instance field or method result was
passed through