csv_plus_plus 0.1.2 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|