2025-09-22 11:35:18 +00:00
|
|
|
import 'dart:math' as Math;
|
2025-08-21 16:35:50 +00:00
|
|
|
|
|
|
|
|
import 'package:d4rt/d4rt.dart';
|
2026-02-26 19:03:19 +00:00
|
|
|
import 'package:get_it/get_it.dart';
|
|
|
|
|
import 'corpus.dart';
|
2025-08-21 16:35:50 +00:00
|
|
|
import 'formula_models.dart';
|
2026-02-09 15:57:53 +00:00
|
|
|
import 'error_handler.dart';
|
2026-02-26 19:03:19 +00:00
|
|
|
import 'd4rt_bridge.dart';
|
2025-08-21 16:35:50 +00:00
|
|
|
|
|
|
|
|
/// Exception thrown when formula evaluation fails
|
|
|
|
|
class FormulaEvaluationException implements Exception {
|
|
|
|
|
final String message;
|
|
|
|
|
final Object? cause;
|
|
|
|
|
|
|
|
|
|
const FormulaEvaluationException(this.message, [this.cause]);
|
|
|
|
|
|
|
|
|
|
@override
|
2026-03-04 21:24:37 +00:00
|
|
|
String toString() =>
|
|
|
|
|
'FormulaEvaluationException: $message'
|
2025-08-21 16:35:50 +00:00
|
|
|
'${cause != null ? ' (caused by: $cause)' : ''}';
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-04 21:24:37 +00:00
|
|
|
class MyMath {
|
2025-10-05 15:25:49 +00:00
|
|
|
static Number myLog(Number x) => Math.log(x);
|
2026-03-04 21:24:37 +00:00
|
|
|
|
|
|
|
|
static Number myPow(Number b, Number e) => Math.pow(b, e) as Number;
|
2025-09-22 11:35:18 +00:00
|
|
|
}
|
|
|
|
|
|
2026-03-04 21:24:37 +00:00
|
|
|
class FormulaResult {
|
2025-11-05 09:35:53 +00:00
|
|
|
const FormulaResult();
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-04 21:24:37 +00:00
|
|
|
class StringResult extends FormulaResult {
|
2025-11-05 09:35:53 +00:00
|
|
|
final String value;
|
2026-03-04 21:24:37 +00:00
|
|
|
|
2025-11-05 09:35:53 +00:00
|
|
|
const StringResult(this.value);
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-04 21:24:37 +00:00
|
|
|
class NumberResult extends FormulaResult {
|
2025-11-05 09:35:53 +00:00
|
|
|
final Number value;
|
2026-03-04 21:24:37 +00:00
|
|
|
|
2025-11-05 09:35:53 +00:00
|
|
|
const NumberResult(this.value);
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-21 16:35:50 +00:00
|
|
|
class FormulaEvaluator {
|
|
|
|
|
final D4rt _interpreter;
|
|
|
|
|
|
2025-10-05 15:25:49 +00:00
|
|
|
static D4rt createDefaultInterpreter() => D4rt();
|
|
|
|
|
|
2026-03-05 17:31:27 +00:00
|
|
|
FormulaEvaluator([D4rt? interpreter]) : _interpreter = interpreter ?? createDefaultInterpreter() {
|
2025-09-22 11:35:18 +00:00
|
|
|
prepareInterpreter(_interpreter);
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-04 21:24:37 +00:00
|
|
|
static Number _getNumberValueOf(String s) {
|
2025-09-22 11:35:18 +00:00
|
|
|
return double.parse(s);
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-04 21:24:37 +00:00
|
|
|
static void prepareInterpreter(D4rt interpreter) {
|
2025-09-22 11:35:18 +00:00
|
|
|
final myMathDefinition = BridgedClass(
|
|
|
|
|
nativeType: MyMath,
|
2025-10-05 15:25:49 +00:00
|
|
|
name: 'MyMath',
|
2025-09-22 11:35:18 +00:00
|
|
|
staticMethods: {
|
2025-10-05 15:25:49 +00:00
|
|
|
'myPow': (visitor, positionalArgs, namedArgs) {
|
2026-03-04 21:24:37 +00:00
|
|
|
final Number base = _getNumberValueOf(positionalArgs[0].toString());
|
|
|
|
|
final Number exp = _getNumberValueOf(positionalArgs[1].toString());
|
|
|
|
|
return MyMath.myPow(base, exp);
|
2025-09-22 11:35:18 +00:00
|
|
|
},
|
2025-10-05 15:25:49 +00:00
|
|
|
'myLog': (visitor, positionalArgs, namedArgs) {
|
2026-03-04 21:24:37 +00:00
|
|
|
final Number x = _getNumberValueOf(positionalArgs[0].toString());
|
2025-10-05 15:25:49 +00:00
|
|
|
return MyMath.myLog(x);
|
2025-09-22 15:00:34 +00:00
|
|
|
},
|
2026-03-04 21:24:37 +00:00
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
|
2026-03-05 17:31:27 +00:00
|
|
|
interpreter.registerBridgedClass(myMathDefinition, "package:d4rt_formulas.dart");
|
2026-02-26 19:03:19 +00:00
|
|
|
registerD4rtBridgeBridges(interpreter);
|
2025-09-22 11:35:18 +00:00
|
|
|
}
|
|
|
|
|
|
2025-11-05 09:35:53 +00:00
|
|
|
static FormulaResult evaluateExpression(String code, [D4rt? interpreter]) {
|
2025-10-15 08:10:29 +00:00
|
|
|
final d4rtInterpreter = interpreter ?? createDefaultInterpreter();
|
|
|
|
|
prepareInterpreter(d4rtInterpreter);
|
2026-03-04 21:24:37 +00:00
|
|
|
final d4rtCode =
|
|
|
|
|
"""
|
2026-02-26 19:03:19 +00:00
|
|
|
$preamble
|
2025-10-05 15:25:49 +00:00
|
|
|
main()
|
|
|
|
|
{
|
2025-10-13 14:31:26 +00:00
|
|
|
late var result;
|
|
|
|
|
result = $code;
|
|
|
|
|
return result;
|
2025-10-05 15:25:49 +00:00
|
|
|
}""";
|
2026-01-21 07:49:56 +00:00
|
|
|
print("evaluateExpression:\n$d4rtCode");
|
2025-10-15 08:10:29 +00:00
|
|
|
final result = d4rtInterpreter.execute(source: d4rtCode);
|
2026-03-04 21:24:37 +00:00
|
|
|
switch (result) {
|
2025-11-05 09:35:53 +00:00
|
|
|
case int value:
|
|
|
|
|
return NumberResult(value.toDouble());
|
|
|
|
|
case Number value:
|
|
|
|
|
return NumberResult(value);
|
|
|
|
|
case String value:
|
|
|
|
|
return StringResult(value);
|
|
|
|
|
default:
|
2026-03-05 17:31:27 +00:00
|
|
|
throw FormulaEvaluationException("Unexpected result type: ${result.runtimeType} -- $result");
|
2025-11-05 09:35:53 +00:00
|
|
|
}
|
2025-10-05 15:25:49 +00:00
|
|
|
}
|
2026-03-04 21:24:37 +00:00
|
|
|
|
2025-08-21 16:35:50 +00:00
|
|
|
dynamic evaluate(Formula formula, Map<String, dynamic> inputValues) {
|
|
|
|
|
_validateInputValues(formula, inputValues);
|
2025-09-22 15:00:34 +00:00
|
|
|
final completeSource = _buildCompleteSource(formula, inputValues);
|
2026-01-25 18:03:57 +00:00
|
|
|
try {
|
|
|
|
|
final result = _interpreter.execute(source: completeSource);
|
|
|
|
|
return result;
|
2026-03-04 21:24:37 +00:00
|
|
|
} catch (e, stack) {
|
2026-02-18 10:25:14 +00:00
|
|
|
// SPECIAL CASE: If the error message starts with signalMagicString, treat it as a signal message and return it instead of throwing an exception
|
|
|
|
|
// SEE signal() function in the generated d4rt code above for how this is used
|
2026-03-04 21:24:37 +00:00
|
|
|
if (e.toString().contains(signalMagicString)) {
|
2026-02-18 10:25:14 +00:00
|
|
|
final signalMessage = e.toString().split(signalMagicString).last.trim();
|
|
|
|
|
return signalMessage;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
errorHandler.notify("$e\n$completeSource", stack);
|
2026-03-05 17:31:27 +00:00
|
|
|
throw FormulaEvaluationException('Error evaluating formula "${formula.name}": $e', e);
|
2026-01-25 18:03:57 +00:00
|
|
|
}
|
2025-08-21 16:35:50 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void _validateInputValues(Formula formula, Map<String, dynamic> inputValues) {
|
|
|
|
|
final missingVars = <String>[];
|
2026-02-07 10:45:25 +00:00
|
|
|
|
2025-08-24 09:52:34 +00:00
|
|
|
for (final inputVar in formula.inputVarNames()) {
|
2025-08-21 16:35:50 +00:00
|
|
|
if (!inputValues.containsKey(inputVar)) {
|
|
|
|
|
missingVars.add(inputVar);
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-02-07 10:45:25 +00:00
|
|
|
|
2025-08-21 16:35:50 +00:00
|
|
|
if (missingVars.isNotEmpty) {
|
|
|
|
|
throw FormulaEvaluationException(
|
|
|
|
|
'Missing required input variables for formula "${formula.name}": '
|
|
|
|
|
'${missingVars.join(', ')}',
|
|
|
|
|
);
|
|
|
|
|
}
|
2026-02-07 10:45:25 +00:00
|
|
|
|
|
|
|
|
// Validate that input values are in the allowed values list if specified
|
|
|
|
|
for (final vs in formula.input) {
|
|
|
|
|
final values = vs.values;
|
|
|
|
|
if (values != null && values.isNotEmpty) {
|
|
|
|
|
final inputValue = inputValues[vs.name];
|
|
|
|
|
if (inputValue != null) {
|
|
|
|
|
// Convert input value to string for comparison since allowed values are stored as strings
|
|
|
|
|
final inputValueAsString = inputValue.toString();
|
2026-03-05 17:31:27 +00:00
|
|
|
final containsValue = values.any((allowedValue) => allowedValue.toString() == inputValueAsString);
|
2026-03-04 21:24:37 +00:00
|
|
|
|
2026-02-07 10:45:25 +00:00
|
|
|
if (!containsValue) {
|
|
|
|
|
throw FormulaEvaluationException(
|
|
|
|
|
'Invalid value for variable "${vs.name}" in formula "${formula.name}". '
|
|
|
|
|
'Expected one of: [${values.join(', ')}], but got: $inputValue',
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-08-21 16:35:50 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
List<String> getInputVariableOrder(Formula formula) {
|
2025-08-24 09:52:34 +00:00
|
|
|
return formula.inputVarNames()..sort();
|
2025-08-21 16:35:50 +00:00
|
|
|
}
|
|
|
|
|
|
2026-02-18 10:25:14 +00:00
|
|
|
static final String signalMagicString = "###";
|
|
|
|
|
|
2026-03-04 21:24:37 +00:00
|
|
|
static final String preamble =
|
|
|
|
|
"""
|
2026-02-26 19:03:19 +00:00
|
|
|
import 'dart:math';
|
|
|
|
|
import "package:d4rt_formulas.dart";
|
|
|
|
|
import "package:formulas/runtime_bridge.dart";
|
|
|
|
|
void signal( String msg ) => throw Exception("$signalMagicString\$msg");
|
|
|
|
|
dynamic fn(String formulaName, Map<String, dynamic> inputValues) => D4rtBridgeImpl.fn(formulaName, inputValues);
|
|
|
|
|
|
2026-02-18 08:46:02 +00:00
|
|
|
""";
|
|
|
|
|
|
2026-03-05 17:31:27 +00:00
|
|
|
static const reservedVariableNames = {"variableValues", "indexOf", "variableAllowedValues"};
|
2026-02-01 15:16:04 +00:00
|
|
|
|
2026-03-05 17:31:27 +00:00
|
|
|
String _buildCompleteSource(Formula formula, Map<String, dynamic> inputValues) {
|
2025-08-21 16:35:50 +00:00
|
|
|
final buffer = StringBuffer();
|
2025-08-24 10:08:10 +00:00
|
|
|
|
2025-09-22 15:00:34 +00:00
|
|
|
buffer.writeln("""
|
2026-02-26 19:03:19 +00:00
|
|
|
$preamble
|
2025-09-22 15:00:34 +00:00
|
|
|
main()
|
|
|
|
|
{
|
2026-03-04 21:24:37 +00:00
|
|
|
""");
|
2025-08-24 10:08:10 +00:00
|
|
|
|
2025-08-21 16:35:50 +00:00
|
|
|
for (final entry in inputValues.entries) {
|
|
|
|
|
final varName = entry.key;
|
|
|
|
|
final value = entry.value;
|
2026-03-04 21:24:37 +00:00
|
|
|
|
2025-08-21 16:35:50 +00:00
|
|
|
if (value is String) {
|
|
|
|
|
final escapedValue = value.replaceAll('"', '\\"');
|
2025-09-22 15:00:34 +00:00
|
|
|
buffer.writeln("""
|
|
|
|
|
final $varName = "$escapedValue";
|
|
|
|
|
""");
|
2025-08-21 16:35:50 +00:00
|
|
|
} else {
|
2025-09-22 15:00:34 +00:00
|
|
|
buffer.writeln("""
|
|
|
|
|
final $varName = $value;
|
|
|
|
|
""");
|
2025-08-21 16:35:50 +00:00
|
|
|
}
|
|
|
|
|
}
|
2026-01-31 18:53:12 +00:00
|
|
|
|
|
|
|
|
buffer.writeln("""
|
|
|
|
|
final variableValues = <String, dynamic>{
|
|
|
|
|
""");
|
|
|
|
|
for (final entry in inputValues.entries) {
|
|
|
|
|
final varName = entry.key;
|
|
|
|
|
final value = entry.value;
|
|
|
|
|
|
|
|
|
|
if (value is String) {
|
|
|
|
|
final escapedValue = value.replaceAll('"', '\\"');
|
|
|
|
|
buffer.writeln("""
|
|
|
|
|
"$varName": "$escapedValue",
|
|
|
|
|
""");
|
|
|
|
|
} else {
|
|
|
|
|
buffer.writeln("""
|
|
|
|
|
"$varName": "$value",
|
|
|
|
|
""");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
buffer.writeln("""
|
|
|
|
|
};
|
|
|
|
|
""");
|
|
|
|
|
|
2026-01-28 10:04:33 +00:00
|
|
|
// Build a Map<String, List<String>> named `variableValues` that exposes allowed values
|
|
|
|
|
// for each VariableSpec (inputs and output) to the interpreted code. Values are
|
|
|
|
|
// converted to strings and quoted in the produced d4rt source.
|
|
|
|
|
final variableValuesMap = <String, List<String>>{};
|
|
|
|
|
|
|
|
|
|
// Include input VariableSpecs when they have allowed values
|
|
|
|
|
for (final vs in formula.input) {
|
|
|
|
|
final values = vs.values;
|
|
|
|
|
if (values != null && values.isNotEmpty) {
|
2026-03-05 17:31:27 +00:00
|
|
|
variableValuesMap[vs.name] = values.map((v) => v.toString()).toList(growable: false);
|
2026-01-28 10:04:33 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// Explicitly include the output VariableSpec if it has allowed values
|
|
|
|
|
final outValues = formula.output.values;
|
|
|
|
|
if (outValues != null && outValues.isNotEmpty) {
|
2026-03-05 17:31:27 +00:00
|
|
|
variableValuesMap[formula.output.name] = outValues.map((v) => v.toString()).toList(growable: false);
|
2026-01-28 10:04:33 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Write the variableValues map into the generated source without escaping names/values
|
2026-01-31 18:53:12 +00:00
|
|
|
buffer.writeln("final variableAllowedValues = {");
|
2026-01-28 10:04:33 +00:00
|
|
|
variableValuesMap.forEach((name, list) {
|
|
|
|
|
final listLiteral = list.map((s) => '"' + s + '"').join(', ');
|
|
|
|
|
buffer.writeln(' "' + name + '": [' + listLiteral + '],');
|
|
|
|
|
});
|
|
|
|
|
buffer.writeln('};');
|
|
|
|
|
|
2026-01-31 18:53:12 +00:00
|
|
|
// Some functions to deal with string values
|
|
|
|
|
buffer.writeln("""
|
2026-02-01 15:16:04 +00:00
|
|
|
// If return type is int, there is an error converting double to int 🤷
|
|
|
|
|
dynamic indexOf(String inputName) {
|
2026-01-31 18:53:12 +00:00
|
|
|
String value = variableValues[inputName];
|
|
|
|
|
String allowedValues = variableAllowedValues[inputName];
|
2026-02-01 15:16:04 +00:00
|
|
|
dynamic ret = allowedValues.indexOf(value) as int;
|
|
|
|
|
return ret as int;
|
2026-01-31 18:53:12 +00:00
|
|
|
}
|
|
|
|
|
""");
|
|
|
|
|
|
2025-09-22 15:00:34 +00:00
|
|
|
buffer.writeln("""
|
2025-11-09 19:29:58 +00:00
|
|
|
late var ${formula.output.name};
|
2025-09-22 15:00:34 +00:00
|
|
|
${formula.d4rtCode}
|
2025-11-09 19:29:58 +00:00
|
|
|
return ${formula.output.name};
|
2025-09-22 15:00:34 +00:00
|
|
|
}
|
2026-01-28 10:04:33 +00:00
|
|
|
""");
|
|
|
|
|
|
2026-03-04 21:24:37 +00:00
|
|
|
return buffer.toString();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-05 17:31:27 +00:00
|
|
|
Number formulaSolver(
|
|
|
|
|
Formula formula,
|
|
|
|
|
String variableToSolve,
|
|
|
|
|
Map<String, dynamic> fixedInputValues, {
|
|
|
|
|
Number hint = 0,
|
|
|
|
|
Number step = 10,
|
|
|
|
|
Number maxDelta = 0.01,
|
|
|
|
|
int maxTries = 100,
|
|
|
|
|
}) {
|
|
|
|
|
if (variableToSolve == formula.output.name) {
|
2026-03-05 17:27:07 +00:00
|
|
|
return FormulaEvaluator().evaluate(formula, fixedInputValues);
|
|
|
|
|
}
|
2026-03-05 17:31:27 +00:00
|
|
|
|
|
|
|
|
if (!formula.inputVarNames().contains(variableToSolve)) {
|
2026-03-05 17:27:07 +00:00
|
|
|
throw ArgumentError(
|
2026-03-05 17:31:27 +00:00
|
|
|
'Variable "$variableToSolve" is not an input or output variable of the formula "${formula.name}".',
|
2026-03-05 17:27:07 +00:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
final modifiedInputValues = Map<String, dynamic>.from(fixedInputValues);
|
|
|
|
|
var evaluator = FormulaEvaluator();
|
|
|
|
|
Number f(Number x) {
|
|
|
|
|
modifiedInputValues[variableToSolve] = x;
|
|
|
|
|
final result = evaluator.evaluate(formula, modifiedInputValues);
|
|
|
|
|
if (result is Number) {
|
|
|
|
|
return result;
|
|
|
|
|
} else {
|
|
|
|
|
throw FormulaEvaluationException(
|
|
|
|
|
'Expected formula evaluation to return a number, but got: $result ${result.runtimeType}',
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var fixedFormulaOutput = fixedInputValues[formula.output.name];
|
|
|
|
|
|
|
|
|
|
return functionSolver(
|
|
|
|
|
(Number x) => f(x) - fixedFormulaOutput,
|
|
|
|
|
hint: hint,
|
|
|
|
|
step: step,
|
|
|
|
|
maxDelta: maxDelta,
|
|
|
|
|
maxTries: maxTries,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-04 21:24:37 +00:00
|
|
|
class NoSolutionException implements Exception {
|
|
|
|
|
final String message;
|
|
|
|
|
|
|
|
|
|
const NoSolutionException(this.message);
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
String toString() => 'NoSolutionException: $message';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Number functionSolver(
|
|
|
|
|
Number Function(Number) f, {
|
|
|
|
|
Number hint = 0,
|
|
|
|
|
Number step = 10,
|
|
|
|
|
Number maxDelta = 0.01,
|
|
|
|
|
int maxTries = 100,
|
|
|
|
|
}) {
|
|
|
|
|
Number sign(Number x) => switch (x) {
|
|
|
|
|
> 0 => 1,
|
|
|
|
|
< 0 => -1,
|
|
|
|
|
_ => 0,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
Number binarySearch(Number low, Number high) {
|
|
|
|
|
var yLow = f(low);
|
|
|
|
|
var yHigh = f(high);
|
|
|
|
|
assert(sign(yLow) != sign(yHigh));
|
|
|
|
|
|
|
|
|
|
int count = 0;
|
|
|
|
|
while ((high - low).abs() > maxDelta) {
|
|
|
|
|
count += 1;
|
|
|
|
|
if (count > maxTries) {
|
|
|
|
|
throw NoSolutionException("Failed to find a root after $maxTries tries.");
|
|
|
|
|
}
|
|
|
|
|
var mid = (low + high) / 2;
|
|
|
|
|
var yMid = f(mid);
|
|
|
|
|
if (sign(yMid) == sign(f(low))) {
|
|
|
|
|
low = mid;
|
|
|
|
|
yLow = yMid;
|
|
|
|
|
} else {
|
|
|
|
|
high = mid;
|
|
|
|
|
yHigh = yMid;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return (low + high) / 2;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
List<Number> searchApproximately(Number x1, Number x2) {
|
|
|
|
|
var y1 = f(x1);
|
|
|
|
|
var y2 = f(x2);
|
|
|
|
|
int count = 0;
|
|
|
|
|
while (sign(y1) == sign(y2)) {
|
|
|
|
|
count += 1;
|
|
|
|
|
if (count > maxTries) {
|
|
|
|
|
throw NoSolutionException("Failed to find a root after $maxTries tries.");
|
|
|
|
|
}
|
|
|
|
|
if (y1.abs() < y2.abs()) {
|
|
|
|
|
x2 = x1;
|
|
|
|
|
x1 = x1 - step;
|
|
|
|
|
y2 = y1;
|
|
|
|
|
y1 = f(x1);
|
|
|
|
|
} else {
|
|
|
|
|
x1 = x2;
|
|
|
|
|
x2 = x2 + step;
|
|
|
|
|
y1 = y2;
|
|
|
|
|
y2 = f(x2);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return [x1, x2];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var approx = searchApproximately(hint, hint + step);
|
|
|
|
|
return binarySearch(approx[0], approx[1]);
|
|
|
|
|
}
|