tom_d4rt 1.229.0
tom_d4rt: ^1.229.0 copied to clipboard
D4rt - A Dart interpreter and runtime with bridging, security sandboxing, and dynamic code execution. Fork of D4rt with extended features.
1.229.0 #
Fixed — integers are typed int, not double, on the web (dfin8) #
Environment.getRuntimeType tested is int and then is double with no
else, so the later test won. Compiled to JavaScript every number is a double
and 1 is double is true there, so every integer was typed double, and
[1, 2] was refused where a List<int> parameter is declared. The test chain
now stops at the first match, with int before double. No change on the VM,
where 1 is double is false. Found by tom_d4rt_ast's new browser test.
Name resolution: no.
1.228.0 #
Changed — a module exports only its own declarations and its exports (dfin6) #
BREAKING for a script that relied on the leak. A module's exported environment
used to receive its whole module environment, imports included, so with
main -> a -> b the entry could call a name only b declares. Dart rejects
that ("Undefined name"), and so does the interpreter now: an importer sees what
the module declares at its top level plus what its export directives name
(with their show / hide). Cyclic imports still load. Unnamed extensions are
still carried, as before.
Name resolution: yes — names an import of an import declares are no longer visible to the importer (dfin6).
1.227.0 #
Fixed — Record is a type; record parameters are checked (dfin5) #
x is Record raised "Undefined variable: Record", and a Record return or
parameter type raised "Type 'Record' not found.", although every record is a
Record in Dart. Both now work, and x as Record refuses a non-record
(dguc7). A RECORD-typed parameter is now checked structurally:
sum((int, int) p) called with ('a', 2) raises
type '(String, int)' is not a subtype of type '(int, int)' of 'p'.
FUNCTION-typed parameters stay unchecked on purpose: the interpreter infers no
context type for a closure literal, so a structural check would refuse correct
callbacks (dguc8).
Name resolution: yes — Record resolves in is, as and type annotations (dfin5).
1.226.0 #
Changed — no web platform claim (dfin4) #
platforms: no longer lists web. This package depends on analyzer and
uses dart:io, so it cannot run on the web; pub.flutter-io.cn showed a web badge it
could not honour. Web embeddings use tom_d4rt_ast's bundle runtime.
Changed — the working directory and process execution are permission-checked (dfin3) #
Directory.current = x is a WRITE on x. Changing the working directory
moves every relative path a grant is checked against, process-wide and for
every later script, so it needs the standing of a write there. The current
and systemTemp getters stay unchecked: each returns a Directory, and
every operation on that is checked.
A process starts when EITHER a ProcessRunPermission covers the command and its
arguments, OR a FilesystemPermission with execute covers the executable. The
executable is the command itself when it names a path, else the first match on
PATH (PATHEXT on Windows), checked on its real path. Process.killPid
names no executable and needs ProcessRunPermission. Until now the gate passed
neither the command nor its arguments, so ProcessRunPermission.command(...)
and .commandWithArgs(...) allowed nothing, and the execute flag gated
nothing. BEHAVIOUR CHANGE: FilesystemPermission.any and
FilesystemPermission.path(...) carry execute, so a script holding either can
now run an executable. A host that wants file access only should grant
FilesystemPermission.read / .write (or the readPath / writePath
forms). The dart:io import gate also admits a script holding only a
ProcessRunPermission (commandAgnostic, as pathAgnostic is for files).
Name resolution: no.
1.225.0 #
Changed — the script runners read imports through the interpreter's permissions (dfin2) #
BREAKING for a script that imports from outside its own directory without a
grant. executeFile, executeSource and executeFileContinued used to
resolve imports with a regex pre-walk that read every transitive import off
disk directly, so a scoped FilesystemPermission did not hold, a symlink
inside the script directory read outside it, and a run with no grant at all
could read anything the process could. executeFile and executeSource now
hand the interpreter only the entry source, and the module loader reads
every import under FilesystemPermission on the file's real path.
executeFileContinued evaluates file by file, so it keeps its own walk, but
every read goes through the same check.
Each run adds one implicit grant: READ on the entry script's directory tree
(the basePath for executeSource), added and removed by identity, so a
host grant is never touched. An import beside or below the script runs as
before, and anything outside needs the host's own grant.
resolveImportsRecursively remains a host-side utility (no sandbox), now
resolving paths with Uri.resolve and taking an optional readFile.
D4rt.loadedSourceModuleCount reports the file: modules of the last run;
sourcesLoaded is read from it.
Name resolution: no — the loader resolves the same imports; only who reads the files changed.
1.224.0 #
Fixed — a conditional import is not refused as an undefined name (sci2) #
scg6's static pass read the dart.library.io of
import 'x.dart' if (dart.library.io) 'y.dart'; as a variable named dart,
so 1.220.0 through 1.223.0 refused every program with a conditional import
before main. A DottedName is now a tag, like the other directive parts.
tom_d4rt_exec's suite found it while taking over the enforcement.
Name resolution: yes — a conditional import's configuration is no longer read as a name (sci2).
1.223.0 #
Fixed — an alias of a bridged enum defines (SCI1) #
typedef MaterialState = WidgetState; is Flutter's deprecated name for an
enum, and the generator emits it in classAliases() beside the class aliases.
Environment.defineBridgeAlias looked the target up among bridged classes
only, logged "target class not found" and defined nothing, so MaterialState
was undefined in every run. Scripts naming it passed only while that line went
unevaluated, until scg6's static name pass refused them before main (the
pre-publish base corpus found it in material/datatable_test.dart). The alias
now falls back to a bridged enum of that name anywhere on the scope chain, and
resolves to the same BridgedEnum. Of the 14 aliases in the Flutter bridges,
it is the only one whose target is an enum.
Name resolution: yes — an alias whose target is a bridged enum now defines the alias name (SCI1).
1.222.0 #
Added — D4rt.lastErrorTrace: the interpreted frames an error left (woneprpd153) #
A script that threw reached the host with the interpreter's own Dart trace —
frames of the visitor — and no position in the script. The interpreter now
keeps an interpreted call stack (D4rtCallStack, on the run's ModuleLoader):
which interpreted functions are active and which statement each is executing.
The first time an error leaves an interpreted call the stack is snapshotted
against it, and D4rt.lastErrorTrace hands the host those frames, innermost
first — D4rtStackFrame(member, line, column, source). A run that succeeds,
or fails outside any interpreted call, leaves it empty.
Synchronous call chains are traced in full. An async function's frames are known only until its first suspension: a continuation resumes outside the call that started it.
Name resolution: no.
1.221.0 #
Added — D4rt.parse and D4rt.executeProgram: parse once, run many times (woneprpd132) #
execute parsed its source on every call, and no public API accepted or
returned a parsed unit, so an embedder that re-runs one script paid for the
parse on every run. parse(source, {basePath}) now returns a D4rtProgram,
and executeProgram(program, {name, positionalArgs, namedArgs, sources, allowFileSystemImports}) runs it exactly as execute runs its source,
without parsing again.
A program carries only syntax: executeProgram resets the global environment
and rebuilds declarations and static coordinates per run, as execute does,
so two runs of one program share no state, and one program may be run by
several interpreters. A program also survives dispose(), which releases
only what the interpreter retains, so an embedder can cache programs and still
dispose between runs. The parse is the one execute uses (it moved into
_parseDirectSource, which both call), so a parse error is the same
SourceCodeD4rtException, raised by parse. basePath belongs to the
program because the unit is anchored at <basePath>/main.dart. A root loaded
by library URI goes through the module loader and stays with execute.
The AST line needs no counterpart: D4rtRunner already runs a pre-built
bundle, which is this split's other half.
1.220.0 #
Changed — a program reading an undefined name is refused before main runs (scg6) #
Dart rejects an undefined name at compile time, so such a program never runs.
d4rt ran everything up to the bad line: a script that recorded a side effect
on its first line and misspelled a name on its second had already recorded
it. After imports and declarations are registered, and before main,
_refuseStaticallyUndefinedNames now runs SCD95's static pass in two stages.
The pass narrows to candidates. The populated environment confirms, through
the new non-evaluating Environment.isDefined (which, unlike lookup, never
calls a registered global getter). Only a name both agree on is refused, and
the refusal evaluates that identifier, so the error is exactly the one the
line would have raised (UndefinedNameD4rtException, including a "not
bridged" reason). A name the runtime would resolve some other way, an
assignment target (whose runtime message differs), and everything the pass
cannot see (after a ., class bodies whose supertype leaves the unit,
extension members, type positions) stay with the runtime guards. Top-level
initializers have already run at that point, so the guarantee is about main.
The pass no longer treats the constructor named by : this.x() /
: super.x() as a read.
Name resolution: yes — an undefined name is now raised before main rather than at its use (scg6).
1.219.0 #
Fixed — reading a member of a value needs no import of its type's library (scf44) #
import 'dart:io'; main() => stdout.encoding.name; failed with "Undefined
property or method 'name' on Utf8Codec. No bridge claims this type" until the
script also imported dart:convert. Imports govern which NAMES a script can
write, not which members a value it holds exposes. A stdlib module's
type→bridge mapping reached the lookup only when the module was imported.
A latent registry of every stdlib library's bridges now answers a native
value's bridge when the imported scope misses. It is asked in two tiers,
each after the imported scope's own: a precise match (exact type, or the name
or nativeNames) before SCC49's suffix guess, and the suffix guess after
it. An imported library therefore still wins at each tier, and the widening
never lets a guess outrank a declared match. Names are untouched: Utf8Codec
still does not resolve as a name without dart:convert.
Name resolution: yes — a native value whose bridge lives in an unimported stdlib library now resolves to that bridge (scf44).
1.218.0 #
Fixed — await in the body of a collection-literal for element (scf43) #
[for (var x in xs) await f(x)] is valid Dart and was refused with "await
is not supported in the body of a collection-literal for element". The
refusal was deliberate, because the interpreter drives await by replay.
Re-evaluating the literal would have re-run the earlier iterations, and the
await-site cache, keyed by the node every iteration shares, would have handed
the second iteration the first one's value.
Both are now handled. Each iteration evaluates into a buffer that is merged
only when the iteration completes. The completed buffers are recorded when a
later iteration suspends, and the replay emits them instead of re-running those
bodies. The await sites an iteration reached leave the cache when it completes.
This covers for-in, pattern and classic for elements, nesting, lists, sets
and maps. The record exists only from a suspension to the literal's
completion, so a for element that never suspends behaves as before.
1.217.0 #
Fixed — the host invoke path reaches inherited bridged members (scf42) #
D4rt.invoke(name, args) on an interpreted instance whose class extends a
bridged class asked only that bridge's own maps. So for
class MyQ extends ListQueue, invoke('elementAt', [1]) and
invoke('first', []) failed with "Method or getter not found", though the
same members worked inside the script (SCF19). The bridged-superclass branch
now uses BridgedClass.findReachable*Adapter, the walk every script path
uses.
1.216.0 #
Fixed — a class name stored by a method or []= reaches a runtimeType lookup (scf40) #
A bare bridged class name evaluates to its BridgedClass. SCD198 stored the
native Type when a set or map literal held one, but a key that reached the
collection any other way stayed a BridgedClass:
(<Type, int>{}..[String] = 1)['x'.runtimeType] was null, and
(<Type>{}..add(String)).contains('x'.runtimeType) false. That broke the
Map<Type, Handler> registry idiom whenever the registry was built rather
than written as a literal. Identity collections missed both ways.
A class name now has one representation in every collection, its native
Type. It is converted at the argument boundary every call crosses (the
same rule ENG-002 applied only to Type-typed parameters), at the index
operators (m[k], m[k] = v, the cascade and ++/-- forms, and a
bridged []/[]=), and in a list literal's elements. A wrapped instance in
a list still keeps its representation, as before.
1.215.0 #
Fixed — an interpreted Error subclass answers as its own class (scf39) #
class MyError extends Error {} is built on a native Error super-object,
and two answers came from that object instead of the script's class.
MyError().toString() printed Instance of 'Error', naming the native class,
while '$e' on the same value printed <instance of MyError>. A native
super-object or proxy whose toString is Object's default now yields the
instance's own default, for a member read and for super.toString() inside
an override alike. A super-object that overrides toString is still the
answer, and now for interpolation too: an ArgumentError subclass
interpolates as its message.
stackTrace stayed null after the error was thrown. Dart sets it on the
first throw; the native super-object is never thrown and its stackTrace
cannot be assigned, so the instance records the stack trace of its first
throw, and a later throw keeps it.
1.214.0 #
Fixed — a native closure resolves to the Function bridge (scf38) #
A function type prints as (params) => Ret, and the name-shaped bridge
passes read its RETURN type. A host-built (int x) => x was claimed by the
int bridge and a () => Map<String, Object> tear-off by Map, so a member
read on the closure ran that bridge's adapters against a function: f.length
on the tear-off failed inside the Map bridge's cast. In the Flutter twins that
is every callback read off a host-built widget, since (BuildContext) => Widget matched Widget. A function value now resolves to the Function
bridge; only an exact registration for its type outranks that. Calling such a
closure stays a native call, with its arguments coerced as before (GEN-110):
the invocation sites no longer route a native function through the bridge's
call, which serves interpreted Callables.
Name resolution: yes — a native function value resolves to the Function bridge instead of the bridge named like its return type (scf38).
1.213.0 #
Fixed — obj.v++ and ++obj.v on a bridged object's property (scf37) #
b.v += 1 worked on a bridged getter/setter pair, but b.v++, b.v--,
++b.v and --b.v threw Cannot increment/decrement property on non-instance object: the four increment/decrement sites accepted only an
interpreted instance as the receiver. A bridged receiver now steps through
its getter and setter adapters, as the compound path does. The walk reaches
adapters the bridged class inherits (SCF19), and the step is computed by the
same computeCompoundValue. Postfix yields the old value, prefix the new; a
property with no setter is refused by name.
1.212.0 #
Fixed — a surplus positional argument is an error, in every stdlib adapter (scf36) #
SCE245's census measured 315 adapters per tree that read positionalArgs[k]
and never checked the length, so an extra argument was dropped in silence:
Stream.value(1).asyncMap((x) => x + 1, 99) yielded [2],
Stream.value(1).contains(1, 2) answered true, Error.safeToString(1, 2)
formatted the 1. Native Dart rejects all of these at compile time. Every
one now opens with D4.checkArity(positionalArgs, '<Class>.<member>', atMost: N).
N is the SDK's own positional count, read through dart:mirrors by
tool/bound_surplus_arity.dart. It is not the highest index the adapter
happens to read, so a member with an optional positional parameter keeps
accepting it. Two adapters turned out to ignore such a parameter, and now
honour it:
num.parse'sonErroris called with the input when the input does not parse.Uri.parseIPv4Addressforwardsstartandend.
The census reports 0 unguarded adapters in both trees.
1.211.0 #
Added — scheduleMicrotask, and the rest of the top-level surface nothing checked (scf35) #
scheduleMicrotask(() => ...) failed with Undefined variable, and the name
was not recorded as deliberately unbridged either. It is now a dart:async
global. The callback runs under the discipline a Timer body uses: queued as a
real microtask, and an error it throws leaves unwrapped, so an embedder's
onUncaughtError receives what the script threw.
The gap existed because the SDK-surface guard (scc73) read only the
constructors and getters of classes. Its new axis, F-SCC73-5, reads every
public top-level function, getter and variable of the eight bridged dart:
libraries and resolves each from a script. On its first run it found four more
names, now bridged: base64UrlEncode, unicodeBomCharacterRune and
unicodeReplacementCharacterRune (dart:convert), and systemEncoding
(dart:io). Six names are deliberately not bridged, each with a recorded
reason: exit, sleep, exitCode and pid (dart:io), and the
deprecated / override annotation constants.
1.210.0 #
Fixed — a script subclass of a concrete bridged class is recognised when native code hands it back (scf31) #
class _RenderMeasureBox extends RenderProxyBox has no proxy: the base is
constructed natively and that native object is what the framework holds and
hands back — to updateRenderObject(..., covariant _RenderMeasureBox r), or
through a viewport's delegate getter. Every check that asked for the script
class refused it (type 'RenderProxyBox' is not a subtype of type '_RenderMeasureBox'), and delegate as _TwoDMgrCountingDelegate returned the
native base, which has none of the script's members.
Setting InterpretedInstance.bridgedSuperObject now records the pairing, and
D4.interpretedBehind answers for a bridged super object as it does for a
proxy. A parameter declared as the script class binds the INSTANCE, as
yields it, is answers for it, and identical / == treat the two as one
object. What crosses to native code is unchanged — handing the instance back
out yields the same native object — so layout and paint are untouched, which
is what the proxy-based attempts (SCE164) broke. An unrelated script class and
a base nobody subclassed are still refused.
D4.ownerOfSuperObject is new. D4.registerInterpretedForNative is now a
no-op for values an Expando cannot key on, instead of throwing.
1.209.0 #
Fixed — four await-resumption routes that re-evaluated or dropped an await (scf29) #
Measured with a next() that counts its calls:
=> add(await next(), await next())answered 4 with three calls;if (add(await next(), await next()) == 3)took the false branch;while (add(await next(), 0) < 2)never ran its body;s = (await next()) + (await next());bound the first await's value and never evaluated the second.
Each now hands its unit back to the state machine so the resolved await sites
replay: an => body's expression, and an if / while / do reached from
its condition, join the statements SCE139 re-runs (_resumableNodeFor), and
an assignment whose right-hand side is more than the await is re-run as
SCD121 re-runs a declaration. The machine's if / while / do branches end
the replay cache when their condition completes, so a loop condition does not
replay a previous iteration's values. The same reading of one await's value as
the whole condition broke if ((await a) + (await b) == 3) and
while (!await f()); both are re-entered the same way, and a prefix operator
now propagates an awaited operand's suspension instead of applying ! to it.
1.208.0 #
Changed — binding a typed collection is O(1) in its length (scf28) #
SCD92 checks a declared List<T> / Set<T> / Map<K, V> against the type a
native collection's contents share, because a native collection carries no
element type the interpreter can read back. That derivation read every
element on every binding — parameters, typed locals, typed patterns, for-each
variables — about 0.096 us per element, half a millisecond per binding of a
5000-element list. It now reads the first Environment.elementTypeSample (8)
elements: the overhead of List<int> v = c; over var v = c; is flat at
about 4 us from 100 to 5000 elements, where it was 98 us and 474 us.
The bound is 8 because both suites pass at 2 and 4 and exactly one case
(a disagreement at index 1) fails at 1. One answer changes: a collection that
agrees for the whole prefix and disagrees later used to count as
heterogeneous and pass; it is now checked against the prefix's type, and
refused only when a prefix element is provably not the declared argument —
which Dart refuses too. A top-type argument (List<dynamic>) was already
exempt before any element is read.
1.207.0 #
Fixed — a bounded generic class accepts its own constructor (scf27) #
class Box<T extends num> { final T v; Box(this.v); } refused Box(3):
a class type argument that is not written is not inferred, is filled with
dynamic — the interpreter's "unknown" — and that unknown was measured
against the bound. The bound is now checked for WRITTEN arguments only, as it
already was for generic functions: Box(3) constructs, and Box<String>('a')
and Box<dynamic>(3) are still refused with the unsatisfied-bound message. A
bounded mixin and a bounded extension type were not affected.
1.206.0 #
Fixed — a bare name reaches the getter or setter it names (scf25) #
Top-level accessors and a class's static ones (inside a static member, where the class's statics resolve without a prefix) were bound as the getter and setter FUNCTIONS under the plain name. So:
- a bare read of a getter answered the function —
int get g => 1; main() => g;returned<fn g>; - a bare write rebound the name and never called the setter —
s = 3leftset sunrun, and a staticset vnormalising its input was skipped; - a getter and a setter of one name replaced each other;
++vread through a getter and then overwrote its binding.
A setter is now bound under Dart's setter name, v= (Environment.setterKey),
which show / hide treat as v. A bare read of a getter calls it. A bare
write — =, compound, ??=, ++ / -- either side — calls the setter unless
a closer binding of v (a local, a parameter) shadows it. From an instance
method a bare write reaches a static setter in the class chain, as SCE125 made
it reach a static field.
Name resolution: yes — a bare name bound to a getter or setter now resolves to a call of it, and v= is a new binding a bare write consults (scf25).
1.205.0 #
Fixed — a script class that declares no toString still has Object's (scf24) #
class C {} answered hashCode, runtimeType, == and '$c', but an
explicit C().toString(), its tear-off and a call through an Object
parameter raised a no-such-method error naming a member the script correctly
did not write. So did super.toString() and super.noSuchMethod(i) from a
class whose superclass is Object — the forms an override commonly uses.
InterpretedInstance.objectMember(name) holds Object's answers:
toString renders as '$c' does, without re-dispatching to a script
override, so super.toString() inside one cannot recurse; noSuchMethod
raises a catchable NoSuchMethodError. InterpretedInstance.get consults it
last, after the script's own members, mixins, interpreted supers and a
bridged super. super in a class whose superclass is Object binds to a new
BoundObjectSuper, and a super chain through interpreted classes that ends
at Object falls back to the same answers. A declared toString and a
bridged super's still win.
1.204.0 #
Changed — a generic element is type-tested by the same predicate as is (scf22) #
[x] is List<T> and m is Map<K, V> asked each element, key and value a
second, smaller copy of the type test. Every divergence found between the two
copies had been a defect in one of them, in both directions, so the element
checks now ask _valueHasType — the predicate behind is, typed patterns and
on clauses — and the second copy is gone.
Three element-position answers change, each to what is already answers:
- a type name the interpreter cannot resolve now fails as
x is Unknowndoes — it raises the same error — where it used to answertrue; - a function or record element type (
List<int Function(int)>,List<(int, String)>) is checked structurally, where any element used to match; List<void>is false for a non-empty list, where it used to be true.
F-SCE101-10 (tom_d4rt/test/sce101_element_type_test.dart) holds the rule
over 14 values × 22 types: a one-element list is a List<T> exactly when its
element is a T.
1.203.0 #
Fixed — an interpreted subclass reaches its bridged superclass's INHERITED members (scf19) #
A bridge carries the members its class declares, so the ListQueue bridge
has removeFirst and no elementAt — that one is Iterable's. A bare
ListQueue() reached it through the supertype walk; class MyQ extends ListQueue {} did not, because every path from an interpreted instance to its
bridged superclass asked that bridge's own maps. elementAt, first, map,
fold were absent through q., bare inside a method and super. alike, each
failing with a different exception type.
BridgedClass.findReachableMethodAdapter / …GetterAdapter /
…SetterAdapter return the bridge's own adapter, else the first registered
supertype bridge's, resolving supertype names in the visitor's environment.
They replace the own-map lookups on the bridged-superclass paths of
InterpretedInstance.get / set and of super. access, and the method
invocation path now supplies the visitor (InterpretedInstance.getForInvocation,
which leaves a miss to the invocation site's own noSuchMethod handling).
Name resolution: yes — a bare name inside an interpreted subclass of a bridged class now resolves to a member the bridged superclass inherits (scf19).
1.202.0 #
Fixed — break / continue in an async body run the finallys they cross (scf6) #
SCE18 fixed the return route; the jump route still went straight to its
target, so for (..) { try { if (i == 1) break; } finally { log.add('f1'); } }
never logged, and a continue skipped the finally for that iteration. The
synchronous visitor was always right and is the oracle.
A jump that leaves a try with a non-empty finally now parks on
AsyncExecutionState.pendingJump and runs the finallys between it and its
target, innermost first and nothing in between; after the last one it
completes exactly as before (loops left, next node). A return, a throw
that escapes the finally, or another jump that leaves it replaces the pending
jump, as in Dart; a jump or catch local to the running finally does not.
1.201.0 #
Fixed — an async* generator obeys its listener (scf4) #
A generator ran to completion whatever its subscriber did: yield added its
value and suspended on an already-completed future, and the stream controller
had no onPause/onResume/onCancel. So a consumer's break out of an
await for cancelled the subscription while the body ran on — SCE16 had fixed
the consumer's interleaving and left exactly this. Now:
yieldwaits for the listener: one microtask so a consumer that pauses on receipt has done so, then for as long as the subscription is paused, which isasync*backpressure.- cancelling the subscription ends the body at its pending
yieldas if byreturn:finallyblocks run, and nocatchclause may claim it (GeneratorCancelledSignal, riding the same route as SCC31's uncatchable undefined name).cancel()completes once the body has finished. yield*stops forwarding once the listener is gone.
1.200.0 #
Fixed — e.hashCode / e.runtimeType answer natively on a bridged value (sce239) #
visitPrefixedIdentifier now answers hashCode and runtimeType on a
bridged value from the native object before consulting any member map, as
visitPropertyAccess already did (SCC78) and as tom_d4rt_ast has in both
places (GEN-075). A bridge that declared either one as a method (the shape
of SCD196's MapEntry.hashCode defect) made e.hashCode return the bound
method instead of an int in this tree, while (expr).hashCode and the
analyzer-free tree answered correctly. That was one line of mirror
divergence with a behavioural consequence.
1.199.0 #
Changed — the unsupported-node message has one shape in both trees (sce236) #
visitNode builds its message identically in this tree and in
tom_d4rt_ast. Only the excerpt helper differs: it still renders with the
analyzer's toSource(), now quoted by the helper itself. The text is
unchanged: Source: '<excerpt>'.
1.198.0 #
Fixed — a class name is identical to the Type it denotes (sce234) #
identical(String, 'x'.runtimeType) is now true, as in Dart, and
identityHashCode(String) == identityHashCode('x'.runtimeType) holds.
SCD198 had made the two equal with the same hash code, but not one object.
identical and identityHashCode already treat a native proxy and the
interpreted instance behind it as one object. A bridged class name and its
native Type now join that rule. Interpreted classes already agreed.
Different types still compare non-identical, and identical(List, [1].runtimeType) is false, as it is in Dart.
Making every class-name expression evaluate to the native Type (the
todo's option (a)) was measured and not taken. Scripts can observe the
difference only through identity, and that option would have changed every
place that consumes a class name.
1.197.0 #
Removed — the num bridge's instance members (sce233) #
The num bridge no longer declares instance methods or getters. It keeps
isAssignable, num.parse and num.tryParse. Scripts see no change: every
num value is an int or a double, Dart forbids any other class to
implement num, and both of those bridges already declare every member. The
32 adapters removed had never run. A test now checks that int and double
keep declaring all of them. The member audits in both trees treat num as
sealed to those two heirs (sealedToHeirs), so they do not report its
members as unreachable.
Every other bridge that SCD197 recorded as unreachable is kept, and its source says why beside its member maps. The measurement undercounted in two ways:
- It stood one instance in for each bridged type.
MatchandFileSystemEntityare reached by values whose own class has no bridge: aString.allMatchesresult, and aLink. - It followed bridged heirs only. An interpreted subclass of
LinkedListEntryorErrorreads inherited members through the bridged superclass.
1.196.0 #
Changed — BridgedInstance.get reads bridged getters (sce232) #
BridgedInstance.get now resolves a member in the same order as the
interpreter's property readers: bridged getter first, then the bound instance
method, then name / index on a wrapped enum. It used to look up methods
only, so a getter-only name threw UndefinedMemberD4rtException. The 1.195.0
note that set works "as get reads through the getter" was not true when it
was written, and is true from this release.
get takes an optional visitor and passes it to the getter adapter, as set
already did for setters. The change is invisible to scripts:
- the full
tom_d4rtsuite (4 416 tests) never calls this method; - no name may be both a getter and a method on one class
(
scd196_member_map_disjointness_test.dart), so no method name answers differently; - none of the 824 stdlib or 20 901 Flutter getters reads its visitor.
Measured 2026-09-29. The change matters to host code that calls the primitive directly. Comments at both reader sites point back to this method.
1.195.0 #
Fixed — the interpreter no longer announces gaps that are not gaps (sce223) #
awaitaccepts a non-Future (await 5is 5,await nullis null) and still yields to the event loop, as Dart does. It was refused as an error.BridgedInstance.setassigns through the bridged setter, asgetreads through the getter; a missing setter is an undefined member. It threw "not implemented" whatever the class declared.o.x = await f()/l[i] = await f()no longer log "Resumption for simple assignment to complex LHS not implemented" — that path IS the implementation, and it evaluates each side once.for (var i = await f(), j = 0; ...)stays unsupported, now saying exactly that shape and the workaround (declare the awaited variable before the loop).- An
awaitin asuper(...)/this(...)initializer is reported as what it is — a constructor cannot be async — instead of "not yet supported".
1.194.0 #
Fixed — pipe accepts the consumers a script can hold (sce217) #
Stream<X>.pipe(StreamConsumer<X>) is a typed call, and Dart generics are
covariant, so a consumer passed only if its element type is a subtype of X.
No consumer a script holds is one: a script's StreamController() is
StreamController<dynamic>, and an IOSink (a file's openWrite(), a
Socket) is StreamConsumer<List<int>> where a socket streams Uint8List.
So socket.pipe(controller) and the proxy client.pipe(upstream) threw a raw
_TypeError — through Socket.pipe's own guard-then-cast copy AND through the
inherited Stream.pipe adapter. The Socket copy is deleted; Stream.pipe
now runs the SDK's own body (addStream, then close) via the new
D4.pipeStream, with the consumer's addStream dispatched dynamically. A
consumer of an unrelated element type still fails inside its own addStream.
1.193.0 #
Changed — fuse says why a script-defined converter is refused (sce216) #
A Converter or Codec a script defines is an interpreted instance, not a
native one, and fuse builds a native pipeline the interpreter does not run,
so every bridged fuse refuses it — a deliberate limit, not an erasure bug.
The refusal used to read "requires another Converter<String, dynamic> as
argument", which looked exactly like the SCD181 erasure defect. All twenty
bridged fuse guards now go through one helper
(stdlib/convert/fuse_argument.dart): a script-defined argument is told it
cannot be fused and to call convert on each stage; any other wrong argument
keeps the plain type requirement. Native-to-native fusion is unchanged.
1.192.0 #
Fixed — a coercion error inside WebSocketTransformer.bind reaches the script (sce208) #
D4.coerceStream turns a wrongly typed element into an error event on the
mapped stream. Eleven of the twelve bridged consumers of a coerced stream
forward that error to the script; WebSocketTransformer.bind did not — the
SDK transformer listens to its source with no onError, so the diagnostic
went to the zone and the bound stream never produced another event: the
script hung. The bridge now binds through the new
D4.bindForwardingSourceErrors, which splits source errors off before the
native consumer and merges them into its output. await upgraded.first
throws the diagnostic, a listener's onError receives it, and the stream
keeps serving the next request. No eager check — nothing is drained ahead of
the consumer.
1.191.0 #
Changed — dart:io imports with EITHER FilesystemPermission or NetworkPermission (sce206) #
dart:io holds the filesystem and every network class, but its import gate
asked only for filesystem access, so a script granted NetworkPermission
alone could not import the library its grant is for. The gate now admits
either capability (decision (a)); no existing grant loses anything. The
per-operation gates do the enforcing — NetworkPermission on every
socket-acquiring call (SCD170), FilesystemPermission on every file
operation — so a network-only script can name File and still cannot touch
one. The refusal with neither now names both permissions.
1.190.0 #
Fixed — a rethrow runs its own try's finally first (sce205) #
try { … } catch (e) { rethrow; } finally { cleanup(); } propagated the right
exception and never ran cleanup() — in synchronous AND async functions, for
two unrelated reasons. The synchronous visitTryStatement relaunched the
rethrow from inside the catch handling, before reaching the finally; it now
holds it and throws it on after the finally, like an unhandled exception. The
async _handleAsyncError advanced its search past the owning try outright
(SCD41's skip); it now keeps an owner that has a non-empty finally, and
SCD169's rule — no clause of a try may match an error from its own catch —
runs that finally and releases the error outward. F-SCE205-1..4 in
scc12_await_in_finally_test.dart pin the sync, async and nested orders and a
non-rethrowing control, each self-limiting against a re-entry spin.
1.189.0 #
Documented — why HashMap and LinkedHashMap keep their Map shadows (sce203) #
Both bridges re-declare Map members (cast, removeWhere, update,
updateAll, …) that SplayTreeMap leaves to the supertype walk. Their class
docs now record that this is kept on purpose: the drift hazard is measured by
the SCC51 shadow differential, and family parity is measured over REACHABLE
members by the new sce203_family_reachable_parity_test.dart, where only the
SDK's own interface differences (SplayTreeMap's sorted-map API,
DoubleLinkedQueue's entry API) separate the collection families. No
behaviour changes.
1.188.0 #
Changed — the eleven typed-data lists share one getter map (sce202) #
Ten typed-list bridges guarded each of their eleven getters with a
wrong-target RuntimeD4rtException; Uint8List used a bare cast. Measured
before deleting: all 176 guard branches (both trees) were instrumented, and
4 092 interpreted calls — every variant, built six ways (including views and
sublistView), through five static types — plus both full suites reached
none. Dispatch selects the bridge from the value's own class and no variant
subtypes another, so the branches were dead. They are gone; all eleven
variants now take length, lengthInBytes, elementSizeInBytes,
offsetInBytes, buffer, first, last, isEmpty, isNotEmpty,
hashCode and runtimeType from the shared typedListGetters, whose
comment records the measurement. No behaviour a script can reach changes.
1.187.0 #
Removed — ServerSocket's 28 copies of Stream members (sce195) #
ServerSocket's bridge spelled out 21 methods (listen, map, where,
fold, toList, transform, …) and 7 getters (first, length,
isEmpty, …) that Stream's bridge also declares. Since SCD38 registered
ServerSocket -> Stream, each shadowed an inherited adapter that would answer
if it were gone, and two implementations of one member on one type drift. They
were deleted only after the SCC51 shadow differential could see them (sce185)
and every pair agreed; listen — a server socket's primary use — is covered
by a new case driving a real accepted connection through the inherited
adapter.
Fixed — arity diagnostics that named the wrong class #
ServerSocket, RawSocket and RawDatagramSocket adapters reported arity
errors as Socket.<member>, copied from the Socket bridge: twelve
diagnostics across the three now name the class the script used.
1.186.0 #
Fixed — ten stdlib adapters that disagreed with the adapter they shadow (sce185) #
The SCC51 differential compares every adapter a bridge redeclares from a registered supertype against the one it hides. It walked only the fourteen collection bridges — 537 of 2 076 shadowed pairs. Widened to every bridge that shadows anything, it found 128 divergences in seven families, each a defect in one of the two adapters:
Runes: thirteen callback members (where,map,any,every,fold,reduce,expand,forEach,firstWhere,lastWhere,singleWhere,skipWhile,takeWhile) cast the script's callback to a Dart function type, which aCallablenever is, so each threw_TypeErroron every call. Deleted; the inheritedIterableadapters serve them.Iterable.reduce,Iterable.followedBy,List.reduce,List.followedBy,List.setRange,Stream.reduceandStream.transformrejected a script's untyped callback, list literal orfromHandlerstransformer on any natively typed receiver ('ab'.codeUnits.toList(),Stream<Socket>), where the typed leaves had coerced all along. They now go through acast<Object?>()view, an element-wise copy, ortransformer.bind(source).List.shuffledropped itsRandom, so a seeded shuffle was not reproducible.Sink.closeandEventSink.closediscarded theFuturethe sink returns.LinkedList.containsthrew on a non-entry instead of answeringfalse.WebSocketTransformer.casthard-coded<HttpRequest, WebSocket>; deleted, socast()iscast<dynamic, dynamic>()as in Dart.- The eleven typed lists'
[]=returned the stored value; the operator isvoid(unobservable to a script, which evaluates an index assignment to the assigned value itself).
F-SCE185-1 now holds the differential's fixture table to the registry, so
the walk cannot shrink back to a subset without failing.
1.185.0 #
Changed — the interpreter/native boundary is stated once, at the entry (sce179) #
Environment._isInterpreterOwned (is this value the interpreter's own
representation, such as a RuntimeType, RuntimeValue, Callable, native
Enum or record?) was consulted at one branch: step 4 of toBridgedInstance.
Everywhere else, the name-shaped passes were kept off the interpreter's own
values only by SCD132's corroboration requirement in PASS B, which was
written about bridge-to-bridge false positives. Measured: with that
requirement ablated, TypeParameter resolved to the Type bridge through
BOTH toBridgedInstance and getRuntimeType, because the claim came from
PASS B before step 4 could run.
There is now one value-level entry, _toBridgedClassForValue, which both
paths go through and which asks the predicate once. An interpreter-owned
value still gets a bridge registered for its exact runtime type, since that
is a declaration, but never one found by name. The step-4 check is gone,
and SCC49's structural fallback now runs inside the entry. With SCD132
ablated, both value paths reject TypeParameter: the boundary no longer
depends on it.
toBridgedClass(Type) itself cannot ask, because it is given a Type, not a
value. Its boundary is still SCD132's, and scd147's guard pins both.
Name resolution: yes — bridge resolution for a value is routed through a new entry; no resolution measured today changes (sce179).
1.184.0 #
Fixed — the Stream bridge claimed two types that are not Streams (sce178) #
scd146's census found two override entries on the Stream bridge's
nativeNames, both there since the first commit. Measured with the SDK and
the live registry of both interpreters:
_StreamIterator(whatStreamIterator(...)returns) was inert: its name reaches theStreamIteratorbridge first._HandlerEventSink, which implementsEventSink, was on BOTH the Stream and EventSink lists, and registration order made Stream win. The live registry answeredStreamfor a type that is not one.
Both entries are gone from Stream. With the duplicate resolved, EventSink's
own entry was redundant, since its name reaches EventSink by suffix, so it
went too. The type is never handed to script code, so no script changes
behaviour; the registry simply stops giving a wrong answer.
sce178_stream_override_resolution_test pins both resolutions in each tree's
live registry.
Name resolution: yes — _HandlerEventSink now resolves to EventSink rather than Stream (sce178).
1.183.0 #
Changed — 85 redundant stdlib nativeNames entries removed (sce177) #
Since SCC49 the structural pass reaches a private SDK type by the bridge name
it ends with, so most allowlist entries were redundant: 85 of 109, including
all 17 on Iterator. They are gone, and both interpreter suites pass. The
analyzer-free line's routing tests now ask toBridgedClass which bridge a
REAL SDK object reaches, not whether a name sits in a list.
24 entries remain across 10 bridges: 19 the SDK abbreviates (no suffix rule
reaches them), 2 overrides left to sce178, and 3 kept deliberately. Two of
those are BytesBuilder's: the suffix pass walks the nearest scope frame
first, and in a Flutter app Builder (a widget) is a nearer suffix of
_CopyingBytesBuilder. Measured with the entries removed, BytesBuilder()
resolved to the Builder widget. The census now fails if a redundant entry
survives without a stated reason.
Name resolution: yes — stdlib private types now reach their bridge by suffix rather than by a precise entry (sce177).
1.182.0 #
Fixed — a StreamTransformer was dispatched as a Stream on the analyzer-free line (sce160) #
StreamTransformer.fromHandlers(...) returns a _StreamHandlerTransformer,
and that name sat on the Stream bridge's nativeNames, where it had been
since the repository's first commit. The analyzer-free line resolves a bare
native by its runtime name alone, so
final t = StreamTransformer<num, num>.fromHandlers(handleData: ...);
t.cast<int, int>(); // type '_StreamHandlerTransformer<...>' is not a
// subtype of type 'Stream<dynamic>' in type cast
The reference line escaped through static types. It surfaced once scd98 made a bridged constructor return the bare native, and the pre-publish pass found it.
Measured against the SDK, none of the four transformer implementations is a
Stream: fromHandlers -> _StreamHandlerTransformer, the unnamed
constructor -> _StreamSubscriptionTransformer, fromBind ->
_StreamBindTransformer, cast / castFrom -> CastStreamTransformer. All
four are now claimed by the StreamTransformer bridge, and the wrong entry is
gone from Stream's list. The last three previously had no bridge at all.
Name resolution: yes — it changes which bridge four native StreamTransformer types resolve to (sce160).
1.181.0 #
Fixed — calling a value that is not a function returned the value (sce176) #
A call through a variable fell through to return calleeValue whenever the
value was non-null and not a function, so
var n = 3;
n(); // returned 3
and the same for any object. Dart rejects the call; the interpreter now throws
'n' (type: int) is not callable .... The fallthrough had already hidden two
defects (DFUB9, GEN-110), and it was hiding a third, below.
Added — Dart's callable-object rule for interpreted classes (sce176) #
a(3) means a.call(3) when a's class, a superclass or a mixin declares an
instance method named call. Through a variable this used to return the
instance (the fallthrough above); through an expression (fns[0](3)) it threw
"not a function". Both call paths now resolve the bound call method first.
Bridged objects with a call adapter and call extensions already worked and
are unchanged.
Changed — four more failing operations on an unbridged native name the missing bridge (sce176) #
scd145 made a member access or method call on a native object that no bridge claims say so. Indexing, a binary operator, both assignment forms (through a prefixed identifier and through a property access) and a call now append the same clause, e.g.
Unsupported target for indexing: Zqwx. No bridge claims this type: no
bridged class is registered for the native type Zqwx, ...
Message only, on paths that were already throwing, with the exception types
unchanged. toString() on such an object still does not throw: every Dart
object has one, and string interpolation of an unbridged value is exactly how
a script author finds out what it is.
1.180.0 #
Fixed — a fuzzy SUFFIX match in a near frame beat a declared nativeNames match in an enclosing one (sce162 / scf26) #
toBridgedClass's PASS A walked the scope chain once, trying every strategy in
each frame before moving outward. One of those strategies is not precise:
_longestNameSuffixMatch is anchored on the BRIDGE's name appearing inside the
native type's name, not on any declared relationship. Running it frame-locally
meant proximity beat precision.
Under the lazy-bridge substrate the Flutter bridges sit in the child frame and the stdlib bridges in the warm parent, so:
const <String>{'shiftLeft'} // an UnmodifiableSetView at runtime
Widget keyRow(String label, Set<String> mods, String hint) { ... }
// type 'View<String>' is not a subtype of type 'Set<String>' of 'mods'
UnmodifiableSetView is named outright in the stdlib Set bridge's
nativeNames. It resolved to Flutter's View WIDGET because View is the
longest bridge name that is a suffix of UnmodifiableSetView, and that frame
was reached one sooner.
THE SAME LESSON AS THE PREFIX CASE, at the strategy it missed.
MappedListIterable → Map was fixed by splitting resolution into a precise
chain walk and a fuzzy one. The suffix match is equally fuzzy and stayed inside
the precise walk, so it alone kept resolving by proximity. It is now PASS A2: a
second chain walk, after every precise strategy has been tried in every frame.
The change can only move a resolution from a fuzzy answer to a precise one —
every candidate the new walk finds was already reachable, just later.
MEASURED. Reproduced in two seconds in process against the working tree, and
ABSENT at the published interpreter, so it is one of the regressions that
accumulated in the unpublished delta rather than a long-standing defect. The
empty const <String>{} case passes either way, which is why the corpus showed
it as one failure in one file rather than everywhere.
Name resolution: yes — it changes which bridge a native type resolves to when a
fuzzy suffix match and a declared nativeNames match sit in different frames.
1.179.0 #
Fixed — a LIST of interpreted proxies was refused where each element was accepted (sce161) #
SCD119 taught the binding check to look behind a D4InterpretedProxy: a script
class extending a bridged one is handed to Flutter as a proxy, getRuntimeType
answers with the BRIDGE's name, and binding it back to a parameter declared as
the script's own class was rejected. That repair reads ONE value.
A COLLECTION of them was still refused, and this is where the corpus actually is — a script that builds widgets builds LISTS of them:
type 'List<StatelessWidget>' is not a subtype of type 'List<_A11yNote>'
with every element individually bindable. The rejection came from the applied type-argument check (SCD92), which derives the collection's arguments from elements that each still answer with the bridge's name. So SCD119 moved the rejection one level down instead of removing it, and down is the commoner level.
MEASURED, AND THE DISCRIMINATOR IS const. Reproduced in process against the
published interpreter: a script subclass constructed with const and bound to a
declared parameter fails; the same subclass constructed WITHOUT const passes.
It is base-independent — StatelessWidget, StatefulWidget and Intent all
fail under const and all pass without it — which is one mechanism rather than
the four-shape symptom list GEN-126 was written around. A declared LOCAL takes
the scalar value fine; only the declared-parameter and declared-collection sites
refuse it.
The retry keeps SCD119's discipline exactly. It runs only after the argument check has already failed, so it can remove a rejection this check added and never add one. It asks the SAME subtype question with the interpreted instances substituted in rather than waving the collection through, so an element standing for an unrelated class is still refused. And it returns the ORIGINAL collection, because the proxies are what native code downstream expects to receive.
Witnesses and rails in both trees: F-SCE161-1/-2 (parameter and declared local)
with F-SCE161-3 as the control in tom_d4rt, and F-SCE161-AST-1 with
F-SCE161-AST-2 in tom_d4rt_ast. Ablated: every witness goes red with the retry
removed and both controls stay green either way.
Name resolution: no — a subtype verdict on a binding; which names bind to what is untouched.
1.178.0 #
Deprecated — the bridge-mapping types nobody ever used (sce155) #
LibraryBridgeDefinition, BarrelMapping and ModuleBridgeInfo in
lib/src/bridge/library_mapping.dart describe a RUNTIME answer to barrel
re-export deduplication: group bridged elements by the canonical library they
came from, record which source libraries each barrel re-exports, let the
interpreter recognise one element reached two ways.
That deduplication happens — in the other layer. PerPackageBridgeOrchestrator
in tom_d4rt_generator maps each source file to the barrel exporting it and
emits one per-package bridge file plus delegating barrels, so the duplicate
never reaches the runtime to be deduplicated. These types are a SUPERSEDED
design, not an unfinished one, and there is no intent to recover.
MEASURED RATHER THAN PRESUMED, in both directions. Workspace-wide, the only
.dart files naming any of the three are the declaring file and the two guards
that record them as dead. Outside it, pub.flutter-io.cn lists exactly four dependents of
tom_d4rt — tom_d4rt_generator, tom_d4rt_exec, tom_d4rt_dcli,
tom_d4rt_flutter, every one of them from this workspace — and no cached
release of any of the four names one of these types. The API has never had a
consumer anywhere.
Deprecated rather than deleted: it is on a published surface and removal is
breaking whether or not anybody is hurt. Removal is due at 2.0.0 and is not
left to memory — test/sce155_dead_surface_removal_test.dart fails the moment
this package's major reaches 2 with the file still present, and separately when
a deprecation stops naming the release that removes it. A todo saying "remove
at 2.0.0" would sit unread until the one release it applied to; a red test
cannot.
Name resolution: no — three unused data classes gain an annotation.
1.177.0 #
Recorded — the PASS B narrowing removes the wrong answer, not just most of it (sce153) #
SCD132 made PASS B's prefix fallback require corroboration. SCE149 established that no Flutter-corpus resolution had been relying on the old rule. What neither measured is what the narrowed rule now does with the names it no longer claims — reduce the wrong answers, or end them.
F-SCD133-4 in both Flutter twins answers that over a registry rather than an
example: it ablates getRuntimeType's enum branch and asks what the class path
alone returns for every bridged enum the live Flutter environment holds.
| line | enums | mistyped, published | mistyped, narrowed |
|---|---|---|---|
| AST | 213 | 104 | 0 |
| source | 151 | 82 | 0 |
Every one of the 213 and 151 now throws. The comment beside the fallthrough claimed a throw is "more honest than returning a wrong bridge"; this is that claim priced. Measured 2026-09-22 through SCD66's pre-publish path resolution — both twins resolve the interpreter from pub.flutter-io.cn (DGUC6), so the published column is what an ordinary run of that guard still reports until this ships.
Comment-only in lib/. Both twins' scd133_registry_enum_resolution_test.dart
headers carry the full table, and now print every number in it on a green run
so the next refresh needs no edit — which is how the AST twin's recorded split
was caught stating 103/110 against a measured 104/109 under an interpreter that
had not moved.
Name resolution: no — comment-only. The narrowing itself shipped in 1.108.0 / 0.95.0; this records what it was measured to do.
1.176.0 #
Verified — the PASS B narrowing measured against the Flutter corpus (sce149) #
SCD132 narrowed Environment.toBridgedClass's prefix fallback to require
corroboration — nativeNames or a supertype-registry edge — instead of claiming
any bridge whose name prefixes the native type name. Both trees' full suites
established that the old rule had no legitimate user; what they could not speak
for is the Flutter corpus, which is the rule's heaviest consumer and which
resolves the interpreter from pub.flutter-io.cn (DGUC6).
Both twins' base corpus, run serially through SCD66's pre-publish path
resolution: 926 / 1 / 1 each, against the 927/1/0 hosted baseline. The
single failure is the same script in both — scf26's const <String>{}
resolving to Flutter's View widget — which is the SUFFIX pass, not this one.
THE EXPECTED FINDING DID NOT MATERIALISE. The work predicted Flutter bridges
would need nativeNames entries once the prefix coincidence stopped covering
for them. None did: no corpus resolution was reaching PASS B uncorroborated.
Name resolution: no — this release changes only the comment recording that measurement. The narrowing itself shipped in 1.108.0 / 0.95.0.
1.175.0 #
Fixed — a return or an invocation with several awaits evaluated each of them twice (sce139) #
Future<int> f() async => (await next()) + (await next()); // was: 4, not 3
next() increments a counter, so the wrong answer and the wrong call count are
the same defect seen twice. The resumption branches for a return statement and
for an invocation with awaits in its arguments both re-evaluated the node inside
_determineNextNodeAfterAwait. That evaluation's suspension CANNOT be
registered with the state machine — the machine only ever attaches to a
suspension raised by executing a node — so it was discarded and the node was
executed again anyway. Every await site the machine had not yet resolved
therefore ran twice per pass, and the discarded pass consumed the value its site
should have received: two awaits answered 4 and cost three calls, three awaits
answered 9 and cost five.
SCD121 had already repaired the declaration route, by evaluating NOTHING in the
resumption branch: hand the statement back to the state machine, let the
resolved sites replay from resolvedAwaitResults, let the first site not yet
reached suspend for real, and let the pass on which nothing suspends do the
work. Its own comment recorded that the return route still carried the double
evaluation and that it was invisible there only because its cases awaited
side-effect-free futures. This is that route, and the invocation route, given
the same shape.
THE SECOND DEFECT, which falls out of WHO completes the function. Completing
inside _determineNextNodeAfterAwait — set lastAwaitResult, return null —
bypasses the state machine's ReturnException handler, and with it the jump
into an enclosing finally:
try { return "${await f()}"; } finally { cleanup(); } // cleanup never ran
One await is enough for that one, so it was never a counting problem. Re-running
the statement makes visitReturnStatement throw for real, and the handler
routes the return through the finally. The return's declared type is checked on
that pass too, which it previously was not.
Only the statement kinds the machine can safely re-enter are handed back — a
variable declaration, an expression statement, a return. Re-running an if or a
loop would restart the construct, so those keep the local re-evaluation.
Name resolution: no — which names bind to what is untouched; the change is which party evaluates an already-parsed statement, and how many times.
1.174.0 #
Fixed — a generic bound written through a type alias threw (sce130) #
typedef N = num;
T pick<T extends N>(T v) => v;
main() => pick(3); // was: Undefined variable: N
A legal program that did not run. The direct form, T pick<T extends num>,
always worked, so the fault was alias-specific: type-parameter bounds are
resolved in PASS 1, by DeclarationVisitor, and type aliases are registered in
PASS 2 — they must be, because an alias may name a class whose placeholder pass
1 is still creating. _extractTypeParameterBounds rethrew on a bound it could
not resolve, with a comment saying the rethrow was deliberate, so the miss
became a hard failure instead of a deferral.
THE MEASUREMENT CHANGED THE FIX, which is the part worth recording. SCD100 left this as a limit and predicted the repair would be a pass-1 reorder — "its own change with its own blast radius". Probing fourteen shapes first showed something cheaper and better founded: pass 1 is the PLACEHOLDER pass, and it is already lenient about an unresolvable RETURN type on the very same declaration, substituting a placeholder and logging that it did. The bound was the one strict thing in an otherwise lenient pass.
So pass 1 is lenient now and pass 2 is not. Nothing is lost by that, because pass 2 REBUILDS the function from its declaration after the alias fixpoint has run, and that build is authoritative:
- an alias bound resolves and IS ENFORCED —
pick<String>('a')still reportsdoes not satisfy bound 'num'(F-SCD100-12); - a genuinely undefined bound still stops the program before
mainruns (F-SCD100-15).
That second point is why leniency alone would have been a retreat rather than a fix, and it is checked rather than asserted.
A CLASS NEEDED TWO MORE TOUCHES, because a class is populated in place and its bounds are never re-extracted:
- the alias fixpoint now runs BEFORE the class pass as well as before the
function pass — in
d4rt_base.dartAND inmodule_loader.dart, which are separate entry points into the same ordering, a trap that file already warned about and that the test suite caught; InterpretedClass.resolveDeferredTypeParameterBoundsrepairs a bound pass 1 had to skip, and throws there if it still does not resolve.
AND A THIRD ENTRY POINT HAD NO ALIAS PHASE AT ALL. AstModuleLoader — the
analyzer-free line's module loader — was never given one when SCD100 added the
phase to the runner and to the reference tree's module_loader.dart, so a
typedef declared in a non-entry MODULE bound nothing there and 1 is N in
that module threw Undefined variable: N. Found by writing this fix down
rather than by a failure, because the reference's own comment about separate
entry points is what prompted the check. F-SCE130-LOADER-1 pins it, and fails
with that exact message when the phase is removed.
MEASURED: every alias target reaches a bound — a core type, a script class, a
bridged type, another alias, a generic target — on a function, a class and a
method, in any declaration order. class Box<T extends N> now behaves exactly
like class Box<T extends num>, including the pre-existing defect they share:
neither infers a type argument, so both reject Box(3) with Type argument 'dynamic' ... does not satisfy bound 'num'. That is scf27, not this.
ABLATED, both halves: removing pass-1 leniency fails F-SCD100-11/-12/-13/-14/-16 and leaves -15 green; removing the early alias fixpoint fails -14 alone.
Name resolution: no — an alias was already bound to its target by SCD100; this changes WHEN a bound consults the environment, not what a name resolves to.
1.173.0 #
Fixed — an all-null collection was refused by the binding check it was written for (sce129) #
SCD92 made a declared parameter's TYPE ARGUMENTS a real check, deriving the argument's own arguments from its CONTENTS because a native collection carries no element type d4rt can read back. Its own entry lists where it therefore stays permissive — an empty collection, a heterogeneous one, a top type, an unbound type parameter, a raw generic, every bridged instance.
ALL-NULL WAS MISSING FROM THAT LIST, and it is the same case as empty. null
inhabits every nullable type, so a collection of nothing but nulls constrains
its element type exactly as little as a collection of nothing does — but it
derives Null rather than nothing, and Null was then compared literally:
int f(List<String?> xs) => xs.length;
f(List<String?>.filled(3, null));
// was: type 'List<Null>' is not a subtype of type 'List<String>' of 'xs'
The annotation that produced the value refused it. Six sites: a parameter bind and a local variable declaration, over a list, a set, and either half of a map. A return and a for-in were unaffected — they reach the derivation by a different route.
IT IS A WIDENING, NOT A SUBTYPE RULE, and the distinction is the whole of it.
Null is not a subtype of String, only of String?; and
_appliedDeclaredType resolves an argument node by NAME, so a declared
List<String?> arrives with its nullability already gone — which is why the
message above says List<String> for an annotation that was written
List<String?>. No subtype rule could recover what was never carried. The
comparison is relaxed instead, in the same shape and the same place as SCD92's
existing int→double widening, and it widens the type used for the COMPARISON
only.
HOW IT WAS FOUND, because the route matters more than the fix. It is three of
the base bridge corpus's failures — widgets/table_test.dart,
foundation/stack_filter_test.dart, gestures/tap_drag_start_details_test.dart
— which have blocked the interpreter publish since 2026-09-15 with every unit
suite in three packages green. sce129 reproduced two of the three in ten lines
of plain Dart with no Flutter, then bisected 103 commits in seven steps to
a31d6ff88 / 1.99.0. Both numbers are the point: the corpus found a defect no
unit test could, and the defect was unit-testable all along.
New: F-SCD92-23..26 (the four shapes) and F-SCD92-27 (anti-vacuity — a wrong non-null element type is still refused); F-SCD92-AST-5 and -6 in the twin. Ablated: without the fix, -23..-26 fail and -27 passes, which is what says -27 is a control rather than a second copy of the claim.
Name resolution: no — the binding check decides subtyping, not which declaration a name reaches.
1.172.0 #
Fixed — two holes in the static name resolver (sce128, phase 2 groundwork) #
SCE128 wired SCD95's report-only pass to the environment and let it REFUSE a program, then ran it over the reference suite — a broader corpus than either of phase 1's sweeps. Enforcement is NOT landed (see the todo); what is landed is what that run found.
TWO FALSE-POSITIVE CLASSES, both structural:
- A LABEL REFERENCE. The declaration is a
Labelnode and was excluded; theouterinbreak outer;is a bareSimpleIdentifierunder aBreakStatement, so it read as an undefined name. The most frequent false positive by a wide margin. - AN EXTENSION TYPE'S REPRESENTATION. Its members read the representation bare
—
int get doubled => value * 2— and nothing recorded it as declared, so every such read was reported, twelve distinct members across the suite including the operator ones.
An extension type is now an OPEN class alongside mixins and extensions, because
it implements types that can leave the unit. That buys correctness at the
price of reach, and F-SCD95-13 states the trade rather than leaving it to be
discovered.
MEASURED, both sweeps re-run: the inline sweep goes to 1045 clean of 1069
(phase 1: 799 of 807, on a smaller corpus) and the flutter sweep to 2083 clean
of 2085 with ONE dirty, down from two. Every remaining flag was read and is a
true positive — a deliberately undefined name, an out-of-scope catch binding
used after its block, or an intentionally-unbridged SDK type.
The pass remains REPORT-ONLY; execute() does not call it.
Name resolution: no — the pass is not wired into execution.
1.171.0 #
Changed — an on clause naming an unresolvable type now fails (sce127) #
Real Dart refuses to compile it (non_type_in_catch_clause). d4rt logged a
warning nobody sees and answered false, so the divergence ran in the most
dangerous direction available — Dart rejects the program, d4rt silently runs a
DIFFERENT BRANCH of it. With a later clause, that clause took the branch and
the script returned a value from a handler its author never meant to reach;
with no later clause, the exception escaped the try as if the handler were
not written. A script author could only discover a dead clause by testing that
it fires, which is exactly the test people skip.
THE OLD CODE'S CONCERN IS ANSWERED RATHER THAN DISCARDED. Its comment read
"letting the lookup failure escape would replace the exception being dispatched
and lose the original" — true, and the reason it returned false. So the
failure does not escape: the diagnostic names BOTH the unresolved type and the
exception that was in flight, and points at the remedy.
MEASURED BEFORE CHANGING IT, because a legitimate unresolvable on type would
make working scripts start throwing. A class declared after main, a class
from another module, a prefixed p.Other, a generic List<int> and a
typedef alias all resolve. The only shape that does not is a type from a
library the script did not import — which Dart also rejects, so it is this same
defect rather than an exception to it. The other candidate, a bridge not
finalized when the clause is reached, does not arise: finalizeBridges runs
implicitly on first execute.
The check fires when the clause is REACHED, which is later than Dart and leaves a dead clause nothing ever reaches silent. That is recorded in the test rather than hidden; closing it needs a whole-program pass, which this change does not add.
SCD94's F-SCD94-1, -2, -3 and F-SCD94-AST-2 pinned the silent fall-through as observed behaviour and are flipped in the same commit, as that todo's successor required.
Name resolution: no. How a name RESOLVES is unchanged — only the reaction to a resolution that fails, inside a catch clause.
1.170.0 #
Fixed — String.fromCharCodes accepts any Iterable, as the SDK declares (sce126) #
SCD93 swept interpreter_visitor.dart for guards that pre-empt a native
operator; SCE126 swept the STDLIB BRIDGES with the same method, where 165 lines
carry a matching shape. 63 expressions were compared against real Dart, error
type for error type.
THE YIELD IS ONE. String.fromCharCodes cast its argument to List where the
SDK declares Iterable<int>, so a Set, a .map(…) or a .where(…) raised
_TypeError for input real Dart accepts. The cast is now to Iterable, which
is the SDK's own domain; a non-Iterable still raises the same _TypeError.
TYPE AGREEMENT OVER BAD INPUT WAS NOT ENOUGH TO FIND IT, and that is the method worth recording: all 37 bad-input cases agreed, so a sweep that asks only "does the wrong argument raise the right error" reports nothing. The divergence is visible only over GOOD input — a value the SDK accepts and the bridge rejects — so a second pass fed twenty-six Iterable- and Pattern-taking members something legal. Twenty-three of the twenty-six already agreed.
The agreements are pinned as well as the fix, because they are what stops the guards being reinstated.
Name resolution: no.
1.169.0 #
Fixed — a bare write to a static field from an instance method updates the static (sce125) #
class Box { static int v = 1; void go() { v += 1; } }
main() { Box().go(); return Box.v; } // real Dart 2, d4rt 1
THE READ PATH WAS ALWAYS RIGHT, which is what made the two halves disagree
rather than both being wrong. InterpretedInstance.get walks the class chain
and finds the static when no instance field shadows it. The WRITE path reached
thisInstance.set(name, …), which CREATES a field when none exists — so the
write minted a per-instance shadow, and the next bare read found that shadow.
IT WAS SILENT BECAUSE THE METHOD COULD READ ITS OWN WRITE BACK. v += 1; return v; answered 2, so any test that checks the value inside the method
passed; only a reader outside the instance saw the static unchanged. Two
instances did not even agree with each other — a saw 2 and b saw 1, of a
field the class declares static.
The write now mirrors the read's walk. In Dart a class cannot declare a static and an instance member of the same name, so finding a static means there is no instance member to prefer and the check can come first; a local, a parameter and an instance setter still take precedence as they always did.
Name resolution: no — this is assignment target resolution inside a class body,
not the Environment/bridge lookup the corpus rule is about.
1.168.0 #
Fixed — is Function answers for every value the interpreter can call (sce121) #
It answered true for a script function or a closure and false for every
native one. The damning measurement is not the false, it is the pair:
var f = 'abc'.substring; f(1) returns 'bc', and f is Function was false.
So the guard rejected a value the interpreter was perfectly able to call, and a
script written the Dart way — a plugin registry, a callback table,
if (x is Function) x() — silently took the else-branch for every native
callable. That reads as "d4rt cannot do that" rather than "the type test is
wrong", which is why it survived.
The population was enumerated before anything changed, as the todo asks:
measured false were a bridged instance-method tear-off, a bridged static
(int.parse), a constructor tear-off (Object.new) and a bridged top-level
(json.decode).
The fix is in the Function bridge rather than in the interpreter's type-test
path: Callable is the interpreter's own "can be invoked" interface and every
tear-off shape implements it, so isAssignable answers with it and the type
test agrees with what invocation already does.
A class that merely declares call is still NOT a Function, which is real
Dart and the case a fix aimed at "anything callable" gets wrong.
TWO HALVES STAY AND ARE WRITTEN DOWN: 'abc'.substring is String Function(int)
is false and runtimeType reports BridgedMethodCallable. Both are the same
fact — there is no function TYPE for a native callable, only the knowledge that
it can be invoked — and doc/d4rt_limitations.md Lim-11 records it, including
why a half-right function type would be worse than an honest class name.
Name resolution: no.
1.167.0 #
Fixed — a read-back LinkedListEntry renders the script's own toString (sce120) #
class E extends LinkedListEntry<E> — the idiom the SDK documents — works end
to end: the implicit super() constructs, add accepts the interpreted
instance, and the list round-trips it, so l.first.v, identical(l.first, e)
and for (final e in l) all reach the script's own class. That was closed by
SCD75's follow-through; SCE120 found the one place it still leaked.
The proxy unwrap is a FALLBACK: the interpreter tries the bridge's own members
first and only reaches D4InterpretedProxy.d4rtInstance when they fail.
myField fails and unwraps; toString SUCCEEDED, on the wrapper — so a script
declaring String toString() => 'E:' + v.toString() read back
LinkedListEntry(E:1), its own rendering wrapped in the name of a class it
never wrote, and the same object printed two different ways depending on
whether it had been through a list.
Fixed where the substitution is manufactured, in
_OwnedLinkedListEntry.toString, rather than by reordering the general unwrap:
the proxy exists to speak for the instance, so it answers with the instance's
own toString — which dispatches to the script's override and degrades to the
diagnostic form when there is none (SCD72, shared since SCE116).
Two wider gaps are pinned rather than fixed, both measured: l.first.runtimeType
reports the proxy type, which is a question about every D4InterpretedProxy;
and an interpreted class declaring no toString cannot have one called at all,
which is the universal-member path for every class.
Name resolution: no.
1.166.0 #
Fixed — every host boundary hands over the same shape (sce118) #
SCE118 was filed against execute() handing a caller a
BridgedInstance<Object> instead of the StateError the script threw. SCD96
closed that; measured before anything was changed, execute, eval, the
onUncaughtError hook and an embedder's own zone all deliver the SDK type.
WHAT WAS STILL OPEN was the todo's own closing instruction — state that all delivery paths hand over the identical shape, because nothing did. Stating it found a live divergence.
invoke() is the fourth boundary and was the last one deciding for itself. It
goes through _tryFunction, whose catch peeled the interpreted-throw carrier
by hand and stringified everything else into "$error : $e". A script method
that hit an ordinary interpreter fault therefore handed the host a String
where execute and eval hand over a RuntimeD4rtException — nothing
catchable by type, and nothing saying so. It is on throwAsHostFacingError
now, so the context the message carried is in the preserved stack trace
instead.
Four hand-rolled peels became one seam: the two eval sites in this tree also
carried their own, including a RuntimeD4rtException branch whose two arms are
the same throw with different static types. Converging those changes no
behaviour — measured — but they are how eval came to differ from the shared
helper by one level in the first place.
Name resolution: no.
1.165.0 #
Fixed — the last route that handed an embedder the interpreter's wrapper (sce117) #
SCD73 made the unwrapping of InternalInterpreterD4rtException unconditional
for every callback the zone REGISTERS, and pinned the one shape it could not
reach: a handler passed to Stream.handleError. The SDK invokes that handler
with no zone registration at all, so there was no register*Callback seam to
wrap — adding runUnary and runBinary was tried and changed nothing except
double-wrapping the Stream.listen case.
Zone.errorCallback fires for that shape and — this is why it is the right
seam rather than merely an available one — IS NOT AN ERROR-ZONE HOOK.
Specifying it leaves Zone.errorZone resolving to the parent's, so the
property that ruled handleUncaughtError out does not arise: an awaiting
caller outside the zone still receives an ordinary script failure rather than
hanging.
THE BLAST RADIUS WAS MEASURED, NOT REASONED. errorCallback is consulted for
errors entering futures generally, so the question was whether an interpreted
catch would start seeing a different value. A thirteen-row matrix of
in-script shapes is byte-identical with and without the seam, and is a
standing test now rather than a hand measurement nobody could re-run.
unwrapScriptError stays public and stays correct on a value that no longer
needs it, so an embedder's defensive call has not become a bug.
Name resolution: no.
1.164.0 #
Fixed — an enum value and an extension-type instance dispatch to their toString override (sce116) #
SCD72 taught InterpretedInstance.toString() to dispatch to a script's own
override and left two siblings in the same file behind.
InterpretedEnumValue got the DEFAULT right and the override wrong — which is
why it was easy to miss, since P.a is what a host wants and what Dart prints
— and InterpretedExtensionTypeInstance got both wrong.
THE MECHANISM IS SHARED, NOT COPIED. SCD72's body — find the override, get a
visitor, guard re-entry, dispatch, degrade on anything recoverable, rethrow
StackOverflowError and OutOfMemoryError — is now
renderInterpretedToString, called by all three. Every line of it was there
for a measured reason and the reasons apply unchanged; a third and fourth copy
would have been three and four places for the next correction to land in.
The re-entry guard is ONE identity set for all three types, because a cycle can run through them and three separate guards would each see a first visit.
InterpretedEnum and InterpretedExtensionType gained the
declaringVisitor wiring InterpretedClass already had, assigned once per
declaration — toString() is a plain Object override with nowhere to receive
a visitor, and the ambient one is null by the time a host reads it.
An enum's override may come from a mixin, so the lookup walks them in the same
order InterpretedEnumValue.get does.
THE CONTRACT STILL SPLITS BY CALLER. stringify — interpolation inside a
script — keeps Dart's semantics and propagates; toString(), which host code
reaches, degrades. Sharing a call between them is how that could have been
lost, so stringify gained explicit branches for the two new types and calls
the strict form.
Name resolution: no.
1.163.0 #
Fixed — an extension-type instance renders as the thing it wraps (sce116) #
extension type Y(int v) {} declares no toString, so the call erases to
Object.toString() on the value Y wraps and real Dart prints 7. d4rt
printed <instance of Y>.
AN EXTENSION TYPE IS ITS REPRESENTATION AT RUN TIME, which is the whole
reason: the wrapper is a static fiction with no runtime existence to describe.
<instance of Y> was not an imprecise rendering of the right object, it was a
rendering of the wrong one.
This is the half of SCE116 that has nothing to do with overrides, and it lands on its own because it is a different defect that happens to live in the same method.
Name resolution: no.
1.162.0 #
Fixed — every transform adapter resolves its argument the same way (sce114) #
There are four transform adapters and only one resolved its argument
properly. Stream.transform went through _asStreamTransformer, which accepts
a native transformer, a bridged one, and — the case StreamTransformerBase
exists to enable — an interpreted class with a bind method. Both socket
adapters wrote
final separator = positionalArgs[0] as StreamTransformer;
A cast admits the first two shapes and rejects the third with a host
_TypeError naming InterpretedInstance. So the same script class worked on
a stream and failed on a socket, and the failure named an interpreter-internal
type rather than the argument.
The resolver moved out of async/stream.dart into
stdlib/stream_transformer_arg.dart, beside run_action.dart and
stream_listen.dart — which were extracted from the same kind of duplication
— and all four adapters now ask one function. A non-transformer argument, or
a missing one, reaches one sentence naming the member the script called.
ServerSocket.transform also reported itself as Socket.transform, copied
along with the body it was copied from.
THE .cast() IN THE SOCKET ADAPTERS STAYS. SCE83's _bindTransformer coerces
the SOURCE instead, and the two sites differ for the reason its doc gives: a
socket's element type is known statically so the cast is computed against it,
while a bare Stream has the cast inverted on it. Only the argument
resolution was shared.
The two halves this was filed for had already been closed, by SCE83 and
SCD187, and neither had a test — response.transform(utf8.decoder), the line
every Dart HTTP example contains, now has one.
Name resolution: no.
1.161.0 #
Fixed — Function.apply takes Symbol keys, as the SDK declares (sce113) #
Function.apply(Function, List?, [Map<Symbol, dynamic>?]) is the SDK
signature. The adapter read Map<String, Object?>, because that is what
d4rt's own Callable.call takes — so {#b: 2}, the only spelling the
analyzer accepts, threw, and {'b': 2}, which no Dart program can contain,
was the one that worked. The named-argument half of Function.apply had
therefore never worked for any legal program.
That matters more than a wrong key type would suggest: Function.apply is how
a script calls a function whose parameters it does not know statically, and
there is no other route to it.
Symbol keys are now translated to names in the adapter — MirrorSystem.getName
is unavailable in the analyzer-free twin, so the name is read off
Symbol.toString(), which is exact because every Symbol a script can produce
is built at run time from a literal or through the Symbol bridge.
String keys are REJECTED rather than accepted alongside Symbols, and the message names the legal spelling. d4rt matches the SDK per construct, so that a script ported from Dart behaves the same and a script written against d4rt still compiles as Dart. The loose spelling was one commit old and unpublished.
Two smaller defects in the same adapter, found while measuring: both argument
lists are nullable in the SDK and null was an error for each, and the
adapter forwarded its own (always-empty) named arguments when the caller gave
no map.
SCD70 made the original visible rather than causing it — before that sweep the
adapter cast and every call died with an opaque _TypeError.
The neighbours were measured and are the counter-example: Invocation.method
and .genericMethod take the same Map<Symbol, …> and keep it Symbol-keyed
all the way through, which is correct, so the translation is scoped to this one
adapter.
Name resolution: no.
1.160.0 #
Fixed — a module's parse errors are English, and on separate lines (sce111) #
The module path's diagnostics read (ligne N, colonne M) — the only French
left in the package, and inconsistent with the direct-source path in
d4rt_base.dart, which is the same report for the same event one caller over.
And they were joined with "\\n", which in a double-quoted Dart string is an
escaped backslash followed by n, not a newline. A module with four syntax
errors therefore arrived as ONE run-on line containing the characters \n,
while the direct-source path — which joins on a real newline — printed one per
line. A reader scanning a module failure for line 1, column 19 found neither
the word nor the line break.
Both were found while converging the module-failure SENTENCE with
tom_d4rt_exec (sce111), which is the more visible half of that todo and the
smaller defect of the three.
Name resolution: no.
1.159.0 #
Fixed — seventeen adapters read an optional positional parameter as named (sce110) #
SCD68 checked that every namedArgs['x'] in a bridged CONSTRUCTOR is a claim
the SDK signature supports, and scoped itself there on a stated hypothesis:
a method adapter "is often a hand-written convenience over several SDK members
and the one-to-one mapping this check relies on does not hold".
Nobody had counted. Counted now, over 279 claims:
| section | claims | resolved one-to-one |
|---|---|---|
| methods | 145 | 145 |
| constructors | 71 | 70 |
| staticMethods | 63 | 63 |
The hypothesis was wrong by 278 to 1 — the single exception is BytesBuilder's
unnamed constructor, a factory on an abstract class mirrors does not expose.
TWO LOOKUP FIXES WERE NEEDED TO SEE THAT, and both were the checker's fault
rather than the bridges'. Resolving only through superclass reported 47
unresolvable, because HashSet IMPLEMENTS Set and reaches firstWhere
through no superclass at all; adding superinterfaces took it to 14. The
remaining 13 were factory constructors exposed as statics — List.filled,
Map.fromIterable, int.fromEnvironment — an ordinary bridge shape whose
signature mirrors can read, so the static lookup now falls back to the
constructor of the same name.
The widening found 17 mismatches, all the same shape as SCD68's:
Uri.parse / tryParse / parseIPv6Address, RandomAccessFile.lock /
lockSync / unlock / unlockSync, and HttpClientResponse.redirect each
read an optional POSITIONAL parameter out of namedArgs. Read that way they
were unreachable — the only spelling that fills them is one Dart refuses to
compile — so Uri.parse(s, 5) ignored its offset and every lock() took an
exclusive whole-file lock whatever it asked for. All read positionally now,
length-guarded, and the guard covers all three sections with its anti-vacuity
floor raised from 55 to 210 so the two new thirds cannot stop being walked
quietly.
Name resolution: no.
1.158.0 #
Fixed — a runtime condition raises the SDK's Error, so on RangeError catches (sce109) #
Two adapters reported a runtime condition of the SCRIPT — an index out of
range, no element matching a test — with an interpreter exception instead of
the SDK's Error. RuntimeD4rtException is not an Error at all, so the
idiomatic handler was skipped and the script fell through to a bare catch, or
to nothing:
try { [1].elementAt(5); } on RangeError { … } // did not catch
THE TWO HAD DIFFERENT CAUSES, and only one was the hand-written guard the defect looked like from outside.
firstWhere was a hand-rolled loop whose no-match branch threw
RuntimeD4rtException('No element found matching the test condition') — an
invented contract. Its neighbours lastWhere and singleWhere delegate to the
native call and were already correct, which is what made the loop's stated
reason ("éviter les problèmes de types génériques") checkable: all three wrap
the callback identically. It delegates now.
elementAt had no guard at all. SCB28 added a heuristic at the DISPATCH
boundary: a bridged call that throws RangeError is assumed to be an adapter
that read past the end of positionalArgs, and is restated as an arity
failure. [1].elementAt(5) is one positional argument on a one-element list,
so the reported range 0..0 matches the argument list's 0..0 and the heuristic
fires on the script's own error.
THE HEURISTIC CANNOT BE MADE EXACT, which is measured rather than assumed:
[1].elementAt(5) and an adapter's positionalArgs[5] produce byte-identical
RangeErrors — same name, start, end, invalidValue, and neither is an
IndexError, so there is no indexable back-reference to compare. Its own doc
had judged the misattribution acceptable because it "only ever changes the
wording of an error that was already being thrown". The wording was not the
only thing that changed.
So the fix makes being wrong cheap rather than pretending to be right: the
detection and the arity message stay, the original error text leads, the arity
reading follows as a hypothesis, and the TYPE is preserved at all nine throw
sites. on RangeError now catches under either reading.
Name resolution: no.
1.157.0 #
Fixed — a failing cast pattern throws, and there is only one cast (sce104) #
case var x as T raised PatternMatchD4rtException when the cast failed, and
every arm-selection site catches exactly that and reads it as "this arm did not
match". So the failure was converted into arm selection: a program that should
stop ran on down default, into a branch its author wrote for a different case.
as in a pattern exists to assert; a cast pattern that cannot fail is a cast
pattern that does nothing.
THE FIX IS NOT "THROW INSTEAD". v as T and case var x as T are the same
operation, and they were implemented twice — an eleven-name ladder in
visitAsExpression and a nine-name ladder in the CastPattern branch, each
with a permissive default. Measured before the merge they disagreed on eight
inputs, and each knew something the other did not:
| input | expression | pattern | Dart |
|---|---|---|---|
's' as int |
throws | MISS | throws |
1 as double |
1.0 | MISS | 1.0 |
1 as Null |
throws | HIT | throws |
's' as I (=int) |
throws | HIT | throws |
's' as Map |
RETURNS 's' | miss | throws |
's' as Set |
RETURNS 's' | miss | throws |
The pattern lacked Null, alias resolution (SCD100) and the int→double
promotion (GEN-094); the expression lacked Map and Set entirely, so a
failing cast to either RETURNED ITS OPERAND. Turning the pattern's ladder into
a throw without merging would have shipped four of those rows wrong. One body,
_tryCast, ends both.
It returns a sentinel rather than throwing, so each construct keeps its own
message: the SDK words a failed as and a failed cast PATTERN differently, and
matching it per construct is the point of D4rtTypeError.
THE CAST'S RESULT IS WHAT BINDS, not the operand — 1 as double binds 1.0, and
a proxy cast to the class it wraps binds the interpreted instance (C21).
DELIBERATELY UNCHANGED: the shared ladder's default stays permissive, so
A() as B between unrelated script classes still succeeds, in the pattern and
the expression alike. That is a separate decision with its own blast radius, and
it is pinned as a case so the limit is recorded rather than discovered.
Blast radius, measured because this change CAN break a green test: zero across the reference suite.
Name resolution: no.
1.156.0 #
Fixed — a typed local is checked, at its declaration and at every write (sce103) #
The fourth and last site that asks "does this value fit this written type", and the widest: every typed local in every script passes through it.
| site | closed by |
|---|---|
| parameter binding | SCC29 |
| typed patterns | SCC18 |
| for-each loop variable | SCD63 |
| typed local | SCE103 |
int x = 'two'; bound the String. So did every later x = 'two';, and so did
for (x in ['two']) — the for-each IDENTIFIER form, which SCD63 pinned as a
known gap precisely because it belongs here: the loop carries no annotation, the
type was written at x's own declaration, and checking it is the same job as
checking any other write to x.
WHY THIS SITE NEEDED MORE THAN A NEW CALL. The other three check at a single
moment and remember nothing. A declaration and a later assignment are two
moments, and only the first carries the annotation — so the resolved binding is
recorded per name in the environment that declares it, and assign consults it.
A frame with no typed local pays one null probe.
late is out of scope and fields and top-level variables are not reached; both
limits are pinned as cases rather than left to be discovered.
THE BLAST RADIUS WAS MEASURED, NOT ASSUMED. Across the reference suite: ONE
behavioural failure, and it was not a false positive. Set<int> numbers = {};
evaluated to a Map — {} is a Map unless the context type says Set, Dart's
rule, and the interpreter has no context type — and the test that failed had
only ever passed because it asked isEmpty, which a Map answers too. Everything
else about such a variable was already broken: s.add(1) threw "Bridged class
'Map' has no instance method named 'add'", and f(Set<int> s) called with {}
threw this very type error through SCC29's parameter check. So the new check did
not break a working program; it found the fourth site of a defect three sites
already had.
The disambiguation now lives in ResolvedBinding.bind, beside the int→double
widening that is there for the same reason. EMPTY ONLY — a non-empty Map bound
to a Set is a real error and still fails.
Name resolution: no.
1.155.0 #
Fixed — an empty loop body inside async no longer ends the function (sce102) #
The async state machine enters a loop body by taking the body's first statement:
currentNode = (node.body as Block).statements.firstOrNull;
currentState.nextStateIdentifier = currentNode;
continue;
For {} that is null, and the machine's own loop is while (currentNode != null) — so the FUNCTION ended there. Not the loop. Every statement after it
was skipped and the declared return value never happened. Measured in an
async function:
| loop | returned | expected |
|---|---|---|
for (final x in [1, 2]) {} |
null | 'ran' |
for (var i = 0; i < 2; i++) {} |
true | 'ran' |
await for (final x in s) {} |
true | 'ran' |
while (i++ < 2) {} |
null | 'ran' |
Three different wrong answers from one cause: the value is whatever
lastResult held, which is null after a for-in and the CONDITION after the two
forms that had just evaluated one. A reader who met only the true would go
looking for a condition bug.
THE RETURN VALUE IS THE SERIOUS PART. A loop that does nothing, doing nothing, is invisible. A function that silently returns the wrong thing is not: the caller gets it, and the failure surfaces wherever that value is finally used, with nothing pointing back at the empty body.
THE FIX WAS ALREADY IN THE FILE. SCE19 met this at the do-while entry and
solved it there — currentNode ??= doNode, fall back to the loop node and let
the loop decide what comes next. The remaining five dispatch sites did not have
it. The C-style for is the instructive one: it had the INTENT, under a
comment explaining the empty-body case and setting nextStateIdentifier = forNode, but never assigned currentNode — so continue re-tested the
machine's while against a null that was still null. Half a fix reads exactly
like a whole one at the call site, and that site had read like a whole one
since it was written.
The synchronous path was never affected; it does not use the state machine.
Name resolution: no.
1.154.0 #
Fixed — a generic element is asked the same question as is (sce101) #
interpreter_visitor.dart carries two predicates for "does this value have
this type". _valueHasType answers the is operator, typed patterns, the
declared-type check and on clauses. _checkValueMatchesType answers the
ELEMENT and KEY/VALUE types of a generic collection, and it answered five
questions differently:
| question | x is T |
[x] is List<T> |
|---|---|---|
null against Null |
true | FALSE |
null against dynamic |
true | FALSE |
a class against Type |
true | FALSE |
| a NATIVE bridged value against its own class | true | FALSE |
a WRAPPED bridged value against List |
true | FALSE |
Null, dynamic and Type had no arm at all — dynamic shared Object's
"non-null", which is wrong for exactly the value dynamic exists to accept.
THE LAST TWO ROWS ARE MIRROR IMAGES, and that is the part worth reading. The
shape arms (int, List, Map, …) answer with the host's own is, so they
needed a native and got the WRAPPED form wrong; the user-type arm required a
BridgedInstance and got the NATIVE form wrong. A bridged value reaches the
interpreter in both forms — dart:collection's bridges hand back natives, a
bridge whose constructor returns a BridgedInstance hands back a wrapper — so
each arm was broken for the input the other one handled.
That symmetry is also why the defect survived being looked for. The obvious
probe is UnmodifiableListView and HashMap, as SCB7 used; measured, those
now evaluate to native objects, so the probe comes back green and the missing
unwrap looks like dead code. It is not — it needs a bridge that still wraps.
The five arms are added in place, and _nativeOrBridgedMatches is extracted
from _valueHasType's bridged branch so both predicates share the reasoning
about operand form rather than carrying a third copy of it. This is NOT a
delegation of one predicate to the other: _checkValueMatchesType is
deliberately lenient where _valueHasType is strict — an unresolvable type
name returns true there and false here — so collapsing them changes answers on
the hot path of every is, and is separate work.
void stays divergent, deliberately: x is void does not parse, so the other
predicate's void arm is unreachable from the operator and its own comment
hedges about the right answer. Agreeing would mean choosing between two
unreachable answers with no case to appeal to.
Name resolution: no. environment.get(typeName) is unchanged and no lookup
rule moves; what changed is what the resolved BridgedClass is compared
against.
1.153.0 #
Documented — the permission gates' null-interpreter branch is the un-sandboxed mode (sce87) #
No behaviour change. The five dart:io permission gates reach the permission
table through a nullable handle and return when it is absent:
final d4rt = visitor.moduleLoader.d4rt;
if (d4rt == null) return; // no D4rt, no check
An early return from a gate GRANTS, in the capabilities the sandbox exists for,
and the analyzer-free twin — which has no nullable handle and always calls
checkPermission — reads as the corrected version. Measured, it is neither a
hole nor a correction:
- the branch is UNREACHABLE FROM A SCRIPT.
d4rt_base.dartconstructs exactly oneModuleLoaderand passesd4rt: this; the onlylib/code that builds a d4rt-less loader is the bridged-enumtoStringfallback, which calls atoStringadapter inside atry/catchand reaches no gate; - the gate is LIVE on the path a script takes — granting
FilesystemPermission(whichdart:ioneeds to import at all) and withholdingDangerousPermissionrefusesPlatform.version; - the twin is PERMISSIVE IN THE SAME STATE.
NoOpModuleContext.checkPermissionreturnstruewhen no checker is wired, under its own comment "be permissive (allow all)".
So both trees grant when nothing sandboxed them, and differ only in where that is expressed. Denying here would make the reference refuse where the twin allows — introducing a behavioural divergence rather than removing one, and breaking the documented mode in which a bridge is driven directly.
All five gates now say this, and both halves are pinned by
sce87_permission_gate_null_handle_test.dart in each tree. The load-bearing
case is the structural one: a second ModuleLoader construction, or that
d4rt: argument going away, is what would turn the branch into a real hole.
1.152.0 #
Fixed (BREAKING for scripts using the old entry form) — LinkedListEntry can be subclassed (sce84) #
LinkedList is only usable through a subclass of LinkedListEntry — the SDK
declares it abstract base mixin class LinkedListEntry<E extends LinkedListEntry<E>>, so there is no other way in. Interpreted code could not
write one:
class E extends LinkedListEntry<E> { final int v; E(this.v); }
final l = LinkedList<E>(); l.add(E(1));
-> Error during implicit bridged super constructor:
Constructor LinkedListEntry(value) expects one positional argument.
The bridge wrapped d4rt's own concrete stand-in, whose constructor carried the
entry's value, so the implicit super() every subclass makes could never
match. It now takes no arguments, as the SDK's does, and a script carries its
payload on its own class where Dart carries it.
Two non-SDK members went with it, and this is breaking for any script that
used them: the LinkedListEntry(value) constructor and the value getter.
The SDK's entry has list, next, previous, insertAfter, insertBefore,
unlink and nothing else, so a script using either ran here and did not
compile as Dart — the same judgement removeFirst got in SCC8, applied to a
constructor and a getter. The diagnostic says what to write instead.
The list hands back the script's own objects. A native LinkedList can
only hold native entries, so first, last, iteration and map produce the
entry the bridge minted; it carries the interpreted instance as a
D4InterpretedProxy, and the interpreter's existing unwrapping makes
list.first.myField reach the script's class.
Identity follows, because two carriers of one object must not answer false
to "is this the entry I added". identical, identityHashCode and the == /
!= operators now see through a native proxy to the instance behind it, and
LinkedList.contains — the one inherited member whose argument is an element —
unwraps its argument. This is the first consumer of D4.interpretedBehind,
promoted from a private helper in the binding checker.
1.151.0 #
Fixed — stream.transform(utf8.decoder) failed for every stream a script made (sce83) #
final s = Stream<List<int>>.fromIterable([[104, 105]]);
await s.transform(utf8.decoder).join();
raised type '_MultiStream<dynamic>' is not a subtype of type 'Stream<List<int>>' of 'stream' — a host TypeError naming an interpreter
internal, uncatchable as a RuntimeD4rtException and telling a script author
nothing to do. SCD187 fixed the dart:io half of this by deleting a stub; the
stream in that case comes from the SDK and really is a Stream<List<int>>.
A stream the SCRIPT built is not.
The cause is the interpreter's value model rather than this member. A script's
values are dynamically typed natively — Stream<List<int>>.fromIterable is a
Stream<dynamic> carrying List<Object?> chunks — and every bridge coerces at
its own boundary instead. utf8.decoder.bind(s) worked on the same stream
throughout, because Utf8Decoder.bind does exactly that coercion. So Dart's
two spellings for one operation disagreed, and the broken one was the one every
tutorial uses.
Stream.transform now coerces the source the same way: element-wise to
List<int> for a byte transformer, to String for a text one, and untouched
for anything else — which includes script-defined transformers, whose input
type the interpreter's own values already satisfy.
Casting the TRANSFORMER instead, which is what the two Socket.transform
adapters do, was tried first and inverts the defect: a File.openRead() stream
then rejects the CastConverter it is handed. Those adapters are right for
themselves because they know their element type statically; a bare Stream
does not. Both directions were measured before either was written.
1.150.0 #
Added — the last 51 confirmed member gaps are bridged (sce82) #
SCD44 widened the audit and took its confirmed-unreachable count from 0 to 51
without touching lib/: every one was a member a script could never call, and
the audit simply could not see them. They are now bridged, and the count is
back to 0.
| class | members |
|---|---|
HttpHeaders |
the 45 remaining static header-name constants |
RawSocketOption |
levelIPv4, levelIPv6, IPv4MulticastInterface, IPv6MulticastInterface |
ConnectionTask |
fromSocket |
Platform |
lineTerminator |
HttpHeaders was the bulk of it, and the half-bridged state was the worst one
to be in: seventeen constants resolved and forty-five did not, so
headers.set(HttpHeaders.acceptRangesHeader, …) failed while
HttpHeaders.acceptHeader beside it worked — the ones that resolve teach the
author to expect the rest.
Each constant DELEGATES to the SDK's own ((visitor) => HttpHeaders.teHeader)
rather than repeating its value, so a wrong name is a compile error rather than
a bridge that resolves and quietly returns the wrong header. The cases assert
against dart:io directly for the other half of that: that each bridge is
wired to the constant it claims.
Platform.lineTerminator goes through the same dangerous-permission gate as
every other Platform getter. It is a pure value, but it is a host property,
and bridging it ungated would have made it the one Platform member a script
can read without a grant.
What remains measured-unreachable is the five ByteBuffer and RawSocket
members that are unreachable BY DECISION, each with its reason recorded.
1.149.0 #
Fixed — a cascade section could not take an awaited argument (sce81) #
sb..write(await Future.value('a'))..write('b');
failed with type 'AsyncSuspensionRequest' is not a subtype of type '(List<Object?>, Map<String, Object?>)' — an interpreter internal, surfaced to
the script author. The same code without the cascade always worked.
Two of the four _executeCascade* helpers returned void, because a cascade
section's VALUE is deliberately discarded: the cascade evaluates to its target.
But a discarded value and an unfinished one are not the same thing, and
_evaluateArguments signals the second by returning the suspension sentinel,
so the record destructuring failed.
THE SECTIONS ARE NOW MEMOISED, which is what makes this a fix rather than a
refusal. Dart evaluates a cascade's target once and runs its sections in order
for their side effects, and the interpreter drives await by REPLAY — so a bare
propagation would re-evaluate the target and re-run every earlier section:
sb..write('a')..write(await f()) would write 'a' twice. The target and the
completed sections are recorded on AsyncExecutionState, keyed by node, and
dropped when the cascade finishes so a cascade inside a loop starts fresh each
iteration.
The resumption side needed two changes of its own. A cascade SECTION cannot be
re-executed standalone — its target is implicit, so accepting it resolved the
method name as a bare identifier — so the await context is lifted to the
enclosing CascadeExpression, which then had to be admitted to the
re-execution branch. Lifting without admitting left nothing to re-execute, and
the machine completed the function with lastAwaitResult, skipping every
statement after the cascade.
Ten cases. One puts an observable side effect on both sides of the awaiting section and asserts each happened exactly once — without it a propagate-and- replay fix that silently doubles work passes everything else.
1.148.0 #
Fixed — await in a collection literal stored the suspension sentinel (sce80) #
[await Future.value(1)] // was: [Instance of 'AsyncSuspensionRequest']
Every collection-literal position had this, silently: list elements, map keys,
map values, set elements, if elements, null-aware elements. Only the spread
case reported anything, and only because a sentinel is not an Iterable.
The cause was visible in the signature. The interpreter drives await by
replay — visitAwaitExpression returns an AsyncSuspensionRequest and every
visitor propagates it upward — but _processCollectionElement returned void,
so it had no way to say "the element I was evaluating has not finished". It
stored the sentinel instead, and the two literal visitors could not propagate
either. It now returns Object?: the suspension, or null when the element
completed.
ONE SHAPE IS REFUSED RATHER THAN FIXED, and that is deliberate. An await in
the BODY of a collection-literal for element cannot propagate: replay
re-evaluates the whole literal, so the loop would run its earlier iterations
again, and resolvedAwaitResults is keyed by the AwaitExpression NODE — which
every iteration shares — so the second iteration would replay the first one's
value. A list of duplicates is the same class of silent defect this change
exists to remove, so the construct is diagnosed and the message names the
statement form that does work:
var out = []; for (var x in xs) { out.add(await f(x)); }
The for element's ITERABLE is evaluated once and propagates normally; only
the body is refused, and only when it actually suspends.
Fifteen cases. Every one compares the collection's CONTENTS: a sentinel counts
as an element perfectly well, so a first probe of the map and set shapes checked
.length and reported them healthy.
1.147.0 #
Fixed — the async catch variable leaked out of its block (sce79) #
Future<dynamic> f() async {
var e = 'outer';
try { throw StateError('x'); } catch (e) { }
return e; // returned the exception, not 'outer'
}
_handleAsyncError defined the exception variable in the FUNCTION's
environment — its own comment said "can cause collisions" — while
visitTryStatement has always given the synchronous path a child environment,
which is what Dart requires. So the variable outlived its block, and in an
async function any caught exception silently overwrote a caller's local of the
same name. e is one of the most common names there is; nothing threw and
nothing logged, and the wrong value surfaced wherever the variable was next
read.
The catch block now runs in its own environment, recorded against its clause on
AsyncExecutionState and SELECTED from the node's position rather than pushed
and popped. That choice is the substance of the fix: the state machine resumes
at a node rather than executing a block, so a stack would have to be popped on
every exit — fallthrough, return, rethrow, break, an error — and one missed
exit would leak the scope again in a way nothing notices. Selecting
structurally, there is no exit to instrument because there is nothing to undo,
and the scope survives suspension for free.
The environment's parent is whatever was selected when the error was handled, so a catch inside a loop still reaches the loop's variables; and the loop environment keeps winning inside a catch, because a loop opened there built its own as a child of the catch's. That one condition — prefer the catch environment only when the already-selected one does not already reach it — is what makes both nestings come out right with no ordering bookkeeping.
Ten cases in test/scc12_await_in_finally_test.dart. The clobber is pinned
beside the leak deliberately: the leak alone could be "fixed" by clearing the
variable after the block, which would leave the clobber untouched and look
green.
1.146.0 #
Fixed — a return inside an async finally hung the interpreter (sce78) #
Future<int> f() async {
try { throw StateError('x'); } finally { return 5; }
}
Real Dart returns 5, and d4rt's synchronous path already did. The async state machine never completed the function's Future.
CAUSE. When a ReturnException reached the state machine it asked whether
activeTryStatement has a finally block, and if so stored the value and jumped
to that block's first statement so the finally would run first. With the
return written INSIDE that same finally, activeTryStatement was still that
try — so the jump went back to the top of the block that had just issued the
return, and did it again. The symptom is a HANG rather than an unresolved
future, and no Dart-level timeout contains it: the machine reschedules through
Future.microtask, so a starved event loop never runs the Timer.
scd43 closed the other half of this shape — a throw inside a finally is offered
to the try ENCLOSING that try — but a return is not an error and never enters
_handleAsyncError, so it needed its own fix on the ReturnException path.
FIXED STRUCTURALLY, per scd41's recorded preference and the way scd43 did its
half: _isInsideFinallyBlockOf asks whether the return sits inside this try's
own finally, which is a property of the AST rather than of what has run, so no
new field on AsyncExecutionState was needed.
The return does not complete the function on the spot. It discards the pending
exception — Dart's rule is that the finally's abrupt completion replaces
whatever the try body was doing — and then leaves through any ENCLOSING try's
finally, whose own return replaces it in turn. Completing immediately answered
1 for try { try { throw X } finally { return 1; } } finally { return 2; }
where Dart answers 2.
Eight cases in test/scc12_await_in_finally_test.dart, including two controls:
the finally must still RUN (not be skipped), and a finally WITHOUT a return must
still propagate the exception.
1.145.0 #
Changed — resolution failures are now marked at the throw site (sce77) #
RuntimeD4rtException.resolutionFailure is a new named constructor setting a
new isResolutionFailure flag. 52 throw sites across interpreter_visitor,
callable, environment and runtime_types use it: the ones that mean A NAME
DID NOT RESOLVE — an absent member, constructor, enum value or variable.
Nothing about the hierarchy changed, and that is deliberate. Every catch
clause and every is RuntimeD4rtException test keeps working unchanged, which
sce67 established is load-bearing — the supertype there was ADDED rather than
swapped precisely because the hierarchy is consulted for control flow in eight
places per visitor. A new subtype would have changed what those sites see. The
messages are untouched too.
WHY. The gap audit decides whether a member exists by classifying the error the
interpreter produced, and it did that by matching the TEXT against a
hand-written list of wordings. Nothing connected that list to the places the
interpreter throws from, so a wording the list did not know made a whole audit
column silently unfalsifiable: the probe ran, the error arrived, and it was
scored as reachable. That happened twice in consecutive todos — scd36's
Cannot access property 'x' on target of type _Foo left the return-type pass
reporting 0 of 411 while blind to every gap it existed to find, and scd39's
operator wording did the same to the operator column. Both were found by
planting a defect; no passing test could have found either.
The flag ties the two together structurally, so a reworded message can no longer leave the set silently.
Sites whose wording READS like a resolution failure but is not one are recorded
rather than tagged — Unsupported operator (...) fires both when an operator
does not resolve and when the operand types are wrong, and tagging it would make
the audit report gaps it invented.
1.144.0 #
Changed (BREAKING for TLS scripts) — installing key material now needs CertificatePermission (sce74h) #
SecurityContext's eight certificate- and key-loading members are gated by a
new CertificatePermission. The four path variants (usePrivateKey,
useCertificateChain, setTrustedCertificates, setClientAuthorities) keep
their existing FilesystemPermission check as well; the four *Bytes variants
were previously ungated entirely and are now gated too.
WHY A CAPABILITY OF ITS OWN when FilesystemPermission already covered the
read. It covered it as an ORDINARY read, and a private key is not an ordinary
read: a script scoped to a directory that happens to hold one could hand it to
a SecurityContext and serve traffic under the host's identity, with nothing
in the grant list saying that was possible. Naming the capability separately is
what lets an embedder allow a script to read its own data directory without
also allowing it to impersonate the host.
WHY THE BYTES VARIANTS ARE GATED, though they read nothing. The capability is "install key material into a TLS context", not "open a file" — scd171 left them ungated on the reasoning that a script holding the bytes had already passed a gated read, which is true of the FILE but not of the installation. Gating only the path forms leaves the shorter route to the same end wide open.
A script that loads certificates needs one more grant:
d4rt.grant(CertificatePermission.load);
The gate lives in stdlib/io/certificate_permission_helper.dart rather than
inline, following scd170's precedent: the permission idiom differs between the
twins, so confining it to a helper keeps io/tls.dart code-identical and
leaves scd49 one small recorded divergence instead of a large one.
1.143.0 #
Fixed — an SDK range error could not be caught across a bridged method (sce74) #
BridgedMethodCallable caught ArgumentError to improve the message for
adapter arity problems. RangeError extends ArgumentError and IndexError implements RangeError, so that one clause swallowed every SDK RANGE failure
raised by any bridged method and reissued it as an uncatchable
RuntimeD4rtException. A script written the idiomatic way —
try { q.elementAt(0); } on RangeError { ... }
did not catch, and the recovery path its author wrote never ran. Measured on
empty Queue, ListQueue and DoubleLinkedQueue, elementAt(0) answered
RuntimeD4rtException: Invalid arguments for bridged method 'ListQueue.elementAt' where Dart answers IndexError.
The arm is narrowed, not deleted: a preceding on RangeError { rethrow; } in
both the instance and static call paths. The clause below still earns its keep
for a plain ArgumentError, which adapters raise for arity and argument-shape
problems that have no SDK counterpart. Nothing in stdlib throws a bare
RangeError or IndexError — zero sites, measured — and the interpreter's own
range failures use Ranged4rtException, so no interpreter error leaks into a
script's on RangeError.
The same clause exists a third time, on the bridged-SUPERCLASS call path in
runtime_types.dart, and is narrowed identically. No test reaches that one:
measured, a super. call resolves only members the bridged superclass DECLARES,
not ones it inherits, so super.removeFirst() works and answers StateError
while super.elementAt(0) answers "Method 'elementAt' not found in bridged
superclass 'ListQueue'" — and every RangeError-raising member is an inherited
one. The arm is kept because fixing two of three call paths is the
incompleteness the mirror rule exists to prevent, and rethrowing rather than
rewrapping cannot break anything. That resolution gap is a separate defect.
This is scd31's shape (2) — throws the WRONG TYPE where the SDK also throws —
which until now had no detector: the sweep and the standing nullable test both
read a returned value, which is what shape (1) changes and shape (2) leaves
alone. test/stdlib/sce74_sdk_error_type_parity_test.dart drives 60 cases over
twelve empty collection receivers, computing the expected type by running the
same operation on a native collection rather than from a written-down table.
1.142.0 #
Documentation — the must-not-widen exception, and why the bundle cannot lift it (sce72) #
coerceElements accepts an int where a double is wanted, which reads as the
widening the audit's rule forbids. It is a measured exception, and the rule now
says so beside itself rather than only in a comment here.
Dart separates Float32List.fromList([1, 2]) (compiles — an int literal in a
double context IS a double) from a List<int> variable (does not) by the STATIC
TYPE of the argument expression. d4rt erases element types, so both arrive
indistinguishable and no rule written at that point can separate them. Accepting
admits the common valid script; rejecting breaks it.
SCE72 then measured the only thing that could lift the limit — could the mirror carry the static type? No, and not for want of effort:
| parse mode | [1, 2] |
ints |
|---|---|---|
parseString — what both interpreters AND the bundler use |
null |
null |
AnalysisContextCollection — resolved |
List<double> |
List<int> |
The analyzer knows it only under RESOLUTION, which nothing in this repo
performs. So the mirror is not failing to carry something it was given; the
information is never computed. At interpret time it cannot be: execute(source:)
takes a string with no file, and the Flutter line runs a bundle on a device with
no analyzer.
The bundler could resolve — it has a path — and it still would not help.
coerceElements is one shared, mirrored helper serving both lines and cannot
know whether its caller came from a resolved bundle, so a bundle-only type would
make the Flutter line stricter than the source line for the same script. That
divergence is what the mirror rule exists to prevent.
Comment and documentation only; no behaviour changes.
1.141.0 #
Changed — a re-wrapped exception keeps what the first wrap preserved (sce70) #
A clause shaped
} on RuntimeD4rtException catch (e) {
throw RuntimeD4rtException("...: ${e.message}");
}
re-wraps a wrapper that already carries originalException and
originalStackTrace, and builds the new one from the MESSAGE ALONE. Everything
SCC11 preserved one frame below is discarded at the re-wrap — which is why two
of SCD34's three trace widenings were still invisible to a script after being
applied.
All such sites now forward both fields: 22 in tom_d4rt, 23 in tom_d4rt_ast.
THIS IS A SEMANTIC CORRECTION, NOT A NEW RULE. visitTryStatement already
prefers originalException over the wrapper when one survives (OPEN B.5), so a
script's catch (e) receives the real native exception and on <NativeType>
dispatch matches. The defect was that whether a script saw the real exception
depended on HOW MANY WRAP LAYERS it had crossed. Where a payload survives, a
script now catches the native exception rather than the RuntimeD4rtException
wrapper, and a trace points at the native throw rather than at the interpreter.
Where no payload was preserved, nothing changes.
The message prefixes are kept rather than replaced by rethrow. They are the
only thing the re-wrap contributes, and the payload outcome is identical either
way: an inner wrapper carrying a native exception reaches the script as that
exception whether it is rethrown or re-wrapped with the payload forwarded. The
prefix is the difference, and it is worth keeping.
Two sites were decided individually rather than by pattern. The type-check
clause re-wraps inside an InternalInterpreterD4rtException and was invisible
to a scan keyed on the outer throw. The extension on-type clause holds two
throws, and only one is a re-wrap: its sibling fires when the caught error is an
UndefinedNameD4rtException, a name that resolved to nothing, which scc31 keeps
deliberately uncatchable and which carries no payload to forward.
1.140.0 #
Changed — a missing member is catchable as NoSuchMethodError (sce67) #
Asking a receiver for something it does not have reported two ways, decided by the member KIND rather than by anything a script can see:
InternetAddressType.IPv4.host -> UndefinedMemberD4rtException
InternetAddressType.IPv4.lookup() -> D4rtNoSuchMethodError
Only the second implements NoSuchMethodError, so a script writing
try { ... } on NoSuchMethodError catch (_) handled the method half of the
same failure and missed the getter half. F-SCC8-5's reasoning — "a missing
member is the same failure real Dart reports at runtime, and asserting the SDK
supertype means a script can catch it the way it would catch the real one" —
does not stop applying at getters.
UndefinedMemberD4rtException now implements NoSuchMethodError as well.
The supertype is ADDED, not swapped. It is still a
RuntimeD4rtException, so every existing on RuntimeD4rtException clause
keeps working, and every internal is UndefinedMemberD4rtException test — the
typed signal several call sites read to decide whether to attempt extension
lookup — is unaffected. Those sites select on the exact type, never on the SDK
supertype.
STATIC member absence and undefined NAMES are deliberately left out. Real
Dart rejects Klass.missing and a bare undefined name at COMPILE time, so
there is no runtime NoSuchMethodError for a script to catch; giving them the
supertype would make d4rt strictly more catchable than the platform — the
mistake D4rtRangeError records for IndexError, and the reason scc31 keeps
the undefined-name case uncatchable. A control case pins that they stay out.
Measured across the seven ways the interpreter reports an absence: five were
uncatchable as NoSuchMethodError before this, and the two instance-getter
rows are the ones real Dart says should not have been.
1.139.0 #
Documentation — asUint8ListView records why it exists (sce65) #
The eleven typed-list asUint8ListView adapters sat under a bare
// Typed methods heading, which says what they are and nothing about why
they exist beyond the SDK. No typed list declares the member: dart analyze
on a native one answers "The method 'asUint8ListView' isn't defined".
That makes it the WIDENING SHAPE — a script using it is green in the
interpreter and does not compile as real Dart, so the error surfaces only when
the script leaves here. The acceptance had been recorded, but in
doc/stdlib_sdk_gap_audit.md rather than where a reader meets the member.
Each definition now carries the reason, and the KEEP decision rather than
inheriting it. The removal precedent — scd24's four InternetAddressType
members — does not apply: those were wired to unrelated Object members and
returned wrong answers, while this returns what its name says. Nothing in the
workspace calls it, so removing it would be a breaking interpreter change
bought for no reader, and all eleven variants are pinned present by
F-SCC60-3-*.
Comment only; no behaviour changes. Identical in both mirrored trees.
1.138.0 #
Changed — doc comments that name a library are marked as prose (sce56) #
_bin/check_doc_references.py resolves package: URIs inside /// comments
against the workspace. Three doc blocks here name libraries that no file backs,
all of them deliberately: library_mapping.dart documents CANONICAL URIs, the
identity a bridge registers under rather than a path, and
d4rt_user_bridge_annotation.dart shows annotation targets in packages this one
does not depend on.
Marked with doc-ref: ok and the reason, so the check stays quiet without
losing the distinction between "illustrative" and "wrong".
1.137.0 #
Name resolution: yes — a name narrowed by import scope is retrieved by kind, not as a class only (sce25).
Fixed — an ambiguous name narrowed by import scope was only retrieved as a class #
Completes the generalisation the previous release began. _resolveAmbiguityInImportScope
narrowed a contested name to the single package the script had imported and then
read _bridgedClasses[name] alone, so a narrowed ENUM or top-level value fell
through the branch and reached the AmbiguousBridgedNameException throw it had
just been cleared of. Retrieval now consults the alias environment by kind —
bridged class, then bridged enum, then value — so narrowing resolves whatever
kind the name actually denotes.
Fixed — getConfiguration's example omitted the library argument #
registerGlobalVariable takes the library URI as a required third argument; the
doc comment showed a two-argument call that does not compile.
1.136.0 #
Name resolution: yes — same-name bridged enums and top-level values are ambiguous rather than last-wins (sce25).
Fixed — same-name bridged enums and top-level values are ambiguous, not last-wins #
The ambiguity machinery — source URIs per name, qualifier.Name aliases,
AmbiguousBridgedNameException, platform precedence, import-scope narrowing —
existed for bridged CLASSES only. Two packages that each bridged an enum Mode
or a top-level parse() left the bare name silently bound to whichever
registered LAST, and the displaced declaration had no qualifier to be reached
by: it was simply gone. Measured before the fix — bare Mode returned the
second enum, and pkg_b.Mode threw UndefinedNameD4rtException.
Generalised rather than copied per kind, which is what the class path already
made possible: the ambiguity map was always name → (qualifier → source URI)
and carries no kind. _collectAmbiguityCandidate, _bindQualifierAlias,
_markAmbiguousBridgeName and _peersAfterPlatformPrecedence now take
Object?, the alias environment binds whichever kind the name designates, and
define / defineBridgedEnum take an optional sourceUri and record it.
The lookup needed two checks rather than one. The class branch had its own, because it must run before the class is returned; a value is found in the FIRST branch of the walk, so a check further down never runs for it.
The rule is gated on the source URI, deliberately, exactly as it is for classes: two candidates that yield no qualifier — an embedder registering directly, a script rebinding its own variable — keep the legacy overwrite. An error whose remedy does not exist is worse than the arbitrary pick it replaces.
The import-scope narrowing needed one more fix than expected. It reduced an
ambiguous enum to a single candidate correctly, then failed to RETRIEVE it: the
retrieval read _bridgedClasses[name] only, so the null fell through and the
name stayed ambiguous however the script imported. It now reads the alias
environment by kind. The narrowing was never the missing piece; the lookup was.
module_loader and the AST runner's warm parent now pass the declaring URI
through, which is what makes the rule reachable rather than theoretical.
1.135.0 #
Fixed — a braceless then branch with an else no longer ends an async function #
var x = 0; var c = true;
if (c) x = 1; else x = 2;
return x + 10; // answered 1, not 11
_findNextSequentialNode returned null for the end of a single-statement
then branch whenever an else was present, on the reasoning that "there is
no next sequential node after the then if there is an else". There is: the
else is the branch NOT taken, and control resumes after the if — which is
exactly what the neighbouring else case had always returned. Null stopped the
state machine, so the function completed with whatever value it had last
evaluated.
The two branches of that decision are now one; the distinction was the bug.
Braced branches were never affected, because a { … } then-branch ends at
the block-end case instead. Almost all Dart is braced, which is why this
survived.
In a loop body a wrong value became no value: the loop never advanced and
the function answered null. That is the DONE WHEN's second clause and the
case worth knowing about — it is also the one that HANGS rather than fails if
it ever regresses, since the loop condition is never re-evaluated.
1.134.0 #
Fixed — a do loop in an async body ran its condition before its body #
do { n++; } while (false); answered 0. A do body always runs once, so
Dart — and this interpreter's own synchronous visitor — answer 1.
The state machine re-enters a DoStatement node for two different reasons:
arriving at the loop from the statement before it, and coming back from the end
of its body. It assumed the second every time. The comment that stood there
said so, and named the cost: "if we entered the loop in a non-standard way,
this could fail". Arriving from the statement before the loop is the standard
way.
A loop whose condition is true on entry was unaffected — do { n++; } while (n < 3) counts to 3 either way — which is why nothing caught it.
AsyncExecutionState.doBodiesStarted now tells the two arrivals apart: absent
means the body has not run and must, present means the body finished and the
condition decides. The mark is forgotten when the loop is left, by a false
condition or by break / a labelled break (_leaveLoopsFor), because a do
nested in another loop runs again on the outer loop's next iteration and would
otherwise check its condition first — the same defect one level in, where it is
much harder to see.
continue deliberately keeps the mark: it returns to the loop from inside, and
Dart evaluates the condition for it (F-SCD4-13 pins the same rule from the jump
side).
An empty body (do {} while (c);) has no first statement to jump to; it falls
back to the loop node so the condition decides, which would otherwise stop the
machine and never return.
1.133.0 #
Fixed — a return that unwinds through a finally in an async body #
try { return 'r'; } finally { l.add(1); } return 'end'; answered 'end'. The
return was recorded, the finally ran, and then the machine carried on with the
statement after the try, whose value won.
It looked like the mechanism was present, because it worked whenever the try was the LAST thing in the function: there is no next node, the machine stops, and the exit at the bottom of the loop completes with the stored value. With anything after the try it did not.
Worse than a lost value: an ordinary statement between an inner try and an
outer finally RAN. The nested case answered end:A,MID,B where Dart — and this
interpreter's own SYNCHRONOUS visitor — answer x. A function that is
unwinding must execute nothing but the finally blocks between the return and
the function boundary.
_findNextSequentialNode's "End of a Finally block" case now consults
returnAfterFinally: with a return pending it walks to the next enclosing
try with a non-empty finally and runs that, or stops so the loop's exit
completes with the value. The walk skips a try reached from its OWN finally
(already running) and one whose finally is empty (nothing to run).
STILL BROKEN, and deliberately not fixed here: break and continue out of a
try in an async body skip its finally. That needs a pending-jump mechanism
rather than this one — see scf6. The tests record today's wrong answers for
both, beside the synchronous visitor's correct ones, so the next change to this
machinery cannot move them silently.
1.132.0 #
Fixed — an await inside an expression-bodied async function no longer swallows its expression #
Future<int> a() async => 2; main() async => "x${await a()}y"; returned 2,
not "x2y". A silent wrong answer, in one of the commonest shapes of async
Dart.
It was not a string-interpolation bug, which is how it presented. Measured:
=> (await a()) + 10 returned 2 rather than 12, and
=> "x" + (await a()).toString() returned 2 rather than "x2".
Interpolation was one instance of "any composite expression".
The discriminator is the BODY. A block body never had this — return "x${await a()}y"; is a ReturnStatement, and SCC40 already re-runs a suspended
statement with per-site replay from resolvedAwaitResults. An expression body
has no statement, so nothing re-ran: _determineNextNodeAfterAwait recognised
no case, returned null, the machine stopped, and the function completed with
lastAwaitResult — the awaited value standing in for the whole expression.
The repair re-uses SCC40 rather than adding a case per expression kind: an expression body is the only other unit the machine executes, so it is handed back and evaluated again. Resolved await sites replay, the first one not yet reached suspends for real, and the pass where nothing suspends produces the value.
=> await a() used to arrive at the same dead end and be right by accident —
the machine stopped, and the awaited value was the whole expression. It now
takes the same route on purpose.
KNOWN, NOT FIXED HERE: an await inside a collection literal
([await a(), 9]) still puts the interpreter's own AsyncSuspensionRequest
into the collection. That one fails in block bodies too, so it is a different
defect — see scf5.
1.131.0 #
Changed — await for is lazy: one element at a time, and the stream is cancelled when the loop is left #
await for suspended ONCE on stream.toList() and then walked the resulting
list. Three consequences, one of them total:
- a loop over an infinite stream never ran its body at all —
toList()never completes, and abreakcannot help because the break is in the body; - every element was produced before the body ran once, so a producer's side effects all landed up front;
breakmeant nothing to the producer: nothing was ever cancelled.
The loop is now driven by a StreamIterator — one suspension per element on
moveNext(), current bound on resumption — and every loop-exit path cancels
the iterator, because truncateLoopStacks (SCD4's centralised exit) is the one
place that cannot be forgotten.
The cancel is deliberately not awaited: every caller is a synchronous exit path
in the state machine. It happens promptly; what is not guaranteed is its
ORDERING against code after the loop, which is why a test observing a
generator's finally has to await a turn first.
Half of SCE16, not all of it. An async* generator still ignores its
listener, so cancelling a subscription does not yet stop the body — see scf4.
F-SCD4-10 stays skipped, but its failure has changed shape: it returned
resumed,v1,v2 and now returns v1,v2,resumed, so the interleaving is right
and only the cancellation is missing.
1.130.0 #
Changed - one environment instead of two in the bridged-enum toString visitors (scd208) #
BridgedEnumValue.get and BridgedEnumValue.toString build a throwaway
InterpreterVisitor to invoke a bridged toString adapter. Each site
constructed TWO separate Environment() objects — one as the visitor's
globalEnvironment, one inside the ModuleLoader it was given — where the
analyzer-free twin has always bound one local and passed it to both.
Inert in practice: both were fresh and empty, and the adapter is called and discarded in the same expression. It is not a shape anyone would choose, and it made two spellings of the same construction read as two different constructions, which is what a mirror baseline is meant to make visible.
Both sites now bind final env = Environment(). What remains between the
trees is the type difference that cannot go away — ModuleLoader against
NoOpModuleContext, the twin having no module loader at all — now four code
lines rather than ten.
1.129.0 #
Changed - two mirrored files stop diverging for reasons that did not survive reading (scd208) #
Both were baselined as SUSPECTED ONE-SIDED EDITS by the mirror guard, meaning the divergence had been noticed and not investigated. Read with a normalised diff, neither was an edit that reached one tree.
environment.dart declared removeLocalValue in both trees, five identical
lines, in a different POSITION — and in this tree the position was wrong: the
method sat BETWEEN define's doc comment and define, so define documented
removeLocalValue and removeLocalValue's doc read as a continuation of
define's. It now sits after getSlot, where the twin has always had it, and
define has its documentation back.
BridgedInstance.get spelled one enum-property lookup — .name and .index
on a wrapped native Enum — as an if-chain here and a switch in the twin.
Behaviour-identical, confirmed by reading both; this tree now carries the
switch, since the analyzer-free tree is where new interpreter work lands.
No behaviour changes. Both pairs now agree whole-file, and the three separate records that described their divergence — the mirror baseline, SCD183's region list and SCD199's body census — were each deleted by the guard that noticed them go stale.
1.128.0 #
Fixed - 49 stdlib adapters no longer discard a surplus argument in silence (scd204) #
socket.add(data, extra), list.skip(2, 3) and 47 others read the arguments
they wanted and returned, dropping the rest without a word. SCC85 closed 443
adapters this way and left a residue of 23 in three files it could not label:
two of them are helpers shared by several bridged classes — inheritedListMethods
by thirteen typed-data lists, setAlgebraMethods by five sets — and the
diagnostic takes a Class.member string that a shared helper has no way to
name where its adapters are written.
inheritedListMethods now takes the class name as a required parameter, threaded
from the thirteen call sites that each already declare it two lines above. It is
required rather than optional so a new typed-data variant cannot omit it and
report the wrong class.
The residue was larger than recorded, and the reason is a definition. SCC85's
sweep skipped an adapter whose body held a length test that could REJECT. That is
the right question for a too-FEW guard and the wrong one for this: if (positionalArgs.length < 2) throw … cannot fire on a surplus, and neither can
positionalArgs.length > 1 ? positionalArgs[1] : null, which yields a value
rather than rejecting. Measured with that corrected, the three files held 49
unguarded adapters rather than 23.
Every guard is atMost, never exactly, so the generic describeArityError
diagnostic keeps the too-few half — the property F-SCC85-4 pins.
tom_d4rt/test/stdlib/scd204_surplus_arity_guard_test.dart keeps the three files
closed in both trees and ratchets the rest.
1.127.0 #
Fixed - a class name used as a value now compares, hashes and tests like a Type (scd198) #
x.runtimeType == Foo had already been reconciled, so == answered correctly.
hashCode had not. Equal objects with different hash codes is an Object
contract violation, and it explains the symptom set exactly: the comparison
looked fine and every hash-based collection missed —
<Type, T>{String: v}[x.runtimeType] was null, Set<Type>.contains false,
List<Type>.indexOf -1.
This is SCC32's shape on the class-name value, and it takes SCC32's fix in both halves, because either alone leaves the two map spellings disagreeing:
BridgedClassdelegates==andhashCodeto itsnativeType.- A class name used as a hash key is normalised to that native at storage, in
_unwrapHashKey— Dart's hash lookup askslookupKey == storedKeywith the lookup key as receiver, and a nativeTypelooking up a storedBridgedClassis rejected byType.==, which no code here can override.
Two bridges for one native type now compare equal. That is deliberate and is what a re-export is; identity is untouched, which is what the shadow machinery relies on.
SomeClass is Type was false for every class in the language while
x.runtimeType is Type was true — the is predicate had no arm for a class
name. It has one now, for bridged and interpreted classes alike.
1.126.0 #
Fixed - MapEntry.hashCode was not readable as a value (scd196) #
hashCode was registered in MapEntry's methods map with no getter beside
it, so e.hashCode — the spelling every Dart program uses — returned the bound
callable and only the uncompilable e.hashCode() produced an int. Nothing
masked it: map.entries.first.hashCode silently was not a hash. That is SCC73's
Runes.iterator failure, unmasked. It is a getter now.
IOSink.toString was registered as a getter; toString is a method, so
f.toString yielded a String where Dart yields a tear-off. Deleted.
Fixed - five members declared in two maps at once (scd196) #
hashCode appeared in both methods and getters on Match, Pattern and
Sink; toString in both on IsolateSpawnException and RemoteError. The
interpreter reads getters first so the duplicates were inert on the paths a
script takes, but BridgedInstance.get is methods-first and would hand back the
bound callable. The wrong entry is deleted in each case — hashCode is a
getter, toString is a method.
scd196_member_map_disjointness_test.dart (both trees) now fails if any bridge
declares one member in two maps. A getter/setter pair is ordinary Dart and is
not reported.
SCD189's SDK-kind guard also gained the implicit Object supertype in its walk.
A class declaring no supertype stopped the walk dead, so every inherited member
read as unspeakable — which is a pass. That gap is what hid both defects above.
1.125.0 #
Name resolution: yes — the enum registry records what it displaces, so a displaced enum is reachable (scd194).
Added - the enum registry records what it displaces (scd194) #
defineBridgedEnum warned about a name collision and then overwrote. The
displaced enum was gone, and the only trace was a log line that is off in a
normal run — the same condition that let SCB26's missing StringSink members
live for that bridge's whole lifetime.
The class registry has recorded every displaced bridge unconditionally since
SCC76, which is what makes findAllBridgedClassesByName and the collision
guard possible. The enum namespace now has the same bookkeeping:
_recordShadowedEnum at both displacement sites (defineBridgedEnum and
importEnvironment's import-wins branch) and findAllBridgedEnumsByName
mirroring the class version across the scope chain.
A colliding enum is RECORDED AND REPORTED, not rejected. The class rule — same
nativeType is a re-export, a different one is Dart's ambiguous-import case —
transfers in principle, but the enum path has no qualifier machinery, so making
a name ambiguous would leave a script no way to say which one it meant.
scc76_bridge_name_collision_test.dart gains the enum axis and the
cross-namespace case (a name must not be both a bridged class and a bridged
enum), in both trees.
1.124.0 #
Fixed - six bridged members registered under the wrong kind (scd189) #
The member audit asks whether a member RESOLVES; the coverage baseline asks
whether it is REACHABLE. Both are satisfied by an adapter that resolves and then
does the wrong thing — SCC73 found Runes.iterator registered as a METHOD, so
it handed back the bound callable instead of the iterator and nothing noticed.
scd189_member_kind_parity_test.dart asks the cheapest useful form of the next
question: is each member registered as the same KIND the SDK declares? It
compares 1 487 members against the SDK source and found six.
BLOCKING: StreamSubscription.onData, onDone and onError were registered as
SETTERS. Dart declares them as methods, so sub.onData(h) — the correct
spelling — failed with "has no instance method named 'onData'" and only the
uncompilable sub.onData = h worked. They are methods now; the one test that
exercised this was itself written in the invalid form and is corrected.
FABRICATIONS: Encoding.inverted, Function.hashCode and
UnmodifiableListView.reversed were registered as methods beside a correct
getter, so utf8.inverted(), f.hashCode() and view.reversed() were
accepted — green in the interpreter, rejected by the Dart analyser. Deleted.
Function.hashCode's method was additionally dead: the universal Object getter
shadowed it.
1.123.0 #
Fixed - HttpClientResponse.transform works, by deleting the stub that broke it (scd187) #
Reading an HTTP body the way every Dart tutorial shows —
await response.transform(utf8.decoder).join() — reported transform not yet implemented in interpreted environment, leaving the hand-folded read
(toList(), concatenate, utf8.decode) as the only way to get a body out.
There was nothing to implement. The HttpClientResponse bridge carried a local
transform adapter whose entire body was that throw, under the comment
"Implementation for transform would be complex, placeholder". But an
HttpClientResponse IS a Stream<List<int>>, and the Stream bridge's
transform already coerces the transformer and delegates. Deleting the local
adapter is the whole fix — verified by deleting it and re-running the
reproduction before the change was written.
This is SCC51's shape one library over: a leaf bridge redeclaring a member its
supertype supplies, and supplying a worse version. SCC51 deleted seventeen of
these in dart:collection for the same reason — the inherited one was already
right. HttpServer.transform worked throughout, because nothing shadowed it
there.
The message was also wider than the defect. "not yet implemented in interpreted
environment" reads as a statement about the interpreter; it was one adapter on
one class, one site per tree. A new repo-wide guard
(scd187_no_unimplemented_stubs_test.dart) asserts that neither stdlib tree
ships a message of that shape — the set is now empty, which is when a ratchet
costs nothing and is worth having.
1.122.0 #
Added - Future.syncValue, and the SDK floor that was hiding it (scd186) #
scc73_sdk_member_completeness_test.dart skips any SDK member annotated
@Since a version above the package's own floor, so that an SDK upgrade cannot
turn it red demanding members the package may not legally compile against.
tom_d4rt declared ^3.9.0 while Future.syncValue is @Since("3.10"), so it
sat knowingly unbridged.
The floor is now ^3.10.4 — the version tom_d4rt_ast and every other package
in the d4rt repo already declared. The two mirrored packages had been
straddling, which SCC26 asks them not to do, and no consumer could use the lower
floor anyway. Raising it made the completeness guard name the member on its own,
with no list to consult; measured across all eight bridged dart: libraries it
was the only one hidden, even against an installed 3.12.2 SDK.
Future.syncValue is not Future.value under another name. Both complete with
the argument, but value ADOPTS a future argument — waiting for it and taking
its result, error included — while syncValue completes with the argument
as-is. That is what the SDK's "guaranteed to not have an error" rests on, and
the tests pin both sides of the difference against real Dart.
Registered as both a constructor and a static, like every other named Future
factory: Future<T>.syncValue(v) routes through constructor lookup and
Future.syncValue(v) through the static path.
1.121.0 #
Fixed - startChunkedConversion accepts a sink a script can build (scd181) #
Every startChunkedConversion adapter in dart:convert guarded on
positionalArgs[0] is! Sink<T> and then cast to Sink<T>. The interpreter
erases type arguments, so ChunkedConversionSink.withCallback(cb) evaluates to
a ChunkedConversionSink<Object?> — and Sink<Object?> is not a
Sink<String>. Fourteen guards across nine files therefore rejected every sink
a script could construct, which made the whole chunked-conversion surface
unreachable. ByteConversionSink.from carried the same guard.
This is the contravariant twin of the Converter.bind defect SCC68 fixed, and
it needs a different remedy. A Stream<T> is a producer, so D4.coerceStream
has elements in hand and maps them; a Sink<T> is a consumer, and nothing has
been produced yet. The new D4.adaptSink<T> returns a forwarding Sink<T>
that delegates add and close to the erased sink underneath, and passes an
already-correctly-typed sink through untouched so a receiver testing for a
concrete subtype still sees it.
The guards stay, narrowed to is! Sink. ArgumentD4rtException (what the
D4.* helpers throw) and RuntimeD4rtException (what stdlib adapters throw)
are siblings under D4rtException, not parent and child, so routing the whole
check through the helper would silently change what a script's catch
dispatches on.
1.120.0 #
Fixed - x is Enum answers what Dart answers (scd176) #
Enum was bridged but declared no isAssignable, and _valueHasType's
bridged branch only reaches its native-predicate fallback when the bridge has
one. So is Enum was FALSE for every bridged value, including genuine SDK
enums — false by omission rather than by any decision.
A PREDICATE, NOT SUPERTYPE EDGES, and the distinction is load-bearing. The
obvious repair is to declare an -> Enum edge on every "enum-shaped" bridge.
Measured against Dart, four of the five usual candidates are not enums at all:
StdioType and InternetAddressType are final class, ProcessSignal is an
interface class, and FileMode is a class — all with static const instances
that look like enum values from a script. Only
HttpClientResponseCompressionState is a real enum. Hand-declared edges
would have turned four correct answers into wrong ones; asking the native
value classifies each correctly with no list to maintain.
is Comparable is deliberately untouched. Dart answers FALSE for all five,
including the genuine enum — Enum does not extend Comparable — and the
bridge declares no compareTo, so the interpreter's existing false is
already right.
The hierarchy audit's one _declinedEdges entry is removed: it held this
question open, and the audit now reports the edge as satisfied via
isAssignable rather than missing by decision.
1.119.0 #
Added - the TLS pair: X509Certificate and SecurityContext (scd171) #
Both were consumed by adapters and registered nowhere. SecurityContext is
what HttpServer.bindSecure and HttpClient(context:) cast their argument to,
so a script had no way to build the value they demand and both entry points
were unreachable. X509Certificate is what the bridged HttpRequest.certificate
and HttpClientResponse.certificate getters return — with no bridge claiming
it, every member call on the result died with Undefined property or method ... on _X509CertificateImpl.
That one failed SILENTLY, and outlived the sweep built to catch it: SCC24 skips
null getters, and certificate is null on a plain-HTTP request. Both trees'
sweeps now capture a real certificate from a loopback TLS handshake, so the
blind spot is closed for this type.
SecurityContext's four file-reading members — usePrivateKey,
useCertificateChain, setTrustedCertificates, setClientAuthorities — go
through the same FilesystemPermission gate as File and Directory. Handing
the path straight to the SDK would let a script scoped to one directory read a
private key anywhere on the host through a TLS API. The *Bytes variants read
nothing and are ungated.
alpnSupported is deliberately not bridged: the SDK deprecates it.
1.118.0 #
Fixed - NetworkPermission now gates every socket-acquiring bridge (scd170) #
NetworkPermission was declared, documented and threaded through the
permission system, and in the whole of lib it gated exactly ONE call site:
InternetAddress.lookup. Everything that opens a socket ran unchecked —
Socket.connect / startConnect, ServerSocket.bind, RawSocket.connect /
startConnect, RawServerSocket.bind, RawDatagramSocket.bind,
HttpServer.bind / bindSecure / listenOn, WebSocket.connect, and all
fourteen HttpClient request methods.
What stood in for a gate was the IMPORT gate on dart:io, which is keyed on
FilesystemPermission. A script granted filesystem access therefore received
unrestricted inbound and outbound network access. The quest's standing
constraint is that the interpreter "must remain fully sandboxed".
It is one sweep rather than a gate per class because a partial gate reads as a
working sandbox: HttpServer.bind alone is bypassable with
ServerSocket.bind + HttpServer.listenOn, HttpClient.getUrl alone with
HttpClient.open, and all of HTTP with Socket.connect.
The host and port are passed through, so NetworkPermission.connectTo now
means something. The one gate that existed took a host argument and dropped
it, asking only {'type': 'network', 'connect': true}.
Each operation asks for exactly one flag — connects ask connect, binds ask
bind, serving an already-bound socket asks listen — because
NetworkPermission.allows requires every requested flag to be granted, so a
combination would make the single-capability grants unusable.
The dart:io import gate's key is deliberately unchanged; decoupling it is a
breaking change to how every existing permission set is read.
1.117.0 #
Fixed - a throw inside an async catch block no longer spins the machine (scd169) #
_handleAsyncError found the enclosing try with _findEnclosingTryStatement,
which for an error raised in a catch block returns the try that catch belongs
to. selectCatchClause then matched a clause of that same try, the clause ran
and threw again, and the error was re-offered to the same try. The script did
not fail and did not complete: it looped. Measured before the fix, the catch
block of a four-line script ran 135,239 times in six seconds.
The symptom reported was "the returned future is never completed", which is
true but misleading — the isolate is spinning, so no Timer runs and neither
dart test's per-test timeout nor a host-side Future.timeout fires. The
process has to be killed with a signal.
Dart's rule is that an exception raised in a catch block of T is not
catchable by T: the handler that would claim it is the one already running.
_isInsideCatchClauseOf answers that from the AST, and it is applied in two
places because they cover different cases — the outward search now treats a try
whose clauses are all ineligible as no handler at all, and clause selection
refuses to match on a try that stays in the search because it has a finally.
That finally still runs before the error carries on outward, as the
synchronous path and the language both require; skipping the try outright would
stop the spin and silently drop it.
It was not limited to interpreter-level errors. An ordinary
throw ArgumentError(...) from a catch block did the same, so this was plain
correct Dart hanging, not an edge case of error reporting.
1.116.0 #
Changed - Uint8List shares the inherited getters with its ten siblings (scd166) #
SCD28 folded this variant's methods and setters onto inheritedListMethods /
inheritedListSetters and left its getters map hand-rolled. That kept
Uint8List the one typed-data variant of eleven that would not receive the
next getter added to inheritedListGetters, which is the exposure that
produced SCB3 (sort/shuffle/asUnmodifiableView resolving here and
nowhere else) and SCC9 (a _TypeError where the family raised a catchable
UnsupportedError).
The three getters it declared — single, iterator, reversed — were
equivalent to the helper's, so the effective member set is unchanged and no
behaviour moves: all eleven variants exposed the same fourteen getters before
this change and after it. What changes is that they now do so from one source.
The comment claiming Uint8List "hand-rolls its instance maps rather than
sharing inheritedListMethods" is removed; it had been false since SCD28, three
lines below the spread it denied.
1.115.0 #
Changed - the ServerSocket bridge records why it shadows 28 Stream members (scd162) #
SCD38 registered ServerSocket -> Stream, which made every Stream adapter
reachable through the walk. The 21 methods and 7 getters this bridge spells out
by hand have shadowed an inherited copy ever since, and nothing said whether
that was a decision or an oversight.
It is now written at the bridge: DELETE THEM, once the shadow differential can
see them. F-SCC51-8 is the only thing that can say 28 copies are redundant
rather than subtly different, and its fixture table covers the collection
bridges alone. SCD152 is why that gate matters — driving the previously-skipped
half of that differential found HashMap.map rebuilding its result wrongly, a
leaf copy that had looked like pure redundancy for as long as nobody invoked it.
Also recorded, because it is wrong today and waits on nothing: the arity
diagnostics in ServerSocketIo say Socket.map, Socket.where, Socket.fold
— copied from the Socket bridge above, naming the wrong class.
Comment only; no adapter changed.
1.114.0 #
Fixed - HashMap.map / LinkedHashMap.map ignored the entry the callback returns (scd152) #
Both bridges carried a local map adapter that rebuilt the result as
MapEntry(key, callbackResult) — keeping the ORIGINAL key and storing whatever
the callback returned as the value. Map.map's contract is that the callback
returns a MapEntry supplying BOTH halves. So
HashMap.from({'a': 1}).map((k, v) => MapEntry(v, k))
produced {'a': MapEntry(1, 'a')} where Dart gives {1: 'a'}. The shared
Map.map adapter has always been right, including unwrapping a
BridgedInstance<MapEntry> an interpreted MapEntry(...) produces, so the fix
is to delete the two local copies and let it answer. SplayTreeMap never had a
copy and was already correct — byte-for-byte the SCB17 addEntries asymmetry,
where two of three map siblings carried the same divergent duplicate.
Changed - the []= adapters on three map bridges no longer diverge #
HashMap, LinkedHashMap and SplayTreeMap each declared a local []=
returning the assigned value, while Map.[]= returns null. Not observable from
a script — the interpreter discards the adapter's result and yields the assigned
value itself — but a shadowed adapter whose body differs from the one it hides is
what SCC51 exists to remove, so the local copies are gone.
Changed - the SCC51 shadow differential drives every pair (scd152) #
F-SCC51-8 compared 281 of 542 shadowed pairs and SKIPPED 261, because its
harness invokes adapters directly and a callback argument needs an
interpreter-side Callable it did not build. The skipped half is where SCC51
predicted divergence would hide, and both defects above were in it.
Seven native callables and a per-member argument recipe remove the skip
category entirely: the walk now compares 537 pairs with no skips. Two
new counters make the ways it could stop measuring visible instead of silent —
undrivable for a shadowed member with no recipe, and vacuous for a pair
where both adapters rejected the arguments, which is agreement about nothing
rather than a passing comparison. Both are asserted zero; the old skip set
reached 261 precisely because nothing objected to it growing.
The skip set was never only about callables, incidentally: of its 38 names, some
twenty — clear, removeLast, insert, setRange, asMap, []= and the rest
— take no callback at all and were simply missing an argument recipe.
1.113.0 #
Changed - _isInterpreterOwned now covers InterpretedRecord (scd147) #
The predicate SCC49 added to keep its structural pass from guessing a bridge for
the interpreter's own representation tested Enum, RuntimeType, RuntimeValue
and Callable. InterpretedRecord implements none of them, so the predicate
could not see it - harmless only because no bridge is named Record, which Dart
3 records make an entirely plausible addition. The predicate's contract is "is
this the interpreter's own representation"; a record is, so leaving it out made
the answer depend on the registry.
The boundary is held by SCD132, not by the predicate - measured #
Measured over the live registry (84 bridges): eight interpreter-owned type
names match a bridge name today, not the one the 43-failure incident found.
BridgedEnum and InterpretedEnum end with Enum; InterpretedFunction with
Function; the four *RuntimeType types with Type; and TypeParameter matches
Type as a >=3-character PREFIX.
All eight are RuntimeType or Callable, so the predicate covers them - but only
at step 4 of toBridgedInstance, the single site that consults it. toBridgedClass
and its PASS B prefix fallback do not ask, and no interpreter-owned type is claimed
there today for a different reason: ablate SCD132's corroboration requirement and
TypeParameter is immediately claimed by the Type bridge.
That protection is incidental - SCD132 was written about bridge-to-bridge false
positives (TextDirection claimed by Text) and knows nothing about this
distinction - and nothing recorded it, so widening PASS B again, or a bridge
declaring one of these in its nativeNames, would reopen the hazard silently.
tom_d4rt/test/scd147_interpreter_owned_boundary_test.dart now pins it, asserting
the property at toBridgedClass precisely because no predicate guards it there.
No behaviour change for any value that resolves today: the predicate addition is
inert while no Record bridge exists, and the guard is a test.
1.112.0 #
Changed - a native object no bridge claims now says so (scd145) #
A member call on a native object the interpreter never bridged reported
Undefined property or method 'moveNext' on _TallyIterator
The real cause - Cannot bridge native object: No registered bridged class found for native type ... - is thrown at the end of Environment.toBridgedClass and
then absorbed: InterpreterVisitorExtension.toBridgedInstance catches it,
revokes it and returns (null, false). That catch is correct and load-bearing,
because its callers use the false as a control-flow signal and fall through to
other registries - an interpreter-internal value legitimately has no bridge. The
cost was that the cause was gone by the time the fallthrough chain gave up, and
the reader was pointed at the member: they went looking for a missing method on a
bridge that does not exist.
The two member-error sites for a raw native receiver now append the cause:
Undefined property or method 'tally' on Zqwx. No bridge claims this type: no
bridged class is registered for the native type Zqwx, so the object was never
bridged and has no members at all - the missing member is a consequence.
Register a bridge for Zqwx, or add 'Zqwx' to an existing bridge's `nativeNames`.
Only the message changes - not the exception type, not memberName, not
receiver, and not the control flow. environment.dart already records why
widening resolution instead broke 43 enum-dispatch tests: callers use the throw
as a signal. A message change on a path that is already failing cannot regress a
passing one.
Two exclusions, and they are the interesting part. Interpreter-internal values
are tested against the abstractions the interpreter owns - RuntimeValue,
RuntimeType, Callable, InterpretedRecord - rather than a list of concrete
types, because a list is what rots when a new value shape appears; a
script-declared class has no bridge and is not supposed to, so the clause would
be noise on every script typo. And a type a bridge DOES claim earns nothing,
because there the member really is the problem.
This matters more after SCC49, not less: that change made implementation types
named after their interface resolve structurally, so what still reaches this path
is the hard residue - types the SDK abbreviates (_StreamSinkWrapper,
_ControllerSubscription) and types with no naming relationship to any bridge.
Those are exactly the cases where the reader most needs the diagnostic to name
the cause.
1.111.0 #
Added - a bridged function typedef can carry its positional arity (scd137) #
scd136 made a bridged function typedef accept any callable, arity-blind,
because BridgedClass(nativeType: Function, name: typedef.name) was all that
survived generation: the signature was discarded, so VoidCallback and
ValueChanged were indistinguishable at runtime. A zero-argument closure
passed where a one-argument callback is required bound happily, then threw at
the call with a message naming neither the parameter nor the typedef.
BridgedClass now carries typedefRequiredPositional /
typedefMaxPositional, and FunctionRuntimeType.isSubtypeOf uses them to
refuse a callable that PROVABLY cannot be invoked - moving the failure to the
binding, where the parameter and the typedef can be named.
The rule is deliberately narrow, because the prize is a better error rather than a caught bug: the program is broken either way, so a false rejection - refusing a callback that works, across every Flutter widget - costs far more than the diagnostic gains.
- Arity only; return types are never consulted. An interpreted closure
always resolves to
dynamic Function(...), so checking returns would refuse working callbacks wholesale. - Only the typedef's REQUIRED positional count is checked, never its maximum. That is the one invocation shape a typedef guarantees; rejecting on a possibility is how a working callback gets refused.
- Optional positionals on the callable side count toward what it accepts, so a closure taking one required and one optional argument serves a one-argument typedef.
- No arity, no opinion. A bridge registered without it behaves exactly as scd136 left it.
registerFunctionTypedef gains two optional named parameters, and the
functionTypedefs record gains two nullable fields - both additive.
This is inert until a generator that emits the arity ships. Every generated bridge in existence registers typedefs without it, so nothing changes for any current consumer. The generator half is tracked separately (sce163): generated output has to compile against the PUBLISHED interpreter, and it cannot emit a call to an API that does not exist yet - which the GEN-121 analyse gate demonstrated by going red the moment it tried.
1.110.0 #
Fixed — an interpreted closure is accepted against a bridged function typedef (scd136 / GEN-125) #
Passing a script closure to any Flutter callback parameter was rejected:
type 'dynamic Function()' is not a subtype of type 'VoidCallback?' of 'onPressed'
type 'dynamic Function(dynamic)' is not a subtype of type 'ValueChanged' of 'onChanged'
A function typedef has no bridgeable class, so it is registered as
BridgedClass(nativeType: Function, name: typedef.name) — and
FunctionRuntimeType.isSubtypeOf ended by identifying Function by name.
The bridge's name is VoidCallback, which is the one property of that
registration deliberately not Function, so the nominal test could never match
one. A bridged typedef is a structural type wearing a nominal name, which is
also why the two escape hatches in _checkArgumentType (skip structural
annotations, exempt a declared Function by name) both miss it.
isSubtypeOf now asks what the bridge IS rather than what it is called:
if (other is BridgedClass && other.nativeType == Function) return true;
This subsumes the nominal test — dart:core's own Function bridge carries
nativeType: Function too — and because the argument check, the return check
and is/as all route through the same method, one rule repairs all three.
Deliberately arity-blind, exactly as the nominal test it subsumes was: the
bridge carries nativeType: Function and nothing else, so there is no signature
to check a closure against. Making it carry one is a generator change, tracked
as scd137. The rule therefore does not widen what is accepted for typedefs that
already matched — it stops rejecting the ones that never could.
The rule was always wrong; nothing consulted it for arguments until
_checkArgumentType began routing declared parameter types through
isSubtypeOf, at which point a second reader of an already-wrong answer turned
it into 15 corpus failures and ~276 framework errors across 109 scripts.
1.109.0 #
Added — three files the barrel never exported (scd134) #
A type a consumer receives but cannot name is not a private type; it is a
public type with a missing export. Three files were in that position, and the
AST twin (tom_d4rt_ast/runtime.dart) exported all three equivalents:
src/bridge/bridged_enum.dart—BridgedEnum,BridgedEnumValue.BridgedEnumDefinition.buildBridgedEnum()returns the first andEnvironment.getRuntimeTypereturns it for any native enum value. Reaching them meant importingpackage:tom_d4rt/src/..., which is exactly what theimplementation_importslint forbids.src/sdk_errors.dart—D4rtTypeError,D4rtNoSuchMethodError,indexRangeError. These are the SDK error types the interpreter raises itself so thaton TypeErrormatches inside interpreted code (SCB10); a host that wants to catch one needs the name.src/module_loader.dart—ModuleLoader,LoadedModule.InterpreterVisitor.moduleLoaderis a public field of typeModuleLoader.
Purely additive: seven names appear on the public surface, none change or move.
test/scd134_barrel_surface_parity_test.dart now compares the two barrels'
exported name sets on every run, with each remaining difference recorded
individually and a reason attached. Twenty remain, and the classification is the
useful part rather than the count: most follow from one line having an analyzer
and the other deliberately not, two are one concept under two names, and five
are recorded as genuine gaps rather than differences (see sce154 and sce155).
1.108.0 #
Name resolution: yes — Environment.toBridgedClass no longer claims a bridge on a bare-name prefix (scd132).
Changed — a bare name prefix no longer claims a bridge (scd132) #
Environment.toBridgedClass ends in a prefix fallback: if nothing else matched
anywhere in the scope chain, it claimed any registered bridge whose name was a
=3-character prefix of the native type name, with no other corroboration. Two of the three false positives that method's own header documents are that rule firing —
MappedListIterableclaimed byMap,TextDirectionclaimed byText— and each was repaired by routing ONE caller around the fallback, so the rule survived every fix and the next name-shaped coincidence was going to be claimed just as silently. A bridge namedSetclaimsSettings.
The prefix now only finds a CANDIDATE. The bridge must also declare the
connection: nativeNames naming the type, or a supertype-registry edge between
the two names. Both are things somebody wrote down. When nothing corroborates,
the method throws — which every caller already handles, and which is more honest
than a silently wrong dispatch.
isAssignable is the obvious third corroboration and is deliberately absent: it
takes a VALUE and this method is given only a Type. Callers that hold the
value already consult it.
Measured before narrowing. A probe on every fallback match across both
trees' full suites fired 12 times: 11 for one test's deliberately prefix-named
proxy, and once for TextDirection → Text, the known false positive.
G-DCLI-05's ProgressBothImpl → Progress — the case the fallback was ADDED
for — never reached it; an earlier pass resolves it today. So the rule had no
measured legitimate user. The one test that relied on it now declares
nativeNames, which is the relationship becoming declared instead of guessed.
1.107.0 #
Fixed — a declaration keeps every await in its initializer (scd121) #
var s = (await a) + (await b); bound s = 1, not 3. The resumption branch for
a variable declaration bound futureResult — the value of ONE await — straight
to the variable and moved on, so everything else in the initializer was
discarded. var s = '${await a},${await b}'; was the same defect wearing a
different symptom: s held the raw int, and the next line failed its own
declared type with an error about a value the script never wrote. SCC40 had
fixed this family on the RETURN route; this branch never got the treatment.
It was never a missing-evaluation bug. The discarded operands WERE evaluated — visible by counting calls — so a repair that produces the right sum by evaluating an operand twice would pass a value-only test and still be wrong. The first attempt did exactly that, giving 5 instead of 3: an evaluation started inside the resumption branch cannot register its suspension with the state machine (the machine only ever attaches to a suspension raised by executing a node), so it is discarded and the statement re-executed anyway.
So the branch now evaluates nothing. It hands the statement back to the
machine, which executes the declaration again: sites already resolved replay
from resolvedAwaitResults, the first one not yet reached suspends for real,
and the variable is bound on the pass where nothing suspends — one evaluation
per pass. The per-site cache is cleared when that statement completes, which for
a declaration can only be seen where it is executed; the return route clears
via its own completion path.
Fixed — a + b does not evaluate b while a is suspended (scd121) #
visitBinaryExpression evaluated BOTH operands before checking either for a
suspension, so a suspending left operand still cost a full evaluation of the
right, whose value was then thrown away. Invisible for pure operands, and
invisible while a declaration never re-ran; once it did, (await next()) + (await next()) against a counter cost three calls for two awaits and six for
three. Dart evaluates a + b left to right and never reaches b while a is
outstanding.
1.106.0 #
Fixed — a native proxy now binds to a parameter declared as the script class it stands for (scd119) #
A script class that extends a bridged class crosses into native code as a
registered D4InterpretedProxy — _InterpretedThemeExtension for
class BrandColors extends ThemeExtension<BrandColors>, and the same for
widgets, painters and states. Ask such a value for its runtime type and the
answer is the BRIDGE's name, so binding it back to a parameter declared as the
script's own class was rejected:
type 'ThemeExtension' is not a subtype of type 'BrandColors' of 'brand'
Every member access on the same value worked, because the property and method
paths already unwrap a proxy. The type check was the only place that did not —
measured by changing the script's parameter to dynamic, which took the
script's framework errors from 1 to 0 with nothing else touched.
ResolvedBinding.bind now retries against the interpreted instance behind a
proxy. The retry runs only AFTER the base check has already failed, so it can
remove a rejection but never add one, and the PROXY is still what gets bound —
the value stays whatever native code downstream expects, only the verdict on it
changes. Teaching Environment.getRuntimeType to see through every proxy is
the more correct model and was deliberately not done: it would change what
is, as and runtimeType answer for every proxied widget in a live tree.
1.105.0 #
Fixed — a type alias now resolves to its target (scd100) #
SCC33 gave both interpreters an explicit handler for type aliases that returned null without recursing. That stopped seven further node types reaching the dispatch backstop, and it made explicit a gap that had been hidden: a typedef had no runtime representation at all. Both handlers' doc comments said "making aliases actually resolve is separate work (SCD100)".
Measured before choosing a fix. Nineteen shapes were probed; nine were already fine by leniency, and the rest split two ways — the first group being the one worth leading with:
- Seven legal programs THREW.
1 is Ithroughtypedef I = intdid not answer "no", it raisedType check failed: Undefined variable: I. So did a generic bound and a RETURN TYPE written through an alias:typedef I = int; I f() => 5;reportedType 'I' not found.and the program never ran. - Three accepted silently what Dart rejects — an
as, a parameter bind and a local, all of which stay lenient when an annotation cannot be resolved.
The handler was never even REACHED: the ordered declaration walk had phases for enums, classes, extensions, extension types, functions and variables, and none for type aliases. That is why the gap was total rather than partial.
The fix is one registration. Every type-resolution path already funnels
through environment.get(typeName) — is/as, parameter binding, the return
check, generic bounds and a collection literal's type argument — so binding the
alias name to the RuntimeType its target resolves to makes each behave exactly
as if the script had written the target. A new phase runs it to a FIXPOINT so
declaration order does not matter, placed after classes so an alias can name one
and before functions so their annotations can name an alias.
It had to land in TWO places, which is worth recording because a fix in
either alone looks complete: the source: form is ordered by d4rt_base.dart
and the sources:/library: form by module_loader.dart. Patching only the
first passed every hand-written probe while the test suite — which uses the
second — still failed on the return-type case.
as needed a separate touch. It does not resolve types at all; it switches
on the WRITTEN name, so an alias fell to its permissive default: and cast
anything to anything. The written name is now resolved through the alias first,
which is a no-op for every non-alias since those resolve to their own name.
The handler still does not recurse, which is the constraint SCC33 left: the
target is read off the annotation by _resolveTypeAnnotationWithEnvironment,
never by accepting a child, so no type-level syntax reaches the backstop.
Two limits measured and left, each with its reason. A generic bound written
through an alias still throws — bounds are extracted in PASS 1, before any phase
of pass 2 can have registered an alias, and fixing it means reordering pass 1
(sce130). A local declared through an alias still accepts a mismatch, which is
NOT alias-specific: int x = 'a' is equally lenient, because local variable
declarations are not type-checked at all (sce131). A GENERIC alias
(typedef L<T> = List<T>) is skipped on purpose — binding it here would bind
T to nothing and answer confidently wrong.
1.104.0 #
Fixed — x.runtimeType == SomeType was false for every type (scd99) #
The todo reported that Duration(seconds: 1).runtimeType.toString() is
'BridgedInstance<Object>'. Measured, it is Duration: the member-access sites
already answer bridgedInstance.nativeObject.runtimeType, and SCD98 then
stopped the constructor producing a wrapper at all. The one-line fix the todo
recommends was already in place at every site an audit finds.
What the todo was about survived that. Its stated harm is "a script that
branches on x.runtimeType takes the wrong branch with no error", and that was
true — for a different reason, and not only for bridged values:
Duration(seconds: 1).runtimeType == Duration // was false
1.runtimeType == int // was false
'a'.runtimeType == String // was false
DateTime(2020).runtimeType == DateTime // was false
Duration == Duration(seconds: 1).runtimeType // was TRUE
Asymmetric by operand order, and false in the order everybody writes. A correct
runtimeType whose result cannot be compared against a type literal is not
worth much.
The reconciliation existed and never ran. visitBinaryExpression carried
the Type-vs-BridgedClass comparison twice, in the == and != arms of its
operator switch. Neither was reachable for this shape:
Environment.toBridgedInstance succeeds on a Type object, so the
bridged-operator dispatch twenty lines ABOVE the switch found an == adapter
and invoked it on the WRAPPER — comparing a wrapped Type against a
BridgedClass and answering false.
That is the wrapper substituting itself for the value it wraps, which is the shape SCD98 removed from the constructor; it survived here because a dispatch site reached it first. The reconciliation is now hoisted above that dispatch, and the two copies in the arms are deleted rather than left unreachable — a second implementation of one rule is what let this diverge unnoticed.
A generic type argument still distinguishes: [1].runtimeType == List stays
false, as real Dart has it, so the fix does not simply make every
Type-versus-name comparison true.
The neighbours were audited, as the todo asked. toString, hashCode,
is, is!, as and cross-route == all already delegate to the native on
both production routes. They are pinned anyway, alongside equality on enum,
String, null, bridged and script-defined receivers — the hoist runs before the
bridged dispatch, so those are the cases that say it intercepts nothing it
should not.
1.103.0 #
Fixed — one representation for a bridged value: the bare native (scd98) #
D4rt had two representations for the same bridged value and no rule about which
one you got. A bridged CONSTRUCTOR call yielded a BridgedInstance<T> wrapper;
every method return, getter return, operator return and static call yielded the
bare native. The same conceptual value arrived in two shapes depending only on
how the script happened to produce it, and the two routinely met in one
collection.
The wrapping table, measured before choosing a direction (the todo made writing it down a precondition, because the two candidate directions have very different blast radii and only the table says which one the codebase is already closer to):
| site | before | after |
|---|---|---|
| bridged constructor, default | wrapper | native |
| bridged constructor, named | wrapper | native |
| bridged constructor, generic factory | wrapper | native |
| redirecting factory target | wrapper | native |
| bridged method / getter / operator | native | native |
| bridged static method, const | native | native |
| argument marshalled INTO a bridge | native | native |
Environment.toBridgedInstance |
wrapper | wrapper |
The constructor was the lone outlier, so converging on the native was a
four-site change at the introduction point rather than a refactor of the value
representation. toBridgedInstance stays a wrapper on purpose — it IS the
bridge-dispatch boundary the wrapper is meant to be confined to.
Why it was nearly invisible. Every observation route a script has already
unwraps: the host boundary, the runtimeType getter, argument marshalling, and
visitBinaryExpression, which unwraps both operands before ==. So
runtimeType, is, == and simple membership all agreed beforehand. It bites
where a NATIVE container compares its own stored elements, because then the
interpreter is not in the loop:
[Duration(seconds: 86400), // constructed -> wrapper
DateTime(2020,1,2).difference(DateTime(2020,1,1)) // method -> native
].toSet().length // was 2, is 1
and it was ORDER-DEPENDENT, which is the fingerprint: [ctor, method] failed
while [method, ctor] passed. SCC32 gave BridgedInstance cross-boundary
==/hashCode, so wrapper == raw is true — but raw == wrapper cannot be,
because a native's == rejects a foreign type. A stored wrapper probed by a
bare native runs the direction that cannot work.
A second defect, already live and unrelated to the wrapper. Chasing the
first surfaced it: _bridgeInterpreterValueToNative rebuilt every List with
.map(...).toList(), which retypes unconditionally, so a Uint8List reaching
the host from a METHOD or GETTER arrived as List<Object?> and an
as Uint8List threw. The CONSTRUCTOR route survived only because its wrapper
took the BridgedInstance branch and never reached the list branch — the same
split, showing up as a type loss rather than a duplicate. The boundary now
rebuilds a list or map only when an element actually changed, and returns the
original instance otherwise.
SCC32 is not removed, and its doc comment now says why. Its cross-boundary
equality and hash-key normalisation are no longer load-bearing for values this
interpreter produces — nothing it produces is a wrapper any more. But
toBridgedInstance still hands one to bridge dispatch and an embedder can put
one into a collection itself, so they stop being a workaround for an internal
inconsistency and become what they read as: a courtesy to a wrapper that
arrives from outside. F-SCC32-10/11/12 keep passing unchanged.
1.102.0 #
Fixed — the host receives the error the script raised, not a bridged shell (scd96) #
A script doing throw FormatException('boom') handed its caller a
BridgedInstance<Object>. An on FormatException written around execute() or
executeBundle() did not match it, and a bare catch (e) saw a d4rt-internal
type the host has no reason to know about.
The todo this came from was written against a stale premise, and re-measuring
found a different leak at the same boundary. It expected
InternalInterpreterD4rtException to reach callers; that carrier has been
peeled since SCC27. The leak is one peel further in: a bridged exception holds
its native object inside the carrier, and throwAsHostFacingError peeled the
carrier and stopped.
unwrapScriptError has documented the pair since SCD73 — "Two peels, not one …
a host that peeled only the first would get a BridgedInstance it cannot
catch on". The zone-callback route already did both, which is why the
behaviour was SPLIT rather than uniformly wrong:
| entry point | before | after |
|---|---|---|
tom_d4rt.execute() |
BridgedInstance |
FormatException |
D4rtRunner.executeBundle |
BridgedInstance |
FormatException |
D4rtRunner.executeBundleAs<T> |
BridgedInstance |
FormatException |
executeBundleAsAsync<T> |
FormatException |
FormatException |
The async variant was right because it runs through the zone callbacks SCD73 wrapped. So the todo's instruction to check the typed variants was pointing at a split that already existed between the synchronous and asynchronous halves of one boundary — the shape SCC27 was written to remove.
The todo offered two fixes and said to prefer teaching the shared helper "if the
sweep is clean, because the asymmetry is the defect and [the narrow fix] only
relocates it". It is clean — both full suites pass unchanged — so the fix is one
line in throwAsHostFacingError, mirrored, and every present and future caller
of that boundary gets it.
Not peeled, deliberately: a script-declared exception class arrives as
InterpretedInstance (there is no native object, and the host cannot name a
type the script invented); a thrown non-error value arrives as itself, because
real Dart lets a script throw 'plain'; and UndefinedNameD4rtException
arrives as itself, because SCC31 exists to make it reach the host and peeling it
would undo that.
This matters most on the analyzer-free line: executeBundle is what a Flutter
app calls to run a downloaded bundle, and it is the one API whose caller cannot
fall back to a different runner.
Older releases #
Releases 1.101.0 and earlier are in CHANGELOG_ARCHIVE.md.
pub.flutter-io.cn refuses a CHANGELOG.md over 262144 bytes, and this file crossed that
limit on 2026-09-28 (SCE209); the history was moved rather than shortened.
DFIN7 moved 1.78.0 to 1.101.0 as well (2026-10-03), when the file
reached 256 KB again.