csv_plus_plus 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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