From 8e3aec9f5480d246f0700aad683dc4a64b2d2e84 Mon Sep 17 00:00:00 2001 From: Chris Mayfield Date: Wed, 7 Aug 2024 09:30:32 -0400 Subject: [PATCH 01/21] remove unused imports and large commented-out chunks --- archive/StringFunc.js | 84 +++++++++++++++ {src => archive}/gui.js | 0 archive/oldBlocks.js | 74 +++++++++++++ src/blocks2tree.js | 22 ---- src/main.js | 5 +- src/newBlocks.js | 200 ++++++++-------------------------- src/toolbox.js | 231 ++++++++++++++++------------------------ src/tree2text.js | 1 - 8 files changed, 295 insertions(+), 322 deletions(-) create mode 100644 archive/StringFunc.js rename {src => archive}/gui.js (100%) create mode 100644 archive/oldBlocks.js diff --git a/archive/StringFunc.js b/archive/StringFunc.js new file mode 100644 index 0000000..4333473 --- /dev/null +++ b/archive/StringFunc.js @@ -0,0 +1,84 @@ +/* blocks2tree.js */ + +praxlyGenerator['praxly_StringFunc_block'] = (block) => { + const expression = block.getInputTargetBlock('EXPRESSION'); + var procedureName = block.getFieldValue('FUNCTYPE'); + var args = block.getInputTargetBlock('PARAMS'); + var argschildren = args.getChildren(true); + var argsList = []; + argschildren.forEach(element => { + argsList.push(praxlyGenerator[element.type](element)); + }); + return customizeMaybe(block, { + blockID: block.id, + type: NODETYPES.SPECIAL_STRING_FUNCCALL, + left: praxlyGenerator[expression.type](expression), + right: { + name: procedureName, + args: argsList, + type: NODETYPES.FUNCCALL + } + }); +} + +/* newBlocks.js */ + +a = [ + { // text 1 + "type": "praxly_StringFunc_block", + "message0": "%1.%2(%3)", + "args0": [ + { + "type": "input_value", + "name": "EXPRESSION" + }, + { + "type": "field_dropdown", + "name": "FUNCTYPE", + "options": [ + ["charAt", StringFuncs.CHARAT], + ["contains", StringFuncs.CONTAINS], + ['indexOf', StringFuncs.INDEXOF], + ["length", StringFuncs.LENGTH], + ["substring", StringFuncs.SUBSTRING], + ["toLowerCase", StringFuncs.TOLOWERCSE], + ["toUpperCase", StringFuncs.TOUPPERCASE], + ] + }, + { + "type": "input_value", + "name": "PARAMS", + "text": "params" + }, + ], + "inputsInline": true, + "output": null, + "style": 'other_blocks', + "tooltip": "String methods:\ncharAt(i) - Returns the character at index i\ncontains(s) - Returns true if s is a substring\nindexOf(s) - Returns the first index of substring s, or -1 if not found\nlength() - Returns the length of the string\nsubstring(i, j) - Extracts characters from index i up to but not including index j\ntoLowerCase() - Converts the string to all lowercase\ntoUpperCase() - Converts the string to all uppercase", + "helpUrl": "" + }, +] + +/* toolbox.js */ + +b = [ + { + 'kind': 'block', + 'type': 'praxly_StringFunc_block', + 'inputs': { + 'EXPRESSION': { + 'shadow': { + 'type': 'praxly_literal_block', + 'fields': { + 'LITERAL': '\"hello, world\"', + } + }, + }, + 'PARAMS': { + 'block': { + 'type': 'praxly_parameter_block', + } + } + } + }, +] diff --git a/src/gui.js b/archive/gui.js similarity index 100% rename from src/gui.js rename to archive/gui.js diff --git a/archive/oldBlocks.js b/archive/oldBlocks.js new file mode 100644 index 0000000..15defc0 --- /dev/null +++ b/archive/oldBlocks.js @@ -0,0 +1,74 @@ +[ + { // not used + "type": "praxly_null_block", + "message0": "null", + "inputsInline": true, + "output": null, + "style": 'expression_blocks', + "tooltip": "", + "helpUrl": "" + }, + + { // not used + "type": "praxly_class_block", + "message0": "class %1 %2 end class", + "args0": [ + { + "type": "input_dummy" + }, + { + "type": "input_statement", + "name": "class" + } + ], + "inputsInline": true, + "previousStatement": null, + "nextStatement": null, + "style": "class_blocks", + "tooltip": "", + "helpUrl": "" + }, + + { // not used + "type": "praxly_String_block", + "message0": "\"%1\"", + "args0": [ + { + "type": "field_input", + "name": "LITERAL", + "text": "String" + } + ], + "output": null, + "style": 'expression_blocks', + "tooltip": "", + "helpUrl": "" + }, + + { // not used + "type": "custom_operation_block", + "message0": "Custom Operation %1 %2", + "args0": [ + { + "type": "input_value", + "name": "input1", + "check": "Number", + "align": "RIGHT", + "defaultType": "math_number", + "defaultValue": 5 + }, + { + "type": "input_value", + "name": "input2", + "check": "Number", + "align": "RIGHT", + "defaultType": "math_number", + "defaultValue": 10 + } + ], + "output": "Number", + "colour": 230, + "tooltip": "Perform custom operation", + "helpUrl": "" + } +] diff --git a/src/blocks2tree.js b/src/blocks2tree.js index 4d0f453..f88373b 100644 --- a/src/blocks2tree.js +++ b/src/blocks2tree.js @@ -1,4 +1,3 @@ -import Blockly from 'blockly'; import { NODETYPES, StringFuncs, TYPES } from './common'; function containsOnlyNumbers(str) { @@ -637,27 +636,6 @@ export const makeGenerator = () => { }) } - // praxlyGenerator['praxly_StringFunc_block'] = (block) => { - // const expression = block.getInputTargetBlock('EXPRESSION'); - // var procedureName = block.getFieldValue('FUNCTYPE'); - // var args = block.getInputTargetBlock('PARAMS'); - // var argschildren = args.getChildren(true); - // var argsList = []; - // argschildren.forEach(element => { - // argsList.push(praxlyGenerator[element.type](element)); - // }); - // return customizeMaybe(block, { - // blockID: block.id, - // type: NODETYPES.SPECIAL_STRING_FUNCCALL, - // left: praxlyGenerator[expression.type](expression), - // right: { - // name: procedureName, - // args: argsList, - // type: NODETYPES.FUNCCALL - // } - // }); - // } - praxlyGenerator['praxly_charAt_block'] = (block) => { const procedureName = StringFuncs.CHARAT; const expression = block.getInputTargetBlock('EXPRESSION'); diff --git a/src/main.js b/src/main.js index f5a60cb..19c37c2 100644 --- a/src/main.js +++ b/src/main.js @@ -1,4 +1,4 @@ -import Blockly, { Block } from 'blockly'; +import Blockly from 'blockly'; import { praxlyDefaultTheme } from "./theme" import { PraxlyDark } from './theme'; import { toolbox } from './toolbox'; @@ -190,7 +190,6 @@ function registerListeners() { }) - document.addEventListener('mouseup', function (e) { isResizingHoriz = false; document.removeEventListener('mousemove', resizeHandler); @@ -206,7 +205,6 @@ function registerListeners() { }); - // these make it so that the blocks and text take turns. blockPane.addEventListener('click', () => { workspace.removeChangeListener(onBlocklyChange); @@ -283,7 +281,6 @@ function toggleTextOn() { } - let isBlocksOn = true; function toggleBlocksOn() { isBlocksOn = !isBlocksOn; diff --git a/src/newBlocks.js b/src/newBlocks.js index 93fa601..0386275 100644 --- a/src/newBlocks.js +++ b/src/newBlocks.js @@ -1,5 +1,5 @@ -import Blockly, { Block } from 'blockly'; -import { NODETYPES, StringFuncs } from './common'; +import Blockly from 'blockly'; +import { NODETYPES } from './common'; export function definePraxlyBlocks(workspace) { let callbacks = { @@ -13,13 +13,10 @@ export function definePraxlyBlocks(workspace) { this.params = state.params || []; for (let i = 0; i < this.params.length; i++) { this.appendValueInput(`PARAM_${i}`); - } } }; - - Blockly.Extensions.registerMutator('praxly_arity', callbacks); Blockly.Extensions.register('addParams', function () { @@ -344,21 +341,6 @@ export function definePraxlyBlocks(workspace) { "output": null }, { // math 1 - "type": "praxly_literal_block", - "message0": "%1", - "args0": [ - { - "type": "field_input", - "name": "LITERAL", - "text": "value" - } - ], - "output": null, - "style": 'math_blocks', - "tooltip": "A literal value in the code", - "helpUrl": "" - }, - { // math 2 "type": "praxly_arithmetic_block", "message0": "%1 %2 %3 %4", "args0": [ @@ -412,7 +394,7 @@ export function definePraxlyBlocks(workspace) { "tooltip": "Arithmetic operators:\n+ addition (and string concatenation)\n- subtraction\n* multiplication\n/ division (integer and floating-point)\n% remainder\n^ exponent", "helpUrl": "", }, - { // math 3 + { // math 2 "type": "praxly_negate_block", "message0": "- %1 %2", "args0": [ @@ -429,6 +411,21 @@ export function definePraxlyBlocks(workspace) { "tooltip": "Negates a value", "helpUrl": "" }, + { // math 3 + "type": "praxly_literal_block", + "message0": "%1", + "args0": [ + { + "type": "field_input", + "name": "LITERAL", + "text": "value" + } + ], + "output": null, + "style": 'math_blocks', + "tooltip": "A literal value in the code", + "helpUrl": "" + }, { // math 4 "type": "praxly_random_block", "message0": "random ( )", @@ -536,7 +533,7 @@ export function definePraxlyBlocks(workspace) { "tooltip": "Returns the higher value", "helpURL": "" }, - { + { // math 11 "type": "praxly_abs_block", "message0": "abs ( %1 )", "args0": [ @@ -551,7 +548,7 @@ export function definePraxlyBlocks(workspace) { "tooltip": "Returns the absolute value", "helpURL": "" }, - { + { // math 12 "type": "praxly_log_block", "message0": "log ( %1 )", "args0": [ @@ -566,7 +563,7 @@ export function definePraxlyBlocks(workspace) { "tooltip": "Calculates the natural logarithm", "helpURL": "" }, - { + { // math 13 "type": "praxly_sqrt_block", "message0": "sqrt ( %1 )", "args0": [ @@ -581,39 +578,6 @@ export function definePraxlyBlocks(workspace) { "tooltip": "Calculates the square root", "helpURL": "" }, - // { // text 1 - // "type": "praxly_StringFunc_block", - // "message0": "%1.%2(%3)", - // "args0": [ - // { - // "type": "input_value", - // "name": "EXPRESSION" - // }, - // { - // "type": "field_dropdown", - // "name": "FUNCTYPE", - // "options": [ - // ["charAt", StringFuncs.CHARAT], - // ["contains", StringFuncs.CONTAINS], - // ['indexOf', StringFuncs.INDEXOF], - // ["length", StringFuncs.LENGTH], - // ["substring", StringFuncs.SUBSTRING], - // ["toLowerCase", StringFuncs.TOLOWERCSE], - // ["toUpperCase", StringFuncs.TOUPPERCASE], - // ] - // }, - // { - // "type": "input_value", - // "name": "PARAMS", - // "text": "params" - // }, - // ], - // "inputsInline": true, - // "output": null, - // "style": 'other_blocks', - // "tooltip": "String methods:\ncharAt(i) - Returns the character at index i\ncontains(s) - Returns true if s is a substring\nindexOf(s) - Returns the first index of substring s, or -1 if not found\nlength() - Returns the length of the string\nsubstring(i, j) - Extracts characters from index i up to but not including index j\ntoLowerCase() - Converts the string to all lowercase\ntoUpperCase() - Converts the string to all uppercase", - // "helpUrl": "" - // }, { // text 1 "type": "praxly_charAt_block", "message0": "%1.charAt (%2)", @@ -745,24 +709,6 @@ export function definePraxlyBlocks(workspace) { "helpUrl": "" }, { // logic 1 - "type": "praxly_true_block", - "message0": "true", - "inputsInline": true, - "output": "Boolean", - "style": 'logic_blocks', - "tooltip": "The literal value true", - "helpUrl": "" - }, - { // logic 2 - "type": "praxly_false_block", - "message0": "false", - "inputsInline": true, - "output": "Boolean", - "style": 'logic_blocks', - "tooltip": "The literal value false", - "helpUrl": "" - }, - { // logic 3 "type": "praxly_if_block", "style": "logic_blocks", "message0": "if ( %1 ) %2 %3 end if", @@ -785,7 +731,7 @@ export function definePraxlyBlocks(workspace) { "tooltip": "If statement", "helpUrl": "" }, - { // logic 4 + { // logic 2 "type": "praxly_if_else_block", "message0": "if ( %1 ) %2 %3 else %4 %5 end if", "args0": [ @@ -815,7 +761,7 @@ export function definePraxlyBlocks(workspace) { "tooltip": "If-else statement", "helpUrl": "" }, - { // logic 5 + { // logic 3 "type": "praxly_boolean_operators_block", "message0": "%1 %2 %3 %4", "args0": [ @@ -853,7 +799,7 @@ export function definePraxlyBlocks(workspace) { "tooltip": "Logical operators", "helpUrl": "" }, - { // logic 6 + { // logic 4 "type": "praxly_not_block", "message0": "not %1 %2", "args0": [ @@ -871,7 +817,7 @@ export function definePraxlyBlocks(workspace) { "tooltip": "Negates a boolean", "helpUrl": "" }, - { // logic 7 + { // logic 5 "type": "praxly_compare_block", "message0": "%1 %2 %3 %4", "args0": [ @@ -923,6 +869,24 @@ export function definePraxlyBlocks(workspace) { "tooltip": "Relational operators:\n< less than\n> greater than\n≤ less than or equal to\n≥ greater than or equal to\n== equal to\n≠ not equal to", "helpUrl": "" }, + { // logic 6 + "type": "praxly_true_block", + "message0": "true", + "inputsInline": true, + "output": "Boolean", + "style": 'logic_blocks', + "tooltip": "The literal value true", + "helpUrl": "" + }, + { // logic 7 + "type": "praxly_false_block", + "message0": "false", + "inputsInline": true, + "output": "Boolean", + "style": 'logic_blocks', + "tooltip": "The literal value false", + "helpUrl": "" + }, { // loops 1 "type": "praxly_for_loop_block", "message0": "for (%1 ; %2 ; %3 )%4 %5 end for", @@ -955,7 +919,7 @@ export function definePraxlyBlocks(workspace) { "tooltip": "For loop (repeat a specific number of times)", "helpUrl": "" }, - { // variables + { // for loop initialize "type": "praxly_assignment_expression_block", "message0": "%1%2 ⬅ %3 %4", "args0": [ @@ -991,7 +955,7 @@ export function definePraxlyBlocks(workspace) { "tooltip": "Declares and initializes the loop variable", "helpUrl": "" }, - { // variables + { // for loop reassignment "type": "praxly_reassignment_expression_block", "message0": "%1⬅%2 %3", "args0": [ @@ -1158,11 +1122,6 @@ export function definePraxlyBlocks(workspace) { 'alt': '*', }, - // { - // "type": "field_input", - // "name": "LITERAL", - // "text": "parameterName" - // }, { 'type': 'field_image', 'src': 'images/icons8-minus-50.png', @@ -1272,75 +1231,6 @@ export function definePraxlyBlocks(workspace) { "tooltip": "Expression statement for calling a void procedure", "helpUrl": "" }, - { // not used - "type": "praxly_null_block", - "message0": "null", - "inputsInline": true, - "output": null, - "style": 'expression_blocks', - "tooltip": "", - "helpUrl": "" - }, - { // not used - "type": "praxly_class_block", - "message0": "class %1 %2 end class", - "args0": [ - { - "type": "input_dummy" - }, - { - "type": "input_statement", - "name": "class" - } - ], - "inputsInline": true, - "previousStatement": null, - "nextStatement": null, - "style": "class_blocks", - "tooltip": "", - "helpUrl": "" - }, - { // not used - "type": "praxly_String_block", - "message0": "\"%1\"", - "args0": [ - { - "type": "field_input", - "name": "LITERAL", - "text": "String" - } - ], - "output": null, - "style": 'expression_blocks', - "tooltip": "", - "helpUrl": "" - }, - { // not used - "type": "custom_operation_block", - "message0": "Custom Operation %1 %2", - "args0": [ - { - "type": "input_value", - "name": "input1", - "check": "Number", - "align": "RIGHT", - "defaultType": "math_number", - "defaultValue": 5 - }, - { - "type": "input_value", - "name": "input2", - "check": "Number", - "align": "RIGHT", - "defaultType": "math_number", - "defaultValue": 10 - } - ], - "output": "Number", - "colour": 230, - "tooltip": "Perform custom operation", - "helpUrl": "" - } ]); // The default context menu from Blockly has an item for annotating a block diff --git a/src/toolbox.js b/src/toolbox.js index 7d78699..0bcc9c7 100644 --- a/src/toolbox.js +++ b/src/toolbox.js @@ -41,6 +41,7 @@ export const toolbox = { }, ] }, + { "kind": "category", "name": "variables", @@ -131,15 +132,12 @@ export const toolbox = { } ] }, + { "kind": "category", "name": "math", "categorystyle": "math_blocks", "contents": [ - // { - // 'kind': 'block', - // 'type': 'praxly_String_block' - // }, { 'kind': 'block', 'type': 'praxly_arithmetic_block', @@ -328,30 +326,12 @@ export const toolbox = { } ] }, + { "kind": "category", "name": "text", "categorystyle": "text_blocks", "contents": [ - // { - // 'kind': 'block', - // 'type': 'praxly_StringFunc_block', - // 'inputs': { - // 'EXPRESSION': { - // 'shadow': { - // 'type': 'praxly_literal_block', - // 'fields': { - // 'LITERAL': '\"hello, world\"', - // } - // }, - // }, - // 'PARAMS': { - // 'block': { - // 'type': 'praxly_parameter_block', - // } - // } - // } - // }, { 'kind': 'block', 'type': 'praxly_charAt_block', @@ -395,103 +375,104 @@ export const toolbox = { } } } - }, - { - 'kind': 'block', - 'type': 'praxly_indexOf_block', - 'inputs': { - 'EXPRESSION': { - 'shadow': { - 'type': 'praxly_literal_block', - 'fields': { - 'LITERAL': '"Hello"', - } - }, + }, + { + 'kind': 'block', + 'type': 'praxly_indexOf_block', + 'inputs': { + 'EXPRESSION': { + 'shadow': { + 'type': 'praxly_literal_block', + 'fields': { + 'LITERAL': '"Hello"', + } }, - 'PARAM': { - 'block': { - 'type': 'praxly_literal_block', - 'fields': { - 'LITERAL': 'string' - } + }, + 'PARAM': { + 'block': { + 'type': 'praxly_literal_block', + 'fields': { + 'LITERAL': 'string' } } } - }, - { - 'kind': 'block', - 'type': 'praxly_length_block', - 'inputs': { - 'EXPRESSION': { - 'shadow': { - 'type': 'praxly_literal_block', - 'fields': { - 'LITERAL': '"Hello"', - } - }, - } - } - }, - { - 'kind': 'block', - 'type': 'praxly_substring_block', - 'inputs': { - 'EXPRESSION': { - 'shadow': { - 'type': 'praxly_literal_block', - 'fields': { - 'LITERAL': '"Hello"', - } - }, + } + }, + { + 'kind': 'block', + 'type': 'praxly_length_block', + 'inputs': { + 'EXPRESSION': { + 'shadow': { + 'type': 'praxly_literal_block', + 'fields': { + 'LITERAL': '"Hello"', + } }, - 'PARAM1': { - 'block': { - 'type': 'praxly_literal_block', - 'fields': { - 'LITERAL': 'indexStart' - } + } + } + }, + { + 'kind': 'block', + 'type': 'praxly_substring_block', + 'inputs': { + 'EXPRESSION': { + 'shadow': { + 'type': 'praxly_literal_block', + 'fields': { + 'LITERAL': '"Hello"', } }, - 'PARAM2': { - 'block': { - 'type': 'praxly_literal_block', - 'fields': { - 'LITERAL': 'indexEnd' - } + }, + 'PARAM1': { + 'block': { + 'type': 'praxly_literal_block', + 'fields': { + 'LITERAL': 'indexStart' + } + } + }, + 'PARAM2': { + 'block': { + 'type': 'praxly_literal_block', + 'fields': { + 'LITERAL': 'indexEnd' } } } - }, - { - 'kind': 'block', - 'type': 'praxly_toLowerCase_block', - 'inputs': { - 'EXPRESSION': { - 'shadow': { - 'type': 'praxly_literal_block', - 'fields': { - 'LITERAL': '"Hello"', - } - }, + } + }, + { + 'kind': 'block', + 'type': 'praxly_toLowerCase_block', + 'inputs': { + 'EXPRESSION': { + 'shadow': { + 'type': 'praxly_literal_block', + 'fields': { + 'LITERAL': '"Hello"', + } }, - } - }, - { - 'kind': 'block', - 'type': 'praxly_toUpperCase_block', - 'inputs': { - 'EXPRESSION': { - 'shadow': { - 'type': 'praxly_literal_block', - 'fields': { - 'LITERAL': '"Hello"', - } - }, + }, + } + }, + { + 'kind': 'block', + 'type': 'praxly_toUpperCase_block', + 'inputs': { + 'EXPRESSION': { + 'shadow': { + 'type': 'praxly_literal_block', + 'fields': { + 'LITERAL': '"Hello"', + } }, - } - }, + }, + } + }, ] }, + { "kind": "category", "name": "logic", @@ -578,6 +559,7 @@ export const toolbox = { } ] }, + { "kind": "category", "name": "loops", @@ -674,7 +656,7 @@ export const toolbox = { { 'kind': 'block', 'type': 'praxly_while_loop_block', - 'inputs' : { + 'inputs': { 'CONDITION': { 'block': { 'type': 'praxly_compare_block', @@ -706,7 +688,7 @@ export const toolbox = { { 'kind': 'block', 'type': 'praxly_do_while_loop_block', - 'inputs' : { + 'inputs': { 'CONDITION': { 'block': { 'type': 'praxly_compare_block', @@ -738,7 +720,7 @@ export const toolbox = { { 'kind': 'block', 'type': 'praxly_repeat_until_loop_block', - 'inputs' : { + 'inputs': { 'CONDITION': { 'block': { 'type': 'praxly_compare_block', @@ -770,37 +752,6 @@ export const toolbox = { ] }, - // { - // "kind": "category", - // "name": "statements", - // "categorystyle": "array_blocks", - // "contents": [ - // { - // 'kind': 'block', - // 'type': 'custom_operation_block' - // }, - // ] - // }, - // { - // "kind": "category", - // "name": "experimental", - // "categorystyle" : "class_blocks", - // // "categorystyle": "class_blocks", - // "contents": [ - // { - // 'kind': 'block', - // 'type': 'praxly_parameter_block', - // 'extraState': { - // 'arity': 3, - // } - // }, - // { - // 'kind': 'block', - // 'type': 'praxly_singular_param_block' - // }, - // ] - // }, - { "kind": "category", "name": "procedures", diff --git a/src/tree2text.js b/src/tree2text.js index 5fd4978..40faa6a 100644 --- a/src/tree2text.js +++ b/src/tree2text.js @@ -1,5 +1,4 @@ import { NODETYPES, TYPES } from "./common"; -import { text2tree } from "./text2tree"; export const tree2text = (node, indentation) => { if (!node?.type) { From a8ac7c1af66feef487b247c39141e963783a2261 Mon Sep 17 00:00:00 2001 From: Chris Mayfield Date: Wed, 7 Aug 2024 09:41:34 -0400 Subject: [PATCH 02/21] reset button again clears the output and variables --- main.html | 8 ++++---- src/main.js | 1 + 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/main.html b/main.html index 9773712..9964568 100644 --- a/main.html +++ b/main.html @@ -149,10 +149,10 @@

Example Programs

- - - - + + + + diff --git a/src/main.js b/src/main.js index 19c37c2..5ec0b54 100644 --- a/src/main.js +++ b/src/main.js @@ -411,6 +411,7 @@ function reset() { // reload the code if the URL hasn't changed textEditor.setValue(configuration.code ?? "", 1); turnCodeToBlocks(); + clear(); } } From 40d8dd1d2de46ee47a1bb0fe05fc23836179931c Mon Sep 17 00:00:00 2001 From: Chris Mayfield Date: Wed, 7 Aug 2024 09:51:42 -0400 Subject: [PATCH 03/21] random() should return type double --- src/ast.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/ast.js b/src/ast.js index 19b96f1..ca89a7c 100644 --- a/src/ast.js +++ b/src/ast.js @@ -573,7 +573,6 @@ class Praxly_print { } } - class Praxly_input { constructor(node) { @@ -598,9 +597,9 @@ class Praxly_random { async evaluate(environment) { // Pure-rand only generates integers. That's strange. We'll generate an // integer in a range and normalize it. - const max = 10000; + const max = 2e9; const x = prand.unsafeUniformIntDistribution(0, max - 1, environment.global.random.generator) / max; - return new Praxly_float(x, this.json); + return new Praxly_double(x, this.json); } } From 0e4cdec6c1da19ed76d0db6736898fcb53f12590 Mon Sep 17 00:00:00 2001 From: Chris Johnson Date: Wed, 7 Aug 2024 09:58:00 -0400 Subject: [PATCH 04/21] Have Escape cancel input. --- src/ast.js | 11 ++++++----- src/common.js | 2 +- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/ast.js b/src/ast.js index 8870e45..549db63 100644 --- a/src/ast.js +++ b/src/ast.js @@ -563,11 +563,12 @@ class Praxly_input { } async evaluate(environment) { - const result = await consoleInput(); - // if (result === null) { - // throw new PraxlyError("input canceled", this.json.line); - // } - return new Praxly_String(result, this.json); + try { + const result = await consoleInput(); + return new Praxly_String(result, this.json); + } catch (e) { + throw new PraxlyError("input canceled", this.json.line); + } } } diff --git a/src/common.js b/src/common.js index 6f35baa..e54fcac 100644 --- a/src/common.js +++ b/src/common.js @@ -153,7 +153,7 @@ export function consoleInput() { inputElement.classList.remove('prompt'); blocker.style.display = 'none'; } else if (event.key === 'Escape') { - // TODO: what should we do on escape? + reject(); } }; inputElement.addEventListener('keyup', listener); From 7d58193340fd872f93b800185a7d25e5947810d8 Mon Sep 17 00:00:00 2001 From: Chris Mayfield Date: Wed, 7 Aug 2024 10:04:40 -0400 Subject: [PATCH 05/21] throw Error, not PraxlyError, if ever reach a default --- src/ast.js | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/src/ast.js b/src/ast.js index ca89a7c..7b2754c 100644 --- a/src/ast.js +++ b/src/ast.js @@ -173,7 +173,7 @@ export function createExecutable(tree) { checkArity(tree, 1); return new Praxly_sqrt(createExecutable(tree.args[0]), tree); } else { - throw Error(`unknown builtin function ${tree.name} (line ${tree.line})`); + throw new Error(`unknown builtin function ${tree.name} (line ${tree.line})`); } } @@ -193,7 +193,7 @@ export function createExecutable(tree) { checkArity(tree.right, 2); break; default: - throw Error(`unknown string method ${tree.right.name} (line ${tree.line})`); + throw new Error(`unknown string method ${tree.right.name} (line ${tree.line})`); } var args = []; tree.right.args.forEach((arg) => { @@ -352,7 +352,7 @@ export function createExecutable(tree) { return new Praxly_emptyLine(tree); default: - throw new PraxlyError("unhandled node type " + tree.type, tree.line); + throw new Error(`unhandled node type ${tree.type} (line ${tree.line})`); } } @@ -1209,7 +1209,7 @@ function typeCoercion(varType, praxlyObj, line) { newValue = String(praxlyObj.value); return new Praxly_String(newValue, praxlyObj.json); default: - throw new PraxlyError("unhandled var type: " + varType, line); + throw new Error(`unhandled var type ${varType} (line ${line})`); } } @@ -1291,7 +1291,7 @@ class Praxly_vardecl { valueEvaluated = new Praxly_String(""); break; default: - throw new PraxlyError("unhandled var type: " + this.json.varType, this.json.line); + throw new Error(`unhandled var type ${this.json.varType} (line ${this.json.line})`); } } if (environment.variableList.hasOwnProperty(this.name)) { @@ -1679,6 +1679,7 @@ class Praxly_String_funccall { throw new PraxlyError(`argument ${parameterName} does not match parameter type.\n\tExpected: ${expected_type}\n\tActual: ${argument.realType}`, this.json.line); } } + async evaluate(environment) { var str = await this.receiver.evaluate(environment); var result; @@ -1712,7 +1713,7 @@ class Praxly_String_funccall { result = str.value.substring(startIndex.value, endIndex.value); return new Praxly_String(result); default: - throw new PraxlyError("unhandled string method " + this.name, this.json.line); + throw new Error(`unhandled string method ${this.name} (line ${this.json.line})`); } } @@ -1761,7 +1762,7 @@ function can_assign(varType, expressionType, line) { } else if (varType === TYPES.CHAR) { return expressionType === TYPES.CHAR; } else { - throw Error(`unknown variable type ${varType} (line ${line})`); + throw new Error(`unknown variable type ${varType} (line ${line})`); } } @@ -1847,8 +1848,7 @@ function can_compare(operation, type1, type2, json) { return TYPES.BOOLEAN; // Result of comparison is always a boolean } } - throw new PraxlyError(`bad operand types for ${operation}, \n\tleft: ${type1}\n\tright: ${type2}`, json.line); - // throw new PraxlyError(`bad operand types for ${operation}, \n\tleft: ${type1}\n\tright: ${type2}`, json.line);// Invalid comparison operation + throw new PraxlyError(`bad operand types for ${operation}, \n\tleft: ${type1}\n\tright: ${type2}`, json.line);// Invalid comparison operation } // yea I know this is sloppy but I am getting tired and I'm running outta time @@ -1862,8 +1862,7 @@ function can_negate(operation, type1, json) { return type1; } } - throw new PraxlyError(`bad operand types for ${operation}, \n\tchild: ${type1}`, json.line); - // throw new PraxlyError(`bad operand types for ${operation}, \n\tleft: ${type1}\n\tright: ${type2}`, json.line);// Invalid comparison operation + throw new PraxlyError(`bad operand types for ${operation}, \n\tchild: ${type1}`, json.line);// Invalid negation } /** @@ -1914,7 +1913,7 @@ function binop_typecheck(operation, type1, type2, json) { return can_compare(operation, type1, type2, json); default: - throw new PraxlyError("unhandled operation " + operation, json.line); + throw new Error(`unhandled operation ${operation} (line ${json.line})`); } } @@ -1937,6 +1936,6 @@ function litNode_new(type, value, json) { case TYPES.INVALID: return new Praxly_invalid(); default: - throw new PraxlyError("unhandled literal type " + type, json.line); + throw new Error(`unhandled literal type ${type} (line ${json.line})`); } } From f13ca98301920bb4729c31b878b9fbfdc49b04b6 Mon Sep 17 00:00:00 2001 From: Chris Johnson Date: Wed, 7 Aug 2024 09:58:00 -0400 Subject: [PATCH 06/21] Have Escape cancel input. --- src/ast.js | 11 ++++++----- src/common.js | 2 +- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/ast.js b/src/ast.js index 19b96f1..f45e864 100644 --- a/src/ast.js +++ b/src/ast.js @@ -581,11 +581,12 @@ class Praxly_input { } async evaluate(environment) { - const result = await consoleInput(); - // if (result === null) { - // throw new PraxlyError("input canceled", this.json.line); - // } - return new Praxly_String(result, this.json); + try { + const result = await consoleInput(); + return new Praxly_String(result, this.json); + } catch (e) { + throw new PraxlyError("input canceled", this.json.line); + } } } diff --git a/src/common.js b/src/common.js index 6d90257..24bc01e 100644 --- a/src/common.js +++ b/src/common.js @@ -175,7 +175,7 @@ export function consoleInput() { inputElement.classList.remove('prompt'); blocker.style.display = 'none'; } else if (event.key === 'Escape') { - // TODO: what should we do on escape? + reject(); } }; inputElement.addEventListener('keyup', listener); From e6d7ff4f55d8e623318873088cc3d781e576f2c0 Mon Sep 17 00:00:00 2001 From: Chris Johnson Date: Wed, 7 Aug 2024 10:19:24 -0400 Subject: [PATCH 07/21] Clear errors between runs. --- src/common.js | 10 ++++++---- src/main.js | 2 +- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/common.js b/src/common.js index 24bc01e..e6fab19 100644 --- a/src/common.js +++ b/src/common.js @@ -168,14 +168,16 @@ export function consoleInput() { return new Promise((resolve, reject) => { const listener = event => { - if (event.key === 'Enter') { - resolve(inputElement.value); + if (event.key === 'Enter' || event.key === 'Escape') { inputElement.removeEventListener('keyup', listener); inputElement.readOnly = true; inputElement.classList.remove('prompt'); blocker.style.display = 'none'; - } else if (event.key === 'Escape') { - reject(); + if (event.key === 'Enter') { + resolve(inputElement.value); + } else if (event.key === 'Escape') { + reject(); + } } }; inputElement.addEventListener('keyup', listener); diff --git a/src/main.js b/src/main.js index 5ec0b54..dbda0a3 100644 --- a/src/main.js +++ b/src/main.js @@ -433,9 +433,9 @@ function refresh() { * this function gets called every time the run button is pressed. */ async function runTasks(startDebug) { - // clear previous results clear(); + clearErrors(); await refresh(); // abort if compile-time error From 5dc7e4d1bd2c21e4dde3248fe0f897976fbef810 Mon Sep 17 00:00:00 2001 From: Chris Mayfield Date: Wed, 7 Aug 2024 11:43:09 -0400 Subject: [PATCH 08/21] review, simplify, and enhance binary operator type checking --- src/ast.js | 176 +++++++++++++++++++---------------------------- src/text2tree.js | 4 +- 2 files changed, 73 insertions(+), 107 deletions(-) diff --git a/src/ast.js b/src/ast.js index 7b2754c..8d7551a 100644 --- a/src/ast.js +++ b/src/ast.js @@ -968,10 +968,10 @@ class Praxly_equals { } async evaluate(environment) { - var left = await this.a_operand.evaluate(environment); - var right = await this.b_operand.evaluate(environment); + var a = await this.a_operand.evaluate(environment); + var b = await this.b_operand.evaluate(environment); await stepInto(environment, this.json); - return new Praxly_boolean(left.value === right.value); + return litNode_new(binop_typecheck(OP.EQUALITY, a.realType, b.realType, this.json), a.value === b.value); } } @@ -986,10 +986,10 @@ class Praxly_not_equals { } async evaluate(environment) { - var left = await this.a_operand.evaluate(environment); - var right = await this.b_operand.evaluate(environment); + var a = await this.a_operand.evaluate(environment); + var b = await this.b_operand.evaluate(environment); await stepInto(environment, this.json); - return new Praxly_boolean(left.value != await right.value); + return litNode_new(binop_typecheck(OP.INEQUALITY, a.realType, b.realType, this.json), a.value !== b.value); } } @@ -1004,10 +1004,10 @@ class Praxly_greater_than { } async evaluate(environment) { - var left = await this.a_operand.evaluate(environment); - var right = await this.b_operand.evaluate(environment); + var a = await this.a_operand.evaluate(environment); + var b = await this.b_operand.evaluate(environment); await stepInto(environment, this.json); - return new Praxly_boolean(left.value > right.value); + return litNode_new(binop_typecheck(OP.GREATER_THAN, a.realType, b.realType, this.json), a.value > b.value); } } @@ -1022,10 +1022,10 @@ class Praxly_less_than { } async evaluate(environment) { - var left = await this.a_operand.evaluate(environment); - var right = await this.b_operand.evaluate(environment); + var a = await this.a_operand.evaluate(environment); + var b = await this.b_operand.evaluate(environment); await stepInto(environment, this.json); - return new Praxly_boolean(left.value < right.value); + return litNode_new(binop_typecheck(OP.LESS_THAN, a.realType, b.realType, this.json), a.value < b.value); } } @@ -1040,10 +1040,10 @@ class Praxly_greater_than_equal { } async evaluate(environment) { - var left = await this.a_operand.evaluate(environment); - var right = await this.b_operand.evaluate(environment); + var a = await this.a_operand.evaluate(environment); + var b = await this.b_operand.evaluate(environment); await stepInto(environment, this.json); - return new Praxly_boolean(left.value >= right.value); + return litNode_new(binop_typecheck(OP.GREATER_THAN_OR_EQUAL, a.realType, b.realType, this.json), a.value >= b.value); } } @@ -1058,10 +1058,10 @@ class Praxly_less_than_equal { } async evaluate(environment) { - var left = await this.a_operand.evaluate(environment); - var right = await this.b_operand.evaluate(environment); + var a = await this.a_operand.evaluate(environment); + var b = await this.b_operand.evaluate(environment); await stepInto(environment, this.json); - return new Praxly_boolean(left.value <= right.value); + return litNode_new(binop_typecheck(OP.LESS_THAN_OR_EQUAL, a.realType, b.realType, this.json), a.value <= b.value); } } @@ -1238,8 +1238,8 @@ class Praxly_assignment { let valueEvaluated = await this.value.evaluate(environment); let currentStoredVariableEvaluated = await this.location.evaluate(environment); if (!can_assign(currentStoredVariableEvaluated.realType, valueEvaluated.realType, this.json.line)) { - throw new PraxlyError(`Error: variable reassignment does not match declared type: \n\t Expected: ` - + `${currentStoredVariableEvaluated.realType}, \n\t Actual: ${valueEvaluated.realType}`, this.json.line); + throw new PraxlyError(`Variable reassignment does not match declared type (Expected: ` + + `${currentStoredVariableEvaluated.realType}, Actual: ${valueEvaluated.realType})`, this.json.line); } valueEvaluated = typeCoercion(currentStoredVariableEvaluated.realType, valueEvaluated, this.json.line); @@ -1322,7 +1322,8 @@ class Praxly_array_assignment { let valueEvaluated = await this.value.evaluate(environment); for (var k = 0; k < valueEvaluated.elements.length; k++) { if (!can_assign(this.json.varType, valueEvaluated.elements[k].realType, this.json.line)) { - throw new PraxlyError(`at least one element in the array did not match declared type:\n\texpected type: ${this.json.varType} \n\texpression type: ${valueEvaluated.realType}`, this.json.line); + throw new PraxlyError(`Array element did not match declared type ` + + `(Expected: ${this.json.varType}, Actual: ${valueEvaluated.elements[k].realType})`, this.json.line); } valueEvaluated.elements[k] = typeCoercion(this.json.varType, valueEvaluated.elements[k], this.json.line); } @@ -1538,7 +1539,7 @@ class Praxly_not { async evaluate(environment) { var a = await this.expression.evaluate(environment); - return new litNode_new(binop_typecheck(OP.NOT, a.realType, this.json), !a.value, this.json); + return new litNode_new(binop_typecheck(OP.NOT, a.realType, null, this.json), !a.value, this.json); } } @@ -1551,7 +1552,7 @@ class Praxly_negate { async evaluate(environment) { var a = await this.expression.evaluate(environment); - return new litNode_new(binop_typecheck(OP.NEGATE, a.realType, this.json), -1 * a.value, this.json); + return new litNode_new(binop_typecheck(OP.NEGATE, a.realType, null, this.json), -1 * a.value, this.json); } } @@ -1629,7 +1630,7 @@ class Praxly_function_call { if (can_assign(parameterType, argument.realType, this.json.line)) { newScope.variableList[parameterName] = argument; } else { - throw new PraxlyError(`argument ${parameterName} does not match parameter type.\n\tExpected: ${parameterType}\n\tActual: ${argument.realType}`, this.json.line); + throw new PraxlyError(`Argument ${parameterName} does not match parameter type (Expected: ${parameterType}, Actual: ${argument.realType})`, this.json.line); } } @@ -1657,7 +1658,7 @@ class Praxly_function_call { throw new PraxlyError(`function ${this.name} missing return statement`, this.json.line); } } else if (!can_assign(returnType, result.realType, this.json.line)) { - throw new PraxlyError(`function ${this.name} returned the wrong type.\n\tExpected: ${returnType}\n\tActual: ${result.realType}`, this.json.line); + throw new PraxlyError(`Function ${this.name} returned the wrong type (Expected: ${returnType}, Actual: ${result.realType})`, this.json.line); } // extra debugger step to highlight the function call after returning @@ -1676,7 +1677,7 @@ class Praxly_String_funccall { typecheckhelper(argument, expected_types) { if (!expected_types.includes(argument.realType)) { - throw new PraxlyError(`argument ${parameterName} does not match parameter type.\n\tExpected: ${expected_type}\n\tActual: ${argument.realType}`, this.json.line); + throw new PraxlyError(`Argument ${parameterName} does not match parameter type (Expected: ${expected_types}, Actual: ${argument.realType})`, this.json.line); } } @@ -1731,6 +1732,15 @@ class Praxly_emptyLine { } } +function is_integer(varType) { + return varType === TYPES.INT || varType === TYPES.SHORT; +} + +function is_numeric(varType) { + return varType === TYPES.DOUBLE || varType === TYPES.FLOAT + || varType === TYPES.INT || varType === TYPES.SHORT; +} + /** * This function is used to determine if something can be assigned. * @param {*} varType @@ -1748,13 +1758,13 @@ function can_assign(varType, expressionType, line) { expressionType = expressionType.slice(0, -2); } - if (varType === TYPES.INT || varType === TYPES.SHORT) { + if (is_integer(varType)) { if (expressionType === TYPES.DOUBLE || expressionType === TYPES.FLOAT) { throw new PraxlyError(`incompatible types: possible lossy conversion from ${expressionType} to ${varType}`, line); } - return expressionType === TYPES.INT || expressionType === TYPES.SHORT; + return is_integer(expressionType); } else if (varType === TYPES.DOUBLE || varType === TYPES.FLOAT) { - return expressionType === TYPES.INT || expressionType === TYPES.DOUBLE || expressionType === TYPES.FLOAT || expressionType === TYPES.SHORT; + return is_numeric(expressionType); } else if (varType === TYPES.STRING) { return expressionType === TYPES.STRING || expressionType === TYPES.NULL; } else if (varType === TYPES.BOOLEAN) { @@ -1766,103 +1776,61 @@ function can_assign(varType, expressionType, line) { } } -function can_add(operation, type1, type2, json) { - if (type1 === type2) { - return type1; - } - if (type1 === TYPES.STRING || type2 === TYPES.STRING) { - return TYPES.STRING; - } - if (type1 === TYPES.INT || type1 === TYPES.DOUBLE || type1 === TYPES.FLOAT || type1 === TYPES.SHORT || type1 === TYPES.CHAR) { - if (type2 === TYPES.INT || type2 === TYPES.DOUBLE || type2 === TYPES.FLOAT || type2 === TYPES.SHORT || type2 === TYPES.CHAR) { - return TYPES.DOUBLE; // Result is promoted to double for numeric types - } - } - throw new PraxlyError(`bad operand types for ${operation}, \n\tleft: ${type1}\n\tright: ${type2}`, json.line);// Invalid addition -} - -function can_subtract(operation, type1, type2, json) { - if (type1 === type2) { - return type1; - } - if (type1 === TYPES.INT || type1 === TYPES.DOUBLE || type1 === TYPES.FLOAT || type1 === TYPES.SHORT || type1 === TYPES.CHAR) { - if (type2 === TYPES.INT || type2 === TYPES.DOUBLE || type2 === TYPES.FLOAT || type2 === TYPES.SHORT || type2 === TYPES.CHAR) { - return TYPES.DOUBLE; // Result is promoted to double for numeric types +function can_numeric(operation, type1, type2, json) { + if (is_numeric(type1) && is_numeric(type2)) { + if (type1 === type2) { + return type1; } - } - throw new PraxlyError(`bad operand types for ${operation}, \n\tleft: ${type1}\n\tright: ${type2}`, json.line);// Invalid subtraction -} - -function can_multiply(operation, type1, type2, json) { - if (type1 === type2) { - return type1; - } - if (type1 === TYPES.INT || type1 === TYPES.DOUBLE || type1 === TYPES.FLOAT || type1 === TYPES.SHORT || type1 === TYPES.CHAR) { - if (type2 === TYPES.INT || type2 === TYPES.DOUBLE || type2 === TYPES.FLOAT || type2 === TYPES.SHORT || type2 === TYPES.CHAR) { - return TYPES.DOUBLE; // Result is promoted to double for numeric types + // Different numeric types; promote to integer or double + if (is_integer(type1) && is_integer(type2)) { + return TYPES.INT; + } else { + return TYPES.DOUBLE; } } - throw new PraxlyError(`bad operand types for ${operation}, \n\tleft: ${type1}\n\tright: ${type2}`, json.line);// Invalid multiplication + throw new PraxlyError(`bad operand types for ${operation}, left: ${type1}, right: ${type2}`, json.line); } -function can_divide(operation, type1, type2, json) { - if (type1 === type2) { - return type1; - } - if (type1 === TYPES.INT || type1 === TYPES.DOUBLE || type1 === TYPES.FLOAT || type1 === TYPES.SHORT || type1 === TYPES.CHAR) { - if (type2 === TYPES.INT || type2 === TYPES.DOUBLE || type2 === TYPES.FLOAT || type2 === TYPES.SHORT || type2 === TYPES.CHAR) { - if (type1 === TYPES.INT && type2 === TYPES.INT) { - return TYPES.INT; // Integer division results in an integer - } else { - return TYPES.DOUBLE; // Result is promoted to double for numeric types - } - } +function can_add(operation, type1, type2, json) { + if (type1 === TYPES.BOOLEAN && type2 === TYPES.BOOLEAN) { + throw new PraxlyError(`bad operand types for ${operation} (left: ${type1}, right: ${type2})`, json.line); } - throw new PraxlyError(`bad operand types for ${operation}, \n\tleft: ${type1}\n\tright: ${type2}`, json.line);// Invalid division -} - -function can_modulus(operation, type1, type2, json) { - if (type1 === type2) { - return type1; + if (type1 === TYPES.CHAR && type2 === TYPES.CHAR) { + return TYPES.STRING; // TODO should this be INT like in Java? (would have to do more work to make INT and CHAR interchangeable) } - if (type1 === TYPES.INT || type1 === TYPES.SHORT || type1 === TYPES.DOUBLE || type1 === TYPES.FLOAT) { - if (type2 === TYPES.INT || type2 === TYPES.SHORT || type2 === TYPES.DOUBLE || type2 === TYPES.FLOAT) { - return TYPES.INT; // Modulus of integers is an integer - } + if (type1 === TYPES.STRING || type2 === TYPES.STRING) { + return TYPES.STRING; // Promote to String if either operand is a String } - throw new PraxlyError(`bad operand types for ${operation}, \n\tleft: ${type1}\n\tright: ${type2}`, json.line);// Invalid modulus + return can_numeric(operation, type1, type2, json); } -function can_boolean_operation(operation, type1, type2, json) { - if ((operation === OP.AND || operation === OP.OR) && type1 === TYPES.BOOLEAN && type2 === TYPES.BOOLEAN) { - return TYPES.BOOLEAN; - } else if (operation === OP.NOT && type1 === TYPES.BOOLEAN) { +function can_boolean(operation, type1, type2, json) { + if (type1 === TYPES.BOOLEAN && type2 === TYPES.BOOLEAN) { return TYPES.BOOLEAN; } - throw new PraxlyError(`bad operand types for ${operation}, \n\tleft: ${type1}\n\tright: ${type2}`, json.line);// Invalid boolean operation + throw new PraxlyError(`bad operand types for ${operation} (left: ${type1}, right: ${type2})`, json.line); } function can_compare(operation, type1, type2, json) { - if (operation === OP.EQUALITY || operation === OP.INEQUALITY || operation === OP.GREATER_THAN || operation === OP.LESS_THAN || operation === OP.GREATER_THAN_OR_EQUAL || operation === OP.LESS_THAN_OR_EQUAL) { - if (type1 === type2) { - return TYPES.BOOLEAN; // Result of comparison is always a boolean - } + if (type1 === type2 || is_numeric(type1) && is_numeric(type2)) { + return TYPES.BOOLEAN; } - throw new PraxlyError(`bad operand types for ${operation}, \n\tleft: ${type1}\n\tright: ${type2}`, json.line);// Invalid comparison operation + throw new PraxlyError(`bad operand types for ${operation} (left: ${type1}, right: ${type2})`, json.line); } // yea I know this is sloppy but I am getting tired and I'm running outta time function can_negate(operation, type1, json) { if (operation === OP.NEGATE) { - if (type1 === TYPES.INT || type1 === TYPES.DOUBLE || type1 === TYPES.FLOAT || type1 === TYPES.SHORT) { + if (is_numeric(type1)) { return type1; } - } if (operation === OP.NOT) { + } + if (operation === OP.NOT) { if (type1 === TYPES.BOOLEAN) { return type1; } } - throw new PraxlyError(`bad operand types for ${operation}, \n\tchild: ${type1}`, json.line);// Invalid negation + throw new PraxlyError(`bad operand type for ${operation} (${type1})`, json.line); } /** @@ -1888,21 +1856,19 @@ function binop_typecheck(operation, type1, type2, json) { return can_add(operation, type1, type2, json); case OP.SUBTRACTION: - return can_subtract(operation, type1, type2, json); + return can_numeric(operation, type1, type2, json); case OP.MULTIPLICATION: case OP.EXPONENTIATION: - return can_multiply(operation, type1, type2, json); + return can_numeric(operation, type1, type2, json); case OP.DIVISION: - return can_divide(operation, type1, type2, json); - case OP.MODULUS: - return can_modulus(operation, type1, type2, json); + return can_numeric(operation, type1, type2, json); case OP.AND: case OP.OR: - return can_boolean_operation(operation, type1, type2, json); + return can_boolean(operation, type1, type2, json); case OP.EQUALITY: case OP.INEQUALITY: diff --git a/src/text2tree.js b/src/text2tree.js index 84c6a20..d7c4ee0 100644 --- a/src/text2tree.js +++ b/src/text2tree.js @@ -170,7 +170,7 @@ class Lexer { this.capture(); } if (!this.has("\'")) { - textError('lexing', 'looks like you didn\'t close your quotes on your char. \n \tRemember chars start and end with a single quote mark (\').', this.currentLine + 1); + textError('lexing', 'unclosed character literal (missing single quote mark)', this.currentLine + 1); this.emit_token(NODETYPES.CHAR); continue; } @@ -190,7 +190,7 @@ class Lexer { this.capture(); } if (!this.has("\"")) { - textError('lexing', 'looks like you didn\'t close your quotes on your String. \n \tRemember Strings start and end with a double quote mark (\").', this.currentLine + 1); + textError('lexing', 'unclosed string literal (missing double quote mark)', this.currentLine + 1); this.emit_token(NODETYPES.STRING); continue; } From dd3871e400b16114bbc42634f2575ba949b85983 Mon Sep 17 00:00:00 2001 From: Chris Mayfield Date: Wed, 7 Aug 2024 12:12:54 -0400 Subject: [PATCH 09/21] move clearErrors() after input canceled to end of runTasks() --- src/main.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/main.js b/src/main.js index dbda0a3..8df81b2 100644 --- a/src/main.js +++ b/src/main.js @@ -433,9 +433,9 @@ function refresh() { * this function gets called every time the run button is pressed. */ async function runTasks(startDebug) { + // clear previous results clear(); - clearErrors(); await refresh(); // abort if compile-time error @@ -501,7 +501,10 @@ async function runTasks(startDebug) { stdErr.innerHTML = errorOutput; if (getDebugMode()) { stopButton.click(); - } + } + if (errorOutput.endsWith("input canceled")) { + clearErrors(); // no errors in the code + } } else { // successful run; replace special chars var pos = textEditor.getCursorPosition(); From 7be55d8c719c9e04586d1680bfd8863ca771c3fd Mon Sep 17 00:00:00 2001 From: Chris Mayfield Date: Wed, 7 Aug 2024 12:28:38 -0400 Subject: [PATCH 10/21] combine event listeners for keyboard shortcuts --- src/main.js | 38 ++++++++++++++++++++------------------ 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/src/main.js b/src/main.js index 8df81b2..1d1750f 100644 --- a/src/main.js +++ b/src/main.js @@ -35,6 +35,7 @@ let stdOut; let stdErr; let examples; +let exampleModal; let titleRefresh; let bottomPart; let darkmodediv; @@ -68,6 +69,7 @@ function initializeGlobals() { darkModeButton = document.getElementById('darkMode'); settingsButton = document.getElementById("settings"); examples = document.getElementById('examplesButton'); + exampleModal = document.querySelector('.exampleModal'); titleRefresh = document.getElementById('titleRefresh'); darkmodediv = document.querySelector('.settingsOptions'); toggleText = document.querySelector('#toggle-text'); @@ -152,11 +154,11 @@ function registerListeners() { }); examples.addEventListener('click', function () { - document.querySelector('.exampleModal').style.display = 'flex'; + exampleModal.style.display = 'flex'; }); document.querySelector('.close').addEventListener('click', function () { - document.querySelector('.exampleModal').style.display = 'none'; + exampleModal.style.display = 'none'; }); } else { // embed only @@ -217,29 +219,29 @@ function registerListeners() { }); - // this is how you add custom keybinds! + /* + * Keyboard shortcuts. + */ + document.addEventListener("keydown", function (event) { - // Check if the event key is 's' and Ctrl or Command key is pressed - if ((event.key === 's' || event.key === 'S') && (event.ctrlKey || event.metaKey) || event.key === 'F5') { - // Prevent the default save action (e.g., opening the save dialog, reloading the page) + // F5 or Ctrl+S + if (event.key === 'F5' || (event.ctrlKey || event.metaKey) && (event.key === 's' || event.key === 'S')) { + // Prevent the default action (i.e., reloading the page / opening the save dialog) event.preventDefault(); - runTasks(false); + if ((!examples || exampleModal.style.display !== 'flex') && resetModal.style.display !== 'flex') { + runTasks(false); + } } - }); - - document.addEventListener('keydown', function(event) { + // Escape if (event.key === 'Escape') { if (examples) { - document.querySelector('.exampleModal').style.display = 'none'; - } - if (resetModal.style.display = 'flex') { - resetModal.style.display = 'none'; + exampleModal.style.display = 'none'; } + resetModal.style.display = 'none'; } }); - - /** + /* * Event listeners for the main buttons along the top. */ @@ -347,7 +349,7 @@ function generateTable() { link.addEventListener('click', function() { textEditor.setValue(codeText[i].code.trimStart(), -1); textPane.click(); - document.querySelector('.exampleModal').style.display = 'none'; + exampleModal.style.display = 'none'; }); link.classList.add("example_links"); const nameCell = document.createElement("td"); @@ -737,7 +739,7 @@ function synchronizeToConfiguration() { if (configuration.code) { textEditor.setValue(configuration.code, 1); } else if (examples) { - document.querySelector('.exampleModal').style.display = 'flex'; + exampleModal.style.display = 'flex'; } stepButton.style.display = 'none'; stopButton.style.display = 'none'; From 181ff2e8920c0c3e0c0f4446378e4aa1550199a8 Mon Sep 17 00:00:00 2001 From: Chris Mayfield Date: Wed, 7 Aug 2024 12:36:28 -0400 Subject: [PATCH 11/21] add F10 as a shortcut key for the debugger --- src/main.js | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/main.js b/src/main.js index 1d1750f..d37f452 100644 --- a/src/main.js +++ b/src/main.js @@ -224,15 +224,20 @@ function registerListeners() { */ document.addEventListener("keydown", function (event) { - // F5 or Ctrl+S if (event.key === 'F5' || (event.ctrlKey || event.metaKey) && (event.key === 's' || event.key === 'S')) { - // Prevent the default action (i.e., reloading the page / opening the save dialog) - event.preventDefault(); + event.preventDefault(); // reloading the page / opening the save dialog if ((!examples || exampleModal.style.display !== 'flex') && resetModal.style.display !== 'flex') { runTasks(false); } } - // Escape + if (event.key === 'F10') { + event.preventDefault(); // browser menu + if (getDebugMode()) { + stepButton.click(); + } else { + debugButton.click(); + } + } if (event.key === 'Escape') { if (examples) { exampleModal.style.display = 'none'; From 0b0000d82620c08cde34c1673ab02814dd86caa5 Mon Sep 17 00:00:00 2001 From: Chris Mayfield Date: Wed, 7 Aug 2024 12:55:01 -0400 Subject: [PATCH 12/21] fix string concatenation with floating-point --- src/ast.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/ast.js b/src/ast.js index 66f24c4..22ed7d9 100644 --- a/src/ast.js +++ b/src/ast.js @@ -821,8 +821,12 @@ class Praxly_addition { let a = await this.a_operand.evaluate(environment); let b = await this.b_operand.evaluate(environment); await stepInto(environment, this.json); - return litNode_new(binop_typecheck(OP.ADDITION, a.realType, - b.realType, this.json), a.value + b.value); + if (a.realType === TYPES.STRING || b.realType === TYPES.STRING) { + // Special case: string concatenation + return litNode_new(TYPES.STRING, + valueToString(a, false, this.json.line) + valueToString(b, false, this.json.line)); + } + return litNode_new(binop_typecheck(OP.ADDITION, a.realType, b.realType, this.json), a.value + b.value); } } From 84182873da6932d508cfc2dcf2bc21798f3d4b3b Mon Sep 17 00:00:00 2001 From: Chris Mayfield Date: Wed, 7 Aug 2024 13:53:48 -0400 Subject: [PATCH 13/21] add index bounds checking for charAt and substring --- src/ast.js | 23 ++++++++++++++++------- src/toolbox.js | 4 ++-- 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/src/ast.js b/src/ast.js index 22ed7d9..808186b 100644 --- a/src/ast.js +++ b/src/ast.js @@ -1235,7 +1235,7 @@ class Praxly_assignment { if (this.location.isArray) { var index = await this.location.index.evaluate(environment); var length = storage[this.location.name].elements.length; - if (index.value >= length) { + if (index.value < 0 || index.value >= length) { throw new PraxlyError(`Array index ${index.value} out of bounds for length ${length}`, this.json.line); } } @@ -1355,7 +1355,7 @@ class Praxly_Location { if (this.isArray) { var index = await this.index.evaluate(environment); var length = storage[this.name].elements.length; - if (index.value >= length) { + if (index.value < 0 || index.value >= length) { throw new PraxlyError(`Array index ${index.value} out of bounds for length ${length}`, this.json.line); } return await storage[this.name].elements[index.value].evaluate(environment); @@ -1686,6 +1686,12 @@ class Praxly_String_funccall { } } + check_bounds(str_value, index_value, inclusive) { + if (index_value < 0 || index_value > str_value.length || inclusive && index_value == str_value.length) { + throw new PraxlyError(`String index ${index_value} out of bounds for length ${str_value.length}`, this.json.line); + } + } + async evaluate(environment) { var str = await this.receiver.evaluate(environment); var result; @@ -1693,6 +1699,7 @@ class Praxly_String_funccall { case StringFuncs.CHARAT: var index = await this.args[0].evaluate(environment); this.typecheckhelper(index, [TYPES.INT, TYPES.SHORT]); + this.check_bounds(str.value, index.value, true); result = str.value[index.value]; return new Praxly_char(result); case StringFuncs.CONTAINS: @@ -1712,11 +1719,13 @@ class Praxly_String_funccall { case StringFuncs.TOUPPERCASE: return new Praxly_String(str.value.toUpperCase()); case StringFuncs.SUBSTRING: - var startIndex = await this.args[0].evaluate(environment); - var endIndex = await this.args[1].evaluate(environment); - this.typecheckhelper(startIndex, [TYPES.INT, TYPES.SHORT]); - this.typecheckhelper(endIndex, [TYPES.INT, TYPES.SHORT]); - result = str.value.substring(startIndex.value, endIndex.value); + var beg = await this.args[0].evaluate(environment); + var end = await this.args[1].evaluate(environment); + this.typecheckhelper(beg, [TYPES.INT, TYPES.SHORT]); + this.typecheckhelper(end, [TYPES.INT, TYPES.SHORT]); + this.check_bounds(str.value, beg.value, false); + this.check_bounds(str.value, end.value, false); + result = str.value.substring(beg.value, end.value); return new Praxly_String(result); default: throw new Error(`unhandled string method ${this.name} (line ${this.json.line})`); diff --git a/src/toolbox.js b/src/toolbox.js index 0bcc9c7..6c97947 100644 --- a/src/toolbox.js +++ b/src/toolbox.js @@ -428,7 +428,7 @@ export const toolbox = { 'block': { 'type': 'praxly_literal_block', 'fields': { - 'LITERAL': 'indexStart' + 'LITERAL': 'beginIndex' } } }, @@ -436,7 +436,7 @@ export const toolbox = { 'block': { 'type': 'praxly_literal_block', 'fields': { - 'LITERAL': 'indexEnd' + 'LITERAL': 'endIndex' } } } From 79f3847d97f658375a0cf4e85335867debccfef1 Mon Sep 17 00:00:00 2001 From: Chris Mayfield Date: Wed, 7 Aug 2024 14:09:46 -0400 Subject: [PATCH 14/21] fix debug state when changing examples or resetting --- src/common.js | 4 +--- src/main.js | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/common.js b/src/common.js index e6fab19..e2be177 100644 --- a/src/common.js +++ b/src/common.js @@ -236,15 +236,14 @@ let debugMode = false; export function setDebugMode(value) { debugMode = value; } - export function getDebugMode() { return debugMode; } + let stepInto = false; export function setStepInto(value) { stepInto = value; } - export function getStepInto() { return stepInto; } @@ -253,7 +252,6 @@ let stopClicked = false; export function setStopClicked(value) { stopClicked = value; } - export function getStopClicked() { return stopClicked; } diff --git a/src/main.js b/src/main.js index d37f452..22a696d 100644 --- a/src/main.js +++ b/src/main.js @@ -460,7 +460,6 @@ async function runTasks(startDebug) { if (startDebug) { showDebug(); setDebugMode(true); - setStopClicked(false); } try { @@ -490,6 +489,7 @@ async function runTasks(startDebug) { environment.global = environment; // Run the compiled program + setStopClicked(false); await executable.evaluate(environment); } catch (error) { From 9a16bdd1af3fff21515d80af205ca4753f87ed51 Mon Sep 17 00:00:00 2001 From: Chris Mayfield Date: Wed, 7 Aug 2024 14:54:50 -0400 Subject: [PATCH 15/21] fix debugger when block programming; don't reformat text when stopping debugger --- src/ast.js | 8 ++++---- src/main.js | 4 ++++ 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/ast.js b/src/ast.js index 808186b..0bafefb 100644 --- a/src/ast.js +++ b/src/ast.js @@ -21,18 +21,18 @@ import prand from 'pure-rand'; const FOR_LOOP_LIMIT = 1000000; const WHILE_LOOP_LIMIT = 1000; -async function stepInto(environment, json) { +async function stepInto(environment, node) { if (getStepInto()) { - let markerId = highlightAstNode(environment, json); + let markerId = highlightAstNode(environment, node); await waitForStep(); textEditor.session.removeMarker(markerId); } } -async function stepOver(environment, json) { +async function stepOver(environment, node) { if (getDebugMode()) { await generateVariableTable(environment, 1); - let markerId = highlightAstNode(environment, json); + let markerId = highlightAstNode(environment, node); await waitForStep(); textEditor.session.removeMarker(markerId); } diff --git a/src/main.js b/src/main.js index 22a696d..628c7ac 100644 --- a/src/main.js +++ b/src/main.js @@ -458,6 +458,7 @@ async function runTasks(startDebug) { // if debug button was clicked if (startDebug) { + turnCodeToBlocks(); // rebuilds indexes for highlight showDebug(); setDebugMode(true); } @@ -496,6 +497,9 @@ async function runTasks(startDebug) { if (error.message === "Stop_Debug") { // special case: abort running (not an error) clear(); + workspace.highlightBlock(null); + textEditor.focus(); + return; } else if (!errorOutput) { // error not previously handled by PraxlyError console.error(error); From 059eb2e096a2aa621ec2e9264cd27216fbd6f8f0 Mon Sep 17 00:00:00 2001 From: Chris Mayfield Date: Wed, 7 Aug 2024 16:31:07 -0400 Subject: [PATCH 16/21] debugger now scrolls line into view if necessary --- src/common.js | 28 +++++++++++------ src/text2tree.js | 30 +++++++++--------- test/highlighting.txt | 71 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 105 insertions(+), 24 deletions(-) create mode 100644 test/highlighting.txt diff --git a/src/common.js b/src/common.js index e2be177..5d03e38 100644 --- a/src/common.js +++ b/src/common.js @@ -186,23 +186,31 @@ export function consoleInput() { /** - * This will highlight a line of code - * @param {number} line the desired line that you want to highlight - * @param {boolean} debug set this flag to true if this is being used for debugging. (it changes the color to green) - * @returns the marker id associated with the marker. This should not be needed. + * This will highlight a line of code. + * old param: {number} line the desired line that you want to highlight + * old param: {boolean} debug set this flag to true if this is being used for debugging. (it changes the color to green) + * old returns: the marker id associated with the marker. */ export function highlightAstNode(environment, node) { - var session = textEditor.session; - - // var errorRange = indexToAceRange(line - 1); - var Range = ace.require('ace/range').Range; if (DEV_LOG) { console.log(`attempting to highlight: `, node.startIndex[0], node.startIndex[1], node.endIndex[0], node.endIndex[1]); } - var errorRange = new Range(node.startIndex[0], node.startIndex[1], node.endIndex[0], node.endIndex[1]); - var markerId = session.addMarker(errorRange, 'step-marker', 'text'); + // scroll the text into view if necessary + if (node.line) { + let row = node.line - 1; + if (!textEditor.isRowFullyVisible(row)) { + // arguments: line number, center vertically, animate the scroll + textEditor.scrollToLine(row, true, true); + } + } + + // highlight the text + var Range = ace.require('ace/range').Range; + var debugRange = new Range(node.startIndex[0], node.startIndex[1], node.endIndex[0], node.endIndex[1]); + var markerId = textEditor.session.addMarker(debugRange, 'step-marker', 'text'); + // highlight the block if (node.blockID) { environment.global.blocklyWorkspace.highlightBlock(node.blockID); } diff --git a/src/text2tree.js b/src/text2tree.js index d7c4ee0..24c043a 100644 --- a/src/text2tree.js +++ b/src/text2tree.js @@ -811,7 +811,6 @@ class Parser { var isArray = false; var type = NODETYPES.VARDECL; var vartype = this.getCurrentToken().value; - var startIndex = this.getCurrentToken().startIndex; this.advance(); if (this.has('[') && this.has_ahead(']')) { this.advance(); @@ -831,13 +830,15 @@ class Parser { location: location, line: this.getCurrentLine(), index: null, - startIndex: startIndex, + startIndex: this.getCurrentToken().startIndex, + endIndex: this.getCurrentToken().endIndex, } // initialization (optional) if (this.hasAny('=', '<-', "←", "⟵")) { this.advance(); result.value = this.parse_expression(); + result.endIndex = this.getCurrentToken().endIndex; } // procedure definition @@ -881,7 +882,6 @@ class Parser { result.codeblock = this.parse_block('end ' + result.name); } - result.endIndex = this.getCurrentToken().endIndex; return result; } @@ -1122,31 +1122,33 @@ class Parser { return this.parse_funcdecl_or_vardecl(); } else { // type conversion function - let contents = this.parse_builtin_function_call(line); + let call = this.parse_builtin_function_call(line); return { type: NODETYPES.STATEMENT, - value: contents, + value: call, blockID: "code", - startIndex: contents?.startIndex, - endIndex: contents?.endIndex, + line: call.line, + startIndex: call?.startIndex, + endIndex: call?.endIndex, }; } } // most likely a function call else { - let contents = this.parse_expression(); + let expr = this.parse_expression(); // special case: assume that assignment is a statement // if in a for loop, later code will rebuild the object - if (contents?.type.endsWith("ASSIGNMENT")) { - return contents; + if (expr?.type.endsWith("ASSIGNMENT")) { + return expr; } result = { type: NODETYPES.STATEMENT, - value: contents, + value: expr, blockID: "code", - startIndex: contents?.startIndex, - endIndex: contents?.endIndex, + line: expr.line, + startIndex: expr?.startIndex, + endIndex: expr?.endIndex, }; } return result; @@ -1174,7 +1176,7 @@ class Parser { if (this.has(NODETYPES.SINGLE_LINE_COMMENT) || this.has(NODETYPES.COMMENT)) { const token = this.advance(); comment = { - type: this.getPrevTokenType(), + type: token.token_type, value: token.value, startIndex: token.startIndex, endIndex: token.endIndex, diff --git a/test/highlighting.txt b/test/highlighting.txt new file mode 100644 index 0000000..ade8b6f --- /dev/null +++ b/test/highlighting.txt @@ -0,0 +1,71 @@ +// common +print "Hello, World!" +String name ← input() +/* skip this line */ + +// variables +boolean b +int i ← 3 +b ← true +print b +float[] a ← {1.2, 3.4} +a[1] ← 5.6 +print a[0] + +// math +print 1 + 2 +print -i +print random() +print randomInt(100) +randomSeed(0) +print int("123") +print float("456.789") +print min(4, 3) +print max(2, 1) +print abs(-5) +print log(2.718) +print sqrt(25) + +// text +print "Hello".charAt(1) +print "Hello".contains("lo") +print "Hello".indexOf("lo") +print "Hello".length() +print "Hello".substring(0, 2) +print "Hello".toLowerCase() +print "Hello".toUpperCase() + +// logic +if (i < 10) + print "Yes" +end if +if (i > 10) + print "Oops" +else + print "Nice" +end if +print b and false +print not b + +// loops +for (int x ← 0; x < 3; x ← x + 1) + print "for " + x +end for +while (i > 2) + print "while " + i + i ← i - 1 +end while +do + print "do " + i +while (i > 2) +repeat + print "repeat " + i +until (i == 2) + +// procedures +int proc(int a, int b) + print "Sum: " + (a + b) + return a + b +end proc +proc(8, 9) +print proc(2, 3) From 15112a8f3fd6ebbae2d49482c37b86f7a12163c9 Mon Sep 17 00:00:00 2001 From: Chris Mayfield Date: Wed, 7 Aug 2024 17:16:21 -0400 Subject: [PATCH 17/21] debug highlight if conditions, return expressions; tweak for loop highlighting --- src/ast.js | 2 ++ src/text2tree.js | 9 +++++++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/ast.js b/src/ast.js index 0bafefb..6357997 100644 --- a/src/ast.js +++ b/src/ast.js @@ -1079,6 +1079,7 @@ class Praxly_if { } async evaluate(environment) { + await stepOver(environment, this.condition.json); var cond = await this.condition.evaluate(environment); if (cond.realType != TYPES.BOOLEAN) { throw new PraxlyError("Invalid condition (must be boolean)", this.json.line); @@ -1100,6 +1101,7 @@ class Praxly_if_else { } async evaluate(environment) { + await stepOver(environment, this.condition.json); var cond = await this.condition.evaluate(environment); if (cond.realType != TYPES.BOOLEAN) { throw new PraxlyError("Invalid condition (must be boolean)", this.json.line); diff --git a/src/text2tree.js b/src/text2tree.js index 24c043a..ca4b4fa 100644 --- a/src/text2tree.js +++ b/src/text2tree.js @@ -811,6 +811,7 @@ class Parser { var isArray = false; var type = NODETYPES.VARDECL; var vartype = this.getCurrentToken().value; + var startIndex = this.getCurrentToken().startIndex; this.advance(); if (this.has('[') && this.has_ahead(']')) { this.advance(); @@ -830,7 +831,7 @@ class Parser { location: location, line: this.getCurrentLine(), index: null, - startIndex: this.getCurrentToken().startIndex, + startIndex: startIndex, endIndex: this.getCurrentToken().endIndex, } @@ -1000,6 +1001,7 @@ class Parser { return result; } this.advance(); + result.initialization.endIndex[1]--; // don't highlight semicolon result.condition = this.parse_expression(); if (this.hasNot(';')) { // 2nd required @@ -1008,12 +1010,13 @@ class Parser { this.advance(); result.increment = this.parse_expression(1); + result.endIndex = this.getCurrentToken().endIndex; if (this.hasNot(')')) { return result; } this.advance(); + result.increment.endIndex[1]--; // don't highlight parenthesis - result.endIndex = this.getCurrentToken().endIndex; result.codeblock = this.parse_block('end for'); return result; } @@ -1045,6 +1048,7 @@ class Parser { } this.advance(); result.condition = this.parse_expression(); + result.endIndex = this.getCurrentToken().endIndex; if (this.hasNot(')')) { return result; } @@ -1097,6 +1101,7 @@ class Parser { const expression = this.parse_expression(); result.type = NODETYPES.RETURN; result.value = expression; + result.endIndex = expression?.endIndex; return result; } From 61b4679066e3ff7ccdb750573e36904088d7992f Mon Sep 17 00:00:00 2001 From: Chris Mayfield Date: Wed, 7 Aug 2024 17:50:59 -0400 Subject: [PATCH 18/21] fix highlighting for expressions, variable declarations, and assignments --- src/text2tree.js | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/text2tree.js b/src/text2tree.js index ca4b4fa..f2be459 100644 --- a/src/text2tree.js +++ b/src/text2tree.js @@ -613,7 +613,9 @@ class Parser { right: r, type: NODETYPES.SPECIAL_STRING_FUNCCALL, blockID: "code", - line: line + line: line, + startIndex: startIndex, + endIndex: r.endIndex, } } return l; @@ -728,7 +730,7 @@ class Parser { startIndex: startIndex, } } - l.endIndex = this.getCurrentToken().endIndex; + l.endIndex = this.tokens[this.i - 1].endIndex; return l; default: @@ -792,6 +794,7 @@ class Parser { line: this.getCurrentLine(), index: null, startIndex: this.tokens[this.i].startIndex, + endIndex: this.tokens[this.i].endIndex, } this.advance(); if (this.has('[')) { @@ -832,14 +835,14 @@ class Parser { line: this.getCurrentLine(), index: null, startIndex: startIndex, - endIndex: this.getCurrentToken().endIndex, + endIndex: location.endIndex, } // initialization (optional) if (this.hasAny('=', '<-', "←", "⟵")) { this.advance(); result.value = this.parse_expression(); - result.endIndex = this.getCurrentToken().endIndex; + result.endIndex = this.tokens[this.i - 1].endIndex; } // procedure definition @@ -1001,7 +1004,6 @@ class Parser { return result; } this.advance(); - result.initialization.endIndex[1]--; // don't highlight semicolon result.condition = this.parse_expression(); if (this.hasNot(';')) { // 2nd required @@ -1015,7 +1017,6 @@ class Parser { return result; } this.advance(); - result.increment.endIndex[1]--; // don't highlight parenthesis result.codeblock = this.parse_block('end for'); return result; From 8b67f60920f3eaec01507380af149dfeffcd0ad4 Mon Sep 17 00:00:00 2001 From: Chris Mayfield Date: Thu, 8 Aug 2024 10:36:57 -0400 Subject: [PATCH 19/21] more robust error handling with try-catch in main.js; complete walkthrough for all blocks and text nodes --- src/ast.js | 23 +++--- src/blocks2tree.js | 106 ++++++++++++------------ src/main.js | 39 +++++++-- src/newBlocks.js | 26 +++--- src/text2tree.js | 24 ++++-- src/toolbox.js | 2 +- src/tree2blocks.js | 201 +++++++++++++++++++++------------------------ src/tree2text.js | 139 ++++++++++--------------------- 8 files changed, 266 insertions(+), 294 deletions(-) diff --git a/src/ast.js b/src/ast.js index 6357997..398b208 100644 --- a/src/ast.js +++ b/src/ast.js @@ -63,12 +63,6 @@ function checkArity(node, expectedArity) { * @returns */ export function createExecutable(tree) { - if (typeof tree === 'undefined' || typeof tree.type === 'undefined') { - if (errorOutput.length === 0) { - defaultError("invalid program (abstract syntax tree is undefined)"); - } - return new Praxly_invalid(tree); - } switch (tree.type) { @@ -345,7 +339,7 @@ export function createExecutable(tree) { case NODETYPES.ARRAY_REFERENCE: return new Praxly_array_reference(tree.name, createExecutable(tree.index), tree); - case 'INVALID': + case NODETYPES.INVALID: return new Praxly_invalid(tree); case NODETYPES.NEWLINE: @@ -612,13 +606,15 @@ class Praxly_random_int { async evaluate(environment) { const maxNode = (await this.max.evaluate(environment)); - if (maxNode.realType === TYPES.INT) { - const maxValue = maxNode.value; - const x = prand.unsafeUniformIntDistribution(0, maxValue - 1, environment.global.random.generator); - return new Praxly_int(x, this.json); - } else { + if (maxNode.realType !== TYPES.INT) { throw new PraxlyError(`randomInt's maximum parameter must be of type int, not ${maxNode.realType}.`, this.json.line); } + const maxValue = maxNode.value; + if (maxValue < 1) { + throw new PraxlyError(`randomInt's maximum parameter must be at least 1`, this.json.line); + } + const x = prand.unsafeUniformIntDistribution(0, maxValue - 1, environment.global.random.generator); + return new Praxly_int(x, this.json); } } @@ -1684,7 +1680,8 @@ class Praxly_String_funccall { typecheckhelper(argument, expected_types) { if (!expected_types.includes(argument.realType)) { - throw new PraxlyError(`Argument ${parameterName} does not match parameter type (Expected: ${expected_types}, Actual: ${argument.realType})`, this.json.line); + const argStr = valueToString(argument, true, this.json.line); + throw new PraxlyError(`Argument ${argStr} does not match parameter type (Expected: ${expected_types}, Actual: ${argument.realType})`, this.json.line); } } diff --git a/src/blocks2tree.js b/src/blocks2tree.js index f88373b..0be77d9 100644 --- a/src/blocks2tree.js +++ b/src/blocks2tree.js @@ -74,8 +74,8 @@ export const makeGenerator = () => { const b = children[1]; const node = { blockID: block.id, - left: praxlyGenerator[a.type](a), - right: praxlyGenerator[b.type](b) + left: praxlyGenerator[a?.type](a), + right: praxlyGenerator[b?.type](b) } node.type = block.getFieldValue('OPERATOR'); return customizeMaybe(block, node); @@ -107,7 +107,7 @@ export const makeGenerator = () => { blockID: block.id, type: NODETYPES.BUILTIN_FUNCTION_CALL, args: [ - praxlyGenerator[expression.type](expression), + praxlyGenerator[expression?.type](expression), ], }); } @@ -119,7 +119,7 @@ export const makeGenerator = () => { blockID: block.id, type: NODETYPES.BUILTIN_FUNCTION_CALL, args: [ - praxlyGenerator[expression.type](expression), + praxlyGenerator[expression?.type](expression), ], }); } @@ -131,7 +131,7 @@ export const makeGenerator = () => { blockID: block.id, type: NODETYPES.BUILTIN_FUNCTION_CALL, args: [ - praxlyGenerator[expression.type](expression), + praxlyGenerator[expression?.type](expression), ], }); } @@ -143,35 +143,35 @@ export const makeGenerator = () => { blockID: block.id, type: NODETYPES.BUILTIN_FUNCTION_CALL, args: [ - praxlyGenerator[expression.type](expression), + praxlyGenerator[expression?.type](expression), ], }); } praxlyGenerator['praxly_min_block'] = (block) => { - const expression = block.getInputTargetBlock('A_MIN'); - const expression2 = block.getInputTargetBlock('B_MIN'); + const a = block.getInputTargetBlock('A_MIN'); + const b = block.getInputTargetBlock('B_MIN'); return customizeMaybe(block, { name: 'min', blockID: block.id, type: NODETYPES.BUILTIN_FUNCTION_CALL, args: [ - praxlyGenerator[expression.type](expression), - praxlyGenerator[expression2.type](expression2), + praxlyGenerator[a?.type](a), + praxlyGenerator[b?.type](b), ], }); } praxlyGenerator['praxly_max_block'] = (block) => { - const expression = block.getInputTargetBlock('A_MAX'); - const expression2 = block.getInputTargetBlock('B_MAX'); + const a = block.getInputTargetBlock('A_MAX'); + const b = block.getInputTargetBlock('B_MAX'); return customizeMaybe(block, { name: 'max', blockID: block.id, type: NODETYPES.BUILTIN_FUNCTION_CALL, args: [ - praxlyGenerator[expression.type](expression), - praxlyGenerator[expression2.type](expression2), + praxlyGenerator[a?.type](a), + praxlyGenerator[b?.type](b), ], }); } @@ -183,7 +183,7 @@ export const makeGenerator = () => { blockID: block.id, type: NODETYPES.BUILTIN_FUNCTION_CALL, args: [ - praxlyGenerator[expression.type](expression), + praxlyGenerator[expression?.type](expression), ], }); } @@ -195,7 +195,7 @@ export const makeGenerator = () => { blockID: block.id, type: NODETYPES.BUILTIN_FUNCTION_CALL, args: [ - praxlyGenerator[expression.type](expression), + praxlyGenerator[expression?.type](expression), ], }); } @@ -207,12 +207,11 @@ export const makeGenerator = () => { blockID: block.id, type: NODETYPES.BUILTIN_FUNCTION_CALL, args: [ - praxlyGenerator[expression.type](expression), + praxlyGenerator[expression?.type](expression), ], }); } - praxlyGenerator['praxly_input_block'] = (block) => { return customizeMaybe(block, { name: 'input', @@ -237,7 +236,7 @@ export const makeGenerator = () => { blockID: block.id, type: NODETYPES.LOCATION, name: block.getFieldValue("VARIABLENAME"), - index: praxlyGenerator[index.type](index), + index: praxlyGenerator[index?.type](index), isArray: true, }) } @@ -306,8 +305,8 @@ export const makeGenerator = () => { const b = children[1]; const node = { blockID: block.id, - left: praxlyGenerator[a.type](a), - right: praxlyGenerator[b.type](b) + left: praxlyGenerator[a?.type](a), + right: praxlyGenerator[b?.type](b) } node.type = block.getFieldValue('OPERATOR'); return customizeMaybe(block, node); @@ -319,8 +318,8 @@ export const makeGenerator = () => { const b = children[1]; const node = { blockID: block.id, - left: praxlyGenerator[a.type](a), - right: praxlyGenerator[b.type](b) + left: praxlyGenerator[a?.type](a), + right: praxlyGenerator[b?.type](b) } node.type = block.getFieldValue('OPERATOR'); return customizeMaybe(block, node); @@ -427,17 +426,17 @@ export const makeGenerator = () => { var varType = block.getFieldValue('VARTYPE'); var variableName = block.getFieldValue('VARIABLENAME'); var args = block.getInputTargetBlock('EXPRESSION'); - var argschildren = args.getChildren(true); + var argschildren = args?.getChildren(true); var argsList = []; - argschildren.forEach(element => { - argsList.push(praxlyGenerator[element.type](element)); + argschildren?.forEach(element => { + argsList.push(praxlyGenerator[element?.type](element)); }); return customizeMaybe(block, { type: 'ARRAY_ASSIGNMENT', name: variableName, value: { - blockID: args.id, + blockID: args?.id, params: argsList, type: NODETYPES.ARRAY_LITERAL, isArray: true, @@ -454,7 +453,7 @@ export const makeGenerator = () => { praxlyGenerator['praxly_reassignment_block'] = (block) => { var variableName = block.getFieldValue('VARIABLENAME'); var expression = block.getInputTargetBlock('EXPRESSION'); - var value = praxlyGenerator[expression.type](expression); + var value = praxlyGenerator[expression?.type](expression); return customizeMaybe(block, { type: NODETYPES.ASSIGNMENT, name: variableName, @@ -493,7 +492,7 @@ export const makeGenerator = () => { var varType = block.getFieldValue('VARTYPE'); var variableName = block.getFieldValue('VARIABLENAME'); var expression = block.getInputTargetBlock('EXPRESSION'); - var value = praxlyGenerator[expression.type](expression); + var value = praxlyGenerator[expression?.type](expression); return customizeMaybe(block, { type: NODETYPES.VARDECL, name: variableName, @@ -510,11 +509,11 @@ export const makeGenerator = () => { praxlyGenerator['praxly_reassignment_expression_block'] = (block) => { var location = block.getInputTargetBlock('LOCATION'); var expression = block.getInputTargetBlock('EXPRESSION'); - var loc = praxlyGenerator[location.type](location); - var value = praxlyGenerator[expression.type](expression); + var loc = praxlyGenerator[location?.type](location); + var value = praxlyGenerator[expression?.type](expression); return customizeMaybe(block, { type: NODETYPES.ASSIGNMENT, - name: loc.name, + name: loc?.name, location: loc, value: value, blockID: block.id, @@ -568,7 +567,7 @@ export const makeGenerator = () => { return customizeMaybe(block, { blockID: block.id, type: NODETYPES.NEGATE, - value: praxlyGenerator[expression.type](expression), + value: praxlyGenerator[expression?.type](expression), }) } @@ -581,21 +580,21 @@ export const makeGenerator = () => { type: NODETYPES.FOR, blockID: block.id, initialization: praxlyGenerator[initialization?.type](initialization), - codeblock: praxlyGenerator['codeBlockJsonBuilder'](statements), - increment: praxlyGenerator[reassignment?.type](reassignment), condition: praxlyGenerator[condition?.type](condition), + increment: praxlyGenerator[reassignment?.type](reassignment), + codeblock: praxlyGenerator['codeBlockJsonBuilder'](statements), }); } praxlyGenerator['praxly_procedure_block'] = (block) => { var returnType = block.getFieldValue('RETURNTYPE'); var args = block.getInputTargetBlock('PARAMS'); - var argschildren = args.getChildren(true); + var argschildren = args?.getChildren(true); var argsList = []; - argschildren.forEach(element => { + argschildren?.forEach(element => { var param = []; - param[0] = (element.getFieldValue('VARTYPE')); - param[1] = element.getFieldValue('VARIABLENAME'); + param[0] = (element?.getFieldValue('VARTYPE')); + param[1] = element?.getFieldValue('VARIABLENAME'); argsList.push(param); }); var procedureName = block.getFieldValue('PROCEDURE_NAME'); @@ -614,10 +613,10 @@ export const makeGenerator = () => { praxlyGenerator['praxly_function_call_block'] = (block) => { var procedureName = block.getFieldValue('PROCEDURE_NAME'); var args = block.getInputTargetBlock('PARAMS'); - var argschildren = args.getChildren(true); + var argschildren = args?.getChildren(true); var argsList = []; - argschildren.forEach(element => { - argsList.push(praxlyGenerator[element.type](element)); + argschildren?.forEach(element => { + argsList.push(praxlyGenerator[element?.type](element)); }); return customizeMaybe(block, { blockID: block.id, @@ -644,10 +643,10 @@ export const makeGenerator = () => { blockID: block.id, name : procedureName, type: NODETYPES.SPECIAL_STRING_FUNCCALL, - left: praxlyGenerator[expression.type](expression), + left: praxlyGenerator[expression?.type](expression), right: { name: procedureName, - args: [praxlyGenerator[index.type](index)], + args: [praxlyGenerator[index?.type](index)], type: NODETYPES.FUNCCALL } }); @@ -661,10 +660,10 @@ export const makeGenerator = () => { blockID: block.id, name: procedureName, type: NODETYPES.SPECIAL_STRING_FUNCCALL, - left: praxlyGenerator[expression.type](expression), + left: praxlyGenerator[expression?.type](expression), right: { name: procedureName, - args: [praxlyGenerator[param.type](param)], + args: [praxlyGenerator[param?.type](param)], type: NODETYPES.FUNCCALL } }); @@ -678,10 +677,10 @@ export const makeGenerator = () => { blockID: block.id, name: procedureName, type: NODETYPES.SPECIAL_STRING_FUNCCALL, - left: praxlyGenerator[expression.type](expression), + left: praxlyGenerator[expression?.type](expression), right: { name: procedureName, - args: [praxlyGenerator[param.type](param)], + args: [praxlyGenerator[param?.type](param)], type: NODETYPES.FUNCCALL } }); @@ -694,7 +693,7 @@ export const makeGenerator = () => { blockID: block.id, name: procedureName, type: NODETYPES.SPECIAL_STRING_FUNCCALL, - left: praxlyGenerator[expression.type](expression), + left: praxlyGenerator[expression?.type](expression), right: { name: procedureName, args: [] @@ -711,10 +710,11 @@ export const makeGenerator = () => { blockID: block.id, name: procedureName, type: NODETYPES.SPECIAL_STRING_FUNCCALL, - left: praxlyGenerator[expression.type](expression), + left: praxlyGenerator[expression?.type](expression), right: { name: procedureName, - args: [praxlyGenerator[param1.type](param1), praxlyGenerator[param2.type](param2)], + args: [praxlyGenerator[param1?.type](param1), + praxlyGenerator[param2?.type](param2)], type: NODETYPES.FUNCCALL } }); @@ -727,7 +727,7 @@ export const makeGenerator = () => { blockID: block.id, name: procedureName, type: NODETYPES.SPECIAL_STRING_FUNCCALL, - left: praxlyGenerator[expression.type](expression), + left: praxlyGenerator[expression?.type](expression), right: { name: procedureName, args: [] @@ -742,7 +742,7 @@ export const makeGenerator = () => { blockID: block.id, name: procedureName, type: NODETYPES.SPECIAL_STRING_FUNCCALL, - left: praxlyGenerator[expression.type](expression), + left: praxlyGenerator[expression?.type](expression), right: { name: procedureName, args: [] diff --git a/src/main.js b/src/main.js index 628c7ac..44200a9 100644 --- a/src/main.js +++ b/src/main.js @@ -436,8 +436,9 @@ function refresh() { }); } + /** - * this function gets called every time the run button is pressed. + * This function gets called every time the run or debug button is pressed. */ async function runTasks(startDebug) { @@ -527,6 +528,22 @@ async function runTasks(startDebug) { } +/** + * Executes code within a try-catch block, and immediately displays + * errors on the screen, prompting the user to submit a bug report. + * @param {Function} fn - The anonymous function to be executed. + */ +function tryToRun(fn) { + try { + fn(); + } catch (error) { + console.error(error); + defaultError(error); + clear(); // show only the error + stdErr.innerHTML = errorOutput; + } +} + export function turnCodeToBlocks() { // only one listener at a time to prevent infinite loop workspace.removeChangeListener(onBlocklyChange); @@ -536,15 +553,19 @@ export function turnCodeToBlocks() { // this is where lexing/parsing begins clearErrors(); - mainTree = text2tree(); + tryToRun(() => { + mainTree = text2tree(); + }); if (DEV_LOG) { console.log("text2tree", mainTree); } // update block side to match workspace.clear(); - tree2blocks(workspace, mainTree); - workspace.render(); + tryToRun(() => { + tree2blocks(workspace, mainTree); + workspace.render(); + }); } function onBlocklyChange(event) { @@ -564,14 +585,18 @@ function turnBlocksToCode() { // this is where block compiling begins clearErrors(); - mainTree = blocks2tree(workspace, praxlyGenerator); + tryToRun(() => { + mainTree = blocks2tree(workspace, praxlyGenerator); + }); if (DEV_LOG) { console.log("blocks2tree", mainTree); } // update text side to match - const text = tree2text(mainTree, 0); - textEditor.setValue(text, -1); + tryToRun(() => { + const text = tree2text(mainTree, 0); + textEditor.setValue(text, -1); + }); }; diff --git a/src/newBlocks.js b/src/newBlocks.js index 0386275..53eb01d 100644 --- a/src/newBlocks.js +++ b/src/newBlocks.js @@ -368,14 +368,14 @@ export function definePraxlyBlocks(workspace) { "/", NODETYPES.DIVISION ], + [ + "^", + NODETYPES.EXPONENTIATION + ], [ "%", NODETYPES.MODULUS ], - [ - "^", - NODETYPES.EXPONENTIATION - ] ] }, { @@ -833,14 +833,6 @@ export function definePraxlyBlocks(workspace) { "==", NODETYPES.EQUALITY ], - [ - "≤", - NODETYPES.LESS_THAN_OR_EQUAL - ], - [ - "≥", - NODETYPES.GREATER_THAN_OR_EQUAL - ], [ "≠", NODETYPES.INEQUALITY @@ -852,7 +844,15 @@ export function definePraxlyBlocks(workspace) { [ ">", NODETYPES.GREATER_THAN - ] + ], + [ + "≤", + NODETYPES.LESS_THAN_OR_EQUAL + ], + [ + "≥", + NODETYPES.GREATER_THAN_OR_EQUAL + ], ] }, { diff --git a/src/text2tree.js b/src/text2tree.js index f2be459..b0d1e83 100644 --- a/src/text2tree.js +++ b/src/text2tree.js @@ -159,6 +159,8 @@ class Lexer { textError('lexing', `looks like you didn\'t close your comment. Remember comments start with a \'/*\' and end with a \'*/\'.`, this.currentLine + 1); } + // Trim whitespace before/after comment text. + this.token_so_far = this.token_so_far.trim() this.emit_token(NODETYPES.COMMENT); continue; } @@ -605,8 +607,8 @@ class Parser { line = this.getCurrentLine(); this.advance(); const r = this.parse_expression(precedence - 1); - if (r.type != NODETYPES.FUNCCALL) { - textError('parsing', "classes are not fully supported yet. the right side of the dot operator must be a supported string function", line); + if (r && r.type != NODETYPES.FUNCCALL) { + textError('parsing', "the right side of the dot operator must be a string method", line); } l = { left: l, @@ -615,7 +617,7 @@ class Parser { blockID: "code", line: line, startIndex: startIndex, - endIndex: r.endIndex, + endIndex: r?.endIndex, } } return l; @@ -835,7 +837,7 @@ class Parser { line: this.getCurrentLine(), index: null, startIndex: startIndex, - endIndex: location.endIndex, + endIndex: location?.endIndex, } // initialization (optional) @@ -969,12 +971,14 @@ class Parser { result.type = NODETYPES.IF; this.advance(); if (this.hasNot('(')) { + textError('parsing', "open parenthesis '(' expected", this.getCurrentLine()); return result; } this.advance(); result.condition = this.parse_expression(); result.endIndex = this.getCurrentToken().endIndex; if (this.hasNot(')')) { + textError('parsing', "closing parenthesis ')' expected", this.getCurrentLine()); return result; } this.advance(); @@ -991,6 +995,7 @@ class Parser { result.type = NODETYPES.FOR; this.advance(); if (this.hasNot('(')) { + textError('parsing', "open parenthesis '(' expected", this.getCurrentLine()); return result; } this.advance(); @@ -1001,12 +1006,14 @@ class Parser { result.initialization = this.parse_expression(1); } if (this.hasNot(';')) { // 1st required + textError('parsing', "semicolon ';' expected", this.getCurrentLine()); return result; } this.advance(); result.condition = this.parse_expression(); if (this.hasNot(';')) { // 2nd required + textError('parsing', "semicolon ';' expected", this.getCurrentLine()); return result; } this.advance(); @@ -1014,6 +1021,7 @@ class Parser { result.increment = this.parse_expression(1); result.endIndex = this.getCurrentToken().endIndex; if (this.hasNot(')')) { + textError('parsing', "closing parenthesis ')' expected", this.getCurrentLine()); return result; } this.advance(); @@ -1027,12 +1035,14 @@ class Parser { result.type = NODETYPES.WHILE; this.advance(); if (this.hasNot('(')) { + textError('parsing', "open parenthesis '(' expected", this.getCurrentLine()); return result; } this.advance(); result.condition = this.parse_expression(); result.endIndex = this.getCurrentToken().endIndex; if (this.hasNot(')')) { + textError('parsing', "closing parenthesis ')' expected", this.getCurrentLine()); return result; } this.advance(); @@ -1045,12 +1055,14 @@ class Parser { this.advance(); result.codeblock = this.parse_block('while'); if (this.hasNot('(')) { + textError('parsing', "open parenthesis '(' expected", this.getCurrentLine()); return result; } this.advance(); result.condition = this.parse_expression(); result.endIndex = this.getCurrentToken().endIndex; if (this.hasNot(')')) { + textError('parsing', "closing parenthesis ')' expected", this.getCurrentLine()); return result; } this.advance(); @@ -1063,11 +1075,13 @@ class Parser { this.advance(); result.codeblock = this.parse_block('until'); if (this.hasNot('(')) { + textError('parsing', "open parenthesis '(' expected", this.getCurrentLine()); return result; } this.advance(); result.condition = this.parse_expression(); if (this.hasNot(')')) { + textError('parsing', "closing parenthesis ')' expected", this.getCurrentLine()); return result; } this.advance(); @@ -1152,7 +1166,7 @@ class Parser { type: NODETYPES.STATEMENT, value: expr, blockID: "code", - line: expr.line, + line: expr?.line, startIndex: expr?.startIndex, endIndex: expr?.endIndex, }; diff --git a/src/toolbox.js b/src/toolbox.js index 6c97947..227897d 100644 --- a/src/toolbox.js +++ b/src/toolbox.js @@ -304,7 +304,7 @@ export const toolbox = { 'shadow': { 'type': 'praxly_literal_block', 'fields': { - 'LITERAL': 2.718 + 'LITERAL': 2.71828 } } } diff --git a/src/tree2blocks.js b/src/tree2blocks.js index ab8a9e8..1929a4b 100644 --- a/src/tree2blocks.js +++ b/src/tree2blocks.js @@ -5,8 +5,9 @@ function connectStatements(statements) { let currentStatement = statements[i]; let nextStatement = statements[i + 1]; if (currentStatement && nextStatement) { - currentStatement.nextConnection.connect(nextStatement.previousConnection); - } else if (currentStatement && !nextStatement) { + currentStatement.nextConnection?.connect(nextStatement.previousConnection); + } + else if (currentStatement && !nextStatement) { // Find the next valid statement in the array var j = i + 2; while (j < statements.length && !statements[j]) { @@ -14,11 +15,11 @@ function connectStatements(statements) { } if (j < statements.length) { // Set the connection from the current statement to the next valid statement - currentStatement.nextConnection.connect(statements[j].previousConnection); + currentStatement.nextConnection?.connect(statements[j].previousConnection); } } else { - console.error("connection failed"); + throw new Error("block connection failed"); } } } @@ -126,68 +127,62 @@ export const tree2blocks = (workspace, node) => { case NODETYPES.ASSOCIATION: var result = tree2blocks(workspace, node?.expression); result.data = JSON.stringify({ - isParenthesized: true, + isParenthesized: true, }); break; case NODETYPES.BUILTIN_FUNCTION_CALL: { if (node.name === 'input') { - result = workspace.newBlock('praxly_input_block'); + result = workspace.newBlock('praxly_input_block'); } else if (node.name === 'random') { - result = workspace.newBlock('praxly_random_block'); + result = workspace.newBlock('praxly_random_block'); } else if (node.name === 'randomInt') { - result = workspace.newBlock('praxly_random_int_block'); - const child = tree2blocks(workspace, node?.args[0]); - result.getInput('MAX').connection.connect(child?.outputConnection); + result = workspace.newBlock('praxly_random_int_block'); + const child = tree2blocks(workspace, node?.args[0]); + result.getInput('MAX').connection.connect(child?.outputConnection); } else if (node.name === 'randomSeed') { - result = workspace.newBlock('praxly_random_seed_block'); - const child = tree2blocks(workspace, node?.args[0]); - result.getInput('SEED').connection.connect(child?.outputConnection); + result = workspace.newBlock('praxly_random_seed_block'); + const child = tree2blocks(workspace, node?.args[0]); + result.getInput('SEED').connection.connect(child?.outputConnection); } else if (node.name === 'int') { - result = workspace.newBlock('praxly_int_conversion_block'); - const child = tree2blocks(workspace, node?.args[0]); - result.getInput('CONVERSION').connection.connect(child?.outputConnection); + result = workspace.newBlock('praxly_int_conversion_block'); + const child = tree2blocks(workspace, node?.args[0]); + result.getInput('CONVERSION').connection.connect(child?.outputConnection); } else if (node.name === 'float') { - result = workspace.newBlock('praxly_float_conversion_block'); - const child = tree2blocks(workspace, node?.args[0]); - result.getInput('CONVERSION').connection.connect(child?.outputConnection); + result = workspace.newBlock('praxly_float_conversion_block'); + const child = tree2blocks(workspace, node?.args[0]); + result.getInput('CONVERSION').connection.connect(child?.outputConnection); } else if (node.name === 'min') { - result = workspace.newBlock('praxly_min_block'); - const child1 = tree2blocks(workspace, node?.args[0]); - result.getInput('A_MIN').connection.connect(child1?.outputConnection); - const child2 = tree2blocks(workspace, node?.args[1]); - result.getInput('B_MIN').connection.connect(child2?.outputConnection); + result = workspace.newBlock('praxly_min_block'); + const child1 = tree2blocks(workspace, node?.args[0]); + result.getInput('A_MIN').connection.connect(child1?.outputConnection); + const child2 = tree2blocks(workspace, node?.args[1]); + result.getInput('B_MIN').connection.connect(child2?.outputConnection); } else if (node.name === 'max') { - result = workspace.newBlock('praxly_max_block'); - const child1 = tree2blocks(workspace, node?.args[0]); - result.getInput('A_MAX').connection.connect(child1?.outputConnection); - const child2 = tree2blocks(workspace, node?.args[1]); - result.getInput('B_MAX').connection.connect(child2?.outputConnection); + result = workspace.newBlock('praxly_max_block'); + const child1 = tree2blocks(workspace, node?.args[0]); + result.getInput('A_MAX').connection.connect(child1?.outputConnection); + const child2 = tree2blocks(workspace, node?.args[1]); + result.getInput('B_MAX').connection.connect(child2?.outputConnection); } else if (node.name === 'abs') { - result = workspace.newBlock('praxly_abs_block'); - const child = tree2blocks(workspace, node?.args[0]); - result.getInput('VALUE').connection.connect(child?.outputConnection); + result = workspace.newBlock('praxly_abs_block'); + const child = tree2blocks(workspace, node?.args[0]); + result.getInput('VALUE').connection.connect(child?.outputConnection); } else if (node.name === 'log') { - result = workspace.newBlock('praxly_log_block'); - const child = tree2blocks(workspace, node?.args[0]); - result.getInput('VALUE').connection.connect(child?.outputConnection); + result = workspace.newBlock('praxly_log_block'); + const child = tree2blocks(workspace, node?.args[0]); + result.getInput('VALUE').connection.connect(child?.outputConnection); } else if (node.name === 'sqrt') { - result = workspace.newBlock('praxly_sqrt_block'); - const child = tree2blocks(workspace, node?.args[0]); - result.getInput('VALUE').connection.connect(child?.outputConnection); + result = workspace.newBlock('praxly_sqrt_block'); + const child = tree2blocks(workspace, node?.args[0]); + result.getInput('VALUE').connection.connect(child?.outputConnection); } break; } case NODETYPES.CODEBLOCK: var statements = node.statements.map(element => { - try { - return tree2blocks(workspace, element); - } - catch (error) { - console.error('invalid statement', error); - return null; - } + return tree2blocks(workspace, element); }); connectStatements(statements); return statements; @@ -407,73 +402,63 @@ export const tree2blocks = (workspace, node) => { case NODETYPES.FOR: var result = workspace.newBlock('praxly_for_loop_block'); - try { - var initialization = tree2blocks(workspace, node?.initialization); - if (!initialization) { - // do nothing; user still typing - } else if (initialization.type == 'praxly_statement_block') { - // unpack the expression statement - var container1 = initialization; - initialization = initialization.getInputTargetBlock('EXPRESSION'); - } else if (initialization.type == 'praxly_assignment_block' - || initialization.type == 'praxly_reassignment_block') { - // convert statement to expression - initialization.dispose(); - if (node?.initialization?.varType) { - initialization = workspace.newBlock('praxly_assignment_expression_block'); - initialization.setFieldValue(node?.initialization?.varType, "VARTYPE"); - initialization.setFieldValue(node?.initialization?.name, "VARIABLENAME"); - } else { - initialization = workspace.newBlock('praxly_reassignment_expression_block'); - var location = tree2blocks(workspace, node?.initialization?.location); - initialization.getInput('LOCATION').connection.connect(location?.outputConnection); - } - var expression = tree2blocks(workspace, node?.initialization?.value); - initialization.getInput('EXPRESSION').connection.connect(expression?.outputConnection); - initialization.initSvg(); - } - - // this will always be an expression, so nothing more to do - var condition = tree2blocks(workspace, node?.condition); - - var increment = tree2blocks(workspace, node?.increment); - if (!increment) { - // do nothing; user still typing - } else if (increment.type == 'praxly_statement_block') { - // unpack the expression statement - var container2 = increment; - increment = increment.getInputTargetBlock('EXPRESSION'); + var initialization = tree2blocks(workspace, node?.initialization); + if (!initialization) { + // do nothing; user still typing + } else if (initialization.type == 'praxly_statement_block') { + // unpack the expression statement + var container1 = initialization; + initialization = initialization.getInputTargetBlock('EXPRESSION'); + } else if (initialization.type == 'praxly_assignment_block' + || initialization.type == 'praxly_reassignment_block') { + // convert statement to expression + initialization.dispose(); + if (node?.initialization?.varType) { + initialization = workspace.newBlock('praxly_assignment_expression_block'); + initialization.setFieldValue(node?.initialization?.varType, "VARTYPE"); + initialization.setFieldValue(node?.initialization?.name, "VARIABLENAME"); } else { - // was likely praxly_reassignment_block - increment.dispose(); - increment = workspace.newBlock('praxly_reassignment_expression_block'); - var location2 = tree2blocks(workspace, node?.increment?.location); - var expression2 = tree2blocks(workspace, node?.increment?.value); - increment.getInput('LOCATION').connection.connect(location2?.outputConnection); - increment.getInput('EXPRESSION').connection.connect(expression2?.outputConnection); - increment.initSvg(); + initialization = workspace.newBlock('praxly_reassignment_expression_block'); + var location = tree2blocks(workspace, node?.initialization?.location); + initialization.getInput('LOCATION').connection.connect(location?.outputConnection); } + var expression = tree2blocks(workspace, node?.initialization?.value); + initialization.getInput('EXPRESSION').connection.connect(expression?.outputConnection); + initialization.initSvg(); + } - // get the for loop body - var codeblock = tree2blocks(workspace, node?.codeblock); - - // connect everything together - result.getInput('INITIALIZATION').connection.connect(initialization?.outputConnection); - container1?.dispose(); - result.getInput('CONDITION').connection.connect(condition?.outputConnection); - result.getInput('REASSIGNMENT').connection.connect(increment?.outputConnection); - container2?.dispose(); - if (codeblock && codeblock.length > 0) { - result.getInput('CODEBLOCK').connection.connect(codeblock[0]?.previousConnection); - } + // this will always be an expression, so nothing more to do + var condition = tree2blocks(workspace, node?.condition); + + var increment = tree2blocks(workspace, node?.increment); + if (!increment) { + // do nothing; user still typing + } else if (increment.type == 'praxly_statement_block') { + // unpack the expression statement + var container2 = increment; + increment = increment.getInputTargetBlock('EXPRESSION'); + } else { + // was likely praxly_reassignment_block + increment.dispose(); + increment = workspace.newBlock('praxly_reassignment_expression_block'); + var location2 = tree2blocks(workspace, node?.increment?.location); + var expression2 = tree2blocks(workspace, node?.increment?.value); + increment.getInput('LOCATION').connection.connect(location2?.outputConnection); + increment.getInput('EXPRESSION').connection.connect(expression2?.outputConnection); + increment.initSvg(); } - catch (error) { - console.error('for loop header', error); - // the question marks here helped the for loop block generate when just typing - // the word "for", giving a little bit of predictive block rendering. - initialization?.dispose(); - condition?.dispose(); - increment?.dispose(); + + // get the for loop body + var codeblock = tree2blocks(workspace, node?.codeblock); + + // connect everything together + result.getInput('INITIALIZATION').connection.connect(initialization?.outputConnection); + container1?.dispose(); + result.getInput('CONDITION').connection.connect(condition?.outputConnection); + result.getInput('REASSIGNMENT').connection.connect(increment?.outputConnection); + container2?.dispose(); + if (codeblock && codeblock.length > 0) { + result.getInput('CODEBLOCK').connection.connect(codeblock[0]?.previousConnection); } break; diff --git a/src/tree2text.js b/src/tree2text.js index 40faa6a..ab25ed8 100644 --- a/src/tree2text.js +++ b/src/tree2text.js @@ -9,71 +9,39 @@ export const tree2text = (node, indentation) => { case TYPES.BOOLEAN: case TYPES.DOUBLE: case TYPES.INT: - try { - var result = node.value.toString(); - return result; - } catch (error) { - return " "; - } + var result = node.value.toString(); + return result; case NODETYPES.LOCATION: - try { - var result = node.name.toString(); - if (node.isArray) { - result += `[${tree2text(node.index, 0)}]`; - } - return result; - } catch (error) { - return " "; + var result = node.name.toString(); + if (node.isArray) { + result += `[${tree2text(node.index, 0)}]`; } + return result; case TYPES.CHAR: - try { - var result = '\'' + node.value + '\''; - return result; - } catch (error) { - return " "; - } + var result = '\'' + node.value + '\''; + return result; case TYPES.STRING: - try { - var result = '\"' + node.value + '\"'; - return result; - } catch (error) { - return " "; - } + var result = '\"' + node.value + '\"'; + return result; case TYPES.INVALID: - try { - var result = "// Invalid " + node.value; - return result; - } catch (error) { - return " "; - } + var result = "// Invalid " + node.value; + return result; case NODETYPES.COMMENT: - try { - var result = ' '.repeat(indentation) + '/*' + node.value + '*/\n'; - return result; - } catch (error) { - return " "; - } + var result = ' '.repeat(indentation) + '/* ' + node.value + ' */\n'; + return result; case NODETYPES.NEWLINE: - try { - var result = '\n'; - return result; - } catch (error) { - return " "; - } + var result = '\n'; + return result; case NODETYPES.SINGLE_LINE_COMMENT: - try { - var result = ' '.repeat(indentation) + '// ' + node.value + '\n'; - return result; - } catch (error) { - return " "; - } + var result = ' '.repeat(indentation) + '// ' + node.value + '\n'; + return result; case NODETYPES.ADDITION: var a_operand = tree2text(node.left, 0); @@ -163,7 +131,7 @@ export const tree2text = (node, indentation) => { var result = ' '.repeat(indentation) + "print "; var expression = tree2text(node.value, 0); if (node.comment) { - expression += ' // ' + node.comment; + expression += ' // ' + node.comment; } expression += '\n'; return result + expression; @@ -223,11 +191,7 @@ export const tree2text = (node, indentation) => { case NODETYPES.CODEBLOCK: var statements = node.statements.map(element => { - try { - return tree2text(element, indentation); - } catch (error) { - return " "; - } + return tree2text(element, indentation); }); return statements.join(''); @@ -256,18 +220,14 @@ export const tree2text = (node, indentation) => { // Note: declaration and assignment (possibly in a for loop) case NODETYPES.VARDECL: - try { - var vartype = node.varType.toString(); - var varname = vartype + ' ' + node.name.toString(); - if (node.value !== undefined) { - var operator = ' ← '; - var expression = tree2text(node.value, 0); - return ' '.repeat(indentation) + varname + operator + expression + '\n'; - } else { - return ' '.repeat(indentation) + varname + '\n'; - } - } catch (error) { - return " "; + var vartype = node.varType.toString(); + var varname = vartype + ' ' + node.name.toString(); + if (node.value !== undefined) { + var operator = ' ← '; + var expression = tree2text(node.value, 0); + return ' '.repeat(indentation) + varname + operator + expression + '\n'; + } else { + return ' '.repeat(indentation) + varname + '\n'; } case NODETYPES.WHILE: @@ -371,36 +331,27 @@ export const tree2text = (node, indentation) => { return result + expression; case NODETYPES.ARRAY_ASSIGNMENT: - try { - var varname = node.varType.toString() + '[] ' + node.name.toString(); - var operator = ' ← '; - var result = '{'; - var argsList = node.value.params; - if (argsList !== null && argsList.length > 0) { - argsList.forEach(element => { - result += tree2text(element, 0) + ', '; - }); - result = result.slice(0, result.length - 2); - } - result += '}\n'; - return ' '.repeat(indentation) + varname + operator + result; - } catch (error) { - return " "; + var varname = node.varType.toString() + '[] ' + node.name.toString(); + var operator = ' ← '; + var result = '{'; + var argsList = node.value.params; + if (argsList !== null && argsList.length > 0) { + argsList.forEach(element => { + result += tree2text(element, 0) + ', '; + }); + result = result.slice(0, result.length - 2); } + result += '}\n'; + return ' '.repeat(indentation) + varname + operator + result; case NODETYPES.ARRAY_REFERENCE_ASSIGNMENT: - try { - var index = tree2text(node.index, 0) + ']'; - var varname = node.name.toString() + '[' + index; - var operator = ' ← '; - var expression = tree2text(node.value, 0) + '\n'; - return ' '.repeat(indentation) + varname + operator + expression; - } catch (error) { - return " "; - } + var index = tree2text(node.index, 0) + ']'; + var varname = node.name.toString() + '[' + index; + var operator = ' ← '; + var expression = tree2text(node.value, 0) + '\n'; + return ' '.repeat(indentation) + varname + operator + expression; default: - console.error("unknown node type " + node.type); - return " "; + throw new Error("unknown node type " + node.type); } } From 57dc34dab0f96a0832e8ab0f7440ea92f9483e57 Mon Sep 17 00:00:00 2001 From: Chris Mayfield Date: Thu, 8 Aug 2024 11:01:03 -0400 Subject: [PATCH 20/21] fix a bug when parsing missing parens --- src/text2tree.js | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/text2tree.js b/src/text2tree.js index b0d1e83..c07c14c 100644 --- a/src/text2tree.js +++ b/src/text2tree.js @@ -321,7 +321,7 @@ class Parser { } getCurrentLine() { - return this.tokens[this.i].line; + return this.tokens[this.i]?.line; } getCurrentValue() { @@ -644,18 +644,18 @@ class Parser { const leftToken = this.advance(); const expression = this.parse_expression(); if (this.has(")")) { - const rightToken = this.advance(); - return { - blockID: "code", - expression, - line, - type: NODETYPES.ASSOCIATION, - startIndex: leftToken.startIndex, - endIndex: rightToken.endIndex, - }; + var rightToken = this.advance(); } else { textError('parsing', 'did not detect closing parentheses', line); } + return { + blockID: "code", + expression, + line, + type: NODETYPES.ASSOCIATION, + startIndex: leftToken.startIndex, + endIndex: rightToken?.endIndex, + }; // ah yes, array literals....very fun case '{': From 43985cf3962b127ceaf68d7d43a1600c9f910159 Mon Sep 17 00:00:00 2001 From: Chris Mayfield Date: Thu, 8 Aug 2024 11:35:03 -0400 Subject: [PATCH 21/21] highlight function return debugger steps in light gray --- public/themes.css | 8 ++++++++ src/ast.js | 10 +++++----- src/common.js | 4 ++-- 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/public/themes.css b/public/themes.css index 4c6cfe1..de9dc6e 100644 --- a/public/themes.css +++ b/public/themes.css @@ -980,3 +980,11 @@ body:not(.embed)>#embed-toolbar { z-index: 3; background-color: rgba(0, 255, 0, 0.2); } + +.return-step { + position: absolute; + /* The active line highlight has a z-index of 2. This marker must appear + * above that. */ + z-index: 3; + background-color: rgba(255, 255, 255, 0.5); +} diff --git a/src/ast.js b/src/ast.js index 398b208..e16e866 100644 --- a/src/ast.js +++ b/src/ast.js @@ -21,18 +21,18 @@ import prand from 'pure-rand'; const FOR_LOOP_LIMIT = 1000000; const WHILE_LOOP_LIMIT = 1000; -async function stepInto(environment, node) { +async function stepInto(environment, node, cssClass = "step-marker") { if (getStepInto()) { - let markerId = highlightAstNode(environment, node); + let markerId = highlightAstNode(environment, node, cssClass); await waitForStep(); textEditor.session.removeMarker(markerId); } } -async function stepOver(environment, node) { +async function stepOver(environment, node, cssClass = "step-marker") { if (getDebugMode()) { await generateVariableTable(environment, 1); - let markerId = highlightAstNode(environment, node); + let markerId = highlightAstNode(environment, node, cssClass); await waitForStep(); textEditor.session.removeMarker(markerId); } @@ -1665,7 +1665,7 @@ class Praxly_function_call { } // extra debugger step to highlight the function call after returning - await stepOver(environment, this.json); + await stepOver(environment, this.json, "return-step"); return result; } } diff --git a/src/common.js b/src/common.js index 5d03e38..3d9e37f 100644 --- a/src/common.js +++ b/src/common.js @@ -191,7 +191,7 @@ export function consoleInput() { * old param: {boolean} debug set this flag to true if this is being used for debugging. (it changes the color to green) * old returns: the marker id associated with the marker. */ -export function highlightAstNode(environment, node) { +export function highlightAstNode(environment, node, cssClass) { if (DEV_LOG) { console.log(`attempting to highlight: `, node.startIndex[0], node.startIndex[1], node.endIndex[0], node.endIndex[1]); } @@ -208,7 +208,7 @@ export function highlightAstNode(environment, node) { // highlight the text var Range = ace.require('ace/range').Range; var debugRange = new Range(node.startIndex[0], node.startIndex[1], node.endIndex[0], node.endIndex[1]); - var markerId = textEditor.session.addMarker(debugRange, 'step-marker', 'text'); + var markerId = textEditor.session.addMarker(debugRange, cssClass, 'text'); // highlight the block if (node.blockID) {
Variable NameTypeValueScopeVariableTypeValueScope