blob: abde6d403140c899531926dc1fc762f669e920cb [file] [edit]
// Copyright (c) 2026, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
/// A simple calculator command-line program.
///
/// To demonstrate the value of dynamic modules, only addition is supported as a
/// built-in operation. The calculator is extensible and new operations can be
/// loaded dynamically through dynamic modules.
///
/// This library is the entrypoint of the host application. That means this code
/// is compiled ahead of time and optimized. Only declarations mentioned in
/// `dynamic_interface.yaml` are exposed to dynamic modules that can be loaded
/// later.
library;
import 'dart:convert';
import 'dart:io';
import 'dart:typed_data';
import 'package:dynamic_modules/dynamic_modules.dart';
import '../common/common.dart';
void main(List<String> args) async {
// Use a directory to find dynamic modules when requested.
if (args.length != 1) {
print('Usage: dart main.dart <modules_directory>');
exit(1);
}
final modulesDir = Directory(args[0]);
if (!modulesDir.existsSync()) {
print('Error: Directory ${modulesDir.path} does not exist.');
exit(1);
}
final registry = CalculatorRegistry();
registry.addOperation(AdditionOperation());
print('Calculator started. Type "help" for instructions.');
void showPrompt() {
stdout.write('\n > ');
stdout.flush();
}
showPrompt();
await for (String line
in stdin.transform(utf8.decoder).transform(const LineSplitter())) {
final trimmed = line.trim();
if (trimmed.isEmpty) continue;
if (trimmed == 'exit' || trimmed == 'quit') {
break;
}
if (trimmed == 'help') {
print('Instructions:');
print(' load <name> - Loads <modules_dir>/<name>.bytecode');
print(' <op> <arg1> <arg2> ... - Applies operation <op> to arguments');
print(' exit - Exits the calculator');
showPrompt();
continue;
}
final parts = trimmed.split(RegExp(r'\s+'));
final command = parts[0];
if (command == 'load') {
if (parts.length != 2) {
print('Error: Invalid load command. Usage: load <module_name>');
showPrompt();
continue;
}
final moduleName = parts[1];
// NOTE: A real application should have additional checks to ensure files
// are loaded from a safe location (e.g. safe folder, using TLS if loaded
// from the network, etc).
final file = File.fromUri(modulesDir.uri.resolve('$moduleName.bytecode'));
if (!file.existsSync()) {
print('Error: Module file ${file.uri} not found.');
showPrompt();
continue;
}
try {
final bytes = await file.readAsBytes();
// NOTE: this API is about to be removed and replaced with a different API
// that requires provenance validation.
final entrypointResult = await loadModuleFromBytes(
Uint8List.fromList(bytes),
);
// We established an implicit contract: dynamic modules for the
// calculator must return a function of this type.
if (entrypointResult is void Function(Registry)) {
entrypointResult(registry);
} else {
print(
'Error: Invalid module entrypoint result type: ${entrypointResult.runtimeType}',
);
}
} catch (e) {
print('Error loading module: $e');
}
showPrompt();
} else {
// Any other name is looked up as an operation in the registry.
final op = registry.operations[command];
if (op == null) {
print('Error: Unknown operation "$command"');
showPrompt();
continue;
}
final args = <num>[];
var valid = true;
for (var i = 1; i < parts.length; i++) {
final argStr = parts[i];
num? val = num.tryParse(argStr);
if (val == null) {
final constantOp = registry.operations[argStr];
if (constantOp != null) {
try {
val = constantOp.apply([]);
} catch (_) {}
}
}
if (val == null) {
print(
'Error: Invalid argument "$argStr", must be a number or constant.',
);
valid = false;
break;
}
args.add(val);
}
if (!valid) {
showPrompt();
continue;
}
try {
final result = op.apply(args);
print('Result: $result');
} catch (e) {
print('Error executing operation: $e');
}
showPrompt();
}
}
}
/// Registry where all operations will be accumulated.
///
/// This is the registry implementation that will be provided to dynamic moduels
/// to register their operations. Even though we only expose the abstract method
/// [Registry.addOperation], the compiler knows that the implementation in this
/// class is a potential target and must be treated as an exposed function of
/// this application.
class CalculatorRegistry implements Registry {
final Map<String, Operation> operations = {};
@override
void addOperation(Operation op) {
if (operations.containsKey(op.name)) {
print("Warning: Overwriting operation '${op.name}'");
}
operations[op.name] = op;
print("Registered operation: ${op.name}");
}
}
/// The built-in addition operation.
///
/// This is the only operation supported upfront before loading dynamic modules.
class AdditionOperation implements Operation {
@override
String get name => '+';
@override
num apply(List<num> arguments) {
return arguments.fold(0, (sum, val) => sum + val);
}
}