| // 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); | |
| } | |
| } |