csv_plus_plus 0.1.2 → 0.1.3

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 (79) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -2
  3. data/{CHANGELOG.md → docs/CHANGELOG.md} +9 -0
  4. data/lib/csv_plus_plus/benchmarked_compiler.rb +70 -20
  5. data/lib/csv_plus_plus/cell.rb +46 -24
  6. data/lib/csv_plus_plus/cli.rb +23 -13
  7. data/lib/csv_plus_plus/cli_flag.rb +1 -2
  8. data/lib/csv_plus_plus/color.rb +32 -7
  9. data/lib/csv_plus_plus/compiler.rb +82 -60
  10. data/lib/csv_plus_plus/entities/ast_builder.rb +27 -43
  11. data/lib/csv_plus_plus/entities/boolean.rb +18 -9
  12. data/lib/csv_plus_plus/entities/builtins.rb +23 -9
  13. data/lib/csv_plus_plus/entities/cell_reference.rb +200 -29
  14. data/lib/csv_plus_plus/entities/date.rb +38 -5
  15. data/lib/csv_plus_plus/entities/entity.rb +27 -61
  16. data/lib/csv_plus_plus/entities/entity_with_arguments.rb +57 -0
  17. data/lib/csv_plus_plus/entities/function.rb +23 -11
  18. data/lib/csv_plus_plus/entities/function_call.rb +24 -9
  19. data/lib/csv_plus_plus/entities/number.rb +24 -10
  20. data/lib/csv_plus_plus/entities/runtime_value.rb +22 -5
  21. data/lib/csv_plus_plus/entities/string.rb +19 -6
  22. data/lib/csv_plus_plus/entities/variable.rb +16 -4
  23. data/lib/csv_plus_plus/entities.rb +20 -13
  24. data/lib/csv_plus_plus/error/error.rb +11 -1
  25. data/lib/csv_plus_plus/error/formula_syntax_error.rb +1 -0
  26. data/lib/csv_plus_plus/error/modifier_syntax_error.rb +53 -5
  27. data/lib/csv_plus_plus/error/modifier_validation_error.rb +34 -14
  28. data/lib/csv_plus_plus/error/syntax_error.rb +22 -9
  29. data/lib/csv_plus_plus/error/writer_error.rb +8 -0
  30. data/lib/csv_plus_plus/error.rb +1 -0
  31. data/lib/csv_plus_plus/google_api_client.rb +7 -2
  32. data/lib/csv_plus_plus/google_options.rb +23 -18
  33. data/lib/csv_plus_plus/lexer/lexer.rb +8 -4
  34. data/lib/csv_plus_plus/lexer/tokenizer.rb +6 -1
  35. data/lib/csv_plus_plus/lexer.rb +24 -0
  36. data/lib/csv_plus_plus/modifier/conditional_formatting.rb +1 -0
  37. data/lib/csv_plus_plus/modifier/data_validation.rb +138 -0
  38. data/lib/csv_plus_plus/modifier/expand.rb +61 -0
  39. data/lib/csv_plus_plus/modifier/google_sheet_modifier.rb +133 -0
  40. data/lib/csv_plus_plus/modifier/modifier.rb +222 -0
  41. data/lib/csv_plus_plus/modifier/modifier_validator.rb +243 -0
  42. data/lib/csv_plus_plus/modifier/rubyxl_modifier.rb +84 -0
  43. data/lib/csv_plus_plus/modifier.rb +82 -158
  44. data/lib/csv_plus_plus/options.rb +64 -19
  45. data/lib/csv_plus_plus/parser/cell_value.tab.rb +5 -5
  46. data/lib/csv_plus_plus/parser/code_section.tab.rb +8 -13
  47. data/lib/csv_plus_plus/parser/modifier.tab.rb +17 -23
  48. data/lib/csv_plus_plus/row.rb +53 -12
  49. data/lib/csv_plus_plus/runtime/can_define_references.rb +87 -0
  50. data/lib/csv_plus_plus/runtime/can_resolve_references.rb +209 -0
  51. data/lib/csv_plus_plus/runtime/graph.rb +68 -0
  52. data/lib/csv_plus_plus/runtime/position_tracker.rb +231 -0
  53. data/lib/csv_plus_plus/runtime/references.rb +110 -0
  54. data/lib/csv_plus_plus/runtime/runtime.rb +126 -0
  55. data/lib/csv_plus_plus/runtime.rb +34 -191
  56. data/lib/csv_plus_plus/source_code.rb +66 -0
  57. data/lib/csv_plus_plus/template.rb +62 -35
  58. data/lib/csv_plus_plus/version.rb +2 -1
  59. data/lib/csv_plus_plus/writer/base_writer.rb +30 -5
  60. data/lib/csv_plus_plus/writer/csv.rb +11 -9
  61. data/lib/csv_plus_plus/writer/excel.rb +9 -2
  62. data/lib/csv_plus_plus/writer/file_backer_upper.rb +1 -0
  63. data/lib/csv_plus_plus/writer/google_sheet_builder.rb +71 -23
  64. data/lib/csv_plus_plus/writer/google_sheets.rb +79 -29
  65. data/lib/csv_plus_plus/writer/open_document.rb +6 -1
  66. data/lib/csv_plus_plus/writer/rubyxl_builder.rb +103 -30
  67. data/lib/csv_plus_plus/writer.rb +39 -9
  68. data/lib/csv_plus_plus.rb +29 -12
  69. metadata +18 -14
  70. data/lib/csv_plus_plus/can_define_references.rb +0 -88
  71. data/lib/csv_plus_plus/can_resolve_references.rb +0 -8
  72. data/lib/csv_plus_plus/data_validation.rb +0 -138
  73. data/lib/csv_plus_plus/expand.rb +0 -20
  74. data/lib/csv_plus_plus/graph.rb +0 -62
  75. data/lib/csv_plus_plus/references.rb +0 -68
  76. data/lib/csv_plus_plus/scope.rb +0 -196
  77. data/lib/csv_plus_plus/validated_modifier.rb +0 -164
  78. data/lib/csv_plus_plus/writer/google_sheet_modifier.rb +0 -77
  79. data/lib/csv_plus_plus/writer/rubyxl_modifier.rb +0 -59
@@ -0,0 +1,209 @@
1
+ # typed: false
2
+ # frozen_string_literal: true
3
+
4
+ module CSVPlusPlus
5
+ module Runtime
6
+ # Methods for resolving functions and variables. These should be included onto a class that has +@variables+ and
7
+ # +@functions+ instance variables.
8
+ module CanResolveReferences
9
+ # Resolve all values in the ast of the current cell being processed
10
+ #
11
+ # @return [Entity]
12
+ def resolve_cell_value
13
+ return unless (ast = @cell&.ast)
14
+
15
+ last_round = nil
16
+ loop do
17
+ refs = ::CSVPlusPlus::Runtime::References.extract(ast, self)
18
+ return ast if refs.empty?
19
+
20
+ # TODO: throw an error here instead I think - basically we did a round and didn't make progress
21
+ return ast if last_round == refs
22
+
23
+ ast = resolve_functions(resolve_variables(ast, refs.variables), refs.functions)
24
+ end
25
+ end
26
+
27
+ # Bind +var_id+ to the current cell
28
+ #
29
+ # @param var_id [Symbol] The name of the variable to bind the cell reference to
30
+ #
31
+ # @return [CellReference]
32
+ def bind_variable_to_cell(var_id)
33
+ def_variable(
34
+ var_id,
35
+ ::CSVPlusPlus::Entities::CellReference.new(
36
+ cell_index: @cell_index,
37
+ row_index: @row_index
38
+ )
39
+ )
40
+ end
41
+
42
+ # Bind +var_id+ relative to an ![[expand]] modifier.
43
+ #
44
+ # @param var_id [Symbol] The name of the variable to bind the cell reference to
45
+ # @param expand [Expand] The expand where the variable is accessible (where it will be bound relative to)
46
+ #
47
+ # @return [CellReference]
48
+ def bind_variable_in_expand(var_id, expand)
49
+ def_variable(
50
+ var_id,
51
+ ::CSVPlusPlus::Entities::CellReference.new(
52
+ scoped_to_expand: expand,
53
+ cell_index: @cell_index
54
+ )
55
+ )
56
+ end
57
+
58
+ # Variables outside of an ![[expand=...] are always in scope. If it's defined within an expand then things
59
+ # get trickier because the variable is only in scope while we're processing cells within that expand.
60
+ #
61
+ # @param var_id [Symbol] The variable's identifier that we are checking if it's in scope
62
+ #
63
+ # @return [boolean]
64
+ def in_scope?(var_id)
65
+ value = @variables[var_id]
66
+
67
+ raise_modifier_syntax_error('Undefined variable reference', var_id.to_s) if value.nil?
68
+
69
+ expand = value.type == ::CSVPlusPlus::Entities::Type::CellReference && value.scoped_to_expand
70
+ return true unless expand
71
+
72
+ unless expand.starts_at
73
+ raise(::CSVPlusPlus::Error::Error, 'Must call Template.expand_rows! before checking the scope of expands.')
74
+ end
75
+
76
+ @row_index >= expand.starts_at && (expand.ends_at.nil? || row_index <= expand.ends_at)
77
+ end
78
+
79
+ private
80
+
81
+ # Resolve all variable references defined statically in the code section
82
+ # def resolve_static_variables!
83
+ # last_var_dependencies = {}
84
+ # loop do
85
+ # var_dependencies, resolution_order = variable_resolution_order(only_static_vars(variables))
86
+ # return if var_dependencies == last_var_dependencies
87
+ #
88
+ # # TODO: make the contract better here
89
+ # @variables = resolve_dependencies(var_dependencies, resolution_order, variables)
90
+ # last_var_dependencies = var_dependencies.clone
91
+ # end
92
+ # end
93
+ #
94
+ # def only_static_vars(var_dependencies)
95
+ # var_dependencies.reject { |k| @runtime.builtin_variable?(k) }
96
+ # end
97
+
98
+ def resolve_functions(ast, refs)
99
+ refs.reduce(ast.dup) do |acc, elem|
100
+ function_replace(acc, elem.id, resolve_function(elem.id))
101
+ end
102
+ end
103
+
104
+ def resolve_variables(ast, refs)
105
+ refs.reduce(ast.dup) do |acc, elem|
106
+ variable_replace(acc, elem.id, resolve_variable(elem.id))
107
+ end
108
+ end
109
+
110
+ # Make a copy of the AST represented by +node+ and replace +fn_id+ with +replacement+ throughout
111
+ # rubocop:disable Metrics/MethodLength
112
+ def function_replace(node, fn_id, replacement)
113
+ if node.type == ::CSVPlusPlus::Entities::Type::FunctionCall && node.id == fn_id
114
+ call_function_or_builtin(replacement, node)
115
+ elsif node.type == ::CSVPlusPlus::Entities::Type::FunctionCall
116
+ # not our function, but continue our depth first search on it
117
+ ::CSVPlusPlus::Entities::FunctionCall.new(
118
+ node.id,
119
+ node.arguments.map { |n| function_replace(n, fn_id, replacement) },
120
+ infix: node.infix
121
+ )
122
+ else
123
+ node
124
+ end
125
+ end
126
+ # rubocop:enable Metrics/MethodLength
127
+
128
+ def resolve_function(fn_id)
129
+ id = fn_id.to_sym
130
+ return @functions[id] if defined_function?(id)
131
+
132
+ ::CSVPlusPlus::Entities::Builtins::FUNCTIONS[id]
133
+ end
134
+
135
+ def call_function_or_builtin(function_or_builtin, function_call)
136
+ if function_or_builtin.type == ::CSVPlusPlus::Entities::Type::Function
137
+ call_function(function_or_builtin, function_call)
138
+ else
139
+ function_or_builtin.resolve_fn.call(self, function_call.arguments)
140
+ end
141
+ end
142
+
143
+ def call_function(function, function_call)
144
+ i = 0
145
+ function.arguments.reduce(function.body.dup) do |ast, argument|
146
+ variable_replace(ast, argument, function_call.arguments[i]).tap do
147
+ i += 1
148
+ end
149
+ end
150
+ end
151
+
152
+ # Make a copy of the AST represented by +node+ and replace +var_id+ with +replacement+ throughout
153
+ def variable_replace(node, var_id, replacement)
154
+ if node.type == ::CSVPlusPlus::Entities::Type::FunctionCall
155
+ arguments = node.arguments.map { |n| variable_replace(n, var_id, replacement) }
156
+ # TODO: refactor these places where we copy functions... it's brittle with the kwargs
157
+ ::CSVPlusPlus::Entities::FunctionCall.new(node.id, arguments, infix: node.infix)
158
+ elsif node.type == ::CSVPlusPlus::Entities::Type::Variable && node.id == var_id
159
+ replacement
160
+ else
161
+ node
162
+ end
163
+ end
164
+
165
+ def resolve_variable(var_id)
166
+ id = var_id.to_sym
167
+ return @variables[id] if defined_variable?(id)
168
+
169
+ raise_formula_syntax_error('Undefined variable', var_id) unless builtin_variable?(var_id)
170
+
171
+ ::CSVPlusPlus::Entities::Builtins::VARIABLES[var_id.to_sym].resolve_fn.call(self)
172
+ end
173
+
174
+ # def check_unbound_vars(dependencies, variables)
175
+ # unbound_vars = dependencies.values.flatten - variables.keys
176
+ # return if unbound_vars.empty?
177
+ #
178
+ # raise_formula_syntax_error('Undefined variables', unbound_vars.map(&:to_s).join(', '))
179
+ # end
180
+
181
+ # def variable_resolution_order(variables)
182
+ # # we have a hash of variables => ASTs but they might have references to each other, so
183
+ # # we need to interpolate them first (before interpolating the cell values)
184
+ # var_dependencies = ::CSVPlusPlus::Graph.dependency_graph(variables, @runtime)
185
+ # # are there any references that we don't have variables for? (undefined variable)
186
+ # check_unbound_vars(var_dependencies, variables)
187
+ #
188
+ # # a topological sort will give us the order of dependencies
189
+ # [var_dependencies, ::CSVPlusPlus::Graph.topological_sort(var_dependencies)]
190
+ # # TODO: don't expose this exception directly to the caller
191
+ # rescue ::TSort::Cyclic
192
+ # @runtime.raise_formula_syntax_error('Cyclic variable dependency detected', var_refs.keys)
193
+ # end
194
+
195
+ # def resolve_dependencies(var_dependencies, resolution_order, variables)
196
+ # {}.tap do |resolved_vars|
197
+ # # for each var and each dependency it has, build up and mutate resolved_vars
198
+ # resolution_order.each do |var|
199
+ # resolved_vars[var] = variables[var].dup
200
+ #
201
+ # var_dependencies[var].each do |dependency|
202
+ # resolved_vars[var] = variable_replace(resolved_vars[var], dependency, variables[dependency])
203
+ # end
204
+ # end
205
+ # end
206
+ # end
207
+ end
208
+ end
209
+ end
@@ -0,0 +1,68 @@
1
+ # typed: false
2
+ # frozen_string_literal: true
3
+
4
+ require 'tsort'
5
+
6
+ module CSVPlusPlus
7
+ module Runtime
8
+ # Graph ordering and searching functions
9
+ module Graph
10
+ # Get a list of all variables references in a given +ast+
11
+ # TODO: this is only used in one place - refactor it
12
+ def self.variable_references(ast, runtime, include_runtime_variables: false)
13
+ depth_first_search(ast) do |node|
14
+ next unless node.type == ::CSVPlusPlus::Entities::Type::Variable
15
+
16
+ node.id if !runtime.builtin_variable?(node.id) || include_runtime_variables
17
+ end
18
+ end
19
+
20
+ # Create a dependency graph of +variables+
21
+ def self.dependency_graph(variables, runtime)
22
+ ::CSVPlusPlus::Runtime::Graph::DependencyGraph[
23
+ variables.map { |var_id, ast| [var_id, variable_references(ast, runtime)] }
24
+ ]
25
+ end
26
+
27
+ # TODO: I don't think we use this anymore - it was useful when I wanted to resolve variables in their dependency
28
+ # order
29
+ #
30
+ # Perform a topological sort on a +DependencyGraph+. A toplogical sort is noteworthy
31
+ # because it will give us the order in which we need to resolve our variable dependencies.
32
+ #
33
+ # Given this dependency graph:
34
+ #
35
+ # { a: [b c], b: [c], c: [d], d: [] }
36
+ #
37
+ # it will return:
38
+ #
39
+ # [d, c, b, a]
40
+ #
41
+ def self.topological_sort(dependencies)
42
+ dependencies.tsort
43
+ end
44
+
45
+ # Do a DFS on an AST starting at +node+
46
+ def self.depth_first_search(node, accum = [], &)
47
+ ret = yield(node)
48
+ accum << ret unless ret.nil?
49
+
50
+ return accum unless node.type == ::CSVPlusPlus::Entities::Type::FunctionCall
51
+
52
+ node.arguments.each { |n| depth_first_search(n, accum, &) }
53
+ accum
54
+ end
55
+
56
+ # A dependency graph represented as a +Hash+ which will be used by our +topological_sort+ function
57
+ class DependencyGraph < Hash
58
+ include ::TSort
59
+ alias tsort_each_node each_key
60
+
61
+ # sort each child
62
+ def tsort_each_child(node, &)
63
+ fetch(node).each(&)
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,231 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module CSVPlusPlus
5
+ module Runtime
6
+ # Functions needed to track all of the runtime pointers: current line number, current row number, current cell, etc.
7
+ # rubocop:disable Metrics/ModuleLength
8
+ module PositionTracker
9
+ extend ::T::Sig
10
+
11
+ sig { params(cell: ::T.nilable(::CSVPlusPlus::Cell)).returns(::T.nilable(::CSVPlusPlus::Cell)) }
12
+ attr_writer :cell
13
+
14
+ sig { params(cell_index: ::T.nilable(::Integer)).returns(::T.nilable(::Integer)) }
15
+ attr_writer :cell_index
16
+
17
+ sig { params(line_number: ::T.nilable(::Integer)).returns(::T.nilable(::Integer)) }
18
+ attr_writer :line_number
19
+
20
+ sig { params(row_index: ::T.nilable(::Integer)).returns(::T.nilable(::Integer)) }
21
+ attr_writer :row_index
22
+
23
+ sig { returns(::CSVPlusPlus::Cell) }
24
+ # The current cell index. This will only be set when processing the CSV section
25
+ #
26
+ # @return [Cell]
27
+ def cell
28
+ @cell ||= ::T.let(nil, ::T.nilable(::CSVPlusPlus::Cell))
29
+ assert_initted!(@cell)
30
+ end
31
+
32
+ sig { returns(::Integer) }
33
+ # The current CSV cell index.
34
+ #
35
+ # This will only be set when processing the CSV section and will throw an exception otherwise. It is up to the
36
+ # caller (the compiler) to make sure it's called in the context of a compilation stage and/or a
37
+ # +#map_row+/+#map_rows+/+#map_lines+
38
+ #
39
+ # @return [Integer]
40
+ def cell_index
41
+ @cell_index ||= ::T.let(nil, ::T.nilable(::Integer))
42
+ assert_initted!(@cell_index)
43
+ end
44
+
45
+ sig { returns(::Integer) }
46
+ # The current CSV row index. This will only be set when processing the CSV section
47
+ #
48
+ # This will only be set when processing the CSV section and will throw an exception otherwise. It is up to the
49
+ # caller (the compiler) to make sure it's called in the context of a compilation stage and/or a
50
+ # +#map_row+/+#map_rows+/+#map_lines+
51
+ #
52
+ # @return [Integer]
53
+ def row_index
54
+ @row_index ||= ::T.let(nil, ::T.nilable(::Integer))
55
+ assert_initted!(@row_index)
56
+ end
57
+
58
+ sig { returns(::Integer) }
59
+ # The current line number being processed. The line number is based on the entire file, irregardless of if it's
60
+ # parsing the code section or the CSV section
61
+ #
62
+ # This will only be set when processing the csvpp file and will throw an exception otherwise. It is up to the
63
+ # caller (the compiler) to make sure it's called in the context of a compilation stage and/or a
64
+ # +#map_row+/+#map_rows+/+#map_lines+
65
+ #
66
+ # @return [Integer]
67
+ def line_number
68
+ @line_number ||= ::T.let(nil, ::T.nilable(::Integer))
69
+ assert_initted!(@line_number)
70
+ end
71
+
72
+ sig { void }
73
+ # Clean up the Tempfile we're using for parsing
74
+ def cleanup!
75
+ input&.close
76
+ input&.unlink
77
+ end
78
+
79
+ sig { returns(::T.nilable(::Tempfile)) }
80
+ # The currently available input for parsing. The tmp state will be re-written
81
+ # between parsing the code section and the CSV section
82
+ #
83
+ # @return [::Tempfile]
84
+ def input
85
+ @input ||= ::T.let(::Tempfile.new, ::T.nilable(::Tempfile))
86
+ end
87
+
88
+ sig do
89
+ type_parameters(:I, :O).params(
90
+ lines: ::T::Enumerable[::T.type_parameter(:I)],
91
+ block: ::T.proc.params(args0: ::T.type_parameter(:I)).returns(::T.type_parameter(:O))
92
+ ).returns(::T::Array[::T.type_parameter(:O)])
93
+ end
94
+ # Map over a csvpp file and keep track of line_number and row_index
95
+ #
96
+ # @param lines [Array]
97
+ #
98
+ # @return [Array]
99
+ def map_lines(lines, &block)
100
+ self.line_number = 1
101
+ lines.map do |line|
102
+ ret = block.call(line)
103
+ next_line!
104
+ ret
105
+ end
106
+ end
107
+
108
+ sig do
109
+ type_parameters(:I, :O)
110
+ .params(
111
+ row: ::T::Enumerable[::T.all(::T.type_parameter(:I), ::Object)],
112
+ block: ::T.proc.params(
113
+ cell: ::T.all(::T.type_parameter(:I), ::Object),
114
+ index: ::Integer
115
+ ).returns(::T.type_parameter(:O))
116
+ )
117
+ .returns(::T::Array[::T.type_parameter(:O)])
118
+ end
119
+ # Map over a single row and keep track of the cell and it's index
120
+ #
121
+ # @param row [Array<Cell>] The row to map each cell over
122
+ #
123
+ # @return [Array]
124
+ def map_row(row, &block)
125
+ row.map.with_index do |cell, index|
126
+ self.cell_index = index
127
+ self.cell = cell if cell.is_a?(::CSVPlusPlus::Cell)
128
+ block.call(cell, index)
129
+ end
130
+ end
131
+
132
+ sig do
133
+ type_parameters(:O).params(
134
+ rows: ::T::Enumerable[::CSVPlusPlus::Row],
135
+ block: ::T.proc.params(row: ::CSVPlusPlus::Row).returns(::T.type_parameter(:O))
136
+ ).returns(::T::Array[::T.type_parameter(:O)])
137
+ end
138
+ # Map over all rows and keep track of row and line numbers
139
+ #
140
+ # @param rows [Array<Row>] The rows to map over (and keep track of indexes)
141
+ #
142
+ # @return [Array]
143
+ def map_rows(rows, &block)
144
+ self.row_index = 0
145
+ map_lines(rows) do |row|
146
+ block.call(row)
147
+ end
148
+ end
149
+
150
+ sig do
151
+ type_parameters(:R)
152
+ .params(rows: ::T::Enumerable[::CSVPlusPlus::Row],
153
+ block: ::T.proc.params(cell: ::CSVPlusPlus::Cell, index: ::Integer).returns(::T.type_parameter(:R)))
154
+ .returns(::T::Array[::T::Array[::T.type_parameter(:R)]])
155
+ end
156
+ # Map over all +rows+ and over all of their +cells+, calling the +&block+ with each +Cell+
157
+ #
158
+ # @param rows [Array<Row>]
159
+ #
160
+ # @return [Array<Array>]
161
+ # rubocop:disable Naming/BlockForwarding
162
+ def map_all_cells(rows, &block)
163
+ self.row_index = 0
164
+ map_lines(rows) { |row| map_row(row.cells, &block) }
165
+ end
166
+ # rubocop:enable Naming/BlockForwarding
167
+
168
+ sig { returns(::Integer) }
169
+ # Return the current spreadsheet row number. It parallels +@row_index+ but starts at 1.
170
+ #
171
+ # @return [Integer, nil]
172
+ def rownum
173
+ row_index + 1
174
+ end
175
+
176
+ sig do
177
+ type_parameters(:R).params(block: ::T.proc.returns(::T.type_parameter(:R))).returns(::T.type_parameter(:R))
178
+ end
179
+ # Each time we run a parse on the input, reset the runtime state starting at the beginning of the file
180
+ def start!(&block)
181
+ @row_index = @cell_index = 0
182
+ self.line_number = 1
183
+
184
+ ret = block.call
185
+ finish!
186
+ ret
187
+ end
188
+
189
+ sig { params(data: ::String).void }
190
+ # We mutate the input over and over. It's ok because it's just a Tempfile
191
+ #
192
+ # @param data [::String] The data to rewrite our input file to
193
+ def rewrite_input!(data)
194
+ input&.truncate(0)
195
+ input&.write(data)
196
+ input&.rewind
197
+ end
198
+
199
+ private
200
+
201
+ sig do
202
+ type_parameters(:R).params(runtime_value: ::T.nilable(::T.type_parameter(:R))).returns(::T.type_parameter(:R))
203
+ end
204
+ def assert_initted!(runtime_value)
205
+ ::T.must_because(runtime_value) do
206
+ 'Runtime value accessed without an initialized runtime. Make sure you call Runtime#start! or ' \
207
+ 'Runtime#start_at_csv! first.'
208
+ end
209
+ end
210
+
211
+ sig { void }
212
+ # Called to mark the trackers dirty. It should be an error to use them outside of an initialized context.
213
+ def finish!
214
+ @line_number = nil
215
+ @row_index = nil
216
+ @cell_index = nil
217
+ @cell = nil
218
+ end
219
+
220
+ sig { returns(::Integer) }
221
+ # Increment state to the next line
222
+ #
223
+ # @return [Integer]
224
+ def next_line!
225
+ self.row_index += 1
226
+ self.line_number += 1
227
+ end
228
+ end
229
+ # rubocop:enable Metrics/ModuleLength
230
+ end
231
+ end
@@ -0,0 +1,110 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module CSVPlusPlus
5
+ module Runtime
6
+ # References in an AST that need to be resolved
7
+ #
8
+ # @attr functions [Array<Entities::Function>] Functions references
9
+ # @attr variables [Array<Entities::Variable>] Variable references
10
+ # TODO: turn this into a CanExtractReferences?
11
+ class References
12
+ extend ::T::Sig
13
+
14
+ sig { returns(::T::Array[::CSVPlusPlus::Entities::FunctionCall]) }
15
+ attr_accessor :functions
16
+
17
+ sig { returns(::T::Array[::CSVPlusPlus::Entities::Variable]) }
18
+ attr_accessor :variables
19
+
20
+ sig do
21
+ params(
22
+ ast: ::CSVPlusPlus::Entities::Entity,
23
+ runtime: ::CSVPlusPlus::Runtime::Runtime
24
+ ).returns(::CSVPlusPlus::Runtime::References)
25
+ end
26
+ # Extract references from an AST and return them in a new +References+ object
27
+ #
28
+ # @param ast [Entity] An +Entity+ to do a depth first search on for references. Entities can be
29
+ # infinitely deep because they can contain other function calls as params to a function call
30
+ # @param runtime [Runtime] The current runtime
31
+ #
32
+ # @return [References]
33
+ def self.extract(ast, runtime)
34
+ new.tap do |refs|
35
+ ::CSVPlusPlus::Runtime::Graph.depth_first_search(ast) do |node|
36
+ unless node.type == ::CSVPlusPlus::Entities::Type::FunctionCall \
37
+ || node.type == ::CSVPlusPlus::Entities::Type::Variable
38
+
39
+ next
40
+ end
41
+
42
+ refs.functions << node if function_reference?(node, runtime)
43
+ refs.variables << node if variable_reference?(node, runtime)
44
+ end
45
+ end
46
+ end
47
+
48
+ sig do
49
+ params(node: ::CSVPlusPlus::Entities::Entity, runtime: ::CSVPlusPlus::Runtime::Runtime).returns(::T::Boolean)
50
+ end
51
+ # Is the node a resolvable variable reference?
52
+ #
53
+ # @param node [Entity] The node to check if it's resolvable
54
+ # @param runtime [Runtime] The current runtime
55
+ #
56
+ # @return [boolean]
57
+ def self.variable_reference?(node, runtime)
58
+ return false unless node.type == ::CSVPlusPlus::Entities::Type::Variable
59
+
60
+ if runtime.in_scope?(node.id)
61
+ true
62
+ else
63
+ runtime.raise_modifier_syntax_error(
64
+ "#{node.id} can only be referenced within the ![[expand]] where it was defined.",
65
+ node.id.to_s
66
+ )
67
+ end
68
+ end
69
+ private_class_method :variable_reference?
70
+
71
+ sig do
72
+ params(node: ::CSVPlusPlus::Entities::Entity, runtime: ::CSVPlusPlus::Runtime::Runtime).returns(::T::Boolean)
73
+ end
74
+ # Is the node a resolvable function reference?
75
+ #
76
+ # @param node [Entity] The node to check if it's resolvable
77
+ # @param runtime [Runtime] The current runtime
78
+ #
79
+ # @return [boolean]
80
+ def self.function_reference?(node, runtime)
81
+ node.type == ::CSVPlusPlus::Entities::Type::FunctionCall \
82
+ && (runtime.defined_function?(node.id) || runtime.builtin_function?(::T.must(node.id)))
83
+ end
84
+ private_class_method :function_reference?
85
+
86
+ sig { void }
87
+ # Create an object with empty references. The caller will build them up as it depth-first-searches
88
+ def initialize
89
+ @functions = ::T.let([], ::T::Array[::CSVPlusPlus::Entities::FunctionCall])
90
+ @variables = ::T.let([], ::T::Array[::CSVPlusPlus::Entities::Variable])
91
+ end
92
+
93
+ sig { params(other: ::CSVPlusPlus::Runtime::References).returns(::T::Boolean) }
94
+ # @param other [References]
95
+ #
96
+ # @return [boolean]
97
+ def ==(other)
98
+ @functions == other.functions && @variables == other.variables
99
+ end
100
+
101
+ sig { returns(::T::Boolean) }
102
+ # Are there any references to be resolved?
103
+ #
104
+ # @return [::T::Boolean]
105
+ def empty?
106
+ @functions.empty? && @variables.empty?
107
+ end
108
+ end
109
+ end
110
+ end