csv_plus_plus 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. checksums.yaml +7 -0
  2. data/lib/csv_plus_plus/cell.rb +51 -0
  3. data/lib/csv_plus_plus/code_section.rb +49 -0
  4. data/lib/csv_plus_plus/color.rb +22 -0
  5. data/lib/csv_plus_plus/expand.rb +18 -0
  6. data/lib/csv_plus_plus/google_options.rb +23 -0
  7. data/lib/csv_plus_plus/graph.rb +68 -0
  8. data/lib/csv_plus_plus/language/cell_value.tab.rb +333 -0
  9. data/lib/csv_plus_plus/language/code_section.tab.rb +443 -0
  10. data/lib/csv_plus_plus/language/compiler.rb +170 -0
  11. data/lib/csv_plus_plus/language/entities/boolean.rb +32 -0
  12. data/lib/csv_plus_plus/language/entities/cell_reference.rb +26 -0
  13. data/lib/csv_plus_plus/language/entities/entity.rb +70 -0
  14. data/lib/csv_plus_plus/language/entities/function.rb +33 -0
  15. data/lib/csv_plus_plus/language/entities/function_call.rb +25 -0
  16. data/lib/csv_plus_plus/language/entities/number.rb +34 -0
  17. data/lib/csv_plus_plus/language/entities/runtime_value.rb +27 -0
  18. data/lib/csv_plus_plus/language/entities/string.rb +29 -0
  19. data/lib/csv_plus_plus/language/entities/variable.rb +25 -0
  20. data/lib/csv_plus_plus/language/entities.rb +28 -0
  21. data/lib/csv_plus_plus/language/references.rb +53 -0
  22. data/lib/csv_plus_plus/language/runtime.rb +147 -0
  23. data/lib/csv_plus_plus/language/scope.rb +199 -0
  24. data/lib/csv_plus_plus/language/syntax_error.rb +61 -0
  25. data/lib/csv_plus_plus/lexer/lexer.rb +64 -0
  26. data/lib/csv_plus_plus/lexer/tokenizer.rb +65 -0
  27. data/lib/csv_plus_plus/lexer.rb +14 -0
  28. data/lib/csv_plus_plus/modifier.rb +124 -0
  29. data/lib/csv_plus_plus/modifier.tab.rb +921 -0
  30. data/lib/csv_plus_plus/options.rb +70 -0
  31. data/lib/csv_plus_plus/row.rb +42 -0
  32. data/lib/csv_plus_plus/template.rb +61 -0
  33. data/lib/csv_plus_plus/version.rb +6 -0
  34. data/lib/csv_plus_plus/writer/base_writer.rb +21 -0
  35. data/lib/csv_plus_plus/writer/csv.rb +31 -0
  36. data/lib/csv_plus_plus/writer/excel.rb +13 -0
  37. data/lib/csv_plus_plus/writer/google_sheet_builder.rb +173 -0
  38. data/lib/csv_plus_plus/writer/google_sheets.rb +139 -0
  39. data/lib/csv_plus_plus/writer/open_document.rb +14 -0
  40. data/lib/csv_plus_plus/writer.rb +25 -0
  41. data/lib/csv_plus_plus.rb +20 -0
  42. metadata +83 -0
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../entities'
4
+
5
+ module CSVPlusPlus
6
+ module Language
7
+ module Entities
8
+ # A basic building block of the abstract syntax tree (AST)
9
+ class Entity
10
+ attr_reader :id, :type
11
+
12
+ # initialize
13
+ def initialize(type, id: nil)
14
+ @type = type.to_sym
15
+ @id = id.downcase.to_sym if id
16
+ end
17
+
18
+ # ==
19
+ def ==(other)
20
+ self.class == other.class && @type == other.type && @id == other.id
21
+ end
22
+
23
+ # Respond to predicates that correspond to types like #boolean?, #string?, etc
24
+ def method_missing(method_name, *_arguments)
25
+ if method_name =~ /^(\w+)\?$/
26
+ t = ::Regexp.last_match(1)
27
+ a_type?(t) && @type == t.to_sym
28
+ else
29
+ super
30
+ end
31
+ end
32
+
33
+ # support predicates by type
34
+ def respond_to_missing?(method_name, *_arguments)
35
+ (method_name =~ /^(\w+)\?$/ && a_type?(::Regexp.last_match(1))) || super
36
+ end
37
+
38
+ private
39
+
40
+ def a_type?(str)
41
+ ::CSVPlusPlus::Language::TYPES.include?(str.to_sym)
42
+ end
43
+ end
44
+
45
+ # An entity that can take arguments
46
+ class EntityWithArguments < Entity
47
+ attr_reader :arguments
48
+
49
+ # initialize
50
+ def initialize(type, id: nil, arguments: [])
51
+ super(type, id:)
52
+ @arguments = arguments
53
+ end
54
+
55
+ # ==
56
+ def ==(other)
57
+ super && @arguments == other.arguments
58
+ end
59
+
60
+ protected
61
+
62
+ attr_writer :arguments
63
+
64
+ def arguments_to_s
65
+ @arguments.join(', ')
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative './entity'
4
+
5
+ module CSVPlusPlus
6
+ module Language
7
+ module Entities
8
+ # A function definition
9
+ class Function < EntityWithArguments
10
+ attr_reader :body
11
+
12
+ # Create a function
13
+ # @param id [Symbool, String] the name of the function - what it will be callable by
14
+ # @param arguments [Array(Symbol)]
15
+ # @param body [Entity]
16
+ def initialize(id, arguments, body)
17
+ super(:function, id:, arguments: arguments.map(&:to_sym))
18
+ @body = body
19
+ end
20
+
21
+ # to_s
22
+ def to_s
23
+ "def #{@id.to_s.upcase}(#{arguments_to_s}) #{@body}"
24
+ end
25
+
26
+ # ==
27
+ def ==(other)
28
+ super && @body == other.body
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CSVPlusPlus
4
+ module Language
5
+ module Entities
6
+ # A function call
7
+ class FunctionCall < EntityWithArguments
8
+ # initialize
9
+ def initialize(id, arguments)
10
+ super(:function_call, id:, arguments:)
11
+ end
12
+
13
+ # to_s
14
+ def to_s
15
+ "#{@id.to_s.upcase}(#{arguments_to_s})"
16
+ end
17
+
18
+ # ==
19
+ def ==(other)
20
+ super && @id == other.id
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CSVPlusPlus
4
+ module Language
5
+ module Entities
6
+ ##
7
+ # A number value
8
+ class Number < Entity
9
+ attr_reader :value
10
+
11
+ # initialize
12
+ def initialize(value)
13
+ super(:number)
14
+ @value =
15
+ if value.instance_of?(::String)
16
+ value.include?('.') ? Float(value) : Integer(value, 10)
17
+ else
18
+ value
19
+ end
20
+ end
21
+
22
+ # to_s
23
+ def to_s
24
+ @value.to_s
25
+ end
26
+
27
+ # ==
28
+ def ==(other)
29
+ super && value == other.value
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CSVPlusPlus
4
+ module Language
5
+ module Entities
6
+ ##
7
+ # A runtime value
8
+ #
9
+ # These are values which can be materialized at any point via the +resolve_fn+
10
+ # which takes an ExecutionContext as a param
11
+ class RuntimeValue < Entity
12
+ attr_reader :resolve_fn
13
+
14
+ # initialize
15
+ def initialize(resolve_fn)
16
+ super(:runtime_value)
17
+ @resolve_fn = resolve_fn
18
+ end
19
+
20
+ # to_s
21
+ def to_s
22
+ '(runtime_value)'
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CSVPlusPlus
4
+ module Language
5
+ module Entities
6
+ ##
7
+ # A string value
8
+ class String < Entity
9
+ attr_reader :value
10
+
11
+ # initialize
12
+ def initialize(value)
13
+ super(:string)
14
+ @value = value.gsub(/^"|"$/, '')
15
+ end
16
+
17
+ # to_s
18
+ def to_s
19
+ "\"#{@value}\""
20
+ end
21
+
22
+ # ==
23
+ def ==(other)
24
+ super && value == other.value
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CSVPlusPlus
4
+ module Language
5
+ module Entities
6
+ # A reference to a variable
7
+ class Variable < Entity
8
+ # initialize
9
+ def initialize(id)
10
+ super(:variable, id:)
11
+ end
12
+
13
+ # to_s
14
+ def to_s
15
+ "$$#{@id}"
16
+ end
17
+
18
+ # ==
19
+ def ==(other)
20
+ super && id == other.id
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'entities/boolean'
4
+ require_relative 'entities/cell_reference'
5
+ require_relative 'entities/entity'
6
+ require_relative 'entities/function'
7
+ require_relative 'entities/function_call'
8
+ require_relative 'entities/number'
9
+ require_relative 'entities/runtime_value'
10
+ require_relative 'entities/string'
11
+ require_relative 'entities/variable'
12
+
13
+ module CSVPlusPlus
14
+ module Language
15
+ TYPES = {
16
+ boolean: ::CSVPlusPlus::Language::Entities::Boolean,
17
+ cell_reference: ::CSVPlusPlus::Language::Entities::CellReference,
18
+ function: ::CSVPlusPlus::Language::Entities::Function,
19
+ function_call: ::CSVPlusPlus::Language::Entities::FunctionCall,
20
+ number: ::CSVPlusPlus::Language::Entities::Number,
21
+ runtime_value: ::CSVPlusPlus::Language::Entities::RuntimeValue,
22
+ string: ::CSVPlusPlus::Language::Entities::String,
23
+ variable: ::CSVPlusPlus::Language::Entities::Variable
24
+ }.freeze
25
+
26
+ public_constant :TYPES
27
+ end
28
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../graph'
4
+ require_relative './scope'
5
+
6
+ module CSVPlusPlus
7
+ module Language
8
+ # References in an AST that need to be resolved
9
+ class References
10
+ attr_accessor :functions, :variables
11
+
12
+ # Extract references from an AST. And return them in a new +References+ object
13
+ def self.extract(ast, code_section)
14
+ new.tap do |refs|
15
+ ::CSVPlusPlus::Graph.depth_first_search(ast) do |node|
16
+ next unless node.function_call? || node.variable?
17
+
18
+ refs.functions << node if function_reference?(node, code_section)
19
+ refs.variables << node if node.variable?
20
+ end
21
+ end
22
+ end
23
+
24
+ # Is the node a resolvable reference?
25
+ def self.function_reference?(node, code_section)
26
+ node.function_call? && (code_section.defined_function?(node.id) || ::BUILTIN_FUNCTIONS.key?(node.id))
27
+ end
28
+
29
+ private_class_method :function_reference?
30
+
31
+ # Create an object with empty references. The caller will build them up as it depth-first-searches
32
+ def initialize
33
+ @functions = []
34
+ @variables = []
35
+ end
36
+
37
+ # are there any references to be resolved?
38
+ def empty?
39
+ @functions.empty? && @variables.empty?
40
+ end
41
+
42
+ # to_s
43
+ def to_s
44
+ "References(functions: #{@functions}, variables: #{@variables})"
45
+ end
46
+
47
+ # ==
48
+ def ==(other)
49
+ @functions == other.functions && @variables == other.variables
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,147 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'entities'
4
+ require_relative 'syntax_error'
5
+ require 'tempfile'
6
+
7
+ ENTITIES = ::CSVPlusPlus::Language::Entities
8
+
9
+ RUNTIME_VARIABLES = {
10
+ rownum: ::ENTITIES::RuntimeValue.new(->(r) { ::ENTITIES::Number.new(r.row_index + 1) }),
11
+ cellnum: ::ENTITIES::RuntimeValue.new(->(r) { ::ENTITIES::Number.new(r.cell_index + 1) })
12
+ }.freeze
13
+
14
+ module CSVPlusPlus
15
+ module Language
16
+ ##
17
+ # The runtime state of the compiler (the current linenumber/row, cell, etc)
18
+ class Runtime
19
+ attr_reader :filename, :length_of_code_section, :length_of_csv_section, :length_of_original_file
20
+
21
+ attr_accessor :cell, :cell_index, :row_index, :line_number
22
+
23
+ # initialize
24
+ def initialize(input:, filename:)
25
+ @filename = filename || 'stdin'
26
+
27
+ init_input!(input)
28
+ init!(1)
29
+ end
30
+
31
+ # map over an unparsed file and keep track of line_number and row_index
32
+ def map_lines(lines, &block)
33
+ @line_number = 1
34
+ lines.map do |line|
35
+ block.call(line).tap { next_line! }
36
+ end
37
+ end
38
+
39
+ # map over a single row and keep track of the cell and it's index
40
+ def map_row(row, &block)
41
+ @cell_index = 0
42
+ row.map.with_index do |cell, index|
43
+ set_cell!(cell, index)
44
+ block.call(cell, index)
45
+ end
46
+ end
47
+
48
+ # map over all rows and keep track of row and line numbers
49
+ def map_rows(rows, cells_too: false, &block)
50
+ @row_index = 0
51
+ map_lines(rows) do |row|
52
+ if cells_too
53
+ # it's either CSV or a Row object
54
+ map_row((row.is_a?(::CSVPlusPlus::Row) ? row.cells : row), &block)
55
+ else
56
+ block.call(row)
57
+ end
58
+ end
59
+ end
60
+
61
+ # Increment state to the next line
62
+ def next_line!
63
+ @row_index += 1 unless @row_index.nil?
64
+ @line_number += 1
65
+ end
66
+
67
+ # Set the current cell and index
68
+ def set_cell!(cell, cell_index)
69
+ @cell = cell
70
+ @cell_index = cell_index
71
+ end
72
+
73
+ # Each time we run a parse on the input, call this so that the runtime state
74
+ # is set to it's default values
75
+ def init!(start_line_number_at)
76
+ @row_index = @cell_index = nil
77
+ @line_number = start_line_number_at
78
+ end
79
+
80
+ # to_s
81
+ def to_s
82
+ "Runtime(cell: #{@cell}, row_index: #{@row_index}, cell_index: #{@cell_index})"
83
+ end
84
+
85
+ # get the current (entity) value of a runtime value
86
+ def runtime_value(var_id)
87
+ if runtime_variable?(var_id)
88
+ ::RUNTIME_VARIABLES[var_id.to_sym].resolve_fn.call(self)
89
+ else
90
+ raise_syntax_error('Undefined variable', var_id)
91
+ end
92
+ end
93
+
94
+ # Is +var_id+ a runtime variable? (it's a static variable otherwise)
95
+ def runtime_variable?(var_id)
96
+ ::RUNTIME_VARIABLES.key?(var_id.to_sym)
97
+ end
98
+
99
+ # Called when an error is encoutered during parsing. It will construct a useful
100
+ # error with the current +@row/@cell_index+, +@line_number+ and +@filename+
101
+ def raise_syntax_error(message, bad_input, wrapped_error: nil)
102
+ raise(::CSVPlusPlus::Language::SyntaxError.new(message, bad_input, self, wrapped_error:))
103
+ end
104
+
105
+ # The currently available input for parsing. The tmp state will be re-written
106
+ # between parsing the code section and the CSV section
107
+ def input
108
+ @tmp
109
+ end
110
+
111
+ # We mutate the input over and over. It's ok because it's just a Tempfile
112
+ def rewrite_input!(data)
113
+ @tmp.truncate(0)
114
+ @tmp.write(data)
115
+ @tmp.rewind
116
+ end
117
+
118
+ # Clean up the Tempfile we're using for parsing
119
+ def cleanup!
120
+ return unless @tmp
121
+
122
+ @tmp.close
123
+ @tmp.unlink
124
+ @tmp = nil
125
+ end
126
+
127
+ private
128
+
129
+ def count_code_section_lines(lines)
130
+ eoc = ::CSVPlusPlus::Lexer::END_OF_CODE_SECTION
131
+ lines.include?(eoc) ? (lines.take_while { |l| l != eoc }).length + 1 : 0
132
+ end
133
+
134
+ def init_input!(input)
135
+ lines = (input || '').split(/\s*\n\s*/)
136
+ @length_of_original_file = lines.length
137
+ @length_of_code_section = count_code_section_lines(lines)
138
+ @length_of_csv_section = @length_of_original_file - @length_of_code_section
139
+
140
+ # we're gonna take our input file, write it to a tmp file then each
141
+ # step is gonna mutate that tmp file
142
+ @tmp = ::Tempfile.new
143
+ rewrite_input!(input)
144
+ end
145
+ end
146
+ end
147
+ end
@@ -0,0 +1,199 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../code_section'
4
+ require_relative '../graph'
5
+ require_relative './entities'
6
+ require_relative './references'
7
+ require_relative './syntax_error'
8
+
9
+ BUILTIN_FUNCTIONS = {
10
+ # =CELLREF(C) === =INDIRECT(CONCAT($$C, $$rownum))
11
+ cellref: ::CSVPlusPlus::Language::Entities::Function.new(
12
+ :cellref,
13
+ [:cell],
14
+ ::CSVPlusPlus::Language::Entities::FunctionCall.new(
15
+ :indirect,
16
+ [
17
+ ::CSVPlusPlus::Language::Entities::FunctionCall.new(
18
+ :concat,
19
+ [
20
+ ::CSVPlusPlus::Language::Entities::Variable.new(:cell),
21
+ ::CSVPlusPlus::Language::Entities::Variable.new(:rownum)
22
+ ]
23
+ )
24
+ ]
25
+ )
26
+ )
27
+ }.freeze
28
+
29
+ module CSVPlusPlus
30
+ module Language
31
+ # A class representing the scope of the current Template and responsible for resolving variables
32
+ # rubocop:disable Metrics/ClassLength
33
+ class Scope
34
+ attr_reader :code_section, :runtime
35
+
36
+ # initialize with a +Runtime+ and optional +CodeSection+
37
+ def initialize(runtime:, code_section: nil)
38
+ @code_section = code_section if code_section
39
+ @runtime = runtime
40
+ end
41
+
42
+ # Resolve all values in the ast of the current cell being processed
43
+ def resolve_cell_value
44
+ return unless (ast = @runtime.cell&.ast)
45
+
46
+ last_round = nil
47
+ loop do
48
+ refs = ::CSVPlusPlus::Language::References.extract(ast, @code_section)
49
+ return ast if refs.empty?
50
+
51
+ # TODO: throw an error here instead I think - basically we did a round and didn't make progress
52
+ return ast if last_round == refs
53
+
54
+ ast = resolve_functions(resolve_variables(ast, refs.variables), refs.functions)
55
+ end
56
+ end
57
+
58
+ # Set the +code_section+ and resolve all inner dependencies in it's variables and functions.
59
+ def code_section=(code_section)
60
+ @code_section = code_section
61
+
62
+ resolve_static_variables!
63
+ resolve_static_functions!
64
+ end
65
+
66
+ # to_s
67
+ def to_s
68
+ "Scope(code_section: #{@code_section}, runtime: #{@runtime})"
69
+ end
70
+
71
+ private
72
+
73
+ # Resolve all variable references defined statically in the code section
74
+ def resolve_static_variables!
75
+ variables = @code_section.variables
76
+ last_var_dependencies = {}
77
+ # TODO: might not need the infinite loop wrap
78
+ loop do
79
+ var_dependencies, resolution_order = variable_resolution_order(only_static_vars(variables))
80
+ return if var_dependencies == last_var_dependencies
81
+
82
+ # TODO: make the contract better here where we're not seting the variables of another class
83
+ @code_section.variables = resolve_dependencies(var_dependencies, resolution_order, variables)
84
+ last_var_dependencies = var_dependencies.clone
85
+ end
86
+ end
87
+
88
+ def only_static_vars(var_dependencies)
89
+ var_dependencies.reject { |k| @runtime.runtime_variable?(k) }
90
+ end
91
+
92
+ # Resolve all functions defined statically in the code section
93
+ def resolve_static_functions!
94
+ # TODO: I'm still torn if it's worth replacing function references
95
+ #
96
+ # my current theory is that if we resolve static functions befor processing each cell,
97
+ # overall compile time will be improved because there will be less to do for each cell
98
+ end
99
+
100
+ def resolve_functions(ast, refs)
101
+ refs.reduce(ast.dup) do |acc, elem|
102
+ function_replace(acc, elem.id, resolve_function(elem.id))
103
+ end
104
+ end
105
+
106
+ def resolve_variables(ast, refs)
107
+ refs.reduce(ast.dup) do |acc, elem|
108
+ variable_replace(acc, elem.id, resolve_variable(elem.id))
109
+ end
110
+ end
111
+
112
+ # Make a copy of the AST represented by +node+ and replace +fn_id+ with +replacement+ throughout
113
+ def function_replace(node, fn_id, replacement)
114
+ if node.function_call? && node.id == fn_id
115
+ apply_arguments(replacement, node)
116
+ elsif node.function_call?
117
+ arguments = node.arguments.map { |n| function_replace(n, fn_id, replacement) }
118
+ ::CSVPlusPlus::Language::Entities::FunctionCall.new(node.id, arguments)
119
+ else
120
+ node
121
+ end
122
+ end
123
+
124
+ def resolve_function(fn_id)
125
+ id = fn_id.to_sym
126
+ return @code_section.functions[id] if @code_section.defined_function?(id)
127
+
128
+ # this will throw a syntax error if it doesn't exist (which is what we want)
129
+ return ::BUILTIN_FUNCTIONS[id] if ::BUILTIN_FUNCTIONS.key?(id)
130
+
131
+ @runtime.raise_syntax_error('Unknown function', fn_id)
132
+ end
133
+
134
+ def apply_arguments(function, function_call)
135
+ i = 0
136
+ function.arguments.reduce(function.body.dup) do |ast, argument|
137
+ variable_replace(ast, argument, function_call.arguments[i]).tap do
138
+ i += 1
139
+ end
140
+ end
141
+ end
142
+
143
+ # Make a copy of the AST represented by +node+ and replace +var_id+ with +replacement+ throughout
144
+ def variable_replace(node, var_id, replacement)
145
+ if node.function_call?
146
+ arguments = node.arguments.map { |n| variable_replace(n, var_id, replacement) }
147
+ ::CSVPlusPlus::Language::Entities::FunctionCall.new(node.id, arguments)
148
+ elsif node.variable? && node.id == var_id
149
+ replacement
150
+ else
151
+ node
152
+ end
153
+ end
154
+
155
+ def resolve_variable(var_id)
156
+ id = var_id.to_sym
157
+ return @code_section.variables[id] if @code_section.defined_variable?(id)
158
+
159
+ # this will throw a syntax error if it doesn't exist (which is what we want)
160
+ @runtime.runtime_value(id)
161
+ end
162
+
163
+ def check_unbound_vars(dependencies, variables)
164
+ unbound_vars = dependencies.values.flatten - variables.keys
165
+ return if unbound_vars.empty?
166
+
167
+ @runtime.raise_syntax_error('Undefined variables', unbound_vars.map(&:to_s).join(', '))
168
+ end
169
+
170
+ def variable_resolution_order(variables)
171
+ # we have a hash of variables => ASTs but they might have references to each other, so
172
+ # we need to interpolate them first (before interpolating the cell values)
173
+ var_dependencies = ::CSVPlusPlus::Graph.dependency_graph(variables, @runtime)
174
+ # are there any references that we don't have variables for? (undefined variable)
175
+ check_unbound_vars(var_dependencies, variables)
176
+
177
+ # a topological sort will give us the order of dependencies
178
+ [var_dependencies, ::CSVPlusPlus::Graph.topological_sort(var_dependencies)]
179
+ # TODO: don't expose this exception directly to the caller
180
+ rescue ::TSort::Cyclic
181
+ @runtime.raise_syntax_error('Cyclic variable dependency detected', var_refs.keys)
182
+ end
183
+
184
+ def resolve_dependencies(var_dependencies, resolution_order, variables)
185
+ {}.tap do |resolved_vars|
186
+ # for each var and each dependency it has, build up and mutate resolved_vars
187
+ resolution_order.each do |var|
188
+ resolved_vars[var] = variables[var].dup
189
+
190
+ var_dependencies[var].each do |dependency|
191
+ resolved_vars[var] = variable_replace(resolved_vars[var], dependency, variables[dependency])
192
+ end
193
+ end
194
+ end
195
+ end
196
+ end
197
+ # rubocop:enable Metrics/ClassLength
198
+ end
199
+ end