diff --git a/src/js/index.js b/src/js/index.js index b3bd943..7431797 100644 --- a/src/js/index.js +++ b/src/js/index.js @@ -1,57 +1,49 @@ import {scan, token_types} from './scanner'; import {parse} from './parser'; -import {add_vector_arrow_to_svg, remove_child, update_vector_arrow} from "./svg_functions"; +import { + add_vector_arrow, + add_vector_arrow_to_svg, + remove_vector_arrow, + update_label_text, + update_vector_arrow +} from "./svg_functions"; -export let vectors = []; // collection of added vectors // maybe move to console.js -const state = {}; +const state = {}; // binding -> value +const bindings = {}; // binding -> {name:binding_name, evaluated: evaluated_lazy_value, previous: previous_value} +const references = {}; const command_input_element = document.getElementById('command_input'); const command_history_element = document.getElementById('command_history'); command_input_element.value = ''; let command_history = ['']; let command_history_index = 0; -let vectors_index_sequence = 0; +let index_sequence = 0; -export const remove_vector = function (vector_or_index) { - let index; - if (vector_or_index.is_vector) { - for (let i = 0; i < vectors.length; i++) { - if (vectors[i].id === vector_or_index.id) { - index = i; - break; - } - } - } else { - index = vector_or_index; - } +const hide = function (vector) { + remove_vector_arrow(vector); + return {description: `vector@${vector.id} is hidden`}; +} - if (!vectors[index]) { - throw {message: `vector@${index} not found`}; - } +const label = function (vector, text) { + vector.label_text = text; + update_label_text(vector.id, text); + return `label set to ${text} on vector@${vector.id}`; +} - vectors.splice(index, 1); - remove_child(document.getElementById('vectors'), index.toString()); - return {description: `vector@${index} removed`}; +const show = function (vector) { + add_vector_arrow_to_svg(vector); + return {description: `vector@${vector.id} is visible`}; } export const update_lazy_objects = function () { - let lazy_objects = Object.values(state).filter(e => Object.prototype.hasOwnProperty.apply(e, ['lazy_expression'])); - lazy_objects.forEach(object => { - let value = visit_expression(object.lazy_expression); - let existing_value = state[object.binding]; - if (existing_value) { - update_vector_arrow(existing_value.id, value); + Object.values(bindings).forEach(binding => { + if (state[binding.name].lazy_expression) { + let value = visit(state[binding.name].lazy_expression); + let existing_value = bindings[binding.name].evaluated; + if (existing_value) { + update_vector_arrow(existing_value.id, value); + bindings[binding.name].evaluated = value; + } } - state[object.binding].x0 = value.x0; - state[object.binding].y0 = value.y0; - state[object.binding].x = value.x; - state[object.binding].y = value.y; - state[object.binding].id = value.id; - let description = state[object.binding].description; - if (!description) { - description = state[object.binding]; - } - - return {description: object.binding + ':' + description}; }); } @@ -103,17 +95,27 @@ const handle_enter = function () { let statement = parse(tokens); let value; try { - value = visit_expression(statement); + value = visit(statement); + let binding; + if (value.is_binding) { // if it's declaration work with the initializer + binding = value.name; // but we also need the name of the bound variable + value = state[binding]; + } + while (value.lazy_expression) { + value = value.get(); + } + if (binding) { + bindings[binding].evaluated = value; // store evaluation result + } if (value.is_visual) { - value.label = value.binding; if (value.is_vector) { - if (value.previous && value.previous.is_visual) { + if (binding && bindings[binding].previous && bindings[binding].previous.is_visual) { update_vector_arrow(value.previous.id, value); } else { if (value.is_new) { + value.label_text = binding ? binding : ""; value.is_new = false; - vectors.push(value); - add_vector_arrow_to_svg(value); + add_vector_arrow(value); } } } @@ -124,43 +126,36 @@ const handle_enter = function () { } catch (e) { value = e.message; } - command_history_element.innerText += value + "\n"; + command_history_element.innerText += value.toString() + "\n"; command_history.push(command); command_history_element.scrollTo(0, command_history_element.scrollHeight); } } } -const visit_expression = function (expr) { +const visit = function (expr) { switch (expr.type) { case 'declaration': { - let value = visit_expression(expr.initializer); - if (!value.is_visual) { // if it's a primitive value, - value = { // wrap it into a object that returns the value - _value: value, // references the original value - toString: function () { - return this._value; - }, - get: function () { - return this._value; - }, - is_visual: false + let value = visit(expr.initializer); + let binding_name = expr.var_name.value; + if (bindings[binding_name]) { // do reassignment + bindings[binding_name].previous = state[binding_name]; // remember previous value, to remove it from the visualisation + } else { + bindings[binding_name] = { + is_binding: true, + name: binding_name, + previous: null, + evaluated: null }; } - value.binding = expr.var_name.value; // store the variable name with it, to handle reassignment. - if (state[value.binding]) { // do reassignment - value.previous = state[value.binding]; // remember previous value, to remove it from the visualisation - } - state[value.binding] = value; // assign new value to binding + state[binding_name] = value; // assign new value to binding - update_lazy_objects(); // reevaluate any lazy expressions - - return value; + return bindings[binding_name]; } case 'group': // expression within parentheses - return visit_expression(expr.expression); + return visit(expr.expression); case 'unary': { - let right_operand = visit_expression(expr.right); + let right_operand = visit(expr.right); if (expr.operator === token_types.MINUS) { return -right_operand; //TODO create negate function (because now it only works for numbers) } else if (expr.operator === token_types.NOT) { @@ -170,50 +165,63 @@ const visit_expression = function (expr) { } } case 'binary': { - let left = visit_expression(expr.left); - let right = visit_expression(expr.right); switch (expr.operator) { case token_types.MINUS: - return subtract(left, right); + return subtract(visit(expr.left), visit(expr.right)); case token_types.PLUS: - return addition(left, right); + return addition(visit(expr.left), visit(expr.right)); case token_types.STAR: - return multiplication(left, right); + return multiplication(visit(expr.left), visit(expr.right)); case token_types.SLASH: - return left / right; + return visit(expr.left) / visit(expr.right); case token_types.DOT: - return method_call(left, expr.right); + return method_call(visit(expr.left), expr.right); // right is not evaluated. It's the method name + // could also be evaluated to itself, BUT it's of type call which would invoke a function (see below) } throw {message: 'illegal binary operator'}; } case 'identifier': { if (state[expr.name]) { - let object = state[expr.name]; - let get = object.get(); - return get; + let value = state[expr.name]; + while (value.lazy_expression) { + value = value.get(); + } + return value; } else { break; } } - case 'literal': - return expr.value; + case 'literal': { + let value = expr.value; + while (value.lazy_expression) { + value = value.get(); + } + return value; + } case 'call': return function_call(expr.name, expr.arguments); case 'lazy': { - let r = visit_expression(expr.value); - r.lazy_expression = expr.value; - return r; + let expression = expr.expression; + let parsed_expression = expr.value; + return { + lazy_expression: parsed_expression, + toString: function () { + return `"${expression}"`; + }, + get: function () { + return visit(parsed_expression); + } + }; + } + case 'reference': { + return references[expr.name]; } } } const function_call = function (function_name, argument_exprs) { - let arguments_list = []; - for (let i = 0; i < argument_exprs.length; i++) { - arguments_list.push(visit_expression(argument_exprs[i])); - } if (Object.prototype.hasOwnProperty.apply(functions, [function_name])) { - return functions[function_name](arguments_list); + return functions[function_name](resolve_arguments(argument_exprs)); } else { let arg_list = ''; for (let i = 0; i < argument_exprs.length; i++) { @@ -226,23 +234,23 @@ const function_call = function (function_name, argument_exprs) { } } -const method_call = function (object_wrapper, method_or_property) { - if (object_wrapper) { +const method_call = function (object, method_or_property) { + if (object) { if (method_or_property.type === 'call') { // method - if (typeof object_wrapper[method_or_property.name] !== 'function') { - throw {message: `method ${method_or_property.name} not found on ${object_wrapper.type}`}; + if (typeof object[method_or_property.name] !== 'function') { + throw {message: `method '${method_or_property.name}' not found on ${object.type}`}; } - return object_wrapper[method_or_property.name].apply(object_wrapper, method_or_property.arguments); + return object[method_or_property.name].apply(object, resolve_arguments(method_or_property.arguments)); } else { // property - if (!Object.prototype.hasOwnProperty.call(object_wrapper, [method_or_property.name])) { - throw {message: `property ${method_or_property.name} not found on ${object_wrapper.type}`}; + if (!Object.prototype.hasOwnProperty.call(object, [method_or_property.name])) { + throw {message: `property '${method_or_property.name}' not found on ${object.type}`}; } - return object_wrapper[method_or_property.name]; + return object[method_or_property.name]; } } else { - throw {message: `not found: ${object_wrapper}`}; + throw {message: `not found: ${object}`}; } } @@ -255,12 +263,15 @@ const functions = { return create_vector({x0: args[0], y0: args[1], x: args[2], y: args[3]}); } }, - remove: (args) => { - if (Object.prototype.hasOwnProperty.call(args[0], ['binding'])) { - delete state[args[0].binding]; - } - return remove_vector(args[0]); + hide: (args) => { + return hide(args[0]); }, + label: (args) =>{ + return label(args[0], args[1]); + }, + show: (args) =>{ + return show(args[0]); + } } const help = function () { @@ -317,17 +328,31 @@ const subtract = function (left, right) { } export const create_vector = function (vector) { //rename to create_vector - vector.id = vectors_index_sequence++; + vector.id = index_sequence++; vector.is_visual = true; - vector.is_vector = true; + vector.is_vector = true; // for comparison + vector.type = 'vector'; // for showing type to user vector.is_new = true; vector.toString = function () { return `vector@${this.id}{x0:${vector.x0},y0:${vector.y0} x:${vector.x},y:${vector.y}}`; }; - vector.get = function () { - return vector; - } + references["@" + vector.id] = vector; + vector.hide = function () { + return hide(this); + }; + vector.label = function (text) { + return label(this, text); + }; + vector.show = function () { + return show(this); + }; return vector; } - +const resolve_arguments = function(argument_exprs) { + let arguments_list = []; + for (let i = 0; i < argument_exprs.length; i++) { + arguments_list.push(visit(argument_exprs[i])); + } + return arguments_list; +} \ No newline at end of file diff --git a/src/js/parser.js b/src/js/parser.js index dd13b27..1299a72 100644 --- a/src/js/parser.js +++ b/src/js/parser.js @@ -1,5 +1,4 @@ -import {token_types} from './scanner'; -import {scan} from './scanner'; +import {scan, token_types} from './scanner'; export const parse = function (tokens) { let token_index = 0; @@ -82,7 +81,7 @@ export const parse = function (tokens) { function call() { let expr = primary(); - for (;;){ + for (; ;) { if (match([token_types.LEFT_PAREN])) { expr = finish_call(expr.name); } else { @@ -112,20 +111,38 @@ export const parse = function (tokens) { if (match([token_types.NUMERIC, token_types.STRING])) { return {type: 'literal', value: previous_token().value, value_type: previous_token().type}; } else if (match([token_types.LAZY])) { - let tokens = scan(previous_token().expression); - let expression = parse(tokens); - return {type: 'lazy', value: expression}; + let expression = previous_token().expression; + let tokens = scan(expression); + let parsed_expression = parse(tokens); + return { + type: 'lazy', + expression: expression, + value: parsed_expression + }; } else if (match([token_types.LEFT_PAREN])) { let expr = expression(); if (expr && match([token_types.RIGHT_PAREN])) { - return {type: 'group', expression: expr}; + return { + type: 'group', + expression: expr + }; } else { throw {message: 'expected expression or )'}; } } else if (check(token_types.IDENTIFIER, token_index)) { - let identifier = {type: 'identifier', name: current_token().value}; + let identifier = { + type: 'identifier', + name: current_token().value + }; advance(); return identifier; + } else if (check(token_types.AT, token_index)) { + let result = { + type: 'reference', + name: current_token().value + }; + advance(); + return result; } } diff --git a/src/js/scanner.js b/src/js/scanner.js index fcf7b72..419af5d 100644 --- a/src/js/scanner.js +++ b/src/js/scanner.js @@ -69,6 +69,11 @@ export const scan = function (command) { return string(); case '"': return lazy_expression(); + case '@': { + let reference = Object.assign({}, token_types.AT); + reference.value = parse_reference(); + return reference; + } } if (is_digit(next_char)) { let token = Object.assign({}, token_types.NUMERIC); @@ -118,6 +123,13 @@ export const scan = function (command) { return is_digit(char) || char === '.'; // no scientific notation for now } + function parse_reference() { + while (current_char() === '@' || is_digit(current_char())) { + advance(); + } + return command.substring(word_start_index, current_index); + } + function parse_number() { while (is_part_of_number(current_char())) { advance(); @@ -192,5 +204,6 @@ export const token_types = { NUMERIC: {type: 'number', value: undefined}, IDENTIFIER: {type: 'identifier', value: undefined}, STRING: {type: 'string', value: undefined}, - LAZY: {type: 'lazy', expression: undefined, parsed_expression:undefined} + LAZY: {type: 'lazy', expression: undefined, parsed_expression: undefined}, + AT: {type: 'reference', value: undefined} }; diff --git a/src/js/svg_functions.js b/src/js/svg_functions.js index e1d5bc7..ccbbd8a 100644 --- a/src/js/svg_functions.js +++ b/src/js/svg_functions.js @@ -1,5 +1,14 @@ -import {update_lazy_objects, vectors} from "./index"; +import {update_lazy_objects} from "./index"; +let vectors_by_id = {}; +const SVG_NS = 'http://www.w3.org/2000/svg'; // program needs these to create svg elements +let grid_size = 100; // this is the nr of pixels for the basis vector (1,0) (0,1) +let half_grid_size = grid_size >> 1; // used to position the grid lines + +let moving_vector; // user can move vector arrows. when moving, this refers to the arrow +let width = window.innerWidth, height = window.innerHeight; +let origin_x = Math.floor((width / grid_size) / 2) * grid_size + half_grid_size, + origin_y = Math.floor((height / grid_size) / 2) * grid_size + half_grid_size; /** * Creates an svg element @@ -15,7 +24,7 @@ const create_svg_element = function (element_type) { * @param element * @param child_id id to remove */ -export const remove_child = function (element, child_id) { +const remove_child = function (element, child_id) { let node = element.firstChild; while (node && child_id !== node.id) { node = node.nextSibling; @@ -92,6 +101,12 @@ const create_arrow = function (id, x0, y0, x1, y1, css_class) { return path; } +export const remove_vector_arrow = function (vector) { + delete vectors_by_id[vector.id]; + remove_child(document.getElementById('vectors'), vector.id.toString()); + remove_child(document.getElementById('vectors'), "l" + vector.id.toString()); // +} + /** * Draws the background grid of the space * @param css_class class for the lines that are 'multiples of the basis vector' @@ -118,6 +133,37 @@ export const create_grid = function (css_class, bg_css_class) { return group; } +export const add_vector_arrow = function (vector) { + vectors_by_id[vector.id] = vector; + add_vector_arrow_to_svg(vector); +} + +function create_label(vector) { + let label = create_svg_element('text'); + label.setAttribute('x', (calc_screen_x(vector.x) + 5).toString()); + label.setAttribute('y', (calc_screen_y(vector.y) + 5).toString()); + label.setAttribute('fill', 'yellow'); + label.setAttribute('id', 'l' + vector.id); + let text_node = document.createTextNode(vector.label_text); + label.appendChild(text_node); + return label; +} + +const update_label = function (id, new_id, x, y) { + let label = document.getElementById('l' + id); + label.setAttribute('x', x.toString()); + label.setAttribute('y', y.toString()); + label.id = 'l' + new_id; +} + +export const update_label_text = function (id, text) { + if (text) { + let label = document.getElementById('l' + id); + label.firstChild.textContent = text; + } +} + + export const add_vector_arrow_to_svg = function (vector) { let vector_group = get_or_create_vector_group(); let vector_arrow = create_arrow(vector.id, vector.x0, vector.y0, vector.x, vector.y, 'vector'); @@ -126,16 +172,10 @@ export const add_vector_arrow_to_svg = function (vector) { }; vector_group.appendChild(vector_arrow); - let label = create_svg_element('text'); - label.setAttribute('x', (calc_screen_x(vector.x) + 5).toString()); - label.setAttribute('y', (calc_screen_y(vector.y) + 5).toString()); - label.setAttribute('fill', 'yellow'); - label.setAttribute('id', 'l' + vector.id); - let text = document.createTextNode(vector.label); - label.appendChild(text); + + let label = create_label(vector); vector_group.appendChild(label); - } /** @@ -149,6 +189,7 @@ export const add_vector_arrow_to_svg = function (vector) { const draw_vectors = function () { const vector_group = get_or_create_vector_group(); + let vectors = Object.values(vectors_by_id); for (let i = 0; i < vectors.length; i++) { add_vector_arrow_to_svg(vectors[i]); } @@ -183,13 +224,13 @@ const redraw_grid = function () { svg.appendChild(create_axes()); } -export const update_vector_arrow = function (id, vector) { - let d = calculate_d(vector.x0, vector.y0, vector.x, vector.y); - let arrow = document.getElementById(id.toString()); +export const update_vector_arrow = function (existing_id, new_vector) { + let d = calculate_d(new_vector.x0, new_vector.y0, new_vector.x, new_vector.y); + let arrow = document.getElementById(existing_id.toString()); if (arrow) { arrow.setAttribute('d', d); - arrow.id = vector.id; - update_label_position(id, vector.id, calc_screen_x(vector.x) + 5, calc_screen_y(vector.y) + 5); + arrow.id = new_vector.id; + update_label(existing_id, new_vector.id, calc_screen_x(new_vector.x) + 5, calc_screen_y(new_vector.y) + 5); } } @@ -236,13 +277,6 @@ const create_defs = function () { return defs; } -const update_label_position = function (id, new_id, x, y) { - let label = document.getElementById('l' + id); - label.setAttribute('x', x.toString()); - label.setAttribute('y', y.toString()); - label.id = 'l' + new_id; -} - /** * The moving operation. Called by onmousemove on the svg ('canvas') * @param event @@ -251,11 +285,14 @@ const move_vector = function (event) { if (moving_vector) { let current_x = event.clientX; let current_y = event.clientY; - vectors[moving_vector.id].x = (current_x - origin_x) / grid_size; - vectors[moving_vector.id].y = (origin_y - current_y) / grid_size; - moving_vector.setAttribute('d', create_d(origin_x, origin_y, current_x, current_y)); - update_label_position(moving_vector.id,moving_vector.id, current_x + 5, current_y + 5); - update_lazy_objects(); + let vector = vectors_by_id[parseInt(moving_vector.id)]; + if (vector) { + vector.x = (current_x - origin_x) / grid_size; + vector.y = (origin_y - current_y) / grid_size; + moving_vector.setAttribute('d', create_d(origin_x, origin_y, current_x, current_y)); + update_label(moving_vector.id, moving_vector.id, current_x + 5, current_y + 5); + update_lazy_objects(); + } } } /** @@ -283,14 +320,6 @@ document.body.onresize = function recalculate_window_dimensions() { redraw(); } -const SVG_NS = 'http://www.w3.org/2000/svg'; // program needs these to create svg elements -let grid_size = 100; // this is the nr of pixels for the basis vector (1,0) (0,1) -let half_grid_size = grid_size >> 1; // used to position the grid lines - -let moving_vector; // user can move vector arrows. when moving, this refers to the arrow -let width = window.innerWidth, height = window.innerHeight; -let origin_x = Math.floor((width / grid_size) / 2) * grid_size + half_grid_size, - origin_y = Math.floor((height / grid_size) / 2) * grid_size + half_grid_size; const svg = create_svg(); document.body.appendChild(svg); svg.appendChild(create_grid('grid', 'bg-grid'));