From 9803b31f782337e4e53770838528f63e0b42cc5b Mon Sep 17 00:00:00 2001 From: cryptogopher Date: Wed, 22 Jan 2020 22:54:54 +0100 Subject: [PATCH] Integarted FormulaBuilder into Formula --- config/locales/en.yml | 7 +- lib/body_tracking/formula.rb | 81 +--------- lib/body_tracking/formula_builder.rb | 230 +++++++++++++++++++++++++++ 3 files changed, 239 insertions(+), 79 deletions(-) create mode 100644 lib/body_tracking/formula_builder.rb diff --git a/config/locales/en.yml b/config/locales/en.yml index 4e8219a..95286f7 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -33,11 +33,10 @@ en: parent: parent_domain_mismatch: 'parent quantity has to be in the same domain' formula: - invalid_formula: 'interpretation failed: %{msg}' - unparsable_formula: 'cannot be parsed' - disallowed_token: 'includes disallowed token "%{token}" (of type: %{ttype})' + disallowed_syntax: 'cannot be parsed: %{syntax}' + disallowed_token: 'includes disallowed token: "%{token}"' disallowed_keyword: 'includes disallowed keyword: "%{keyword}"' - disallowed_method: 'includes disallowed method call: %{method}' + disallowed_method: 'includes disallowed method call: "%{method}"' unknown_quantity: 'contains undefined quantity: %{quantity}' body_trackers: index: diff --git a/lib/body_tracking/formula.rb b/lib/body_tracking/formula.rb index e08911d..21747ae 100644 --- a/lib/body_tracking/formula.rb +++ b/lib/body_tracking/formula.rb @@ -1,15 +1,11 @@ module BodyTracking module Formula - require 'ripper' - QUANTITY_TTYPES = [:on_ident, :on_tstring_content, :on_const] - FUNCTIONS = ['abs', 'nil?'] - class InvalidFormula < RuntimeError; end class Formula def initialize(project, formula) @project_quantities = Quantity.where(project: project) @formula = formula - @paramed_formula = nil + @parts = nil @quantities = nil end @@ -20,84 +16,18 @@ module BodyTracking # working test vectors: # ((Energy-Calculated)/Energy).abs > 0.2 # Fats.nil? || Fats/Proteins > 2 - errors = [] - # 1st: check if formula is syntactically valid Ruby code - begin - eval("-> { #{@formula} }") - rescue ScriptError => e - errors << [:invalid_formula, {msg: e.message}] - end + parser = FormulaBuilder.new(@formula) + identifiers, parts = parser.parse + errors = parser.errors - # 2nd: check if formula contains only allowed token types - # 3rd: check for disallowed function calls - identifiers = [] - disallowed = Hash.new { |h,k| h[k] = Set.new } - - stree = [Ripper.sexp(@formula)] - errors << [:unparsable_formula, {}] unless stree.first - - while stree.first - ttype, token, *rest = stree.shift - case ttype - when :program, :args_add_block, :paren - stree.unshift(*token) - when :binary - operator, token2 = rest - stree.unshift(token, token2) - when :method_add_arg - stree.unshift(token, *rest) - when :call - stree.unshift(token) - dot, method = rest - ftype, fname, floc = method - disallowed[:function] << fname unless FUNCTIONS.include?(fname) - when :fcall - ftype, fname, floc = token - disallowed[:function] << fname - when :vcall - ftype, fname, floc = token - identifiers << fname - when :arg_paren - stree.unshift(token) - when :var_ref - vtype, vname, vloc = token - case vtype - when :@const - identifiers << vname - when :@kw - disallowed[:keyword] << token if vname != 'nil' - end - when :@int, :@float - else - errors << [:disallowed_token, {token: token, ttype: ttype}] - end - end - - disallowed[:function].each { |f| errors << [:disallowed_function, {function: f}] } - disallowed[:keyword].each { |k| errors << [:disallowed_keyword, {keyword: k}] } - - # 4th: check if identifiers used in formula correspond to existing quantities - identifiers.uniq! quantities = @project_quantities.where(name: identifiers) quantities_names = quantities.pluck(:name) (identifiers - quantities_names).each do |q| errors << [:unknown_quantity, {quantity: q}] end - if errors.empty? - @quantities = quantities - @paramed_formula = Ripper.lex(@formula).map do |*, ttype, token| - if QUANTITY_TTYPES.include?(ttype) && quantities_names.include?(token) - "params['#{token}'][_index]" - else - token - end - end.join - @paramed_formula = - "params.values.first.each_with_index.map { |*, _index| #{@paramed_formula} }" - end - + @parts, @quantities = parts, quantities if errors.empty? errors end @@ -111,6 +41,7 @@ module BodyTracking @quantities.to_a end + #"params.values.first.each_with_index.map { |*, _index| #{@paramed_formula} }" def calculate(inputs) raise RuntimeError, 'Invalid formula' unless self.valid? diff --git a/lib/body_tracking/formula_builder.rb b/lib/body_tracking/formula_builder.rb new file mode 100644 index 0000000..b1745c8 --- /dev/null +++ b/lib/body_tracking/formula_builder.rb @@ -0,0 +1,230 @@ +module BodyTracking + module FormulaBuilder + require 'ripper' + require 'set' + + class FormulaBuilder < Ripper::SexpBuilder + def initialize(*args) + super(*args) + @disallowed = Hash.new { |h,k| h[k] = Set.new } + @identifiers = Set.new + @parts = [] + end + + def errors + @errors = [] + @disallowed.each { |k, v| v.each { |e| @errors << ["disallowed_#{k}", {k => e} ] } } + @errors + end + + private + + DECIMAL_METHODS = ['abs', 'nil?'] + QUANTITY_METHODS = ['all', 'lastBefore'] + METHODS = DECIMAL_METHODS + QUANTITY_METHODS + + events = private_instance_methods(false).grep(/\Aon_/) {$'.to_sym} + (PARSER_EVENTS - events).each do |event| + module_eval(<<-End, __FILE__, __LINE__ + 1) + def on_#{event}(*args) + @disallowed[:token] << args.to_s + ' [#{event}]' + [:bt_unimplemented, args] + end + End + end + + def on_parse_error(error) + @disallowed[:syntax] << error + end + + def on_program(stmts) + @parts << {type: :indexed, content: join_stmts(stmts)} + [@identifiers, @parts] + end + + def on_string_content + '' + end + + def on_string_add(str, new_str) + str << new_str + end + + def on_string_literal(str) + @identifiers << str + [:bt_quantity, str] + end + + def on_args_new + [] + end + + def on_args_add(args, new_arg) + args << new_arg + end + + def on_args_add_block(args, block) + raise NotImplementedError if block + args + end + + def on_arg_paren(args) + "(" << + args.map do |arg| + ttype, token = arg + case ttype + when :bt_quantity + "quantities['#{token}']" + when :bt_expression + @parts << {type: :indexed, content: token} + "parts[#{@parts.length - 1}]" + else + raise NotImplementedError + end + end.join(',') << + ")" + end + + def on_stmts_new + [] + end + + def on_stmts_add(stmts, new_stmt) + stmts << new_stmt + end + + def on_paren(stmts) + [ + :bt_expression, + "(" << join_stmts(stmts) << ")" + ] + end + + def on_call(left, dot, right) + method, mtype = + case right[0] + when :bt_ident + case + when DECIMAL_METHODS.include?(right[1]) + [right[1], :numeric_method] + when QUANTITY_METHODS.include?(right[1]) + [right[1], :quantity_method] + else + @disallowed[:method] << right[1] + [right[1], :unknown_method] + end + else + raise NotImplementedError + end + + case left[0] + when :bt_quantity + if mtype == :quantity_method + part_index = @parts.length + @parts << {type: :unindexed, + content: "quantities['#{left[1]}']#{dot.to_s}#{method}"} + [:bt_quantity_method_call, "parts[#{part_index}]", part_index] + else + [:bt_numeric_method_call, "quantities['#{left[1]}'][_index]#{dot.to_s}#{method}"] + end + when :bt_quantity_method_call + if mtype == :quantity_method + @parts[left[2]][:content] << "#{dot.to_s}#{method}" + left + else + [:bt_numeric_method_call, "#{left[1]}#{dot.to_s}#{method}"] + end + when :bt_numeric_method_call + if mtype == :quantity_method + # TODO: add error reporting + raise NotImplementedError + else + [:bt_numeric_method_call, "#{left[1]}#{dot.to_s}#{method}"] + end + else + raise NotImplementedError + end + end + + def on_fcall(token) + @disallowed[:method] = token[1] + [:bt_numeric_method_call, token[1]] + end + + def on_vcall(token) + case token[0] + when :bt_ident + @identifiers << token[1] + [:bt_quantity, token[1]] + else + raise NotImplementedError + end + end + + def on_method_add_arg(method, paren) + case method[0] + when :bt_quantity_method_call + @parts[method[2]][:content] << paren + method + when :bt_numeric_method_call + [:bt_numeric_method_call, "#{method[1]}#{paren}"] + else + raise NotImplementedError + end + end + + def on_binary(left, op, right) + [ + :bt_expression, + [left, right].map do |side| + side[0] == :bt_quantity ? "quantities['#{side[1]}'][_index]" : "#{side[1]}" + end.join(op.to_s) + ] + end + + def on_var_ref(var_ref) + var_ref[0] == :bt_quantity ? var_ref : raise(NotImplementedError) + end + + silenced_events = [:lparen, :rparen, :op, :period, :sp, :int, :float, + :tstring_beg, :tstring_end] + (SCANNER_EVENTS - silenced_events).each do |event| + module_eval(<<-End, __FILE__, __LINE__ + 1) + def on_#{event}(token) + @disallowed[:token] << token + ' [#{event}]' + [:bt_unimplemented, token] + end + End + end + + def on_const(token) + @identifiers << token + [:bt_quantity, token] + end + + def on_ident(token) + [:bt_ident, token] + end + + def on_kw(token) + @disallowed[:keyword] << token unless token == 'nil' + end + + def on_tstring_content(token) + token + end + + def join_stmts(stmts) + stmts.map do |stmt| + ttype, token = stmt + case ttype + when :bt_expression + token + else + raise NotImplementedError + end + end.join(';') + end + end + end +end