Skip to content

Instantly share code, notes, and snippets.

@wispborne
Last active May 22, 2026 19:34
Show Gist options
  • Select an option

  • Save wispborne/070448335d0e65ab83d9a926dcd312fb to your computer and use it in GitHub Desktop.

Select an option

Save wispborne/070448335d0e65ab83d9a926dcd312fb to your computer and use it in GitHub Desktop.
dart_3.12.0-aot-crash
/// Dart AOT codegen crash reproducer.
///
/// Bug: `dart compile exe` generates native code that crashes with an access
/// violation (0xC0000005). `dart run` (JIT) works fine. The same exe with the
/// workaround (`fixed`) also works fine.
///
/// Trigger: an async method containing a for-loop-with-await, where the method
/// is part of a mutual-recursion cycle with another complex async method. The
/// recursive call does not need to execute at runtime — it only needs to exist
/// in the call graph.
///
/// Workaround: extract the for-loop-with-await into a separate async method.
///
/// Usage:
/// dart compile exe repro.dart -o repro.exe
/// ./repro.exe buggy # crashes (access violation)
/// ./repro.exe fixed # completes fine (workaround applied)
/// dart run repro.dart buggy # JIT — always works
///
/// Confirmed: Dart 3.12.0 stable, windows_x64.
import 'dart:async';
void debugPrint(String? message) {
if (message != null) print(message);
}
class ModInfo {
final String id;
final String? name;
final Version? version;
final String? author;
final List<Dependency> dependencies;
ModInfo(
this.id, {
this.name,
this.version,
this.author,
this.dependencies = const [],
});
}
class Version {
final int major;
final int minor;
final int patch;
final String? label;
Version(this.major, this.minor, this.patch, {this.label});
@override
String toString() => '$major.$minor.$patch${label != null ? '-$label' : ''}';
@override
int get hashCode => Object.hash(major, minor, patch, label);
@override
bool operator ==(Object other) =>
other is Version &&
major == other.major &&
minor == other.minor &&
patch == other.patch &&
label == other.label;
}
class Dependency {
final String? id;
final String? name;
final Version? version;
Dependency({this.id, this.name, this.version});
}
class Variant {
final String id;
final ModInfo modInfo;
final bool isModInfoEnabled;
Variant(this.id, this.modInfo, {this.isModInfoEnabled = true});
String get smolId {
final idPart = modInfo.id.length > 6
? modInfo.id.substring(0, 6)
: modInfo.id;
final vPart = modInfo.version?.toString() ?? 'unknown';
final hash = Object.hash(modInfo.id, modInfo.version).abs();
return '$idPart-$vPart-$hash';
}
String get bestVersion => modInfo.version?.toString() ?? modInfo.id;
List<DependencyCheck> checkDependencies(
List<Variant> allVariants,
List<String> enabledMods,
String? gameVersion,
) {
return modInfo.dependencies.map((dep) {
final found = allVariants.where((v) => v.modInfo.id == dep.id);
if (found.isEmpty) return DependencyCheck(dep, const Missing());
final enabled = found.where((v) => enabledMods.contains(v.modInfo.id));
if (enabled.isEmpty) return DependencyCheck(dep, Disabled(found.first));
return DependencyCheck(dep, const Satisfied());
}).toList();
}
}
sealed class SatisfiedAmount {
const SatisfiedAmount();
}
class Satisfied extends SatisfiedAmount {
const Satisfied();
}
class Missing extends SatisfiedAmount {
const Missing();
}
class Disabled extends SatisfiedAmount {
final Variant? modVariant;
const Disabled([this.modVariant]);
}
class VersionInvalid extends SatisfiedAmount {
const VersionInvalid();
}
class DependencyCheck {
final Dependency dependency;
final SatisfiedAmount satisfiedAmount;
DependencyCheck(this.dependency, this.satisfiedAmount);
}
class Mod {
final String id;
final List<Variant> modVariants;
final bool isEnabledInGame;
Mod(this.id, this.modVariants, {this.isEnabledInGame = true});
bool isEnabled(Variant v) => v.isModInfoEnabled;
List<Variant> get enabledVariants =>
modVariants.where((v) => v.isModInfoEnabled).toList();
Variant? get findFirstEnabled {
for (final v in modVariants) {
if (v.isModInfoEnabled) return v;
}
return null;
}
Variant? get findHighestEnabledVersion {
final enabled = enabledVariants;
if (enabled.isEmpty) return null;
return enabled.reduce((a, b) {
final av = a.modInfo.version;
final bv = b.modInfo.version;
if (av == null) return b;
if (bv == null) return a;
if (bv.major > av.major) return b;
if (bv.minor > av.minor) return b;
if (bv.patch > av.patch) return b;
return a;
});
}
bool isEnabledInGameSync(List<String> enabledMods) =>
enabledMods.contains(id);
}
class StateStore {
Map<String, bool> enabledMods = {'modA': true, 'modB': true};
List<Variant> allVariants = [];
List<String> auditLog = [];
bool autoValidateDependencies = true;
String? gameVersion = '0.97a';
List<Mod> getMods() {
final grouped = <String, List<Variant>>{};
for (final v in allVariants) {
(grouped[v.modInfo.id] ??= []).add(v);
}
return grouped.entries
.map(
(e) =>
Mod(e.key, e.value, isEnabledInGame: enabledMods[e.key] == true),
)
.toList();
}
}
class ModManager {
final StateStore _state;
ModManager(this._state) {
_state.allVariants = [
Variant(
'modA-v1',
ModInfo(
'modA',
name: 'Mod Alpha',
version: Version(1, 0, 0),
dependencies: [Dependency(id: 'modB', name: 'Mod Beta')],
),
),
Variant(
'modA-v2',
ModInfo('modA', name: 'Mod Alpha', version: Version(2, 0, 0)),
isModInfoEnabled: false,
),
Variant(
'modB-v1',
ModInfo('modB', name: 'Mod Beta', version: Version(1, 0, 0)),
),
Variant(
'modC-v1',
ModInfo(
'modC',
name: 'Mod Charlie',
version: Version(3, 2, 1, label: 'rc1'),
dependencies: [
Dependency(id: 'modA', name: 'Mod Alpha'),
Dependency(id: 'modD', name: 'Mod Delta'),
],
),
isModInfoEnabled: false,
),
];
}
/// BUGGY: crashes under AOT. Contains a for-loop-with-await and is mutually
/// recursive with _validateModDependencies.
Future<String> changeActiveModVariant_BUGGY(
Mod mod,
Variant? modVariant, {
bool notifyWatchers = true,
bool validateDependencies = true,
}) async {
final isDisablingMod = modVariant == null;
debugPrint(
isDisablingMod
? "Disabling ${mod.id}."
: "Changing active variant of ${mod.id} to ${modVariant.smolId}. (current: ${mod.findFirstEnabled?.smolId}).",
);
final modVariantParentModId = modVariant?.modInfo;
if (modVariantParentModId != null && mod.id != modVariantParentModId.id) {
throw Exception("Mod variant does not belong to mod ${mod.id}.");
}
if (modVariant != null && mod.isEnabled(modVariant)) {
if (mod.modVariants.where((it) => mod.isEnabled(it)).length <= 1) {
return 'Already enabled.';
}
}
final modInfoEnabledVariants = mod.modVariants
.where((it) => it.isModInfoEnabled)
.toList();
if (modVariant == null && modInfoEnabledVariants.isEmpty) {
return 'Nothing to do.';
}
// CRASH SITE: for-loop-with-await in a mutually recursive method.
for (final variant in modInfoEnabledVariants) {
if (variant.smolId != modVariant?.smolId) {
try {
await _disableModVariant(
variant,
disableModInVanillaLauncher: isDisablingMod,
brickModInfo: !isDisablingMod && mod.modVariants.length > 1,
reason: isDisablingMod
? "You disabled ${mod.id} (${variant.modInfo.version} was enabled before)."
: "Changed ${mod.id} to ${modVariant!.modInfo.version}, so ${variant.bestVersion} has to be disabled.",
);
} catch (e, st) {
debugPrint("Error disabling mod variant: $e\n$st");
}
}
}
if (!isDisablingMod) {
await _enableModVariant(
modVariant,
mod,
enableInVanillaLauncher: true,
reason: "enable",
);
} else {
final disabledModVariants = mod.modVariants
.where((v) => !v.isModInfoEnabled)
.toList();
for (final disabledVariant in disabledModVariants) {
try {
await _enableModInfoFile(disabledVariant);
} catch (e, st) {
debugPrint("Error enabling mod_info.json file: $e\n$st");
}
}
}
if (notifyWatchers) {
await _reloadModVariants(onlyVariants: mod.modVariants);
}
if (validateDependencies) {
await _validateModDependencies(modsToFreeze: [mod.id]);
}
return 'Done (buggy path).';
}
/// FIXED: identical logic, but the for-loops-with-await are extracted into
/// separate methods. This breaks the complex async state machine that AOT
/// miscompiles. Runs fine under AOT.
Future<String> changeActiveModVariant_FIXED(
Mod mod,
Variant? modVariant, {
bool notifyWatchers = true,
bool validateDependencies = true,
}) async {
final isDisablingMod = modVariant == null;
debugPrint(isDisablingMod
? "Disabling ${mod.id}."
: "Changing active variant of ${mod.id} to ${modVariant.smolId}. (current: ${mod.findFirstEnabled?.smolId}).");
final modVariantParentModId = modVariant?.modInfo;
if (modVariantParentModId != null && mod.id != modVariantParentModId.id) {
throw Exception("Mod variant does not belong to mod ${mod.id}.");
}
if (modVariant != null && mod.isEnabled(modVariant)) {
if (mod.modVariants.where((it) => mod.isEnabled(it)).length <= 1) {
return 'Already enabled.';
}
}
final modInfoEnabledVariants =
mod.modVariants.where((it) => it.isModInfoEnabled).toList();
if (modVariant == null && modInfoEnabledVariants.isEmpty) {
return 'Nothing to do.';
}
// Workaround: for-loop-with-await extracted into its own method.
await _disableOtherVariants(modInfoEnabledVariants, modVariant,
isDisablingMod: isDisablingMod, mod: mod);
if (!isDisablingMod) {
await _enableModVariant(modVariant, mod,
enableInVanillaLauncher: true, reason: "enable");
} else {
await _unbrickModInfoFiles(mod);
}
if (notifyWatchers) {
await _reloadModVariants(onlyVariants: mod.modVariants);
}
if (validateDependencies) {
await _validateModDependencies(modsToFreeze: [mod.id]);
}
return 'Done (fixed path).';
}
Future<void> _disableOtherVariants(
List<Variant> modInfoEnabledVariants,
Variant? modVariant, {
required bool isDisablingMod,
required Mod mod,
}) async {
for (final variant in modInfoEnabledVariants) {
if (variant.smolId != modVariant?.smolId) {
try {
await _disableModVariant(
variant,
disableModInVanillaLauncher: isDisablingMod,
brickModInfo: !isDisablingMod && mod.modVariants.length > 1,
reason: isDisablingMod
? "You disabled ${mod.id} (${variant.modInfo.version} was enabled before)."
: "Changed ${mod.id} to ${modVariant!.modInfo.version}, so ${variant.bestVersion} has to be disabled.",
);
} catch (e, st) {
debugPrint("Error disabling mod variant: $e\n$st");
}
}
}
}
Future<void> _unbrickModInfoFiles(Mod mod) async {
final disabledModVariants =
mod.modVariants.where((v) => !v.isModInfoEnabled).toList();
for (final disabledVariant in disabledModVariants) {
try {
await _enableModInfoFile(disabledVariant);
} catch (e, st) {
debugPrint("Error enabling mod_info.json file: $e\n$st");
}
}
}
Future<void> _disableModVariant(
Variant modVariant, {
bool brickModInfo = false,
bool disableModInVanillaLauncher = true,
required String reason,
}) async {
final mods = _state.getMods();
if (brickModInfo) {
debugPrint(' Bricking mod_info for ${modVariant.smolId}');
}
if (disableModInVanillaLauncher) {
final mod = mods.firstWhere((m) => m.id == modVariant.modInfo.id);
if (mod.isEnabledInGame) {
await _disableModInEnabledMods(modVariant.modInfo.id);
}
}
_state.auditLog.add('disable ${modVariant.smolId}: $reason');
}
Future<void> _enableModVariant(
Variant modVariant,
Mod mod, {
bool enableInVanillaLauncher = true,
required String reason,
}) async {
if (mod.isEnabled(modVariant)) return;
await _enableModInfoFile(modVariant);
if (enableInVanillaLauncher && !mod.isEnabledInGame) {
await _enableModInEnabledMods(modVariant.modInfo.id);
}
_state.auditLog.add('enable ${modVariant.smolId}: $reason');
}
Future<void> _enableModInfoFile(Variant modVariant) async {
await Future<void>.delayed(const Duration(milliseconds: 1));
}
Future<void> _disableModInEnabledMods(String modId) async {
_state.enabledMods[modId] = false;
await Future<void>.delayed(const Duration(milliseconds: 1));
}
Future<void> _enableModInEnabledMods(String modId) async {
_state.enabledMods[modId] = true;
await Future<void>.delayed(const Duration(milliseconds: 1));
}
Future<void> _reloadModVariants({List<Variant>? onlyVariants}) async {
await Future<void>.delayed(const Duration(milliseconds: 5));
}
/// Mutually recursive with changeActiveModVariant_BUGGY. The recursive call
/// does not execute at runtime in this test, but its presence in the call
/// graph is required to trigger the crash.
Future<void> _validateModDependencies({List<String>? modsToFreeze}) async {
if (!_state.autoValidateDependencies) return;
final modifiedModIds = modsToFreeze?.toSet() ?? {};
var numModsChangedLastLoop = 0;
final gameVersion = _state.gameVersion;
do {
numModsChangedLastLoop = 0;
final enabledMods = _state.enabledMods.entries
.where((e) => e.value)
.map((e) => e.key)
.toList();
final allVariants = _state.allVariants;
final allMods = _state.getMods();
for (final mod in allMods) {
if (!mod.isEnabledInGameSync(enabledMods)) continue;
if (mod.enabledVariants.length > 1) {
final highestEnabled = mod.findHighestEnabledVersion;
for (var value in mod.enabledVariants.where(
(variant) => variant.smolId != highestEnabled?.smolId,
)) {
try {
await _disableModVariant(
value,
brickModInfo: true,
disableModInVanillaLauncher: false,
reason: "validate ${value.smolId}",
);
} catch (e, st) {
debugPrint("Error disabling mod variant: $e\n$st");
}
}
}
final enabledVariant = mod.findFirstEnabled;
if (enabledVariant == null) continue;
final dependenciesFound = enabledVariant.checkDependencies(
allVariants,
enabledMods,
gameVersion,
);
for (final dependencyCheck in dependenciesFound) {
if (dependencyCheck.dependency.id == null) continue;
if (!modifiedModIds.contains(dependencyCheck.dependency.id) &&
dependencyCheck.satisfiedAmount is Disabled) {
final dependency =
(dependencyCheck.satisfiedAmount as Disabled).modVariant;
if (dependency != null) {
modifiedModIds.add(mod.id);
final depMod = allMods.firstWhere(
(m) => m.id == dependency.modInfo.id,
);
await changeActiveModVariant_BUGGY(
depMod,
dependency,
validateDependencies: false,
);
numModsChangedLastLoop++;
}
} else if (!modifiedModIds.contains(mod.id) &&
dependencyCheck.satisfiedAmount is VersionInvalid ||
dependencyCheck.satisfiedAmount is Missing ||
dependencyCheck.satisfiedAmount is Disabled) {
modifiedModIds.add(mod.id);
await changeActiveModVariant_BUGGY(
mod,
null,
validateDependencies: false,
);
numModsChangedLastLoop++;
}
}
}
} while (numModsChangedLastLoop > 0);
}
}
Future<void> main(List<String> args) async {
final mode = args.isNotEmpty ? args.first : 'buggy';
final manager = ModManager(StateStore());
final mod = manager._state.getMods().firstWhere((m) => m.id == 'modA');
print('mode = $mode (disable modA)');
final String result;
if (mode == 'fixed') {
result = await manager.changeActiveModVariant_FIXED(mod, null);
} else {
result = await manager.changeActiveModVariant_BUGGY(mod, null);
}
print('result = $result');
print('Completed without crashing.');
}
# Reproduce the Dart AOT codegen crash, and show the workaround surviving AOT.
#
# Requires Dart SDK 3.12.0 (the version with the bug). Uses `dart` from PATH by
# default; if your default Dart isn't 3.12, point this at the right SDK:
# $env:DART = "fvm dart"; ./run.ps1
$Dart = if ($env:DART) { $env:DART } else { "dart" }
Write-Host "=== Dart version ==="
Invoke-Expression "$Dart --version"
Write-Host "`n=== JIT, buggy branch (expected: runs fine) ==="
Invoke-Expression "$Dart run repro.dart buggy"
Write-Host "JIT buggy exit code: $LASTEXITCODE"
Write-Host "`n=== AOT compile ==="
Invoke-Expression "$Dart compile exe repro.dart -o repro.exe"
Write-Host "`n=== AOT, FIXED branch (expected: runs fine - the workaround) ==="
& ".\repro.exe" fixed
Write-Host "AOT fixed exit code: $LASTEXITCODE"
Write-Host "`n=== AOT, BUGGY branch (expected: crash, access violation) ==="
& ".\repro.exe" buggy
Write-Host "AOT buggy exit code: $LASTEXITCODE"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment