NewYour coding agent can read the release notes before it upgrades.Set up the MCP server →
pub.dev · #4465 most downloaded on pub.dev
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.
Last release 28 days ago
09 Sep 2026
Ships unpredictably
gaps range from 8 days to 2.1 years
Nearly every release is documented
notes for 59 of the last 60 stable releases
Nothing withdrawn
no release was ever pulled
6 years old
159 releases · first in 2021
Five features from the Dart language evolution that the Dart front end did not support yet. All five are parsed, interpreted and translated; see doc/d
Five features from the Dart language evolution
that the Dart front end did not support yet. All five are parsed, interpreted
and translated; see doc/dart_language_gaps.md
for what is still missing.
{1, 2} was a syntax error, and Set was not a type ApolloVM knew:
var s = {1, 2, 2}; // was: [SyntaxError] ";" expectedSet literals now parse ({1, 2}, <int>{}, and Set<T> as a declared type),
with the Dart disambiguation kept intact: a bare {} is still an empty map,
and {k: v} is still a map literal, so an empty set has to be written <T>{}.
At run time a set answers to length, isEmpty/isNotEmpty, first/last,
contains, add/addAll, remove, clear, toList and for-in, and
duplicate elements collapse to the first occurrence.
Every target can be generated, using each language's own set — or, for the two
that have none, its idiom:
var s = {1, 2};new HashSet<int>(){{ add(1); add(2); }} // JavamutableSetOf<Int>(1, 2) // Kotlinnew HashSet<int>(){1, 2} // C#new Set<number>([1, 2]) // TypeScript / JavaScript{1, 2} # Python (`set()` when empty)map[int]struct{}{1: {}, 2: {}} // Go{ [1] = true, [2] = true } -- LuaOnly the Dart grammar parses a set literal so far. The Wasm backend refuses to
compile one (UnsupportedSyntaxError) rather than lower it to a list and
silently drop the uniqueness the program relies on.
Rich enums accepted fields, a const constructor and methods, but an accessor
in the body was a syntax error:
enum Level {
low(1), high(10);
final int weight;
const Level(this.weight);
int get doubled => this.weight * 2; // was: [SyntaxError] "=" expected
}An enum body now takes the same members a class body does. Dart round-trips
them, Kotlin emits val doubled: Int get() { … }, and every other target
refuses an enum accessor exactly as it refuses a class accessor, instead of
dropping it.
A constructor could not initialize a field it inherits: this.x is only legal
for a field of the class itself, and super.x — the form Dart 2.17 added for
exactly this — was a syntax error.
class Shape {
final int x;
Shape(this.x);
}
class Square extends Shape {
final int border;
Square(super.x, this.border); // was: [SyntaxError] "$" expected
}Super parameters now parse and run, positionally, named (with required), with
a default, in a primary constructor header, and through however many levels of
inheritance the field is declared at. The value is assigned to the inherited
field on the instance being built.
Dart output spells it super. again, and this. for a field the class owns.
The distinction is re-derived from the class hierarchy when a binary AST is
decoded — an initializing formal may only name a field of its own class, so one
that names an inherited field is a super parameter by construction — which is
why the image format is unchanged. Targets with no equivalent name the
parameter and drop the qualifier.
Still missing, and unchanged by this: the rest of the initializer list (an
explicit : super(v) call), and running the superclass constructor's body
— no subclass instantiation does that yet.
An initializing formal for a private field could only be called by its private
name, which is not what Dart 3.12 means:
class Point {
final int _x;
Point({required this._x});
}
Point(x: 1); // was: Can't find function "Point" ... {namedTypes: [x: int]}The leading _ is now stripped from the parameter name, so the call site uses
the public name while the parameter keeps writing the private field. Only named
parameters are renamed (a positional this._x has no call-site name), and only
when what is left is a valid public name — __x and _ have none and are left
alone. The declaration keeps this._x in generated Dart, and the rename is
re-derived when a binary AST is decoded, so images stay readable by builds that
predate the feature.
A constructor can now be declared in the class header:
class Point(final int x, var int y) {
int sum() => this.x + this.y;
}
class Counter(var count);A parameter marked var or final is a declaring parameter: it also declares
the field it initializes (mutable for var, final for final). A parameter
with neither is an ordinary constructor parameter and declares no field. Named
{…} and optional […] groups, required and default values all work, the
body may be replaced by ;, and the class may still declare members, extends
and implements.
The header is desugared as it is parsed into the classic form — the induced
fields plus a constructor taking this.x formals — so every runner, generator
and codec sees a shape it already handles, and Dart output comes back classic:
class Point {
final int x;
int y;
Point(this.x, this.y);
int sum() { return this.x + this.y; }
}Not supported yet: a named primary constructor (class Point.custom(…)),
class const Point(…), and the in-body this : … form — each needs a
constructor feature ApolloVM does not have (named constructors, compile-time
constants, initializer lists).
List.valueOf(x) — the static conversion Java-shaped source uses — failed the
parse with type 'Null' is not a subtype of type 'ASTClass'. Resolving the
core List class asked for the element type, and a List named in source
carries none, so the lookup returned null into a cast. It now falls back to
the element-type-less List class and the call resolves (List.valueOf(5) →
[5], a list passes through, null becomes []).
An empty map literal with explicit type arguments (<K,V>{}) threw
type 'String' is not a subtype of type 'ASTType?' while parsing, because the
value type was read from the comma's position. That is the form an empty {}
is generated as, so a program containing one could not re-parse its own
translated Dart.
One column per quarter.
v2.31.0 - Five Dart language features Latest
Latest
Compare
Dart: multiple variables per declaration
A declaration could only introduce one variable, so the ordinary Dart form of declaring several at once was a syntax error:
void main() {
num nr = 0.96, ng = 0.24, nb = 0.56; // was: [SyntaxError] ";" expected
print(nr);
print(ng);
print(nb);
}Every declarator is now accepted, in a function body and at the top level, sharing the declared type and the final / const / late modifier of the first one. A declarator may omit its initializer (int a, b = 5;) and may read the ones before it (int p = 1, q = p + 1;), matching Dart's left-to-right initialization.
The declarators are expanded into one declaration each as the code is parsed, so translation emits the form every target already supports:
num nr = 0.96;
num ng = 0.24;
num nb = 0.56;nr: float = 0.96
ng: float = 0.24
nb: float = 0.56A for initializer still takes a single declarator: a for header has no place for the extra declarations the expansion produces, and the comma-separated form does not exist in every target language.
Full changelog: v2.29.0...v2.30.0
v2.30.0 - Multiple variables per Dart declaration
Compare
int and double now expose the same num interface
int and double now expose the same num interfaceEach primitive only carried the conversion that changed its type: int had
toDouble() and double had toInt(), so the identity half of the pair was
missing on both. 3.toInt() and 1.5.toDouble() — both valid Dart, since
num.toInt() / num.toDouble() return this — failed with
Bad state: Can't find core function: int.toInt(...).
The same gap covered the rest of the num interface on int, which had none
of the members that only double had been given:
on int |
before | now |
|---|---|---|
toInt() |
✗ | ✓ (identity) |
round(), floor(), ceil(), truncate() |
✗ | ✓ (identities) |
toStringAsFixed(), toStringAsExponential(), toStringAsPrecision() |
✗ | ✓ |
toDouble() |
✓ | ✓ |
and on double, toDouble() is added as the matching identity.
This mattered most for a num-typed variable. ApolloVM has no num core
class, so num n = ... dispatches on the runtime value's class — meaning
n.toInt() worked or failed depending on whether n happened to hold a
double or an int. Both now work either way:
num n = 3; // was: Can't find core function: int.toInt
n.toInt(); // 3
n.toDouble(); // 3.0toNum() remains unsupported, as it is not a method on num, int or
double in Dart.
v2.29.0 - int and double share the same num interface
Compare
Fixed: compile <file> --target=ast silently compiled to Wasm
compile <file> --target=ast silently 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,
so apollovm compile foo.dart --target=ast left the flag sitting in the
leftover positional arguments, --target kept its wasm default, and the
command compiled to WebAssembly without saying that it had ignored anything.
It surfaced as an unrelated Wasm codegen crash — UnimplementedError: Wasm maps with value type dynamic are not supported yet — for a program that was
never meant to reach the Wasm backend. 2.28.0's own compile --help showed
that exact ordering.
Trailing options are now parsed for compile and translate, whose only
positional argument is the source file, so both orderings work. run keeps
them unparsed, because everything after the file there belongs to the program
being executed and must reach it untouched — including arguments that look
like apollovm's own flags.
A leftover positional argument is now reported rather than ignored:
apollovm compile foo.dart oops fails with Unexpected argument after the source file: oops.
compile: target inferred from the output extension, and -t--output now selects the target when --target is omitted, so naming the
file is enough:
apollovm compile foo.dart -o foo.avma # binary AST
apollovm compile foo.dart -o foo.wasm # WebAssembly.avma means the AST target and .wasm means Wasm; any other extension falls
back to the wasm default as before. An explicit --target always wins, so
-t ast -o out.bin still writes an image under that name. The extension is
read from the final path segment, so a . in a parent directory
(build.v2/out) is not mistaken for it.
--target also gained the abbreviation -t, on both compile and
translate.
…silently producing a wrong AST. Raising it is a breaking change and will only ever ship in a major release, announced here.
A parsed AST can now be saved as a compact binary image and loaded back without
running a parser. Parsing dominates the cost of loading code, so an application
can parse once — at build time, or on first run — and afterwards load the same
code unit by decoding bytes.
Such an image is an .avma file: an Apollo Virtual Machine
Archive. It holds one parsed code unit, or a whole VM's worth of them.
Measured on a ~4 KB Dart program: decoding is about 11× faster than parsing
and the image is about two thirds the size of the source. For a very small
unit the fixed header and pools cost more than the source is worth; the saving
appears once there is a program to speak of.
import 'package:apollovm/apollovm_serialization.dart';
var image = vm.saveCodeUnitAST(codeUnit); // Uint8List
await ApolloVM().loadCodeUnitAST(image); // no parser involvedvm.saveAllAST() / vm.loadAllAST() do the same for a whole VM, bundling every
loaded code unit into one archive. The CLI gained
apollovm compile --target=ast, and apollovm run recognizes an image by its
magic bytes — not its extension — taking the language from the image itself.
Everything is Uint8List in and Uint8List out, so file access stays with the
caller and the whole feature works unchanged on the web. The Chrome suite covers
it, including the places where a JavaScript double behaves unlike a VM int.
Loading a decoded unit needs no new path: ApolloVM.loadCodeUnit only reaches
for a parser when a unit has no AST yet, so namespace registration, the
null-safety check and incremental-resolution invalidation all behave exactly as
they do for parsed source. Nothing derivable is stored — parent links,
scope-variable resolution, superclass and extension targets and the this.field
constructor-parameter promotion are re-established by one resolveNode call
after decoding, exactly as every grammar does after a parse.
Nodes holding live Dart state are refused with an error naming where in the
program they were found: external functions and getters, which carry a closure,
and runtime values such as a class instance or a pending future. All of them are
injected by the VM at run time rather than produced by a parser, so a parsed AST
never contains them and the same bindings are re-injected after a binary load.
Coverage is enforced by a test that scans lib/src/ast/ and requires every
concrete AST* class to be registered, pooled as a type, encoded inline by a
parent, or listed as refused with a written reason — so adding a node kind and
forgetting its codec fails the build rather than silently dropping a field.
Every image carries a CRC-32, verified on load. It detects corruption, not
tampering: anyone who can modify a file can recompute the checksum in
microseconds. Only a signature made with a key the attacker does not have makes
an image tamper-evident, and an unsigned image deserves exactly as much trust as
the source it came from — loading one and running it is equivalent to running
arbitrary code from that source.
Signing is optional and pluggable (ASTBinarySigner / ASTBinaryVerifier), so
an HMAC, a public-key signature or a hardware key store all fit;
HmacSha256Signer is built in, which is why crypto becomes a direct
dependency — it was already present transitively. The signature covers
everything up to and including the CRC, so an attacker who edits a section and
recomputes the checksum still fails verification.
An image records two version numbers: the container revision that wrote it, and
the oldest revision that can decode it correctly. Every section is
length-prefixed, so a reader skips any section it does not recognize, and every
section is decoded from a bounded view, so fields appended by a newer writer are
ignored rather than misread. A newer ApolloVM's output therefore keeps loading
in an older ApolloVM for as long as the new information is purely additive, and
an older image keeps loading in every future ApolloVM — the reader retains the
decode path for every format version it has ever supported. When a change
genuinely cannot be understood by an older reader, the writer raises the minimum
reader version and that older reader fails immediately with an
ASTBinaryException naming both versions, rather than silently producing a
wrong AST. Raising it is a breaking change and will only ever ship in a major
release, announced here.
A real image written by format version 1 is committed in the test suite and must
keep loading; it is the only check that can catch an accidental incompatible
change, since an image synthesized by the current writer would move with it.
data_serializer 1.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.readLeb128SignedInt sign-extended from the wrong byte on every-2 decoded as 126 and 64 as -16320. The binary AST formatdart2js — butshiftLeftInt/shiftRightInt fell back to a 32-bit <</>>,0.|=, also a 32-bit operation, and2^shift — the rangeDateTime.microsecondsSinceEpoch occupies.Together those meant an integer literal of 2^32 or more decoded to the wrong
number on the web: 4294967296 came back as 0, and 1000000000000000 as
2764472320. A test now covers literals from 2^28 upwards, positive and
negative, and it runs under --platform chrome.
set name(T value) { … } is implemented. It was the last accessor with no support at all — no grammar rule, no AST node, no dispatch. 2.26.0 turned it
set name(T value) { … } is implemented. It was the last accessor with no
support at all — no grammar rule, no AST node, no dispatch. 2.26.0 turned it
from a silent misparse (into a method named name returning a type named set)
into a clean parse error; it now works.
Parsed on classes and extensions, with a typed or untyped parameter and an arrow
or block body:
class Box {
int _v = 0;
int get value => _v;
set value(int x) { _v = x; }
set scaled(v) => _v = v * 10;
}A setter runs on every write position — obj.x = v, this.x = v, and an
unqualified x = v inside the class, the write-side mirror of an unqualified
getter read. A real variable in scope still wins, so a setter's own parameter
cannot re-enter it. Compound assignment (obj.x += 1) reads through the getter
when there is one and falls back to the backing field; ??= short-circuits, so
the setter does not run when the current value is non-null. Inherited and
overridden setters resolve through the superclass chain, and extension setters
work like extension getters.
Only Dart generates setters. Every other target refuses with
UnsupportedSyntaxError, and Wasm refuses rather than letting an assignment
silently write the backing field instead of running the setter body.
An arrow-bodied setter regenerates as an arrow: emitting it as a block would
produce set x(v) { return …; }, which Dart rejects because a setter returns
void. The generated output is verified with the real dart analyze.
Those three generators never iterated the class's accessors, so a class with a
getter translated to any of them lost the accessor entirely while the method
bodies kept referencing the property — broken code that looked fine, rather than
the honest UnsupportedSyntaxError Java/C#/JS/TS already produced. They now
refuse, like the others.
ASTBlock.set dropped settersIt copied functions, getters and statements, so every class member survived the
temporary parse block except the new ones.
For getters and setters alike: static accessors, top-level accessors, and an
unqualified read of a getter (return value; inside a method — this.value
works). Kotlin generates getters but not setters.
Full changelog: https://github.com/ApolloVM/apollovm_dart/blob/v2.27.0/CHANGELOG.md
Annotations ( @override , @Deprecated('x') , @pragma(...) , @a.B(1) ) at every position Dart allows. There was no @ in any grammar in the repo, so a s…
for (var e in l) print('- $e'); is ordinary Dart, and it did not parse. Only
the plain if accepted an unbraced single-statement body — every loop, every
if/else and every else if demanded { }.
All seven remaining rules now accept either form, in Dart, Java 11, Kotlin,
C#, JavaScript and TypeScript: for, for-in/for-each, while,
do/while, if/else, the else if chain, and the final else. A braced
body is still tried first, so nothing about existing sources changes.
singleLineStatement was also far too narrow — return …; or an expression
statement, nothing else — so even the if that already supported it rejected
if (x) break;, if (x) throw e; and a nested if. It is now each language's
full statement set minus declarations and bare blocks.
A dangling else binds to the nearest if, matching every target
language. Python gains the equivalent construct, the inline suite
(if x: return 1, def f(): pass, while c: i += 1; j += 1), at every
suite() position.
Go is deliberately excluded — its spec defines
Block = "{" StatementList "}" — and Lua has no such form. A single-statement
body translated to either is emitted braced / 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.
else-prefix miscompile, fixed firstBaseGrammarLexer.token() is a prefix matcher with no word-boundary guard, so
string('else') matches the start of an identifier like elseCount. That was
unreachable while every else arm followed a mandatory braced block, and would
have become a silent miscompile the moment bodies could be unbraced:
if (a) x();
elseCount = 1; // `else` matches; `Count = 1;` becomes the else armThe branch and loop rules of all six C-style grammars now use whole-word
keyword tokens. Also covers Kotlin's when entry labels and if expression.
'''Hello $name''' yielded$name. Wrong output, not an error.assert(c) / assert(c, m) as a real statement (it parsed as a call to aassert). Each target emits its own idiom: Javaassert c : m;, Python assert c, m, Kotlin assert(c) { m }, C#Debug.Assert, Lua's built-in, JS/TS/Go lowered to an explicit check. Wasmrequired named parameters on plain, constructor-typed and this.({required int a}) was a hard parse failure.@override, @Deprecated('x'), @pragma(...), @a.B(1))@ in any grammar in the repo,@override broke the whole class body — which matters beyondlib/src/pub loads real pub packages. Parsed andlate on locals and fields (accepted and dropped).const at use sites: const Foo(), const [], const {}.async bodies on local and anonymous functions.catch (e, s) now binds the stack trace — it was parsed and thrown away,s failed.%= &= |= ^= <<= >>=.get twice => …) now parse.if (a) {} else if (b) {} with no trailing else failed toelse non-optional, unlike every other language.do/while translated to Python emitted literaldo { … } while (c);. Now lowers to while True: … if not (c): break.a += 1), which Lua doesa = a + 1.const [1] silently misparsed as an index read on a variable named const.set value(int v) {} silently became a method named value returning aset. Now a clean parse error at the set. Full setter supportFull changelog: https://github.com/ApolloVM/apollovm_dart/blob/v2.26.0/CHANGELOG.md
for (var e in l) print('- $e'); is ordinary Dart, and it did not parse. Only
the plain if accepted an unbraced single-statement body — every loop, every
if/else and every else if demanded { }.
All seven remaining rules now accept either form, in Dart, Java 11, Kotlin,
C#, JavaScript and TypeScript: for, for-in/for-each, while,
do/while, if/else, the else if chain, and the final else. A braced
body is still tried first, so nothing about existing sources changes.
singleLineStatement was also far too narrow — return …; or an expression
statement, nothing else — so even the if that already supported it rejected
if (x) break;, if (x) throw e; and a nested if. It is now the language's
full statement set minus declarations and bare blocks.
A dangling else binds to the nearest if, matching every target
language. Python gets the equivalent construct, the inline suite
(if x: return 1, def f(): pass, while c: i += 1; j += 1), for every
suite() position: if/elif/else, for, while, try, def, class
and case.
Go is deliberately excluded — its spec defines Block = "{" StatementList "}"
and every control-flow statement takes a Block — and Lua has no such form. A
single-statement body translated to either is emitted braced / 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.
else-prefix miscompile, latent until nowBaseGrammarLexer.token() is a prefix matcher with no word-boundary guard, so
string('else') matches the start of an identifier such as elseCount. That
was unreachable while every else arm was preceded by a mandatory braced
block, and would have become a silent miscompile the moment bodies could be
unbraced:
if (a) x();
elseCount = 1; // `else` matches; `Count = 1;` becomes the else arm
The branch and loop rules of all six C-style grammars now use whole-word
keyword tokens. Also covers Kotlin's when entry labels and if expression.
if (a) {} else if (b) {} with no trailing else failed to
parse — both made the final else non-optional, unlike every other language.do/while translated to Python emitted literal
do { … } while (c);. It now lowers to while True: … if not (c): break.a += 1), which is not
valid Lua. It now lowers to a = a + 1.codeBlockOrSingleLineBlock cluster that was defined
but never referenced.'''Hello $name''' yielded
the literal text $name — wrong output, not an error. Raw r'''…''' is
unaffected.assert(c) / assert(c, m) as a real statement. It previously parsed as
a call to a user function named assert and failed later with a confusing
message. A failed assertion throws and is catchable. Every target emits its
own idiom (Java assert c : m;, Python assert c, m, Kotlin
assert(c) { m }, C# Debug.Assert, Lua's built-in, JS/TS/Go lowered to an
explicit check). Wasm refuses to compile it rather than mis-compile it.required named parameters on plain, constructor-typed and this.
forms. ({required int a}) was a hard parse failure. Dart output keeps the
modifier; other targets express required-ness by the absence of a default.@override, @Deprecated('x'), @pragma(...), @a.B(1)
— at every position Dart allows. There was no @ in any grammar in the repo,
so one @override broke the whole class body; this matters beyond
hand-written sources, since lib/src/pub loads real pub packages. Parsed and
discarded for now.late on locals and fields (accepted and dropped).const at use sites: const Foo(), const [], const {}. const [1]
used to silently misparse as an index read on a variable named const.async bodies on local and anonymous functions:
int f(int x) => x * 2; inside a function, and (x) async => ….catch (e, s) now binds the stack trace. It was parsed and thrown away,
so any handler that referenced s failed. ApolloVM has no stack traces, so
it binds to an empty string; targets whose catch header has no second
variable declare it as the handler's first statement.%= &= |= ^= <<= >>=. In JS/TS %= was already in
the grammar but missing from the shared operator enum, so it surfaced as a
SyntaxError.get twice => …) now parse. They failed because
type().optional() greedily ate get and petitparser's optional() cannot
backtrack.set no longer misparses into a methodsimpleType() accepted any identifier, so set value(int v) {} silently became
a method named value returning a type named set, failing much later with
a confusing error. get/set are now rejected in a type position, turning that
into a parse error at the set.
Full setter support remains unimplemented: it would mirror the entire getter subsystem plus assignment dispatch, and getters are generated for only 2 of 9 targets. This change converts a silent misparse into a clear error.
v2.26.0 - Control-flow bodies no longer require braces
Compare
No change to this package's code — this release exists to carry the apollovm_wasm bump, the way 2.23.2 carried apollovm_wasm 1.1.0.
No change to this package's code — this release exists to carry the
apollovm_wasm bump, the way 2.23.2 carried apollovm_wasm 1.1.0.
apollovm_wasm declared apollovm: ^2.0.0, but it is not a loosely-coupled
consumer: it decodes what this package's Wasm generator encodes. The
boxed-Object cell layout and its _boxTag* values are a contract, and both
wasm_runner.dart and wasm_generator.dart carry a comment saying the
constants must match.
That constraint let pub pair the runtime with any 2.x, including releases whose
box encoding it was never built against — a mismatch that shows up as a wrong
value or a trap at run time, not as a resolution failure. It is now
apollovm: ^2.25.0, widened deliberately rather than by default. Its wasm_run
also moves to ^0.2.0+2 (patch, no API change).
Wasm: ?. on a boxed slot no longer refuses to compile
?. on a boxed slot no longer refuses to compilevar x = null; x?.length was an UnimplementedError — "Wasm getter .length
on Null is not supported yet". That was reachable from ordinary code, and the
advice the neighbouring error gives ("declare the variable as var / Object?
/ dynamic") led straight into it.
A boxed slot is the only Wasm representation that can hold null, so it is also
the only place ?. has anything to do — on a concrete slot the receiver cannot
be null and the null-awareness is vacuous. .length / .isEmpty / .isNotEmpty
on a boxed receiver now dispatch on the box tag at runtime:
var missing = null;
var n = missing?.length; // -> null
return n ?? -1; // -> -1Only a boxed String carries these members — List/Map have no boxed form
at all, and the int/double/bool/instance tags have no length — so any other tag
traps rather than reading a length word out of a payload that is not a string
pointer. A plain . on a null box traps too, matching the interpreter's
ApolloVMNullPointerException.
The null-aware result is itself boxed, because it can be null and a nullable
value has no unboxed encoding. That is what lets it flow into ?? and
== null, which already understand boxes. The plain form still yields an
unboxed int/bool.
An explicitly-declared Object? local initialized from a concrete value keeps
the initializer's type instead of being boxed, so Object? s = '' makes s a
String slot. s?.isEmpty then takes the concrete-String path and stores its i32
boolean into a slot sized i64, producing a module that compiles but fails Wasm
validation. That reproduces byte-identically on 2.24.0, so it is a separate
defect in the declaration path rather than a consequence of this change; it is
pinned as a skipped test in test/wasm/apollovm_wasm_boxed_member_test.dart.
Full Changelog: v2.24.0...v2.25.0
v2.25.0 - Wasm: ?. on a boxed slot no longer refuses to compile
Compare
Null-aware access is now really checked on every target, not dropped
a?.b was emitted as a plain a.b on Java, C#, JavaScript, Lua and Python: the
generated code compiled, but the null check was gone — it threw on exactly the
input ?. exists to handle. The README's null-safety matrix marked those cells
⚠️ lossy.
Java, Lua and Python now lower the access to an explicit guard, and the guard
wraps the whole chain rather than the null-aware link alone:
// Dart source
int chain(A? a) { return a?.m().toInt(); }// Java, before: threw when `a` was null
return a.m().toInt();
// Java, now
return (a != null ? a.m().toInt() : null);# Python
return (a.m().toInt() if a is not None else None)C# and JavaScript were lossy for a simpler reason — both have had the operator
all along (C# 6, ES2020) and merely never declared it — so they now emit a?.b
natively. JavaScript uses ?.[i] for element access, and its postfix ! is
handled separately: JavaScript has no null-assertion operator (a postfix ! is
logical NOT), so a! is emitted as plain a rather than negating the value.
Go continues to report ?. / ?[ as unsupported: it represents a nullable T?
as *T, so a degraded access would both skip the nil check and yield the wrong
type.
??, &&, || and x == null are AST nodes of their 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 != null to promote a variable, the Go backend probed
for a null literal to compare the pointer instead of dereferencing it, and the
Wasm backend re-tested the operator — and the enum's exhaustiveness forced
throw StateError('unreachable') arms in three places.
ASTExpressionNullCoalesce, ASTExpressionLogicalAnd, ASTExpressionLogicalOr
and ASTExpressionNullCheck now carry those shapes, built by the new
astExpressionOperation() factory that the shared grammar reduction uses, so
all nine front-ends get them. Consumers dispatch on the node type instead of
pattern-matching, and evaluating x == null no longer concretizes both operands
and runs them through equality dispatch.
This is internal structure — the generated source is unchanged, except where a
target has a better idiom that the dedicated node makes reachable. Python is the
one such case:
# before
if a == None:
# now
if a is None:== dispatches through __eq__, which a class can redefine to return True
for None; is cannot be intercepted, and is the form PEP 8 mandates.
The Python grammar learned is None / is not None to match, so the
generated source still round-trips — ApolloVM can now read back what it writes.
This is deliberately limited to the None comparison: general a is b is
identity, and mapping it to == would silently turn it into equality, so it
stays unparsed as before.
Building a binary operation directly with ASTExpressionOperation still works
for the ordinary operators. Constructing one with ??, && or || now throws
rather than silently evaluating both operands, which would no longer be a
short-circuit.
Full Changelog: v2.23.3...v2.24.0
v2.24.0 - Null-aware access is now really checked on every target, not dropped
Compare
A signature mismatch is no longer reported as a missing entry function
apollovm.execute returned the same diagnostic whether the entry name was absent or existed but rejected the passed arguments, so a caller that guessed the parameters was told to fix the name:
$ apollovm mcp call apollovm.execute --language dart --args '[1,2,3]' \
--source 'int main(int a, String b){ return 1; }'
{
"diagnostics": [
{ "severity": "error", "message": "Entry function not found: main" }
],
"isError": true
}
ApolloRuntime.execute now separates the two causes. When the name exists but no overload matches, the diagnostic shows the call that was attempted — with the passed argument types — and every declared signature of that name:
No entry function matching the passed arguments: `main(int, int, int)`.
A function named `main` exists, but with a different signature:
`int main(int a, String b)`. Adjust the arguments to match a declared signature.
Several declarations are all listed, so an overload set or a name shared across classes shows the full menu:
No entry function matching the passed arguments: `run(double, double, double)`.
2 functions named `run` exist, but with different signatures:
`int A.run(int a)`, `int B.run(bool x, bool y)`.
Signatures are qualified with their class, including for a method reached through the auto-discovery order (no className given) — the case where the caller cannot otherwise tell which class answered.
An explicit className that does not exist is now reported as a missing class instead of blaming the method: Entry class not found: Bar (looking for the method `run`). A name that is genuinely absent from the source still reports Entry function not found, unchanged.
The extra lookup is read-only and runs only on the failure path, so successful execution is untouched.
Full Changelog: v2.23.2...v2.23.3
Calling apollovm.execute with arguments that no declaration of the entry name
accepts reported the same thing as a typo in the name — Entry function not found: main — leaving the caller with nothing to correct:
$ apollovm mcp call apollovm.execute --language dart --args '[1,2,3]' \
--source 'int main(int a, String b){ return 1; }'
{
"diagnostics": [
{ "severity": "error", "message": "Entry function not found: main" }
],
"isError": true
}
ApolloRuntime.execute now separates the two causes. When the name exists but
no overload matches, the diagnostic shows the call that was attempted and every
declared signature of that name (qualified with the class when it is a method,
including one reached by the auto-discovery order):
No entry function matching the passed arguments: `main(int, int, int)`.
A function named `main` exists, but with a different signature:
`int main(int a, String b)`. Adjust the arguments to match a declared signature.
An explicit className that does not exist now reports Entry class not found: Bar (looking for the method run) instead of blaming the method. A name that
is genuinely absent from the source still reports Entry function not found.
v2.23.3 - Signature mismatch is no longer reported as a missing entry function
Compare
apollovm_wasm 1.1.0 — wasm_run ^0.2.0+1
apollovm_wasm 1.1.0 — wasm_run ^0.2.0+1wasm_run 0.2.0 is a breaking release. It is absorbed inside apollovm_wasm, so consumers keep the same WasmRuntimeIO API and gain one thing: there is no install step any more.
dart run wasm_run:setup is gone. The SDK's build hooks download the native library into .dart_tool/lib/ during dart run / dart test / dart compile, so that directory is now searched first — walking up to every enclosing package root, so a nested package finds a library fetched by its workspace — along with .dart_tool/wasm_run/ (the wasm_run:build_binaries output).
Three further breaks needed handling:
flutter_rust_bridge 2.x, so the symbol that identifies a genuine wasm_run library changed: wire_compile_wasm → frb_get_rust_content_hash. Validating the old one rejected every 0.2 library.WasmRunLibrary.isReachable() became Future<bool>.wasm_run 0.2 initializes its Rust bindings asynchronously, and no longer locates its own library in a pure-Dart application. WasmRunLibrary.setUp() is now awaited once, lazily, on the first module compile — ensureBooted() and isSupported stay synchronous, as WasmRuntime requires, and only probe for the library.WASM_RUN_DART_DYNAMIC_LIBRARY (the variable wasm_run itself reads) now overrides the library path. The older WASM_RUN_LIB_PATH is still honored, and is finally read as a path: it used to be consulted only on platforms with no known library name, and then joined with candidate directories as if it were a file name.
apollovm_wasm now requires Dart >= 3.10, matching wasm_run 0.2.
apollovm 2.23.2Nothing changes for package:apollovm itself — it still compiles Wasm everywhere and pulls in no native toolchain. This release only corrects the places that told people to run a command that no longer exists: mcp doctor, the Wasm test suite and WASM_BACKEND_PLAN.md. CI drops its three wasm_run:setup steps.
The upstream aarch64-apple-darwin 0.2.0 binary is killed by macOS (SIGKILL, Code Signature Invalid) the moment wasmtime executes JIT-compiled Wasm inside the JIT Dart VM — i.e. under dart run and dart test. Compiling and instantiating modules is fine; only the call into generated code trips it, and wasm_run 0.1.0+2 does not hit it on the same machine.
Ahead-of-time compilation is unaffected, so on macOS arm64 run Wasm through dart test --compiler exe / dart compile exe. It is a macOS code-signing enforcement, so other platforms are not expected to be affected — Linux is verified in CI (2541 tests, JIT and AOT).
Full Changelog: v2.23.1...v2.23.2
apollovm_wasm 1.1.0 moves to wasm_run 0.2, which dropped dart run wasm_run:setup in favor
of the SDK's build hooks: the native library is downloaded automatically by dart run,
dart test and dart compile. mcp doctor and the Wasm test suite said otherwise, so they now
just point at package:apollovm_wasm.
Nothing changes for package:apollovm itself — it still compiles Wasm everywhere and pulls in no
native toolchain. See the apollovm_wasm changelog for the details,
including a macOS/Apple-Silicon issue in the upstream 0.2.0 binary.
v2.23.2 - apollovm_wasm 1.1.0: wasm_run 0.2, no install step
Compare
A client that fills in every field rather than omitting the optional ones sends "" — and "" was taken as a real name to look up, so nothing matched:
className/function no 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.execute now normalizes both entry names: they are trimmed, and a
name that is empty after trimming means "not specified" — className falls back
to the full discovery order (top-level function, then any class method) and
function falls back to main. Trimming also makes " main " resolve, which
previously did not. A name that is genuinely absent from the source still
reports Entry function not found, now with the trimmed name in the message.
Class lookup is guarded at the source as well, so an empty name can never match
an entry regardless of the caller: LanguageNamespaces.getClass,
CodeNamespace.getClass/containsClass, ASTRoot.getClass and
ApolloRunner.getClassMethod return early for a blank class name.
lsp/example_workspace/broken.dart is intentionally unparseable (it is what
makes the language server emit a parse diagnostic), which made dart format .
fail with exit 65 and made the analyzer report it whenever it was opened in an
editor. The root analyzer: exclude: did not cover either case, and the
formatter has no exclude option at all.
The fixture is now lsp/example_workspace/.broken.dart: both dart format and
dart analyze skip dot-prefixed paths during a directory walk, while the editor
still sees a .dart file and hands it to the ApolloVM language server, so the
demo is unchanged. A local analysis_options.yaml silences the remaining
fixture diagnostics. No published code is affected — lsp/ is .pubignored.
2.22.0 added ApolloVM(nullSafetyChecks: true), but the only way to turn it on was to construct the VM in Dart. It is now exposed at every entry point
nullSafetyChecks is 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-safety on run, translate and compile (added once on the
shared CommandSourceFileBase, like --pub):
$ apollovm run --null-safety foo.dart
** NULL SAFETY: Can't load `foo.dart` (dart): 1 null-safety error(s).
- The operand 'b' can be 'null', so it can't be used in an operation
unconditionally. Use '??', '!' or a null check.
$ echo $?
1
The rejection is printed as a report rather than escaping as an unhandled error
with a stack trace, and it is not restated as a parse failure. main now maps a
command result of false to exit status 1, so a check can gate a build; no
existing command path returns false.
MCP — every source tool takes an optional nullSafety argument, and
--null-safety on both apollovm mcp serve and apollovm mcp call sets the
default (a per-call value always wins). The two tool kinds behave differently,
by design:
apollovm.execute, apollovm.translate,
apollovm.wasm — reject the source, returning the findings as diagnostics
rather than throwing;apollovm.parse, apollovm.ast,
apollovm.symbols, apollovm.types — never load anything, so a "fail the
load" flag would be meaningless. They add the findings to diagnostics and
still succeed, which is the useful form for inspection.The server default is merged into the tool arguments at the single dispatch
point shared by the in-process and isolate paths. That matters: _IsolateJob
carries only the arguments and limits, so a field on the server object would
never reach a spawned isolate — and apollovm.execute runs in one by default.
It is deliberately not stored in McpLimits, which is documented as resource
and security limits.
Adds mcpCoerceBool beside mcpCoerceInt, so a client sending "true" or 1
is handled rather than crashing the tool on a hard cast — the same class of bug
2.16.0 fixed for the integer arguments.
The LSP was checked and deliberately left alone: its Analyzer uses its VM only
for getParser and never loads a code unit, and it already reports null-safety
findings as diagnostics. An editor must report, not refuse to open a file.
NullSafetyAnalyzer.analyze walked root.descendantChildren looking for invocables. But ASTBlock.children is only [functions, statements], and ASTRoot k
NullSafetyAnalyzer.analyze walked root.descendantChildren looking for
invocables. But ASTBlock.children is only [functions, statements], and
ASTRoot keeps its classes in a separate map — so the traversal never entered a
class body. ASTClass compounds it by keeping constructors and getters outside
children too.
The effect: every method, constructor and getter of every class went unanalyzed, including in the LSP/Problems panel. Only top-level functions were ever checked, which is also why the existing tests — all written against top-level functions — never caught it.
class Foo {
static void main(int a, int? b) {
var c = a + b; // reported no diagnostic at all
}
}
analyze now walks each class and extension explicitly, plus their constructors
and getters. The reported case produces unchecked-nullable-operand with no rule
changes — the rules were fine, nothing was reaching them.
ApolloVM(nullSafetyChecks: true) makes loadCodeUnit throw the new
NullSafetyError when the AST has null-safety errors, before the unit is
registered:
var vm = ApolloVM(nullSafetyChecks: true);
await vm.loadCodeUnit(unit); // throws — nothing printed, nothing executed
Without it, the snippet above prints 5 and null and then throws
ApolloVMNullPointerException from the +, having already produced output.
NullSafetySeverity.error blocks; warnings (e.g. null!) and info stay
diagnostic-only, exactly as in the LSP.BinaryCodeUnit (Wasm) carries no AST and is skipped.NullSafetyError carries the offending findings.No language gate is needed — only the Dart grammar parses ?, so every other
language's types are non-nullable and produce no findings. There is a test
asserting that rather than assuming it.
Writing the README's null-safety matrix meant generating every construct into every target and reading the result. Two cells were emitting source that
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:
xs?[0]. Kotlin has no ?[ operator — its
null-aware element access is the call xs?.get(0). The shared
nullAwareIndexOpen hook gained a matching nullAwareIndexClose, so a target
can close with ) instead of ].null. Lua's is nil, so the generated code
referenced an undefined global: a == null was always false rather than a nil
test. Both null-rendering hooks are now overridden for Lua.The feature tables covered control flow, operators and OOP, but nothing of the
null-safety work from 2.16.0 onwards. A new Null safety section carries a
per-language table for nullable types, ?? / ??=, ?. / ?[, !, cascades,
== null and the static analyzer — each cell verified by generating the
construct and reading the output, not from memory.
It adds a ⚠️ lossy marker for targets that emit a form which compiles but
drops the null check (a?.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, the null
row in the operators table now points at the Wasm limits, and the Wasm status
paragraph mentions the boxed-null domain.
Go had no representation for nullability at all. A nullable local was emitted as var s string = nil — source that does not compile — and ?? was report
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 to nil. 2.19.0
turned the broken output into an explicit error; this release implements the
representation.
A nullable T? now becomes a Go pointer *T, which is the one Go form that can
hold nil:
int? a is a *int,
a String? name field is name *string.(*a)), including a bare return x, which takes its own
generation path.x == null is x == nil, not a deref.func goPtr[T any](v T) *T helper, since Go cannot write &5. The helper is
emitted only in modules that need it.a ?? b lowers to an inline function that nil-checks the pointer:
func() int { if a != nil { return *a }; return b }(). Go has no conditional
expression, so this is the only correct form.List<int>? stays []int,
not *[]int, and likewise for maps and interfaces.Every shape above is verified by compiling the generated source with a real Go
toolchain (go build), not by asserting on text — which is what let
var s string = nil ship in the first place.
Two constructs remain unsupported in Go, and both now report rather than emit something wrong:
?. — the shared fallback degrades it to ., which was merely lossy when
a nullable was a plain T. Against a *T it would skip the nil check and
yield a value where a pointer is expected. Lowering it needs the accessed
member's type at generation time, which the generator does not resolve yet.??= — lowering it to t = t ?? v needs the target's element type to
build the inline function's return type, and the text-level assignment hook
does not have it. Plain ?? is unaffected.x == null — the most common null check in Dart — threw for any typed left operand:
x == null no 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 cast
No ternary, nullable slot or ?. was needed; String s = 'x'; s == null failed
the same way. Only the reversed null == x worked, because ASTValueNull.equals
type-tests instead of casting.
ASTValue.equals read the other operand through _getValue, which casts it to
this value's T. That is right for arithmetic — a mismatched operand there is
a real error — but wrong for equality, where a different type must compare
false. Equality now reads both operands uncast, so x == null is false,
x == 'other type' is false, and neither throws. The three equals overrides
(ASTValueStatic, ASTValuePrimitive, ASTValueNum) had the same cast and were
fixed with it.
This predates the null-safety work — it is in the base ASTValue — but 2.16.0
made x == null the idiom people reach for, so it went from obscure to
prominent.
== null / != null against the null boxWith null representable since 2.18.0, the equality paths had to learn about it.
A comparison against a null literal is now recognised before the String and
numeric paths:
a[0] == null on a List<Object> answers correctly (it previously took the
__streq route and compared contents against address 0, reporting a non-null
String as null);int, double, String, an instance) can never be
the null box, so the result is a constant — and, importantly, a valid module.
String s; s == null previously pushed two i32 handles into an i64.eq and
produced a module that failed to validate.(expr).m().fieldA group invocation followed by member access did not parse — the
group-invocation rule chains only further invocations, so a trailing .field had
nothing to match it. A new rule reuses that rule for the head and folds the
trailing segments. It requires at least one trailing segment, so (expr).m()
keeps its own rule and parenthesized arithmetic is untouched (reordering the
rules instead changes how the operation chain groups and breaks round-trip
generation).
var s string = nilThe Go generator emitted var s string = nil for a nullable local — source that
does not compile. It now reports an UnsupportedSyntaxError naming the type,
consistent with how ?? is already handled. A List/Map still accepts nil,
since those become a Go slice/map, which are nilable.
Go's nullable representation remains the open item: supporting T? properly
means generating *T throughout — declarations, zero values, every dereference,
and parameter/return types.
Compiling a null literal to Wasm threw a bare UnimplementedError: generateASTExpressionNullValue — a leftover TODO — so an ordinary Dart idiom such as
null in the boxed-Object domainCompiling a null literal to Wasm threw a bare
UnimplementedError: generateASTExpressionNullValue — a leftover TODO — so an
ordinary Dart idiom such as var a = args.length > 0 ? args[0] : null; could
not be compiled at all.
null is now a real value in the backend's boxed domain: the boxed-Object
pointer 0. The heap never allocates at address 0, so it needs no cell, costs no
allocation, and is distinguishable from every real box.
null forces that type to a boxed
Object — otherwise c ? 1 : null mixed an i64 with an i32 and produced a
module that failed to validate.?? tests a boxed operand. It previously always yielded its left operand
("a Wasm value is never null"), which remains correct in the numeric domain.
A boxed operand is now compared against the null box, so a ?? 99 falls back
when a is null.null. The box-to-string helper checks the
null box before dereferencing a tag.__alloc look-ahead. Boxing allocates, but the alloc export is decided
before the Code section while the boxing is only discovered while writing
it — so [1, null, 3] and a ?? 99 aborted at run time with
No exported Wasm function __alloc. The generator now scans function bodies
for a null literal up front.Object? return decodes to null instead of the raw pointer 0. Two
causes: the return path had no Object case, and _typeTag's fallback gave
tag 5 to both Object and a class instance, so the runner could not tell
a box from a bare instance pointer. A class instance now carries tag 8,
leaving 5 unambiguously "boxed value".Where null genuinely has no representation — a slot whose Wasm type is
concrete, such as int (i64) or String (a string pointer) — the compiler now
reports an UnsupportedSyntaxError naming the type and suggesting var /
Object? / dynamic, rather than emitting a module that fails to validate.
The analyzer checked unconditional member/method/index access on a nullable,
and the null literal assigned to a non-nullable slot. Two adjacent mistakes
went unreported:
x + (y ?? 0) where x is int?)
now reports unchecked-nullable-operand. ??, == and != are exempt — a
nullable operand is exactly what they exist to handle — and ! or a preceding
null check still suppress it.int x = a; where a
is int?) now reports nullable-to-non-nullable. Previously only a literal
null was caught, so the same error one step removed passed silently.Four gaps found by exercising the 2.16.0 null-safety surface end-to-end.
Four gaps found by exercising the 2.16.0 null-safety surface end-to-end.
T? parameter now accepts a T argument. Calling a method with a
String? parameter and a non-null String failed with parameters signature not compatible, though passing null worked. Argument passing is an
assignment, so ASTFunctionParameters.parameterAcceptsType now uses
ASTType.acceptsAssignment instead of acceptsType. (int?/double? only
appeared to work because StrictType ignores nullable in ==, while
ASTTypeString compares it — so String? and String were unequal and
ASTTypeString.acceptsType rejected the argument.) A non-nullable parameter
still rejects null.var v = s?.length on a null
receiver threw Class not set for type: Null, and var v = xs?[0] threw
Can't cast initial (null) value to type: int. ?. (getter and method) and
?[ now resolve to T?, and resolving the type of an access whose receiver
is null reports Null instead of trying to find a class for it. Using the
result in a return, inline with ??, or in a string interpolation already
worked and is unchanged.a.b.c, a.b?.c, a.b!.c, a.b.m(),
a.b?.m(x) and chained writes (a.b.c = v) previously failed — the grammar
accepted only a single identifier receiver, so a.next?.value reported
SyntaxError: digit expected (the ? was read as the start of a number)
and even plain a.next.value reported "(" expected. A new chain rule
folds each segment onto the previous one, wrapping it in the new
ASTExpressionVariable (an ASTVariable backed by an expression) so the
existing object-access nodes supply the runtime, null-aware and
code-generation behaviour. Chains of any depth work, in any mix of ., ?.
and !. Single-segment access keeps its own rules — the chain rule requires
two or more segments — so enum entries, static fields and import prefixes
resolve exactly as before. A field read off a parenthesized receiver
((a).v, (a)?.v) also parses now; only (expr).m().field remains
unsupported.?? and ??= no longer leak into targets that cannot compile them.
Java, Lua, Python and Go emitted a ?? b verbatim, and ??= leaked into
every target — including Kotlin, which had a correct ?: for ?? but no
hook for the assignment form. ?? now goes through an overridable
renderNullCoalesce, and ??= is lowered to t = t ?? v wherever
supportsNullCoalesceAssignment is false, so each target defines only one
desugaring:
(a != null ? a : b)(a if a is not None else b) — an is not None test, so 0/''/
False are preservednil test, because
both a or b and the a ~= nil and a or b idiom return b for a non-nil
false; it also binds a to a local, so it is evaluated once?: for ??, and t = t ?: v for ??=UnsupportedSyntaxError. Go has neither a null-coalescing
operator nor a conditional expression, and this generator maps a nullable
T? onto a plain Go T, which for a value type cannot be compared to
nil — any rendering would be code that does not compile. Representing
T? as *T throughout the Go generator is separate work.Also adds an overridable resolveASTAssignmentOperatorText, which lets the
Python generator drop its whole generateASTExpressionVariableAssignment
override (it existed only to spell integer division //=).
Nullable type syntax (`T?`). The Dart grammar now parses the ? suffix on simple, generic, collection and function types (String?, List , Map , Future
T?). The Dart grammar now parses the ? suffix on
simple, generic, collection and function types (String?, List<String?>,
Map<String, User?>, Future<User?>, void Function()?,
String? Function(int)). ASTType carries an isNullable/nullable flag
(via asNullable() / withoutNullability()), and the Dart/Kotlin generators
round-trip the ? suffix (TypeScript renders nullable types without the
suffix — T? on a member isn't valid TS — and other targets drop it,
best-effort).?? (null-coalescing), ??= (null-coalescing assignment), ?.
(null-aware getter and method invocation), ?[ (null-aware indexing), postfix
! (null assertion — standalone x! and before access: x!.f, x!.m(),
x![i] — throwing ApolloVMNullPointerException on null), and cascades
.. / ?.. (null-aware). ?? is wired into the operator-precedence
reducer as the loosest binary operator (relational/equality/logical tiers were
made explicit at the same time).ASTType.acceptsAssignment (used by the
runtime declaration/instance-of checks) accepts null only for nullable slots
and lets a T? slot accept a T value.NullSafetyAnalyzer) reports assigning null to a non-nullable
declaration/parameter and unconditional member/method/index access on a
nullable local, with flow promotion for if (x != null) { … } /
if (x == null) … else { … } and suppression via ?./!. Its diagnostics are
surfaced through the LSP analyzer for Dart documents.?: 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).x! compiles to x, a ?? b to its left operand,
?./?[ to plain access, and a nullable T? to the underlying numeric type.
A null literal stays an explicit unsupported-construct error (Wasm has no
null value) rather than a silent miscompilation.textDocument/references now honors context.includeDeclaration. The
flag was plumbed through every layer but discarded, so the declaration
occurrence was always returned. When includeDeclaration is false, the
occurrence that coincides with the symbol's declaration is now excluded.documentSymbol range covers the whole declaration. A
field/variable range previously stopped at the name; it now extends to the
terminating ; (skipping over any (/[/{ } in an initializer), matching
how methods and enum members already cover their full entry.double (e.g. 1000.0) or as strings, so
timeoutMs/maxDepth/args/className were coerced via new
mcpCoerceInt/_strOrNull/_listOrEmpty helpers rather than an unsafe
as int?/as List? that threw a raw TypeError and crashed the tool. Applies
to apollovm.execute/apollovm.ast and both isolate executors.HttpSseTransport now
responds to OPTIONS with Access-Control-Allow-Origin/Methods/Headers
(204) instead of a 404, unblocking cross-origin browser POSTs.&^ (AND NOT / bit clear) now computes a & (~b). It was mapped to a
plain bitwise-AND (a & b), silently producing wrong results and corrupting
Go→Go round-trips. It is now desugared at parse time to a bitwise-AND whose
right operand is the bitwise complement of the right-hand side, at the same
precedence as &.... A + whose operands are
statically String-typed (e.g. s1 + s2, with no string literal to trip
the old heuristic) now generates Lua .. instead of an invalid +.list[0]) now generates list[1]
(list[i] → list[(i) + 1]). Map/table key access is left unchanged. The Lua
grammar does not parse index-access expressions, so no Lua→Lua round-trip is
double-shifted... (e.g. "${a < b}") is now wrapped in
parentheses ("x" .. (a < b)) so it no longer mis-associates under Lua's
concatenation precedence.Nothing published for this version
The on-the-fly WebAssembly compiler now compiles String == String and String != String to content equality via the __streq synth helper (the same byte
String == / String != content equalityThe on-the-fly WebAssembly compiler now compiles String == String and
String != String to content equality via the __streq synth helper (the same
byte-comparison already used for switch cases and Map<String, …> key
lookups), instead of leaving the two String handles to flow into a numeric
comparison — which tested pointer identity and, when the handles reached an
i64.eq, produced invalid Wasm. The result is a proper bool, so it can be
returned directly, used as an if/&&/|| condition, or otherwise combined
logically. != inverts the helper's result with i32.eqz. Covers
literal/variable operands and the empty string.
The on-the-fly WebAssembly compiler now supports a generic class with a type-parameter field (class Box { T value; ... }). The field is stored boxed (
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 an
Object → String / bool / instance unbox case alongside the existing numeric
one. Covers Box<int|double|String|bool>, generic fields in arithmetic, and
multi-parameter classes such as Pair<int, String>.
Also adds regression tests confirming that returning a List/Map across the
module boundary works (List<int|double|String> via literal, arrow, or
built-with-.add, and Map<String,int>).
With this, every lettered gap in WASM_BACKEND_PLAN.md (A–G) is closed.
Adds String[i] — Dart's index operator, which returns the character at i as a length-1 String — to the AST interpreter core (which was missing it, thr
s[i] (interpreter + Wasm)Adds String[i] — Dart's index operator, which returns the character at i as a
length-1 String — to the AST interpreter core (which was missing it, throwing a
null-pointer error) and to the on-the-fly WebAssembly compiler. Both backends now
agree. This restores functionality briefly dropped in 2.11.0, where s[i] had
been mistaken for invalid Dart; it is valid, so it is now supported end to end.
The on-the-fly WebAssembly compiler now supports String.split(sep), returning a List . It is compiled as two passes over the [len][utf8] layout: the f
splitThe on-the-fly WebAssembly compiler now supports String.split(sep), returning a
List<String>. It is compiled as two passes over the [len][utf8] layout: the
first counts the separators to size the list (pieces = count + 1), the second
allocates each piece as a fresh String and stores its pointer in the list buffer.
Handles multi-char separators and leading/trailing empty pieces; an empty sep
yields a single whole-string piece (Dart's char-split for '' is a follow-up).
With this, the String method surface in Wasm is broadly complete (case, length,
slice/search, trim/pad, replace, compareTo, split). The main remaining Wasm
backend gaps are value-representation ones: generic-typed fields and returning a
List/Map across the module boundary.
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 ba
compareTo (interpreter + Wasm)Adds String.compareTo(other) to the AST interpreter core (which was missing it)
and to the on-the-fly WebAssembly compiler, so it now works on both backends with
matching results. The Wasm implementation is a lexicographic byte comparison over
the [len][utf8] layout, returning -1 / 0 / 1 (a shorter string that is a
prefix of the other sorts first).
Still to come for String in Wasm: split (returns a List) and index [].
The on-the-fly WebAssembly compiler now supports replaceAll(from, to) and replaceFirst(from, to) over its [len][utf8] layout. Each is compiled as two
replaceAll / replaceFirstThe on-the-fly WebAssembly compiler now supports replaceAll(from, to) and
replaceFirst(from, to) over its [len][utf8] layout. Each is compiled as two
passes — the first counts non-overlapping matches to size the output buffer, the
second builds it (copying to for a match, else one byte). Handles a replacement
that grows, shrinks, or removes the match, and matches at either end; an empty
from returns a copy (avoiding a non-terminating scan).
Still to come for String: split (returns a List), index [], and compareTo
(which also needs String.compareTo in the interpreter core first).
The on-the-fly WebAssembly compiler now supports the String trim and pad methods over its [len][utf8] layout: trim() / trimLeft() / trimRight() (ASCII
The on-the-fly WebAssembly compiler now supports the String trim and pad methods
over its [len][utf8] layout: trim() / trimLeft() / trimRight() (ASCII
whitespace stripping) and padLeft(width, [pad]) / padRight(width, [pad])
(single-byte pad, default space). These join the slice/search methods added in
2.7.0. Byte-indexed, so exact for ASCII text.
Still to come: split, replaceAll / replaceFirst, index [], and
compareTo (which also needs String.compareTo in the interpreter core first).
The on-the-fly WebAssembly compiler now supports the common String slice/search methods over its [len][utf8] layout: substring(start, [end]) (a fresh-
The on-the-fly WebAssembly compiler now supports the common String slice/search
methods over its [len][utf8] layout: substring(start, [end]) (a fresh-buffer
memory.copy slice), codeUnitAt(i), startsWith / endsWith, indexOf, and
contains (byte scans, guarded against out-of-bounds reads). These join the
already-supported length / isEmpty / isNotEmpty getters and
toUpperCase / toLowerCase. Methods are byte-indexed, so results are exact for
ASCII text.
Still to come: trim, split, replaceAll / replaceFirst, padLeft /
padRight, compareTo, and index []; chaining a method onto a method result
(the receiver must be a named local).
The on-the-fly WebAssembly compiler now supports collections nested inside collections and chained subscript access. A nested collection is stored as
m[0][1])The on-the-fly WebAssembly compiler now supports collections nested inside
collections and chained subscript access. A nested collection is stored as a
pointer to the inner header, so a List/Map literal can hold List/Map
elements ([[1, 2], [3, 4]], {'a': {'b': 5}}), and chained subscripts read and
write through every level (m[0][1], m['a']['b'], m[0]['x'], m['k'][1]),
including compound assignment (m[0][1] += 5) and multi-level nesting. Nested
literals use depth-offset scratch locals so an inner literal never clobbers the
enclosing one's buffers; single-level collections stay byte-identical.
Writing into an innermost Map (list[0]['k'] = v) — reads of that shape
already work — and a subscript/method on a non-variable receiver (getList()[0],
m[1].length) remain follow-ups.
The on-the-fly WebAssembly compiler now supports user-declared instance getters (int get x { ... }). A getter is synthesized as a zero-argument instan
The on-the-fly WebAssembly compiler now supports user-declared instance getters
(int get x { ... }). A getter is synthesized as a zero-argument instance method,
so an access via a receiver (c.x) lowers to a 0-arg method call — reusing the
whole instance-method path (argument marshalling, return conversion) and the
superclass-chain resolution, so inherited and overridden getters resolve just like
methods. Covers int/double/bool/String getters, a computed-expression
getter body, and a getter used inside an expression (read once or twice), plus
inherited and overridden getters.
Bare getter access inside a method body (x resolving to this.x, no receiver)
and setters remain follow-ups (bare access is not resolved by the interpreter yet
either).
The on-the-fly WebAssembly compiler now supports single inheritance, matching the interpreter. A subclass instance carries its superclass's fields fir
extends / super)The on-the-fly WebAssembly compiler now supports single inheritance, matching the
interpreter. A subclass instance carries its superclass's fields first in its
heap layout, so an inherited field sits at the same offset as on the superclass —
a superclass method, compiled once, reads/writes the correct slot when invoked on
a subclass instance. Method resolution walks the extends chain (an override on
the subclass wins, otherwise the inherited superclass method), the subclass
constructor runs inherited field initializers, and super.method(args) keeps the
current instance as the receiver while dispatching to the superclass (skipping the
override). Covers inherited method calls (bare and via a receiver, with
arguments), inherited field read/write (including double), own+inherited fields
at distinct offsets, override-wins, super.method()/super.method(args), and
multi-level extends chains.
Dispatch is static (by the receiver's declared type); virtual dispatch through an
upcast receiver, super.field/super.getter, and inherited user-getters remain
follow-ups. Static members are intentionally not inherited (matching Dart).
The on-the-fly WebAssembly compiler now supports static class fields (bringing it toward parity with the interpreter, which gained them in 2.1.1). Eac
static class fieldsThe on-the-fly WebAssembly compiler now supports static class fields (bringing
it toward parity with the interpreter, which gained them in 2.1.1). Each static
field becomes a typed, mutable module global seeded with its literal
int/double/bool initializer; a bare reference inside a static method
reads and writes it (including compound assignment like c += 1), and values
persist across calls. Qualified Class.field from another class, inherited
static fields, and non-literal initializers remain follow-ups.
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 assignme
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.
extends) is now functionalextends was parsed but had no runtime effect — inherited methods and fields
were invisible and super was unresolved. Now:
base() inside a subclass) and receiver calls (obj.base()); a subclass
override still wins over the inherited method.static fields read/write the base
class that declares them (Sub.staticField).super dispatches to the parent: super.method() calls the overridden
parent method (resolved relative to the class where the call is written, so it
is correct for multi-level hierarchies), super.getter reads the parent
getter, and super.field reads/writes the inherited field.extends), C# (: Base) and JavaScript (extends)
grammars now record the superclass they were previously dropping, so
inheritance works across those languages (and TypeScript / Python) too.
(Kotlin's class B : A() base clause is not parsed yet.)Not yet supported: constructor initializer lists with an explicit
super-constructor call (B(v) : super(v)) — inherited fields are still set from
a constructor body or a this.param.
static fields previously had no class-level storage — they were initialized onto every instance, and reading one (ClassName.field, or a bare reference
static class fields are now supportedstatic fields previously had no class-level storage — they were initialized
onto every instance, and reading one (ClassName.field, or a bare reference
inside a static method) threw at runtime. Now each class has a lazily
initialized static-field store, shared by qualified ClassName.field access and
bare references inside the class's own static methods, for read, write and
compound assignment. The Java and C# grammars also now record the static field
modifier (they were dropping it), so this works across those languages too.
~/= no longer crashes the parser. ~/= (Dart) and
//= (Python) matched the grammar but had no handler, so parsing threw an
uncaught error out of loadCodeUnit instead of building the AST. ~/=///=
are now fully supported, and any other still-unsupported compound operator
(e.g. JavaScript/TypeScript %=) now surfaces as a clean SyntaxError
instead of an uncaught crash.Enum.values can be assigned to a typed or inferred list. It was built
with a dynamic element type while its declared type is List<Enum>, so
var v = Color.values; / List<Color> v = Color.values; failed the
declaration cast. It now carries the enum's own element type.ASTValue.fromValue<int> on a non-whole double now throws a clean
ApolloVMCastException instead of a raw Dart TypeError.Division semantics are unchanged and remain intentionally per-language
(Dart/JS/TS/Python / yields a double; Java/C#/Go/Kotlin / on ints is
truncating integer division); regression guards now pin both.
Fixed: a `++`/`--` statement inside a `while`/`do-while` body produced an invalid module. A bare increment/decrement (i++, ++i, i--, --i) used as a st
++/-- statement inside a while/do-while body produced an
invalid module. A bare increment/decrement (i++, ++i, i--, --i) used
as a statement left the operator's value on the operand stack; inside a loop's
void block that value reached the block end and the compiled module failed
WebAssembly validation ("values remaining on stack at end of block"). Only
explicitly int-typed counters written as i = i + 1 happened to avoid it.
The discarded value is now dropped in statement position; the expression form
(x = i++) still yields its value, and for-header updates are unchanged.String.length, .isEmpty, .isNotEmpty compile to Wasm. They read
the string's [len:i32][utf8] header length word.String.toUpperCase() / .toLowerCase() compile to Wasm for ASCII
text — a fresh buffer is allocated and each ASCII letter is shifted by the
case bit; other bytes are copied unchanged.The stored String length is the UTF-8 byte count, so these match Dart semantics
for ASCII input. See WASM_BACKEND_PLAN.md for the re-verified backend status
(8 of the 9 previously documented gaps already work) and the remaining gaps.
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#).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
A Dart enum constant is a constructor invocation, but the language server's
declaration scanner recorded its range as the name alone: earth(5.97, 6371)
selected only earth, dropping the arguments. Every other declaration kind has
its range extended past the name — a class member's covers its parameters and
body — so enum constants were the one exception, and folding, hover and the
document outline all saw a truncated span.
.named
constructor, <T> type arguments, the argument list, or an = value
(Red = 1). Its name span is unchanged, so go-to-definition and rename
still target the name alone., even when nested inside the
argument list, so scanning resumed mid-arguments and read the argument label
as another constant: earth(mass: 5.97, radius: 6371) emitted a phantom
radius enum member into the outline. Entry consumption is now depth-aware —
only a ,/;/} outside brackets ends an entry — which fixes both the
truncated range and the phantom symbols.Constructors declared in an enum body (const Planet(this.mass);) were
already correct and are unchanged.
Executing a compiled Wasm module on the Dart VM needs a native engine (package:wasm_run), which brings an FFI/Rust toolchain and a long-abandoned flut
apollovm no 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-abandoned
flutter_rust_bridge 1.x with it. That dependency was paid by every consumer
of apollovm — including the many that only parse, translate or generate code
and never execute Wasm at all. Worse, flutter_rust_bridge 1.x pins
shelf_web_socket ^1.0.2 and web_socket_channel ^2.2.0, so anything depending
on apollovm (however indirectly) could not also use a modern shelf/WebSocket
stack.
The native runtime now lives in its own package, apollovm_wasm. Nothing
else moved: Wasm compilation is unchanged and still in core.
package:wasm_run is no longer a dependency. On the Dart VM,
WasmRuntime() now returns an unsupported runtime
(isSupported == false) unless the native engine is registered. Wasm still
compiles; it just cannot be executed out of the box.
To execute Wasm on the VM, add apollovm_wasm and register it once:
import 'package:apollovm/apollovm.dart';
import 'package:apollovm_wasm/apollovm_wasm.dart';
void main() {
registerApolloVMWasmRuntime();
final runtime = WasmRuntime()..ensureBooted();
print(runtime.isSupported); // true
}
Browsers are unaffected — they have a Wasm engine built in, and the browser runtime remains in core.
createWasmRuntime() is no longer part of the VM conditional-import
chain. The platform default is now resolved through
WasmRuntime.registerProvider(), which apollovm_wasm calls.
WasmRuntime.registerProvider() — installs the implementation that
WasmRuntime() instantiates, so an engine can be supplied from outside the
package.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
Every ApolloVM grammar's top rule is compilationUnit().star().trim().end().
When a syntax error appeared deep in a file, the last top-level definition failed
deep in the input, but star() treated that as "stop, success" and discarded
the failure, so end() reported a generic end of input expected at offset 0
(line 1) — technically correct, useless in an editor. petitparser's plain
parse() does not track the farthest failure, so the deep position was lost.
Parsers now record the farthest point the grammar actually reached and report the error there, at or immediately adjacent to the real mistake:
ApolloSourceCodeParser.trackFarthestFailure. When a parse fails,
the source is re-parsed with a copy of the grammar (built via petitparser's
transformParser + callCC) in which every parser records each Failure it
produces if it is the deepest seen. This captures failures that star() /
optional() would otherwise swallow. The deeper of the two positions is used,
so the reported position never gets worse. The happy path is untouched — the
fast plain parser still runs first; the tracking re-parse only runs on failure.locateParseError now runs its structural heuristics (bracket-imbalance,
missing ;) before trusting the raw parser position, then falls back to the
now-precise parser position. Editor-friendly locations (a missing ; reported
at the end of the offending value; '(' is never closed) are preserved, while
the fallback for every other failure is far better than the old offset 0.Three defects made Dart -> Go -> parse -> run fail (or, worse, silently produce the wrong answer) for programs the 1.9.1 fixtures did not cover.
Three defects made Dart -> Go -> parse -> run fail (or, worse, silently
produce the wrong answer) for programs the 1.9.1 fixtures did not cover.
Point(this.x, this.y) shorthand generated
func NewPoint(x int, y int) *Point { o := &Point{}; return o } — the
arguments were never assigned, so every field stayed zero-valued and the
program ran to a wrong result rather than failing. The factory now emits
o.x = x for each this. parameter, after the field's declared initial
value so an explicit argument wins. The long-hand form
(Point(int x, int y) { this.x = x; }) was already correct.var map = ... or a field named type became map := ... /
o.type, which does not parse — map and type are two of Go's 25
reserved words. Such a name is now escaped with a trailing _
(map_, type_) at every site: declarations, reads, parameters, struct
fields, methods and the members written after a ..NewPoint(...) against the declarations it has already
seen, so a top-level function emitted first could not resolve the struct.
generateASTRoot now emits structs (and their factories) before the
top-level function block.Supporting change:
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.ApolloVM's Go parser remains order-dependent: hand-written Go that calls
NewPoint(...) before declaring type Point struct still fails to resolve,
though Go itself is order-independent. The generator no longer produces that
shape.
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 her
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.
String.length, String.isEmpty, String.isNotEmpty and the sign of
int/double only worked when called. s.length() worked and s.length
threw — the opposite of how Dart, Kotlin and C# source reads. List and Map
already exposed theirs as getters. They are now getters and methods, so
Java-style s.length() keeps working.new HashMap<>(){{ put("a", 1); }} — exactly what the Java generator emits for
a Dart map literal — handed the '>' character to a cast expecting an
ASTType and threw a raw TypeError. A separate typo made the map literal's
diamond alternative match << instead of <>. Generic type arguments are now
picked out by type rather than by position, in all four list/map literal rules.
A Dart map literal now round-trips through Java.&T{} composite literal, so any Go source containing
a func NewFoo() *Foo { o := &Foo{} ... } factory — the exact shape the Go
generator emits — failed to parse. The zero-valued form now parses as the
struct's no-argument constructor. Field-initializing literals (&Foo{x: 1})
remain unsupported.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.o was a plain local, so o.x = x failed with "Can't
find variable: 'o'". It is now bound as the receiver before the body parses,
which the factory-to-constructor conversion already assumed.Point(int x) { this.x = x; } emitted o.x = o.x, silently assigning the
field to itself. A parameter now shadows a same-named field.p := Point(a, 1), which is not Go. Instantiation
now emits the factory call p := NewPoint(a, 1), and every struct gets a
factory (a field-less struct used to get none) so the call always resolves.
This is the only change to the expected output of the go_*.test.xml
fixtures.Dart -> Go -> parse -> run now works end to end.
ApolloGenerator carried ~140 lines of dead dispatch (generateASTNode,
generateASTRoot, generateASTType, generateASTStatement,
generateASTBranch, generateASTExpression, generateASTValue). Every one
was shadowed by both subclasses, and they had already drifted: the base
generateASTRoot never learned to emit extensions, so a future third
generator would have silently dropped them. They are now abstract, or removed
where nothing dispatches through the base.dart:math, and every String, int, double,
List and Map member, in both getter and method form where both exist.Several grammars parse only a subset of what their own generator emits: the Lua
generator emits a + b for string concatenation and s.m() for method calls,
where Lua wants a .. b and s:m() (Lua classes are already marked unsupported
in the README matrix, so that generator is outside the stated contract); Python
cannot parse the lambda it generates; JS and TS cannot parse the map literal
they generate. Each is listed in
test/unit/apollovm_generator_matrix_test.dart.
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.
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.
extension NumExt on int { int doubled() { return this * 2; } int get twice { … } }
(the name is optional).fun Int.doubled(): Int { … } and val Int.twice: Int get() = …;
members are grouped by receiver into one extension.static class NumExt { public static int Doubled(this int self) { … } }; the
self-parameter and this are translated in both directions. C# has no extension property,
so an extension with a getter cannot be emitted as C#.Extensions apply to core types (int, String, …) as well as user classes, support
overloads, and may call one another (this.doubled() or plain doubled()). A class member
always wins over an extension member. Extensions are module-local: they are not carried across
import. Java, JavaScript, TypeScript, Python, Go and Lua have no equivalent construct, so
asking for one throws UnsupportedSyntaxError instead of emitting a shim that would mean
something else.
Supporting changes:
int get x { … } / => …) in class bodies too; the
ASTClassGetterDeclaration node existed but no grammar reached it.ASTExtension node and ASTRoot.extensions registry, consulted as a fallback when a
receiver's own class lookup misses.'${a.doubled()}') were never linked into the
AST parentNode chain, so they could not resolve anything reached through it.ASTGetterDeclaration now serializes its name, return type and
modifiers instead of appearing as an anonymous node.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 ove
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.
RemoteRepositoryAdapter (web-safe, in
package:apollovm/apollovm_repository.dart): a RepositoryAdapter that talks
to a repository server over HTTP, implementing the full interface (filesystem,
search, git). Connect with
RemoteRepositoryAdapter.connect('http://host:port'); it reports
RepoCapabilities.isRemote. Backed by package:http, so it runs in the
browser.RepositoryRpc (web-safe): a transport-agnostic JSON dispatcher that
maps {op, ...args} requests to a RepositoryService. A shared Op constant
set is the single source of truth for the wire contract, so server and client
can't drift.fromJson on every repository value type (RepoFile, RepoEntry,
RepoStat, RepoEdit, TextMatch, RepoCapabilities, and the git types),
completing the JSON round-trip alongside the existing toJson.tool/repository_server.dart: a lightweight HTTP server exposing a
local checkout (via LocalRepositoryAdapter) over POST /rpc, with reflected,
permissive CORS so a browser app on another origin can reach it. Flags:
--workspace, --allow-write, --allow-git-write, --port, --address.
The server remains the authority on permissions regardless of what a client
requests.New standalone libraries `package:apollovm/apollovm_repository.dart` (web-safe) and `apollovm_repository_io.dart` (adds the on-disk adapter). The repo
package:apollovm/apollovm_repository.dart
(web-safe) and apollovm_repository_io.dart (adds the on-disk adapter). The
repository features are not MCP-exclusive — a web IDE, a desktop editor, an
agent or a test can use them directly via RepositoryService, a typed
façade returning typed results (no MCP/JSON). It bundles the filesystem/search/
git operations of a pluggable RepositoryAdapter with language-aware code
navigation (outline/definition/references/hover/diagnostics/
workspaceSymbols/searchSymbols) powered by ApolloVM's parsers via an
in-process LspService.RepositoryAdapter implementations: LocalRepositoryAdapter
(dart:io + git), InMemoryRepositoryAdapter (web-safe), plus room for a
remote/web backend — enabling file edits and git commands from the browser. A
PermissionGuard decorator enforces a RepoConfig uniformly across backends.apollovm.fs.*, apollovm.search.*, apollovm.code.* and
apollovm.git.* tools are a thin JSON layer over RepositoryService, letting
an agent read, search, navigate, edit and version-control real files instead of
shelling out to POSIX (cat, ls, find, grep, sed, git). Enabled by
serving with --workspace <dir> (or by passing a RepositoryAdapter to
ApolloMcpServer/serveStdio/HttpSseTransport); with no workspace the server
stays inline-source-only exactly as before.--allow-write / --allow-git-write). Paths are confined to the
workspace root (../absolute rejected), git is invoked with an argument list
(never a shell) pinned to the root, and fs.edit accepts an atLine safety
anchor (made mandatory by --require-line-match).RepositoryAdapter,
and the apollovm mcp list/schema/info CLI advertise them only with
--workspace. mcp call invokes them via --json-args + --workspace.A member with no `{ ... }` body is now fully covered by its `documentSymbol` range. A ;-terminated constructor (const Foo(this.x);, a redirecting Foo.
{ ... } body is now fully covered by its documentSymbol
range. A ;-terminated constructor (const Foo(this.x);, a redirecting
Foo.zero() : this(0);) or an abstract method previously stopped its range at
the name, so selecting it excluded the parameters. The range now extends to
the terminating ;, covering the whole signature — parameters and any
initializer list. Applies to class and enum constructors alike; bodied and
expression-bodied members are unchanged.Members with an expression body are now fully covered by their `documentSymbol` range. A => expr; member (Dart/C#) previously stopped at its name, and
documentSymbol range. A => expr; member (Dart/C#) previously stopped at
its name, and a brace-less = expr member (Kotlin) even swallowed the next
member because the scanner never recovered. The token indexer now consumes the
expression: the => form extends to its terminating ;, and the brace-less
= form is bounded to the end of its line (so the following member is still
recognized). Balanced ()/[]/{} inside the expression (e.g. a map literal
body => {1: 2}) are handled without corrupting brace tracking. Works for
both class and enum members.Enum members declared after the constant list are now recognized as real members. Everything after the ; that ends an enum's constant list (fields, co
; that ends an enum's constant list (fields,
constructors, methods) was being swallowed as bogus enum constants (named
int, const, final, …), so documentSymbol gave enum methods no body
range and did not recognize enum constructors at all. The token indexer now
tracks the constant-list terminator per enum: after it, identifiers in the
enum body are parsed like class members — real kinds and full-body ranges.
Pure-constant enums (enum E { a, b, c }) are unaffected; class constructors
were already handled correctly.`documentSymbol` ranges now span a member's whole body, not just its signature. Previously only class/enum declarations had their range extended to th
documentSymbol ranges now span a member's whole body, not just its
signature. Previously only class/enum declarations had their range
extended to the closing }; a function/method/constructor stopped at the end
of its name, so an editor "select symbol" or outline action only highlighted
the signature. The token indexer now tracks member bodies too — skipping the
parameter list first, so Dart named-parameter braces ({ ... }) are not
mistaken for the body — and extends fullEnd to the body's closing }.
Bodyless members (abstract/;-terminated) are left unchanged. The narrower
selectionRange (the name) is untouched.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.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.Completion now surfaces in-scope identifiers, and works while the buffer does not parse. Previously textDocument/completion only offered AST symbols (
textDocument/completion only offered AST symbols
(top-level/members) plus keywords — so mid-edit, when the parse fails (the
common case), it fell back to keywords only. It now also harvests identifiers
from the raw token stream, which survives a failed parse and includes local
variables and parameters the symbol table omits. Results are de-duplicated and
ranked: local-scope symbols, then other symbols, then harvested identifiers,
then keywords. The partial token under the cursor is skipped.;. ApolloVM's PEG parser reports
a generic "end of input expected" at offset 0 for most structural mistakes;
the LSP layer already recovered the real position for bracket imbalances, and
now also for a missing statement terminator in ;-required languages (Dart,
Java, C#). A balanced-but-unterminated statement is pinned to the end of the
offending value (with an expected ';' hint) instead of defaulting to the top
of the file. The heuristic is conservative — it skips continuation shapes
(operators, ., ?, :, ,, open brackets, continuation keywords like
return, and annotations) and does not apply to languages where ; is
optional/absent (Kotlin, JavaScript, Lua, Python).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
https://apollovm.github.io/apollovm_web_example/), not the old
/www/ path.`package:apollovm/apollovm_mcp.dart` is now fully web-safe — it imports no dart:io and no dart:isolate, so the MCP server and every apollovm.* / apoll
package:apollovm/apollovm_mcp.dart is now fully web-safe — it imports no
dart:io and no dart:isolate, so the MCP server and every apollovm.* /
apollovm.lsp.* tool compile and run in a browser (dart2js / DDC). Construct
ApolloMcpServer on any StreamChannel<String> (e.g. a web MessageChannel)
and drive all tools in-process. (In 1.4.0 the apollovm.lsp.* tools were
documented as web-safe, but apollovm_mcp.dart still transitively imported
dart:io/dart:isolate, so a web build failed to compile.)dart:io-only pieces moved to a new package:apollovm/apollovm_mcp_io.dart
(which re-exports apollovm_mcp.dart): serveStdio, HttpSseTransport and
the CommandMcp CLI group. Native embedders and the apollovm CLI import
this. Migration: if you imported serveStdio / HttpSseTransport /
CommandMcp from apollovm_mcp.dart, switch that import to
apollovm_mcp_io.dart (a one-line change).wasm_runtime.dart): a killable-isolate executor on native, and an in-process
executor with a cooperative timeout on the web (no dart:isolate). Tools in
McpLimits.isolateTools therefore still run, degrading to a soft timeout on
the web.test/mcp/web_compat_test.dart runs the tools, the
in-process LspClient and the server under dart test --platform chrome,
guarding the web-safe surface against future dart:io/dart:isolate leaks.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 prec
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).LspService;
no socket, no dart:io), runs in-process or inside an isolate like the other
bounded tools, and honors the existing maxSourceChars limit. The parser is
selected from the language argument, so a mismatched URI cannot change it.package:apollovm/apollovm_mcp.dart: LspRuntime,
buildLspTools, computeLspTool, isLspTool, lspToolNames. The CLI
apollovm mcp call gained --line, --character and --query flags.example/apollovm_example_mcp_lsp.dart.LspService in package:apollovm/apollovm_lsp.dart: a
document-oriented facade that embeds an in-process LspServer and exposes the
language features as plain typed Dart calls — no transport, no socket and no
initialize/initialized handshake to run by hand. Being dart:io-free it
runs unchanged in a browser (compiled to JS) or an AI agent.
analyze(uri, text) returns the buffer's diagnostics directly;
open/change/close manage documents (versions tracked automatically);
every query resolves against the current buffer.hover, definition, documentSymbols, completion,
references, documentHighlight, prepareRename, rename,
workspaceSymbols, plus a diagnostics stream and a ready
(InitializeResult) future.LspService.wrap(client) layers the same convenience over an existing
LspClient (e.g. one connected to a remote server).example/apollovm_example_lsp_api.dart
drives the whole flow through LspService with no transport wiring.`textDocument/documentHighlight` — highlights every occurrence of the identifier under the cursor, marking the declaration site as Write and other use
textDocument/documentHighlight — highlights every occurrence of the
identifier under the cursor, marking the declaration site as Write and other
uses as Read (documentHighlightProvider capability).textDocument/prepareRename — validates a rename target, returning its
range and placeholder, or null when the cursor is not on an identifier
(advertised via the rename provider's prepareProvider).LspClient in package:apollovm/apollovm_lsp.dart: a JSON-RPC client
that consumes the ApolloVM LspServer. It correlates responses to the
requests it sends, streams server-pushed diagnostics
(Stream<PublishDiagnosticsParams> get diagnostics), and exposes typed
helpers — initialize, didOpen/didChange/didClose, hover,
definition, documentSymbol, completion, references,
documentHighlight, prepareRename, rename, workspaceSymbol,
shutdown/exit — plus low-level sendRequest/sendNotification.LspClient.inProcess() pairs a client with a fresh LspServer over a
linked MessageLspEndpoint pair, all in one isolate — no subprocess, no
dart:io. The client works over any LspEndpoint, so an out-of-process
server can be driven with LspClient(StreamLspEndpoint(out, in)).LspEndpoint now routes JSON-RPC responses (previously ignored) to a new
onResponse hook, enabling the client role. Pure-server usage is unchanged.fromJson factories (Hover, Location,
Diagnostic, DocumentSymbol, CompletionItem, TextEdit, WorkspaceEdit),
and new types were added: InitializeResult, ServerInfo,
PublishDiagnosticsParams, WorkspaceSymbol, CompletionList,
DocumentHighlight/DocumentHighlightKind, PrepareRenameResult.example/apollovm_example_lsp.dart
drives a full session (handshake, diagnostics, symbols, hover, definition,
completion, highlight, prepare-rename, rename) through
LspClient.inProcess().Fix broken 1.2.0 package on pub.dev: the .pubignore pattern lsp/ was unanchored, so besides the intended root lsp/ dev-tooling directory it also strip
.pubignore pattern lsp/ was
unanchored, so besides the intended root lsp/ dev-tooling directory it also
stripped lib/src/lsp/ (the LSP implementation) from the published archive,
making dart pub global activate apollovm fail to compile. The pattern is
now anchored as /lsp/..pubignore now also repeats the relevant .gitignore exclusions
(*.exe, *.iml, etc.), since a .pubignore file replaces .gitignore
for publishing — 1.2.0 accidentally shipped a 7 MB bin/apollovm.exe.A Dart-first language server is now part of the `apollovm` package, exposed as a separate library package:apollovm/apollovm_lsp.dart (the existing pac
apollovm package, exposed
as a separate library package:apollovm/apollovm_lsp.dart (the existing
package:apollovm/apollovm.dart exports are unchanged). Source lives in
lib/src/lsp/.apollovm lsp; and embedded / web — the library imports no dart:io, so a
browser IDE or an AI agent can drive it with decoded JSON-RPC messages via
MessageLspEndpoint (no byte framing). StreamLspEndpoint provides
Content-Length framing for stdio/sockets. Both share a transport-agnostic
LspEndpoint.initialize/shutdown, incremental diagnostics (parse +
unresolvable core imports), documentSymbol, hover (kind/signature/type/
documentation), definition; plus single-file references/rename and a
basic ranked completion.(/{) with a hint, instead of pointing at the top of the file.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).dart analyze (clean), 17 passing tests in test/lsp/
(including full stdio and message-level protocol sessions), and a benchmark
comfortably under its latency targets (open, hover, completion).package: imports can now be resolved against real pub packages, via an
optional importer exposed at package:apollovm/apollovm_pub.dart (kept out
of the web-safe apollovm.dart).
PackageProvider: PackageConfigProvider (default, VM-only, zero
extra deps — resolves through .dart_tool/package_config.json, exact pub
semantics) and PubDevProvider (web-compatible) — downloads archives
from pub.dev or a configurable/private/mirror host, extracts them in memory,
caches them (MemoryPackageCache by default, FilePackageCache on the VM),
and honors pubspec version constraints. Built on web-safe libraries only
(http, archive, pub_semver, yaml — no dart:io), so it runs on the
VM and in the browser.PubDevProvider accepts an injectable http.Client, a custom
host, and a rewriteUrl hook to route requests through a CORS proxy — a
ready-made proxy ships in tool/pub_cors_proxy.dart.DartPackageLoader + DartPackageImporter.provision() fetch each reachable
package: import transitively and load its source into the VM; injected via
the new settable ApolloVM.moduleLoader. A generic CompositeModuleLoader
chains loaders.apollovm run/translate --pub (with --pub-host / --pub-cache)
resolves package: imports before executing.http, archive, pub_semver, yaml to direct dependencies
(all web-safe); only the filesystem members (PackageConfigProvider,
FilePackageCache) are behind conditional imports with web stubs.doc/module_resolution.md and example/apollovm_example_pub_importer.dart.ASTStatementImport (named/show/hide, wildcard, whole-module
prefix alias, per-symbol alias) plus new ASTStatementExport and
ASTTypeAlias nodes.lib/src/resolution/): pluggable
ModuleLoader (in-memory VMModuleLoader), four-level SymbolTables +
ImportScope, ModuleResolver, a DependencyGraph (Tarjan cycle
detection, Kahn topological order, incremental affectedBy invalidation),
structured ImportDiagnostics (missing module/symbol, duplicate symbol,
circular import, invalid export), a ResolutionCache, and the
ModuleResolutionEngine facade.ApolloVM.resolve() returns aggregated diagnostics; resolution is triggered
lazily by the runner and invalidated incrementally on loadCodeUnit.show/
hide/wildcard/alias/re-export/typedef); other languages keep basic
imports and compile unchanged against the additive AST.<source> (cross-module) tests.doc/module_resolution.md and example/apollovm_example_imports.dart.Go.go / go, alias golang) through the shared AST,
bidirectionally with every other supported language (and on-the-fly Wasm).lib/src/languages/go/ (go_grammar_lexer.dart,
go_grammar.dart, go_generator.dart, go_parser.dart, go_runner.dart) and
wired into ApolloVM (getParser/createRunner/createCodeGenerator and the
.go file-extension mapping).func (o *Name) m(...)),
struct types with fields and factory constructors (func NewName(...) *Name),
var/:= type inference, if/else if/else, the four for forms (C-style,
condition-only as while, range as for-each, infinite / do-while), Go
switch (no fall-through), slices/maps ([]T{…}, map[K]V{…}), closures, all
arithmetic/comparison/logical/bitwise operators, string + concatenation, and
fmt.Println (normalized to the VM's print).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.apollovm mcp command group exposes ApolloVM as an MCP (Model Context
Protocol) server and tools, turning it into a programmable, sandboxed execution
engine for AI agents. Built on the official dart_mcp SDK. Subcommands:
mcp serve (run the server over stdio or HTTP/SSE via --http <port>),
mcp list (tool definitions), mcp call <tool> (one-shot tool invocation for
scripting/CI), mcp info, mcp schema, and mcp doctor.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.print is
exposed; inputs are inline source only); apollovm.execute runs in a killable
isolate so a hard timeout is enforced even against runaway synchronous loops
(per-tool configurable via --isolate-tools); input/output size caps
(--max-source-chars, --max-output-chars); best-effort process-level memory.package:apollovm/apollovm_mcp.dart (ApolloMcpServer,
serveStdio, HttpSseTransport, McpLimits, computeTool).dart_mcp: ^0.5.2 (and stream_channel). New mcp-tagged
integration tests under test/mcp/. See doc/MCP.md.`apollovm run foo.wasm` now runs the binary module via the Wasm runtime (ApolloRunnerWasm) instead of trying to decode it as UTF-8 source. The file is
run executes .wasm files through the Wasm runtimeapollovm run foo.wasm now runs the binary module via the Wasm runtime
(ApolloRunnerWasm) instead of trying to decode it as UTF-8 source. The file
is loaded as a BinaryCodeUnit, parsed for its exported functions by
ApolloParserWasm, and its entry function (e.g. main) is invoked — closing
the compile → run loop from the command line..wasm file extension now maps to the wasm language in
ApolloVM.parseLanguageFromFilePathExtension.`/` on integer operands is now integer (truncating) division in Java/Kotlin/C# — where the expression resolves to int — instead of an f64 quotient (so
/ on ints) + division-by-zero/ on integer operands is now integer (truncating) division in
Java/Kotlin/C# — where the expression resolves to int — instead of an f64
quotient (so 10 / 3 is 3, not 3.33). Dart's / keeps its double
result; ~/ is unchanged.IntegerDivisionByZeroException for /,
Unsupported operation: Infinity or NaN toInt for ~/). Applies to both ~/
and integer /.print whose argument is built from a value that just raised (e.g. the
quotient in print('q = ${a ~/ b}')) is skipped when an exception is
pending, matching the interpreter, which never reaches the print.These fix the Dart, Java and Kotlin exception (try/catch/finally) examples.
`num` (a TypeScript/JavaScript `number`) is now supported in the Wasm backend. A plain num has no fixed width; the VM treats integer-valued numbers as
num (TypeScript/JS number) + switch on a boxed scrutineenum (a TypeScript/JavaScript number) is now supported in the Wasm
backend. A plain num has no fixed width; the VM treats integer-valued
numbers as int, so num is represented as i64. This fixes string
interpolation/concatenation of a num (e.g. "sum=" + (a + b)), switch on
a num scrutinee, and num arithmetic — unblocking the TypeScript Class,
Conditional, Exceptions and Switch examples.switch on a boxed dynamic/Object scrutinee (e.g. a List<Object>
element) now compiles: the scrutinee is unboxed to a concrete i64 to drive
the integer branch table.Object/dynamic entry-point parameters are now marshalled. An
untyped parameter (e.g. a JavaScript/Python main(a, b), or an explicit Dart
dynamic parameter) is passed as a host-allocated box instead of a raw scalar
the module would read as a garbage pointer — fixing the JavaScript, Lua and
Python Class/Conditional/Switch examples, which previously ran with all
arguments seen as 0. (The apollovm_sig section is now emitted whenever a
public function has an Object/dynamic parameter, and a plain num is
tagged as int so it is passed as a raw i64 rather than boxed.)n => n * 2 from
C#/Lua/Python) now compile: the parameter type is inferred from its body. A
nested closure's return no longer makes the enclosing (void) function
non-void.let twice = (n) => …, which
JavaScript/TypeScript parse as a function declaration rather than a var) are
hoisted and lowered to a direct call.Object/dynamic slot. A concrete
value passed to a generic T field/parameter (represented as a boxed
Object) is boxed; a boxed value used in arithmetic or passed to a typed
numeric parameter is unboxed. This makes generic Box<T> (Dart, Java, Kotlin,
C#, TypeScript) work.var whose initializer is a boxed-operand
expression (e.g. var s = a + b where a/b are Object[]/List<Object>
elements) is refined to the result's concrete type, and the == 0 fast path
(i64.eqz) unboxes a boxed operand first. Fixes the Java Class example and the
JavaScript try/catch example.`Map`/`List` → `String` coercion in print(...) / string interpolation (e.g. print('Map: $m'), '$list'). Renders Dart's {k: v, …} / [e, …] form by scan
Map/List → String coercion in print(...) / string interpolation
(e.g. print('Map: $m'), '$list'). Renders Dart's {k: v, …} / [e, …]
form by scanning the runtime key/value (or element) buffers and coercing each
entry through the existing string-coercion path. (Nested collections inside a
Map/List toString still throw a clear UnimplementedError.)Object/dynamic operands, such as
values read from a List<Object> (args[1] + 5, args[2] ~/ 2,
args[3] * 3, c > 120). These carry a box pointer, not a number; they are
now unboxed to a concrete numeric value (dispatching on the runtime box tag:
int→i64, double→f64) before the operation, instead of feeding the pointer
into i64.add/f64.div (which produced invalid Wasm).Object value flowing into a typed numeric Map/List slot (e.g.
<String,int>{'a': a} where a is dynamic) is unboxed to match the slot's
i64/f64 width.var and called directlyvar and invoked by name now compile (e.g.
var twice = (int n) => n * 2; … twice(x)). The return type is inferred from
the body when no typed call context provides it, and the variable adopts the
closure's concrete function signature so the call resolves."" export name, producing an invalid
module. Anonymous functions are internal (table-dispatched) and are no longer
exported.var that is only ever
called (never used as a value, reassigned, or captured) is lowered to a
direct call — no environment heap allocation, no call_indirect, and the
function-table / element sections are omitted entirely. Closures used as
first-class values or that capture variables keep the environment + table
path.Your coding agent can read these notes before it upgrades. Set up the MCP server →