diff --git a/archive/style.css b/archive/style.css index 4fceb7a..8cf0c0d 100644 --- a/archive/style.css +++ b/archive/style.css @@ -430,6 +430,7 @@ main { display: flex; /* Add this */ flex-direction: row; /* Add this */ flex-basis: 25%; + user-select: all; } .output { diff --git a/index.html b/index.html index 18ecaf2..72e9810 100644 --- a/index.html +++ b/index.html @@ -93,7 +93,7 @@

Dr. Michael Stewart

const reportButton = document.querySelector('.report-button'); reportButton.addEventListener('click', function () { - window.open("https://docs.google.com/forms/d/e/1FAIpQLSeo2mUrrhNZejPch9UcQQDHWk5e6ql_xFfFSdS6oiaNA-Tk8Q/viewform?embedded=true", '_blank'); + window.open("https://docs.google.com/forms/d/e/1FAIpQLSeo2mUrrhNZejPch9UcQQDHWk5e6ql_xFfFSdS6oiaNA-Tk8Q/viewform", '_blank'); }) praxlyButton.addEventListener('click', function () { diff --git a/public/themes.css b/public/themes.css index 0031732..4c6cfe1 100644 --- a/public/themes.css +++ b/public/themes.css @@ -531,6 +531,7 @@ body:not(.embed) #bottom-part { flex-direction: row-reverse; /* Add this */ flex-basis: 25%; + user-select: text; } .output { @@ -554,6 +555,15 @@ body:not(.embed) #bottom-part { color: red; } +.stderr a { + color: skyblue; +} + +.stderr a:active, +.stderr a:hover { + color: white; +} + /* not sure where any of this goes */ .menuButtons { diff --git a/src/ast.js b/src/ast.js index 178298b..19b96f1 100644 --- a/src/ast.js +++ b/src/ast.js @@ -42,16 +42,17 @@ async function stepOver(environment, json) { * this is meant to halt the execution wherever it is at for return statements. */ class ReturnException extends Error { - constructor(errorData) { - super(`attempting to return. this should return ${errorData}`); + constructor(returnValue) { + super(`attempting to return. this should return ${returnValue}`); this.name = this.constructor.name; - this.errorData = errorData; + this.returnValue = returnValue; } } function checkArity(node, expectedArity) { - if (node.parameters.length !== expectedArity) { - throw new PraxlyError(`Function ${node.name} expects ${expectedArity} parameter${expectedArity === 1 ? '' : 's'}, not ${node.parameters.length}.`, node.line); + const actualArity = node.args.length; + if (actualArity !== expectedArity) { + throw new PraxlyError(`Function ${node.name} expects ${expectedArity} parameter${expectedArity === 1 ? '' : 's'}, not ${actualArity}.`, node.line); } } @@ -64,7 +65,7 @@ function checkArity(node, expectedArity) { export function createExecutable(tree) { if (typeof tree === 'undefined' || typeof tree.type === 'undefined') { if (errorOutput.length === 0) { - defaultError("invalid program."); + defaultError("invalid program (abstract syntax tree is undefined)"); } return new Praxly_invalid(tree); } @@ -146,32 +147,54 @@ export function createExecutable(tree) { return new Praxly_random(tree); } else if (tree.name === 'randomInt') { checkArity(tree, 1); - return new Praxly_random_int(createExecutable(tree.parameters[0]), tree); + return new Praxly_random_int(createExecutable(tree.args[0]), tree); } else if (tree.name === 'randomSeed') { checkArity(tree, 1); - return new Praxly_random_seed(createExecutable(tree.parameters[0]), tree); + return new Praxly_random_seed(createExecutable(tree.args[0]), tree); } else if (tree.name === 'int') { checkArity(tree, 1); - return new Praxly_int_conversion(createExecutable(tree.parameters[0]), tree); + return new Praxly_int_conversion(createExecutable(tree.args[0]), tree); } else if (tree.name === 'float') { checkArity(tree, 1); - return new Praxly_float_conversion(createExecutable(tree.parameters[0]), tree); + return new Praxly_float_conversion(createExecutable(tree.args[0]), tree); } else if (tree.name === 'min') { - return new Praxly_min(createExecutable(tree.parameters[0]), createExecutable(tree.parameters[1]), tree); + checkArity(tree, 2); + return new Praxly_min(createExecutable(tree.args[0]), createExecutable(tree.args[1]), tree); } else if (tree.name === 'max') { - return new Praxly_max(createExecutable(tree.parameters[0]), createExecutable(tree.parameters[1]), tree); + checkArity(tree, 2); + return new Praxly_max(createExecutable(tree.args[0]), createExecutable(tree.args[1]), tree); } else if (tree.name === 'abs') { - return new Praxly_abs(createExecutable(tree.parameters[0]), tree); + checkArity(tree, 1); + return new Praxly_abs(createExecutable(tree.args[0]), tree); } else if (tree.name === 'log') { - return new Praxly_log(createExecutable(tree.parameters[0]), tree); + checkArity(tree, 1); + return new Praxly_log(createExecutable(tree.args[0]), tree); } else if (tree.name == 'sqrt') { - return new Praxly_sqrt(createExecutable(tree.parameters[0]), tree); + checkArity(tree, 1); + return new Praxly_sqrt(createExecutable(tree.args[0]), tree); } else { - throw new Error("unknown builtin function: " + tree.name); + throw Error(`unknown builtin function ${tree.name} (line ${tree.line})`); } } case NODETYPES.SPECIAL_STRING_FUNCCALL: + switch (tree.right.name) { + case StringFuncs.LENGTH: + case StringFuncs.TOLOWERCSE: + case StringFuncs.TOUPPERCASE: + checkArity(tree.right, 0); + break; + case StringFuncs.CHARAT: + case StringFuncs.CONTAINS: + case StringFuncs.INDEXOF: + checkArity(tree.right, 1); + break; + case StringFuncs.SUBSTRING: + checkArity(tree.right, 2); + break; + default: + throw Error(`unknown string method ${tree.right.name} (line ${tree.line})`); + } var args = []; tree.right.args.forEach((arg) => { args.push(createExecutable(arg)); @@ -193,7 +216,7 @@ export function createExecutable(tree) { case NODETYPES.IF: try { - return new Praxly_if(createExecutable(tree.condition), createExecutable(tree.statement), tree); + return new Praxly_if(createExecutable(tree.condition), createExecutable(tree.codeblock), tree); } catch (error) { return new Praxly_statement(null); @@ -201,18 +224,18 @@ export function createExecutable(tree) { case NODETYPES.IF_ELSE: try { - return new Praxly_if_else(createExecutable(tree.condition), createExecutable(tree.statement), createExecutable(tree.alternative), tree); + return new Praxly_if_else(createExecutable(tree.condition), createExecutable(tree.codeblock), createExecutable(tree.alternative), tree); } catch (error) { return new Praxly_statement(null); } case NODETYPES.ASSIGNMENT: + case NODETYPES.ARRAY_REFERENCE_ASSIGNMENT: try { - return new Praxly_assignment(tree, createExecutable(tree.location), createExecutable(tree.value), tree); + return new Praxly_assignment(createExecutable(tree.location), createExecutable(tree.value), tree); } catch (error) { - // console.error('assignment error: ', error); return null; } @@ -226,10 +249,9 @@ export function createExecutable(tree) { case NODETYPES.ARRAY_ASSIGNMENT: try { - return new Praxly_array_assignment(tree, createExecutable(tree.location), createExecutable(tree.value)); + return new Praxly_array_assignment(createExecutable(tree.location), createExecutable(tree.value), tree); } catch (error) { - // console.error('array assignment error: ', error); return null; } @@ -242,7 +264,7 @@ export function createExecutable(tree) { return new Praxly_Location(tree, index); } catch (error) { - return; + return null; } case NODETYPES.FOR: @@ -250,44 +272,40 @@ export function createExecutable(tree) { var initialization = createExecutable(tree.initialization); var condition = createExecutable(tree.condition); var incrementation = createExecutable(tree.increment); - var statement = createExecutable(tree.statement); - return new Praxly_for(initialization, condition, incrementation, statement, tree); + var codeblock = createExecutable(tree.codeblock); + return new Praxly_for(initialization, condition, incrementation, codeblock, tree); } catch (error) { - // console.error('for statement error: ', error); return new Praxly_statement(null); } case NODETYPES.WHILE: try { var condition = createExecutable(tree.condition); - var statement = createExecutable(tree.statement); - return new Praxly_while(condition, statement, tree); + var codeblock = createExecutable(tree.codeblock); + return new Praxly_while(condition, codeblock, tree); } catch (error) { - // console.error('while statement error: ', error); return new Praxly_statement(null); } case NODETYPES.DO_WHILE: try { var condition = createExecutable(tree.condition); - var statement = createExecutable(tree.statement); - return new Praxly_do_while(condition, statement, tree); + var codeblock = createExecutable(tree.codeblock); + return new Praxly_do_while(condition, codeblock, tree); } catch (error) { - // console.error('do while statement error: ', error); return new Praxly_statement(null); } case NODETYPES.REPEAT_UNTIL: try { var condition = createExecutable(tree.condition); - var statement = createExecutable(tree.statement); - return new Praxly_repeat_until(condition, statement, tree); + var codeblock = createExecutable(tree.codeblock); + return new Praxly_repeat_until(condition, codeblock, tree); } catch (error) { - // console.error('repeat until statement error: ', error); return new Praxly_statement(null); } @@ -304,8 +322,8 @@ export function createExecutable(tree) { return new Praxly_single_line_comment(tree.value, tree); case NODETYPES.FUNCDECL: - var contents = createExecutable(tree.contents); - return new Praxly_function_declaration(tree.returnType, tree.name, tree.params, contents, tree); + var codeblock = createExecutable(tree.codeblock); + return new Praxly_function_declaration(tree.returnType, tree.name, tree.params, codeblock, tree); case NODETYPES.FUNCCALL: var args = []; @@ -327,9 +345,6 @@ export function createExecutable(tree) { case NODETYPES.ARRAY_REFERENCE: return new Praxly_array_reference(tree.name, createExecutable(tree.index), tree); - case NODETYPES.ARRAY_REFERENCE_ASSIGNMENT: - return new Praxly_array_reference_assignment(tree.name, createExecutable(tree.index), createExecutable(tree.value), tree); - case 'INVALID': return new Praxly_invalid(tree); @@ -337,23 +352,7 @@ export function createExecutable(tree) { return new Praxly_emptyLine(tree); default: - console.error("Unhandled node type: " + tree.type); - return new Praxly_invalid(tree); - } -} - -class Praxly_array_reference_assignment { - - constructor(name, index, value, node) { - this.json = node; - this.name = name; - this.value = value; - this.index = index; - } - - async evaluate(environment) { - var index = await this.index.evaluate(environment); - environment.variableList[this.name].elements[index.value] = await this.value.evaluate(environment); + throw new PraxlyError("unhandled node type " + tree.type, tree.line); } } @@ -517,13 +516,14 @@ class Praxly_array_literal { } } -export function valueToString(child, json) { +export function valueToString(child, quotes, line) { if (child === "Exit_Success") { - throw new PraxlyError("no value returned from void procedure", json.line); + throw new PraxlyError("no value returned from void procedure", line); } var result; if (child.jsonType === 'Praxly_array') { - let values = child.elements.map(valueToString); + // always show quote marks for arrays + let values = child.elements.map((element) => valueToString(element, true, line)); result = '{' + values.join(", ") + '}'; } else { result = child.value.toString(); @@ -532,6 +532,14 @@ export function valueToString(child, json) { result += '.0'; } } + if (quotes) { + if (child.realType === TYPES.CHAR) { + result = "'" + result + "'"; + } + if (child.realType === TYPES.STRING) { + result = '"' + result + '"'; + } + } } return result; } @@ -545,7 +553,7 @@ class Praxly_print { async evaluate(environment) { var child = await (this.expression.evaluate(environment)); - var result = valueToString(child, this.json); + var result = valueToString(child, false, this.json.line); let suffix; if (this.json.comment) { @@ -1060,10 +1068,10 @@ class Praxly_less_than_equal { class Praxly_if { - constructor(condition, code, node) { + constructor(condition, codeblock, node) { this.json = node; this.condition = condition; - this.code = code; + this.codeblock = codeblock; } async evaluate(environment) { @@ -1072,7 +1080,7 @@ class Praxly_if { throw new PraxlyError("Invalid condition (must be boolean)", this.json.line); } if (cond.value) { - await this.code.evaluate(environment); + await this.codeblock.evaluate(environment); } return 'success'; } @@ -1080,10 +1088,10 @@ class Praxly_if { class Praxly_if_else { - constructor(condition, code, alternative, node) { + constructor(condition, codeblock, alternative, node) { this.json = node; this.condition = condition; - this.code = code; + this.codeblock = codeblock; this.alternative = alternative; } @@ -1093,7 +1101,7 @@ class Praxly_if_else { throw new PraxlyError("Invalid condition (must be boolean)", this.json.line); } if (cond.value) { - await this.code.evaluate(environment); + await this.codeblock.evaluate(environment); } else { await this.alternative.evaluate(environment); } @@ -1140,6 +1148,9 @@ class Praxly_codeBlock { // for each statement in the block for (let i = 0; i < this.praxly_blocks.length; i++) { const element = this.praxly_blocks[i]; + if (element.json === undefined) { + throw new PraxlyError("Incomplete code (undefined)", 0); // no line number + } // skip elements that have no effect if (element.json.type == NODETYPES.NEWLINE || element.json.type === NODETYPES.COMMENT || element.json.type === NODETYPES.SINGLE_LINE_COMMENT) { continue; @@ -1171,7 +1182,7 @@ function accessLocation(environment, json) { } // converts the evaluated value to the variable's type when assigned. -function typeCoercion(varType, praxlyObj) { +function typeCoercion(varType, praxlyObj, line) { if (varType === praxlyObj.realType) { return praxlyObj; } @@ -1199,37 +1210,41 @@ function typeCoercion(varType, praxlyObj) { newValue = String(praxlyObj.value); return new Praxly_String(newValue, praxlyObj.json); default: - console.error("Unhandled var type: " + varType); - return praxlyObj; + throw new PraxlyError("unhandled var type: " + varType, line); } } class Praxly_assignment { - constructor(json, location, expression, node) { + constructor(location, expression, node) { this.json = node; this.location = location; this.value = expression; } async evaluate(environment) { - // if it is a reassignment, the variable must be in the list and have a matching type. - let valueEvaluated = await this.value.evaluate(environment); + // the variable must be in the environment and have a matching type var storage = accessLocation(environment, this.location); if (!storage) { throw new PraxlyError(`Variable ${this.location.name} does not exist in this scope.`, this.json.line); } + if (this.location.isArray) { + var index = await this.location.index.evaluate(environment); + var length = storage[this.location.name].elements.length; + if (index.value >= length) { + throw new PraxlyError(`Array index ${index.value} out of bounds for length ${length}`, this.json.line); + } + } + 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); } - // console.warn(storage); - valueEvaluated = typeCoercion(currentStoredVariableEvaluated.realType, valueEvaluated); + valueEvaluated = typeCoercion(currentStoredVariableEvaluated.realType, valueEvaluated, this.json.line); if (this.location.isArray) { - var index = await this.location.index.evaluate(environment); storage[this.location.name].elements[index.value] = valueEvaluated; } else { storage[this.location.name] = valueEvaluated; @@ -1277,8 +1292,7 @@ class Praxly_vardecl { valueEvaluated = new Praxly_String(""); break; default: - console.error("Unhandled var type: " + this.json.varType); - break; + throw new PraxlyError("unhandled var type: " + this.json.varType, this.json.line); } } if (environment.variableList.hasOwnProperty(this.name)) { @@ -1288,7 +1302,7 @@ class Praxly_vardecl { if (!can_assign(this.json.varType, valueEvaluated.realType, this.json.line)) { throw new PraxlyError(`incompatible types: ${valueEvaluated.realType} cannot be converted to ${this.json.varType}`, this.json.line); } - valueEvaluated = typeCoercion(this.json.varType, valueEvaluated); + valueEvaluated = typeCoercion(this.json.varType, valueEvaluated, this.json.line); environment.variableList[this.name] = valueEvaluated; return; @@ -1297,7 +1311,7 @@ class Praxly_vardecl { class Praxly_array_assignment { - constructor(json, location, expression) { + constructor(location, expression, json) { this.json = json; this.location = location; this.value = expression; @@ -1311,28 +1325,13 @@ class Praxly_array_assignment { 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); } - valueEvaluated.elements[k] = typeCoercion(this.json.varType, valueEvaluated.elements[k]); + valueEvaluated.elements[k] = typeCoercion(this.json.varType, valueEvaluated.elements[k], this.json.line); } + // store in current environment, because the array is being declared and initialized environment.variableList[this.name] = valueEvaluated; } } -class Praxly_variable { - - constructor(json, name, node) { - this.json = node; - this.name = name; - } - - async evaluate(environment) { - if (!environment.variableList.hasOwnProperty(this.name)) { - throw new PraxlyError(`the variable \'${this.name}\' is not recognized by the program. \n\tPerhaps you forgot to initialize it?`, this.json.line); - // return new Praxly_invalid(this.json); - } - return environment.variableList[this.name]; - } -} - class Praxly_Location { constructor(json, index) { @@ -1349,12 +1348,12 @@ class Praxly_Location { } if (this.isArray) { - var index = await this.index.evaluate(environment).value; - if (index >= storage[this.name].elements.length) { - throw new PraxlyError(`index ${index} out of bounds for array named ${this.name}`, this.json.line); + var index = await this.index.evaluate(environment); + var length = storage[this.name].elements.length; + if (index.value >= length) { + throw new PraxlyError(`Array index ${index.value} out of bounds for length ${length}`, this.json.line); } - var ind = await this.index.evaluate(environment); - return await storage[this.name].elements[ind.value].evaluate(environment); + return await storage[this.name].elements[index.value].evaluate(environment); } else { return await storage[this.name].evaluate(environment); } @@ -1560,21 +1559,21 @@ class Praxly_negate { class Praxly_invalid { constructor() { - this.value = 'error'; + this.value = 'INVALID'; } async evaluate(environment) { - console.info(`invalid tree. Problem detected here:`); + throw new PraxlyError("attempting to evaluate invalid tree node"); } } class Praxly_function_declaration { - constructor(returnType, name, params, contents, node) { + constructor(returnType, name, params, codeblock, node) { this.returnType = returnType; this.name = name; this.params = params; - this.codeblock = contents; + this.codeblock = codeblock; this.json = node; } @@ -1582,7 +1581,7 @@ class Praxly_function_declaration { environment.functionList[this.name] = { returnType: this.returnType, params: this.params, - contents: this.codeblock, + codeblock: this.codeblock, } } } @@ -1608,11 +1607,11 @@ class Praxly_function_call { //this one was tricky async evaluate(environment) { var func = findFunction(this.name, environment, this.json); - var functionParams = func.params; - var functionContents = func.contents; + var functionArgs = func.params; + var functionBody = func.codeblock; var returnType = func.returnType; - if (functionParams.length !== this.args.length) { - throw new PraxlyError(`incorrect amount of arguments passed, expected ${functionParams.length}, was ${this.args.length}`, this.json.line); + if (functionArgs.length !== this.args.length) { + throw new PraxlyError(`incorrect amount of arguments passed, expected ${functionArgs.length}, was ${this.args.length}`, this.json.line); } //NEW: parameter list is now a linkedList. expect some errors till I fix it. @@ -1624,8 +1623,8 @@ class Praxly_function_call { global: environment.global, }; for (let i = 0; i < this.args.length; i++) { - let parameterName = functionParams[i][1]; - let parameterType = functionParams[i][0]; + let parameterName = functionArgs[i][1]; + let parameterType = functionArgs[i][0]; let argument = await this.args[i].evaluate(environment); if (can_assign(parameterType, argument.realType, this.json.line)) { @@ -1638,11 +1637,11 @@ class Praxly_function_call { // call the user's function let result = null; try { - result = await functionContents.evaluate(newScope); + result = await functionBody.evaluate(newScope); } catch (error) { if (error instanceof ReturnException) { - result = error.errorData; + result = error.returnValue; } else if (error instanceof RangeError) { // most likely infinite recursion @@ -1693,12 +1692,12 @@ class Praxly_String_funccall { case StringFuncs.CONTAINS: var char = await this.args[0].evaluate(environment); this.typecheckhelper(char, [TYPES.STRING, TYPES.CHAR]); - result = str.includes(char.value) + result = str.value.includes(char.value); return new Praxly_boolean(result); case StringFuncs.INDEXOF: - var index = await this.args[0].evaluate(environment); - this.typecheckhelper(char, [TYPES.CHAR]); - result = str.value.indexOf(index.value); + var substr = await this.args[0].evaluate(environment); + this.typecheckhelper(substr, [TYPES.STRING, TYPES.CHAR]); + result = str.value.indexOf(substr.value); return new Praxly_int(result); case StringFuncs.LENGTH: return new Praxly_int(str.value.length); @@ -1714,7 +1713,7 @@ class Praxly_String_funccall { result = str.value.substring(startIndex.value, endIndex.value); return new Praxly_String(result); default: - throw new PraxlyError(`unrecognized function name ${this.name} for strings.`, this.json.line); + throw new PraxlyError("unhandled string method " + this.name, this.json.line); } } @@ -1763,7 +1762,7 @@ function can_assign(varType, expressionType, line) { } else if (varType === TYPES.CHAR) { return expressionType === TYPES.CHAR; } else { - return false; // Invalid varType + throw Error(`unknown variable type ${varType} (line ${line})`); } } @@ -1916,7 +1915,7 @@ function binop_typecheck(operation, type1, type2, json) { return can_compare(operation, type1, type2, json); default: - throw new PraxlyError(`typecheck called when it shouldn't have been`, json.line);// Invalid operation + throw new PraxlyError("unhandled operation " + operation, json.line); } } @@ -1937,9 +1936,8 @@ function litNode_new(type, value, json) { case TYPES.SHORT: return new Praxly_short(value); case TYPES.INVALID: - // console.error("Invalid literal: ", json); return new Praxly_invalid(); default: - throw new PraxlyError("Unknown literal type", json.line); + throw new PraxlyError("unhandled literal type " + type, json.line); } } diff --git a/src/blocks2tree.js b/src/blocks2tree.js index 71dec56..4d0f453 100644 --- a/src/blocks2tree.js +++ b/src/blocks2tree.js @@ -1,5 +1,5 @@ import Blockly from 'blockly'; -import { NODETYPES, TYPES } from './common'; +import { NODETYPES, StringFuncs, TYPES } from './common'; function containsOnlyNumbers(str) { return /^-?\d+$/.test(str); @@ -13,7 +13,6 @@ function isValidIdentifier(str) { export const blocks2tree = (workspace, generator) => { var topBlocks = workspace.getTopBlocks(); - // console.error(topBlocks); if (topBlocks.length === 0) { return { type: NODETYPES.PROGRAM, @@ -32,7 +31,7 @@ export const blocks2tree = (workspace, generator) => { } function customizeMaybe(block, node) { - if (block.data) { + if (block?.data) { const data = JSON.parse(block.data); if (data.isParenthesized) { node = { @@ -48,21 +47,24 @@ function customizeMaybe(block, node) { export const makeGenerator = () => { const praxlyGenerator = []; - praxlyGenerator['codeBlockJsonBuilder'] = (headBlock) => { - // console.log('this is the head block'); - // console.log(headBlock); + praxlyGenerator[undefined] = (block) => { + return null; // incomplete block + } + praxlyGenerator['codeBlockJsonBuilder'] = (headBlock) => { var codeblock = { type: NODETYPES.CODEBLOCK, blockID: "blocks[]", } var statements = []; let currentBlock = headBlock; - while (currentBlock.getNextBlock() != null) { + if (currentBlock) { + while (currentBlock.getNextBlock() != null) { + statements.push(praxlyGenerator[currentBlock.type](currentBlock)); + currentBlock = currentBlock.getNextBlock(); + } statements.push(praxlyGenerator[currentBlock.type](currentBlock)); - currentBlock = currentBlock.getNextBlock(); } - statements.push(praxlyGenerator[currentBlock.type](currentBlock)); codeblock.statements = statements; return customizeMaybe(headBlock, codeblock); } @@ -85,7 +87,7 @@ export const makeGenerator = () => { return customizeMaybe(block, { blockID: block.id, type: 'PRINT', - value: praxlyGenerator[expression.type](expression), + value: praxlyGenerator[expression?.type](expression), comment: block.getCommentText(), }) } @@ -95,7 +97,7 @@ export const makeGenerator = () => { name: 'random', blockID: block.id, type: NODETYPES.BUILTIN_FUNCTION_CALL, - parameters: [], + args: [], }); } @@ -105,7 +107,7 @@ export const makeGenerator = () => { name: 'randomInt', blockID: block.id, type: NODETYPES.BUILTIN_FUNCTION_CALL, - parameters: [ + args: [ praxlyGenerator[expression.type](expression), ], }); @@ -117,7 +119,7 @@ export const makeGenerator = () => { name: 'randomSeed', blockID: block.id, type: NODETYPES.BUILTIN_FUNCTION_CALL, - parameters: [ + args: [ praxlyGenerator[expression.type](expression), ], }); @@ -129,7 +131,7 @@ export const makeGenerator = () => { name: 'int', blockID: block.id, type: NODETYPES.BUILTIN_FUNCTION_CALL, - parameters: [ + args: [ praxlyGenerator[expression.type](expression), ], }); @@ -141,7 +143,7 @@ export const makeGenerator = () => { name: 'float', blockID: block.id, type: NODETYPES.BUILTIN_FUNCTION_CALL, - parameters: [ + args: [ praxlyGenerator[expression.type](expression), ], }); @@ -154,7 +156,7 @@ export const makeGenerator = () => { name: 'min', blockID: block.id, type: NODETYPES.BUILTIN_FUNCTION_CALL, - parameters: [ + args: [ praxlyGenerator[expression.type](expression), praxlyGenerator[expression2.type](expression2), ], @@ -168,7 +170,7 @@ export const makeGenerator = () => { name: 'max', blockID: block.id, type: NODETYPES.BUILTIN_FUNCTION_CALL, - parameters: [ + args: [ praxlyGenerator[expression.type](expression), praxlyGenerator[expression2.type](expression2), ], @@ -181,7 +183,7 @@ export const makeGenerator = () => { name: 'abs', blockID: block.id, type: NODETYPES.BUILTIN_FUNCTION_CALL, - parameters: [ + args: [ praxlyGenerator[expression.type](expression), ], }); @@ -193,7 +195,7 @@ export const makeGenerator = () => { name: 'log', blockID: block.id, type: NODETYPES.BUILTIN_FUNCTION_CALL, - parameters: [ + args: [ praxlyGenerator[expression.type](expression), ], }); @@ -205,7 +207,7 @@ export const makeGenerator = () => { name: 'sqrt', blockID: block.id, type: NODETYPES.BUILTIN_FUNCTION_CALL, - parameters: [ + args: [ praxlyGenerator[expression.type](expression), ], }); @@ -217,7 +219,7 @@ export const makeGenerator = () => { name: 'input', blockID: block.id, type: NODETYPES.BUILTIN_FUNCTION_CALL, - parameters: [], + args: [], }); } @@ -226,7 +228,7 @@ export const makeGenerator = () => { return customizeMaybe(block, { blockID: block.id, type: NODETYPES.STATEMENT, - value: praxlyGenerator[expression.type](expression), + value: praxlyGenerator[expression?.type](expression), }) } @@ -241,6 +243,10 @@ export const makeGenerator = () => { }) } + // NOTE: praxly_literal_block and praxly_variable_block work the same way. + // The only differences are the color of the outline and the error message + // when invalid. Ideally this code would not be duplicated. + praxlyGenerator['praxly_literal_block'] = (block) => { const input = block.getFieldValue('LITERAL'); const node = { @@ -366,8 +372,8 @@ export const makeGenerator = () => { return customizeMaybe(block, { type: NODETYPES.IF, blockID: block.id, - condition: praxlyGenerator[condition.type](condition), - statement: praxlyGenerator['codeBlockJsonBuilder'](statements) + condition: praxlyGenerator[condition?.type](condition), + codeblock: praxlyGenerator['codeBlockJsonBuilder'](statements) }) } @@ -378,8 +384,8 @@ export const makeGenerator = () => { return customizeMaybe(block, { type: NODETYPES.IF_ELSE, blockID: block.id, - condition: praxlyGenerator[condition.type](condition), - statement: praxlyGenerator['codeBlockJsonBuilder'](statements), + condition: praxlyGenerator[condition?.type](condition), + codeblock: praxlyGenerator['codeBlockJsonBuilder'](statements), alternative: praxlyGenerator['codeBlockJsonBuilder'](alternative), }) } @@ -399,11 +405,12 @@ export const makeGenerator = () => { }) } + // Note: declaration and assignment (as a statement) praxlyGenerator['praxly_assignment_block'] = (block) => { 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, @@ -419,7 +426,6 @@ export const makeGenerator = () => { praxlyGenerator['praxly_array_assignment_block'] = (block) => { var varType = block.getFieldValue('VARTYPE'); - // console.log(`field input is ${varType}`); var variableName = block.getFieldValue('VARIABLENAME'); var args = block.getInputTargetBlock('EXPRESSION'); var argschildren = args.getChildren(true); @@ -447,8 +453,6 @@ export const makeGenerator = () => { } praxlyGenerator['praxly_reassignment_block'] = (block) => { - var varType = block.getFieldValue('VARTYPE'); - // console.log(`field input is ${varType}`); var variableName = block.getFieldValue('VARIABLENAME'); var expression = block.getInputTargetBlock('EXPRESSION'); var value = praxlyGenerator[expression.type](expression); @@ -457,7 +461,10 @@ export const makeGenerator = () => { name: variableName, value: value, blockID: block.id, - varType: 'reassignment' + location: { + name: variableName, + type: NODETYPES.LOCATION, + }, }) } @@ -465,20 +472,26 @@ export const makeGenerator = () => { var variableName = block.getFieldValue('VARIABLENAME'); var expression = block.getInputTargetBlock('EXPRESSION'); var indexInput = block.getInputTargetBlock('INDEX'); - var value = praxlyGenerator[expression.type](expression); - var index = praxlyGenerator[indexInput.type](indexInput); + var value = praxlyGenerator[expression?.type](expression); + var index = praxlyGenerator[indexInput?.type](indexInput); return customizeMaybe(block, { type: NODETYPES.ARRAY_REFERENCE_ASSIGNMENT, name: variableName, index: index, value: value, blockID: block.id, + location: { + name: variableName, + type: NODETYPES.LOCATION, + isArray: true, + index: index, + }, }) } + // declaration and assignment (likely in a for loop) praxlyGenerator['praxly_assignment_expression_block'] = (block) => { var varType = block.getFieldValue('VARTYPE'); - // console.log(`field input is ${varType}`); var variableName = block.getFieldValue('VARIABLENAME'); var expression = block.getInputTargetBlock('EXPRESSION'); var value = praxlyGenerator[expression.type](expression); @@ -496,8 +509,6 @@ export const makeGenerator = () => { } praxlyGenerator['praxly_reassignment_expression_block'] = (block) => { - var varType = block.getFieldValue('VARTYPE'); - // console.log(`field input is ${varType}`); var location = block.getInputTargetBlock('LOCATION'); var expression = block.getInputTargetBlock('EXPRESSION'); var loc = praxlyGenerator[location.type](location); @@ -508,7 +519,6 @@ export const makeGenerator = () => { location: loc, value: value, blockID: block.id, - varType: varType, }) } @@ -518,8 +528,8 @@ export const makeGenerator = () => { return customizeMaybe(block, { type: NODETYPES.WHILE, blockID: block.id, - condition: praxlyGenerator[condition.type](condition), - statement: praxlyGenerator['codeBlockJsonBuilder'](statements) + condition: praxlyGenerator[condition?.type](condition), + codeblock: praxlyGenerator['codeBlockJsonBuilder'](statements) }); } @@ -529,8 +539,8 @@ export const makeGenerator = () => { return customizeMaybe(block, { type: NODETYPES.DO_WHILE, blockID: block.id, - condition: praxlyGenerator[condition.type](condition), - statement: praxlyGenerator['codeBlockJsonBuilder'](statements) + condition: praxlyGenerator[condition?.type](condition), + codeblock: praxlyGenerator['codeBlockJsonBuilder'](statements) }); } @@ -540,8 +550,8 @@ export const makeGenerator = () => { return customizeMaybe(block, { type: NODETYPES.REPEAT_UNTIL, blockID: block.id, - condition: praxlyGenerator[condition.type](condition), - statement: praxlyGenerator['codeBlockJsonBuilder'](statements) + condition: praxlyGenerator[condition?.type](condition), + codeblock: praxlyGenerator['codeBlockJsonBuilder'](statements) }); } @@ -550,7 +560,7 @@ export const makeGenerator = () => { return customizeMaybe(block, { blockID: block.id, type: NODETYPES.NOT, - value: praxlyGenerator[expression.type](expression), + value: praxlyGenerator[expression?.type](expression), }) } @@ -571,16 +581,15 @@ export const makeGenerator = () => { return customizeMaybe(block, { type: NODETYPES.FOR, blockID: block.id, - initialization: praxlyGenerator[initialization.type](initialization), - statement: praxlyGenerator['codeBlockJsonBuilder'](statements), - increment: praxlyGenerator[reassignment.type](reassignment), - condition: praxlyGenerator[condition.type](condition), + initialization: praxlyGenerator[initialization?.type](initialization), + codeblock: praxlyGenerator['codeBlockJsonBuilder'](statements), + increment: praxlyGenerator[reassignment?.type](reassignment), + condition: praxlyGenerator[condition?.type](condition), }); } praxlyGenerator['praxly_procedure_block'] = (block) => { var returnType = block.getFieldValue('RETURNTYPE'); - // console.log(`field input is ${varType}`); var args = block.getInputTargetBlock('PARAMS'); var argschildren = args.getChildren(true); var argsList = []; @@ -591,14 +600,14 @@ export const makeGenerator = () => { argsList.push(param); }); var procedureName = block.getFieldValue('PROCEDURE_NAME'); - const statements = block.getInputTargetBlock("CONTENTS"); + const statements = block.getInputTargetBlock("CODEBLOCK"); block.setFieldValue(procedureName, 'END_PROCEDURE_NAME'); return customizeMaybe(block, { type: NODETYPES.FUNCDECL, name: procedureName, params: argsList, returnType: returnType, - contents: praxlyGenerator['codeBlockJsonBuilder'](statements), + codeblock: praxlyGenerator['codeBlockJsonBuilder'](statements), blockID: block.id, }) } @@ -624,29 +633,143 @@ export const makeGenerator = () => { return customizeMaybe(block, { blockID: block.id, type: NODETYPES.RETURN, - value: praxlyGenerator[expression.type](expression), + value: praxlyGenerator[expression?.type](expression), }) } - praxlyGenerator['praxly_StringFunc_block'] = (block) => { + // 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'); - 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)); + const index = block.getInputTargetBlock('INDEX'); + return customizeMaybe(block, { + blockID: block.id, + name : procedureName, + type: NODETYPES.SPECIAL_STRING_FUNCCALL, + left: praxlyGenerator[expression.type](expression), + right: { + name: procedureName, + args: [praxlyGenerator[index.type](index)], + type: NODETYPES.FUNCCALL + } }); + } + + praxlyGenerator['praxly_contains_block'] = (block) => { + const procedureName = StringFuncs.CONTAINS; + const expression = block.getInputTargetBlock('EXPRESSION'); + const param = block.getInputTargetBlock('PARAM'); return customizeMaybe(block, { blockID: block.id, + name: procedureName, type: NODETYPES.SPECIAL_STRING_FUNCCALL, left: praxlyGenerator[expression.type](expression), right: { name: procedureName, - args: argsList, + args: [praxlyGenerator[param.type](param)], type: NODETYPES.FUNCCALL } - }) + }); + } + + praxlyGenerator['praxly_indexOf_block'] = (block) => { + const procedureName = StringFuncs.INDEXOF; + const expression = block.getInputTargetBlock('EXPRESSION'); + const param = block.getInputTargetBlock('PARAM'); + return customizeMaybe(block, { + blockID: block.id, + name: procedureName, + type: NODETYPES.SPECIAL_STRING_FUNCCALL, + left: praxlyGenerator[expression.type](expression), + right: { + name: procedureName, + args: [praxlyGenerator[param.type](param)], + type: NODETYPES.FUNCCALL + } + }); + } + + praxlyGenerator['praxly_length_block'] = (block) => { + const procedureName = StringFuncs.LENGTH; + const expression = block.getInputTargetBlock('EXPRESSION'); + return customizeMaybe(block, { + blockID: block.id, + name: procedureName, + type: NODETYPES.SPECIAL_STRING_FUNCCALL, + left: praxlyGenerator[expression.type](expression), + right: { + name: procedureName, + args: [] + } + }); + } + + praxlyGenerator['praxly_substring_block'] = (block) => { + const procedureName = StringFuncs.SUBSTRING; + const expression = block.getInputTargetBlock('EXPRESSION'); + const param1 = block.getInputTargetBlock('PARAM1'); + const param2 = block.getInputTargetBlock('PARAM2'); + return customizeMaybe(block, { + blockID: block.id, + name: procedureName, + type: NODETYPES.SPECIAL_STRING_FUNCCALL, + left: praxlyGenerator[expression.type](expression), + right: { + name: procedureName, + args: [praxlyGenerator[param1.type](param1), praxlyGenerator[param2.type](param2)], + type: NODETYPES.FUNCCALL + } + }); + } + + praxlyGenerator['praxly_toLowerCase_block'] = (block) => { + const procedureName = StringFuncs.TOLOWERCSE; + const expression = block.getInputTargetBlock('EXPRESSION'); + return customizeMaybe(block, { + blockID: block.id, + name: procedureName, + type: NODETYPES.SPECIAL_STRING_FUNCCALL, + left: praxlyGenerator[expression.type](expression), + right: { + name: procedureName, + args: [] + } + }); + } + + praxlyGenerator['praxly_toUpperCase_block'] = (block) => { + const procedureName = StringFuncs.TOUPPERCASE; + const expression = block.getInputTargetBlock('EXPRESSION'); + return customizeMaybe(block, { + blockID: block.id, + name: procedureName, + type: NODETYPES.SPECIAL_STRING_FUNCCALL, + left: praxlyGenerator[expression.type](expression), + right: { + name: procedureName, + args: [] + } + }); } return praxlyGenerator; diff --git a/src/common.js b/src/common.js index 6d90a72..6d90257 100644 --- a/src/common.js +++ b/src/common.js @@ -44,7 +44,6 @@ export const OP = { DIVISION: "DIVISION", MODULUS: "MODULUS", EXPONENTIATION: "EXPONENTIATION", - ASSIGNMENT: "ASSIGNMENT", EQUALITY: "EQUALITY", INEQUALITY: "INEQUALITY", GREATER_THAN: "GREATER THAN", @@ -94,7 +93,7 @@ export const MAX_LOOP = 100; // prevents accidental infinite loops // this is the special Error type that is thrown when there is in error in the IDE. export class PraxlyError extends Error { constructor(message, line) { - super(`runtime error occurred on line ${line}:\n\t${message}`); + super("runtime error occurred on line " + line + ":
" + message); textError("runtime", message, line); } } @@ -110,14 +109,17 @@ export function textError(type, message, line) { if (errorOutput) { errorOutput += "

"; } - errorOutput += `${type} error occurred on line ${line}:\n\t${message}`; + errorOutput += type + " error occurred on line " + line + ":
" + message; } export function defaultError(message) { if (errorOutput) { errorOutput += "

"; } - errorOutput += `${message}

We have not written an error message for this issue yet.`; + errorOutput += message + + '


Will you please submit a ' + + 'bug report' + + ' for this error?'; } export function clearErrors() { @@ -188,10 +190,9 @@ export function consoleInput() { * @returns the marker id associated with the marker. This should not be needed. */ export function highlightAstNode(environment, node) { - // console.log(`attempting to highlight index [${node.startIndex[0]},${ node.startIndex[1]}] to [${ node.endIndex[0]}, ${ node.endIndex[1] - 1}]`) var session = textEditor.session; - // var errorRange = indextoAceRange(line - 1); + // 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]); @@ -283,5 +284,5 @@ export function comingSoon() { } -//this will let information that I deemed important to be logged to the console. +// this will let information deemed important to be logged to the console export const DEV_LOG = false; diff --git a/src/debugger.js b/src/debugger.js index 1838672..3c8fdc7 100644 --- a/src/debugger.js +++ b/src/debugger.js @@ -64,7 +64,6 @@ export async function generateVariableTable(environment, level) { } let parent = environment.parent; const variableList = environment.variableList; - // console.error(variableList); for (const key in variableList) { if (Object.hasOwnProperty.call(variableList, key)) { const value = variableList[key]; @@ -76,10 +75,7 @@ export async function generateVariableTable(environment, level) { typeCell.textContent = value.realType; let valueEvaluated = await value.evaluate(environment); - valueCell.textContent = valueToString(valueEvaluated); - if (value.realType === "String") { - valueCell.textContent = '"' + valueCell.textContent + '"'; - } + valueCell.textContent = valueToString(valueEvaluated, true, value.json?.line); const locationCell = document.createElement("td"); locationCell.textContent = environment.name; diff --git a/src/examples.js b/src/examples.js index 56bbd03..e5c3a8c 100644 --- a/src/examples.js +++ b/src/examples.js @@ -92,7 +92,7 @@ mystery(6) int countCharacter(String str, char targetChar) int count ← 0 for (int i ← 0; i < str.length(); i ← i + 1) - if (str.charAt(i) == targetChar) + if (str.charAt(i) == targetChar) count ← count + 1 end if end for @@ -150,8 +150,8 @@ int[] myArray ← {5, 2, 9, 1, 5, 6} void bubbleSort() int n ← 6 for (int i ← 0; i < n - 1; i ← i + 1) - for (int j ← 0; j < n - i - 1; j ← j + 1) - if (myArray[j] > myArray[j + 1]) + for (int j ← 0; j < n - i - 1; j ← j + 1) + if (myArray[j] > myArray[j + 1]) // Swap elements if they are in the wrong order int temp ← myArray[j] myArray[j] ← myArray[j + 1] @@ -165,7 +165,7 @@ end bubbleSort void printArray() int n ← 6 for (int i ← 0; i < n; i ← i + 1) - print myArray[i] + print myArray[i] end for end printArray @@ -186,10 +186,10 @@ int[] myArray ← {5, 2, 9, 1, 5, 6} void selectionSort() int n ← 6 for (int i ← 0; i < n - 1; i ← i + 1) - // Find the minimum element in the unsorted part of the array + // Find the minimum element in the unsorted part of the array int minIndex ← i for (int j ← i + 1; j < n; j ← j + 1) - if (myArray[j] < myArray[minIndex]) + if (myArray[j] < myArray[minIndex]) minIndex ← j end if end for @@ -205,7 +205,7 @@ end selectionSort void printArray() int n ← 6 for (int i ← 0; i < n; i ← i + 1) - print myArray[i] + print myArray[i] end for end printArray diff --git a/src/main.js b/src/main.js index b21e5be..f5a60cb 100644 --- a/src/main.js +++ b/src/main.js @@ -226,7 +226,6 @@ function registerListeners() { // Prevent the default save action (e.g., opening the save dialog, reloading the page) event.preventDefault(); runTasks(false); - // console.log(trees); } }); @@ -493,9 +492,9 @@ async function runTasks(startDebug) { // special case: abort running (not an error) clear(); } else if (!errorOutput) { - // error not previously handled (by PraxlyError) - defaultError(error); + // error not previously handled by PraxlyError console.error(error); + defaultError(error); } } @@ -515,8 +514,9 @@ async function runTasks(startDebug) { textEditor.focus(); } + export function turnCodeToBlocks() { - // I need to make the listeners only be one at a time to prevent an infinite loop. + // only one listener at a time to prevent infinite loop workspace.removeChangeListener(onBlocklyChange); if (getDebugMode()) { stopButton.click(); @@ -526,9 +526,10 @@ export function turnCodeToBlocks() { clearErrors(); mainTree = text2tree(); if (DEV_LOG) { - console.log(mainTree); + console.log("text2tree", mainTree); } + // update block side to match workspace.clear(); tree2blocks(workspace, mainTree); workspace.render(); @@ -543,12 +544,21 @@ function onBlocklyChange(event) { } function turnBlocksToCode() { + // only one listener at a time to prevent infinite loop textEditor.removeEventListener("input", turnCodeToBlocks); + if (getDebugMode()) { + stopButton.click(); + } + + // this is where block compiling begins + clearErrors(); mainTree = blocks2tree(workspace, praxlyGenerator); if (DEV_LOG) { - console.log(mainTree); + console.log("blocks2tree", mainTree); } - const text = tree2text(mainTree, 0, 0); + + // update text side to match + const text = tree2text(mainTree, 0); textEditor.setValue(text, -1); }; diff --git a/src/newBlocks.js b/src/newBlocks.js index e38f8e6..93fa601 100644 --- a/src/newBlocks.js +++ b/src/newBlocks.js @@ -145,39 +145,6 @@ export function definePraxlyBlocks(workspace) { "tooltip": "Blank line in the code", "helpUrl": "" }, - { // common 6 - "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": "" - }, { // variables 1 "type": "praxly_vardecl_block", "message0": "%1%2%3", @@ -198,7 +165,7 @@ export function definePraxlyBlocks(workspace) { { "type": "field_input", "name": "VARIABLENAME", - "text": "VariableName" + "text": "variableName" }, { "type": "input_dummy" @@ -231,7 +198,7 @@ export function definePraxlyBlocks(workspace) { { "type": "field_input", "name": "VARIABLENAME", - "text": "VariableName" + "text": "variableName" }, { "type": "input_value", @@ -255,7 +222,7 @@ export function definePraxlyBlocks(workspace) { { "type": "field_input", "name": "VARIABLENAME", - "text": "VariableName" + "text": "variableName" }, { "type": "input_value", @@ -387,7 +354,7 @@ export function definePraxlyBlocks(workspace) { } ], "output": null, - "style": 'expression_blocks', + "style": 'math_blocks', "tooltip": "A literal value in the code", "helpUrl": "" }, @@ -441,7 +408,7 @@ export function definePraxlyBlocks(workspace) { ], "inputsInline": true, "output": null, - "style": 'expression_blocks', + "style": 'math_blocks', "tooltip": "Arithmetic operators:\n+ addition (and string concatenation)\n- subtraction\n* multiplication\n/ division (integer and floating-point)\n% remainder\n^ exponent", "helpUrl": "", }, @@ -458,7 +425,7 @@ export function definePraxlyBlocks(workspace) { } ], "output": null, - "style": 'expression_blocks', + "style": 'math_blocks', "tooltip": "Negates a value", "helpUrl": "" }, @@ -467,7 +434,7 @@ export function definePraxlyBlocks(workspace) { "message0": "random ( )", "inputsInline": true, "output": null, - "style": 'expression_blocks', + "style": 'math_blocks', "tooltip": "Generates a random double greater than or equal to 0 and less than 1", "helpUrl": "" }, @@ -482,7 +449,7 @@ export function definePraxlyBlocks(workspace) { ], "inputsInline": true, "output": null, - "style": 'expression_blocks', + "style": 'math_blocks', "tooltip": "Generates a random integer greater than or equal to 0 and less than x", "helpUrl": "" }, @@ -497,7 +464,7 @@ export function definePraxlyBlocks(workspace) { ], "inputsInline": true, "output": null, - "style": 'expression_blocks', + "style": 'math_blocks', "tooltip": "Sets the seed of the random number generator", "helpUrl": "" }, @@ -512,7 +479,7 @@ export function definePraxlyBlocks(workspace) { ], "inputsInline": true, "output": null, - "style": 'expression_blocks', + "style": 'math_blocks', "tooltip": "Converts a String into an int", "helpURL": "" }, @@ -527,7 +494,7 @@ export function definePraxlyBlocks(workspace) { ], "inputsInline": true, "output": null, - "style": 'expression_blocks', + "style": 'math_blocks', "tooltip": "Converts a String into a float", "helpURL": "" }, @@ -546,7 +513,7 @@ export function definePraxlyBlocks(workspace) { ], "inputsInline": true, "output": null, - "style": 'expression_blocks', + "style": 'math_blocks', "tooltip": "Returns the lower value", "helpURL": "" }, @@ -565,7 +532,7 @@ export function definePraxlyBlocks(workspace) { ], "inputsInline": true, "output": null, - "style": 'expression_blocks', + "style": 'math_blocks', "tooltip": "Returns the higher value", "helpURL": "" }, @@ -580,7 +547,7 @@ export function definePraxlyBlocks(workspace) { ], "inputsInline": true, "output": null, - "style": 'expression_blocks', + "style": 'math_blocks', "tooltip": "Returns the absolute value", "helpURL": "" }, @@ -595,7 +562,7 @@ export function definePraxlyBlocks(workspace) { ], "inputsInline": true, "output": null, - "style": 'expression_blocks', + "style": 'math_blocks', "tooltip": "Calculates the natural logarithm", "helpURL": "" }, @@ -610,10 +577,173 @@ export function definePraxlyBlocks(workspace) { ], "inputsInline": true, "output": null, - "style": 'expression_blocks', + "style": 'math_blocks', "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)", + "args0": [ + { + "type": "input_value", + "name": "EXPRESSION" + }, + { + "type": "input_value", + "name": "INDEX", + "text": "params" + }, + ], + "inputsInline": true, + "output": null, + "style": 'text_blocks', + "tooltip": "Returns the character at the index", + "helpUrl": "" + }, + { // text 2 + "type": "praxly_contains_block", + "message0": "%1.contains (%2)", + "args0": [ + { + "type": "input_value", + "name": "EXPRESSION" + }, + { + "type": "input_value", + "name": "PARAM", + "text": "params" + }, + ], + "inputsInline": true, + "output": null, + "style": 'text_blocks', + "tooltip": "Returns true if string is a substring", + "helpUrl": "" + }, + { // text 3 + "type": "praxly_indexOf_block", + "message0": "%1.indexOf (%2)", + "args0": [ + { + "type": "input_value", + "name": "EXPRESSION" + }, + { + "type": "input_value", + "name": "PARAM", + "text": "params" + }, + ], + "inputsInline": true, + "output": null, + "style": 'text_blocks', + "tooltip": "Returns the first index of substring value, or -1 if not found", + "helpUrl": "" + }, + { // text 4 + "type": "praxly_length_block", + "message0": "%1.length ( )", + "args0": [ + { + "type": "input_value", + "name": "EXPRESSION" + }, + ], + "inputsInline": true, + "output": null, + "style": 'text_blocks', + "tooltip": "Returns the length of the string", + "helpUrl": "" + }, + { // text 5 + "type": "praxly_substring_block", + "message0": "%1.substring (%2 , %3)", + "args0": [ + { + "type": "input_value", + "name": "EXPRESSION" + }, + { + "type": "input_value", + "name": "PARAM1", + "text": "params" + }, + { + "type": "input_value", + "name": "PARAM2", + "text": "params" + } + ], + "inputsInline": true, + "output": null, + "style": 'text_blocks', + "tooltip": "Extracts characters from the start index up to but not including the end index", + "helpUrl": "" + }, + { // text 6 + "type": "praxly_toLowerCase_block", + "message0": "%1.toLowerCase ( )", + "args0": [ + { + "type": "input_value", + "name": "EXPRESSION" + } + ], + "inputsInline": true, + "output": null, + "style": 'text_blocks', + "tooltip": "Converts the string to all lowercase", + "helpUrl": "" + }, + { // text 7 + "type": "praxly_toUpperCase_block", + "message0": "%1.toUpperCase ( )", + "args0": [ + { + "type": "input_value", + "name": "EXPRESSION" + } + ], + "inputsInline": true, + "output": null, + "style": 'text_blocks', + "tooltip": "Converts the string to all uppercase", + "helpUrl": "" + }, { // logic 1 "type": "praxly_true_block", "message0": "true", @@ -999,7 +1129,7 @@ export function definePraxlyBlocks(workspace) { }, { "type": "input_statement", - "name": "CONTENTS" + "name": "CODEBLOCK" }, { "type": "field_input", diff --git a/src/text2tree.js b/src/text2tree.js index 5e7714d..84c6a20 100644 --- a/src/text2tree.js +++ b/src/text2tree.js @@ -1,4 +1,4 @@ -import { MAX_LOOP, NODETYPES, OP, TYPES, indexToAceRange, textEditor, textError } from './common'; +import { MAX_LOOP, NODETYPES, OP, textEditor, textError } from './common'; /** * this will take all of the text currently in the editor and generate the corresponding Intermediate Representation . @@ -10,8 +10,6 @@ export function text2tree() { let lexer = new Lexer(code); let ir; let tokens = lexer.lex(); - // console.info('here are the tokens:'); - // console.debug(tokens); let parser = new Parser(tokens); ir = parser?.parse(); return ir; @@ -33,8 +31,9 @@ class Token { class Lexer { constructor(source) { + // insert a final newline at the end of the program if missing if (source?.length > 0 && source[source?.length - 1] !== '\n') { - source += "\n"; + source += '\n'; } this.source = source; this.tokens = []; @@ -43,7 +42,7 @@ class Lexer { this.length = this.source?.length; this.token_so_far = ""; this.multi_Char_symbols = ['>', '<', '=', '!', '-']; - this.symbols = [",", ";", "(", ")", "{", "}", "[", "]", ".", "+", "/", "*", "%", "^", "≠", , "←", "⟵", "≥", "≤"]; + this.symbols = [",", ";", "(", ")", "{", "}", "[", "]", ".", "+", "/", "*", "%", "^", "≠", "←", "⟵", "≥", "≤"]; this.builtins = ['input', 'random', 'randomInt', 'randomSeed', 'int', 'float', 'min', 'max', 'abs', 'log', 'sqrt']; this.keywords = ["if", "else", "end", "print", "for", "while", 'and', 'or', 'do', 'repeat', 'until', 'not', 'return', 'null']; @@ -124,13 +123,9 @@ class Lexer { emit_token(type = null) { var endIndex = this.i - this.index_before_this_line; - // console.error(endIndex); this.tokens.push(new Token(type ?? this.token_so_far, this.token_so_far, this.currentLine, this.startToken, [this.currentLine, endIndex])); this.token_so_far = ''; - - // console.error([this.currentLine, endIndex]); this.startToken = [this.currentLine, endIndex]; - // console.error(this.startToken); } /** @@ -140,6 +135,7 @@ class Lexer { lex() { while (this.i < this.length) { + // end-of-line comment if (this.has_short_comment()) { this.skip(2); while (this.hasNot('\n')) { @@ -151,55 +147,64 @@ class Lexer { continue; } + // missing code (/* comment */) if (this.has_long_comment()) { this.skip(2); - while (this.hasNot('*') && this.hasNot_ahead('/')) { + while (this.hasNot('*') && this.hasNot_ahead('/') && this.hasNot('\n')) { this.capture(); } - if (this.i === this.length) { + if (this.hasNot('\n')) { + this.skip(2); + } else { textError('lexing', `looks like you didn\'t close your comment. Remember comments - start with a \'/*\' and end with a \'*/\'.`, commentStart, this.currentLine); + start with a \'/*\' and end with a \'*/\'.`, this.currentLine + 1); } - this.skip(2); this.emit_token(NODETYPES.COMMENT); continue; } + // character literal (single quotes) if (this.has('\'')) { this.skip(); - if (this.has_letter && this.has_ahead('\'')) { + while (this.i < this.length && !this.has("\'") && !this.has("\n")) { this.capture(); - this.skip(); + } + 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); this.emit_token(NODETYPES.CHAR); continue; } - 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); + this.skip(); // close single quote + const length = this.token_so_far.length; + if (this.token_so_far.length != 1) { + textError('lexing', 'incorrect character length: ' + length, this.currentLine + 1); + } + this.emit_token(NODETYPES.CHAR); + continue; } + // string literal (double quotes) if (this.has("\"")) { - var stringStart = this.currentLine; this.skip(); while (this.i < this.length && !this.has("\"") && !this.has("\n")) { this.capture(); } - if (this.has("\"")) { - this.skip(); + 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); this.emit_token(NODETYPES.STRING); continue; } - textError('lexing', 'looks like you didn\'t close your quotes on your String. \n \tRemember Strings start and end with a double quote mark (\").', stringStart); - this.i -= 1; - this.token_so_far = ""; + this.skip(); // close double quote this.emit_token(NODETYPES.STRING); continue; } + // operators and punctuation if (this.has_valid_symbol()) { this.capture(); this.emit_token(); continue; } - if (this.has_multi_char_symbol()) { while (this.has_multi_char_symbol()) { this.capture(); @@ -208,6 +213,7 @@ class Lexer { continue; } + // integers and floating-points if (this.has_digit()) { while (this.i < this.length && this.has_digit()) { this.capture(); @@ -223,10 +229,14 @@ class Lexer { this.emit_token(NODETYPES.INT); continue; } + + // ignore spaces if (this.has(' ')) { this.skip(); continue; } + + // newline (increment line number) if (this.has("\n")) { this.capture(); this.emit_token("\n"); @@ -235,13 +245,18 @@ class Lexer { this.startToken = [this.currentLine, this.i - this.index_before_this_line]; continue; } + + // get the next word if (!this.has_letter()) { - textError('lexing', `unrecognized character \"${this.source[this.i]}\"`, this.i, this.i + 1); - break; + textError('lexing', `unrecognized character \"${this.source[this.i]}\"`, this.currentLine + 1); + this.skip(); + continue; } while (this.i < this.length && (this.has_letter() || this.has_digit())) { this.capture(); } + + // built-in types and functions if (this.has_type()) { this.emit_token('Type'); continue; @@ -250,59 +265,84 @@ class Lexer { this.emit_token(NODETYPES.BUILTIN_FUNCTION_CALL); continue; } + + // end keyword (if, for, while, proc) if (this.token_so_far === 'end') { - while (this.hasNot('\n')) { + if (this.has(' ')) { + this.capture(); + } else { + textError('lexing', 'missing space after end keyword', this.currentLine + 1); + } + while (this.has(' ')) { // ignore extra spaces + this.skip(); + } + while (this.has_letter()) { this.capture(); } this.emit_token(); continue; } + + // true/false literal if (this.has_boolean()) { this.emit_token(NODETYPES.BOOLEAN); continue; } + + // other keywords if (this.has_keyword()) { this.emit_token(); continue; } + + // variable/procedure names this.emit_token('Location'); } + + // That's all folks! this.emit_token("EOF"); return this.tokens; } } + class Parser { constructor(tokens) { - this.statements = []; this.tokens = tokens; this.i = 0; this.length = tokens?.length; - this.eof = false; - this.keywords = ["if", "else", "then", "done"]; - this.statementKeywords = ['if', 'print', 'for', 'while']; - this.specialStringFunctionKEywords = ["charAt", "contains", "indexOf", "length", "substring", "toLowerCase", "toUpperCase"]; - } - - hasNot(type) { - return this.i < this.length && this.tokens[this.i].token_type !== type; } getCurrentToken() { return this.tokens[this.i]; } + getCurrentLine() { + return this.tokens[this.i].line; + } + + getCurrentValue() { + return this.tokens[this.i].value; + } + + getPrevTokenType() { + return this.tokens[this.i - 1].token_type; + } + has(type) { return this.i < this.length && this.tokens[this.i].token_type === type; } + hasNot(type) { + return this.i < this.length && this.tokens[this.i].token_type !== type; + } + hasAny() { var types = Array.prototype.slice.call(arguments); return this.i < this.length && types.includes(this.tokens[this.i].token_type); } - hasNotAny() { var types = Array.prototype.slice.call(arguments); return this.i < this.length && !types.includes(this.tokens[this.i].token_type); @@ -320,18 +360,11 @@ class Parser { return this.i < this.length && this.tokens[this.i].token_type === 'Type'; } - has_keyword() { - return this.keywords.includes(this.tokens[this.i].token_type); - } - has_statementKeyword() { - return this.statementKeywords.includes(this.tokens[this.i].token_type); - } - match_and_discard_next_token(type) { if (this.tokens[this.i].token_type === type) { this.advance(); } else { - textError('parsing', `did not detect desired token at this location. \nexpected: \'${type}\'\n but was: ${this.tokens[this.i].token_type}`, this.tokens[this.i].line); + textError('parsing', `did not detect desired token at this location. \nexpected: \'${type}\'\n but was: ${this.tokens[this.i].token_type}`, this.getCurrentLine()); } } @@ -347,8 +380,6 @@ class Parser { return this.parse_program(); } - - /** * This function creates new nodes for the AST for any binary operation * @param {*} operation the operation symbol @@ -414,7 +445,7 @@ class Parser { type = OP.OR; break; default: - // handle unknown type + textError('parsing', 'invalid operator ' + operation, line); break; } return { @@ -423,13 +454,11 @@ class Parser { left: l, right: r, type: type, - startIndex: l.startIndex, - endIndex: r.endIndex, + startIndex: l?.startIndex, + endIndex: r?.endIndex, } } - - /** * Creates a new node for literal values in the AST. * @param {*} token @@ -457,7 +486,6 @@ class Parser { unaryOPNode_new(operation, expression, line, startIndex) { var type; switch (operation) { - case '!': case 'not': type = OP.NOT; break; @@ -471,7 +499,7 @@ class Parser { value: expression, type: type, startIndex: startIndex, - endIndex: expression.endIndex, + endIndex: expression?.endIndex, } } @@ -482,92 +510,103 @@ class Parser { */ parse_expression(precedence = 9) { let operation = this.getCurrentToken().token_type; - let line = this.tokens[this.i].line; + let line = this.getCurrentLine(); let startIndex = this.getCurrentToken().startIndex; - let endIndex = this.getCurrentToken().endIndex; switch (precedence) { + + // or logical operator case 9: var l = this.parse_expression(precedence - 1); while (this.has("or")) { operation = this.getCurrentToken().token_type; - line = this.getCurrentToken().line; + line = this.getCurrentLine(); this.advance(); const r = this.parse_expression(precedence - 1); l = this.binaryOpNode_new(operation, l, r, line); } return l; + + // and logical operator case 8: var l = this.parse_expression(precedence - 1); while (this.has("and")) { operation = this.getCurrentToken().token_type; - line = this.getCurrentToken().line; + line = this.getCurrentLine(); this.advance(); const r = this.parse_expression(precedence - 1); l = this.binaryOpNode_new(operation, l, r, line); } return l; + // relational operators case 7: var l = this.parse_expression(precedence - 1); while (this.hasAny('<', '>', '==', '!=', '>=', '<=', '≠', '≥', '≤')) { operation = this.getCurrentToken().token_type; - line = this.getCurrentToken().line; + line = this.getCurrentLine(); this.advance(); const r = this.parse_expression(precedence - 1); l = this.binaryOpNode_new(operation, l, r, line); } return l; + + // addition, subtraction case 6: var l = this.parse_expression(precedence - 1); while (this.hasAny('+', '-')) { operation = this.getCurrentToken().token_type; - line = this.getCurrentToken().line; + line = this.getCurrentLine(); this.advance(); const r = this.parse_expression(precedence - 1); l = this.binaryOpNode_new(operation, l, r, line); } return l; + + // multiplication, division, modulo case 5: var l = this.parse_expression(precedence - 1); while (this.hasAny('*', '/', '%')) { operation = this.getCurrentToken().token_type; - line = this.getCurrentToken().line; + line = this.getCurrentLine(); this.advance(); const r = this.parse_expression(precedence - 1); l = this.binaryOpNode_new(operation, l, r, line); } return l; + + // raise to the power case 4: var l = this.parse_expression(precedence - 1); while (this.hasAny('^')) { operation = this.getCurrentToken().token_type; - line = this.getCurrentToken().line; + line = this.getCurrentLine(); this.advance(); - const r = this.parse_expression(precedence); + const r = this.parse_expression(precedence - 1); l = this.binaryOpNode_new(operation, l, r, line); } return l; + // not logical operator, negation operator case 3: - if (this.hasNotAny('not', '!', '-')) { + if (this.hasNotAny('not', '-')) { return this.parse_expression(precedence - 1); } operation = this.getCurrentToken().token_type; - line = this.getCurrentToken().line; + line = this.getCurrentLine(); this.advance(); var exp = this.parse_expression(precedence - 1); return this.unaryOPNode_new(operation, exp, line, startIndex); - // This is the dot operator + // dot operator (for string methods) case 2: var l = this.parse_expression(precedence - 1); while (this.hasAny('.')) { operation = this.getCurrentToken().token_type; - line = this.getCurrentToken().line; + line = this.getCurrentLine(); this.advance(); - const r = this.parse_expression(precedence); + 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 . operator must be a supported string function", line); + textError('parsing', "classes are not fully supported yet. the right side of the dot operator must be a supported string function", line); } l = { left: l, @@ -579,13 +618,13 @@ class Parser { } return l; - //This one gets really complicated + // This one gets really complicated case 1: switch (operation) { case 'EOF': - this.eof = true; return 'EOF'; + // literal value case NODETYPES.BOOLEAN: case NODETYPES.CHAR: case NODETYPES.DOUBLE: @@ -596,13 +635,10 @@ class Parser { this.advance(); return this.literalNode_new(this.tokens[this.i - 1]); - case NODETYPES.BUILTIN_FUNCTION_CALL: - case 'Type': // type conversion function - return this.parse_builtin_function_call(line); - + // parentheses case '(': const leftToken = this.advance(); - const expression = this.parse_expression(9); + const expression = this.parse_expression(); if (this.has(")")) { const rightToken = this.advance(); return { @@ -614,10 +650,10 @@ class Parser { endIndex: rightToken.endIndex, }; } else { - textError('parsing', 'did not detect closing parentheses', line,); + textError('parsing', 'did not detect closing parentheses', line); } - //ah yes, array literals....very fun + // ah yes, array literals....very fun case '{': let result = { blockID: 'code', @@ -630,7 +666,7 @@ class Parser { this.advance(); var loopBreak = 0; while (this.hasNot('}') && loopBreak < MAX_LOOP) { - var param = this.parse_expression(9); + var param = this.parse_expression(); args.push(param); if (this.has(',')) { this.advance(); @@ -639,19 +675,30 @@ class Parser { } result.params = args; if (this.hasNot('}')) { - textError('parsing', "didn't detect closing curly brace in the array declaration", this.tokens[this.i].line); + textError('parsing', "didn't detect closing curly brace in the array declaration", this.getCurrentLine()); } result.endIndex = this.getCurrentToken().endIndex; this.advance(); return result; + // built-in function call + case NODETYPES.BUILTIN_FUNCTION_CALL: + case 'Type': // type conversion function + if (this.has_ahead('(')) { + return this.parse_builtin_function_call(line); + } + else { + // parse as 'Location' instead (Ex: variable named max) + } + + // variable assignment or procedure call case 'Location': var l = this.parse_location(); if (this.hasAny('=', '<-', "←", "⟵")) { this.advance(); - var value = this.parse_expression(9); + var value = this.parse_expression(); l = { - type: NODETYPES.ASSIGNMENT, + type: l.isArray ? NODETYPES.ARRAY_REFERENCE_ASSIGNMENT : NODETYPES.ASSIGNMENT, blockID: "code", line: line, location: l, @@ -663,7 +710,7 @@ class Parser { var args = []; var loopBreak = 0; while (this.hasNot(')') && loopBreak < MAX_LOOP) { - var param = this.parse_expression(9); + var param = this.parse_expression(); args.push(param); if (this.has(',')) { this.advance(); @@ -680,21 +727,20 @@ class Parser { args: args, startIndex: startIndex, } - // this is used to give different behavior to these functions in particular - // if (this.specialStringFunctionKEywords.includes(l.name)){ - // l.type = NODETYPES.SPECIAL_STRING_FUNCCALL; - // } } l.endIndex = this.getCurrentToken().endIndex; return l; default: - // TODO: this case needs to raise an exception or return some error - // object. Right now if an expression can't be parsed, it - // implicitly returns null/undefined. - textError('parsing', `invalid Token ${this.getCurrentToken().value}`, line); - } - } + if (this.getCurrentValue() != '\n') { + textError('parsing', `invalid token ${this.getCurrentValue()}`, line); + this.advance(); + } else { + textError('parsing', `invalid end of line`, line); + } + + } // switch (operation) + } // switch (precedence) } parse_builtin_function_call(line) { @@ -703,15 +749,15 @@ class Parser { if (this.has('(')) { this.advance(); - // Expect 0 or more parameters. - const parameters = []; + // Expect 0 or more arguments. + const args = []; if (this.hasNot(')')) { - const parameter = this.parse_expression(9); - parameters.push(parameter); + const parameter = this.parse_expression(); + args.push(parameter); while (this.has(',')) { this.advance(); - const parameter = this.parse_expression(9); - parameters.push(parameter); + const parameter = this.parse_expression(); + args.push(parameter); } } @@ -721,7 +767,7 @@ class Parser { blockID: "code", name: nameToken.value, line, - parameters, + args, type: NODETYPES.BUILTIN_FUNCTION_CALL, startIndex: nameToken.startIndex, endIndex: rightParenthesisToken.endIndex, @@ -730,17 +776,20 @@ class Parser { textError('parsing', 'did not detect right parenthesis', line); } } else { - textError('parsing', 'did not detect left parenthesis', line); + textError('parsing', `missing parentheses for built-in ${nameToken.value}() function`, line); } } parse_location() { + if (this.getCurrentValue() == '\n') { + return; // incomplete line + } var result = { type: NODETYPES.LOCATION, - name: this.tokens[this.i].value, + name: this.getCurrentValue(), isArray: false, blockID: 'code', - line: this.tokens[this.i].line, + line: this.getCurrentLine(), index: null, startIndex: this.tokens[this.i].startIndex, } @@ -757,9 +806,11 @@ class Parser { // this one kinda a mess ngl, but my goal is to support variable initialization and assignment all together parse_funcdecl_or_vardecl() { + + // return type var isArray = false; var type = NODETYPES.VARDECL; - var vartype = this.tokens[this.i].value; + var vartype = this.getCurrentToken().value; var startIndex = this.getCurrentToken().startIndex; this.advance(); if (this.has('[') && this.has_ahead(']')) { @@ -769,37 +820,42 @@ class Parser { isArray = true; } + // variable/procedure name var location = this.parse_location(); var result = { type: type, varType: vartype, - name: location.name, + name: location?.name, isArray: isArray, blockID: 'code', location: location, - line: this.tokens[this.i].line, + line: this.getCurrentLine(), index: null, startIndex: startIndex, } + + // initialization (optional) if (this.hasAny('=', '<-', "←", "⟵")) { this.advance(); - result.value = this.parse_expression(9); - if (this.has(';')) { - this.advance(); - } - } else if (this.has('(')) { + result.value = this.parse_expression(); + } + + // procedure definition + else if (this.has('(')) { result.type = NODETYPES.FUNCDECL; result.returnType = vartype; if (type == NODETYPES.ARRAY_ASSIGNMENT) { result.returnType += "[]"; } + + // parameter list this.match_and_discard_next_token('('); var params = []; var stopLoop = 0; while (this.hasNot(')') && stopLoop < MAX_LOOP) { var param = []; if (this.has_type()) { - param.push(this.tokens[this.i].value); + param.push(this.getCurrentValue()); this.advance(); } if (this.has('[') && this.has_ahead(']')) { @@ -808,7 +864,7 @@ class Parser { param[0] += "[]"; } if (this.has('Location')) { - param.push(this.tokens[this.i].value); + param.push(this.getCurrentValue()); this.advance(); } params.push(param); @@ -817,34 +873,18 @@ class Parser { } stopLoop += 1; } - - result.endIndex = this.getCurrentToken().endIndex; this.match_and_discard_next_token(')'); result.params = params; - if (this.has(';')) { - this.advance(); - } - if (this.has('\n')) { - this.advance(); - } - - var contents = this.parse_block('end ' + result.name); - result.contents = contents; - if (this.hasNot('end ' + result.name)) { - textError('parsing', `missing the \'end ${result.name}\' token`, result.line); - result.endIndex = this.getCurrentToken().endIndex; - return result; - } - this.advance(); + // procedure body + result.codeblock = this.parse_block('end ' + result.name); } + result.endIndex = this.getCurrentToken().endIndex; return result; } - - parse_program() { return { type: "PROGRAM", @@ -854,110 +894,131 @@ class Parser { } /** - * parses a block of statements (think curlybraces) + * parses a block of statements (think curly braces) * @param {...any} endToken any tokens that will determine the end of the block. * @returns */ parse_block(...endToken) { - let praxly_blocks = []; - while (this.hasNotAny(...endToken)) { + let block_statements = []; + + // parse end of line at beginning of the block + if (this.i > 0) { + let comment = this.parse_end_of_line(true); + if (comment) { + // move trailing comment into the new block + block_statements.push(comment); + } + } - if (this.has('EOF')) { - break; + // parse all statements inside the block + while (!this.has('EOF') && this.hasNotAny(...endToken)) { + let stmt = this.parse_statement(); + let comment = this.parse_end_of_line(false); + if (stmt?.codeblock) { + // move trailing comment after the block end + block_statements.push(stmt); + if (comment) { + block_statements.push(comment); + } + } else { + // move trailing comment above the statement + if (comment) { + block_statements.push(comment); + } + block_statements.push(stmt); } - praxly_blocks.push(this.parse_statement()); - // note: I for some reason always assumed that statements will not parse the final token, so I always did it here. - // I think its because I assumed that all statements end with a newline. + } + + // make sure the block is correctly terminated + if (this.hasAny(...endToken)) { this.advance(); + } else { + let name = endToken[endToken.length - 1]; + textError('parsing', `missing the '${name}' token`, this.getCurrentLine()); } + + // return the resulting block of statements return { type: NODETYPES.CODEBLOCK, - statements: praxly_blocks, + statements: block_statements, blockID: "code" } } parse_statement() { - var line = this.tokens[this.i].line; + var line = this.getCurrentLine(); let result = { startIndex: this.getCurrentToken().startIndex, endIndex: this.getCurrentToken().endIndex, blockID: 'code', line: line, }; + + // blank line (no statement) if (this.has('\n')) { result.type = NODETYPES.NEWLINE; return result; } - if (this.has("if")) { + // if or if-else + else if (this.has("if")) { result.type = NODETYPES.IF; this.advance(); if (this.hasNot('(')) { return result; } this.advance(); - result.condition = this.parse_expression(9); + result.condition = this.parse_expression(); result.endIndex = this.getCurrentToken().endIndex; if (this.hasNot(')')) { return result; } this.advance(); - if (this.has('\n')) { - this.advance(); - result.statement = this.parse_block('else', 'end if'); - if (this.has('else')) { - this.advance(); - if (this.has('\n')) { - this.advance(); - } - result.type = NODETYPES.IF_ELSE; - result.alternative = this.parse_block('end if'); - } - if (this.has('end if')) { - this.advance(); - return result; - } - else { - textError('parsing', "missing the \'end if\' token", result.line); - } + + result.codeblock = this.parse_block('else', 'end if'); + if (this.getPrevTokenType() == 'else') { + result.type = NODETYPES.IF_ELSE; + result.alternative = this.parse_block('end if'); } + } - } else if (this.has('for')) { + // for loop + else if (this.has('for')) { result.type = NODETYPES.FOR; this.advance(); if (this.hasNot('(')) { return result; } this.advance(); - result.initialization = this.parse_statement(); - result.initialization.endIndex[1] -= 3; // HACK - result.condition = this.parse_expression(9); - if (this.has(';')) { - this.advance(); - result.increment = this.parse_expression(1); - if (this.hasNot(')')) { - return result; - } - this.advance(); - if (this.has('\n')) { - result.endIndex = this.getCurrentToken().endIndex; - this.advance(); - result.statement = this.parse_block('end for'); - if (this.has('end for')) { - this.advance(); - return result; - } - else { - textError('parsing', "missing the \'end for\' token", result.line); - } - } + if (this.has_type()) { + result.initialization = this.parse_funcdecl_or_vardecl(); + } else { + result.initialization = this.parse_expression(1); + } + if (this.hasNot(';')) { // 1st required + return result; } - console.log(`parser messing up, current token is ${this.tokens[this.i].token_type}`); + this.advance(); + + result.condition = this.parse_expression(); + if (this.hasNot(';')) { // 2nd required + return result; + } + this.advance(); + + result.increment = this.parse_expression(1); + if (this.hasNot(')')) { + return result; + } + this.advance(); + + result.endIndex = this.getCurrentToken().endIndex; + result.codeblock = this.parse_block('end for'); return result; } + // while loop else if (this.has('while')) { result.type = NODETYPES.WHILE; this.advance(); @@ -965,74 +1026,56 @@ class Parser { return result; } this.advance(); - result.condition = this.parse_expression(9); + result.condition = this.parse_expression(); result.endIndex = this.getCurrentToken().endIndex; if (this.hasNot(')')) { return result; } this.advance(); - if (this.has('\n')) { - this.advance(); - result.statement = this.parse_block('end while'); - } - if (this.has('end while')) { - this.advance(); - return result; - } else { - textError('parsing', "missing the \'end while\' token", result.line); - } + result.codeblock = this.parse_block('end while'); } + // do-while loop else if (this.has('do')) { result.type = NODETYPES.DO_WHILE; this.advance(); - if (this.has('\n')) { - this.advance(); - result.statement = this.parse_block('while'); + result.codeblock = this.parse_block('while'); + if (this.hasNot('(')) { + return result; } - if (this.has('while')) { - this.advance(); - if (this.hasNot('(')) { - return result; - } - this.advance(); - result.condition = this.parse_expression(9); - if (this.hasNot(')')) { - return result; - } - this.advance(); + this.advance(); + result.condition = this.parse_expression(); + if (this.hasNot(')')) { return result; } + this.advance(); + return result; } + // repeat-until loop else if (this.has('repeat')) { result.type = NODETYPES.REPEAT_UNTIL; this.advance(); - if (this.has('\n')) { - this.advance(); - result.statement = this.parse_block('until'); + result.codeblock = this.parse_block('until'); + if (this.hasNot('(')) { + return result; } - if (this.has('until')) { - this.advance(); - if (this.hasNot('(')) { - return result; - } - this.advance(); - result.condition = this.parse_expression(9); - if (this.hasNot(')')) { - return result; - } - this.advance(); + this.advance(); + result.condition = this.parse_expression(); + if (this.hasNot(')')) { return result; } + this.advance(); + return result; } + // print statement else if (this.has("print")) { this.advance(); - const expression = this.parse_expression(9); + const expression = this.parse_expression(); result.endIndex = expression?.endIndex ?? this.getCurrentToken().endIndex; - if (this.has(';')) { + while (this.has(';')) { // special case: bundle comment with print node this.advance(); } @@ -1048,31 +1091,32 @@ class Parser { return result; } + // return statement else if (this.has("return")) { this.advance(); - const expression = this.parse_expression(9); - if (this.has(';')) { - this.advance(); - } - if (this.has('\n')) { - result.type = NODETYPES.RETURN; - result.value = expression; - return result; - } + const expression = this.parse_expression(); + result.type = NODETYPES.RETURN; + result.value = expression; + return result; + } - } else if (this.has(NODETYPES.COMMENT)) { + // missing code (/* comment */) + else if (this.has(NODETYPES.COMMENT)) { const token = this.advance(); result.type = NODETYPES.COMMENT; result.value = token.value; return result; + } - } else if (this.has(NODETYPES.SINGLE_LINE_COMMENT)) { + // end-of-line comment + else if (this.has(NODETYPES.SINGLE_LINE_COMMENT)) { const token = this.advance(); result.type = NODETYPES.SINGLE_LINE_COMMENT; result.value = token.value; return result; } + // variable/function declaration else if (this.has_type()) { if (this.hasNot_ahead('(')) { return this.parse_funcdecl_or_vardecl(); @@ -1089,24 +1133,13 @@ class Parser { } } - else if (this.has('\n')) { - return { - type: NODETYPES.NEWLINE, - blockID: 'code', - } - } - + // most likely a function call else { - // this is a stand alone expression as a statement. - let contents = this.parse_expression(9); - - // if (!contents?.startIndex ?? false){ - // console.error(this.getCurrentToken()); - // console.error(this.tokens[this.i + 1]); - // } - - if (this.has(';')) { - this.advance(); + let contents = 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; } result = { type: NODETYPES.STATEMENT, @@ -1118,4 +1151,48 @@ class Parser { } return result; } + + /** + * Called at the expected end of each line to advance the parser to + * the next line. Parses any semicolons, optional comments, and the + * newline character. + * + * @param {boolean} new_block true if starting a new block (no semicolons) + */ + parse_end_of_line(new_block) { + + // optional semicolons + if (new_block && this.has(';')) { + textError('parsing', "semicolon not allowed here", this.getCurrentLine()); + } + while (this.has(';')) { + this.advance(); + } + + // optional end-of-line comment + let comment; + if (this.has(NODETYPES.SINGLE_LINE_COMMENT) || this.has(NODETYPES.COMMENT)) { + const token = this.advance(); + comment = { + type: this.getPrevTokenType(), + value: token.value, + startIndex: token.startIndex, + endIndex: token.endIndex, + blockID: 'code', + line: token.line, + }; + } + + // required newline character + if (this.has('\n')) { + this.advance(); + } else if (!this.has('EOF')) { + textError('parsing', "expected end of line", this.getCurrentLine()) + while (!this.has('\n')) { + this.advance(); + } + this.advance(); + } + return comment; + } } diff --git a/src/theme.js b/src/theme.js index a003568..92d7204 100644 --- a/src/theme.js +++ b/src/theme.js @@ -63,13 +63,13 @@ export const PraxlyDark = Blockly.Theme.defineTheme('PraxlyDark', { 'base': Blockly.Themes.Classic, 'blockStyles': { 'loop_blocks': { - 'colourPrimary': '#0361FF' + 'colourPrimary': '#395BBF' }, 'array_blocks': { 'colourPrimary': '#FA0000' }, 'logic_blocks': { - 'colourPrimary': '#00D084' + 'colourPrimary': '#3CB371' }, 'class_blocks': { 'colourPrimary': '#6381fe' @@ -78,19 +78,22 @@ export const PraxlyDark = Blockly.Theme.defineTheme('PraxlyDark', { 'colourPrimary': '#808080' }, 'procedure_blocks': { - 'colourPrimary': '#6381fe' + 'colourPrimary': '#9370DB' }, 'variable_blocks': { 'colourPrimary': '#f80069' }, - 'expression_blocks': { - 'colourPrimary': '#a7ca00' + 'math_blocks': { + 'colourPrimary': '#FF9966' }, 'parameter_blocks': { 'colourPrimary': '#8F48B7' }, 'other_blocks': { 'colourPrimary': '#00BFFF' + }, + 'text_blocks': { + 'colourPrimary': '#FFC050', } }, 'categoryStyles': { @@ -98,10 +101,10 @@ export const PraxlyDark = Blockly.Theme.defineTheme('PraxlyDark', { 'colour': '#395BBF' }, 'procedure_blocks': { - 'colour': '#6381fe' + 'colour': '#9370DB' }, 'logic_blocks': { - 'colour': '#00D084' + 'colour': '#3CB371' }, 'class_blocks': { 'colour': '#6381fe' @@ -115,8 +118,11 @@ export const PraxlyDark = Blockly.Theme.defineTheme('PraxlyDark', { 'variable_blocks': { 'colour': '#f80069' }, - 'expression_blocks': { - 'colour': '#a7ca00' + 'math_blocks': { + 'colour': '#FF9966' + }, + 'text_blocks': { + 'colour': '#FFC050' } }, 'componentStyles': { diff --git a/src/toolbox.js b/src/toolbox.js index 86b4759..7d78699 100644 --- a/src/toolbox.js +++ b/src/toolbox.js @@ -17,7 +17,7 @@ export const toolbox = { 'shadow': { 'type': 'praxly_literal_block', 'fields': { - 'LITERAL': '\"hello, world\"', + 'LITERAL': '\"Hello, World!\"', } }, }, @@ -39,25 +39,6 @@ export const toolbox = { 'kind': 'block', 'type': 'praxly_emptyline_block' }, - { - 'kind': 'block', - 'type': 'praxly_StringFunc_block', - 'inputs': { - 'EXPRESSION': { - 'shadow': { - 'type': 'praxly_literal_block', - 'fields': { - 'LITERAL': '\"hello, world\"', - } - }, - }, - 'PARAMS': { - 'block': { - 'type': 'praxly_parameter_block', - } - } - } - } ] }, { @@ -65,10 +46,6 @@ export const toolbox = { "name": "variables", "categorystyle": "variable_blocks", "contents": [ - { - 'kind': 'block', - 'type': 'praxly_variable_block' - }, { 'kind': 'block', 'type': 'praxly_vardecl_block', @@ -81,7 +58,7 @@ export const toolbox = { 'shadow': { 'type': 'praxly_literal_block', 'fields': { - 'LITERAL': 0, + 'LITERAL': 'value', } }, }, @@ -93,14 +70,18 @@ export const toolbox = { 'inputs': { 'EXPRESSION': { 'shadow': { - 'type': 'praxly_literal_block', + 'type': 'praxly_variable_block', 'fields': { - 'LITERAL': 0, + 'LITERAL': 'value', } }, }, } }, + { + 'kind': 'block', + 'type': 'praxly_variable_block' + }, { 'kind': 'block', 'type': 'praxly_array_assignment_block', @@ -120,10 +101,18 @@ export const toolbox = { 'shadow': { 'type': 'praxly_literal_block', 'fields': { - 'LITERAL': '0', + 'LITERAL': 'index', } }, }, + 'EXPRESSION': { + 'shadow': { + 'type': 'praxly_literal_block', + 'fields': { + 'LITERAL': 'value' + } + } + } } }, { @@ -134,7 +123,7 @@ export const toolbox = { 'shadow': { 'type': 'praxly_literal_block', 'fields': { - 'LITERAL': '0', + 'LITERAL': 'index', } }, }, @@ -145,12 +134,8 @@ export const toolbox = { { "kind": "category", "name": "math", - "categorystyle": "expression_blocks", + "categorystyle": "math_blocks", "contents": [ - { - 'kind': 'block', - 'type': 'praxly_literal_block' - }, // { // 'kind': 'block', // 'type': 'praxly_String_block' @@ -191,6 +176,10 @@ export const toolbox = { }, } }, + { + 'kind': 'block', + 'type': 'praxly_literal_block' + }, { 'kind': 'block', 'type': 'praxly_random_block' @@ -303,7 +292,7 @@ export const toolbox = { 'shadow': { 'type': 'praxly_literal_block', 'fields': { - 'LITERAL': 1 + 'LITERAL': -5 } } } @@ -317,7 +306,7 @@ export const toolbox = { 'shadow': { 'type': 'praxly_literal_block', 'fields': { - 'LITERAL': 1 + 'LITERAL': 2.718 } } } @@ -331,7 +320,7 @@ export const toolbox = { 'shadow': { 'type': 'praxly_literal_block', 'fields': { - 'LITERAL': 1 + 'LITERAL': 25 } } } @@ -341,24 +330,194 @@ export const toolbox = { }, { "kind": "category", - "name": "logic", - "categorystyle": "logic_blocks", + "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_true_block' + 'type': 'praxly_charAt_block', + 'inputs': { + 'EXPRESSION': { + 'shadow': { + 'type': 'praxly_literal_block', + 'fields': { + 'LITERAL': '"Hello"', + } + }, + }, + 'INDEX': { + 'block': { + 'type': 'praxly_literal_block', + 'fields': { + 'LITERAL': 'index' + } + } + } + } }, { 'kind': 'block', - 'type': 'praxly_false_block' - }, + 'type': 'praxly_contains_block', + 'inputs': { + 'EXPRESSION': { + 'shadow': { + 'type': 'praxly_literal_block', + 'fields': { + 'LITERAL': '"Hello"', + } + }, + }, + 'PARAM': { + 'block': { + 'type': 'praxly_literal_block', + 'fields': { + 'LITERAL': 'string' + } + } + } + } + }, + { + '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' + } + } + } + } + }, + { + '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"', + } + }, + }, + '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_toUpperCase_block', + 'inputs': { + 'EXPRESSION': { + 'shadow': { + 'type': 'praxly_literal_block', + 'fields': { + 'LITERAL': '"Hello"', + } + }, + }, + } + }, + ] + }, + { + "kind": "category", + "name": "logic", + "categorystyle": "logic_blocks", + "contents": [ { 'kind': 'block', - 'type': 'praxly_if_block' + 'type': 'praxly_if_block', + 'inputs': { + 'CONDITION': { + 'shadow': { + 'type': 'praxly_true_block' + } + } + } }, { 'kind': 'block', - 'type': 'praxly_if_else_block' + 'type': 'praxly_if_else_block', + 'inputs': { + 'CONDITION': { + 'shadow': { + 'type': 'praxly_true_block' + } + } + } }, { 'kind': 'block', @@ -366,25 +525,26 @@ export const toolbox = { 'inputs': { 'A_OPERAND': { 'shadow': { - 'type': 'praxly_literal_block', - 'fields': { - 'LITERAL': true, - } + 'type': 'praxly_true_block', }, }, 'B_OPERAND': { 'shadow': { - 'type': 'praxly_literal_block', - 'fields': { - 'LITERAL': false, - } + 'type': 'praxly_false_block', }, } } }, { 'kind': 'block', - 'type': 'praxly_not_block' + 'type': 'praxly_not_block', + 'inputs': { + 'EXPRESSION': { + 'shadow': { + 'type': 'praxly_true_block' + } + } + } }, { 'kind': 'block', @@ -407,6 +567,14 @@ export const toolbox = { }, } } + }, + { + 'kind': 'block', + 'type': 'praxly_true_block' + }, + { + 'kind': 'block', + 'type': 'praxly_false_block' } ] }, @@ -655,7 +823,17 @@ export const toolbox = { }, { 'kind': 'block', - 'type': 'praxly_return_block' + 'type': 'praxly_return_block', + 'inputs': { + 'EXPRESSION': { + 'block': { + 'type': 'praxly_literal_block', + 'fields': { + 'LITERAL': "value" + } + } + } + } }, { 'kind': 'block', diff --git a/src/tree2blocks.js b/src/tree2blocks.js index 2a5e597..ab8a9e8 100644 --- a/src/tree2blocks.js +++ b/src/tree2blocks.js @@ -1,4 +1,4 @@ -import { NODETYPES, TYPES } from "./common"; +import { NODETYPES, StringFuncs, TYPES } from "./common"; function connectStatements(statements) { for (let i = 0; i < statements.length - 1; i++) { @@ -18,7 +18,7 @@ function connectStatements(statements) { } } else { - console.log("connection failed"); + console.error("connection failed"); } } } @@ -137,47 +137,43 @@ export const tree2blocks = (workspace, node) => { result = workspace.newBlock('praxly_random_block'); } else if (node.name === 'randomInt') { result = workspace.newBlock('praxly_random_int_block'); - const child = tree2blocks(workspace, node?.parameters[0]); + 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?.parameters[0]); + 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?.parameters[0]); + 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?.parameters[0]); + 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?.parameters[0]); + const child1 = tree2blocks(workspace, node?.args[0]); result.getInput('A_MIN').connection.connect(child1?.outputConnection); - - const child2 = tree2blocks(workspace, node?.parameters[1]); + 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?.parameters[0]); + const child1 = tree2blocks(workspace, node?.args[0]); result.getInput('A_MAX').connection.connect(child1?.outputConnection); - - const child2 = tree2blocks(workspace, node?.parameters[1]); + 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?.parameters[0]); + 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?.parameters[0]); + 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?.parameters[0]); + const child = tree2blocks(workspace, node?.args[0]); result.getInput('VALUE').connection.connect(child?.outputConnection); } break; @@ -189,10 +185,9 @@ export const tree2blocks = (workspace, node) => { return tree2blocks(workspace, element); } catch (error) { - console.error('An error occurred: empty statement', error); + console.error('invalid statement', error); return null; } - }); connectStatements(statements); return statements; @@ -209,24 +204,24 @@ export const tree2blocks = (workspace, node) => { case NODETYPES.IF: var result = workspace.newBlock('praxly_if_block'); var condition = tree2blocks(workspace, node?.condition); - var codeblocks = tree2blocks(workspace, node?.statement); + var codeblock = tree2blocks(workspace, node?.codeblock); result.getInput('CONDITION').connection.connect(condition?.outputConnection); - if (codeblocks && codeblocks.length > 0) { - result.getInput('STATEMENT').connection.connect(codeblocks[0]?.previousConnection); + if (codeblock && codeblock.length > 0) { + result.getInput('STATEMENT').connection.connect(codeblock[0]?.previousConnection); } break; case NODETYPES.IF_ELSE: var result = workspace.newBlock('praxly_if_else_block'); var condition = tree2blocks(workspace, node?.condition); - var statements = tree2blocks(workspace, node?.statement); - var alternatives = tree2blocks(workspace, node?.alternative); + var codeblock = tree2blocks(workspace, node?.codeblock); + var alternative = tree2blocks(workspace, node?.alternative); result.getInput('CONDITION').connection.connect(condition?.outputConnection); - if (statements && statements.length > 0) { - result.getInput('STATEMENT').connection.connect(statements[0]?.previousConnection); + if (codeblock && codeblock.length > 0) { + result.getInput('STATEMENT').connection.connect(codeblock[0]?.previousConnection); } - if (alternatives && alternatives.length > 0) { - result.getInput('ALTERNATIVE').connection.connect(alternatives[0]?.previousConnection); + if (alternative && alternative.length > 0) { + result.getInput('ALTERNATIVE').connection.connect(alternative[0]?.previousConnection); } break; @@ -243,11 +238,10 @@ export const tree2blocks = (workspace, node) => { break; case NODETYPES.ASSIGNMENT: + var result = workspace.newBlock('praxly_reassignment_block'); + result.setFieldValue(node.location.name, "VARIABLENAME"); var expression = tree2blocks(workspace, node?.value); - var result = workspace.newBlock('praxly_reassignment_expression_block'); - var location = tree2blocks(workspace, node.location); result.getInput('EXPRESSION').connection.connect(expression?.outputConnection); - result.getInput('LOCATION').connection.connect(location?.outputConnection); break; case NODETYPES.VARDECL: @@ -257,6 +251,12 @@ export const tree2blocks = (workspace, node) => { result.setFieldValue(node.varType, "VARTYPE"); result.setFieldValue(node.name, "VARIABLENAME"); result.getInput('EXPRESSION').connection.connect(expression?.outputConnection); + } else if (node.varType == TYPES.VOID) { + // procedures look like variables until left paren is typed + var result = workspace.newBlock('praxly_procedure_block'); + result.setFieldValue(node.varType, "RETURNTYPE"); + result.setFieldValue(node.name, 'PROCEDURE_NAME'); + result.setFieldValue(node.name, 'END_PROCEDURE_NAME'); } else { var result = workspace.newBlock('praxly_vardecl_block'); result.setFieldValue(node.varType, "VARTYPE"); @@ -267,30 +267,30 @@ export const tree2blocks = (workspace, node) => { case NODETYPES.WHILE: var result = workspace.newBlock('praxly_while_loop_block'); var condition = tree2blocks(workspace, node?.condition); - var codeblocks = tree2blocks(workspace, node?.statement); + var codeblock = tree2blocks(workspace, node?.codeblock); result.getInput('CONDITION').connection.connect(condition?.outputConnection); - if (codeblocks && codeblocks.length > 0) { - result.getInput('STATEMENT').connection.connect(codeblocks[0]?.previousConnection); + if (codeblock && codeblock.length > 0) { + result.getInput('STATEMENT').connection.connect(codeblock[0]?.previousConnection); } break; case NODETYPES.DO_WHILE: var result = workspace.newBlock('praxly_do_while_loop_block'); var condition = tree2blocks(workspace, node?.condition); - var codeblocks = tree2blocks(workspace, node?.statement); + var codeblock = tree2blocks(workspace, node?.codeblock); result.getInput('CONDITION').connection.connect(condition?.outputConnection); - if (codeblocks && codeblocks.length > 0) { - result.getInput('STATEMENT').connection.connect(codeblocks[0]?.previousConnection); + if (codeblock && codeblock.length > 0) { + result.getInput('STATEMENT').connection.connect(codeblock[0]?.previousConnection); } break; case NODETYPES.REPEAT_UNTIL: var result = workspace.newBlock('praxly_repeat_until_loop_block'); var condition = tree2blocks(workspace, node?.condition); - var codeblocks = tree2blocks(workspace, node?.statement); + var codeblock = tree2blocks(workspace, node?.codeblock); result.getInput('CONDITION').connection.connect(condition?.outputConnection); - if (codeblocks && codeblocks.length > 0) { - result.getInput('STATEMENT').connection.connect(codeblocks[0]?.previousConnection); + if (codeblock && codeblock.length > 0) { + result.getInput('STATEMENT').connection.connect(codeblock[0]?.previousConnection); } break; @@ -327,22 +327,60 @@ export const tree2blocks = (workspace, node) => { break; case NODETYPES.SPECIAL_STRING_FUNCCALL: - var result = workspace.newBlock('praxly_StringFunc_block'); - var params = workspace.newBlock('praxly_parameter_block'); - var recipient = tree2blocks(workspace, node.left); - result.setFieldValue(node?.right?.name, 'FUNCTYPE'); - result.getInput('PARAMS').connection.connect(params?.outputConnection); - var argsList = node?.right?.args; - for (var i = 0; i < (argsList?.length ?? 0); i++) { - params.appendValueInput(`PARAM_${i}`); - var argument = tree2blocks(workspace, argsList[i]); - params.getInput(`PARAM_${i}`).connection.connect(argument?.outputConnection); + if (!node.right) { + break; // user still typing (nothing after the dot) + } + const name = node.right.name; + const args = node.right.args; + + // create applicable string method block and connect args + if (name === StringFuncs.CHARAT) { + var result = workspace.newBlock('praxly_charAt_block'); + if (args?.length == 1) { + const index = tree2blocks(workspace, args[0]); + result.getInput('INDEX').connection.connect(index?.outputConnection); + } + } + else if (name === StringFuncs.CONTAINS) { + var result = workspace.newBlock('praxly_contains_block'); + if (args?.length == 1) { + const param = tree2blocks(workspace, args[0]); + result.getInput('PARAM').connection.connect(param?.outputConnection); + } + } + else if (name === StringFuncs.INDEXOF) { + result = workspace.newBlock('praxly_indexOf_block'); + if (args?.length == 1) { + const param = tree2blocks(workspace, args[0]); + result.getInput('PARAM').connection.connect(param?.outputConnection); + } } + else if (name === StringFuncs.LENGTH) { + result = workspace.newBlock('praxly_length_block'); + } + else if (name === StringFuncs.SUBSTRING) { + result = workspace.newBlock('praxly_substring_block'); + if (args?.length == 2) { + const param1 = tree2blocks(workspace, args[0]); + const param2 = tree2blocks(workspace, args[1]); + result.getInput('PARAM1').connection.connect(param1?.outputConnection); + result.getInput('PARAM2').connection.connect(param2?.outputConnection); + } + } + else if (name === StringFuncs.TOLOWERCSE) { + result = workspace.newBlock('praxly_toLowerCase_block'); + } + else if (name === StringFuncs.TOUPPERCASE) { + result = workspace.newBlock('praxly_toUpperCase_block'); + } else { + break; // user still typing or misspelled name + } + + // connect the string on the left of the result block + var recipient = tree2blocks(workspace, node.left); result.getInput("EXPRESSION").connection.connect(recipient.outputConnection); - params.initSvg(); break; - case NODETYPES.FUNCDECL: var returnType = node?.returnType; var argsList = node?.params; @@ -352,9 +390,9 @@ export const tree2blocks = (workspace, node) => { result.setFieldValue(node?.name, 'PROCEDURE_NAME'); result.setFieldValue(node?.name, 'END_PROCEDURE_NAME'); result.getInput('PARAMS').connection.connect(params?.outputConnection); - var contents = tree2blocks(workspace, node?.contents); - if (contents && contents.length > 0) { - result.getInput('CONTENTS').connection.connect(contents[0]?.previousConnection); + var codeblock = tree2blocks(workspace, node?.codeblock); + if (codeblock && codeblock.length > 0) { + result.getInput('CODEBLOCK').connection.connect(codeblock[0]?.previousConnection); } for (var i = 0; i < (argsList?.length ?? 0); i++) { params.appendValueInput(`PARAM_${i}`); @@ -371,43 +409,71 @@ export const tree2blocks = (workspace, node) => { var result = workspace.newBlock('praxly_for_loop_block'); try { var initialization = tree2blocks(workspace, node?.initialization); - if (!initialization || initialization.type !== 'praxly_statement_block') { + 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(); - var initialization = workspace.newBlock('praxly_assignment_expression_block'); + 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.setFieldValue(node?.initialization?.varType, "VARTYPE"); - initialization.setFieldValue(node?.initialization?.name, "VARIABLENAME"); initialization.getInput('EXPRESSION').connection.connect(expression?.outputConnection); initialization.initSvg(); - } else { - var container = initialization; - initialization = initialization.getInputTargetBlock('EXPRESSION'); - } - // var increment = workspace.newBlock('praxly_reassignment_expression_block'); - // var expression2 = tree2blocks(workspace, node?.increment?.value); - // increment.setFieldValue(node?.increment?.name, "VARIABLENAME"); - // increment.getInput('EXPRESSION').connection.connect(expression2?.outputConnection); - // increment.initSvg(); - + // this will always be an expression, so nothing more to do var condition = tree2blocks(workspace, node?.condition); + var increment = tree2blocks(workspace, node?.increment); - var codeblocks = tree2blocks(workspace, node?.statement); + 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(); + } + + // get the for loop body + var codeblock = tree2blocks(workspace, node?.codeblock); + // connect everything together result.getInput('INITIALIZATION').connection.connect(initialization?.outputConnection); - container?.dispose(); + container1?.dispose(); result.getInput('CONDITION').connection.connect(condition?.outputConnection); result.getInput('REASSIGNMENT').connection.connect(increment?.outputConnection); - if (codeblocks && codeblocks.length > 0) { - result.getInput('CODEBLOCK').connection.connect(codeblocks[0]?.previousConnection); + container2?.dispose(); + if (codeblock && codeblock.length > 0) { + result.getInput('CODEBLOCK').connection.connect(codeblock[0]?.previousConnection); } } catch (error) { - console.error('An error occurred: could not generate the nested block', error); - initialization?.dispose(); - increment?.dispose(); // the question marks here helped the for loop block generate when just typing + 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(); } break; @@ -424,8 +490,8 @@ export const tree2blocks = (workspace, node) => { case NODETYPES.ARRAY_REFERENCE_ASSIGNMENT: var result = workspace.newBlock('praxly_array_reference_reassignment_block'); - result.setFieldValue(node.name, "VARIABLENAME"); - var child = tree2blocks(workspace, node?.index); + result.setFieldValue(node.location.name, "VARIABLENAME"); + var child = tree2blocks(workspace, node.location.index); result.getInput('INDEX').connection.connect(child?.outputConnection); var expression = tree2blocks(workspace, node?.value); result.getInput('EXPRESSION').connection.connect(expression?.outputConnection); diff --git a/src/tree2text.js b/src/tree2text.js index 06c4b56..5fd4978 100644 --- a/src/tree2text.js +++ b/src/tree2text.js @@ -2,7 +2,7 @@ import { NODETYPES, TYPES } from "./common"; import { text2tree } from "./text2tree"; export const tree2text = (node, indentation) => { - if (!node.type) { + if (!node?.type) { return; // undefined } @@ -21,7 +21,7 @@ export const tree2text = (node, indentation) => { try { var result = node.name.toString(); if (node.isArray) { - result += `[${tree2text(node.index)}]`; + result += `[${tree2text(node.index, 0)}]`; } return result; } catch (error) { @@ -77,92 +77,92 @@ export const tree2text = (node, indentation) => { } case NODETYPES.ADDITION: - var a_operand = tree2text(node.left, indentation); + var a_operand = tree2text(node.left, 0); var operator = " + "; - var b_operand = tree2text(node.right, node.endIndex, indentation); + var b_operand = tree2text(node.right, 0); return a_operand + operator + b_operand; case NODETYPES.SUBTRACTION: - var a_operand = tree2text(node.left, indentation); + var a_operand = tree2text(node.left, 0); var operator = " - "; - var b_operand = tree2text(node.right, node.endIndex, indentation); + var b_operand = tree2text(node.right, 0); return a_operand + operator + b_operand; case NODETYPES.MULTIPLICATION: - var a_operand = tree2text(node.left, indentation); + var a_operand = tree2text(node.left, 0); var operator = " * "; - var b_operand = tree2text(node.right, node.endIndex, indentation); + var b_operand = tree2text(node.right, 0); return a_operand + operator + b_operand; case NODETYPES.DIVISION: - var a_operand = tree2text(node.left, indentation); + var a_operand = tree2text(node.left, 0); var operator = " / "; - var b_operand = tree2text(node.right, node.endIndex, indentation); + var b_operand = tree2text(node.right, 0); return a_operand + operator + b_operand; case NODETYPES.EXPONENTIATION: - var a_operand = tree2text(node.left, indentation); + var a_operand = tree2text(node.left, 0); var operator = " ^ "; - var b_operand = tree2text(node.right, node.endIndex, indentation); + var b_operand = tree2text(node.right, 0); return a_operand + operator + b_operand; case NODETYPES.MODULUS: - var a_operand = tree2text(node.left, indentation); + var a_operand = tree2text(node.left, 0); var operator = " % "; - var b_operand = tree2text(node.right, node.endIndex, indentation); + var b_operand = tree2text(node.right, 0); return a_operand + operator + b_operand; case NODETYPES.AND: - var a_operand = tree2text(node.left, indentation); + var a_operand = tree2text(node.left, 0); var operator = " and "; - var b_operand = tree2text(node.right, node.endIndex, indentation); + var b_operand = tree2text(node.right, 0); return a_operand + operator + b_operand; case NODETYPES.OR: - var a_operand = tree2text(node.left, indentation); + var a_operand = tree2text(node.left, 0); var operator = " or "; - var b_operand = tree2text(node.right, node.endIndex, indentation); + var b_operand = tree2text(node.right, 0); return a_operand + operator + b_operand; case NODETYPES.EQUALITY: - var a_operand = tree2text(node.left, indentation); + var a_operand = tree2text(node.left, 0); var operator = " == "; - var b_operand = tree2text(node.right, node.endIndex, indentation); + var b_operand = tree2text(node.right, 0); return a_operand + operator + b_operand; case NODETYPES.LESS_THAN_OR_EQUAL: - var a_operand = tree2text(node.left, indentation); + var a_operand = tree2text(node.left, 0); var operator = " ≤ "; - var b_operand = tree2text(node.right, node.endIndex, indentation); + var b_operand = tree2text(node.right, 0); return a_operand + operator + b_operand; case NODETYPES.GREATER_THAN_OR_EQUAL: - var a_operand = tree2text(node.left, indentation); + var a_operand = tree2text(node.left, 0); var operator = " ≥ "; - var b_operand = tree2text(node.right, node.endIndex, indentation); + var b_operand = tree2text(node.right, 0); return a_operand + operator + b_operand; case NODETYPES.GREATER_THAN: - var a_operand = tree2text(node.left, indentation); + var a_operand = tree2text(node.left, 0); var operator = " > "; - var b_operand = tree2text(node.right, node.endIndex, indentation); + var b_operand = tree2text(node.right, 0); return a_operand + operator + b_operand; case NODETYPES.LESS_THAN: - var a_operand = tree2text(node.left, indentation); + var a_operand = tree2text(node.left, 0); var operator = " < "; - var b_operand = tree2text(node.right, node.endIndex, indentation); + var b_operand = tree2text(node.right, 0); return a_operand + operator + b_operand; case NODETYPES.INEQUALITY: - var a_operand = tree2text(node.left, indentation); + var a_operand = tree2text(node.left, 0); var operator = " ≠ "; - var b_operand = tree2text(node.right, node.endIndex, indentation); + var b_operand = tree2text(node.right, 0); return a_operand + operator + b_operand; case NODETYPES.PRINT: var result = ' '.repeat(indentation) + "print "; - var expression = tree2text(node.value, node.endIndex, indentation); + var expression = tree2text(node.value, 0); if (node.comment) { expression += ' // ' + node.comment; } @@ -170,7 +170,7 @@ export const tree2text = (node, indentation) => { return result + expression; case NODETYPES.ASSOCIATION: - return `(${tree2text(node.expression, node.endIndex, indentation)})`; + return `(${tree2text(node.expression, 0)})`; case NODETYPES.BUILTIN_FUNCTION_CALL: { if (node.name === 'input') { @@ -178,40 +178,40 @@ export const tree2text = (node, indentation) => { } else if (node.name === 'random') { return "random()"; } else if (node.name === 'randomInt') { - const max = tree2text(node.parameters[0], indentation); + const max = tree2text(node.args[0], 0); return `randomInt(${max})`; } else if (node.name === 'randomSeed') { - const seed = tree2text(node.parameters[0], indentation); + const seed = tree2text(node.args[0], 0); return `randomSeed(${seed})`; } else if (node.name === 'int') { - const conversion = tree2text(node.parameters[0], indentation); + const conversion = tree2text(node.args[0], 0); return `int(${conversion})`; } else if (node.name === 'float') { - const conversion = tree2text(node.parameters[0], indentation); + const conversion = tree2text(node.args[0], 0); return `float(${conversion})`; } else if (node.name === 'min') { - const a_value = tree2text(node.parameters[0], indentation); - const b_value = tree2text(node.parameters[1], indentation); + const a_value = tree2text(node.args[0], 0); + const b_value = tree2text(node.args[1], 0); return "min(" + a_value + ", " + b_value + ")"; } else if (node.name === 'max') { - const a_value = tree2text(node.parameters[0], indentation); - const b_value = tree2text(node.parameters[1], indentation); + const a_value = tree2text(node.args[0], 0); + const b_value = tree2text(node.args[1], 0); return "max(" + a_value + ", " + b_value + ")"; } else if (node.name === 'abs') { - const value = tree2text(node.parameters[0], indentation); + const value = tree2text(node.args[0], 0); return `abs(${value})`; } else if (node.name === 'log') { - const value = tree2text(node.parameters[0], indentation); + const value = tree2text(node.args[0], 0); return `log(${value})`; } else if (node.name = 'sqrt') { - const value = tree2text(node.parameters[0], indentation); + const value = tree2text(node.args[0], 0); return `sqrt(${value})`; } } case NODETYPES.RETURN: var result = ' '.repeat(indentation) + "return "; - var expression = tree2text(node.value, node.endIndex, indentation) + '\n'; + var expression = tree2text(node.value, 0) + '\n'; return result + expression; case NODETYPES.PROGRAM: @@ -219,7 +219,7 @@ export const tree2text = (node, indentation) => { case NODETYPES.STATEMENT: var result = ' '.repeat(indentation); - var expression = tree2text(node.value, node.endIndex, indentation) + '\n'; + var expression = tree2text(node.value, 0) + '\n'; return result + expression; case NODETYPES.CODEBLOCK: @@ -227,8 +227,7 @@ export const tree2text = (node, indentation) => { try { return tree2text(element, indentation); } catch (error) { - console.error('An error occurred: empty statement', error); - return null; + return " "; } }); return statements.join(''); @@ -236,68 +235,69 @@ export const tree2text = (node, indentation) => { case NODETYPES.IF: var result = ' '.repeat(indentation) + "if ("; var condition = tree2text(node.condition, 0) + ")\n"; - var contents = tree2text(node.statement, indentation + 1) + + var codeblock = tree2text(node.codeblock, indentation + 1) + ' '.repeat(indentation) + 'end if\n'; - return result + condition + contents; + return result + condition + codeblock; case NODETYPES.IF_ELSE: var result = ' '.repeat(indentation) + "if ("; - var condition = tree2text(node.condition, indentation) + ")\n"; - var contents = tree2text(node.statement, indentation + 1); + var condition = tree2text(node.condition, 0) + ")\n"; + var codeblock = tree2text(node.codeblock, indentation + 1); var alternative = ' '.repeat(indentation) + '\else\n' + tree2text(node.alternative, indentation + 1) + ' '.repeat(indentation) + 'end if\n'; - return result + condition + contents + alternative; + return result + condition + codeblock + alternative; + // Note: reassignment (either a statement or in a for loop) case NODETYPES.ASSIGNMENT: - var varname = tree2text(node.location, node.endIndex, indentation); + var varname = tree2text(node.location, indentation); var operator = ' ← '; - var expression = tree2text(node.value, node.endIndex, indentation); - return varname + operator + expression; + var expression = tree2text(node.value, 0); + return ' '.repeat(indentation) + varname + operator + expression + '\n'; + // 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, node.endIndex, indentation); + var expression = tree2text(node.value, 0); return ' '.repeat(indentation) + varname + operator + expression + '\n'; } else { return ' '.repeat(indentation) + varname + '\n'; } } catch (error) { - console.error(error); return " "; } case NODETYPES.WHILE: var result = ' '.repeat(indentation) + "while"; - var condition = " (" + tree2text(node.condition, indentation) + ")\n"; - var contents = tree2text(node.statement, indentation + 1) + + var condition = " (" + tree2text(node.condition, 0) + ")\n"; + var codeblock = tree2text(node.codeblock, indentation + 1) + ' '.repeat(indentation) + 'end while\n'; - return result + condition + contents; + return result + condition + codeblock; case NODETYPES.DO_WHILE: var result = ' '.repeat(indentation) + 'do\n'; - var contents = ' '.repeat(indentation) + tree2text(node.statement, indentation + 1); - var condition = ' '.repeat(indentation) + "while (" + tree2text(node.condition, indentation) + ")\n"; - return result + contents + condition; + var codeblock = tree2text(node.codeblock, indentation + 1); + var condition = ' '.repeat(indentation) + "while (" + tree2text(node.condition, 0) + ")\n"; + return result + codeblock + condition; case NODETYPES.REPEAT_UNTIL: var result = ' '.repeat(indentation) + 'repeat\n'; - var contents = ' '.repeat(indentation) + tree2text(node.statement, indentation + 1); - var condition = ' '.repeat(indentation) + "until (" + tree2text(node.condition, indentation) + ")\n"; - return result + contents + condition; + var codeblock = tree2text(node.codeblock, indentation + 1); + var condition = ' '.repeat(indentation) + "until (" + tree2text(node.condition, 0) + ")\n"; + return result + codeblock + condition; case NODETYPES.NOT: var result = "not "; - var expression = tree2text(node.value, node.endIndex, indentation); + var expression = tree2text(node.value, 0); return result + expression; case NODETYPES.NEGATE: var result = "-"; - var expression = tree2text(node.value, node.endIndex, indentation); + var expression = tree2text(node.value, 0); return result + expression; case NODETYPES.FOR: @@ -306,10 +306,10 @@ export const tree2text = (node, indentation) => { initialization = initialization.replace("\n", "") + '; '; var condition = tree2text(node.condition, 0) + '; '; var increment = tree2text(node.increment, 0); - increment = increment + ")\n"; - var contents = ' '.repeat(indentation) + tree2text(node.statement, indentation + 1) + + increment = increment.replace("\n", "") + ')\n'; + var codeblock = tree2text(node.codeblock, indentation + 1) + ' '.repeat(indentation) + 'end for\n'; - return result + initialization + condition + increment + contents; + return result + initialization + condition + increment + codeblock; case NODETYPES.FUNCDECL: var vartype = node.returnType.toString(); @@ -323,8 +323,8 @@ export const tree2text = (node, indentation) => { } result += ')'; result += '\n'; - var contents = ' '.repeat(indentation) + tree2text(node.contents, indentation + 1); - result += contents; + var codeblock = tree2text(node.codeblock, indentation + 1); + result += codeblock; result += ' '.repeat(indentation) + `end ${node.name}\n`; return result; @@ -333,7 +333,7 @@ export const tree2text = (node, indentation) => { var argsList = node.args; if (argsList !== null && argsList.length > 0) { argsList.forEach(element => { - result += tree2text(element, indentation) + ', '; + result += tree2text(element, 0) + ', '; }); result = result.slice(0, result.length - 2); } @@ -341,12 +341,12 @@ export const tree2text = (node, indentation) => { return result; case NODETYPES.SPECIAL_STRING_FUNCCALL: - var result = ' '.repeat(indentation) + tree2text(node.left, indentation) + '.' + node.right.name; + var result = ' '.repeat(indentation) + tree2text(node.left, 0) + '.' + node.right.name; result += '('; var argsList = node.right.args; if (argsList !== null && argsList.length !== 0) { argsList.forEach(element => { - result += tree2text(element, indentation) + ', '; + result += tree2text(element, 0) + ', '; }); result = result.slice(0, result.length - 2) } @@ -359,7 +359,7 @@ export const tree2text = (node, indentation) => { var argsList = node.params; if (argsList !== null && argsList.length > 0) { argsList.forEach(element => { - result += tree2text(element, indentation) + ', '; + result += tree2text(element, 0) + ', '; }); result = result.slice(0, result.length - 2); } @@ -368,7 +368,7 @@ export const tree2text = (node, indentation) => { case NODETYPES.ARRAY_REFERENCE: result = node.name + '['; - var expression = tree2text(node.index, node.endIndex, indentation) + ']'; + var expression = tree2text(node.index, 0) + ']'; return result + expression; case NODETYPES.ARRAY_ASSIGNMENT: @@ -379,30 +379,29 @@ export const tree2text = (node, indentation) => { var argsList = node.value.params; if (argsList !== null && argsList.length > 0) { argsList.forEach(element => { - result += tree2text(element, indentation) + ', '; + result += tree2text(element, 0) + ', '; }); result = result.slice(0, result.length - 2); } result += '}\n'; return ' '.repeat(indentation) + varname + operator + result; } catch (error) { - console.error(error); - return "assignment for arrays broke"; + return " "; } case NODETYPES.ARRAY_REFERENCE_ASSIGNMENT: try { - var index = tree2text(node.index, node.endIndex, indentation) + ']'; + var index = tree2text(node.index, 0) + ']'; var varname = node.name.toString() + '[' + index; var operator = ' ← '; - var expression = tree2text(node.value, node.endIndex, indentation) + '\n'; + var expression = tree2text(node.value, 0) + '\n'; return ' '.repeat(indentation) + varname + operator + expression; } catch (error) { return " "; } default: - console.warn("Unknown node.type:" + node.type); - break; + console.error("unknown node type " + node.type); + return " "; } } diff --git a/test/basics.csv b/test/basics.csv index a4874a6..1762a1d 100644 --- a/test/basics.csv +++ b/test/basics.csv @@ -30,14 +30,14 @@ a 7 8 nine", -Assign float to int,int x ← 1.2,,,"error occurred on line 1: - incompatible types: possible lossy conversion from double to int" +Assign float to int,int x ← 1.2,,,"runtime error occurred on line 1: +incompatible types: possible lossy conversion from double to int" Assign int to float,"float y = 1 print y",,1.0, -Assign str to int,String s = 1,,,"error occurred on line 1: - incompatible types: int cannot be converted to String" -Assign int to str,"int i = ""0""",,,"error occurred on line 1: - incompatible types: String cannot be converted to int" +Assign str to int,String s = 1,,,"runtime error occurred on line 1: +incompatible types: int cannot be converted to String" +Assign int to str,"int i = ""0""",,,"runtime error occurred on line 1: +incompatible types: String cannot be converted to int" Arithmetic ops,"int x ← 2 int y ← 3 print x + y @@ -94,3 +94,24 @@ Placeholder,"print ""Before"" /* missing code */ print ""After""",,"Before After", +Print quotes,"int i ← 5 +double d ← 6 +char c ← '$' +String s ← ""Hi!"" +int[] a1 ← {1, 2, 3} +char[] a2 ← {'a', 'b', 'c'} +String[] a3 ← {""Hi"", ""Mom""} + +print i +print d +print c +print s +print a1 +print a2 +print a3",,"5 +6.0 +$ +Hi! +{1, 2, 3} +{'a', 'b', 'c'} +{""Hi"", ""Mom""}", diff --git a/test/canvas.csv b/test/canvas.csv index 33a3317..64b9c7f 100644 --- a/test/canvas.csv +++ b/test/canvas.csv @@ -154,25 +154,24 @@ if( secret_number != guess ) end if",14,"Guess a number! You did not get it...", -conditionals/5.4 if-statements.md random guess,"// get the random number -int r ⟵ random +conditionals/5.4 if-statements.md random guess,"// get a random integer between 0 and 99 +int n ← randomInt(100); -// scale the number so it is between 1 and 100 -int nf ⟵ float (r) / 6.0 * 100.0; -int n ⟵ int (nf); +// adjust integer to be between 1 and 100 +n ← n + 1; -// get the guess from the user; -int guess ⟵ int(input()) +// get the guess from the user +int guess ← int(input()); // compare n to guess -if(n == guess) +if (n == guess) print 'y'; end if -if(n != guess) +if (n != guess) print 'n'; -end if -",,TODO random, +end if",123," +n", conditionals/5.5 more-ifs.md if guess 5,"print ""Guess a number between 1 and 10""; int n ⟵ int(input()) @@ -184,32 +183,36 @@ end if",5,"Guess a number between 1 and 10 You got it!", conditionals/5.5 more-ifs.md if else-if else,"print ""Guess a number between 1 and 10""; -int n ⟵ int(input()) +int n ← int(input()); -if(n == 5) +if (n == 5) print ""Correct!""; -else if (n == 4 or n == 6) - print ""You're close!""; else - print ""Incorrect!""; + if (n == 4 or n == 6) + print ""You're close!""; + else + print ""Incorrect!""; + end if end if",4,"Guess a number between 1 and 10 You're close!", conditionals/5.5 more-ifs.md nested if-else,"print ""Guess a number between 1 and 10""; -int n ⟵ int(input()) +int n ← int(input()); if (n < 5) print ""Too low!""; if (n == 4) print ""Pretty close, though.""; end if -else if (n > 5) - print ""Too high!""; - if(n == 6) - print ""Pretty close, though.""; - end if else - print ""You got it!""; + if (n > 5) + print ""Too high!""; + if (n == 6) + print ""Pretty close, though.""; + end if + else + print ""You got it!""; + end if end if",6,"Guess a number between 1 and 10 Too high! @@ -218,8 +221,8 @@ conditionals/5.6 loops.md infinite loop,"int count ⟵ 0; while ( count < 5) // print ""Looping!""; -end while",,,"error occurred on line 3: - This is probably an infinite loop." +end while",,,"runtime error occurred on line 3: +This is probably an infinite loop." conditionals/5.6 loops.md while loop 5,"int count ⟵ 0; while (count < 5 ) @@ -249,13 +252,16 @@ conditionals/5.6 loops.md do-while false,"int count ⟵ 2; do print ""Loop!""; while ( count < 2 )",,Loop!, -conditionals/5.7 compound-boolean.md game,"print ""Welcome to the game!""; -int answer ⟵ random; +conditionals/5.7 compound-boolean.md game,"randomSeed(0) + +print ""Welcome to the game!""; +int answer ⟵ randomInt(6) + 1; int guesses ⟵ 0; +int n; while (guesses <= 3) print ""Guess a number between 1 and 6""; - int n ⟵ int(input()) + n ⟵ int(input()) if(n == answer) print ""You got it!""; @@ -273,7 +279,15 @@ if (n == answer) end if else print ""You lost!""; -end if",,TODO random, +end if","1 +2","Welcome to the game! +Guess a number between 1 and 6 + +That's not it! +Guess a number between 1 and 6 + +You got it! +You won!", conditionals/5.7 compound-boolean.md license,"print ""How old are you?""; int age = int(input()) @@ -328,13 +342,13 @@ Hello! How are you?", procedures/2-procedure-basics.md global vs local,"// procedure definition void getRandom() - int r ⟵ random // local variable + double r ⟵ random(); // local variable end getRandom // main program -int r ⟵ 10; // global variable -getRandom(); // call the procedure -print r;",,TODO random, +double r ⟵ 10; // global variable +getRandom(); // call the procedure +print r;",,10.0, procedures/2-procedure-basics.md no global vars,"// procedure definition void printSum() int a ⟵ 10; @@ -344,8 +358,8 @@ end printSum // main program printSum(); -print a * b; // ERROR!",,25,"error occurred on line 10: - Variable a does not exist." +print a * b; // ERROR!",,25,"runtime error occurred on line 10: +Variable a does not exist." procedures/3-params-and-return.md capital of VA,"print ""What is the capital of Virginia?""; print ""1: Washington 2: Richmond 3: Norfolk""; int ans ⟵ int(input()) @@ -534,7 +548,7 @@ print index",,2, search-sort/4-search-algorithms.md binary search,"int binarySearch( String [] arr, int arrLength, String target ) int lower ← 0 // lower and upper indices define range int upper ← arrLength - 1 - while ( lower < upper ) + while ( lower <= upper ) int middle ← (lower+upper) / 2 // middle index in range String current ← arr[middle] if ( target > current ) // adjust range for next iteration @@ -574,6 +588,7 @@ search-sort/5-sorting-algorithms.md selection sort,"void selectionSort( String [ for ( int k ← j + 1; k < length; k ← k + 1 ) // find minimum if (arr[k] < arr[min]) min ← k + end if end for String temp ← arr[j] // swap arr[j] ← arr[min] diff --git a/test/colors.py b/test/colors.py new file mode 100644 index 0000000..7e7e239 --- /dev/null +++ b/test/colors.py @@ -0,0 +1,26 @@ +import matplotlib.pyplot as plt + +# Define the darker pastel colors +darker_colors = { + "Darker Pastel Violet": "#9966CC", + "Darker Pastel Blue": "#6699CC", + "Darker Pastel Green": "#66B266", + "Darker Pastel Yellow": "#FFCC66", + "Darker Pastel Orange": "#FF9966", + "Darker Pastel Red": "#FF6666", +} + +# Create a figure and axis +fig, ax = plt.subplots(figsize=(8, 6)) + +# Plot each darker color as a rectangle with white text on top +for i, (color_name, color_hex) in enumerate(darker_colors.items()): + ax.add_patch(plt.Rectangle((0, i), 1, 1, color=color_hex)) + ax.text(0.5, i + 0.5, color_name, ha='center', va='center', fontsize=14, color='white') + +# Remove axes +ax.set_xlim(0, 1) +ax.set_ylim(0, len(darker_colors)) +ax.axis('off') + +plt.show() diff --git a/test/run_csv.py b/test/run_csv.py index 4f157ae..258125b 100644 --- a/test/run_csv.py +++ b/test/run_csv.py @@ -22,7 +22,7 @@ WAIT = 3 # How long to sleep before performing actions. -PAUSE = 0.2 +PAUSE = 0.25 def main(csv_name, html_name): @@ -65,17 +65,18 @@ def main(csv_name, html_name): expect_out = row[3].rstrip().replace("\r", "") expect_err = row[4].rstrip().replace("\r", "") + time.sleep(PAUSE) print(f"Test {test_id}: {name}...", end="", flush=True) if expect_out.startswith("TODO"): print(todo_msg) continue # reset Praxly, paste code, and run - time.sleep(PAUSE) reset.click() yes.click() driver.execute_script(f'ace.edit("aceCode").setValue(`{code}`);') editor.click() + time.sleep(PAUSE) play.click() # simulate each line of user input diff --git a/test/studycomp.csv b/test/studycomp.csv index 288798f..e6681b6 100644 --- a/test/studycomp.csv +++ b/test/studycomp.csv @@ -125,8 +125,7 @@ print ( age > 10 ) and ( height > 36 ) print not ( ( age ≤ 10 ) or ( height ≤ 36 ) )",,"true true", Q14,"String value ← ""Applesauce"" -value ← value.substring ( 0, 1 ).toLowerCase () + - value.substring ( 1, value.length () ).toUpperCase () +value ← value.substring ( 0, 1 ).toLowerCase () + value.substring ( 1, value.length () ).toUpperCase () print value",,aPPLESAUCE, Q15,"int mystery ( int n ) int temp ← 0 diff --git a/test/variables.csv b/test/variables.csv index 921f576..ae61b15 100644 --- a/test/variables.csv +++ b/test/variables.csv @@ -4,15 +4,15 @@ double u ← 6 short t ← 12.2 float f ← 3.75 boolean b ← true -char c ← a",,,"error occurred on line 3: - incompatible types: possible lossy conversion from double to short" +char c ← a",,,"runtime error occurred on line 3: +incompatible types: possible lossy conversion from double to short" Missing quotes,"int n ← 12 double u ← 6 //short t ← 12.2 float f ← 3.75 boolean b ← true -char c ← a",,,"error occurred on line 6: - Variable a does not exist." +char c ← a",,,"runtime error occurred on line 6: +Variable a does not exist." Correct vars,"int n ← 12 double u ← 6 //short t ← 12.2 @@ -43,8 +43,8 @@ print f1 / f2",,"0 0.5", Redeclare,"double a = 3.5 * 2.1 double a = 3.5 * 2.1 -print a",,,"error occurred on line 2: - variable a has already been declared in this scope." +print a",,,"runtime error occurred on line 2: +variable a has already been declared in this scope." Number arrays,"int[] a ⟵ {5, 9, 2, 10, 15}; print a double[] b ⟵ {5, 9, 2, 10, 15}; @@ -59,5 +59,5 @@ end printSum // main program printSum(); -print a * b; // ERROR!",,25,"error occurred on line 10: - Variable a does not exist." +print a * b; // ERROR!",,25,"runtime error occurred on line 10: +Variable a does not exist."