electr 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/.gitignore +15 -0
- data/.rspec +2 -0
- data/.travis.yml +3 -0
- data/CHANGELOG.md +17 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +196 -0
- data/Rakefile +7 -0
- data/bin/electr +27 -0
- data/electr.gemspec +25 -0
- data/lib/electr.rb +20 -0
- data/lib/electr/ast.rb +9 -0
- data/lib/electr/ast/ast.rb +62 -0
- data/lib/electr/ast/constant_ast.rb +14 -0
- data/lib/electr/ast/func_args_ast.rb +11 -0
- data/lib/electr/ast/func_ast.rb +12 -0
- data/lib/electr/ast/func_name_ast.rb +14 -0
- data/lib/electr/ast/numeric_ast.rb +13 -0
- data/lib/electr/ast/operator_ast.rb +13 -0
- data/lib/electr/ast/root_ast.rb +11 -0
- data/lib/electr/ast/value_ast.rb +12 -0
- data/lib/electr/compiler.rb +24 -0
- data/lib/electr/evaluator.rb +64 -0
- data/lib/electr/exceptions.rb +6 -0
- data/lib/electr/option.rb +70 -0
- data/lib/electr/parser.rb +5 -0
- data/lib/electr/parser/lexer.rb +50 -0
- data/lib/electr/parser/lexical_unit.rb +67 -0
- data/lib/electr/parser/rules/base_rule.rb +60 -0
- data/lib/electr/parser/rules/expression_rule.rb +79 -0
- data/lib/electr/parser/rules/root_rule.rb +21 -0
- data/lib/electr/parser/sya.rb +105 -0
- data/lib/electr/parser/syntaxer.rb +21 -0
- data/lib/electr/parser/tokenizer.rb +79 -0
- data/lib/electr/repl.rb +56 -0
- data/lib/electr/rules.rb +3 -0
- data/lib/electr/version.rb +3 -0
- data/spec/compiler_spec.rb +42 -0
- data/spec/electr_spec.rb +7 -0
- data/spec/evaluator_spec.rb +47 -0
- data/spec/lexer_spec.rb +29 -0
- data/spec/spec_helper.rb +8 -0
- data/spec/sya_spec.rb +153 -0
- data/spec/tokenizer_spec.rb +102 -0
- metadata +138 -0
@@ -0,0 +1,79 @@
|
|
1
|
+
module Electr
|
2
|
+
|
3
|
+
class ExpressionRule < BaseRule
|
4
|
+
|
5
|
+
def apply!
|
6
|
+
while @units.size > 0
|
7
|
+
if first_unit_fname?
|
8
|
+
accept(:fname)
|
9
|
+
accept(:open_parenthesis, '(')
|
10
|
+
while @units.first && (not @units.first.closed_parenthesis?)
|
11
|
+
accept(@units.first.type)
|
12
|
+
end
|
13
|
+
accept(:closed_parenthesis, ')')
|
14
|
+
else
|
15
|
+
accept(@units.first.type)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
sya = Sya.new(@series.dup)
|
20
|
+
@series = sya.run
|
21
|
+
|
22
|
+
if @series.first.numeric? || @series.first.constant? ||
|
23
|
+
@series.first.value? || @series.first.fname?
|
24
|
+
dig_series(@ast_node)
|
25
|
+
else
|
26
|
+
unit = @series.shift
|
27
|
+
node = OperatorAST.new(unit.value)
|
28
|
+
dig_series(node) while @series.size > 0
|
29
|
+
@ast_node.add_child(node)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def first_unit_number?
|
36
|
+
@units.first && @units.first.number?
|
37
|
+
end
|
38
|
+
|
39
|
+
def first_unit_numeric?
|
40
|
+
@units.first && @units.first.numeric?
|
41
|
+
end
|
42
|
+
|
43
|
+
def first_unit_operator?
|
44
|
+
@units.first && @units.first.operator?
|
45
|
+
end
|
46
|
+
|
47
|
+
def first_unit_fname?
|
48
|
+
@units.first && @units.first.fname?
|
49
|
+
end
|
50
|
+
|
51
|
+
def dig_series(node)
|
52
|
+
while unit = @series.shift
|
53
|
+
if unit && unit.numeric? && node.size < 2
|
54
|
+
node.add_child(NumericAST.new(unit.value))
|
55
|
+
elsif unit && unit.constant? && node.size < 2
|
56
|
+
node.add_child(ConstantAST.new(unit.value))
|
57
|
+
elsif unit && unit.value? && node.size < 2
|
58
|
+
node.add_child(ValueAST.new(unit.value))
|
59
|
+
elsif unit && unit.fname? && node.size < 2
|
60
|
+
func = FuncAST.new
|
61
|
+
func_name = FuncNameAST.new(unit.value)
|
62
|
+
func_args = FuncArgsAST.new
|
63
|
+
dig_series(func_args)
|
64
|
+
func.add_child(func_name)
|
65
|
+
func.add_child(func_args)
|
66
|
+
node.add_child(func)
|
67
|
+
elsif unit && unit.operator?
|
68
|
+
new_node = OperatorAST.new(unit.value)
|
69
|
+
dig_series(new_node)
|
70
|
+
node.add_child(new_node)
|
71
|
+
else
|
72
|
+
@series.unshift(unit)
|
73
|
+
break
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Electr
|
2
|
+
|
3
|
+
class RootRule < BaseRule
|
4
|
+
|
5
|
+
def apply!
|
6
|
+
root_node = RootAST.new
|
7
|
+
while more_units?
|
8
|
+
ExpressionRule.new(@units, root_node).apply!
|
9
|
+
end
|
10
|
+
@ast_node.add_child(root_node)
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
def more_units?
|
16
|
+
@units.size > 0
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
@@ -0,0 +1,105 @@
|
|
1
|
+
module Electr
|
2
|
+
|
3
|
+
# Shunting Yard Algorithm.
|
4
|
+
#
|
5
|
+
# It produces prefix notation instead of the most common reverse
|
6
|
+
# polish notation to ease producing the AST.
|
7
|
+
class Sya
|
8
|
+
|
9
|
+
PRECEDENCE = {
|
10
|
+
'()' => {assoc: 'L', val: 99},
|
11
|
+
')' => {assoc: 'L', val: 99},
|
12
|
+
'(' => {assoc: 'L', val: 99},
|
13
|
+
'*' => {assoc: 'L', val: 10},
|
14
|
+
'/' => {assoc: 'L', val: 10},
|
15
|
+
'-' => {assoc: 'L', val: 1},
|
16
|
+
'+' => {assoc: 'L', val: 1},
|
17
|
+
}
|
18
|
+
|
19
|
+
# lexical_units - Array of LexicalUnit in the order they have been
|
20
|
+
# parsed (natural ordering).
|
21
|
+
def initialize(lexical_units)
|
22
|
+
@units = lexical_units
|
23
|
+
insert_multiplication
|
24
|
+
@output = []
|
25
|
+
@operator = []
|
26
|
+
end
|
27
|
+
|
28
|
+
# Run the Shunting Yard Algorithm.
|
29
|
+
#
|
30
|
+
# Returns Array of LexicalUnit ordered with prefix notation.
|
31
|
+
def run
|
32
|
+
while unit = @units.pop
|
33
|
+
if unit.number?
|
34
|
+
@output.push(unit)
|
35
|
+
elsif unit.fname?
|
36
|
+
@operator.push(unit)
|
37
|
+
elsif unit.type == :closed_parenthesis
|
38
|
+
@operator.push(unit)
|
39
|
+
elsif unit.type == :open_parenthesis
|
40
|
+
while @operator.last &&
|
41
|
+
@operator.last.type != :closed_parenthesis
|
42
|
+
@output.push(@operator.pop)
|
43
|
+
end
|
44
|
+
rparen = @operator.pop
|
45
|
+
if @operator.last && @operator.last.type == :fname
|
46
|
+
@output.push(@operator.pop)
|
47
|
+
end
|
48
|
+
# If the stack runs out without finding a right parenthesis,
|
49
|
+
# then there are mismatched parentheses.
|
50
|
+
elsif unit.operator?
|
51
|
+
while @operator.size > 0 &&
|
52
|
+
(@operator.last.operator? || @operator.last.fname?) &&
|
53
|
+
(precedence(unit) < precedence(@operator.last))
|
54
|
+
@output.push(@operator.pop)
|
55
|
+
end
|
56
|
+
@operator.push(unit)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
@output.push(@operator.pop) while @operator.size > 0
|
61
|
+
|
62
|
+
@output.reverse
|
63
|
+
end
|
64
|
+
|
65
|
+
private
|
66
|
+
|
67
|
+
# Look up the precedence of an operator.
|
68
|
+
#
|
69
|
+
# operator - LexicalUnit operator.
|
70
|
+
#
|
71
|
+
# Returns the Fixnum precedence of `operator`.
|
72
|
+
def precedence(operator)
|
73
|
+
if operator.fname?
|
74
|
+
PRECEDENCE['()'][:val]
|
75
|
+
else
|
76
|
+
PRECEDENCE[operator.value][:val]
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
# Insert the multiplication operator (`*`) where it is needed inside
|
81
|
+
# the @units member. This is because in Electr the `*` is implicit.
|
82
|
+
#
|
83
|
+
# Examples
|
84
|
+
#
|
85
|
+
# # @units before: 2 3
|
86
|
+
# # @units after: 2 * 3
|
87
|
+
#
|
88
|
+
# Returns nothing.
|
89
|
+
def insert_multiplication
|
90
|
+
temp = []
|
91
|
+
while unit = @units.shift
|
92
|
+
temp << unit
|
93
|
+
if @units.first
|
94
|
+
if (@units.first.number? && unit.number?) ||
|
95
|
+
(@units.first.open_parenthesis? && unit.number?) ||
|
96
|
+
(@units.first.fname? && unit.number?)
|
97
|
+
temp << LexicalUnit.operator('*')
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
@units = temp
|
102
|
+
end
|
103
|
+
|
104
|
+
end
|
105
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Electr
|
2
|
+
|
3
|
+
# A Syntaxer transform a list of lexical units into an AST.
|
4
|
+
class Syntaxer
|
5
|
+
|
6
|
+
# lexical_units - Array of LexicalUnits
|
7
|
+
def initialize(lexical_units)
|
8
|
+
@units = lexical_units
|
9
|
+
@ast = AST.new "ast"
|
10
|
+
end
|
11
|
+
|
12
|
+
# Public: Compile lexical units into an AST.
|
13
|
+
#
|
14
|
+
# Returns the AST.
|
15
|
+
def run
|
16
|
+
RootRule.new(@units, @ast).apply!
|
17
|
+
@ast
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
module Electr
|
2
|
+
|
3
|
+
class Tokenizer
|
4
|
+
|
5
|
+
ONE_CHAR_SYMBOLS = %w( + - / )
|
6
|
+
|
7
|
+
def initialize(string)
|
8
|
+
@index = 0
|
9
|
+
@token = ''
|
10
|
+
@look_ahead = ''
|
11
|
+
@codeline = string
|
12
|
+
forward_look_ahead
|
13
|
+
end
|
14
|
+
|
15
|
+
def has_more_token?
|
16
|
+
@index <= @codeline.size
|
17
|
+
end
|
18
|
+
|
19
|
+
def next_token
|
20
|
+
ret = produce_next_token
|
21
|
+
skip_white
|
22
|
+
@token = ''
|
23
|
+
ret
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def forward_look_ahead
|
29
|
+
@look_ahead = @codeline[@index, 1]
|
30
|
+
@index += 1
|
31
|
+
end
|
32
|
+
|
33
|
+
def produce_next_token
|
34
|
+
if @look_ahead =~ /[0-9]/
|
35
|
+
get_number
|
36
|
+
elsif ONE_CHAR_SYMBOLS.include?(@look_ahead)
|
37
|
+
add_this_char
|
38
|
+
elsif @look_ahead == '('
|
39
|
+
add_this_char
|
40
|
+
elsif @look_ahead == ')'
|
41
|
+
add_this_char
|
42
|
+
else
|
43
|
+
get_word
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def skip_white
|
48
|
+
forward_look_ahead while @look_ahead == ' '
|
49
|
+
end
|
50
|
+
|
51
|
+
def get_number
|
52
|
+
add_look_ahead while @look_ahead =~ /[0-9.]/
|
53
|
+
if @token[-1] != '.'
|
54
|
+
add_look_ahead while @look_ahead =~ /[kKRuFpΩμAmWV]/
|
55
|
+
end
|
56
|
+
@token
|
57
|
+
end
|
58
|
+
|
59
|
+
def add_look_ahead
|
60
|
+
@token << @look_ahead
|
61
|
+
forward_look_ahead
|
62
|
+
end
|
63
|
+
|
64
|
+
def get_word
|
65
|
+
add_look_ahead while @look_ahead =~ /\w/
|
66
|
+
@token
|
67
|
+
end
|
68
|
+
|
69
|
+
# Add current character to the current token and return it.
|
70
|
+
# Usefull shorthand for single character's tokens.
|
71
|
+
#
|
72
|
+
# Returns the String current token.
|
73
|
+
def add_this_char
|
74
|
+
add_look_ahead
|
75
|
+
@token
|
76
|
+
end
|
77
|
+
|
78
|
+
end
|
79
|
+
end
|
data/lib/electr/repl.rb
ADDED
@@ -0,0 +1,56 @@
|
|
1
|
+
module Electr
|
2
|
+
|
3
|
+
class Repl
|
4
|
+
|
5
|
+
BANNER = "\nElectr version #{Electr::VERSION}\n" +
|
6
|
+
"A tiny language for electronic formulas\n\n" +
|
7
|
+
"Hit Ctrl+C to quit Electr\n\n"
|
8
|
+
|
9
|
+
def initialize(options)
|
10
|
+
@options = options
|
11
|
+
puts BANNER
|
12
|
+
end
|
13
|
+
|
14
|
+
def run
|
15
|
+
if @options[:ast]
|
16
|
+
print_ast(read_ast) while true
|
17
|
+
else
|
18
|
+
_print(_eval(_read)) while true
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def _read
|
25
|
+
print("E> ")
|
26
|
+
line = STDIN.gets.chomp
|
27
|
+
compiler = Compiler.new
|
28
|
+
compiler.compile_to_pn(line)
|
29
|
+
end
|
30
|
+
|
31
|
+
def read_ast
|
32
|
+
print("E--ast> ")
|
33
|
+
line = STDIN.gets.chomp
|
34
|
+
compiler = Compiler.new
|
35
|
+
compiler.compile_to_ast(line)
|
36
|
+
end
|
37
|
+
|
38
|
+
def _eval(pns)
|
39
|
+
evaluator = Evaluator.new
|
40
|
+
evaluator.evaluate_pn(pns)
|
41
|
+
end
|
42
|
+
|
43
|
+
def _print(result)
|
44
|
+
if result == result.truncate
|
45
|
+
puts result.truncate
|
46
|
+
else
|
47
|
+
puts result.round(10)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def print_ast(ast)
|
52
|
+
ast.display
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|
56
|
+
end
|
data/lib/electr/rules.rb
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
include Electr
|
4
|
+
|
5
|
+
describe Compiler do
|
6
|
+
|
7
|
+
CODES_TO_COMPILE = [
|
8
|
+
"21 + 3",
|
9
|
+
"2",
|
10
|
+
"2 pi",
|
11
|
+
"2 11K",
|
12
|
+
"sqrt(49) + 1",
|
13
|
+
"sqrt(49)",
|
14
|
+
"1 + sqrt(49)",
|
15
|
+
"3 / (2 + 1)",
|
16
|
+
"sqrt(40 + 9)",
|
17
|
+
"3 (2 + 1)",
|
18
|
+
"2 sqrt(3)",
|
19
|
+
]
|
20
|
+
|
21
|
+
specify "#compile_to_ast" do
|
22
|
+
CODES_TO_COMPILE.each do |code|
|
23
|
+
compiler = Compiler.new
|
24
|
+
result = compiler.compile_to_ast(code)
|
25
|
+
expect(result).to be_a AST
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
specify do
|
30
|
+
compiler = Compiler.new
|
31
|
+
result = compiler.compile_to_ast("2 pi 0.5uF sqrt(11K 22K)")
|
32
|
+
result.display
|
33
|
+
expect(result).to be_a AST
|
34
|
+
end
|
35
|
+
|
36
|
+
specify "#compile_to_pn" do
|
37
|
+
compiler = Compiler.new
|
38
|
+
result = compiler.compile_to_pn("2 pi")
|
39
|
+
expect(result).to be_a Array
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
data/spec/electr_spec.rb
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
include Electr
|
4
|
+
|
5
|
+
describe Evaluator do
|
6
|
+
|
7
|
+
CODES_TO_EVAL = [
|
8
|
+
{code: "2", result: 2},
|
9
|
+
{code: "2 3", result: 6},
|
10
|
+
{code: "2 pi", result: 2 * Math::PI},
|
11
|
+
{code: "2 + 3", result: 5},
|
12
|
+
{code: "2 - 3", result: -1},
|
13
|
+
{code: "3 / 2", result: 1.5},
|
14
|
+
{code: "2 11k", result: 22_000},
|
15
|
+
{code: "sqrt(49)", result: 7},
|
16
|
+
{code: "3V / 25mA", result: 120},
|
17
|
+
{code: "110V / 2A", result: 55},
|
18
|
+
{code: "10mV 100R", result: 1},
|
19
|
+
{code: "10W 2", result: 20},
|
20
|
+
{code: "100mW 2", result: 0.2},
|
21
|
+
]
|
22
|
+
|
23
|
+
specify "#evaluate_pn" do
|
24
|
+
CODES_TO_EVAL.each do |code|
|
25
|
+
compiler = Compiler.new
|
26
|
+
pns = compiler.compile_to_pn(code[:code])
|
27
|
+
|
28
|
+
evaluator = Evaluator.new
|
29
|
+
result = evaluator.evaluate_pn(pns)
|
30
|
+
|
31
|
+
expect(result).to eq code[:result]
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
specify do
|
36
|
+
compiler = Compiler.new
|
37
|
+
pns = compiler.compile_to_pn("1 - 1 + 4 - 1 - 1")
|
38
|
+
|
39
|
+
evaluator = Evaluator.new
|
40
|
+
result = evaluator.evaluate_pn(pns)
|
41
|
+
|
42
|
+
expect(result).to eq 2
|
43
|
+
end
|
44
|
+
|
45
|
+
|
46
|
+
|
47
|
+
end
|