commit 4076440ff3910d0e9895b217376ffd23ae3b6bdf Author: Sander Hautvast Date: Fri Feb 12 17:42:15 2021 +0100 supports vectors with functions, methods, properties diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..5008ddf Binary files /dev/null and b/.DS_Store differ diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..caa32e6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.idea/ +*.iml \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..2c09b9c --- /dev/null +++ b/README.md @@ -0,0 +1,20 @@ +**MatRepl** +* is a Matrix +* and a repl: Read–Eval–Print Loop, where Print is doing operations on vectors and matrices in a graphic environment + + + +The repl has the following syntax (It's work in progress, new capabilities will be added) +* simple arithmetic expressions: + ** add, subtract, divide, multiply + ** variable declaration eg: a= ... + ** vector(x0,y0,x,y) adds a vector + ** remove(x) removes bindings (when it's an object (eg vector), removes it from the matrix) + ** method calls: + *** a = vector(0,0,12,1) + *** a.type() + *** > vector + ** property lookup + *** a.x + *** 12 + \ No newline at end of file diff --git a/screenshot-1.png b/screenshot-1.png new file mode 100644 index 0000000..c101a20 Binary files /dev/null and b/screenshot-1.png differ diff --git a/src/app.css b/src/app.css new file mode 100644 index 0000000..ac1dbb1 --- /dev/null +++ b/src/app.css @@ -0,0 +1,81 @@ +body { + font: 13px Arial, sans-serif; + background: black; + overflow: hidden; +} + +.background { + position: absolute; + left: 0; + top: 0; + width: 100%; + height: 100%; +} + +svg { + position: absolute; + width: 100%; + height: 100%; + z-index: -1; +} + +.grid { + fill: none; + stroke: #4682b4; + stroke-width: 3; +} + +.bg-grid { + fill: none; + stroke: gray; + stroke-width: 0.5; +} + +#console { + padding: 5px; + color: greenyellow; + background: black; + position: absolute; + right: 10px; + bottom: 0; + width: 40%; + height: 10em; + border: 2px solid darkgray; + border-radius: 10px; + z-index: 10; +} + +.axis { + stroke-width: 1.5; + stroke: lightpink; +} + +.vector { + stroke-width: 2.5; + stroke: yellow; + stroke-linecap: round; +} + +#prompt{ + position: absolute; + bottom: 0; +} + +#command_input{ + border: none transparent; + background: black; + color: greenyellow; + outline: none; + width: 39vw; +} + +#command_history{ + font-size: 12px; + color: greenyellow; + position: absolute; + bottom: 1.5em; + max-height: 9em; + width: 100%; + overflow-y: visible; + overflow-x: hidden; +} \ No newline at end of file diff --git a/src/console.js b/src/console.js new file mode 100644 index 0000000..1b38c84 --- /dev/null +++ b/src/console.js @@ -0,0 +1,183 @@ +/** + * handles user input from the console div + */ +(function () { + const state = {}; + const command_input_element = document.getElementById('command_input'); + const command_history_element = document.getElementById('command_history'); + const bottom = document.getElementById('bottom'); + command_input_element.value = ''; + let command_history = []; + let command_history_index = 0; + + command_input_element.onkeyup = function handle_key_input(event) { + if (event.key === 'ArrowUp') { + 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') { + 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') { + let command = command_input_element.value; + command_history_element.innerText += command + "\n"; + command_input_element.value = ''; + command_history_index = command_history.length; + let tokens = scan(command); + let statement = parse(tokens); + let result; + try { + result = visit_expression(statement); + if (result.description) { + result = result.description; + } + } catch (e) { + result = e.message; + } + command_history_element.innerText += result + "\n"; + command_history.push(command); + command_history_element.scrollTo(0, command_history_element.scrollHeight); + } + }; + + let visit_expression = function (expr) { + switch (expr.type) { + case 'declaration': + let value = visit_expression(expr.initializer); + let existing_value = state[expr.var_name.value]; + if (existing_value) { + if (existing_value.type === 'vector') { + remove_vector(existing_value.object); // remove from screen + } + } + value.binding = expr.var_name.value; + state[expr.var_name.value] = value; + let description = state[expr.var_name.value].description; + if (!description) { + description = state[expr.var_name.value]; //questionable. use toString instead of message? + } + return {description: expr.var_name.value + ':' + description}; + case 'group': + return visit_expression(expr.expression); + case 'unary': + let right_operand = visit_expression(expr.right); + if (expr.operator === token_types.MINUS) { + return -right_operand; + } else if (expr.operator === token_types.NOT) { + return !right_operand; + } else { + throw {message: 'illegal unary operator'}; + } + case 'binary': + let left = visit_expression(expr.left); + let right = visit_expression(expr.right); + switch (expr.operator) { + case token_types.MINUS: + return left - right; + case token_types.PLUS: + return addition(left, right); + case token_types.STAR: + return multiplication(left, right); + case token_types.SLASH: + return left / right; + case token_types.DOT: + return method_call(left, expr.right); + } + throw {message: 'illegal binary operator'} + case 'identifier': { + if (state[expr.name]) { + return state[expr.name]; + } else { + break; + } + } + case 'literal': + return expr.value; + case 'call': + return call(expr.name, expr.arguments); + } + } + + const call = function (function_name, argument_exprs) { + let arguments = []; + for (let i = 0; i < argument_exprs.length; i++) { + arguments.push(visit_expression(argument_exprs[i])); + } + if (functions[function_name]) { + return functions[function_name](arguments); + } 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_wrapper, method_or_property) { + if (object_wrapper) { + if (method_or_property.type === 'call') { // method + if (typeof object_wrapper.object[method_or_property.name] !== 'function') { + throw {message: `method ${method_or_property.name} not found on ${object_wrapper.type}`}; + } + return object_wrapper.object[method_or_property.name].apply(object_wrapper, method_or_property.arguments); + + } else { // property + if (!object_wrapper.object.hasOwnProperty(method_or_property.name)){ + throw {message: `property ${method_or_property.name} not found on ${object_wrapper.type}`}; + } + return object_wrapper.object[method_or_property.name]; + } + } else { + throw {message: `not found: ${object_wrapper}`}; + } + } + + const functions = { + help: () => help(), + vector: (args) => add_vector({x0: args[0], y0: args[1], x: args[2], y: args[3]}), + remove: (args) => { + if (args[0].hasOwnProperty('binding')) { + delete this.state[args[0].binding]; + return remove_vector(args[0].object); // by binding value + } else { + return remove_vector(args[0]); // by index (@...) + } + + }, + } + + const help = function () { + return {message: 'vector(x0, y0, x, y): draws a vector from x0,y0 to x,y'} + } + + const multiplication = function (left, right) { + if (left.object && left.type === 'vector' && !right.object) { + return left.object.multiply(right); + } + if (right.object && right.type === 'vector' && !left.object) { + return right.object.multiply(left); + } + return left * right; + } + + const addition = function (left, right) { + if (left.object && left.type === 'vector' && right.object && right.type === 'vector') { + return left.object.add(right.object); + } + return left + right; + } + } +)(); \ No newline at end of file diff --git a/src/index.html b/src/index.html new file mode 100644 index 0000000..cfbe7ce --- /dev/null +++ b/src/index.html @@ -0,0 +1,20 @@ + + + + + Interactive Linear Algebra + + + +
+
+ +
+ + + + + + \ No newline at end of file diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..c415c77 --- /dev/null +++ b/src/index.js @@ -0,0 +1,297 @@ +let add_vector, + remove_vector; + +/** + * Main entry. draws the matrix + */ +(function () { + 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 vectors = []; // collection of added vectors + 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 + * @param element_type path,g, etc + * @returns SVG element + */ + const create = function (element_type) { + return document.createElementNS(SVG_NS, element_type); + } + + /** + * creates the d attribute string + * @param x0 start_x + * @param y0 start_y + * @param x1 end_x + * @param y1 end y + * @returns {string} to put in an SVG path + */ + const calculate_d = function (x0, y0, x1, y1) { + return "M" + x0 + " " + y0 + " L" + x1 + " " + y1; + } + + /** + * creates a SVG line (path) + * @param x0 start_x + * @param y0 start_y + * @param x1 end_x + * @param y1 end_y + * @param css_class the css class to make up the element + * @returns an SVG path element + */ + const create_line = function (x0, y0, x1, y1, css_class) { + let path = create('path'); + path.setAttribute('d', calculate_d(x0, y0, x1, y1)); + path.setAttribute('class', css_class); + return path; + } + + /** + * creates the arrow path element + * @param id attribute + * @param x0 start_x + * @param y0 start_y + * @param x1 end_x + * @param y1 end_y + * @param css_class class attribute + * @returns {SVGPathElement} + */ + const arrow = function (id, x0, y0, x1, y1, css_class) { + let path = create('path'); + + path.setAttribute('d', calculate_d(x0, y0, x1, y1)); + path.id = id; + path.setAttribute('class', css_class); + path.setAttribute('marker-end', 'url(#arrow)'); + return path; + } + + /** + * Draws the background grid of the space + * @param css_class class for the lines that are 'multiples of the basis vector' + * @param bg_css_class class for in between lines + * @returns {SVGGElement} + */ + const create_grid = function (css_class, bg_css_class) { + const group = create('g'); + group.setAttribute('id', 'grid'); + const horizontal = create('g'); + horizontal.setAttribute('id', 'horizontal'); + for (let y = 0; y < height; y += grid_size) { + horizontal.appendChild(create_line(0, y + half_grid_size, width, y + half_grid_size, css_class)); + horizontal.appendChild(create_line(0, y, width, y, bg_css_class)); + } + group.appendChild(horizontal); + const vertical = create('g'); + vertical.setAttribute('id', 'vertical'); + for (let x = 0; x < width; x += grid_size) { + vertical.appendChild(create_line(x + half_grid_size, 0, x + half_grid_size, height, css_class)); + vertical.appendChild(create_line(x, 0, x, height, bg_css_class)); + } + group.appendChild(vertical); + return group; + } + + /** + * removes child from element by id if found + * @param element + * @param child_id id to remove + */ + const remove_child = function (element, child_id) { + let node = element.firstChild; + while (node && child_id !== node.id) { + node = node.nextSibling; + } + if (node) { + element.removeChild(node); + } + } + + /** + * removes the grid from the DOM and adds an updated one. + */ + const redraw_grid = function () { + remove_child(svg, "grid"); + svg.appendChild(create_grid('grid', 'bg-grid')); + svg.appendChild(create_axes()); + } + + /** + * Adds a vector to the set. + * @param vector + */ + add_vector = function (vector) { + vector.id = vectors.length; + vectors.push(vector); + redraw(); + vector.add = (other) => add_vector({ + x0: vector.x0 + other.x0, + y0: vector.x0 + other.x0, + x: vector.x + other.x, + y: vector.y + other.y + }); + vector.multiply = (scalar) => add_vector({ + x0: vector.x0 * scalar, + y0: vector.y0 * scalar, + x: vector.x * scalar, + y: vector.y * scalar + }); + vector.is_vector = true; + vector.type = () => 'vector'; + return { //object_wrapper + type: 'vector', + object: vector, + description: `vector@${vector.id}{x0:${vector.x0},y0:${vector.y0} x:${vector.x},y:${vector.y}}`, + }; + + } + + 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; + } + + if (!vectors[index]) { + throw {message: `vector@${index} not found`}; + } + + vectors.splice(index, 1); + redraw(); + return {description: `vector@${index} removed`}; + } + + /** + * The moving operation. Called by onmousemove on the svg ('canvas') + * @param event + */ + const move = 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', calculate_d(origin_x, origin_y, current_x, current_y)); + } + } + + /** + * Draws all the vectors. + * + * vector { + * x0,y0 origin + * x,y coordinates + * } + */ + const draw_vectors = function () { + const vector_group = create("g"); + vector_group.id = 'vectors'; + + for (let i = 0; i < vectors.length; i++) { + let vector_arrow = arrow(vectors[i].id, + origin_x + vectors[i].x0 * grid_size, + origin_y - vectors[i].y0 * grid_size, + origin_x + vectors[i].x * grid_size, + origin_y - vectors[i].y * grid_size, + 'vector'); + vector_arrow.onmousedown = function start_moving_vector(event) { + moving_vector = event.target; + }; + vector_group.appendChild(vector_arrow); + } + svg.appendChild(vector_group); + } + + /** + * Removes all vectors in the svg and calls draw_vectors to draw updated versions. + */ + const redraw_vectors = function () { + remove_child(svg, 'vectors'); + draw_vectors(); + } + + /** + * (re)draws all + */ + const redraw = function () { + redraw_grid(); + redraw_vectors(); + } + + const create_axes = function () { + let axes_group = create('g'); + let x = create_line(0, origin_y, width, origin_y, 'axis'); + x.id = 'x-axis'; + axes_group.appendChild(x); + let y = create_line(origin_x, 0, origin_x, height, 'axis'); + y.id = 'y-axis'; + axes_group.appendChild(y); + return axes_group; + } + + /** + * setup the arrow head for the vector + * @returns {SVGDefsElement} + */ + function create_defs() { + let defs = create('defs'); + let marker = create('marker'); + marker.id = 'arrow'; + marker.setAttribute('orient', 'auto'); + marker.setAttribute('viewBox', '0 0 10 10'); + marker.setAttribute('markerWidth', '3'); + marker.setAttribute('markerHeight', '4'); + marker.setAttribute('markerUnits', 'strokeWidth'); + marker.setAttribute('refX', '6'); + marker.setAttribute('refY', '5'); + let polyline = create('polyline'); + polyline.setAttribute('points', '0,0 10,5 0,10 1,5'); + polyline.setAttribute('fill', 'yellow'); + marker.appendChild(polyline); + defs.appendChild(marker); + return defs; + } + + /** + * Creates the SVG + * @returns {SVGElement} + */ + const create_svg = function () { + let svg = create('svg'); + + svg.onmousemove = move; + svg.onmouseup = function stop_moving_vector() { + moving_vector = undefined; + }; + + let defs = create_defs(); + svg.appendChild(defs); + return svg; + } + + document.body.onresize = function recalculate_window_dimensions() { + width = window.innerWidth; + height = window.innerHeight; + 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; + redraw(); + } + + const svg = create_svg(); + document.body.appendChild(svg); + + svg.appendChild(create_grid('grid', 'bg-grid')); + svg.appendChild(create_axes()); +}) +(); \ No newline at end of file diff --git a/src/parser.js b/src/parser.js new file mode 100644 index 0000000..a3f8d74 --- /dev/null +++ b/src/parser.js @@ -0,0 +1,185 @@ +const parse = function (tokens) { + let token_index = 0; + + return statement(); + + function statement() { + if (check(token_types.IDENTIFIER, token_index) && check(token_types.EQUALS, token_index + 1)) { + let var_name = current_token(); + advance(); + advance(); + return {type: 'declaration', var_name: var_name, initializer: expression()}; + } else { + return expression(); + } + } + + function expression() { + return equality(); + } + + function equality() { + let expr = comparison() + + while (match([token_types.EQUALS_EQUALS, token_types.NOT_EQUALS])) { + let operator = previous_token(); + let right = unary(); + expr = {type: 'binary', left: expr, operator: operator, right: right}; + } + + return expr; + } + + function comparison() { + let expr = addition(); + + while (match([token_types.LESS, token_types.LESS_OR_EQUAL, token_types.GREATER, token_types.GREATER_OR_EQUAL])) { + let operator = previous_token(); + let right = addition(); + expr = {type: 'binary', left: expr, operator: operator, right: right}; + } + + return expr; + } + + function addition() { + let expr = multiplication(); + + while (match([token_types.MINUS, token_types.PLUS])) { + let operator = previous_token(); + let right = multiplication(); + expr = {type: 'binary', left: expr, operator: operator, right: right}; + } + + return expr; + } + + function multiplication() { + let expr = unary(); + + while (match([token_types.SLASH, token_types.STAR, token_types.DOT])) { + let operator = previous_token(); + let right = unary(); + expr = {type: 'binary', left: expr, operator: operator, right: right}; + } + + return expr; + } + + function unary() { + if (match([token_types.NOT, token_types.MINUS])) { + let operator = previous_token(); + let right = unary(); + return {type: 'unary', operator: operator, right: right}; + } else { + return call(); + } + } + + function call() { + let expr = primary(); + + while (true) { + if (match([token_types.LEFT_PAREN])) { + expr = finish_call(expr.name); + } else { + break; + } + } + + return expr; + } + + function finish_call(callee) { + let arguments = []; + if (!check(token_types.RIGHT_PAREN, token_index)) { + do { + arguments.push(expression()); + } while (match([token_types.COMMA])); + } + if (!match([token_types.RIGHT_PAREN])) { + throw {message: "Expect ')' after arguments."}; + } + + return {type: 'call', name: callee, arguments: arguments}; + } + + + function primary() { + 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.LEFT_PAREN])) { + let expr = expression(); + if (expr && match([token_types.RIGHT_PAREN])) { + 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}; + advance(); + return identifier; + } + } + + /** + * matches token against array of tokens to check for equality (matching type) + * @param tokens_to_match array of tokens + * @returns {boolean} + */ + function match(tokens_to_match) { + for (let i = 0; i < tokens_to_match.length; i++) { + if (are_same(tokens_to_match[i], current_token())) { + advance() + return true; + } + } + return false; + } + + /** + * Checks if token at position index matches the given + * @param token_to_check expected token type + * @param index of token to check + * @returns {boolean} + */ + function check(token_to_check, index) { + let token = tokens[index]; + if (!token) { + return false; + } + return are_same(token_to_check, token); + + } + + /** + * checks if 2 tokens have same type + * @param token_1 + * @param token_2 + * @returns {boolean} + */ + function are_same(token_1, token_2) { + if (is_at_end()) { + return false; + } else { + return token_1.type === token_2.type; + } + + } + + function is_at_end() { + return token_index >= tokens.length; + } + + function advance() { + token_index += 1; + } + + function previous_token() { + return tokens[token_index - 1]; + } + + function current_token() { + return tokens[token_index]; + } +} \ No newline at end of file diff --git a/src/scanner.js b/src/scanner.js new file mode 100644 index 0000000..92be530 --- /dev/null +++ b/src/scanner.js @@ -0,0 +1,181 @@ +/** + * Creates an array of tokens from a line of input. + * + * @param command: string + * @returns {token_type[]} + */ +const scan = function(command) { + let current_index = 0, // current index of char to look at in the command string + word_start_index = 0, // marker for start of a literal or identifier + tokens = []; + + while (!is_at_end()) { + word_start_index = current_index; + let token = scan_token(); + if (token) { // undefined mostly means whitespace + tokens.push(token); + } + } + return tokens; + + function scan_token() { + let next_char = advance(); + switch (next_char) { + case '(': + return token_types.LEFT_PAREN; + case ')': + return token_types.RIGHT_PAREN; + case '[': + return token_types.LEFT_BRACKET; + case ']': + return token_types.RIGHT_BRACKET; + case ',': + return token_types.COMMA; + case '.': + return token_types.DOT; + case '-': + return token_types.MINUS; + case '+': + return token_types.PLUS; + case '*': + return token_types.STAR; + case '/': + return token_types.SLASH; + case '>': + if (expect('=')) { + return token_types.GREATER_OR_EQUAL; + } else { + return token_types.GREATER; + } + case '<': + if (expect('=')) { + return token_types.LESS_OR_EQUAL; + } else { + return token_types.LESS; + } + case '!': + if (expect('=')) { + return token_types.NOT_EQUALS; + } else { + return token_types.NOT; + } + case '=': + if (expect('=')) { + return token_types.EQUALS_EQUALS; + } else { + return token_types.EQUALS; + } + case '\'': + return string('\''); + case '\"': + return string('\"'); + } + if (is_digit(next_char)) { + let token = Object.assign({}, token_types.NUMERIC); + token.value = parse_number(); + return token; + } else { + if (is_alpha_or_underscore(next_char)) { + let token = Object.assign({}, token_types.IDENTIFIER); + token.value = parse_identifier(); + return token; + } + } + } + + function expect(expected_char) { + if (is_at_end()) { + return false; + } + if (current_char() === expected_char) { + advance(); + return true; + } else { + return false; + } + } + + function advance() { + if (current_index < command.length) { + current_index += 1; + } + return command[current_index - 1]; + } + + function is_at_end() { + return current_index >= command.length; + } + + function current_char() { + return command[current_index]; + } + + function is_digit(char) { + return char >= '0' && char <= '9'; + } + + function is_part_of_number(char) { + return is_digit(char) || char === '.'; // no scientific notation for now + } + + function parse_number() { + while (is_part_of_number(current_char())) { + advance(); + } + let number_string = command.substring(word_start_index, current_index); + return Number.parseFloat(number_string); + } + + function is_alpha_or_underscore(char) { + return (char >= 'a' && char <= 'z') || (char >= 'A' && char <= 'Z') || char === '_'; + } + + function is_alphanumeric_or_underscore(char) { + return (char >= 'a' && char <= 'z') || (char >= 'A' && char <= 'Z') || is_digit(char) || char === '_'; + } + + function parse_identifier() { + while (is_alphanumeric_or_underscore(current_char())) { + advance(); + } + return command.substring(word_start_index, current_index); + } + + function string(quote) { // as of yet strings may not unclude escaped quotes that are also the start/end quote + while (current_char() !== quote && !is_at_end()) { + advance(); + } + if (is_at_end() && current_char() !== quote) { + throw {message: 'unterminated string'} + } else { + let string_token = Object.assign({}, token_types.STRING); + string_token.value = command.substring(word_start_index + 1, current_index); + advance(); + return string_token; + } + } +}; + +const token_types = { + LEFT_PAREN: {type: 'left_paren'}, + RIGHT_PAREN: {type: 'right_paren'}, + LEFT_BRACKET: {type: 'left_bracket'}, + RIGHT_BRACKET: {type: 'right_bracket'}, + COMMA: {type: 'comma'}, + DOT: {type: 'dot'}, + MINUS: {type: 'minus'}, + PLUS: {type: 'plus'}, + STAR: {type: 'star'}, + SLASH: {type: 'slash'}, + EQUALS: {type: 'equals'}, + EQUALS_EQUALS: {type: 'equals_equals'}, + NOT_EQUALS: {type: 'not_equals'}, + NOT: {type: 'not'}, + GREATER: {type: 'greater'}, + GREATER_OR_EQUAL: {type: 'greater_or_equal'}, + LESS: {type: 'less'}, + LESS_OR_EQUAL: {type: 'less_or_equal'}, + NUMERIC: {type: 'number', value: undefined}, + IDENTIFIER: {type: 'identifier', value: undefined}, + STRING: {type: 'string', value: undefined} +};