matrepl/src/js/index.js
2021-03-05 19:43:38 +01:00

316 lines
No EOL
12 KiB
JavaScript

import '../css/app.css';
import {scan, token_types} from './scanner';
import {parse} from './parser';
import {add_vector_arrow, add_vector_arrow_to_svg, update_vector_arrow} from "./svg_functions";
import {
addition,
division,
functions,
label,
logical_and,
logical_or,
multiplication,
subtraction,
test_equal
} from './functions.js';
const state = {}; // binding -> value
const bindings = {}; // binding -> {name:binding_name, evaluated: evaluated_lazy_value, previous: previous_value}
const references = {}; // for retrieval of objects by reference (@n, where n is a number)
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;
const keywords = {
'true': true,
'false': false,
'pi': Math.PI,
'PI': Math.PI,
'e': Math.E
}
export const update_visible_objects = function () {
Object.entries(bindings).forEach(entry => { // a lazy expression must be bound
const [name, binding] = entry;
let value = state[binding.name];
if (value.lazy_expression) {
let new_value = visit(value.lazy_expression); // reevaluate,
let existing_value = binding.evaluated; // update view
if (new_value.is_vector || existing_value.is_vector) {
bindings[name].evaluated = new_value;
new_value.label_text = existing_value.label_text; // SAD
update_vector(existing_value, new_value, name);
}
}
});
Object.entries(references).forEach(entry => {
const [id, value] = entry;
if (value.lazy_expression) {
let new_value = visit(value.lazy_expression);
let existing_value = value;
if (new_value.is_vector || existing_value.is_vector) {
new_value.lazy_expression = value.lazy_expression;
references[id] = new_value;
if (existing_value && existing_value.id) {
// update view after reevaluation of lazy vectors
update_vector_arrow(existing_value.id, new_value);
} else if (new_value.is_new && new_value.is_vector) {
// hidden lazy vector reappears
add_vector_arrow_to_svg(new_value);
}
}
}
});
}
const update_vector = function (existing_value, new_value, binding_name) {
if (existing_value && existing_value.id) {
// update view after reevaluation of lazy vectors
update_vector_arrow(existing_value.id, new_value);
} else if (new_value.is_new && new_value.is_vector) {
// hidden lazy vector reappears
new_value.label_text = binding_name;
add_vector_arrow_to_svg(new_value);
}
}
export const adjust_input_element_height = function () {
let num_lines = command_input_element.value.split(/\n/).length;
command_input_element.setAttribute('style', 'height: ' + num_lines + 'em');
if (num_lines > 1) {
command_input_element.setAttribute('class', 'multiline');
} else {
command_input_element.setAttribute('class', 'single_line');
}
}
command_input_element.onkeypress = function handle_key_input(event) {
if (event.key === 'Enter') {
event.preventDefault();
}
}
command_input_element.onkeyup = function handle_key_input(event) {
adjust_input_element_height();
if (event.key === 'ArrowUp' && !event.shiftKey) {
if (command_history_index > -1) {
command_input_element.value = command_history[command_history_index];
if (command_history_index > 0) {
command_history_index -= 1;
}
}
}
if (event.key === 'ArrowDown' && !event.shiftKey) {
if (command_history_index < command_history.length - 1) {
command_history_index += 1;
command_input_element.value = command_history[command_history_index];
} else {
command_input_element.value = '';
}
}
if (event.key === 'Enter') {
handle_enter();
}
};
const handle_enter = function () {
let commands = command_input_element.value;
command_input_element.value = '';
adjust_input_element_height();
let command_array = commands.split(/\n/);
for (let i = 0; i < command_array.length; i++) {
let command = command_array[i];
if (command.length > 0) {
command_history_element.innerText += command + "\n";
command_input_element.value = '';
command_history_index = command_history.length;
let value;
try {
let tokens = scan(command);
let statement = parse(tokens);
value = visit(statement);
if (value !== undefined) {
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]; // lookup the value for the binding
} else if (Object.prototype.hasOwnProperty.call(value, ['id'])) {
references['@' + value.id] = value;
}
while (value.lazy_expression) {
value = value.get();
}
if (binding) {
bindings[binding].evaluated = value; // store evaluation result
}
if (value.is_visual) {
if (binding && bindings[binding].previous && bindings[binding].previous.is_visual) {
update_vector_arrow(bindings[binding].previous.id, value);
} else {
if (value.is_new) {
value.label_text = binding ? binding : "";
value.is_new = false;
add_vector_arrow(value);
}
}
} else {
if (binding && bindings[binding].previous && bindings[binding].previous.is_visual) {
label(bindings[binding].previous, '@' + bindings[binding].previous.id);
}
}
update_visible_objects();
if (value.description) {
value = value.description;
}
} else {
value = 'value is undefined';
}
} catch (exception) {
value = exception.message;
}
command_history_element.innerText += value.toString() + "\n";
command_history.push(command);
command_history_element.scrollTo(0, command_history_element.scrollHeight);
}
}
}
const visit = function (expr) {
switch (expr.type) {
case 'declaration': {
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
};
}
state[binding_name] = value; // assign new value to binding
return bindings[binding_name]; // don't return the value itself, but the binding_object
} // with which you can lookup the value
case 'group': // expression within parentheses
return visit(expr.expression);
case 'unary': {
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) {
return !right_operand;
} else {
throw {message: 'illegal unary operator'};
}
}
case 'binary': {
switch (expr.operator) {
case token_types.MINUS:
return subtraction(visit(expr.left), visit(expr.right));
case token_types.PLUS:
return addition(visit(expr.left), visit(expr.right));
case token_types.STAR:
return multiplication(visit(expr.left), visit(expr.right));
case token_types.SLASH:
return division(visit(expr.left), visit(expr.right));
case token_types.DOT:
return method_call(visit(expr.left), expr.right); // right is not evaluated. It's the method name
case token_types.EQUALS_EQUALS:
return test_equal(visit(expr.left), visit(expr.right));
case token_types.AND:
return logical_and(visit(expr.left), visit(expr.right));
case token_types.OR:
return logical_or(visit(expr.left), visit(expr.right));
}
throw {message: 'illegal binary operator'};
}
case 'identifier': {
if (expr.name in keywords) {
return keywords[expr.name];
} else {
if (state[expr.name]) {
let value = state[expr.name];
while (value.lazy_expression) {
value = value.get();
}
return value;
} else {
return undefined;
}
}
}
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 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) {
if (Object.prototype.hasOwnProperty.apply(functions, [function_name])) {
return functions[function_name](resolve_arguments(argument_exprs));
} else {
return `unknown function: ${function_name}(${argument_exprs.map(e => e.value_type).join(',')})`;
}
}
const method_call = function (object, method_or_property) {
if (object) {
if (method_or_property.type === 'call') { // method
if (typeof object[method_or_property.name] !== 'function') {
throw {message: `method '${method_or_property.name}' not found on ${object.type}`};
}
return object[method_or_property.name].apply(object, resolve_arguments(method_or_property.arguments));
} else { // property
if (!Object.prototype.hasOwnProperty.call(object, [method_or_property.name])) {
throw {message: `property '${method_or_property.name}' not found on ${object.type}`};
}
return object[method_or_property.name];
}
} else {
throw {message: `not found: ${object}`};
}
}
const resolve_arguments = function (argument_exprs) {
return argument_exprs.map(expr => {
let value = visit(expr);
while (value.lazy_expression) {
value = value.get();
}
return value;
});
}