Browse Source

Merge branch 'master' into ast-parse-rework

Lucas de Souza 4 years ago
parent
commit
b2afb4a785
43 changed files with 2697 additions and 20432 deletions
  1. 3 0
      .eslintignore
  2. 17 0
      .eslintrc.json
  3. 21 1
      README.md
  4. BIN
      css/fonts/NimbusSanLConBold.ttf
  5. BIN
      css/fonts/NimbusSanLConRegular.ttf
  6. BIN
      css/fonts/texgyreheros-regular.otf
  7. 118 0
      css/ivprog-assessment.css
  8. 21 4
      css/ivprog-term.css
  9. 39 3
      css/ivprog-visual-1.0.css
  10. 3 1
      i18n/en/ui.json
  11. 3 1
      i18n/es/ui.json
  12. 6 3
      i18n/pt/error.json
  13. 6 2
      i18n/pt/message.json
  14. 12 1
      i18n/pt/ui.json
  15. 1 0
      img/empty.svg
  16. 2 1491
      js/Sortable.js
  17. 35 103
      js/assessment/ivprogAssessment.js
  18. 227 0
      js/assessment/output_matching/assessment_result.js
  19. 175 0
      js/assessment/output_matching/output_matching.js
  20. 22 0
      js/assessment/output_matching/output_result.js
  21. 104 30
      js/iassign-integration-functions.js
  22. 0 2
      js/jquery-3.3.1.min.js
  23. 0 18706
      js/jquery-ui.js
  24. 5 1
      js/main.js
  25. 2 2
      js/processor/definedFunctions.js
  26. 18 5
      js/processor/error/processorErrorFactory.js
  27. 9 9
      js/processor/ivprogProcessor.js
  28. 21 21
      js/processor/lib/math.js
  29. 17 12
      js/processor/semantic/semanticAnalyser.js
  30. 3 3
      js/runner.js
  31. 4 4
      js/services/localizedStringsService.js
  32. 4 3
      js/typeSystem/parsers.js
  33. 41 0
      js/util/auto_gen_output.js
  34. 135 0
      js/util/base64.js
  35. 28 0
      js/util/input_assessment.js
  36. 7 1
      js/util/outputTest.js
  37. 776 0
      js/util/string_diff.js
  38. 52 1
      js/util/utils.js
  39. 0 2
      js/visualUI/commands/generic_expression.js
  40. 28 17
      js/visualUI/functions.js
  41. 729 3
      package-lock.json
  42. 1 0
      package.json
  43. 2 0
      webpack.config.js

+ 3 - 0
.eslintignore

@@ -0,0 +1,3 @@
+/js/visualUI/
+/js/Sortable.js
+/js/iassign-integration-functions.js

+ 17 - 0
.eslintrc.json

@@ -0,0 +1,17 @@
+{
+    "env": {
+        "browser": true,
+        "es6": true
+    },
+    "extends": "eslint:recommended",
+    "globals": {
+        "Atomics": "readonly",
+        "SharedArrayBuffer": "readonly"
+    },
+    "parserOptions": {
+        "ecmaVersion": 2018,
+        "sourceType": "module"
+    },
+    "rules": {
+    }
+}

+ 21 - 1
README.md

@@ -1,5 +1,25 @@
 # iVProg
+
 Módulo interativo de aprendizagem para o ensino de lógica de programação desenvolvido pelo [LInE](https://usp.br/line)
 
 # Downloads
-As versão estáveis podem ser baixaas através do seguinte [link](http://200.144.254.107/release/ivprog). O projeto usa uma numeração de versão seguindo a formatação: YYYY-MM-DD_HH-MM-SS
+
+As versões estáveis podem ser baixadas através do seguinte [link](http://200.144.254.107/release/ivprog). O projeto usa uma numeração de versão seguindo a formatação: YYYY-MM-DD_HH-MM-SS.
+
+# Credits
+
+- "Empty" icon by amante de icono, from thenounproject.com
+
+# Desenvolvimento
+
+O projeto utiliza o npm como gerenciador de pacotes e ferramenta de construção. Para montar o programa a partir do código fonte é necessário ter instalado o Java (para o analisador léxico baseado em antlr4) e nodejs(^10.16.0) com npm(^6.9.0).
+Após clone este repositório execute os seguintes comandos a partir da pasta raiz:
+
+```
+npm install
+npm run build
+npm run start
+```
+
+Após a execução desses comandos, você poderá acessar o localhost na porta 8080 para acessar a sua versão local do iVProg.
+Existem também o comando _npm run watch_ para compilar os arquivos enquanto você faz modificações no código

BIN
css/fonts/NimbusSanLConBold.ttf


BIN
css/fonts/NimbusSanLConRegular.ttf


BIN
css/fonts/texgyreheros-regular.otf


+ 118 - 0
css/ivprog-assessment.css

@@ -0,0 +1,118 @@
+@font-face {
+  font-family: 'NimbusSanLConRegular';
+  src: url(fonts/NimbusSanLConRegular.ttf) format('truetype');
+  font-weight: normal;
+  font-style: normal;
+}
+@font-face {
+  font-family: 'NimbusSanLConBold';
+  src: url(fonts/NimbusSanLConBold.ttf) format('truetype');
+  font-weight: bold;
+  font-style: normal;
+}
+@font-face {
+  font-family: 'TeXGyreHerosRegular';
+  src: url(fonts/texgyreheros-regular.otf) format('opentype');
+  font-weight: normal;
+  font-style: normal;
+}
+body {
+  font-family: 'TeXGyreHerosRegular';
+  background-color: #b9c7ca;
+  margin-left: 2rem;
+}
+.details-body > .details-header > h2 {margin-bottom: 0.5rem;}
+.details-body > .details-header > p {
+  padding-left: 1rem;
+  margin: 0;
+}
+table td { font-size: 14pt;}
+.stringdiff-delete, .stringdiff-insert {font-weight: bold;}
+.stringdiff-insert, .assessment-input-read {color: #22a222}
+.stringdiff-delete {
+  text-decoration: line-through;
+  color: #d02929
+}
+.details-body h3 {
+  margin-top: 0.5rem;
+  margin-bottom: 0.5rem;
+}
+.detaisl-div-table {
+  padding-left: 1rem;
+  margin-top: 1rem;
+}
+.assessment-output-table {
+  border-collapse: collapse;
+  border-style: hidden;
+}
+.assessment-output-table tr:nth-child(odd) {background: #CCC}
+.assessment-output-table tr:nth-child(even) {background: #f7f2c9}
+.assessment-output-table tr > th {
+  background-color: #000;
+  color: #fff;
+  font-family: 'NimbusSansLBoldCond';
+  font-weight: bold;
+  font-style: normal; 
+  letter-spacing: .05rem;
+}
+.assessment-output-table td, .assessment-output-table th {
+  border-left: 1px solid black;
+  padding: 0.5rem;
+  text-align: center;
+  max-width: 16rem;
+  font-family: 'NimbusSanLConRegular';
+  vertical-align: middle;
+  letter-spacing: .05rem;
+}
+.assessment-string-expected, .assessment-string-generated, .assessment-string-diff {text-align: left;}
+p.assessment-failed-execution {padding-left: 1rem;}
+.assessment-failed-case {color:#FF1212}
+.assessment-input-unread {color: #d02929}
+.assessment-number-result-failed, .assessment-bool-result-failed {color: #d02929}
+.assessment-number-result, .assessment-bool-result, .assessment-string-result {color: #22a222}
+.assessment-popup {
+  position: relative;
+  display: inline-block;
+  cursor: pointer;
+}
+.assessment-popup .assessment-popuptext {
+  visibility: hidden;
+  width: 160px;
+  background-color: #555;
+  color: #fff;
+  text-align: center;
+  border-radius: 6px;
+  padding: 8px 0;
+  position: absolute;
+  z-index: 1;
+  bottom: 125%;
+  left: 50%;
+  margin-left: -80px;
+}
+.assessment-popup .assessment-popuptext::after {
+  content: "";
+  position: absolute;
+  top: 100%;
+  left: 50%;
+  margin-left: -5px;
+  border-width: 5px;
+  border-style: solid;
+  border-color: #555 transparent transparent transparent;
+}
+.assessment-popup:hover .assessment-popuptext {
+  visibility: visible;
+  -webkit-animation: fadeIn 1s;
+  animation: fadeIn 1s;
+}
+@-webkit-keyframes fadeIn {
+  from {opacity: 0;} 
+  to {opacity: 1;}
+}
+@keyframes fadeIn {
+  from {opacity: 0;}
+  to {opacity:1 ;}
+}
+.assessment-empty-output {
+  height: 1.5rem;
+  width: 1.5rem;
+}

+ 21 - 4
css/ivprog-term.css

@@ -10,15 +10,15 @@
 }
 
 .ivprog-term-userText, .ivprog-term-userInput {
-  color: white;
+  color: #f2d6d6;
 }
 
 .ivprog-term-info {
-  color: green;
+  color: #28a628;
 }
 
 .ivprog-term-error {
-  color: red;
+  color: #df4242;
 }
 
 .ivprog-term-input {
@@ -108,7 +108,7 @@
 .bash-body {
   /* margin: 0;
   padding: 5px; */
-  background: #141414;
+  background: #111010;
   /* list-style: none; */
   color: #F8F8FF;
   
@@ -175,4 +175,21 @@
   width: 0;
   height: 0;
   opacity: 0;
+}
+
+.ivprog-term-error > span > ul {
+  margin:0 !important;
+}
+
+.ivprog-term-error > span > ul > li {
+  padding: 0;
+  margin: 0;
+  line-height: 1rem;
+}
+
+.assessment-div-detail:hover {
+  cursor: pointer;
+}
+.assessment-div-detail:hover > span {
+  text-decoration: underline;
 }

+ 39 - 3
css/ivprog-visual-1.0.css

@@ -390,9 +390,6 @@ div.buttons_manage_columns {
 	padding: 3px;
 }
 
-.ui.button_add_case {
-	margin-top: 10px;
-}
 .accordion {
 	margin: auto;
 }
@@ -1018,4 +1015,43 @@ div.ui.checkbox.transition.visible {
 .circular.inverted.teal.question.icon {
 	font-size: 12px;
 	margin-left: 10px;
+}
+
+.table_buttons {
+	width: 100%;
+	margin-top: 10px;
+}
+.table_buttons .right_align {
+	text-align: right;
+}
+.text_area_output, .text_area_input {
+	width: 80%;
+}
+.tabular .item {
+	font-size: 1.1em;
+}
+.tabular.menu {
+	margin-top: 1em !important;
+}
+.ui.bottom.attached.tab.segment, .tabular.menu {
+	margin-left: 1em !important;
+	margin-right: 1em !important;
+}
+
+.ui.blue.table thead tr:first-child > th {
+     position: sticky !important;
+     top: 0;
+     z-index: 2;
+}
+
+.ui.bottom.attached.tab.segment.tab_algorithm {
+	height: 90%;
+}
+
+.ui.segment.settings_topic h3 {
+	margin-bottom: 20px;
+}
+
+.settings_topic .content_segment_settings {
+	margin-left: 40px;
 }

+ 3 - 1
i18n/en/ui.json

@@ -142,5 +142,7 @@
   "text_ivprog_version":"Version",
   "text_teacher_filter": "Filter",
   "text_teacher_filter_active": "Activate",
-  "text_teacher_filter_help": "When filter is activated, all iVProg modifications will be blocked."
+  "text_teacher_filter_help": "When filter is activated, all iVProg modifications will be blocked.",
+  "text_teacher_generate_outputs": "Generate outputs",
+  "text_teacher_generate_outputs_algorithm": "Before generate outputs, create an algorithm!"
 }

+ 3 - 1
i18n/es/ui.json

@@ -141,5 +141,7 @@
   "text_ivprog_version":"Version",
   "text_teacher_filter": "Filter",
   "text_teacher_filter_active": "Activate",
-  "text_teacher_filter_help": "When filter is activated, all iVProg modifications will be blocked."
+  "text_teacher_filter_help": "When filter is activated, all iVProg modifications will be blocked.",
+  "text_teacher_generate_outputs": "Generate outputs",
+  "text_teacher_generate_outputs_algorithm": "Before generate outputs, create an algorithm!"
 }

+ 6 - 3
i18n/pt/error.json

@@ -84,8 +84,9 @@
   "invalid_array_literal_column": "Esperava-se $0 colunas mas encontrou $1.",
   "exceeded_input_request": "A quantidade de leituras requisitadas execedeu a quantidade de entradas disponíveis.",
   "test_case_few_reads": "Caso de teste $0 falhou: ainda restam entradas!",
-  "test_case_failed": "Caso de teste $0 falhou: <ul> <li>entrada(s): $1</li> <li>saída(s) esperada(s): $2</li> <li>saída(s): $3</li></ul>",
-  "test_case_failed_exception": "Caso de teste $0 falhou: $1",
+  "test_case_failed": "<div class='assessment-div-detail' onClick='ivprogCore.openAssessmentDetail(event)' data-page=\"$1\"> <span>Caso de teste $0 não executou com sucesso.</span></div>",
+  "test_case_failed_exception": "<div class='assessment-div-detail' onClick='ivprogCore.openAssessmentDetail(event)' data-page=\"$2\"> <span>Caso de teste $0 falhou</span>: $1",
+  "test_case_exception": "Ocorreu uma exceção no caso de teste $0: $1",
   "invalid_type_conversion": "O valor $0 não pode ser convertido para o tipo $1",
   "invalid_read_type":"A entrada \"$0\" não é do tipo $1, que é o tipo da variável <span class='ivprog-error-varname'>$2</span>.",
   "invalid_read_type_array":"A entrada \"$0\" não é do tipo $1, que é o tipo aceito pela variável <span class='ivprog-error-varname'>$2</span> que é um $3.",
@@ -101,5 +102,7 @@
   "cannot_infer_matrix_column": "Não é possível inferir o número de colunas da matriz $0 na linha $1. É necessário que ela seja inicializada ou que o valor seja informado de forma explícita.",
   "cannot_infer_vector_size": "Não é possível inferir o número de elementos do vetor $0 na linha $1. É necessário que ele seja inicializado ou que o valor seja informado de forma explícita",
   "matrix_to_vector_attr":  "Erro na linha $0: $1 representa uma matriz e não pode ser atribuída ao vetor $2.",
-  "vector_to_matrix_attr":  "Erro na linha $0: $1 representa um vetor e não pode ser atribuído a matriz $2."
+  "vector_to_matrix_attr":  "Err na linha $0: $1 representa um vetor e não pode ser atribuído a matriz $2.",
+  "invalid_const_ref_full": "A variável $0 fornecida como parâmetro para a função $1 na linha $2 é uma constante e não pode ser usada neste contexto. Use uma variável ou posição de vetor.",
+  "invalid_const_ref": "A variável $0 fornecida como parâmetro para a função $1 é uma constante e não pode ser usada neste contexto. Use uma variável ou posição de vetor."
 }

+ 6 - 2
i18n/pt/message.json

@@ -1,6 +1,10 @@
 {
-  "test_case_success": "Caso de teste $0: OK",
+  "test_case_success": "<div class='assessment-div-detail' onClick='ivprogCore.openAssessmentDetail(event)' data-page=\"$1\"><span>Caso de teste $0</span>: OK</div>",
   "test_case_duration": "Levou $0ms",
   "test_suite_grade": "A sua solução alcançou $0% da nota.",
-  "awaiting_input_message": "O seu programa está em execução e aguardando uma entrada! Digite algo e pressione ENTER..."
+  "awaiting_input_message": "O seu programa está em execução e aguardando uma entrada! Digite algo e pressione ENTER...",
+  "assessment-empty-expected-tooltip": "A saída gerada foi além do esperado",
+  "assessment-empty-generated-tooltip": "O programa não gerou saídas suficientes",
+  "testcase_autogen_unused_input": "O caso de teste $0 possui mais entradas do que as leituras feitas no programa.",
+  "testcase_autogen_empty": "O caso de teste $0 não gerou qualquer saída."
 }

+ 12 - 1
i18n/pt/ui.json

@@ -115,5 +115,16 @@
   "text_ivprog_version":"Versão",
   "text_teacher_filter": "Filtro",
   "text_teacher_filter_active": "Ativado",
-  "text_teacher_filter_help": "Ao ativar o filtro, as modificações do iVProg estarão bloqueadas."
+  "text_teacher_filter_help": "Ao ativar o filtro, as modificações do iVProg estarão bloqueadas.",
+  "text_join_assessment_outputs": " ; ",
+  "assessment-detail-time-label": "Duração",
+  "assessment-detail-grade-label": "Nota",
+  "assessment-detail-input-label": "Entradas",
+  "assessment-detail-output-label": "Saídas",
+  "assessment-detail-expected-label": "Esperava",
+  "assessment-detail-generated-label": "Gerou",
+  "assessment-detail-result-label": "Resultado",
+  "assessment-detail-title": "Caso de Teste $0",
+  "text_teacher_generate_outputs": "Gerar saídas",
+  "text_teacher_generate_outputs_algorithm": "Antes de gerar as saídas, elabore um algoritmo!"
 }

File diff suppressed because it is too large
+ 1 - 0
img/empty.svg


File diff suppressed because it is too large
+ 2 - 1491
js/Sortable.js


+ 35 - 103
js/assessment/ivprogAssessment.js

@@ -1,12 +1,8 @@
-import { Decimal } from 'decimal.js';
 import line_i18n from 'line-i18n'
-import { SemanticAnalyser } from "./../processor/semantic/semanticAnalyser";
 import { IVProgProcessor } from "./../processor/ivprogProcessor";
-import { InputTest } from "./../util/inputTest";
-import { OutputTest } from "./../util/outputTest";
 import { DOMConsole} from "./../io/domConsole";
 import * as LocalizedStringsService from "../services/localizedStringsService";
-import { Config } from "../util/config";
+import { OutputMatching } from './output_matching/output_matching';
 
 
 const LocalizedStrings = LocalizedStringsService.getInstance();
@@ -15,31 +11,51 @@ const StringTypes = line_i18n.StringTypes;
 
 export class IVProgAssessment {
 
-  constructor (textCode, testCases, domConsole) {
-    this.textCode = textCode;
+  constructor (ast_code, testCases, domConsole) {
+    this.ast_code = ast_code;
     this.testCases = testCases;
     this.domConsole = domConsole;
   }
 
   runTest () {
-    const outerRef = this
+    const outerRef = this;
     try {
-      const validTree = SemanticAnalyser.analyseFromSource(this.textCode);
       // loop test cases and show messages through domconsole
       const partialTests = this.testCases.map( (t, name) => {
-        return outerRef.partialEvaluateTestCase(new IVProgProcessor(validTree), t.input, t.output, name);
+        return new OutputMatching(new IVProgProcessor(outerRef.ast_code), t.input, t.output, name);
       });
-      const testResult = partialTests.reduce((acc, curr) => acc.then(curr), Promise.resolve(0));
-      return testResult.then(function (total) {
-        const grade = total / outerRef.testCases.length;
+      const testResult = partialTests.map(om => om.eval());
+      return Promise.all(testResult).then(results => {
+        let grade = 0;
+        for(let i = 0; i < results.length; ++i) {
+          const result = results[i];
+          grade += result.grade;
+          if(result.grade == 1) {
+            outerRef.writeToConsole(DOMConsole.INFO, StringTypes.MESSAGE,'test_case_success',
+              result.name + 1, result.generateOutput());
+          } else if (result.status == 1) {
+            outerRef.writeToConsole(DOMConsole.ERR, StringTypes.ERROR,'test_case_failed_exception',
+              result.name + 1, result.error_msg, result.generateOutput());
+          } else {
+            outerRef.writeToConsole(DOMConsole.ERR, StringTypes.ERROR,'test_case_failed',
+              result.name + 1, result.generateOutput());
+          }
+        }
+        grade /= results.length;
         const channel = grade == 1 ? DOMConsole.INFO : DOMConsole.ERR;
-        outerRef.writeToConsole(channel, StringTypes.MESSAGE, "test_suite_grade", grade * 100);
-        return Promise.resolve(grade)
-      }).catch(err => {
-        outerRef.domConsole.err("Erro inesperado durante o cálculo da nota.");// try and show error messages through domconsole
-        outerRef.domConsole.err(err.message);
-        return Promise.resolve(0);
+        outerRef.writeToConsole(channel, StringTypes.MESSAGE, "test_suite_grade", (grade * 100).toFixed(2));
+        return grade;
       });
+      // return testResult.then(function (total) {
+      //   const grade = total / outerRef.testCases.length;
+      //   const channel = grade == 1 ? DOMConsole.INFO : DOMConsole.ERR;
+      //   outerRef.writeToConsole(channel, StringTypes.MESSAGE, "test_suite_grade", (grade * 100).toFixed(2));
+      //   return Promise.resolve(grade)
+      // }).catch(err => {
+      //   outerRef.domConsole.err("Erro inesperado durante o cálculo da nota.");// try and show error messages through domconsole
+      //   outerRef.domConsole.err(err.message);
+      //   return Promise.resolve(0);
+      // });
     } catch (error) {
       outerRef.domConsole.err("Erro inesperado durante a execução do programa");// try and show error messages through domconsole
       outerRef.domConsole.err(error.message);
@@ -47,90 +63,6 @@ export class IVProgAssessment {
     }
   }
 
-  evaluateTestCase (prog, inputList, expectedOutputs, name, accumulator) {
-    const outerThis = this;
-    const input = new InputTest(inputList);
-    const output = new OutputTest();
-    prog.registerInput(input);
-    prog.registerOutput(output);
-    const startTime = Date.now()
-    return prog.interpretAST().then( _ => {
-      const millis = Date.now() - startTime;
-      if (input.inputList.length !== input.index) {
-        outerThis.showErrorMessage('test_case_few_reads', name+1);
-        outerThis.showInfoMessage('test_case_duration', millis);
-        return Promise.resolve(accumulator + (input.index/inputList.length));
-      } else if (output.list.length != expectedOutputs.length) {
-        outerThis.showErrorMessage('test_case_failed', name + 1, inputList.join(','),
-          expectedOutputs.join(','), output.list.join(','));
-        outerThis.showInfoMessage('test_case_duration', millis);
-        // must check for a partial match of the generated output
-        const numMatchedOutputs = output.list.reduce((acc, actualOutput, index) => {
-          if(outerThis.checkOutputValues(actualOutput, expectedOutputs[index])) {
-            return acc + 1;
-          } else {
-            return acc;
-          }
-        }, 0);
-        const maxLength = Math.max(expectedOutputs.length, output.list.length);
-        return Promise.resolve(accumulator + (numMatchedOutputs/maxLength));
-      } else {
-        const isOk = outerThis.checkOutputLists(output.list, expectedOutputs);
-        if(!isOk) {
-          console.log("not ok.");
-          outerThis.showErrorMessage('test_case_failed', name + 1, inputList.join(','),
-            expectedOutputs.join(','), output.list.join(','));
-          outerThis.showInfoMessage('test_case_duration', millis);
-          return Promise.resolve(accumulator);
-        } else {
-          outerThis.showInfoMessage('test_case_success', name + 1);
-          outerThis.showInfoMessage('test_case_duration', millis);
-          return Promise.resolve(accumulator + 1);
-        }
-      }
-    }).catch( error => {
-      outerThis.showErrorMessage('test_case_failed_exception', name + 1, error.message);
-      return Promise.resolve(accumulator);
-    });
-  }
-
-  partialEvaluateTestCase (prog, inputList, expectedOutputs, name) {
-    return this.evaluateTestCase.bind(this, prog, inputList, expectedOutputs, name);
-  }
-
-  checkOutputLists (actualOutputs, expectedOutputs) {
-    for (let i = 0; i < actualOutputs.length; i++) {
-      const outValue = actualOutputs[i];
-      const expectedValue = expectedOutputs[i];
-      if(!this.checkOutputValues(outValue, expectedValue)) {
-        return false;
-      }
-    }
-    return true;
-  }
-
-  checkOutputValues (actualValue, expectedValue) {
-    let castNumberA = parseFloat(actualValue);
-    if(!Number.isNaN(castNumberA)) {
-      let castNumberB = parseFloat(expectedValue);
-      if(Number.isNaN(castNumberB)) {
-        return false;
-      }
-      castNumberA = new Decimal(castNumberA);
-      castNumberB = new Decimal(castNumberB);
-      const decimalPlaces = Math.min(castNumberB.dp(), Config.decimalPlaces);
-      castNumberA = new Decimal(castNumberA.toFixed(decimalPlaces, Decimal.ROUND_FLOOR));
-      castNumberB = new Decimal(castNumberB.toFixed(decimalPlaces, Decimal.ROUND_FLOOR));
-      const aEqualsB = castNumberA.eq(castNumberB);
-      if (!aEqualsB) {
-        return false;
-      }
-    } else if(actualValue != expectedValue) {
-      return false;
-    }
-    return true;
-  }
-
   showErrorMessage (errorID, ...args) {
     this.domConsole.err(LocalizedStrings.getError(errorID, args));
   }

+ 227 - 0
js/assessment/output_matching/assessment_result.js

@@ -0,0 +1,227 @@
+import StringDiff from "./../../util/string_diff";
+import { LocalizedStrings } from './../../services/localizedStringsService'
+
+export class OutputAssessmentResult {
+
+  static get PAGE_TEMPLATE () {
+    return `<!DOCTYPE html>
+    <html>
+      <head>
+        <meta http-equiv='content-type' content='text/html; charset=UTF-8'>
+        <link rel='stylesheet' href='css/ivprog-assessment.css' type='text/css'/>
+      </head>
+    <body>
+      :assessment-result:
+    </body>
+    </html>`;
+  }
+
+  static get DETAIL_TEMPLATE () {
+    return `<div class='details-body'>
+        <div class='details-header'>
+          <h2>:test-name:</h2>
+          <p>:time-label:: <span>:time:ms</span></p>
+          <p>:grade-label:: <span>:grade:%</span></p>
+        </div>
+        <div>
+          <h3>:input-label:</h3>
+          <ul>
+            <li>:input-list:</li>
+          </ul>
+        </div>
+        <div>
+          <h3>:output-label:</h3>
+          :output-result:
+        </div>
+      </div>`;
+  }
+
+  static get OUPUT_TABLE_TEMPLATE () {
+    return `<div class='detaisl-div-table'>
+        <table class='assessment-output-table'>
+          <tr>
+            <th>:expected-label:</th>
+            <th>:generated-label:</th>
+            <th>:result-label:</th>
+          </tr>
+          :results:
+        </table>
+      </div>`;
+  }
+
+  static get OUTPUT_TEMPLATE () {
+    return `<tr><td class=':class-expected:'>$0</td>
+            <td class=':class-generated:'>$1</td>
+            <td class=':class-result:'>$2</td></tr>`;
+  }
+
+  static get EMPTY_OUTPUT_TEMPLATE () {
+    return `<div class='assessment-popup'><img class='assessment-empty-output' src='img/empty.svg'>
+      <span class='assessment-popuptext'>$0</span></div>`;
+  }
+
+  static get FAILED_TEMPLATE () {
+    return `<p class='assessment-failed-execution'><span class='assessment-failed-case'>✗</span>$0</p>`;
+  }
+
+  static get INPUT_INFO_TEMPLATE () {
+    return `<span class='$0'>$1</span>`;
+  }
+
+  // Status code - it is not grade related!
+  // 0 - Succesful execution
+  // 1 - failed execution
+  constructor (name, status, inputs, result, store, time, error_msg = '') {
+    this.name = name;
+    this.status = status;
+    this.inputs = inputs;
+    this.results = result;
+    this.store = store;
+    this.time = time;
+    this.error_msg = error_msg;
+  }
+
+  get grade () {
+    if(this.results == null) {
+      return 0;
+    }
+    return this.results.reduce((prev, val) => prev + val.grade, 0) / this.results.length;
+  }
+
+  prepareResults () {
+    let template = OutputAssessmentResult.DETAIL_TEMPLATE;
+    const grade = (this.grade * 100).toFixed(2);
+    const time = this.time || "-";
+    template = template.replace(':test-name:', LocalizedStrings.getUI('assessment-detail-title', [this.name + 1]));
+    template = template.replace(':time-label:', LocalizedStrings.getUI('assessment-detail-time-label'));
+    template = template.replace(':time:', time);
+    template = template.replace(':grade-label:', LocalizedStrings.getUI('assessment-detail-grade-label'));
+    template = template.replace(':grade:', grade);
+    const input_spans = this.prepareInputList(this.inputs);
+    template = template.replace(':input-label:', LocalizedStrings.getUI('assessment-detail-input-label'));
+    template = template.replace(':input-list:', input_spans);
+    template = template.replace(':output-label:', LocalizedStrings.getUI('assessment-detail-output-label'));
+    if(this.status == 0) {
+      const output_rows = this.results.map(result => {
+        if(result.type == "string") {
+          return this.formatString(result);
+        } else if (result.type == "number") {
+          return this.formatNumber(result);
+        } else {
+          return this.formatBool(result);
+        }
+      }, this);
+      template = template.replace(':output-result:', this.prepareOutputTable(output_rows));
+    } else {
+      let failed_text =  OutputAssessmentResult.FAILED_TEMPLATE;
+      failed_text = failed_text.replace("$0", this.error_msg);
+      template = template.replace(":output-result:", failed_text);
+    }
+    return template;
+  }
+
+  prepareInputList (input_list) {
+    const list = input_list.map(input => {
+      let template = OutputAssessmentResult.INPUT_INFO_TEMPLATE;
+      template = template.replace("$1", input.value);
+      if(input.read) {
+        template = template.replace("$0", "assessment-input-read");
+      } else {
+        template = template.replace("$0", "assessment-input-unread");
+      }
+      return template;
+    }, this);
+    return list.join(LocalizedStrings.getUI('text_join_assessment_outputs'));
+  }
+
+  prepareOutputTable (output_rows) {
+    let template = OutputAssessmentResult.OUPUT_TABLE_TEMPLATE;
+    template = template.replace(':expected-label:', LocalizedStrings.getUI('assessment-detail-expected-label'));
+    template = template.replace(':generated-label:', LocalizedStrings.getUI('assessment-detail-generated-label'));
+    template = template.replace(':result-label:', LocalizedStrings.getUI('assessment-detail-result-label'));
+    template = template.replace(':results:', output_rows.join(''));
+    return template;
+  }
+
+  generateOutput () {
+    const assessment_result =  this.prepareResults();
+    let page = OutputAssessmentResult.PAGE_TEMPLATE;
+    page = page.replace(':assessment-result:', assessment_result);
+    page = page.replace(/(\r|\n|\t)/gm,'').replace(/> *</g, '><');
+    return page;
+  }
+
+  formatNumber (result) {
+    const result_class = result.grade == 1 ? 'assessment-number-result' : 'assessment-number-result-failed'; 
+    let template = this.formatOutput('assessment-number-expected',
+      'assessment-number-generated', result_class, result);
+    return template
+  }
+
+  formatBool (result) {
+    const result_class = result.grade == 1 ? 'assessment-bool-result' : 'assessment-bool-result-failed';
+    let template = this.formatOutput('assessment-bool-expected',
+      'assessment-bool-generated', result_class, result);
+    return template
+  }
+
+  formatOutput (expected_class, generated_class, result_class, result) {
+    let template = OutputAssessmentResult.OUTPUT_TEMPLATE;
+    template = template.replace(":class-expected:", expected_class);
+    template = template.replace(":class-generated:", generated_class);
+    template = template.replace(":class-result:", result_class);
+    let expected_tmpl = result.expected;
+    let generated_tmpl = result.generated;
+    if(expected_tmpl == null) {
+      expected_tmpl = OutputAssessmentResult.EMPTY_OUTPUT_TEMPLATE.replace('$0',
+        LocalizedStrings.getMessage('assessment-empty-expected-tooltip'));
+    } else if(generated_tmpl == null) {
+      generated_tmpl = OutputAssessmentResult.EMPTY_OUTPUT_TEMPLATE.replace('$0',
+        LocalizedStrings.getMessage('assessment-empty-generated-tooltip'));
+    }
+    template = template.replace("$0", expected_tmpl);
+    template = template.replace("$1", generated_tmpl);
+    const final_result = result.grade == 1 ? "✓" : "✗"
+    template = template.replace("$2", final_result);
+    return template
+  }
+
+  formatString (result) {
+    const expected_class = 'assessment-string-expected';
+    const generated_class = 'assessment-string-generated';
+    //const result_class = 'assessment-string-result';
+
+    let template = OutputAssessmentResult.OUTPUT_TEMPLATE;
+    template = template.replace(":class-expected:", expected_class);
+    template = template.replace(":class-generated:", generated_class);
+    //template = template.replace(":class-result:", result_class);
+
+    const g_string = result.generated || "";
+    const e_string = result.expected || "";
+    template = template.replace("$0", result.expected);
+    template = template.replace("$1", result.generated);
+    if(result.grade == 1) {
+      template = template.replace("$2", "✓");
+      template = template.replace(":class-result:", 'assessment-string-result');
+    } else {
+      const diff = StringDiff(g_string, e_string);
+      const diff_vec = diff.map(part => this.getDiffStringStyle(part[1], part[0]), this);
+      const diff_string = diff_vec.reduce((prev, actual) => prev + actual, "");
+      template = template.replace("$2", "<span class='assessment-failed-case'>✗</span>" + diff_string);
+      template = template.replace(":class-result:", "assessment-string-diff");
+    }
+    return template;
+  }
+
+  getDiffStringStyle (text, action) {
+    const template = "<span class='$0'>$1</span>"
+    switch(action) {
+      case StringDiff.INSERT:
+        return template.replace("$0", "stringdiff-insert").replace("$1", text);
+      case StringDiff.DELETE:
+        return template.replace("$0", "stringdiff-delete").replace("$1", text);
+      case StringDiff.EQUAL:
+        return template.replace("$0", "stringdiff-equal").replace("$1", text);
+    }
+  }
+}

+ 175 - 0
js/assessment/output_matching/output_matching.js

@@ -0,0 +1,175 @@
+import { Decimal } from 'decimal.js';
+import { InputAssessment } from "../../util/input_assessment";
+import { OutputTest } from "../../util/outputTest";
+import { Config } from "../../util/config";
+import { levenshteinDistance } from "../../util/utils";
+import { OutputAssessmentResult } from './assessment_result';
+import * as TypeParser from "./../../typeSystem/parsers";
+import * as LocalizedStringsService from "../../services/localizedStringsService";
+import * as OutputResult from "./output_result";
+
+const LocalizedStrings = LocalizedStringsService.getInstance();
+
+export class OutputMatching {
+
+  static get NUM_REGEX () {
+    return /^[+-]?([0-9]+([.][0-9]*)?(e[+-]?[0-9]+)?)$/;
+  } 
+
+  static get NUM_IN_STRING_REGEX () {
+    return /[+-]?([0-9]+([.][0-9]*)?(e[+-]?[0-9]+)?)/g;
+  }
+
+  static get BOOLEAN_REGEX () {
+    const str = `^(${LocalizedStrings.getUI("logic_value_true")}|${LocalizedStrings.getUI("logic_value_false")})$`;
+    return new RegExp(str);
+  }
+
+  static get BOOLEAN_IN_STRING_REGEX () {
+    const str = `(${LocalizedStrings.getUI("logic_value_true")}|${LocalizedStrings.getUI("logic_value_false")})`;
+    return new RegExp(str, 'g');
+  }
+  
+  constructor (program, input_list, expected_output, test_name) {
+    this.program = program;
+    this.name = test_name;
+    this.input_list = input_list;
+    this.expected_output = expected_output;
+  }
+
+  eval () {
+    const refThis = this;
+    const input = new InputAssessment(this.input_list);
+    const gen_output = new OutputTest();
+    this.program.registerInput(input);
+    this.program.registerOutput(gen_output);
+    const start_time = Date.now();
+    return this.program.interpretAST().then( sto => {
+      const final_time = Date.now() - start_time;
+      if(input.isInputAvailable()) {
+        return new OutputAssessmentResult(this.name, 1, input.input_list,
+          null, sto, final_time, refThis.getErrorMessage('test_case_few_reads', this.name+1))
+      }
+      const result = gen_output.list.map((g_out, i) => {
+        if(i >= this.expected_output.length) {
+          return new OutputResult.OutputMatchResult(null, g_out, 0, this.getPotentialOutputType(g_out));
+        }
+        return this.outputMatch(g_out, this.expected_output[i]);
+      }, this);
+      if(this.expected_output.length > gen_output.list.length) {
+        for(let i = gen_output.list.length; i < this.expected_output.length; ++i) {
+          const e_out = this.expected_output[i];
+          result.push(new OutputResult.OutputMatchResult(e_out, null, 0, this.getPotentialOutputType(e_out)));
+        }
+      }
+      return new OutputAssessmentResult(this.name, 0,  input.input_list, result, sto, final_time);
+    }).catch(error => {
+      return new OutputAssessmentResult(this.name, 1,  input.input_list, null, null,
+        null, refThis.getErrorMessage('test_case_exception', this.name + 1, error.message))
+    });
+  }
+
+  getPotentialOutputType (output) {
+    if(OutputMatching.NUM_REGEX.test(output)) {
+      return "number";
+    } else if (OutputMatching.BOOLEAN_REGEX.test(output)) {
+      return "bool";
+    } else {
+      return "string";
+    }
+  }
+
+  outputMatch (g_output, e_output) {
+    if(OutputMatching.NUM_REGEX.test(e_output)) {
+      if(!OutputMatching.NUM_REGEX.test(g_output)) {
+        return OutputResult.createNumberResult(e_output, g_output, 0);
+      }
+      const g_num = new Decimal(g_output);
+      const e_num = new Decimal(e_output);
+      return this.checkNumbers(g_num, e_num);
+    } else if (OutputMatching.BOOLEAN_REGEX.test(e_output)) {
+      if (!OutputMatching.BOOLEAN_REGEX.test(g_output)) {
+        return OutputResult.createBoolResult(e_output, g_output, 0);
+      }
+      const g_bool = TypeParser.toBool(g_output);
+      const e_bool = TypeParser.toBool(e_output);
+      return this.checkBoolean(g_bool, e_bool);
+    } else {
+      return this.checkStrings(g_output, e_output);
+    }
+  }
+
+  checkNumbers (g_num, e_num) {
+    const decimalPlaces = Math.min(e_num.dp(), Config.decimalPlaces);
+    g_num = new Decimal(g_num.toFixed(decimalPlaces, Decimal.ROUND_FLOOR));
+    e_num = new Decimal(e_num.toFixed(decimalPlaces, Decimal.ROUND_FLOOR));
+    const result = g_num.eq(e_num);
+    const grade = result ? 1 : 0;
+    return OutputResult.createNumberResult(e_num.toNumber(), g_num.toNumber(), grade);
+  }
+
+  checkBoolean (g_bool, e_bool) {
+    const grade = g_bool == e_bool ? 1 : 0;
+    const g_bool_text = TypeParser.convertBoolToString(g_bool);
+    const e_bool_text = TypeParser.convertBoolToString(e_bool);
+    return OutputResult.createBoolResult(e_bool_text, g_bool_text, grade);
+  }
+
+  checkStrings (g_output, e_ouput) {
+    const assessmentList = []
+    let e_output_clean = e_ouput;
+    let g_output_clean = g_output;
+    if (OutputMatching.NUM_IN_STRING_REGEX.test(e_ouput)) {
+      const expected_numbers = e_ouput.match(OutputMatching.NUM_IN_STRING_REGEX);
+      const generated_numbers = g_output.match(OutputMatching.NUM_IN_STRING_REGEX) || [];
+      const result = generated_numbers.map((val, i) => {
+        if(i >= expected_numbers.length) {
+          return OutputResult.createNumberResult(null, val, 0);
+        }
+        const g_val = new Decimal(val)
+        const e_val = new Decimal(expected_numbers[i]);
+        return this.checkNumbers(g_val, e_val);
+      }, this);
+      if(expected_numbers.length > generated_numbers.length) {
+        for(let i = generated_numbers.length; i < expected_numbers.length; ++i) {
+          result.push(OutputResult.createNumberResult(expected_numbers[i], null, 0));
+        }
+      }
+      e_output_clean = e_output_clean.replace(OutputMatching.NUM_IN_STRING_REGEX, '').trim();
+      g_output_clean = g_output_clean.replace(OutputMatching.NUM_IN_STRING_REGEX, '').trim();
+      const numberGrade = result.reduce((prev, r) => prev + r.grade, 0) / result.length;
+      assessmentList.push(numberGrade);
+    } 
+    if(OutputMatching.BOOLEAN_IN_STRING_REGEX.test(e_ouput)) {
+      const expected_bools = e_ouput.match(OutputMatching.BOOLEAN_IN_STRING_REGEX);
+      const generated_bools = g_output.match(OutputMatching.BOOLEAN_IN_STRING_REGEX) || [];
+      const result = generated_bools.map((val, i) => {
+        if(i >= expected_bools.length) {
+          return OutputResult.createBoolResult(null, val, 0);
+        }
+        const g_bool = TypeParser.toBool(val);
+        const e_bool = TypeParser.toBool(expected_bools[i]);
+        return this.checkBoolean(g_bool, e_bool );
+      }, this);
+      if(expected_bools.length > generated_bools.length) {
+        for(let i = generated_bools.length; i < expected_bools.length; ++i) {
+          result.push(OutputResult.createBoolResult(expected_bools[i], null, 0));
+        }
+      }
+      e_output_clean = e_output_clean.replace(OutputMatching.BOOLEAN_IN_STRING_REGEX, '').trim();
+      g_output_clean = g_output_clean.replace(OutputMatching.BOOLEAN_IN_STRING_REGEX, '').trim();
+      const boolGrade = result.reduce((prev, r) => prev + r.grade, 0) / result.length;
+      assessmentList.push(boolGrade);
+    }
+    const dist = levenshteinDistance(g_output_clean, e_output_clean);
+    const gradeDiff = Math.max(0, e_output_clean.length - dist)/e_output_clean.length;
+    const assessment_size = assessmentList.length + 1;
+    const gradeAcc = assessmentList.reduce((prev, val) => prev + val/assessment_size, 0);
+    const finalGrade = 1 * (gradeDiff/assessment_size + gradeAcc);
+    return OutputResult.createStringResult(e_ouput, g_output, finalGrade);
+  }
+  
+  getErrorMessage (errorID, ...args) {
+    return LocalizedStrings.getError(errorID, args);
+  }
+}

+ 22 - 0
js/assessment/output_matching/output_result.js

@@ -0,0 +1,22 @@
+
+export function createNumberResult (expected, generated, grade) {
+  return new OutputMatchResult(expected, generated, grade, "number");
+}
+
+export function createBoolResult (expected, generated, grade) {
+  return new OutputMatchResult(expected, generated, grade, "bool");
+}
+
+export function createStringResult (expected, generated, grade) {
+  return new OutputMatchResult(expected, generated, grade, "string");
+}
+
+export class OutputMatchResult {
+
+  constructor (expected, generated, grade, type) {
+    this.expected = expected;
+    this.generated = generated;
+    this.grade = grade;
+    this.type = type;
+  }
+}

+ 104 - 30
js/iassign-integration-functions.js

@@ -117,7 +117,10 @@ function getEvaluation () {
     // Observe que a chamada parte do iLM para o iTarefa
     //parent.getEvaluationCallback(window.studentGrade);
 
-    runCodeAssessment();
+    var canRunAssessment = runCodeAssessment();
+    if(canRunAssessment === -1) {
+      parent.getEvaluationCallback(-1);
+    }
   }
 }
 
@@ -398,11 +401,18 @@ function iassingIntegration () {
 // Função para preparar a interface para o professor criar atividade:
 function prepareActivityCreation () {
 
-  $('.add_accordion').addClass('accordion');
-
-  $('.default_visual_title').toggle();
-  $('.default_visual_title').append('<span>'+LocalizedStrings.getUI('text_teacher_algorithm')+'</span>');
-  $('.height_100').removeClass('height_100');
+  var menuTab = $('<div class="ui top attached tabular menu">'
+        + '<a class="item active" data-tab="testcases">' + LocalizedStrings.getUI('text_teacher_test_case') + '</a>'
+        + '<a class="item" data-tab="algorithm">' + LocalizedStrings.getUI('text_teacher_algorithm') + '</a>'
+        + '<a class="item" data-tab="settings">' + LocalizedStrings.getUI('text_teacher_config') + '</a>'
+        + '</div>'
+        + '<div class="ui bottom attached tab segment active tab_test_cases" data-tab="testcases"></div>'
+        + '<div class="ui bottom attached tab segment tab_algorithm" data-tab="algorithm"></div>'
+        + '<div class="ui bottom attached tab segment tab_settings" data-tab="settings"></div>');
+
+  menuTab.insertBefore('.add_accordion');
+  $('.tabular.menu .item').tab();
+  
   $('.main_title').remove();
   $('.ui.accordion').addClass('styled');
   
@@ -410,21 +420,21 @@ function prepareActivityCreation () {
 
   $('<div class="ui checkbox"><input type="checkbox" name="include_algo" class="include_algo" tabindex="0" class="hidden"><label>'+LocalizedStrings.getUI('text_teacher_algorithm_include')+'</label></div>').insertAfter('.content_margin');
   
-  var cases_test_div = $('<div class="ui accordion styled"><div class="active title"><i class="dropdown icon"></i>'+LocalizedStrings.getUI('text_teacher_test_case')+'</div><div class="active content"></div></div>');
-
-  cases_test_div.insertBefore('.accordion');
+  var cases_test_div = $('<div></div>');
 
-  var config_div = $('<div class="ui accordion styled"><div class="title"><i class="dropdown icon"></i>'+LocalizedStrings.getUI('text_teacher_config')+'</div><div class="content"></div></div>');
+  $('.tab_test_cases').append(cases_test_div);
 
-  config_div.insertAfter(cases_test_div);
+  var config_div = $('<div></div>');
 
-  $('.ui.accordion').accordion();
+  $('.tab_settings').append(config_div);
 
   $('.ui.checkbox').checkbox();
 
-  prepareTableSettings(config_div.find('.content'));
+  $('.tab_algorithm').append($('.add_accordion'));
 
-  prepareTableTestCases(cases_test_div.find('.content'));
+  prepareTableSettings(config_div);
+
+  prepareTableTestCases(cases_test_div);
 
   if (inIframe()) {
       $('.ui.styled.accordion').css('width', '96%');
@@ -438,11 +448,75 @@ function prepareTableTestCases (div_el) {
 
   div_el.append(table_el);
 
-  div_el.append('<button class="ui teal labeled icon button button_add_case"><i class="plus icon"></i>'+LocalizedStrings.getUI('text_teacher_test_case_add')+'</button>');
+  var table_buttons = '<table class="table_buttons"><tr><td>'
+    + '<button class="ui teal labeled icon button button_add_case"><i class="plus icon"></i>'+LocalizedStrings.getUI('text_teacher_test_case_add')+'</button>'
+    + '</td><td class="right_align">'
+    + '<button class="ui orange labeled icon button button_generate_outputs"><i class="sign-in icon"></i>'+LocalizedStrings.getUI('text_teacher_generate_outputs')+'</button>'
+    + '</td></tr></table>';
+
+  div_el.append(table_buttons);
+
+  div_el.append($('<div class="ui basic modal"><div class="content"><p>Olá</p></div><div class="actions"><div class="ui green ok inverted button">Fechar</div></div></div>'));
 
   $('.button_add_case').on('click', function(e) {
     addTestCase();
   });
+  $('.button_generate_outputs').on('click', function(e) {
+    generateOutputs();
+  });
+}
+
+function showAlert (msg) {
+  $('.ui.basic.modal .content').html('<h3>'+msg+'</h3>');
+  $('.ui.basic.modal').modal('show');
+}
+
+function generateOutputs () {
+  if (window.program_obj.functions.length == 1 && window.program_obj.functions[0].commands.length == 0) {
+    showAlert(LocalizedStrings.getUI('text_teacher_generate_outputs_algorithm'));
+    return;
+  }
+  // código:
+  var code_teacher = window.generator();
+  // array com as entradas já inseridas:
+  var test_cases = JSON.parse(prepareTestCases().replace('"testcases" :', ''));
+  ivprogCore.autoGenerateTestCaseOutput(code_teacher, test_cases).catch(function (error) {
+    showAlert("Houve um erro durante a execução do seu programa: "+error.message);
+  });
+  
+}
+
+function outputGenerated (test_cases) {
+  var fields = $('.text_area_output');
+  /*for (var i = 0; i < test_cases.length; i++) {
+    $(fields[i]).val('');
+    for (var j = 0; j < test_cases[i].output.length; j++) {
+      $(fields[i]).val($(fields[i]).val() + test_cases[i].output[j]);
+      if (j < test_cases[i].output.length - 1) {
+        $(fields[i]).val($(fields[i]).val() + '\n');
+      }
+    }
+    $(fields[i]).attr('rows', test_cases[i].output.length);
+  }*/
+  animateOutput(fields, test_cases, 0);
+
+  
+}
+
+function animateOutput (list, test_cases, index) {
+  if (list.length == index) return;
+  $(list[index]).val('');
+  for (var j = 0; j < test_cases[index].output.length; j++) {
+    $(list[index]).val($(list[index]).val() + test_cases[index].output[j]);
+    if (j < test_cases[index].output.length - 1) {
+      $(list[index]).val($(list[index]).val() + '\n');
+    }
+  }
+  $(list[index]).attr('rows', test_cases[index].output.length);
+
+  $(list[index]).effect('highlight', null, 50, function() {
+    animateOutput(list, test_cases, index + 1);
+  });
 }
 
 var hist = false;
@@ -517,23 +591,23 @@ function updateTestCaseCounter () {
 
 function prepareTableSettings (div_el) {
 
-  div_el.append('<h4 class="ui header">'+LocalizedStrings.getUI('text_config_programming')+'</h4>');
-  div_el.append('<form name="settings_programming_type"><div class="ui stackable five column grid">'
+  div_el.append('<div class="ui segment settings_topic"><h3 class="ui header"><i class="window maximize outline icon"></i><div class="content">'+LocalizedStrings.getUI('text_config_programming')+'</div></h3>'
+    +'<div class="content content_segment_settings"><form name="settings_programming_type"><div class="ui stackable five column grid">'
     +'<div class="column"><div class="ui radio"><input type="radio" name="programming_type" id="programming_textual" value="textual" tabindex="0" class="hidden small"><label for="programming_textual">'+LocalizedStrings.getUI('text_config_programming_textual')+'</label></div></div>'
     +'<div class="column"><div class="ui radio"><input type="radio" name="programming_type" id="programming_visual" value="visual" checked tabindex="0" class="hidden small"><label for="programming_visual">'+LocalizedStrings.getUI('text_config_programming_visual')+'</label></div></div>'
-    +'</div></form>');
+    +'</div></form></div></div>');
 
-  div_el.append('<h4 class="ui header">'+LocalizedStrings.getUI('text_teacher_data_types')+'</h4>');
-  div_el.append('<form name="settings_data_types"><div class="ui stackable five column grid">'
+  div_el.append('<div class="ui segment settings_topic"><h3 class="ui header"><i class="qrcode icon"></i><div class="content">'+LocalizedStrings.getUI('text_teacher_data_types')+'</div></h3>'
+    +'<div class="content content_segment_settings"><form name="settings_data_types"><div class="ui stackable five column grid">'
     +'<div class="column"><div class="ui checkbox"><input type="checkbox" name="integer_data_type" checked tabindex="0" class="hidden small"><label>'+LocalizedStrings.getUI('type_integer')+'</label></div></div>'
     +'<div class="column"><div class="ui checkbox"><input type="checkbox" name="real_data_type" checked tabindex="0" class="hidden small"><label>'+LocalizedStrings.getUI('type_real')+'</label></div></div>'
     +'<div class="column"><div class="ui checkbox"><input type="checkbox" name="text_data_type" checked tabindex="0" class="hidden small"><label>'+LocalizedStrings.getUI('type_text')+'</label></div></div>'
     +'<div class="column"><div class="ui checkbox"><input type="checkbox" name="boolean_data_type" checked tabindex="0" class="hidden small"><label>'+LocalizedStrings.getUI('type_boolean')+'</label></div></div>'
     +'<div class="column"><div class="ui checkbox"><input type="checkbox" name="void_data_type" checked tabindex="0" class="hidden small"><label>'+LocalizedStrings.getUI('type_void')+'</label></div></div>'
-    +'</div></form>');
+    +'</div></form></div></div>');
 
-  div_el.append('<h4 class="ui header">'+LocalizedStrings.getUI('text_teacher_commands')+'</h4>');
-  div_el.append('<form name="settings_commands"><div class="ui stackable three column grid">'
+  div_el.append('<div class="ui segment settings_topic"><h3 class="ui header"><i class="code icon"></i><div class="content">'+LocalizedStrings.getUI('text_teacher_commands')+'</div></h3>'
+    +'<div class="content content_segment_settings"><form name="settings_commands"><div class="ui stackable three column grid">'
     +'<div class="column"><div class="ui checkbox"><input type="checkbox" name="commands_read" checked tabindex="0" class="hidden small"><label>'+LocalizedStrings.getUI('text_read_var')+'</label></div></div>'
     +'<div class="column"><div class="ui checkbox"><input type="checkbox" name="commands_write" checked tabindex="0" class="hidden small"><label>'+LocalizedStrings.getUI('text_write_var')+'</label></div></div>'
     +'<div class="column"><div class="ui checkbox"><input type="checkbox" name="commands_comment" checked tabindex="0" class="hidden small"><label>'+LocalizedStrings.getUI('text_comment')+'</label></div></div>'
@@ -544,18 +618,18 @@ function prepareTableSettings (div_el) {
     +'<div class="column"><div class="ui checkbox"><input type="checkbox" name="commands_while" checked tabindex="0" class="hidden small"><label>'+LocalizedStrings.getUI('text_whiletrue')+'</label></div></div>'
     +'<div class="column"><div class="ui checkbox"><input type="checkbox" name="commands_dowhile" checked tabindex="0" class="hidden small"><label>'+LocalizedStrings.getUI('text_dowhiletrue')+'</label></div></div>'
     +'<div class="column"><div class="ui checkbox"><input type="checkbox" name="commands_switch" checked tabindex="0" class="hidden small"><label>'+LocalizedStrings.getUI('text_switch')+'</label></div></div>'
-    +'</div></form>');
+    +'</div></form></div></div>');
 
-  div_el.append('<h4 class="ui header">'+LocalizedStrings.getUI('text_teacher_functions')+'</h4>');
-  div_el.append('<form name="settings_functions"><div class="ui stackable one column grid">'
+  div_el.append('<div class="ui segment settings_topic"><h3 class="ui header"><i class="terminal icon"></i><div class="content">'+LocalizedStrings.getUI('text_teacher_functions')+'</div></h3>'
+    +'<div class="content content_segment_settings"><form name="settings_functions"><div class="ui stackable one column grid">'
     +'<div class="column"><div class="ui checkbox"><input type="checkbox" name="functions_creation" checked tabindex="0" class="hidden small"><label>'+LocalizedStrings.getUI('text_teacher_create_functions')+'</label></div></div>'
     +'<div class="column"><div class="ui checkbox"><input type="checkbox" name="functions_move" checked tabindex="0" class="hidden small"><label>'+LocalizedStrings.getUI('text_teacher_create_movement_functions')+'</label></div></div>'
-    +'</div></form>');
+    +'</div></form></div></div>');
 
-  div_el.append('<h4 class="ui header">'+LocalizedStrings.getUI('text_teacher_filter')+'<i class="circular inverted teal question icon"></i></h4>');
-  div_el.append('<form name="settings_filter"><div class="ui stackable one column grid">'
+  div_el.append('<div class="ui segment settings_topic"><h3 class="ui header"><i class="filter icon"></i><div class="content">'+LocalizedStrings.getUI('text_teacher_filter')+'</div><i class="circular inverted teal question icon"></i></h3>'
+    +'<div class="content content_segment_settings"><form name="settings_filter"><div class="ui stackable one column grid">'
     +'<div class="column"><div class="ui checkbox"><input type="checkbox" name="filter_active" tabindex="0" class="hidden small"><label>'+LocalizedStrings.getUI('text_teacher_filter_active')+'</label></div></div>'
-    +'</div></form>');
+    +'</div></form></div></div>');
 
   $('.circular.inverted.teal.question.icon').popup({
     content : LocalizedStrings.getUI("text_teacher_filter_help"),

File diff suppressed because it is too large
+ 0 - 2
js/jquery-3.3.1.min.js


File diff suppressed because it is too large
+ 0 - 18706
js/jquery-ui.js


+ 5 - 1
js/main.js

@@ -6,7 +6,9 @@ import * as LocalizedStringsService from './services/localizedStringsService';
 import { i18nHelper } from "./services/i18nHelper";
 import { ActionTypes, getLogs, getLogsAsString, registerClick, registerUserEvent, parseLogs } from "./services/userLog";
 import { prepareActivityToStudentHelper, autoEval } from "./util/iassignHelpers";
+import { openAssessmentDetail } from "./util/utils";
 import * as CodeEditorAll from "./visualUI/text_editor";
+import {autoGenerateTestCaseOutput} from './util/auto_gen_output';
 
 const CodeEditor = {
   setCode: CodeEditorAll.setCode,
@@ -36,5 +38,7 @@ export {
   registerUserEvent,
   parseLogs,
   ActionTypes,
-  CodeEditor
+  CodeEditor,
+  openAssessmentDetail,
+  autoGenerateTestCaseOutput
 }

+ 2 - 2
js/processor/definedFunctions.js

@@ -18,7 +18,7 @@ import {createAbsFun, createCosFun,
 
 function valueToKey (value, object) {
   for (const key in object) {
-    if(object.hasOwnProperty(key)){
+    if(Object.prototype.hasOwnProperty.call(object, key)){
       if (object[key] === value) {
         return key;
       }
@@ -32,7 +32,7 @@ function concatObjects (...objs) {
   for (let i = 0; i < objs.length; i++) {
     const obj = objs[i];
     for(const key in obj) {
-      if(obj.hasOwnProperty(key)) {
+      if(Object.prototype.hasOwnProperty.call(obj, key)) {
         result[key] = obj[key];
       }
     }

+ 18 - 5
js/processor/error/processorErrorFactory.js

@@ -1,6 +1,7 @@
 import { RuntimeError } from './runtimeError';
 import { SemanticError } from './semanticError';
 import * as  LocalizedStringsService from './../../services/localizedStringsService';
+import { LanguageDefinedFunction } from '../definedFunctions';
 
 const LocalizedStrings = LocalizedStringsService.getInstance();
 
@@ -269,16 +270,16 @@ export const ProcessorErrorFactory  = Object.freeze({
     const context = [id, expected, actual];
     return new SemanticError(LocalizedStrings.getError("invalid_parameters_size", context));
   },
-  invalid_parameter_type_full: (id, exp, sourceInfo) => {
+  invalid_parameter_type_full: (fun_name, exp, sourceInfo) => {
     if(sourceInfo) {
-      const context = [exp, id, sourceInfo.line];
+      const context = [exp, LanguageDefinedFunction.getLocalName(fun_name), sourceInfo.line];
       return new SemanticError(LocalizedStrings.getError("invalid_parameter_type_full", context));
     } else {
-      return ProcessorErrorFactory.invalid_parameter_type(id, exp);
+      return ProcessorErrorFactory.invalid_parameter_type(fun_name, exp);
     }
   },
-  invalid_parameter_type: (id, exp) => {
-    const context = [exp, id];
+  invalid_parameter_type: (fun_name, exp) => {
+    const context = [exp, LanguageDefinedFunction.getLocalName(fun_name)];
     return new SemanticError(LocalizedStrings.getError("invalid_parameter_type_full", context));
   },
   invalid_ref_full: (id, exp, sourceInfo) => {
@@ -386,5 +387,17 @@ export const ProcessorErrorFactory  = Object.freeze({
   invalid_read_type_array: (exp, typePos, dimPos, name, typeArray, dimArray) => {
     const context = [exp, LocalizedStrings.translateType(typePos, dimPos), name,LocalizedStrings.translateType(typeArray, dimArray)];
     return new RuntimeError(LocalizedStrings.getError("invalid_read_type_array", context))
+  },
+  invalid_const_ref_full: (fun_name, exp, sourceInfo)=> {
+    if(sourceInfo) {
+      const context = [exp, LanguageDefinedFunction.getLocalName(fun_name), sourceInfo.line];
+      return new SemanticError(LocalizedStrings.getError("invalid_const_ref_full", context));
+    } else {
+      return ProcessorErrorFactory.invalid_const_ref(fun_name, exp);
+    }
+  },
+  invalid_const_ref: (fun_name, exp) => {
+    const context = [exp, LanguageDefinedFunction.getLocalName(fun_name)];
+    return new SemanticError(LocalizedStrings.getError("invalid_const_ref", context));
   }
 });

+ 9 - 9
js/processor/ivprogProcessor.js

@@ -946,9 +946,9 @@ export class IVProgProcessor {
           return new StoreObject(resultType, left.value.minus(right.value));
         case Operators.MULT.ord: {
           result = left.value.times(right.value);
-          if(result.dp() > Config.decimalPlaces) {
-            result = new Decimal(result.toFixed(Config.decimalPlaces));
-          }
+          // if(result.dp() > Config.decimalPlaces) {
+          //   result = new Decimal(result.toFixed(Config.decimalPlaces));
+          // }
           return new StoreObject(resultType, result);
         }
         case Operators.DIV.ord: {
@@ -956,9 +956,9 @@ export class IVProgProcessor {
             result = left.value.divToInt(right.value);
           else
             result = left.value.div(right.value);
-          if(result.dp() > Config.decimalPlaces) {
-            result = new Decimal(result.toFixed(Config.decimalPlaces));
-          }
+          // if(result.dp() > Config.decimalPlaces) {
+          //   result = new Decimal(result.toFixed(Config.decimalPlaces));
+          // }
           return new StoreObject(resultType, result);
         }
         case Operators.MOD.ord: {
@@ -970,9 +970,9 @@ export class IVProgProcessor {
             rightValue = rightValue.trunc();
           }
           result = leftValue.modulo(rightValue);
-          if(result.dp() > Config.decimalPlaces) {
-            result = new Decimal(result.toFixed(Config.decimalPlaces));
-          }
+          // if(result.dp() > Config.decimalPlaces) {
+          //   result = new Decimal(result.toFixed(Config.decimalPlaces));
+          // }
           return new StoreObject(resultType, result);
         }          
         case Operators.GT.ord: {

+ 21 - 21
js/processor/lib/math.js

@@ -40,9 +40,9 @@ export function createSinFun () {
      } else {
        result = Decimal.sin(convertToRadians(angle));
      }
-     if(result.dp() > Config.decimalPlaces) {
-      result = new Decimal(result.toFixed(Config.decimalPlaces));
-    }
+    //  if(result.dp() > Config.decimalPlaces) {
+    //   result = new Decimal(result.toFixed(Config.decimalPlaces));
+    // }
      const temp = new StoreObject(Types.REAL, result);
      sto.mode = Modes.RETURN;
      return Promise.resolve(sto.updateStore('$', temp));
@@ -68,9 +68,9 @@ export function createCosFun () {
       result = new Decimal(0)
     }
     result = Decimal.cos(convertToRadians(angle));
-    if(result.dp() > Config.decimalPlaces) {
-      result = new Decimal(result.toFixed(Config.decimalPlaces));
-    }
+    // if(result.dp() > Config.decimalPlaces) {
+    //   result = new Decimal(result.toFixed(Config.decimalPlaces));
+    // }
     const temp = new StoreObject(Types.REAL, result);
     sto.mode = Modes.RETURN;
     return Promise.resolve(sto.updateStore('$', temp));
@@ -91,9 +91,9 @@ export function createTanFun () {
       return Promise.reject("Tangent of "+x.value.toNumber()+"° is undefined.");
     }
     let result = Decimal.tan(convertToRadians(angle));
-    if(result.dp() > Config.decimalPlaces) {
-      result = new Decimal(result.toFixed(Config.decimalPlaces));
-    }
+    // if(result.dp() > Config.decimalPlaces) {
+    //   result = new Decimal(result.toFixed(Config.decimalPlaces));
+    // }
     const temp = new StoreObject(Types.REAL, result);
     sto.mode = Modes.RETURN;
     return Promise.resolve(sto.updateStore('$', temp));
@@ -110,9 +110,9 @@ export function createSqrtFun () {
   const sqrtFun = (sto, _) => {
     const x = sto.applyStore('x');
     let result = x.value.sqrt();
-    if(result.dp() > Config.decimalPlaces) {
-      result = new Decimal(result.toFixed(Config.decimalPlaces));
-    }
+    // if(result.dp() > Config.decimalPlaces) {
+    //   result = new Decimal(result.toFixed(Config.decimalPlaces));
+    // }
     const temp = new StoreObject(Types.REAL, result);
     sto.mode = Modes.RETURN;
     return Promise.resolve(sto.updateStore('$', temp));
@@ -130,9 +130,9 @@ export function createPowFun () {
     const x = sto.applyStore('x');
     const y = sto.applyStore('y');
     let result = x.value.pow(y.value);
-    if(result.dp() > Config.decimalPlaces) {
-      result = new Decimal(result.toFixed(Config.decimalPlaces));
-    }
+    // if(result.dp() > Config.decimalPlaces) {
+    //   result = new Decimal(result.toFixed(Config.decimalPlaces));
+    // }
     const temp = new StoreObject(Types.REAL, result);
     sto.mode = Modes.RETURN;
     return Promise.resolve(sto.updateStore('$', temp));
@@ -153,9 +153,9 @@ export function createLogFun () {
       return Promise.reject(new Error("the value passed to log function cannot be negative"));
     }
     let result = Decimal.log10(x.value);
-    if(result.dp() > Config.decimalPlaces) {
-      result = new Decimal(result.toFixed(Config.decimalPlaces));
-    }
+    // if(result.dp() > Config.decimalPlaces) {
+    //   result = new Decimal(result.toFixed(Config.decimalPlaces));
+    // }
     const temp = new StoreObject(Types.REAL, result);
     sto.mode = Modes.RETURN;
     return Promise.resolve(sto.updateStore('$', temp));
@@ -205,9 +205,9 @@ export function createInvertFun () {
   const invertFun = (sto, _) => {
     const x = sto.applyStore('x');
     let result = toReal(1).dividedBy(x.value);
-    if(result.dp() > Config.decimalPlaces) {
-      result = new Decimal(result.toFixed(Config.decimalPlaces));
-    }
+    // if(result.dp() > Config.decimalPlaces) {
+    //   result = new Decimal(result.toFixed(Config.decimalPlaces));
+    // }
     const temp = new StoreObject(Types.REAL, result);
     sto.mode = Modes.RETURN;
     return Promise.resolve(sto.updateStore('$', temp));

+ 17 - 12
js/processor/semantic/semanticAnalyser.js

@@ -2,7 +2,7 @@ import { ProcessorErrorFactory } from './../error/processorErrorFactory';
 import { LanguageDefinedFunction } from './../definedFunctions';
 import { LanguageService } from './../../services/languageService';
 import { ArrayDeclaration, While, For, Switch, Assign, Break, IfThenElse, Return, ArrayIndexAssign } from '../../ast/commands';
-import { InfixApp, UnaryApp, FunctionCall, IntLiteral, RealLiteral, StringLiteral, BoolLiteral, VariableLiteral, ArrayLiteral, ArrayAccess } from '../../ast/expressions';
+import { InfixApp, UnaryApp, FunctionCall, IntLiteral, RealLiteral, StringLiteral, BoolLiteral, VariableLiteral, ArrayAccess } from '../../ast/expressions';
 import { Literal } from '../../ast/expressions/literal';
 import { resultTypeAfterInfixOp, resultTypeAfterUnaryOp } from '../compatibilityTable';
 import { Types } from '../../typeSystem/types';
@@ -66,13 +66,13 @@ export class SemanticAnalyser {
   findFunction (name) {
     if(name.match(/^\$.+$/)) {
       const fun = LanguageDefinedFunction.getFunction(name);
-      if(!!!fun) {
+      if(!fun) {
         throw ProcessorErrorFactory.not_implemented(name);
       }
       return fun;
     } else {
       const val = this.ast.functions.find( v => v.name === name);
-      if (!!!val) {
+      if (!val) {
         return null;
       }
       return val;
@@ -108,7 +108,7 @@ export class SemanticAnalyser {
 
     } else {
       if(declaration.initial === null) {
-        this.insertSymbol(declaration.id, {id: declaration.id, type: declaration.type});
+        this.insertSymbol(declaration.id, {id: declaration.id, type: declaration.type, isConst: declaration.isConst});
         return;
       }
       const resultType = this.evaluateExpressionType(declaration.initial);
@@ -118,7 +118,7 @@ export class SemanticAnalyser {
           const info = stringInfo[0];
           throw ProcessorErrorFactory.incompatible_types_full(info.type, info.dim, declaration.sourceInfo);
         }
-        this.insertSymbol(declaration.id, {id: declaration.id, type: declaration.type})
+        this.insertSymbol(declaration.id, {id: declaration.id, type: declaration.type, isConst: declaration.isConst})
       } else if((!declaration.type.isCompatible(resultType) && !Config.enable_type_casting)
         || (!declaration.type.isCompatible(resultType) && Config.enable_type_casting
         && !Store.canImplicitTypeCast(declaration.type, resultType))) {
@@ -126,7 +126,7 @@ export class SemanticAnalyser {
         const info = stringInfo[0];
         throw ProcessorErrorFactory.incompatible_types_full(info.type, info.dim, declaration.sourceInfo);
       } else {
-        this.insertSymbol(declaration.id, {id: declaration.id, type: declaration.type});
+        this.insertSymbol(declaration.id, {id: declaration.id, type: declaration.type, isConst: declaration.isConst});
       }
     }
   }
@@ -491,10 +491,15 @@ export class SemanticAnalyser {
     for (let i = 0; i < actualParametersList.length; ++i) {
       const param = actualParametersList[i];
       const formalParam = fun.formalParameters[i];
-      const id = formalParam.id;
+      // const id = formalParam.id;
       if(formalParam.byRef) {
-        if (!(param instanceof VariableLiteral || param instanceof ArrayAccess)) {
-          throw ProcessorErrorFactory.invalid_parameter_type_full(id, param.toString(), param.sourceInfo);
+        if(param instanceof VariableLiteral) {
+          const variable = this.findSymbol(param.id, this.symbolMap);
+          if (variable.isConst) {
+            throw ProcessorErrorFactory.invalid_const_ref_full(fun.name, param.toString(), param.sourceInfo);  
+          }
+        } else if (!(param instanceof VariableLiteral || param instanceof ArrayAccess)) {
+          throw ProcessorErrorFactory.invalid_parameter_type_full(fun.name, param.toString(), param.sourceInfo);
         }
       }
       const resultType = this.evaluateExpressionType(param);
@@ -514,7 +519,7 @@ export class SemanticAnalyser {
               }
             }
           }
-          throw ProcessorErrorFactory.invalid_parameter_type_full(id, param.toString(), param.sourceInfo);
+          throw ProcessorErrorFactory.invalid_parameter_type_full(fun.name, param.toString(), param.sourceInfo);
         }
       } else if (resultType instanceof MultiType) {
         if(!resultType.isCompatible(formalParam.type)) {
@@ -525,7 +530,7 @@ export class SemanticAnalyser {
               }
             }
           }
-          throw ProcessorErrorFactory.invalid_parameter_type_full(id, param.toString(), param.sourceInfo);
+          throw ProcessorErrorFactory.invalid_parameter_type_full(fun.name, param.toString(), param.sourceInfo);
         }
       } else if(!formalParam.type.isCompatible(resultType)) {
         if(Config.enable_type_casting && !formalParam.byRef) {
@@ -533,7 +538,7 @@ export class SemanticAnalyser {
             continue;
           }
         }
-        throw ProcessorErrorFactory.invalid_parameter_type_full(id, param.toString(), param.sourceInfo);
+        throw ProcessorErrorFactory.invalid_parameter_type_full(fun.name, param.toString(), param.sourceInfo);
       }
 
     }

+ 3 - 3
js/runner.js

@@ -19,15 +19,15 @@ const ivprogLexer = LanguageService.getCurrentLexer();
 //     i++;
 // }
 // const anaSin = new IVProgParser(input, ivprogLexer);
-const editor = new JsonEditor('#json-renderer', {});
+const editor = new window.JsonEditor('#json-renderer', {});
 const domConsole = new DOMConsole("#console", true);
 domConsole.hide();
 // proc.interpretAST().then( sto => {
 //   console.log(sto.applyStore('a'));
 // }).catch(e => console.log(e));
 try {
-  $('#btn').click( () => {
-    const input = $('#input').val();
+  window.$('#btn').click( () => {
+    const input = window.$('#input').val();
     const analiser = new IVProgParser(input, ivprogLexer);
     try {
       const data = analiser.parseTree();

+ 4 - 4
js/services/localizedStringsService.js

@@ -11,15 +11,15 @@ class IVProgLocalizedStrings extends line_i18n.LocalizedStrings {
   }
 
   translateType (type, dim) {
+    const type_string = this.getUI(`type_${type}`);
     switch (dim) {
       case 0:
-        return this.getUI(`type_${type}`);
+        return type_string;
       default:
-        const transType = this.getUI(`type_${type}`);
         if(dim === 1)
-          return this.getUI("matrix_info_string", [transType])
+          return this.getUI("matrix_info_string", [type_string])
         else
-          return this.getUI("vector_info_string", [transType])
+          return this.getUI("vector_info_string", [type_string])
     }
   }
   

+ 4 - 3
js/typeSystem/parsers.js

@@ -1,6 +1,7 @@
 import { LanguageService } from "../services/languageService";
 import { Types } from "./types";
 import Decimal from "decimal.js";
+import { Config } from "../util/config";
 
 export function toInt (str) {
   return new Decimal(str);
@@ -13,8 +14,8 @@ export function toString (str) {
   value = value.replace(/\\t/g, "\t");
   value = value.replace(/\\n/g, "\n");
   value = value.replace(/\\r/g, "\r");
-  value = value.replace(/\\\"/g, "\"");
-  value = value.replace(/\\\'/g, "\'");
+  value = value.replace(/\\"/g, "\"");
+  value = value.replace(/\\'/g, '\'');
   value = value.replace(/\\\\/g, "\\");
   return value;
 }
@@ -57,7 +58,7 @@ export function convertToString(value, type) {
       if (value.dp() <= 0) {
         return value.toFixed(1);  
       } else {
-        return value.toNumber();
+        return value.toFixed(Config.decimalPlaces);
       }
     }
     case Types.BOOLEAN.ord:

+ 41 - 0
js/util/auto_gen_output.js

@@ -0,0 +1,41 @@
+import {SemanticAnalyser} from './../processor/semantic/semanticAnalyser';
+import {IVProgProcessor} from './../processor/ivprogProcessor';
+import { InputTest } from './inputTest';
+import { OutputTest } from './outputTest';
+import { LocalizedStrings } from './../services/localizedStringsService'
+
+export function autoGenerateTestCaseOutput (program_text, testCases) {
+  let copyTestCases = testCases.map((test) => Object.assign({}, test));
+  try {
+    const program = SemanticAnalyser.analyseFromSource(program_text);
+    const resultList = testCases.map((test, id) => {
+      const input = new InputTest(test.input);
+      const output = new OutputTest();
+      const exec =  new IVProgProcessor(program);
+      exec.registerInput(input);
+      exec.registerOutput(output);
+      return exec.interpretAST().then(_ => {
+        return {id: id, program: exec};
+      });
+    });
+    return Promise.all(resultList).then(result_final => {
+      for(let i = 0; i < result_final.length; ++i) {
+        const result = result_final[i];
+        const output = result.program.output.list;
+        const input = result.program.input;
+        if(input.index != input.inputList.length) {
+          window.showAlert(LocalizedStrings.getMessage("testcase_autogen_unused_input", [result.id+1]));
+          return Promise.resolve(false);
+        }
+        if(output.length == 0) {
+          window.showAlert(LocalizedStrings.getMessage("testcase_autogen_empty", [result.id+1]));
+        }
+        copyTestCases[result.id].output = output;
+      }
+      window.outputGenerated(copyTestCases);
+      return Promise.resolve(true);
+    });
+  }catch (error) {
+    return Promise.reject(error)
+  }
+}

+ 135 - 0
js/util/base64.js

@@ -0,0 +1,135 @@
+/**
+*
+*  Base64 encode / decode
+*  Source: http://www.webtoolkit.info/
+*  Modified by: @lucascalion - 24/07/2019
+*
+**/
+
+const _keyStr = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
+  
+export function encode (input) {
+      let output = "";
+      let chr1, chr2, chr3, enc1, enc2, enc3, enc4;
+      let i = 0;
+  
+      input = _utf8_encode(input);
+  
+      while (i < input.length) {
+  
+          chr1 = input.charCodeAt(i++);
+          chr2 = input.charCodeAt(i++);
+          chr3 = input.charCodeAt(i++);
+  
+          enc1 = chr1 >> 2;
+          enc2 = ((chr1 & 3) << 4) | (chr2 >> 4);
+          enc3 = ((chr2 & 15) << 2) | (chr3 >> 6);
+          enc4 = chr3 & 63;
+  
+          if (isNaN(chr2)) {
+              enc3 = enc4 = 64;
+          } else if (isNaN(chr3)) {
+              enc4 = 64;
+          }
+  
+          output = output +
+            _keyStr.charAt(enc1) + _keyStr.charAt(enc2) +
+            _keyStr.charAt(enc3) + _keyStr.charAt(enc4);
+  
+      }
+  
+      return output;
+}
+
+export function decode (input) {
+      let output = "";
+      let chr1, chr2, chr3;
+      let enc1, enc2, enc3, enc4;
+      let i = 0;
+  
+      input = input.replace(/[^A-Za-z0-9+/=]/g, "");
+  
+      while (i < input.length) {
+  
+          enc1 = _keyStr.indexOf(input.charAt(i++));
+          enc2 = _keyStr.indexOf(input.charAt(i++));
+          enc3 = _keyStr.indexOf(input.charAt(i++));
+          enc4 = _keyStr.indexOf(input.charAt(i++));
+  
+          chr1 = (enc1 << 2) | (enc2 >> 4);
+          chr2 = ((enc2 & 15) << 4) | (enc3 >> 2);
+          chr3 = ((enc3 & 3) << 6) | enc4;
+  
+          output = output + String.fromCharCode(chr1);
+  
+          if (enc3 != 64) {
+              output = output + String.fromCharCode(chr2);
+          }
+          if (enc4 != 64) {
+              output = output + String.fromCharCode(chr3);
+          }
+  
+      }
+  
+      output = _utf8_decode(output);
+  
+      return output;
+  
+}
+
+function  _utf8_encode (string) {
+      string = string.replace(/\r\n/g,"\n");
+      let utftext = "";
+  
+      for (let n = 0; n < string.length; n++) {
+  
+          const c = string.charCodeAt(n);
+  
+          if (c < 128) {
+              utftext += String.fromCharCode(c);
+          }
+          else if((c > 127) && (c < 2048)) {
+              utftext += String.fromCharCode((c >> 6) | 192);
+              utftext += String.fromCharCode((c & 63) | 128);
+          }
+          else {
+              utftext += String.fromCharCode((c >> 12) | 224);
+              utftext += String.fromCharCode(((c >> 6) & 63) | 128);
+              utftext += String.fromCharCode((c & 63) | 128);
+          }
+  
+      }
+  
+      return utftext;
+}
+
+function _utf8_decode (utftext) {
+      let string = "";
+      let i = 0;
+      let c, c2, c3;
+      c = c2 = c3 = 0;
+  
+      while ( i < utftext.length ) {
+  
+          c = utftext.charCodeAt(i);
+  
+          if (c < 128) {
+              string += String.fromCharCode(c);
+              i++;
+          }
+          else if((c > 191) && (c < 224)) {
+              c2 = utftext.charCodeAt(i+1);
+              string += String.fromCharCode(((c & 31) << 6) | (c2 & 63));
+              i += 2;
+          }
+          else {
+              c2 = utftext.charCodeAt(i+1);
+              c3 = utftext.charCodeAt(i+2);
+              string += String.fromCharCode(((c & 15) << 12) | ((c2 & 63) << 6) | (c3 & 63));
+              i += 3;
+          }
+  
+      }
+  
+      return string;
+}

+ 28 - 0
js/util/input_assessment.js

@@ -0,0 +1,28 @@
+import { Input } from './../io/input';
+import { LocalizedStrings } from '../services/localizedStringsService';
+
+export class InputAssessment extends Input {
+
+  constructor (input_list) {
+    super();
+    this.index = 0;
+    this.input_list = input_list.map((val) => {
+      return {"value": val, "read": false};
+    });
+  }
+
+  requestInput (callback) {
+    if(this.index < this.input_list.length) {
+      const input = this.input_list[this.index];
+      input.read = true;
+      this.index += 1;
+      callback(input.value);
+    } else {
+      throw new Error(LocalizedStrings.getError("exceeded_input_request"));
+    }
+  }
+
+  isInputAvailable () {
+    return this.index < this.input_list.length;
+  }
+}

+ 7 - 1
js/util/outputTest.js

@@ -8,6 +8,12 @@ export class OutputTest extends Output {
   }
 
   sendOutput (text) {
-    this.list.push(text);
+    const output = ''+text;
+    output.split("\n").forEach(t => {
+      t = t.replace(/\t/g,'  ');
+      t = t.replace(/\s/g," ");
+      this.list.push(t);
+    },this);
+    
   }
 }

+ 776 - 0
js/util/string_diff.js

@@ -0,0 +1,776 @@
+/**
+ * Author: @jhchen - https://github.com/jhchen
+ * Modified by: @lucascalion - 23/07/2019
+ * This library modifies the diff-patch-match library by Neil Fraser
+ * by removing the patch and match functionality and certain advanced
+ * options in the diff function. The original license is as follows:
+ *
+ * ===
+ *
+ * Diff Match and Patch
+ *
+ * Copyright 2006 Google Inc.
+ * http://code.google.com/p/google-diff-match-patch/
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+/**
+ * The data structure representing a diff is an array of tuples:
+ * [[DIFF_DELETE, 'Hello'], [DIFF_INSERT, 'Goodbye'], [DIFF_EQUAL, ' world.']]
+ * which means: delete 'Hello', add 'Goodbye' and keep ' world.'
+ */
+const DIFF_DELETE = -1;
+const DIFF_INSERT = 1;
+const DIFF_EQUAL = 0;
+
+
+/**
+ * Find the differences between two texts.  Simplifies the problem by stripping
+ * any common prefix or suffix off the texts before diffing.
+ * @param {string} text1 Old string to be diffed.
+ * @param {string} text2 New string to be diffed.
+ * @param {Int|Object} [cursor_pos] Edit position in text1 or object with more info
+ * @return {Array} Array of diff tuples.
+ */
+function diff_main(text1, text2, cursor_pos, _fix_unicode) {
+  // Check for equality
+  if (text1 === text2) {
+    if (text1) {
+      return [[DIFF_EQUAL, text1]];
+    }
+    return [];
+  }
+
+  if (cursor_pos != null) {
+    const editdiff = find_cursor_edit_diff(text1, text2, cursor_pos);
+    if (editdiff) {
+      return editdiff;
+    }
+  }
+
+  // Trim off common prefix (speedup).
+  let commonlength = diff_commonPrefix(text1, text2);
+  const commonprefix = text1.substring(0, commonlength);
+  text1 = text1.substring(commonlength);
+  text2 = text2.substring(commonlength);
+
+  // Trim off common suffix (speedup).
+  commonlength = diff_commonSuffix(text1, text2);
+  const commonsuffix = text1.substring(text1.length - commonlength);
+  text1 = text1.substring(0, text1.length - commonlength);
+  text2 = text2.substring(0, text2.length - commonlength);
+
+  // Compute the diff on the middle block.
+  const diffs = diff_compute_(text1, text2);
+
+  // Restore the prefix and suffix.
+  if (commonprefix) {
+    diffs.unshift([DIFF_EQUAL, commonprefix]);
+  }
+  if (commonsuffix) {
+    diffs.push([DIFF_EQUAL, commonsuffix]);
+  }
+  diff_cleanupMerge(diffs, _fix_unicode);
+  return diffs;
+}
+
+
+/**
+ * Find the differences between two texts.  Assumes that the texts do not
+ * have any common prefix or suffix.
+ * @param {string} text1 Old string to be diffed.
+ * @param {string} text2 New string to be diffed.
+ * @return {Array} Array of diff tuples.
+ */
+function diff_compute_(text1, text2) {
+  let diffs;
+
+  if (!text1) {
+    // Just add some text (speedup).
+    return [[DIFF_INSERT, text2]];
+  }
+
+  if (!text2) {
+    // Just delete some text (speedup).
+    return [[DIFF_DELETE, text1]];
+  }
+
+  const longtext = text1.length > text2.length ? text1 : text2;
+  const shorttext = text1.length > text2.length ? text2 : text1;
+  const i = longtext.indexOf(shorttext);
+  if (i !== -1) {
+    // Shorter text is inside the longer text (speedup).
+    diffs = [
+      [DIFF_INSERT, longtext.substring(0, i)],
+      [DIFF_EQUAL, shorttext],
+      [DIFF_INSERT, longtext.substring(i + shorttext.length)]
+    ];
+    // Swap insertions for deletions if diff is reversed.
+    if (text1.length > text2.length) {
+      diffs[0][0] = diffs[2][0] = DIFF_DELETE;
+    }
+    return diffs;
+  }
+
+  if (shorttext.length === 1) {
+    // Single character string.
+    // After the previous speedup, the character can't be an equality.
+    return [[DIFF_DELETE, text1], [DIFF_INSERT, text2]];
+  }
+
+  // Check to see if the problem can be split in two.
+  var hm = diff_halfMatch_(text1, text2);
+  if (hm) {
+    // A half-match was found, sort out the return data.
+    var text1_a = hm[0];
+    var text1_b = hm[1];
+    var text2_a = hm[2];
+    var text2_b = hm[3];
+    var mid_common = hm[4];
+    // Send both pairs off for separate processing.
+    var diffs_a = diff_main(text1_a, text2_a);
+    var diffs_b = diff_main(text1_b, text2_b);
+    // Merge the results.
+    return diffs_a.concat([[DIFF_EQUAL, mid_common]], diffs_b);
+  }
+
+  return diff_bisect_(text1, text2);
+}
+
+
+/**
+ * Find the 'middle snake' of a diff, split the problem in two
+ * and return the recursively constructed diff.
+ * See Myers 1986 paper: An O(ND) Difference Algorithm and Its Variations.
+ * @param {string} text1 Old string to be diffed.
+ * @param {string} text2 New string to be diffed.
+ * @return {Array} Array of diff tuples.
+ * @private
+ */
+function diff_bisect_(text1, text2) {
+  // Cache the text lengths to prevent multiple calls.
+  const text1_length = text1.length;
+  const text2_length = text2.length;
+  const max_d = Math.ceil((text1_length + text2_length) / 2);
+  const v_offset = max_d;
+  const v_length = 2 * max_d;
+  const v1 = new Array(v_length);
+  const v2 = new Array(v_length);
+  // Setting all elements to -1 is faster in Chrome & Firefox than mixing
+  // integers and undefined.
+  for (var x = 0; x < v_length; x++) {
+    v1[x] = -1;
+    v2[x] = -1;
+  }
+  v1[v_offset + 1] = 0;
+  v2[v_offset + 1] = 0;
+  const delta = text1_length - text2_length;
+  // If the total number of characters is odd, then the front path will collide
+  // with the reverse path.
+  const front = (delta % 2 !== 0);
+  // Offsets for start and end of k loop.
+  // Prevents mapping of space beyond the grid.
+  let k1start = 0;
+  let k1end = 0;
+  let k2start = 0;
+  let k2end = 0;
+  for (let d = 0; d < max_d; d++) {
+    // Walk the front path one step.
+    for (let k1 = -d + k1start; k1 <= d - k1end; k1 += 2) {
+      const k1_offset = v_offset + k1;
+      let x1;
+      if (k1 === -d || (k1 !== d && v1[k1_offset - 1] < v1[k1_offset + 1])) {
+        x1 = v1[k1_offset + 1];
+      } else {
+        x1 = v1[k1_offset - 1] + 1;
+      }
+      let y1 = x1 - k1;
+      while (
+        x1 < text1_length && y1 < text2_length &&
+        text1.charAt(x1) === text2.charAt(y1)
+      ) {
+        x1++;
+        y1++;
+      }
+      v1[k1_offset] = x1;
+      if (x1 > text1_length) {
+        // Ran off the right of the graph.
+        k1end += 2;
+      } else if (y1 > text2_length) {
+        // Ran off the bottom of the graph.
+        k1start += 2;
+      } else if (front) {
+        const k2_offset = v_offset + delta - k1;
+        if (k2_offset >= 0 && k2_offset < v_length && v2[k2_offset] !== -1) {
+          // Mirror x2 onto top-left coordinate system.
+          const x2 = text1_length - v2[k2_offset];
+          if (x1 >= x2) {
+            // Overlap detfrontected.
+            return diff_bisectSplit_(text1, text2, x1, y1);
+          }
+        }
+      }
+    }
+
+    // Walk the reverse path one step.
+    for (let k2 = -d + k2start; k2 <= d - k2end; k2 += 2) {
+      const k2_offset = v_offset + k2;
+      let x2;
+      if (k2 === -d || (k2 !== d && v2[k2_offset - 1] < v2[k2_offset + 1])) {
+        x2 = v2[k2_offset + 1];
+      } else {
+        x2 = v2[k2_offset - 1] + 1;
+      }
+      let y2 = x2 - k2;
+      while (
+        x2 < text1_length && y2 < text2_length &&
+        text1.charAt(text1_length - x2 - 1) === text2.charAt(text2_length - y2 - 1)
+      ) {
+        x2++;
+        y2++;
+      }
+      v2[k2_offset] = x2;
+      if (x2 > text1_length) {
+        // Ran off the left of the graph.
+        k2end += 2;
+      } else if (y2 > text2_length) {
+        // Ran off the top of the graph.
+        k2start += 2;
+      } else if (!front) {
+        const k1_offset = v_offset + delta - k2;
+        if (k1_offset >= 0 && k1_offset < v_length && v1[k1_offset] !== -1) {
+          const x1 = v1[k1_offset];
+          const y1 = v_offset + x1 - k1_offset;
+          // Mirror x2 onto top-left coordinate system.
+          x2 = text1_length - x2;
+          if (x1 >= x2) {
+            // Overlap detected.
+            return diff_bisectSplit_(text1, text2, x1, y1);
+          }
+        }
+      }
+    }
+  }
+  // Diff took too long and hit the deadline or
+  // number of diffs equals number of characters, no commonality at all.
+  return [[DIFF_DELETE, text1], [DIFF_INSERT, text2]];
+}
+
+
+/**
+ * Given the location of the 'middle snake', split the diff in two parts
+ * and recurse.
+ * @param {string} text1 Old string to be diffed.
+ * @param {string} text2 New string to be diffed.
+ * @param {number} x Index of split point in text1.
+ * @param {number} y Index of split point in text2.
+ * @return {Array} Array of diff tuples.
+ */
+function diff_bisectSplit_(text1, text2, x, y) {
+  const text1a = text1.substring(0, x);
+  const text2a = text2.substring(0, y);
+  const text1b = text1.substring(x);
+  const text2b = text2.substring(y);
+
+  // Compute both diffs serially.
+  const diffs = diff_main(text1a, text2a);
+  const diffsb = diff_main(text1b, text2b);
+
+  return diffs.concat(diffsb);
+}
+
+
+/**
+ * Determine the common prefix of two strings.
+ * @param {string} text1 First string.
+ * @param {string} text2 Second string.
+ * @return {number} The number of characters common to the start of each
+ *     string.
+ */
+function diff_commonPrefix(text1, text2) {
+  // Quick check for common null cases.
+  if (!text1 || !text2 || text1.charAt(0) !== text2.charAt(0)) {
+    return 0;
+  }
+  // Binary search.
+  // Performance analysis: http://neil.fraser.name/news/2007/10/09/
+  let pointermin = 0;
+  let pointermax = Math.min(text1.length, text2.length);
+  let pointermid = pointermax;
+  let pointerstart = 0;
+  while (pointermin < pointermid) {
+    if (
+      text1.substring(pointerstart, pointermid) ==
+      text2.substring(pointerstart, pointermid)
+    ) {
+      pointermin = pointermid;
+      pointerstart = pointermin;
+    } else {
+      pointermax = pointermid;
+    }
+    pointermid = Math.floor((pointermax - pointermin) / 2 + pointermin);
+  }
+
+  if (is_surrogate_pair_start(text1.charCodeAt(pointermid - 1))) {
+    pointermid--;
+  }
+
+  return pointermid;
+}
+
+
+/**
+ * Determine the common suffix of two strings.
+ * @param {string} text1 First string.
+ * @param {string} text2 Second string.
+ * @return {number} The number of characters common to the end of each string.
+ */
+function diff_commonSuffix(text1, text2) {
+  // Quick check for common null cases.
+  if (!text1 || !text2 || text1.slice(-1) !== text2.slice(-1)) {
+    return 0;
+  }
+  // Binary search.
+  // Performance analysis: http://neil.fraser.name/news/2007/10/09/
+  let pointermin = 0;
+  let pointermax = Math.min(text1.length, text2.length);
+  let pointermid = pointermax;
+  let pointerend = 0;
+  while (pointermin < pointermid) {
+    if (
+      text1.substring(text1.length - pointermid, text1.length - pointerend) ==
+      text2.substring(text2.length - pointermid, text2.length - pointerend)
+    ) {
+      pointermin = pointermid;
+      pointerend = pointermin;
+    } else {
+      pointermax = pointermid;
+    }
+    pointermid = Math.floor((pointermax - pointermin) / 2 + pointermin);
+  }
+
+  if (is_surrogate_pair_end(text1.charCodeAt(text1.length - pointermid))) {
+    pointermid--;
+  }
+
+  return pointermid;
+}
+
+
+/**
+ * Do the two texts share a substring which is at least half the length of the
+ * longer text?
+ * This speedup can produce non-minimal diffs.
+ * @param {string} text1 First string.
+ * @param {string} text2 Second string.
+ * @return {Array.<string>} Five element Array, containing the prefix of
+ *     text1, the suffix of text1, the prefix of text2, the suffix of
+ *     text2 and the common middle.  Or null if there was no match.
+ */
+function diff_halfMatch_(text1, text2) {
+  const longtext = text1.length > text2.length ? text1 : text2;
+  const shorttext = text1.length > text2.length ? text2 : text1;
+  if (longtext.length < 4 || shorttext.length * 2 < longtext.length) {
+    return null;  // Pointless.
+  }
+
+  /**
+   * Does a substring of shorttext exist within longtext such that the substring
+   * is at least half the length of longtext?
+   * Closure, but does not reference any external variables.
+   * @param {string} longtext Longer string.
+   * @param {string} shorttext Shorter string.
+   * @param {number} i Start index of quarter length substring within longtext.
+   * @return {Array.<string>} Five element Array, containing the prefix of
+   *     longtext, the suffix of longtext, the prefix of shorttext, the suffix
+   *     of shorttext and the common middle.  Or null if there was no match.
+   * @private
+   */
+  function diff_halfMatchI_(longtext, shorttext, i) {
+    // Start with a 1/4 length substring at position i as a seed.
+    const seed = longtext.substring(i, i + Math.floor(longtext.length / 4));
+    let j = -1;
+    let best_common = '';
+    let best_longtext_a, best_longtext_b, best_shorttext_a, best_shorttext_b;
+    while ((j = shorttext.indexOf(seed, j + 1)) !== -1) {
+      const prefixLength = diff_commonPrefix(
+        longtext.substring(i), shorttext.substring(j));
+      const suffixLength = diff_commonSuffix(
+        longtext.substring(0, i), shorttext.substring(0, j));
+      if (best_common.length < suffixLength + prefixLength) {
+        best_common = shorttext.substring(
+          j - suffixLength, j) + shorttext.substring(j, j + prefixLength);
+        best_longtext_a = longtext.substring(0, i - suffixLength);
+        best_longtext_b = longtext.substring(i + prefixLength);
+        best_shorttext_a = shorttext.substring(0, j - suffixLength);
+        best_shorttext_b = shorttext.substring(j + prefixLength);
+      }
+    }
+    if (best_common.length * 2 >= longtext.length) {
+      return [
+        best_longtext_a, best_longtext_b,
+        best_shorttext_a, best_shorttext_b, best_common
+      ];
+    } else {
+      return null;
+    }
+  }
+
+  // First check if the second quarter is the seed for a half-match.
+  const hm1 = diff_halfMatchI_(longtext, shorttext, Math.ceil(longtext.length / 4));
+  // Check again based on the third quarter.
+  const hm2 = diff_halfMatchI_(longtext, shorttext, Math.ceil(longtext.length / 2));
+  let hm;
+  if (!hm1 && !hm2) {
+    return null;
+  } else if (!hm2) {
+    hm = hm1;
+  } else if (!hm1) {
+    hm = hm2;
+  } else {
+    // Both matched.  Select the longest.
+    hm = hm1[4].length > hm2[4].length ? hm1 : hm2;
+  }
+
+  // A half-match was found, sort out the return data.
+  let text1_a, text1_b, text2_a, text2_b;
+  if (text1.length > text2.length) {
+    text1_a = hm[0];
+    text1_b = hm[1];
+    text2_a = hm[2];
+    text2_b = hm[3];
+  } else {
+    text2_a = hm[0];
+    text2_b = hm[1];
+    text1_a = hm[2];
+    text1_b = hm[3];
+  }
+  const mid_common = hm[4];
+  return [text1_a, text1_b, text2_a, text2_b, mid_common];
+}
+
+
+/**
+ * Reorder and merge like edit sections.  Merge equalities.
+ * Any edit section can move as long as it doesn't cross an equality.
+ * @param {Array} diffs Array of diff tuples.
+ * @param {boolean} fix_unicode Whether to normalize to a unicode-correct diff
+ */
+function diff_cleanupMerge(diffs, fix_unicode) {
+  diffs.push([DIFF_EQUAL, '']);  // Add a dummy entry at the end.
+  let pointer = 0;
+  let count_delete = 0;
+  let count_insert = 0;
+  let text_delete = '';
+  let text_insert = '';
+  let commonlength;
+  let previous_equality;
+  while (pointer < diffs.length) {
+    if (pointer < diffs.length - 1 && !diffs[pointer][1]) {
+      diffs.splice(pointer, 1);
+      continue;
+    }
+    switch (diffs[pointer][0]) {
+      case DIFF_INSERT:
+        count_insert++;
+        text_insert += diffs[pointer][1];
+        pointer++;
+        break;
+      case DIFF_DELETE:
+        count_delete++;
+        text_delete += diffs[pointer][1];
+        pointer++;
+        break;
+      case DIFF_EQUAL:
+        previous_equality = pointer - count_insert - count_delete - 1;
+        if (fix_unicode) {
+          // prevent splitting of unicode surrogate pairs.  when fix_unicode is true,
+          // we assume that the old and new text in the diff are complete and correct
+          // unicode-encoded JS strings, but the tuple boundaries may fall between
+          // surrogate pairs.  we fix this by shaving off stray surrogates from the end
+          // of the previous equality and the beginning of this equality.  this may create
+          // empty equalities or a common prefix or suffix.  for example, if AB and AC are
+          // emojis, `[[0, 'A'], [-1, 'BA'], [0, 'C']]` would turn into deleting 'ABAC' and
+          // inserting 'AC', and then the common suffix 'AC' will be eliminated.  in this
+          // particular case, both equalities go away, we absorb any previous inequalities,
+          // and we keep scanning for the next equality before rewriting the tuples.
+          if (previous_equality >= 0 && ends_with_pair_start(diffs[previous_equality][1])) {
+            const stray = diffs[previous_equality][1].slice(-1);
+            diffs[previous_equality][1] = diffs[previous_equality][1].slice(0, -1);
+            text_delete = stray + text_delete;
+            text_insert = stray + text_insert;
+            if (!diffs[previous_equality][1]) {
+              // emptied out previous equality, so delete it and include previous delete/insert
+              diffs.splice(previous_equality, 1);
+              pointer--;
+              var k = previous_equality - 1;
+              if (diffs[k] && diffs[k][0] === DIFF_INSERT) {
+                count_insert++;
+                text_insert = diffs[k][1] + text_insert;
+                k--;
+              }
+              if (diffs[k] && diffs[k][0] === DIFF_DELETE) {
+                count_delete++;
+                text_delete = diffs[k][1] + text_delete;
+                k--;
+              }
+              previous_equality = k;
+            }
+          }
+          if (starts_with_pair_end(diffs[pointer][1])) {
+            const stray = diffs[pointer][1].charAt(0);
+            diffs[pointer][1] = diffs[pointer][1].slice(1);
+            text_delete += stray;
+            text_insert += stray;
+          }
+        }
+        if (pointer < diffs.length - 1 && !diffs[pointer][1]) {
+          // for empty equality not at end, wait for next equality
+          diffs.splice(pointer, 1);
+          break;
+        }
+        if (text_delete.length > 0 || text_insert.length > 0) {
+          // note that diff_commonPrefix and diff_commonSuffix are unicode-aware
+          if (text_delete.length > 0 && text_insert.length > 0) {
+            // Factor out any common prefixes.
+            commonlength = diff_commonPrefix(text_insert, text_delete);
+            if (commonlength !== 0) {
+              if (previous_equality >= 0) {
+                diffs[previous_equality][1] += text_insert.substring(0, commonlength);
+              } else {
+                diffs.splice(0, 0, [DIFF_EQUAL, text_insert.substring(0, commonlength)]);
+                pointer++;
+              }
+              text_insert = text_insert.substring(commonlength);
+              text_delete = text_delete.substring(commonlength);
+            }
+            // Factor out any common suffixes.
+            commonlength = diff_commonSuffix(text_insert, text_delete);
+            if (commonlength !== 0) {
+              diffs[pointer][1] =
+                text_insert.substring(text_insert.length - commonlength) + diffs[pointer][1];
+              text_insert = text_insert.substring(0, text_insert.length - commonlength);
+              text_delete = text_delete.substring(0, text_delete.length - commonlength);
+            }
+          }
+          // Delete the offending records and add the merged ones.
+          const n = count_insert + count_delete;
+          if (text_delete.length === 0 && text_insert.length === 0) {
+            diffs.splice(pointer - n, n);
+            pointer = pointer - n;
+          } else if (text_delete.length === 0) {
+            diffs.splice(pointer - n, n, [DIFF_INSERT, text_insert]);
+            pointer = pointer - n + 1;
+          } else if (text_insert.length === 0) {
+            diffs.splice(pointer - n, n, [DIFF_DELETE, text_delete]);
+            pointer = pointer - n + 1;
+          } else {
+            diffs.splice(pointer - n, n, [DIFF_DELETE, text_delete], [DIFF_INSERT, text_insert]);
+            pointer = pointer - n + 2;
+          }
+        }
+        if (pointer !== 0 && diffs[pointer - 1][0] === DIFF_EQUAL) {
+          // Merge this equality with the previous one.
+          diffs[pointer - 1][1] += diffs[pointer][1];
+          diffs.splice(pointer, 1);
+        } else {
+          pointer++;
+        }
+        count_insert = 0;
+        count_delete = 0;
+        text_delete = '';
+        text_insert = '';
+        break;
+    }
+  }
+  if (diffs[diffs.length - 1][1] === '') {
+    diffs.pop();  // Remove the dummy entry at the end.
+  }
+
+  // Second pass: look for single edits surrounded on both sides by equalities
+  // which can be shifted sideways to eliminate an equality.
+  // e.g: A<ins>BA</ins>C -> <ins>AB</ins>AC
+  let changes = false;
+  pointer = 1;
+  // Intentionally ignore the first and last element (don't need checking).
+  while (pointer < diffs.length - 1) {
+    if (diffs[pointer - 1][0] === DIFF_EQUAL &&
+      diffs[pointer + 1][0] === DIFF_EQUAL) {
+      // This is a single edit surrounded by equalities.
+      if (diffs[pointer][1].substring(diffs[pointer][1].length -
+        diffs[pointer - 1][1].length) === diffs[pointer - 1][1]) {
+        // Shift the edit over the previous equality.
+        diffs[pointer][1] = diffs[pointer - 1][1] +
+          diffs[pointer][1].substring(0, diffs[pointer][1].length -
+            diffs[pointer - 1][1].length);
+        diffs[pointer + 1][1] = diffs[pointer - 1][1] + diffs[pointer + 1][1];
+        diffs.splice(pointer - 1, 1);
+        changes = true;
+      } else if (diffs[pointer][1].substring(0, diffs[pointer + 1][1].length) ==
+        diffs[pointer + 1][1]) {
+        // Shift the edit over the next equality.
+        diffs[pointer - 1][1] += diffs[pointer + 1][1];
+        diffs[pointer][1] =
+          diffs[pointer][1].substring(diffs[pointer + 1][1].length) +
+          diffs[pointer + 1][1];
+        diffs.splice(pointer + 1, 1);
+        changes = true;
+      }
+    }
+    pointer++;
+  }
+  // If shifts were made, the diff needs reordering and another shift sweep.
+  if (changes) {
+    diff_cleanupMerge(diffs, fix_unicode);
+  }
+}
+
+function is_surrogate_pair_start(charCode) {
+  return charCode >= 0xD800 && charCode <= 0xDBFF;
+}
+
+function is_surrogate_pair_end(charCode) {
+  return charCode >= 0xDC00 && charCode <= 0xDFFF;
+}
+
+function starts_with_pair_end(str) {
+  return is_surrogate_pair_end(str.charCodeAt(0));
+}
+
+function ends_with_pair_start(str) {
+  return is_surrogate_pair_start(str.charCodeAt(str.length - 1));
+}
+
+function remove_empty_tuples(tuples) {
+  const ret = [];
+  for (let i = 0; i < tuples.length; i++) {
+    if (tuples[i][1].length > 0) {
+      ret.push(tuples[i]);
+    }
+  }
+  return ret;
+}
+
+function make_edit_splice(before, oldMiddle, newMiddle, after) {
+  if (ends_with_pair_start(before) || starts_with_pair_end(after)) {
+    return null;
+  }
+  return remove_empty_tuples([
+    [DIFF_EQUAL, before],
+    [DIFF_DELETE, oldMiddle],
+    [DIFF_INSERT, newMiddle],
+    [DIFF_EQUAL, after]
+  ]);
+}
+
+function find_cursor_edit_diff(oldText, newText, cursor_pos) {
+  // note: this runs after equality check has ruled out exact equality
+  const oldRange = typeof cursor_pos === 'number' ?
+    { index: cursor_pos, length: 0 } : cursor_pos.oldRange;
+  const newRange = typeof cursor_pos === 'number' ?
+    null : cursor_pos.newRange;
+  // take into account the old and new selection to generate the best diff
+  // possible for a text edit.  for example, a text change from "xxx" to "xx"
+  // could be a delete or forwards-delete of any one of the x's, or the
+  // result of selecting two of the x's and typing "x".
+  const oldLength = oldText.length;
+  const newLength = newText.length;
+  if (oldRange.length === 0 && (newRange === null || newRange.length === 0)) {
+    // see if we have an insert or delete before or after cursor
+    const oldCursor = oldRange.index;
+    const oldBefore = oldText.slice(0, oldCursor);
+    const oldAfter = oldText.slice(oldCursor);
+    const maybeNewCursor = newRange ? newRange.index : null;
+    editBefore: {
+      // is this an insert or delete right before oldCursor?
+      const newCursor = oldCursor + newLength - oldLength;
+      if (maybeNewCursor !== null && maybeNewCursor !== newCursor) {
+        break editBefore;
+      }
+      if (newCursor < 0 || newCursor > newLength) {
+        break editBefore;
+      }
+      const newBefore = newText.slice(0, newCursor);
+      const newAfter = newText.slice(newCursor);
+      if (newAfter !== oldAfter) {
+        break editBefore;
+      }
+      const prefixLength = Math.min(oldCursor, newCursor);
+      const oldPrefix = oldBefore.slice(0, prefixLength);
+      const newPrefix = newBefore.slice(0, prefixLength);
+      if (oldPrefix !== newPrefix) {
+        break editBefore;
+      }
+      const oldMiddle = oldBefore.slice(prefixLength);
+      const newMiddle = newBefore.slice(prefixLength);
+      return make_edit_splice(oldPrefix, oldMiddle, newMiddle, oldAfter);
+    }
+    editAfter: {
+      // is this an insert or delete right after oldCursor?
+      if (maybeNewCursor !== null && maybeNewCursor !== oldCursor) {
+        break editAfter;
+      }
+      const cursor = oldCursor;
+      const newBefore = newText.slice(0, cursor);
+      const newAfter = newText.slice(cursor);
+      if (newBefore !== oldBefore) {
+        break editAfter;
+      }
+      const suffixLength = Math.min(oldLength - cursor, newLength - cursor);
+      const oldSuffix = oldAfter.slice(oldAfter.length - suffixLength);
+      const newSuffix = newAfter.slice(newAfter.length - suffixLength);
+      if (oldSuffix !== newSuffix) {
+        break editAfter;
+      }
+      const oldMiddle = oldAfter.slice(0, oldAfter.length - suffixLength);
+      const newMiddle = newAfter.slice(0, newAfter.length - suffixLength);
+      return make_edit_splice(oldBefore, oldMiddle, newMiddle, oldSuffix);
+    }
+  }
+  if (oldRange.length > 0 && newRange && newRange.length === 0) {
+    replaceRange: {
+      // see if diff could be a splice of the old selection range
+      const oldPrefix = oldText.slice(0, oldRange.index);
+      const oldSuffix = oldText.slice(oldRange.index + oldRange.length);
+      const prefixLength = oldPrefix.length;
+      const suffixLength = oldSuffix.length;
+      if (newLength < prefixLength + suffixLength) {
+        break replaceRange;
+      }
+      const newPrefix = newText.slice(0, prefixLength);
+      const newSuffix = newText.slice(newLength - suffixLength);
+      if (oldPrefix !== newPrefix || oldSuffix !== newSuffix) {
+        break replaceRange;
+      }
+      const oldMiddle = oldText.slice(prefixLength, oldLength - suffixLength);
+      const newMiddle = newText.slice(prefixLength, newLength - suffixLength);
+      return make_edit_splice(oldPrefix, oldMiddle, newMiddle, oldSuffix);
+    }
+  }
+
+  return null;
+}
+
+function diff(text1, text2, cursor_pos) {
+  // only pass fix_unicode=true at the top level, not when diff_main is
+  // recursively invoked
+  return diff_main(text1, text2, cursor_pos, true);
+}
+
+diff.INSERT = DIFF_INSERT;
+diff.DELETE = DIFF_DELETE;
+diff.EQUAL = DIFF_EQUAL;
+
+export default diff;

+ 52 - 1
js/util/utils.js

@@ -153,7 +153,7 @@ function fillCache () {
 export function getCodeEditorModeConfig () {
   const blockList = ["RK_SWITCH", "RK_PROGRAM","RK_CASE","RK_DEFAULT","RK_FOR",
     "RK_FUNCTION","RK_DO","RK_WHILE","RK_IF","RK_ELSE"]
-  const keywordsList = [,"RK_CONST","RK_RETURN","RK_BREAK"];
+  const keywordsList = ["RK_CONST","RK_RETURN","RK_BREAK"];
   const typeList = ["RK_REAL","RK_VOID","RK_BOOLEAN","RK_STRING","RK_INTEGER"];
   const atomList = ["RK_FALSE", "RK_TRUE"];
 
@@ -215,3 +215,54 @@ export function getCodeEditorModeConfig () {
     blocks: blocks
   }
 }
+
+/**
+ * Source: https://gist.github.com/andrei-m/982927
+ * @param {string} a 
+ * @param {string} b 
+ */
+export function levenshteinDistance (a, b) {
+  if(a.length == 0) return b.length; 
+  if(b.length == 0) return a.length; 
+
+  const matrix = [];
+
+  // increment along the first column of each row
+  let i;
+  for(i = 0; i <= b.length; i++){
+    matrix[i] = [i];
+  }
+
+  // increment each column in the first row
+  let j;
+  for(j = 0; j <= a.length; j++){
+    matrix[0][j] = j;
+  }
+
+  // Fill in the rest of the matrix
+  for(i = 1; i <= b.length; i++){
+    for(j = 1; j <= a.length; j++){
+      if(b.charCodeAt(i-1) == a.charCodeAt(j-1)){
+        matrix[i][j] = matrix[i-1][j-1];
+      } else {
+        matrix[i][j] = Math.min(matrix[i-1][j-1] + 1, // substitution
+                        Math.min(matrix[i][j-1] + 1, // insertion
+                          matrix[i-1][j] + 1)); // deletion
+      }
+    }
+  }
+  return matrix[b.length][a.length];
+}
+
+let win = null
+export function openAssessmentDetail (event) {
+  event.preventDefault();
+  const page_code = event.currentTarget.dataset.page;
+  if(win != null) {
+    win.close()
+  }
+  win = window.open("", "DetailWindow", "width=550,height=600");
+  win.document.open();
+  win.document.write(page_code);
+  win.document.close();
+}

+ 0 - 2
js/visualUI/commands/generic_expression.js

@@ -108,7 +108,6 @@ export function renderExpression (command, function_obj, div_to_render, expressi
 	        put: false // Do not allow items to be put into this list
 	    },
 	    draggable: '.single_element_expression',
-	    handle: '.element_expression_handle',
 	    sort: false,
 	    filter: '.not_allowed',
 	    
@@ -248,7 +247,6 @@ function renderElements (command, function_obj, div_to_render, expression_array,
 	for (i = 0; i < expression_array.length; i++) {
 		if (expression_array[i].type == "var_value") {
 			var div_temp = $('<div class="single_element_expression" data-index="'+i+'"></div>');
-			div_temp.append('<div class="element_expression_handle" style="display: inline; width: 2px solid red; width: 20px; height: 15px;"></div>');
 			if (i == 0) {
 				if (expression_array.length > 0  && !expression_array[0].type_op) {
 

+ 28 - 17
js/visualUI/functions.js

@@ -195,7 +195,7 @@ function addHandlers (function_obj, function_container) {
       selectOnKeydown: false
   });
 
-  function_container.find( ".function_name_div" ).on('click', function(e){
+  function_container.find( ".name_function_updated" ).on('click', function(e){
     enableNameFunctionUpdate(function_obj, function_container);
   });
 
@@ -750,8 +750,8 @@ export function initVisualUI () {
   });
 
   $('.assessment').on('click', () => {
-    runCodeAssessment();
     is_iassign = true;
+    runCodeAssessment();
   });
 
   $('.div_toggle_console').on('click', () => {
@@ -898,7 +898,8 @@ function updateSequenceFunction (oldIndex, newIndex) {
 
 function runCodeAssessment () {
   if (isRunning) {
-    return;
+    // cannot run assessment or it's already running
+    return -1;
   }
   
   let strCode = null;
@@ -912,28 +913,38 @@ function runCodeAssessment () {
   }
 
   if (strCode == null) {
-    return;
+    // No code, so no assessment
+    return -1;
   }
 
   toggleConsole(true);
 
-  // if(domConsole == null)
-  //   domConsole = new DOMConsole("#ivprog-term");
-  // $("#ivprog-term").slideDown(500);
-  const runner = new IVProgAssessment(strCode, _testCases, domConsole);
-  isRunning = true;
-  runner.runTest().then(grade => {
+  try {
+    const data = SemanticAnalyser.analyseFromSource(strCode);
+    isRunning = true;
+    const runner = new IVProgAssessment(data, _testCases, domConsole);
+    runner.runTest().then(grade => {
+      if (!is_iassign) {
+        parent.getEvaluationCallback(grade);
+      } else {
+        is_iassign = false;
+      }
+      isRunning = false;
+    }).catch( err => {
+      console.log(err);
+      isRunning = false;
+    });
+  } catch (error) {
+    isRunning = false;
+    domConsole.err(error.message);
+    console.log(error);
     if (!is_iassign) {
-      parent.getEvaluationCallback(grade);
+      parent.getEvaluationCallback(0);
     } else {
       is_iassign = false;
     }
-    isRunning = false;
-  }).catch( err => {
-    console.log(err);
-    isRunning = false;
-  });
-  
+  }
+  return 0;
 }
 
 function runCode () {

File diff suppressed because it is too large
+ 729 - 3
package-lock.json


+ 1 - 0
package.json

@@ -32,6 +32,7 @@
     "babel-loader": "^8.0.5",
     "clean-webpack-plugin": "^2.0.1",
     "copy-webpack-plugin": "^5.0.2",
+    "eslint": "^6.1.0",
     "html-webpack-plugin": "^4.0.0-beta.5",
     "jasmine-core": "^3.4.0",
     "karma": "^4.1.0",

+ 2 - 0
webpack.config.js

@@ -59,11 +59,13 @@ module.exports = {
         {from:'js/iassign-integration-functions.js', to:path.resolve(__dirname, 'build/js')},
         {from:"css/ivprog-visual-1.0.css", to:path.resolve(__dirname, 'build/css')},
         {from:"css/ivprog-term.css", to:path.resolve(__dirname, 'build/css')},
+        {from:"css/ivprog-assessment.css", to:path.resolve(__dirname, 'build/css')},
         {from:"css/ivprog-editor.css", to:path.resolve(__dirname, 'build/css')},
         {from:"css/roboto.css", to:path.resolve(__dirname, 'build/css')},
         {from:"css/fonts/", to:path.resolve(__dirname, 'build/css/fonts')},
         {from:'js/Sortable.js', to:path.resolve(__dirname, 'build/js')},
         {from: 'img/trash-icon.png', to:path.resolve(__dirname, 'build/img')},
+        {from: 'img/empty.svg', to:path.resolve(__dirname, 'build/img')},
         {from:'js/jquery.json-editor.min.js', to:path.resolve(__dirname, 'build/js')},
         {from:'node_modules/codemirror/lib/codemirror.css', to:path.resolve(__dirname, 'build/css')},
         {from:'node_modules/codemirror/addon/hint/show-hint.css', to:path.resolve(__dirname, 'build/css')},