csv_plus_plus 0.1.2 → 0.2.0
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.
- checksums.yaml +4 -4
- data/README.md +9 -5
- data/{CHANGELOG.md → docs/CHANGELOG.md} +25 -0
- data/lib/csv_plus_plus/a1_reference.rb +202 -0
- data/lib/csv_plus_plus/benchmarked_compiler.rb +70 -20
- data/lib/csv_plus_plus/cell.rb +29 -41
- data/lib/csv_plus_plus/cli.rb +53 -80
- data/lib/csv_plus_plus/cli_flag.rb +71 -71
- data/lib/csv_plus_plus/color.rb +32 -7
- data/lib/csv_plus_plus/compiler.rb +98 -66
- data/lib/csv_plus_plus/entities/ast_builder.rb +30 -39
- data/lib/csv_plus_plus/entities/boolean.rb +26 -10
- data/lib/csv_plus_plus/entities/builtins.rb +66 -24
- data/lib/csv_plus_plus/entities/date.rb +42 -6
- data/lib/csv_plus_plus/entities/entity.rb +17 -69
- data/lib/csv_plus_plus/entities/entity_with_arguments.rb +44 -0
- data/lib/csv_plus_plus/entities/function.rb +34 -11
- data/lib/csv_plus_plus/entities/function_call.rb +49 -10
- data/lib/csv_plus_plus/entities/has_identifier.rb +19 -0
- data/lib/csv_plus_plus/entities/number.rb +30 -11
- data/lib/csv_plus_plus/entities/reference.rb +77 -0
- data/lib/csv_plus_plus/entities/runtime_value.rb +43 -13
- data/lib/csv_plus_plus/entities/string.rb +23 -7
- data/lib/csv_plus_plus/entities.rb +7 -16
- data/lib/csv_plus_plus/error/cli_error.rb +17 -0
- data/lib/csv_plus_plus/error/compiler_error.rb +17 -0
- data/lib/csv_plus_plus/error/error.rb +25 -2
- data/lib/csv_plus_plus/error/formula_syntax_error.rb +12 -12
- data/lib/csv_plus_plus/error/modifier_syntax_error.rb +34 -12
- data/lib/csv_plus_plus/error/modifier_validation_error.rb +21 -27
- data/lib/csv_plus_plus/error/positional_error.rb +15 -0
- data/lib/csv_plus_plus/error/writer_error.rb +8 -0
- data/lib/csv_plus_plus/error.rb +5 -1
- data/lib/csv_plus_plus/error_formatter.rb +111 -0
- data/lib/csv_plus_plus/google_api_client.rb +25 -10
- data/lib/csv_plus_plus/lexer/racc_lexer.rb +144 -0
- data/lib/csv_plus_plus/lexer/tokenizer.rb +58 -17
- data/lib/csv_plus_plus/lexer.rb +64 -1
- data/lib/csv_plus_plus/modifier/conditional_formatting.rb +1 -0
- data/lib/csv_plus_plus/modifier/data_validation.rb +138 -0
- data/lib/csv_plus_plus/modifier/expand.rb +78 -0
- data/lib/csv_plus_plus/modifier/google_sheet_modifier.rb +133 -0
- data/lib/csv_plus_plus/modifier/modifier.rb +222 -0
- data/lib/csv_plus_plus/modifier/modifier_validator.rb +243 -0
- data/lib/csv_plus_plus/modifier/rubyxl_modifier.rb +84 -0
- data/lib/csv_plus_plus/modifier.rb +89 -160
- data/lib/csv_plus_plus/options/file_options.rb +49 -0
- data/lib/csv_plus_plus/options/google_sheets_options.rb +42 -0
- data/lib/csv_plus_plus/options/options.rb +97 -0
- data/lib/csv_plus_plus/options.rb +34 -77
- data/lib/csv_plus_plus/parser/cell_value.tab.rb +66 -67
- data/lib/csv_plus_plus/parser/code_section.tab.rb +86 -83
- data/lib/csv_plus_plus/parser/modifier.tab.rb +57 -53
- data/lib/csv_plus_plus/reader/csv.rb +50 -0
- data/lib/csv_plus_plus/reader/google_sheets.rb +129 -0
- data/lib/csv_plus_plus/reader/reader.rb +27 -0
- data/lib/csv_plus_plus/reader/rubyxl.rb +37 -0
- data/lib/csv_plus_plus/reader.rb +14 -0
- data/lib/csv_plus_plus/row.rb +53 -12
- data/lib/csv_plus_plus/runtime/graph.rb +68 -0
- data/lib/csv_plus_plus/runtime/position.rb +242 -0
- data/lib/csv_plus_plus/runtime/references.rb +115 -0
- data/lib/csv_plus_plus/runtime/runtime.rb +132 -0
- data/lib/csv_plus_plus/runtime/scope.rb +280 -0
- data/lib/csv_plus_plus/runtime.rb +34 -191
- data/lib/csv_plus_plus/source_code.rb +71 -0
- data/lib/csv_plus_plus/template.rb +71 -39
- data/lib/csv_plus_plus/version.rb +2 -1
- data/lib/csv_plus_plus/writer/csv.rb +37 -8
- data/lib/csv_plus_plus/writer/excel.rb +25 -5
- data/lib/csv_plus_plus/writer/file_backer_upper.rb +27 -13
- data/lib/csv_plus_plus/writer/google_sheets.rb +29 -85
- data/lib/csv_plus_plus/writer/google_sheets_builder.rb +179 -0
- data/lib/csv_plus_plus/writer/merger.rb +31 -0
- data/lib/csv_plus_plus/writer/open_document.rb +21 -2
- data/lib/csv_plus_plus/writer/rubyxl_builder.rb +140 -42
- data/lib/csv_plus_plus/writer/writer.rb +42 -0
- data/lib/csv_plus_plus/writer.rb +79 -10
- data/lib/csv_plus_plus.rb +47 -18
- metadata +50 -21
- data/lib/csv_plus_plus/can_define_references.rb +0 -88
- data/lib/csv_plus_plus/can_resolve_references.rb +0 -8
- data/lib/csv_plus_plus/data_validation.rb +0 -138
- data/lib/csv_plus_plus/entities/cell_reference.rb +0 -60
- data/lib/csv_plus_plus/entities/variable.rb +0 -25
- data/lib/csv_plus_plus/error/syntax_error.rb +0 -58
- data/lib/csv_plus_plus/expand.rb +0 -20
- data/lib/csv_plus_plus/google_options.rb +0 -27
- data/lib/csv_plus_plus/graph.rb +0 -62
- data/lib/csv_plus_plus/lexer/lexer.rb +0 -85
- data/lib/csv_plus_plus/references.rb +0 -68
- data/lib/csv_plus_plus/scope.rb +0 -196
- data/lib/csv_plus_plus/validated_modifier.rb +0 -164
- data/lib/csv_plus_plus/writer/base_writer.rb +0 -20
- data/lib/csv_plus_plus/writer/google_sheet_builder.rb +0 -147
- data/lib/csv_plus_plus/writer/google_sheet_modifier.rb +0 -77
- data/lib/csv_plus_plus/writer/rubyxl_modifier.rb +0 -59
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module CSVPlusPlus
|
|
5
|
+
module Runtime
|
|
6
|
+
# Keeps track of the position in a file where the parser is. The parser makes various passes over the input but
|
|
7
|
+
# it always needs to track the same things (line number, cell/row index, current cell)
|
|
8
|
+
#
|
|
9
|
+
# @attr cell [Cell] The current cell being processed
|
|
10
|
+
# @attr cell_index [Integer] The index of the current cell being processed (starts at 0)
|
|
11
|
+
# @attr row_index [Integer] The index of the current row being processed (starts at 0)
|
|
12
|
+
# @attr line_number [Integer] The line number of the original csvpp template (starts at 1)
|
|
13
|
+
# rubocop:disable Metrics/ClassLength
|
|
14
|
+
class Position
|
|
15
|
+
extend ::T::Sig
|
|
16
|
+
|
|
17
|
+
sig { params(cell: ::T.nilable(::CSVPlusPlus::Cell)).returns(::T.nilable(::CSVPlusPlus::Cell)) }
|
|
18
|
+
attr_writer :cell
|
|
19
|
+
|
|
20
|
+
sig { params(cell_index: ::T.nilable(::Integer)).returns(::T.nilable(::Integer)) }
|
|
21
|
+
attr_writer :cell_index
|
|
22
|
+
|
|
23
|
+
sig { params(line_number: ::T.nilable(::Integer)).returns(::T.nilable(::Integer)) }
|
|
24
|
+
attr_writer :line_number
|
|
25
|
+
|
|
26
|
+
sig { params(row_index: ::T.nilable(::Integer)).returns(::T.nilable(::Integer)) }
|
|
27
|
+
attr_writer :row_index
|
|
28
|
+
|
|
29
|
+
sig { params(input: ::String).void }
|
|
30
|
+
# @param input [String]
|
|
31
|
+
def initialize(input)
|
|
32
|
+
rewrite_input!(::CSVPlusPlus::Lexer.preprocess(input))
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
sig { returns(::CSVPlusPlus::Cell) }
|
|
36
|
+
# The current cell index. This will only be set when processing the CSV section
|
|
37
|
+
#
|
|
38
|
+
# @return [Cell]
|
|
39
|
+
def cell
|
|
40
|
+
@cell ||= ::T.let(nil, ::T.nilable(::CSVPlusPlus::Cell))
|
|
41
|
+
assert_initted!(@cell)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
sig { returns(::Integer) }
|
|
45
|
+
# The current CSV cell index.
|
|
46
|
+
#
|
|
47
|
+
# This will only be set when processing the CSV section and will throw an exception otherwise. It is up to the
|
|
48
|
+
# caller (the compiler) to make sure it's called in the context of a compilation stage and/or a
|
|
49
|
+
# +#map_row+/+#map_rows+/+#map_lines+
|
|
50
|
+
#
|
|
51
|
+
# @return [Integer]
|
|
52
|
+
def cell_index
|
|
53
|
+
@cell_index ||= ::T.let(nil, ::T.nilable(::Integer))
|
|
54
|
+
assert_initted!(@cell_index)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
sig { returns(::Integer) }
|
|
58
|
+
# The current CSV row index. This will only be set when processing the CSV section
|
|
59
|
+
#
|
|
60
|
+
# This will only be set when processing the CSV section and will throw an exception otherwise. It is up to the
|
|
61
|
+
# caller (the compiler) to make sure it's called in the context of a compilation stage and/or a
|
|
62
|
+
# +#map_row+/+#map_rows+/+#map_lines+
|
|
63
|
+
#
|
|
64
|
+
# @return [Integer]
|
|
65
|
+
def row_index
|
|
66
|
+
@row_index ||= ::T.let(nil, ::T.nilable(::Integer))
|
|
67
|
+
assert_initted!(@row_index)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
sig { returns(::Integer) }
|
|
71
|
+
# The current line number being processed. The line number is based on the entire file, irregardless of if it's
|
|
72
|
+
# parsing the code section or the CSV section
|
|
73
|
+
#
|
|
74
|
+
# This will only be set when processing the csvpp file and will throw an exception otherwise. It is up to the
|
|
75
|
+
# caller (the compiler) to make sure it's called in the context of a compilation stage and/or a
|
|
76
|
+
# +#map_row+/+#map_rows+/+#map_lines+
|
|
77
|
+
#
|
|
78
|
+
# @return [Integer]
|
|
79
|
+
def line_number
|
|
80
|
+
@line_number ||= ::T.let(nil, ::T.nilable(::Integer))
|
|
81
|
+
assert_initted!(@line_number)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
sig { void }
|
|
85
|
+
# Clean up the Tempfile we're using for parsing
|
|
86
|
+
def cleanup!
|
|
87
|
+
input&.close
|
|
88
|
+
input&.unlink
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
sig { returns(::T.nilable(::Tempfile)) }
|
|
92
|
+
# The currently available input for parsing. The tmp state will be re-written
|
|
93
|
+
# between parsing the code section and the CSV section
|
|
94
|
+
#
|
|
95
|
+
# @return [::Tempfile]
|
|
96
|
+
def input
|
|
97
|
+
@input ||= ::T.let(::Tempfile.new, ::T.nilable(::Tempfile))
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
sig do
|
|
101
|
+
type_parameters(:I, :O).params(
|
|
102
|
+
lines: ::T::Enumerable[::T.type_parameter(:I)],
|
|
103
|
+
block: ::T.proc.params(args0: ::T.type_parameter(:I)).returns(::T.type_parameter(:O))
|
|
104
|
+
).returns(::T::Array[::T.type_parameter(:O)])
|
|
105
|
+
end
|
|
106
|
+
# Map over a csvpp file and keep track of line_number and row_index
|
|
107
|
+
#
|
|
108
|
+
# @param lines [Array]
|
|
109
|
+
#
|
|
110
|
+
# @return [Array]
|
|
111
|
+
def map_lines(lines, &block)
|
|
112
|
+
self.line_number = 1
|
|
113
|
+
lines.map do |line|
|
|
114
|
+
ret = block.call(line)
|
|
115
|
+
next_line!
|
|
116
|
+
ret
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
sig do
|
|
121
|
+
type_parameters(:I, :O)
|
|
122
|
+
.params(
|
|
123
|
+
row: ::T::Enumerable[::T.all(::T.type_parameter(:I), ::Object)],
|
|
124
|
+
block: ::T.proc.params(
|
|
125
|
+
cell: ::T.all(::T.type_parameter(:I), ::Object),
|
|
126
|
+
index: ::Integer
|
|
127
|
+
).returns(::T.type_parameter(:O))
|
|
128
|
+
)
|
|
129
|
+
.returns(::T::Array[::T.type_parameter(:O)])
|
|
130
|
+
end
|
|
131
|
+
# Map over a single row and keep track of the cell and it's index
|
|
132
|
+
#
|
|
133
|
+
# @param row [Array<Cell>] The row to map each cell over
|
|
134
|
+
#
|
|
135
|
+
# @return [Array]
|
|
136
|
+
def map_row(row, &block)
|
|
137
|
+
row.map.with_index do |cell, index|
|
|
138
|
+
self.cell_index = index
|
|
139
|
+
self.cell = cell if cell.is_a?(::CSVPlusPlus::Cell)
|
|
140
|
+
block.call(cell, index)
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
sig do
|
|
145
|
+
type_parameters(:O).params(
|
|
146
|
+
rows: ::T::Enumerable[::CSVPlusPlus::Row],
|
|
147
|
+
block: ::T.proc.params(row: ::CSVPlusPlus::Row).returns(::T.type_parameter(:O))
|
|
148
|
+
).returns(::T::Array[::T.type_parameter(:O)])
|
|
149
|
+
end
|
|
150
|
+
# Map over all rows and keep track of row and line numbers
|
|
151
|
+
#
|
|
152
|
+
# @param rows [Array<Row>] The rows to map over (and keep track of indexes)
|
|
153
|
+
#
|
|
154
|
+
# @return [Array]
|
|
155
|
+
def map_rows(rows, &block)
|
|
156
|
+
self.row_index = 0
|
|
157
|
+
map_lines(rows) do |row|
|
|
158
|
+
block.call(row)
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
sig do
|
|
163
|
+
type_parameters(:R)
|
|
164
|
+
.params(rows: ::T::Enumerable[::CSVPlusPlus::Row],
|
|
165
|
+
block: ::T.proc.params(cell: ::CSVPlusPlus::Cell, index: ::Integer).returns(::T.type_parameter(:R)))
|
|
166
|
+
.returns(::T::Array[::T::Array[::T.type_parameter(:R)]])
|
|
167
|
+
end
|
|
168
|
+
# Map over all +rows+ and over all of their +cells+, calling the +&block+ with each +Cell+
|
|
169
|
+
#
|
|
170
|
+
# @param rows [Array<Row>]
|
|
171
|
+
#
|
|
172
|
+
# @return [Array<Array>]
|
|
173
|
+
# rubocop:disable Naming/BlockForwarding
|
|
174
|
+
def map_all_cells(rows, &block)
|
|
175
|
+
self.row_index = 0
|
|
176
|
+
map_lines(rows) { |row| map_row(row.cells, &block) }
|
|
177
|
+
end
|
|
178
|
+
# rubocop:enable Naming/BlockForwarding
|
|
179
|
+
|
|
180
|
+
sig { returns(::Integer) }
|
|
181
|
+
# Return the current spreadsheet row number. It parallels +@row_index+ but starts at 1.
|
|
182
|
+
#
|
|
183
|
+
# @return [Integer, nil]
|
|
184
|
+
def rownum
|
|
185
|
+
row_index + 1
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
sig do
|
|
189
|
+
type_parameters(:R).params(block: ::T.proc.returns(::T.type_parameter(:R))).returns(::T.type_parameter(:R))
|
|
190
|
+
end
|
|
191
|
+
# Each time we run a parse on the input, reset the runtime state starting at the beginning of the file
|
|
192
|
+
def start!(&block)
|
|
193
|
+
@row_index = @cell_index = 0
|
|
194
|
+
|
|
195
|
+
ret = block.call
|
|
196
|
+
finish!
|
|
197
|
+
ret
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
sig { params(data: ::String).void }
|
|
201
|
+
# We mutate the input over and over. It's ok because it's just a Tempfile
|
|
202
|
+
#
|
|
203
|
+
# @param data [::String] The data to rewrite our input file to
|
|
204
|
+
def rewrite_input!(data)
|
|
205
|
+
input&.truncate(0)
|
|
206
|
+
input&.write(data)
|
|
207
|
+
input&.rewind
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
private
|
|
211
|
+
|
|
212
|
+
sig do
|
|
213
|
+
type_parameters(:R).params(runtime_value: ::T.nilable(::T.type_parameter(:R))).returns(::T.type_parameter(:R))
|
|
214
|
+
end
|
|
215
|
+
def assert_initted!(runtime_value)
|
|
216
|
+
::T.must_because(runtime_value) do
|
|
217
|
+
'Runtime value accessed without an initialized runtime. Make sure you call Runtime#start! or ' \
|
|
218
|
+
'Runtime#start_at_csv! first.'
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
sig { void }
|
|
223
|
+
# Called to mark the trackers dirty. It should be an error to use them outside of an initialized context.
|
|
224
|
+
def finish!
|
|
225
|
+
@line_number = nil
|
|
226
|
+
@row_index = nil
|
|
227
|
+
@cell_index = nil
|
|
228
|
+
@cell = nil
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
sig { returns(::Integer) }
|
|
232
|
+
# Increment state to the next line
|
|
233
|
+
#
|
|
234
|
+
# @return [Integer]
|
|
235
|
+
def next_line!
|
|
236
|
+
self.row_index += 1
|
|
237
|
+
self.line_number += 1
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
# rubocop:enable Metrics/ClassLength
|
|
241
|
+
end
|
|
242
|
+
end
|
|
@@ -0,0 +1,115 @@
|
|
|
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
|
+
class References
|
|
11
|
+
extend ::T::Sig
|
|
12
|
+
|
|
13
|
+
sig { returns(::T::Array[::CSVPlusPlus::Entities::FunctionCall]) }
|
|
14
|
+
attr_accessor :functions
|
|
15
|
+
|
|
16
|
+
sig { returns(::T::Array[::CSVPlusPlus::Entities::Reference]) }
|
|
17
|
+
attr_accessor :variables
|
|
18
|
+
|
|
19
|
+
sig do
|
|
20
|
+
params(
|
|
21
|
+
ast: ::CSVPlusPlus::Entities::Entity,
|
|
22
|
+
position: ::CSVPlusPlus::Runtime::Position,
|
|
23
|
+
scope: ::CSVPlusPlus::Runtime::Scope
|
|
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 scope [Scope] The current scope
|
|
31
|
+
#
|
|
32
|
+
# @return [References]
|
|
33
|
+
def self.extract(ast, position, scope)
|
|
34
|
+
new.tap do |refs|
|
|
35
|
+
::CSVPlusPlus::Runtime::Graph.depth_first_search(ast) do |node|
|
|
36
|
+
unless node.is_a?(::CSVPlusPlus::Entities::FunctionCall) || node.is_a?(::CSVPlusPlus::Entities::Reference)
|
|
37
|
+
next
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
refs.functions << node if function_reference?(node, scope)
|
|
41
|
+
refs.variables << node if variable_reference?(node, position, scope)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
sig do
|
|
47
|
+
params(
|
|
48
|
+
node: ::CSVPlusPlus::Entities::Entity,
|
|
49
|
+
position: ::CSVPlusPlus::Runtime::Position,
|
|
50
|
+
scope: ::CSVPlusPlus::Runtime::Scope
|
|
51
|
+
).returns(::T::Boolean)
|
|
52
|
+
end
|
|
53
|
+
# Is the node a resolvable variable reference?
|
|
54
|
+
#
|
|
55
|
+
# @param node [Entity] The node to check if it's resolvable
|
|
56
|
+
# @param scope [Scope] The current scope
|
|
57
|
+
#
|
|
58
|
+
# @return [boolean]
|
|
59
|
+
def self.variable_reference?(node, position, scope)
|
|
60
|
+
return false unless node.is_a?(::CSVPlusPlus::Entities::Reference)
|
|
61
|
+
|
|
62
|
+
id = node.id
|
|
63
|
+
return false unless id && scope.variables.key?(id)
|
|
64
|
+
|
|
65
|
+
return true if scope.in_scope?(id, position)
|
|
66
|
+
|
|
67
|
+
raise(
|
|
68
|
+
::CSVPlusPlus::Error::ModifierSyntaxError.new(
|
|
69
|
+
"Reference #{node.ref} can only be referenced within the ![[expand]] where it was defined.",
|
|
70
|
+
bad_input: node.ref.to_s
|
|
71
|
+
)
|
|
72
|
+
)
|
|
73
|
+
end
|
|
74
|
+
private_class_method :variable_reference?
|
|
75
|
+
|
|
76
|
+
sig do
|
|
77
|
+
params(node: ::CSVPlusPlus::Entities::Entity, scope: ::CSVPlusPlus::Runtime::Scope).returns(::T::Boolean)
|
|
78
|
+
end
|
|
79
|
+
# Is the node a resolvable function reference?
|
|
80
|
+
#
|
|
81
|
+
# @param node [Entity] The node to check if it's resolvable
|
|
82
|
+
# @param scope [Scope] The current scope
|
|
83
|
+
#
|
|
84
|
+
# @return [boolean]
|
|
85
|
+
def self.function_reference?(node, scope)
|
|
86
|
+
node.is_a?(::CSVPlusPlus::Entities::FunctionCall) \
|
|
87
|
+
&& (scope.functions.key?(node.id) || ::CSVPlusPlus::Entities::Builtins.builtin_function?(node.id))
|
|
88
|
+
end
|
|
89
|
+
private_class_method :function_reference?
|
|
90
|
+
|
|
91
|
+
sig { void }
|
|
92
|
+
# Create an object with empty references. The caller will build them up as it depth-first-searches
|
|
93
|
+
def initialize
|
|
94
|
+
@functions = ::T.let([], ::T::Array[::CSVPlusPlus::Entities::FunctionCall])
|
|
95
|
+
@variables = ::T.let([], ::T::Array[::CSVPlusPlus::Entities::Reference])
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
sig { params(other: ::CSVPlusPlus::Runtime::References).returns(::T::Boolean) }
|
|
99
|
+
# @param other [References]
|
|
100
|
+
#
|
|
101
|
+
# @return [boolean]
|
|
102
|
+
def ==(other)
|
|
103
|
+
@functions == other.functions && @variables == other.variables
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
sig { returns(::T::Boolean) }
|
|
107
|
+
# Are there any references to be resolved?
|
|
108
|
+
#
|
|
109
|
+
# @return [::T::Boolean]
|
|
110
|
+
def empty?
|
|
111
|
+
@functions.empty? && @variables.empty?
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module CSVPlusPlus
|
|
5
|
+
module Runtime
|
|
6
|
+
# The runtime state of the compiler (the current +line_number+/+row_index+, +cell+ being processed, etc) for parsing
|
|
7
|
+
# a given file. We take multiple runs through the input file for parsing so it's really convenient to have a
|
|
8
|
+
# central place for these things to be managed.
|
|
9
|
+
#
|
|
10
|
+
# @attr_reader position [Runtime::Position]
|
|
11
|
+
# @attr_reader scope [Runtime::Scope]
|
|
12
|
+
# @attr_reader source_code [SourceCode]
|
|
13
|
+
class Runtime
|
|
14
|
+
extend ::T::Sig
|
|
15
|
+
|
|
16
|
+
sig { returns(::CSVPlusPlus::Runtime::Position) }
|
|
17
|
+
attr_reader :position
|
|
18
|
+
|
|
19
|
+
sig { returns(::CSVPlusPlus::Runtime::Scope) }
|
|
20
|
+
attr_reader :scope
|
|
21
|
+
|
|
22
|
+
sig { returns(::CSVPlusPlus::SourceCode) }
|
|
23
|
+
attr_reader :source_code
|
|
24
|
+
|
|
25
|
+
sig do
|
|
26
|
+
params(
|
|
27
|
+
source_code: ::CSVPlusPlus::SourceCode,
|
|
28
|
+
position: ::T.nilable(::CSVPlusPlus::Runtime::Position),
|
|
29
|
+
scope: ::T.nilable(::CSVPlusPlus::Runtime::Scope)
|
|
30
|
+
).void
|
|
31
|
+
end
|
|
32
|
+
# @param position [Position, nil] The (optional) position to start at
|
|
33
|
+
# @param source_code [SourceCode] The source code being compiled
|
|
34
|
+
# @param scope [Runtime::Scope, nil] The (optional) scope if it already exists
|
|
35
|
+
def initialize(source_code:, position: nil, scope: nil)
|
|
36
|
+
@source_code = source_code
|
|
37
|
+
@scope = ::T.let(scope || ::CSVPlusPlus::Runtime::Scope.new, ::CSVPlusPlus::Runtime::Scope)
|
|
38
|
+
@position = ::T.let(
|
|
39
|
+
position || ::CSVPlusPlus::Runtime::Position.new(source_code.input),
|
|
40
|
+
::CSVPlusPlus::Runtime::Position
|
|
41
|
+
)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
sig { params(var_id: ::Symbol).returns(::CSVPlusPlus::Entities::Reference) }
|
|
45
|
+
# Bind +var_id+ to the current cell
|
|
46
|
+
#
|
|
47
|
+
# @param var_id [Symbol] The name of the variable to bind the cell reference to
|
|
48
|
+
#
|
|
49
|
+
# @return [Entities::Reference]
|
|
50
|
+
def bind_variable_to_cell(var_id)
|
|
51
|
+
::CSVPlusPlus::Entities::Reference.new(
|
|
52
|
+
a1_ref: ::CSVPlusPlus::A1Reference.new(cell_index: position.cell_index, row_index: position.row_index)
|
|
53
|
+
).tap do |var|
|
|
54
|
+
scope.def_variable(var_id, var)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
sig do
|
|
59
|
+
params(var_id: ::Symbol, expand: ::CSVPlusPlus::Modifier::Expand).returns(::CSVPlusPlus::Entities::Reference)
|
|
60
|
+
end
|
|
61
|
+
# Bind +var_id+ relative to a cell relative to an ![[expand]] modifier. The variable can only be referenced
|
|
62
|
+
# inside rows of that expand.
|
|
63
|
+
#
|
|
64
|
+
# @param var_id [Symbol] The name of the variable to bind the cell reference to
|
|
65
|
+
# @param expand [Expand] The expand where the variable is accessible (where it will be bound relative to)
|
|
66
|
+
#
|
|
67
|
+
# @return [Entities::Reference]
|
|
68
|
+
def bind_variable_in_expand(var_id, expand)
|
|
69
|
+
::CSVPlusPlus::Entities::Reference.new(
|
|
70
|
+
a1_ref: ::CSVPlusPlus::A1Reference.new(scoped_to_expand: expand, cell_index: position.cell_index)
|
|
71
|
+
).tap do |var|
|
|
72
|
+
scope.def_variable(var_id, var)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
sig { returns(::T::Boolean) }
|
|
77
|
+
# Is the parser currently inside of the CSV section?
|
|
78
|
+
#
|
|
79
|
+
# @return [T::Boolean]
|
|
80
|
+
def parsing_csv_section?
|
|
81
|
+
source_code.in_csv_section?(position.line_number)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
sig { params(ast: ::CSVPlusPlus::Entities::Entity).returns(::CSVPlusPlus::Entities::Entity) }
|
|
85
|
+
# Resolve all values in the ast of the current cell being processed
|
|
86
|
+
#
|
|
87
|
+
# @param ast [Entities::Entity] The AST to replace references within
|
|
88
|
+
#
|
|
89
|
+
# @return [Entity] The AST with all references replaced
|
|
90
|
+
# rubocop:disable Metrics/MethodLength
|
|
91
|
+
def resolve_cell_value(ast)
|
|
92
|
+
last_round = nil
|
|
93
|
+
::Kernel.loop do
|
|
94
|
+
refs = ::CSVPlusPlus::Runtime::References.extract(ast, position, scope)
|
|
95
|
+
return ast if refs.empty?
|
|
96
|
+
|
|
97
|
+
# TODO: throw a +CompilerError+ here instead I think - basically we did a round and didn't make progress
|
|
98
|
+
return ast if last_round == refs
|
|
99
|
+
|
|
100
|
+
ast = scope.resolve_functions(
|
|
101
|
+
position,
|
|
102
|
+
scope.resolve_variables(position, ast, refs.variables),
|
|
103
|
+
refs.functions
|
|
104
|
+
)
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
# rubocop:enable Metrics/MethodLength
|
|
108
|
+
|
|
109
|
+
sig do
|
|
110
|
+
type_parameters(:R).params(block: ::T.proc.returns(::T.type_parameter(:R))).returns(::T.type_parameter(:R))
|
|
111
|
+
end
|
|
112
|
+
# Each time we run a parse on the input, reset the runtime state starting at the beginning of the file
|
|
113
|
+
# rubocop:disable Naming/BlockForwarding
|
|
114
|
+
def start!(&block)
|
|
115
|
+
position.line_number = 1
|
|
116
|
+
position.start!(&block)
|
|
117
|
+
end
|
|
118
|
+
# rubocop:enable Naming/BlockForwarding
|
|
119
|
+
|
|
120
|
+
sig do
|
|
121
|
+
type_parameters(:R).params(block: ::T.proc.returns(::T.type_parameter(:R))).returns(::T.type_parameter(:R))
|
|
122
|
+
end
|
|
123
|
+
# Reset the runtime state starting at the CSV section
|
|
124
|
+
# rubocop:disable Naming/BlockForwarding
|
|
125
|
+
def start_at_csv!(&block)
|
|
126
|
+
position.line_number = source_code.length_of_code_section + 1
|
|
127
|
+
position.start!(&block)
|
|
128
|
+
end
|
|
129
|
+
# rubocop:enable Naming/BlockForwarding
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|