diff --git a/Dockerfile b/Dockerfile index 773912d..534fd40 100644 --- a/Dockerfile +++ b/Dockerfile @@ -10,6 +10,16 @@ ENV PUB_CACHE=/cache/pub-cache ENV GRADLE_USER_HOME=/cache/gradle-cache RUN mkdir -p $PUB_CACHE $GRADLE_USER_HOME +# To avoid: fatal: detected dubious ownership in repository at '/sdks/flutter' +# Pass this during build: --build-arg USER_ID=$(id -u) --build-arg GROUP_ID=$(id -g) +ARG USER_ID +ARG GROUP_ID +RUN echo "Using UID: $USER_ID and GID: $GROUP_ID" +RUN chown -R $USER_ID:$GROUP_ID $PUB_CACHE $GRADLE_USER_HOME +RUN chown -R $USER_ID:$GROUP_ID /sdks/flutter + +USER $USER_ID:$GROUP_ID + # Copy pubspec files and get dependencies # Commented out to avoid building the app during image creation, this will be handled externally by makefile # COPY pubspec.yaml pubspec.lock ./ diff --git a/Makefile b/Makefile index 287ab74..a2e95d6 100644 --- a/Makefile +++ b/Makefile @@ -19,11 +19,14 @@ build-linux-debug-container: pub-get-container build-web-debug-container: pub-get-container ./docker-exec.sh exec flutter build web --debug -run-linux-debug-container: build-linux-debug-container +run-linux-debug-container: pub-get-container ./docker-exec.sh exec flutter run -d linux -run-linux-debug: build-linux-debug-container +run-web-debug-container: pub-get-container + ./docker-exec.sh exec flutter run --web-port $${WEB_PORT:-8081} -d web-server + +run-linux-debug-native: build-linux-debug-container ./build/linux/x64/debug/bundle/d4rt_formulas -run-web-debug-container: build-web-debug-container - cd build/web && python3 -m http.server 8080 +run-web-debug-native: build-web-debug-container + cd build/web && python3 -m http.server $${WEB_PORT:-8081} diff --git a/assets/formulas/formulas.d4rt b/assets/formulas/formulas.d4rt index 67ee938..7291928 100644 --- a/assets/formulas/formulas.d4rt +++ b/assets/formulas/formulas.d4rt @@ -132,9 +132,9 @@ Where: {"name": "Reflexes", "values": ["No response", "Grimace on aggressive stimulation", "Cry on stimulation"] }, {"name": "SkinColor", "values": ["Blue or pale", "Blue extremities, pink body", "Pink"] } ], - "output": {"name": "Result", "unit": "scalar"}, + "output": {"name": "Result", "unit": "string"}, "d4rtCode": """ - var total = HeartRate + Breathing + MuscleTone + Reflexes + SkinColor; + var total = indexOf("HeartRate") + indexOf("Breathing") + indexOf("MuscleTone") + indexOf("Reflexes") + indexOf("SkinColor"); late var interpretation; if( total < 4 ) { interpretation = 'Critical condition'; @@ -154,12 +154,12 @@ Where: "name": "Compare price per mass", "description": "Compares two products by their price per mass and returns which is cheaper.", "input": [ - {"name": "price1", "unit": "scalar"}, + {"name": "price1", "unit": "currency"}, {"name": "mass1", "unit": "kilogram"}, - {"name": "price2", "unit": "scalar"}, + {"name": "price2", "unit": "currency"}, {"name": "mass2", "unit": "kilogram"} ], - "output": {"name": "Result", "unit": "scalar"}, + "output": {"name": "Result", "unit": "string"}, "d4rtCode": """ var p1 = price1 / mass1; var p2 = price2 / mass2; diff --git a/assets/units/currency.d4rt.units b/assets/units/currency.d4rt.units new file mode 100644 index 0000000..82a454f --- /dev/null +++ b/assets/units/currency.d4rt.units @@ -0,0 +1,3 @@ +[ + {"name": "currency", "symbol": "¤", "isBase": true}, +] \ No newline at end of file diff --git a/assets/units/scalar.d4rt.units b/assets/units/scalar.d4rt.units index 0533d87..a235aff 100644 --- a/assets/units/scalar.d4rt.units +++ b/assets/units/scalar.d4rt.units @@ -1,3 +1,4 @@ [ - {"name": "scalar", "symbol": "", "isBase": true}, + {"name": "scalar", "symbol": "㊷", "isBase": true}, + {"name": "string", "symbol": "🔤", "isBase": true} ] \ No newline at end of file diff --git a/docker-exec.sh b/docker-exec.sh index 664b007..b5599ac 100755 --- a/docker-exec.sh +++ b/docker-exec.sh @@ -19,9 +19,12 @@ detect_container(){ fi } +clean_build_cache(){ + $DOCKER builder prune --all --force +} build_image(){ - $DOCKER build -t d4rt-formulas-builder -f Dockerfile . + $DOCKER build --build-arg USER_ID=$(id -u) --build-arg GROUP_ID=$(id -g) --progress=plain -t d4rt-formulas-builder -f Dockerfile . } graphic_options(){ @@ -45,19 +48,37 @@ graphic_options(){ fi } -exec_in_container(){ - local SPIOPTIONS="--env AT_SPI_BUS=/run/user/$(id -u)/at-spi/bus_0 --volume=/run/user/$(id -u)/at-spi:/run/user/$(id -u)/at-spi --device=/dev/dri" +spi_options(){ + if [ -e /run/user/$(id -u)/at-spi/bus_0 ] + then + printf " %s " "--env AT_SPI_BUS=/run/user/$(id -u)/at-spi/bus_0" + fi + if [ -e /run/user/$(id -u)/at-spi ] + then + printf " %s " "--volume=/run/user/$(id -u)/at-spi:/run/user/$(id -u)/at-spi" + fi + if [ -e /dev/dri ] + then + printf " %s " "--device /dev/dri" + fi +} + +exec_in_container(){ + SPIOPTIONS=$(spi_options) local GRAPHICOPTIONS=$(graphic_options) local BUILDCACHE=./.build-container-cache mkdir -p $BUILDCACHE $DOCKER run \ -it \ + --userns=keep-id \ + --user $(id -u):$(id -g) \ --init \ --rm \ $GRAPHICOPTIONS \ $SPIOPTIONS \ + -p ${WEBPORT:-8081}:8081 \ -v $BUILDCACHE:/cache:z \ -v .:/app:z \ -e FLUTTER_FLAVOR=prod \ @@ -73,12 +94,18 @@ main(){ return $? fi + if [ "$1" = "cleancache" ]; then + clean_build_cache + return $? + fi + + if [ "$1" = "exec" ]; then exec_in_container ${@:2} return $? fi - echo "Usage: $0 {build|exec }" + echo "Usage: $0 {build|cleancache|exec }" return 1 } diff --git a/lib/ai/formula_screen.dart b/lib/ai/formula_screen.dart index 5324954..6a35b9c 100644 --- a/lib/ai/formula_screen.dart +++ b/lib/ai/formula_screen.dart @@ -94,7 +94,6 @@ class _FormulaScreenState extends State { void _evaluateFormula() { print( "EVALUATE FORMULA"); - if (!_formKey.currentState!.validate()) return; try { final inputValues = {}; @@ -145,7 +144,7 @@ class _FormulaScreenState extends State { // Convert output to selected unit if needed String? unit = widget.formula.output.unit; - if (unit != null && unit is Number) { + if (unit != null && result is Number) { final converted = widget.corpus.convert(result, unit, _selectedOutputUnit!); if (converted is num) { _result = converted.toStringAsFixed(2); @@ -274,6 +273,7 @@ class _FormulaScreenState extends State { onUnitChanged: (unit) { _selectedOutputUnit = unit; _evaluateFormula(); + print( "En output unit changed to $unit: $_result"); setState(() { }); }, diff --git a/lib/defaults/default_corpus.dart b/lib/defaults/default_corpus.dart index af7f983..52d84f2 100644 --- a/lib/defaults/default_corpus.dart +++ b/lib/defaults/default_corpus.dart @@ -20,6 +20,7 @@ Future createDefaultCorpus() async{ final unitResources = [ "assets/units/angle.d4rt.units", "assets/units/area.d4rt.units", + "assets/units/currency.d4rt.units", "assets/units/distance.d4rt.units", "assets/units/energy.d4rt.units", "assets/units/force.d4rt.units", @@ -32,6 +33,7 @@ Future createDefaultCorpus() async{ ]; for (final unitRes in unitResources) { + print( "Loading units from $unitRes"); final literal = await loadResourceAsString(unitRes); final units = UnitSpec.fromArrayStringLiteral(literal); corpus.loadUnits(units); diff --git a/lib/formula_evaluator.dart b/lib/formula_evaluator.dart index 0532ee2..40e67f7 100644 --- a/lib/formula_evaluator.dart +++ b/lib/formula_evaluator.dart @@ -104,8 +104,9 @@ class FormulaEvaluator { final result = _interpreter.execute(source: completeSource); return result; } - catch (e) { + catch (e, stack) { print( "Error evaluating formula source:\n$completeSource" ); + print( stack ); throw FormulaEvaluationException( 'Error evaluating formula "${formula.name}": $e', e, @@ -141,6 +142,8 @@ class FormulaEvaluator { import "package:d4rt_formulas.dart"; """; + static const reservedVariableNames = { "variableValues", "indexOf", "variableAllowedValues"} ; + String _buildCompleteSource(Formula formula, Map inputValues) { final buffer = StringBuffer(); @@ -168,6 +171,29 @@ class FormulaEvaluator { """); } } + + buffer.writeln(""" + final variableValues = { + """); + 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(""" + }; + """); + // Build a Map> 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. @@ -187,13 +213,24 @@ class FormulaEvaluator { } // Write the variableValues map into the generated source without escaping names/values - buffer.writeln("final variableValues = {"); + buffer.writeln("final variableAllowedValues = {"); variableValuesMap.forEach((name, list) { final listLiteral = list.map((s) => '"' + s + '"').join(', '); buffer.writeln(' "' + name + '": [' + listLiteral + '],'); }); buffer.writeln('};'); + // Some functions to deal with string values + buffer.writeln(""" + // If return type is int, there is an error converting double to int 🤷‍ + dynamic indexOf(String inputName) { + String value = variableValues[inputName]; + String allowedValues = variableAllowedValues[inputName]; + dynamic ret = allowedValues.indexOf(value) as int; + return ret as int; + } + """); + buffer.writeln(""" late var ${formula.output.name}; ${formula.d4rtCode} diff --git a/lib/formula_models.dart b/lib/formula_models.dart index 9a0c5f6..bff597c 100644 --- a/lib/formula_models.dart +++ b/lib/formula_models.dart @@ -1,5 +1,6 @@ import 'package:d4rt/d4rt.dart'; import 'package:collection/collection.dart'; +import 'package:d4rt_formulas/d4rt_formulas.dart'; abstract class SetUtils { static Object safeGet(Map map, String key) { @@ -105,6 +106,13 @@ class VariableSpec { final List? values; VariableSpec({required this.name, this.unit, this.values}){ + validate(); + } + + void validate(){ + if( FormulaEvaluator.reservedVariableNames.contains(name) ){ + throw ArgumentError("$name: is a reserved variable name for FormulaEvaluator"); + } final valuesValid = values != null && values?.isNotEmpty == true; if( unit == null && !valuesValid ){ throw ArgumentError("$name: at least unit or allowedValues should be valid");