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.
- checksums.yaml +7 -0
- data/lib/csv_plus_plus/cell.rb +51 -0
- data/lib/csv_plus_plus/code_section.rb +49 -0
- data/lib/csv_plus_plus/color.rb +22 -0
- data/lib/csv_plus_plus/expand.rb +18 -0
- data/lib/csv_plus_plus/google_options.rb +23 -0
- data/lib/csv_plus_plus/graph.rb +68 -0
- data/lib/csv_plus_plus/language/cell_value.tab.rb +333 -0
- data/lib/csv_plus_plus/language/code_section.tab.rb +443 -0
- data/lib/csv_plus_plus/language/compiler.rb +170 -0
- data/lib/csv_plus_plus/language/entities/boolean.rb +32 -0
- data/lib/csv_plus_plus/language/entities/cell_reference.rb +26 -0
- data/lib/csv_plus_plus/language/entities/entity.rb +70 -0
- data/lib/csv_plus_plus/language/entities/function.rb +33 -0
- data/lib/csv_plus_plus/language/entities/function_call.rb +25 -0
- data/lib/csv_plus_plus/language/entities/number.rb +34 -0
- data/lib/csv_plus_plus/language/entities/runtime_value.rb +27 -0
- data/lib/csv_plus_plus/language/entities/string.rb +29 -0
- data/lib/csv_plus_plus/language/entities/variable.rb +25 -0
- data/lib/csv_plus_plus/language/entities.rb +28 -0
- data/lib/csv_plus_plus/language/references.rb +53 -0
- data/lib/csv_plus_plus/language/runtime.rb +147 -0
- data/lib/csv_plus_plus/language/scope.rb +199 -0
- data/lib/csv_plus_plus/language/syntax_error.rb +61 -0
- data/lib/csv_plus_plus/lexer/lexer.rb +64 -0
- data/lib/csv_plus_plus/lexer/tokenizer.rb +65 -0
- data/lib/csv_plus_plus/lexer.rb +14 -0
- data/lib/csv_plus_plus/modifier.rb +124 -0
- data/lib/csv_plus_plus/modifier.tab.rb +921 -0
- data/lib/csv_plus_plus/options.rb +70 -0
- data/lib/csv_plus_plus/row.rb +42 -0
- data/lib/csv_plus_plus/template.rb +61 -0
- data/lib/csv_plus_plus/version.rb +6 -0
- data/lib/csv_plus_plus/writer/base_writer.rb +21 -0
- data/lib/csv_plus_plus/writer/csv.rb +31 -0
- data/lib/csv_plus_plus/writer/excel.rb +13 -0
- data/lib/csv_plus_plus/writer/google_sheet_builder.rb +173 -0
- data/lib/csv_plus_plus/writer/google_sheets.rb +139 -0
- data/lib/csv_plus_plus/writer/open_document.rb +14 -0
- data/lib/csv_plus_plus/writer.rb +25 -0
- data/lib/csv_plus_plus.rb +20 -0
- 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
|