electr 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- 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
|