398 lines
No EOL
14 KiB
JavaScript
398 lines
No EOL
14 KiB
JavaScript
import {scan, token_types} from './scanner';
|
|
import {parse} from './parser';
|
|
import {
|
|
add_vector_arrow,
|
|
add_vector_arrow_to_svg,
|
|
remove_vector_arrow,
|
|
update_label_text,
|
|
update_vector_arrow
|
|
} from "./svg_functions";
|
|
|
|
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;
|
|
let index_sequence = 0;
|
|
|
|
const hide = function (vector) {
|
|
vector.visible = false;
|
|
remove_vector_arrow(vector);
|
|
return {description: `vector@${vector.id} is hidden`};
|
|
}
|
|
|
|
const label = function (vector, text) {
|
|
vector.label_text = text;
|
|
update_label_text(vector.id, text);
|
|
return `label set to ${text} on vector@${vector.id}`;
|
|
}
|
|
|
|
const show = function (vector) {
|
|
vector.visible = true;
|
|
add_vector_arrow_to_svg(vector);
|
|
return {description: `vector@${vector.id} is visible`};
|
|
}
|
|
|
|
export const update_lazy_objects = function () {
|
|
let b = Object.values(bindings); // a lazy expression must be bound
|
|
for (let i = 0; i < b.length; i++) { // unbound ones can exist, but are meaningless, because you cannot refer to them
|
|
let binding = b[i];
|
|
if (state[binding.name].lazy_expression) { // if lazy,
|
|
let value = visit(state[binding.name].lazy_expression); // reevaluate,
|
|
let existing_value = bindings[binding.name].evaluated; // update view
|
|
if (existing_value.id) {
|
|
// update view after reevaluation of lazy vectors
|
|
update_vector_arrow(existing_value.id, value);
|
|
bindings[binding.name].evaluated = value;
|
|
} else if (value.is_new && value.is_vector){
|
|
// hidden lazy vector reappears
|
|
value.label_text=binding.name;
|
|
add_vector_arrow_to_svg(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);
|
|
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) {
|
|
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_lazy_objects();
|
|
if (value.description) {
|
|
value = value.description;
|
|
}
|
|
} catch (e) {
|
|
value = e.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];
|
|
}
|
|
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 subtract(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
|
|
// 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 value = state[expr.name];
|
|
while (value.lazy_expression) {
|
|
value = value.get();
|
|
}
|
|
return value;
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
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 {
|
|
let arg_list = '';
|
|
for (let i = 0; i < argument_exprs.length; i++) {
|
|
if (i > 0) {
|
|
arg_list += ',';
|
|
}
|
|
arg_list += argument_exprs[i].value_type;
|
|
}
|
|
return 'unimplemented: ' + function_name + '(' + arg_list + ')';
|
|
}
|
|
}
|
|
|
|
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 functions = {
|
|
help: () => help(),
|
|
vector: (args) => {
|
|
if (args.length === 2) {
|
|
return create_vector({x0: 0, y0: 0, x: args[0], y: args[1]});
|
|
} else {
|
|
return create_vector({x0: args[0], y0: args[1], x: args[2], y: args[3]});
|
|
}
|
|
},
|
|
hide: (args) => {
|
|
return hide(args[0]);
|
|
},
|
|
label: (args) => {
|
|
return label(args[0], args[1]);
|
|
},
|
|
show: (args) => {
|
|
return show(args[0]);
|
|
}
|
|
}
|
|
|
|
const help = function () {
|
|
return {
|
|
description:
|
|
`- vector(<x0>, <y0>, <x>, <y>): draws a vector from x0,y0 to x,y
|
|
- remove(<identifier>|<ref>): removes an object,
|
|
a ref is @n where n is the reference number asigned to the object`
|
|
}
|
|
}
|
|
|
|
const multiplication = function (left, right) {
|
|
|
|
const multiply = function (vector, scalar) {
|
|
return create_vector({
|
|
x0: vector.x0 * scalar,
|
|
y0: vector.y0 * scalar,
|
|
x: vector.x * scalar,
|
|
y: vector.y * scalar
|
|
});
|
|
};
|
|
|
|
if (left.is_vector && !right.is_vector) {
|
|
return multiply(left, right);
|
|
}
|
|
if (right.is_vector && !left.is_vector) {
|
|
return multiply(right, left);
|
|
}
|
|
return left * right;
|
|
}
|
|
|
|
const division = function (left, right) {
|
|
const divide = function (vector, scalar) {
|
|
return create_vector({
|
|
x0: vector.x0 / scalar,
|
|
y0: vector.y0 / scalar,
|
|
x: vector.x / scalar,
|
|
y: vector.y / scalar
|
|
});
|
|
};
|
|
|
|
if (left.is_vector && !right.is_vector) {
|
|
return divide(left, right);
|
|
}
|
|
if (!left.is_vector && !right.is_vector) {
|
|
return left / right;
|
|
}
|
|
throw {message: 'meaningless division'};
|
|
}
|
|
|
|
const addition = function (left, right) {
|
|
if (left && left.is_vector && right && right.is_vector) {
|
|
return create_vector({
|
|
x0: left.x0 + right.x0,
|
|
y0: left.x0 + right.x0,
|
|
x: left.x + right.x,
|
|
y: left.y + right.y
|
|
});
|
|
}
|
|
return left + right;
|
|
}
|
|
|
|
const subtract = function (left, right) {
|
|
if (left && left.is_vector && right && right.is_vector) {
|
|
return create_vector({
|
|
x0: left.x0 - right.x0,
|
|
y0: left.x0 - right.x0,
|
|
x: left.x - right.x,
|
|
y: left.y - right.y
|
|
});
|
|
}
|
|
return left - right;
|
|
}
|
|
|
|
export const create_vector = function (vector) { //rename to create_vector
|
|
vector.id = index_sequence++;
|
|
vector.is_visual = true;
|
|
vector.is_vector = true; // for comparison
|
|
vector.type = 'vector'; // for showing type to user
|
|
vector.is_new = true;
|
|
vector.visible = true;
|
|
vector.toString = function () {
|
|
return `vector@${this.id}{x0:${vector.x0},y0:${vector.y0} x:${vector.x},y:${vector.y}}`;
|
|
};
|
|
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;
|
|
} |