tom_d4rt 1.10.1
tom_d4rt: ^1.10.1 copied to clipboard
D4rt - A Dart interpreter and runtime with bridging, security sandboxing, and dynamic code execution. Fork of D4rt with extended features.
tom_d4rt #
Attribution. The
tom_d4rtproject is an extended clone of the original d4rt project by Moustapha Kodjo Amadou, initially published in 2025. The complete interpreter is based on his idea.
A secure, sandboxed Dart interpreter written in Dart — the analyzer-based reference implementation of the D4rt runtime.
Overview #
tom_d4rt executes Dart source code at runtime without compilation. It is built on top of the analyzer package, which provides a full parse tree (AST) for any Dart 3 source string. The interpreter walks that AST in two passes — a declaration pass that registers classes and mixins as placeholders, followed by an interpretation pass that evaluates imports, resolves members, and calls the target function.
Two execution modes are supported:
execute()— full-script, two-pass execution. Parses the source, runs both theDeclarationVisitor(pass 1) andInterpreterVisitor(pass 2), then calls the named entry point. Each call gets a fresh module loader and global environment.eval()— REPL-style incremental evaluation. Reuses theInterpreterVisitorandEnvironmentfrom the lastexecute()call, so previously defined variables and functions are still in scope.
Scripts run inside an isolated Environment scope chain. Sensitive operations — dart:io, dart:isolate, process execution, network access — are blocked by default. The host process grants fine-grained permissions via the grant() API.
tom_d4rt is the stable reference that all existing projects in this workspace depend on. Its public API (D4rt, BridgedClass, BridgedEnumDefinition, D4, and the permission classes) is kept in 1:1 lockstep with the analyzer-free successor line (tom_d4rt_ast) so that bridge code generated by tom_d4rt_generator works against either interpreter without modification.
Source-based vs analyzer-free — which line to use #
D4rt ships in two execution families that share one bridge API:
- Source-based (analyzer) — this package, plus
tom_d4rt_dcli(REPL) andtom_d4rt_flutter(Flutter Material bridges). The host parses Dart source with theanalyzerpackage and interprets it directly. - Analyzer-free (mirror AST) —
tom_ast_model,tom_d4rt_ast,tom_ast_generator,tom_d4rt_exec,tom_dcli_exec. These run from pre-compiledSAstNodetrees with no analyzer dependency:
tom_ast_model ← tom_d4rt_ast ← tom_ast_generator ← tom_d4rt_exec ← tom_dcli_exec
tom_d4rt is the stable reference and is usually the preferable choice. The analyzer-free line exists for the one case the analyzer cannot serve: the web (where the analyzer package is too large to ship) and on-the-fly / OTA UI updates, where a server compiles source to an AST bundle that a thin client interprets without redeploying. It is otherwise a complete alternative, but because the generated AST bundles are large, reach for it only when that web/OTA constraint actually applies.
tom_d4rt_exec is the analyzer-free counterpart of this package: a drop-in D4rt-style entry point that parses with the analyzer at build time and interprets via tom_d4rt_ast at run time. tom_d4rt_ast shares this package's bridge API surface, and both lines are kept in sync — any fix merged into tom_d4rt is back-ported to tom_d4rt_ast and vice versa.
Installation #
dependencies:
tom_d4rt: ^1.8.23
dart pub add tom_d4rt
Requires Dart SDK ^3.5.0. The only runtime dependencies are analyzer: ^8.0.0 and pub_semver: ^2.2.0.
Features #
Language coverage #
tom_d4rt passes all 20 areas of the Dart language overview test suite (1,680+ tests). Covered language constructs include:
| Area | Details |
|---|---|
| Classes | Declarations, constructors (factory, named, redirecting, const), inheritance, super, abstract/final/sealed/interface/base class modifiers |
| Mixins | with, mixin abstract method satisfaction, enum-with-mixin |
| Generics | Generic classes and functions, type bounds, variance, F-bounded polymorphism, is checks with parameterized types |
| Patterns | Destructuring, switch expressions and statements, logical-OR patterns, when guards, record patterns (named fields, shorthand :name) |
| Records | Positional and named fields (up to 9 positional fields returned as native Dart records; larger records return InterpretedRecord) |
| Async/await | async/await, async* generators, sync* generators, await for, Future, Stream, StreamController, StreamTransformer, Timer, Completer |
| Extensions | Instance and static extension members, extensions on bridged types, imported extensions |
| Extension types | Dart 3.3+ inline classes / extension types |
| Enums | Enhanced enums with members, enums with mixins, .name, .index, .values, .byName |
| Error handling | try/catch/finally, rethrow, custom exception classes |
| Operators | All arithmetic, comparison, bitwise, cascade (.., ?..), spread, null-aware, type (is, as, is!) |
late variables |
Lazy initialization, late final, static and instance late fields, LateInitializationError |
| Control flow | if/else, for, for-in, while, do-while, switch, break/continue with labels |
| Collections | List, Set, Map, spread operator, collection-if / collection-for, const collections |
| Null safety | Full null-safety — nullable types, null-aware operators (?., ??, ??=, !) |
const |
Const expressions, const constructors, const fields, const collections |
| Typedefs | Function and type typedefs |
| Annotations | Declaration annotations |
| Libraries | import/export with show/hide, relative imports, multi-file source maps, file-system imports |
| Isolates | Isolate, SendPort, ReceivePort, Capability (communication bridged; spawning interpreted closures across isolate boundaries is a known limitation) |
Standard library bridges #
The following dart: libraries are bridged out of the box:
| Library | Key types |
|---|---|
dart:core |
int, double, num, bool, String, StringBuffer, List, Map, Set, Iterable, Iterator, DateTime, Duration, RegExp, Uri, BigInt, Symbol, Runes, Enum, Function, Type, StackTrace, Error, and all standard exceptions |
dart:async |
Future, Stream, StreamController, StreamSubscription, StreamTransformer, Completer, Timer |
dart:collection |
HashMap, HashSet, LinkedHashMap, LinkedList, ListQueue, Queue, SplayTreeMap, UnmodifiableListView |
dart:convert |
jsonEncode/jsonDecode, base64Encode/base64Decode, utf8, ascii, latin1, LineSplitter, HtmlEscape, codecs and converters |
dart:math |
min, max, sqrt, pow, log, sin, cos, tan, Random, constants (pi, e, ln2, …) |
dart:typed_data |
Uint8List, Int16List, Float32List, ByteData, ByteBuffer, Endian — eagerly registered so Flutter bridge code that uses ByteData without an explicit import works out of the box |
dart:io |
File, Directory, HttpClient, Socket, Process, IOSink, Stdin/Stdout — available only after grant(FilesystemPermission.any) / grant(NetworkPermission.any) |
dart:isolate |
Available only after grant(IsolatePermission.any) |
External dart: URIs not in the list above are checked for registered bridge content before raising an error, allowing embedding projects to supply bridges for dart:ui or other platform libraries.
Bridging native code #
Any native Dart class, enum, top-level function, or variable can be exposed to interpreted scripts by registering it before execution. The tom_d4rt_generator package automates bridge creation from annotated source files.
Permission sandboxing #
Scripts run in a deny-by-default sandbox. The host process grants and revokes permissions at any granularity.
Quick start #
import 'package:tom_d4rt/tom_d4rt.dart';
void main() {
final d4rt = D4rt();
// Execute a script — calls main() by default
d4rt.execute(
source: '''
void main() {
print('Hello from D4rt!');
}
''',
);
// Call a named function with arguments
final result = d4rt.execute(
source: '''
String greet(String name, int age) {
return 'Hello \$name, you are \$age';
}
''',
name: 'greet',
positionalArgs: ['Alice', 30],
);
print(result); // Hello Alice, you are 30
}
Example projects #
Runnable, self-contained samples live in tom_d4rt_samples/. They are ordered as a learning path — each one introduces exactly one new capability on top of the last:
| Sample | What it teaches | Interpreter |
|---|---|---|
| d4rt_introduction_sample | Run multi-file D4rt programs with nothing but the interpreter — no bridges, no host wiring. Includes shebang launchers (#!/usr/bin/env run_example). Start here. |
tom_d4rt |
| d4rt_advanced_sample | Cross the first boundary: bridge a native Dart library into scripts using tom_d4rt_generator. |
tom_d4rt |
| d4rt_userbridges_sample | Hand-write D4UserBridge overrides for what the generator can't bridge automatically. |
tom_d4rt |
| d4rt_dcli_sample | Shell scripting plus your own bridges via the tom_d4rt_dcli REPL. |
tom_d4rt |
| d4rt_flutter_sample | Interpret a live Flutter UI from source at runtime. | tom_d4rt_flutter |
If you are new to D4rt, clone the introduction sample and run one of its examples directly — it is the shortest path from "installed" to "running a script".
Usage #
Full-script execution #
execute() always initializes a fresh ModuleLoader and Environment. Every call is independent unless you explicitly use continuedExecute() or eval().
final d4rt = D4rt();
// With named and positional arguments
final result = d4rt.execute(
source: '''
String greet({required String name, int times = 1}) {
return List.generate(times, (_) => 'Hello \$name').join(', ');
}
''',
name: 'greet',
namedArgs: {'name': 'World', 'times': 3},
);
// 'Hello World, Hello World, Hello World'
// Multi-file execution via a source map
d4rt.execute(
source: '''
import 'package:my_app/utils.dart';
void main() {
print(formatDate(DateTime.now()));
}
''',
sources: {
'package:my_app/utils.dart': '''
String formatDate(DateTime d) => '\${d.year}-\${d.month}-\${d.day}';
''',
},
);
// File-system imports (requires filesystem permission)
d4rt.grant(FilesystemPermission.read);
d4rt.execute(
source: "import './lib/utils.dart'; void main() { helper(); }",
basePath: '/path/to/project',
allowFileSystemImports: true,
);
continuedExecute() reuses the existing environment from a prior execute() call, letting you add declarations without resetting state.
REPL-style evaluation #
After calling execute() to establish a context, eval() evaluates expressions and statements incrementally in the same environment:
final d4rt = D4rt();
d4rt.execute(source: '''
var counter = 0;
void increment() { counter++; }
''');
d4rt.eval('increment()');
d4rt.eval('increment()');
print(d4rt.eval('counter')); // 2
// Define a new function in the same session
d4rt.eval('int doubled() => counter * 2;');
print(d4rt.eval('doubled()')); // 4
eval() first tries to parse the string as a top-level declaration. If that succeeds, it registers the declaration and returns null. Otherwise it wraps the string in dynamic __eval__() { return <expr>; } and executes it, returning the value.
To reset the session between REPL interactions without rebuilding the bridge registrations, call resetScriptDeclarations().
Code introspection #
analyze() parses source and returns an IntrospectionResult describing all top-level declarations without executing any function:
final result = d4rt.analyze(source: '''
class Person {
final String name;
final int age;
Person(this.name, this.age);
String greet() => "Hi, I'm \$name";
}
int add(int a, int b) => a + b;
final greeting = 'Hello';
''');
print(result.classes); // [ClassInfo(Person)]
print(result.functions); // [FunctionInfo(add)]
print(result.variables); // [VariableInfo(greeting)]
Registering bridged classes #
A BridgedClass adapts a native Dart class for use inside interpreted scripts. Constructors, instance methods, instance getters/setters, static methods, and static getters/setters are all expressed as Dart closures with a standard adapter signature.
import 'package:tom_d4rt/tom_d4rt.dart';
class Counter {
int value;
Counter(this.value);
void increment() => value++;
void add(int n) => value += n;
}
void main() {
final d4rt = D4rt();
final counterBridge = BridgedClass(
nativeType: Counter,
name: 'Counter',
constructors: {
// Default constructor — named '' for the unnamed constructor
'': (visitor, positional, named) => Counter(positional[0] as int),
},
getters: {
'value': (visitor, target) => (target as Counter).value,
},
setters: {
'value': (visitor, target, v) => (target as Counter).value = v as int,
},
methods: {
'increment': (visitor, target, positional, named, typeArgs) {
(target as Counter).increment();
return null;
},
'add': (visitor, target, positional, named, typeArgs) {
(target as Counter).add(positional[0] as int);
return null;
},
},
);
d4rt.registerBridgedClass(counterBridge, 'package:my_app/counter.dart');
final result = d4rt.execute(source: '''
import 'package:my_app/counter.dart';
int main() {
final c = Counter(10);
c.increment();
c.add(5);
return c.value; // 16
}
''');
print(result); // 16
}
Additional registration methods on D4rt:
| Method | Purpose |
|---|---|
registerBridgedEnum(def, library) |
Expose a native enum to scripts |
registerBridgedExtension(def, library) |
Expose a Dart extension to scripts |
registertopLevelFunction(name, fn, library) |
Expose a top-level function |
registerGlobalVariable(name, value, library) |
Expose a top-level variable (eager) |
registerGlobalGetter(name, getter, library) |
Expose a top-level variable (lazy, evaluated on access) |
registerGlobalSetter(name, setter, library) |
Expose a top-level setter |
registerClassAlias(alias, target, library) |
Register a typedef-style class alias |
registerFunctionTypedef(name, library) |
Register a function typedef name for type resolution |
registerLibraryReExport(source, target, {show, hide}) |
Mirror export directives so scripts that import a barrel get all re-exported symbols |
registerExtensions(packageName, body) |
Queue a post-registration callback for relaxers / proxy factories |
finalizeBridges() |
Run all queued extension callbacks (called automatically on first execute/eval) |
registerRelaxerFactory(baseTypeName, factory) |
Register a relaxer that coerces interpreted values into a native parameterized bridged type |
registerInterfaceProxy(bridgedTypeName, factory) |
Register a proxy so an interpreted instance can satisfy a bridged abstract interface |
registerGenericConstructor(className, ctorName, factory) |
Register a factory that builds a native generic bridged instance from interpreted args + type args |
warmup() |
Finalize bridges and JIT-warm the parser/interpreter with a throwaway build |
The three facades (registerRelaxerFactory / registerInterfaceProxy /
registerGenericConstructor) are thin wrappers over the static D4 registries,
meant to be called from inside a registerExtensions body so they run once at
finalize time, in package order, after the standard bridges are wired up. See
the User Guide → Extension Registration and Facades
for the full contract. Set D4.usageLogEnabled = true (or the env var
D4RT_LOG_RELAXER_USAGE) to audit which relaxers/proxies are hit at runtime.
For large bridge surfaces, use tom_d4rt_generator to generate all adapter boilerplate from annotated native source. Duplicated registrations are safely deduplicated via sourceUri tracking.
For the complete registration reference see the Bridging Guide; for a worked end-to-end example, the d4rt_advanced_sample bridges a native library with the generator, and the d4rt_userbridges_sample shows hand-written overrides.
Permission system #
All sensitive operations are blocked by default. Grant permissions before executing code that needs them:
final d4rt = D4rt();
// Filesystem
d4rt.grant(FilesystemPermission.read); // read any path
d4rt.grant(FilesystemPermission.writePath('/tmp')); // write under /tmp only
d4rt.grant(FilesystemPermission.any); // read + write + execute, any path
// Network
d4rt.grant(NetworkPermission.connectTo('api.example.com'));
d4rt.grant(NetworkPermission.listenOn(8080));
d4rt.grant(NetworkPermission.any);
// Process execution
d4rt.grant(ProcessRunPermission.command('git'));
d4rt.grant(ProcessRunPermission.any);
// Isolates
d4rt.grant(IsolatePermission.spawn);
d4rt.grant(IsolatePermission.any);
// Dangerous (use with extreme caution)
d4rt.grant(DangerousPermission.codeEvaluation);
d4rt.grant(DangerousPermission.nativePlugins);
Permissions can be revoked at any time with d4rt.revoke(permission). Check the current set with d4rt.hasPermission(permission) or d4rt.checkPermission(operation).
The full permission class hierarchy:
| Class | Static constants | Factory constructors |
|---|---|---|
FilesystemPermission |
.read, .write, .execute, .any |
.readPath(p), .writePath(p), .executePath(p), .path(p) |
NetworkPermission |
.connect, .listen, .bind, .any |
.connectTo(host), .connectToPort(host, port), .listenOn(port) |
ProcessRunPermission |
.any |
.command(cmd), .commandWithArgs(cmd, args) |
IsolatePermission |
.spawn, .communicate, .any |
— |
DangerousPermission |
.codeEvaluation, .nativePlugins, .any |
— |
D4 bridge helper #
The D4 class provides static utilities used in generated and hand-written bridge adapters:
- Type coercion:
D4.coerceList<T>(raw),D4.coerceMap<K,V>(raw) - Argument extraction:
D4.getRequiredArg<T>(positional, index, name, className),D4.getOptionalArg<T>(...),D4.getNamedArg<T>(named, name, className) - Target validation:
D4.validateTarget<T>(target, className)— ensures the receiver is of the expected native type before calling instance methods - Arity checking:
D4.checkArity(positional, expected, methodName) - Callback bridging:
D4.callInterpreterCallback(visitor, fn, args)— dispatches a call from a native callback into the interpreter - Active visitor:
D4.withActiveVisitor(visitor, body)— sets the thread-local active visitor used by interface-proxy factories - Generic wrappers:
D4.registerGenericTypeWrapper<T>(factory),D4.extractBridgedArg<T>(raw, visitor)for covariant generic type resolution
Configuration inspection #
d4rt.getConfiguration() returns a D4rtConfiguration snapshot listing all registered bridges, granted permissions, global variables and getters, and global functions. d4rt.getEnvironmentState() returns an EnvironmentState with the names of variables, bridged classes, and bridged enums currently live in the global scope.
d4rt.validateRegistrations(source: ...) runs a full parse and import pass in error-collection mode, returning a list of registration conflict messages without aborting on the first error.
Architecture #
Two-pass execution #
Source string
│
▼
analyzer.parseString() ──► CompilationUnit (AST)
│
├─ Pass 1: DeclarationVisitor
│ Registers InterpretedClass / InterpretedMixin placeholders
│ in the global Environment. No member resolution yet.
│
└─ Pass 2: InterpreterVisitor
1. ImportDirectives → ModuleLoader loads bridge libraries
2. EnumDeclarations → populate enum value tables
3. ClassDeclarations → populate constructors, methods, fields
(static field inits deferred to avoid forward-ref issues)
4. ExtensionDeclarations / ExtensionTypeDeclarations
5. FunctionDeclarations
6. TopLevelVariableDeclarations
7. Call named entry point
Environment #
Environment is a lexical scope chain backed by a Map<String, Object?>. Each function call or block creates a child Environment that delegates lookups to its enclosing scope. The global environment holds stdlib bridges, user-registered bridges, and top-level script declarations. InterpreterVisitor carries a reference to the current Environment and updates it as it walks the AST.
Bridging system #
A BridgedClass wraps a native Dart type. When the interpreter encounters new MyNative() or a method call on a BridgedInstance, it looks up the registered BridgedClass and invokes the corresponding adapter closure. The adapter receives an InterpreterVisitor (for re-entering the interpreter from callbacks), the native target object, and the evaluated argument lists.
BridgedInstance wraps a native value and carries a reference to its BridgedClass. It exposes get, set, and call dispatch and handles all Dart operators (+, -, [], []=, ==, etc.).
BridgedEnumDefinition exposes native Enum values, including .name, .index, .values, and .byName, as well as custom methods and getters.
BridgedExtensionDefinition exposes extension methods so they are discoverable by Environment.findExtensionMember.
Module loader #
ModuleLoader manages a map of package URI to source string. When an import directive is processed, it parses the target source, runs pass 1 and pass 2 in that module's own Environment, and merges the exported names (respecting show/hide) into the importing scope. Bridged libraries are registered into a per-module environment without requiring a source string. registerLibraryReExport lets bridge packages model export directives so transitive re-exports are automatically resolved.
Key types exposed from package:tom_d4rt/tom_d4rt.dart #
| Type | Role |
|---|---|
D4rt |
Main interpreter class — entry point for all execution |
InterpreterVisitor |
AST visitor that drives interpretation; accessible via d4rt.visitor |
DeclarationVisitor |
Pass-1 AST visitor that seeds class placeholders |
Environment |
Lexical scope chain |
BridgedClass |
Adapter descriptor for a native class |
BridgedInstance |
Runtime wrapper around a native object |
BridgedEnumDefinition |
Adapter descriptor for a native enum |
BridgedExtensionDefinition |
Adapter descriptor for a native extension |
D4 |
Static helpers for generated bridge adapters |
Permission (and subclasses) |
Sandbox permission objects |
IntrospectionResult |
Output of d4rt.analyze() |
ScriptExecutionResult |
Structured result from file-based script execution |
D4rtConfiguration |
Snapshot of all registered bridges and permissions |
RuntimeD4rtException |
Thrown for interpreter-level errors |
SourceCodeD4rtException |
Thrown for parse errors in the source |
LibraryVariable, LibraryGetter, LibrarySetter, LibraryFunction, LibraryClass, LibraryEnum, LibraryExtension |
Wrappers used when registering bridge elements under a library URI |
Ecosystem #
tom_d4rt (this package)
│
├─ tom_d4rt_generator
│ Source-generator for BridgedClass / BridgedEnumDefinition boilerplate.
│ Reads @D4rtUserBridge annotations, emits bridge .dart files.
│ See: ../tom_d4rt_generator/doc/bridgegenerator_user_guide.md
│
└─ tom_d4rt_dcli (tom_dcli)
DCli-based CLI runner that uses tom_d4rt to execute *.dcli.dart scripts.
Analyzer-free parallel line (same bridge API, no analyzer dependency):
tom_ast_model ← tom_d4rt_ast ← tom_ast_generator ← tom_d4rt_exec ← tom_dcli_exec
Bridges generated by tom_d4rt_generator (or its AST-line counterpart tom_ast_generator) compile against this package's API. Both generators produce code that calls identical registerBridgedClass / registerBridgedEnum / registerExtensions / finalizeBridges sequences, so a bridge package works against either the tom_d4rt or tom_d4rt_ast interpreter without branching.
Further documentation #
This package's own guides (in doc/):
- User Guide — execution modes, configuration, multi-file scripts, extension registration & facades
- Bridging Guide — detailed coverage of every registration API
- Advanced Bridging Guide — D4 helper class, type coercion, argument extraction, interface proxies, generic constructors
- Limitations — canonical interpreter limitations reference. Every AST-variant, exec, and Flutter project links back to this file for shared interpreter limits and documents only its own deltas.
- Issues — tracked interpreter issues and their status
Related packages (don't duplicate — follow the link):
- tom_d4rt_generator — automated
BridgedClass/BridgedEnumDefinitiongeneration from annotated source (user guide) - tom_d4rt_dcli — DCli REPL and
*.dcli.dartscript runner built on this interpreter - tom_d4rt_exec — see also: the analyzer-free counterpart of this package, for web / OTA embeddings (see Source-based vs analyzer-free)
Status #
Stable. Published at 1.8.23 on pub.dev.
The test suite covers 1,680+ tests (all passing; 2 known Won't Fix limitations for records with >9 positional fields and for spawning interpreted closures across isolate boundaries).
Repository: github.com/al-the-bear/tom_d4rt — tom_d4rt