salt 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 +37 -0
- data/.rspec +2 -0
- data/.rubocop.yml +11 -0
- data/.travis.yml +10 -0
- data/.yardopts +6 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +59 -0
- data/LICENSE +22 -0
- data/README.md +2 -0
- data/Rakefile +6 -0
- data/bin/salt +7 -0
- data/lib/salt.rb +19 -0
- data/lib/salt/cli.rb +40 -0
- data/lib/salt/compiler/type.rb +15 -0
- data/lib/salt/generator.rb +11 -0
- data/lib/salt/generator/main.rb +7 -0
- data/lib/salt/parser.ace +353 -0
- data/lib/salt/parser.rb +4031 -0
- data/lib/salt/scanner.rb +143 -0
- data/lib/salt/scanner/main.rb +134 -0
- data/lib/salt/scanner/token.rb +45 -0
- data/lib/salt/version.rb +3 -0
- data/lib/salt/walker.rb +47 -0
- data/salt.gemspec +30 -0
- data/spec/salt/language/scanner_spec.rb +26 -0
- data/spec/spec_helper.rb +71 -0
- data/test.rb +33 -0
- metadata +173 -0
data/lib/salt/scanner.rb
ADDED
@@ -0,0 +1,143 @@
|
|
1
|
+
require 'forwardable'
|
2
|
+
require 'salt/scanner/main'
|
3
|
+
require 'salt/scanner/token'
|
4
|
+
|
5
|
+
module Salt
|
6
|
+
# Scans a given input.
|
7
|
+
#
|
8
|
+
# @see http://ruby-doc.org/stdlib-2.1.2/libdoc/strscan/rdoc/StringScanner.html
|
9
|
+
class Scanner
|
10
|
+
extend Forwardable
|
11
|
+
include Main
|
12
|
+
|
13
|
+
# An array of the tokens that the scanner scanned.
|
14
|
+
#
|
15
|
+
# @return [Array<Token>]
|
16
|
+
attr_reader :tokens
|
17
|
+
|
18
|
+
# Scans a file. It returns the tokens resulting from scanning.
|
19
|
+
#
|
20
|
+
# @param source [String] the source to scan. This should be
|
21
|
+
# compatible with StringScanner.
|
22
|
+
# @param name [String] the name of the source file. This is
|
23
|
+
# primarily used in backtrace information.
|
24
|
+
# @return [Array<Token>]
|
25
|
+
# @see #tokens
|
26
|
+
def self.scan(source, name = '(file)')
|
27
|
+
new(source, name).scan_file
|
28
|
+
end
|
29
|
+
|
30
|
+
# Initialize the scanner with the input.
|
31
|
+
#
|
32
|
+
# @param input [String] The source to scan.
|
33
|
+
# @param source [String] the source file. This is primarily
|
34
|
+
# used in backtrace information.
|
35
|
+
def initialize(input, source = '(file)')
|
36
|
+
@source = source
|
37
|
+
@scanner = StringScanner.new(input)
|
38
|
+
@tokens = []
|
39
|
+
end
|
40
|
+
|
41
|
+
# Scans the file in parts.
|
42
|
+
#
|
43
|
+
# @raise [SyntaxError] if the source is malformed in some way.
|
44
|
+
# @return [Array<Token>] the tokens that
|
45
|
+
# were scanned in this file.
|
46
|
+
# @see #scan_first_part
|
47
|
+
# @see #scan_second_part
|
48
|
+
# @see #scan_third_part
|
49
|
+
# @see #tokens
|
50
|
+
def scan_file
|
51
|
+
@line = 1
|
52
|
+
scan
|
53
|
+
tokens
|
54
|
+
rescue SyntaxError => e
|
55
|
+
start = [@scanner.pos - 8, 0].max
|
56
|
+
stop = [@scanner.pos + 8, @scanner.string.length].min
|
57
|
+
snip = @scanner.string[start..stop].strip.inspect
|
58
|
+
char = @scanner.string[@scanner.pos]
|
59
|
+
char = if char
|
60
|
+
char.inspect
|
61
|
+
else
|
62
|
+
'EOF'
|
63
|
+
end
|
64
|
+
|
65
|
+
new_line = "#{@source}:#{@line}: unexpected #{char} " \
|
66
|
+
"(near #{snip})"
|
67
|
+
|
68
|
+
raise e, e.message, [new_line, *e.backtrace]
|
69
|
+
end
|
70
|
+
|
71
|
+
# Scans for whitespace. If the next character is whitespace, it
|
72
|
+
# will consume all whitespace until the next non-whitespace
|
73
|
+
# character.
|
74
|
+
#
|
75
|
+
# @return [Boolean] if any whitespace was matched.
|
76
|
+
def scan_whitespace
|
77
|
+
@line += @scanner[1].count("\n") if @scanner.scan(/(\s+)/)
|
78
|
+
end
|
79
|
+
|
80
|
+
private
|
81
|
+
|
82
|
+
attr_reader :line
|
83
|
+
|
84
|
+
def column
|
85
|
+
@scanner.pos - (@scanner.string.rindex(/\n|\r/, @scanner.pos) || 0)
|
86
|
+
end
|
87
|
+
|
88
|
+
# Raises an error.
|
89
|
+
#
|
90
|
+
# @raise [SyntaxError] always.
|
91
|
+
# @return [void]
|
92
|
+
def error!
|
93
|
+
raise SyntaxError, 'invalid syntax'
|
94
|
+
end
|
95
|
+
|
96
|
+
# Matches using the scanner. If it matches, it returns a truthy
|
97
|
+
# value. If it fails to match, it returns nil.
|
98
|
+
#
|
99
|
+
# @param scan [String, Symbol, Regexp] The match. If it is a
|
100
|
+
# string or a symbol, it is turned into a regular expression
|
101
|
+
# through interpolation.
|
102
|
+
# @return [Boolean, nil]
|
103
|
+
def on(scan)
|
104
|
+
case scan
|
105
|
+
when String, Symbol
|
106
|
+
@scanner.scan(/#{Regexp.escape(scan.to_s)}/)
|
107
|
+
when Regexp
|
108
|
+
@scanner.scan(scan)
|
109
|
+
else
|
110
|
+
raise ArgumentError, "Unexpected #{scan.class}, expected " \
|
111
|
+
'String, Symbol, or Regexp'
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
# Generates a token with the given name and values.
|
116
|
+
#
|
117
|
+
# @param name [Symbol] the name of the token.
|
118
|
+
# @param values [Object] the values to be part of the token.
|
119
|
+
# @return [Token]
|
120
|
+
def token(name, *values)
|
121
|
+
Token.new(name, values, line, column)
|
122
|
+
end
|
123
|
+
|
124
|
+
# Emits a token, by first generating the token, and then
|
125
|
+
# appending it to the token array.
|
126
|
+
#
|
127
|
+
# @see #token
|
128
|
+
# @param (see #token)
|
129
|
+
# @return [Array<Token>] The token array.
|
130
|
+
def emit(name, *values)
|
131
|
+
tokens << token(name, *values)
|
132
|
+
end
|
133
|
+
|
134
|
+
# The group from the last match. It responds to #[] only.
|
135
|
+
#
|
136
|
+
# @return [#[]]
|
137
|
+
def group
|
138
|
+
@scanner
|
139
|
+
end
|
140
|
+
|
141
|
+
def_delegators :@scanner, :eos?
|
142
|
+
end
|
143
|
+
end
|
@@ -0,0 +1,134 @@
|
|
1
|
+
module Salt
|
2
|
+
class Scanner
|
3
|
+
module Main
|
4
|
+
NUMBER = %{
|
5
|
+
(
|
6
|
+
[0-9]+
|
7
|
+
| 0[xX][0-9a-f]+
|
8
|
+
| 0b[01]+
|
9
|
+
)
|
10
|
+
}
|
11
|
+
|
12
|
+
DOUBLE = %{([0-9]+\.[0-9]+)}
|
13
|
+
|
14
|
+
def scan
|
15
|
+
until eos?
|
16
|
+
scan_keyword || scan_grouping || scan_logimath ||
|
17
|
+
scan_control || scan_string || scan_number ||
|
18
|
+
scan_identifier || scan_comment || scan_whitespace ||
|
19
|
+
error!
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def scan_keyword
|
24
|
+
case
|
25
|
+
when on(:function) then emit(:function)
|
26
|
+
when on(:func) then emit(:func)
|
27
|
+
when on(:class) then emit(:class)
|
28
|
+
when on(:struct) then emit(:struct)
|
29
|
+
when on(:union) then emit(:union)
|
30
|
+
when on(:continue) then emit(:continue)
|
31
|
+
when on(:break) then emit(:break)
|
32
|
+
when on(:if) then emit(:if)
|
33
|
+
when on(:else) then emit(:else)
|
34
|
+
when on(:for) then emit(:for)
|
35
|
+
when on(:while) then emit(:while)
|
36
|
+
when on(:do) then emit(:do)
|
37
|
+
when on(:goto) then emit(:goto)
|
38
|
+
when on(:return) then emit(:return)
|
39
|
+
when on(:typedef) then emit(:typedef)
|
40
|
+
when on(:label) then emit(:label)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def scan_grouping
|
45
|
+
case
|
46
|
+
when on('{') then emit(:lbrace)
|
47
|
+
when on('}') then emit(:rbrace)
|
48
|
+
when on('(') then emit(:lparen)
|
49
|
+
when on(')') then emit(:rparen)
|
50
|
+
when on('[') then emit(:lbrack)
|
51
|
+
when on(']') then emit(:rbrack)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def scan_logimath
|
56
|
+
case
|
57
|
+
when on('++') then emit(:dplus)
|
58
|
+
when on('--') then emit(:dminus)
|
59
|
+
when on('<<') then emit(:lshift)
|
60
|
+
when on('>>') then emit(:rshift)
|
61
|
+
when on('==') then emit(:comp)
|
62
|
+
when on('!=') then emit(:ncmp)
|
63
|
+
when on('&&') then emit(:land)
|
64
|
+
when on('||') then emit(:lor)
|
65
|
+
when on('=') then emit(:equals)
|
66
|
+
when on('+') then emit(:plus)
|
67
|
+
when on('-') then emit(:minus)
|
68
|
+
when on('/') then emit(:divide)
|
69
|
+
when on('%') then emit(:mod)
|
70
|
+
when on('&') then emit(:amber)
|
71
|
+
when on('^') then emit(:caret)
|
72
|
+
when on('*') then emit(:star)
|
73
|
+
when on('<') then emit(:lbrk)
|
74
|
+
when on('>') then emit(:rbrk)
|
75
|
+
when on('!') then emit(:lnot)
|
76
|
+
when on('~') then emit(:bnot)
|
77
|
+
when on('|') then emit(:bor)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def scan_control
|
82
|
+
case
|
83
|
+
when on('::') then emit(:dcolon)
|
84
|
+
when on(':') then emit(:colon)
|
85
|
+
when on(';') then emit(:semicolon)
|
86
|
+
when on('#') then emit(:hash)
|
87
|
+
when on(',') then emit(:comma)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def scan_string
|
92
|
+
case
|
93
|
+
when on(/\" ([^\"] | \\")* \"/)
|
94
|
+
emit(:string, group[1])
|
95
|
+
when on(/'(.)'/)
|
96
|
+
emit(:character, group[1])
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
def scan_number
|
101
|
+
case
|
102
|
+
when on(/#{NUMBER}(ull|llu)/x)
|
103
|
+
emit(:ullnumber, group[1])
|
104
|
+
when on(/#{NUMBER}(ul|lu)/x)
|
105
|
+
emit(:ulnumber, group[1])
|
106
|
+
when on(/#{NUMBER}(su|us)/x)
|
107
|
+
emit(:usnumber, group[1])
|
108
|
+
when on(/#{NUMBER}ll/x)
|
109
|
+
emit(:llnumber, group[1])
|
110
|
+
when on(/#{NUMBER}l/x)
|
111
|
+
emit(:lnumber, group[1])
|
112
|
+
when on(/#{NUMBER}s/x)
|
113
|
+
emit(:snumber, group[1])
|
114
|
+
when on(/#{NUMBER}u/x)
|
115
|
+
emit(:unumber, group[1])
|
116
|
+
when on(/#{NUMBER}/x)
|
117
|
+
emit(:number, group[1])
|
118
|
+
when on(/#{DOUBLE}f/)
|
119
|
+
emit(:float, group[1])
|
120
|
+
when on(DOUBLE)
|
121
|
+
emit(:double, group[1])
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
def scan_identifier
|
126
|
+
emit(:identifier, group[1]) if on(/([A-Za-z][A-Za-z0-9_]*)/)
|
127
|
+
end
|
128
|
+
|
129
|
+
def scan_comment
|
130
|
+
on(%r{//(.*?)\n})
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
require 'forwardable'
|
2
|
+
|
3
|
+
module Salt
|
4
|
+
class Scanner
|
5
|
+
class Token
|
6
|
+
attr_reader :type
|
7
|
+
attr_reader :values
|
8
|
+
attr_reader :line
|
9
|
+
attr_reader :column
|
10
|
+
extend Forwardable
|
11
|
+
include Comparable
|
12
|
+
|
13
|
+
def_delegators :to_a, :[]
|
14
|
+
|
15
|
+
def initialize(type, values, line = 0, column = 0)
|
16
|
+
@type = type.to_s.upcase.intern
|
17
|
+
@values = values
|
18
|
+
@line = line
|
19
|
+
@column = column
|
20
|
+
@array = [@type].concat(@values.clone.freeze).freeze
|
21
|
+
freeze
|
22
|
+
end
|
23
|
+
|
24
|
+
def to_a
|
25
|
+
@array
|
26
|
+
end
|
27
|
+
|
28
|
+
def inspect
|
29
|
+
"#<#{self.class}(#{self.type}) #{self.values.inspect}>"
|
30
|
+
end
|
31
|
+
|
32
|
+
def <=>(other)
|
33
|
+
case other
|
34
|
+
when Token
|
35
|
+
to_a <=> other.to_a
|
36
|
+
when Array
|
37
|
+
to_a <=> other
|
38
|
+
else
|
39
|
+
raise ArgumentError,
|
40
|
+
"Don't know how to compare Token with #{other}"
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
data/lib/salt/version.rb
ADDED
data/lib/salt/walker.rb
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'pry'
|
4
|
+
|
5
|
+
module Salt
|
6
|
+
# A class that is used to help walk the AST generated by Salt (or
|
7
|
+
# even any AST that uses arrays to define Nodes and Tokens).
|
8
|
+
class Walker
|
9
|
+
attr_reader :ast
|
10
|
+
|
11
|
+
def initialize(ast)
|
12
|
+
@ast = ast
|
13
|
+
@stack = []
|
14
|
+
end
|
15
|
+
|
16
|
+
def perform
|
17
|
+
to([@ast])
|
18
|
+
until @stack.empty?
|
19
|
+
yield self
|
20
|
+
@stack.shift
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def on(action)
|
25
|
+
if action.is_a?(Hash) && action.key?(current_type)
|
26
|
+
action[current_type].each do |index|
|
27
|
+
to(current[index])
|
28
|
+
end
|
29
|
+
elsif !action.is_a?(Hash)
|
30
|
+
yield current[1..-1] if current_type == action
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def to(sub)
|
35
|
+
return unless sub
|
36
|
+
@stack.concat(sub)
|
37
|
+
end
|
38
|
+
|
39
|
+
def current
|
40
|
+
@stack.first
|
41
|
+
end
|
42
|
+
|
43
|
+
def current_type
|
44
|
+
current[0]
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
data/salt.gemspec
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'salt/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = 'salt'
|
8
|
+
spec.version = Salt::VERSION
|
9
|
+
spec.authors = ['Jeremy Rodi']
|
10
|
+
spec.email = ['redjazz96@gmail.com']
|
11
|
+
spec.summary = 'A sane alternative language to C.'
|
12
|
+
spec.description = 'A sane alternative language to C. It ' \
|
13
|
+
'compiles directly to C, which then compiles.'
|
14
|
+
spec.homepage = 'https://github.com/medcat/salt'
|
15
|
+
spec.license = 'MIT'
|
16
|
+
|
17
|
+
spec.files = `git ls-files -z`.split("\x0")
|
18
|
+
spec.executables = spec.files.grep(/^bin\//) { |f| File.basename(f) }
|
19
|
+
spec.test_files = spec.files.grep(/^spec\//)
|
20
|
+
spec.require_paths = ['lib']
|
21
|
+
|
22
|
+
spec.add_development_dependency 'bundler', '~> 1.6'
|
23
|
+
spec.add_development_dependency 'rake', '~> 10.0'
|
24
|
+
spec.add_development_dependency 'rspec', '~> 3.1'
|
25
|
+
spec.add_development_dependency 'rubocop'
|
26
|
+
spec.add_development_dependency 'rubocop-rspec'
|
27
|
+
|
28
|
+
spec.add_dependency 'toml-rb', '~> 0.3'
|
29
|
+
spec.add_dependency 'commander'
|
30
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
RSpec.describe Salt::Language::Scanner do
|
2
|
+
let(:input) { %{function main(): int { return 0; } } }
|
3
|
+
let(:source) { 'test.sa' }
|
4
|
+
subject { described_class.scan(input, source) }
|
5
|
+
|
6
|
+
it 'generates tokens' do
|
7
|
+
expect(subject.any?).to be true
|
8
|
+
expect(subject.map(&:class).
|
9
|
+
all? { |_| _ == Salt::Language::Scanner::Token }).to be true
|
10
|
+
end
|
11
|
+
|
12
|
+
it 'properly tokenizes' do
|
13
|
+
expect(subject.map(&:to_a)).to eq [
|
14
|
+
[:FUNCTION], [:IDENTIFIER, 'main'], [:LPAREN], [:RPAREN],
|
15
|
+
[:COLON], [:IDENTIFIER, 'int'],
|
16
|
+
[:LBRACE], [:RETURN], [:NUMBER, '0'], [:SEMICOLON], [:RBRACE]
|
17
|
+
]
|
18
|
+
end
|
19
|
+
|
20
|
+
it 'adds line and column information' do
|
21
|
+
expect(subject.map { |_| [_.line, _.column] }).to eq [
|
22
|
+
[1, 0], [1, 9], [1, 13], [1, 14], [1, 15], [1, 17], [1, 21],
|
23
|
+
[1, 23], [1, 30], [1, 31], [1, 33]
|
24
|
+
]
|
25
|
+
end
|
26
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,71 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'salt'
|
4
|
+
require 'timeout'
|
5
|
+
require 'benchmark'
|
6
|
+
|
7
|
+
RSpec.configure do |config|
|
8
|
+
config.expect_with :rspec do |expectations|
|
9
|
+
# This option will default to `true` in RSpec 4. It makes the `description`
|
10
|
+
# and `failure_message` of custom matchers include text for helper methods
|
11
|
+
# defined using `chain`, e.g.:
|
12
|
+
# be_bigger_than(2).and_smaller_than(4).description
|
13
|
+
# # => "be bigger than 2 and smaller than 4"
|
14
|
+
# ...rather than:
|
15
|
+
# # => "be bigger than 2"
|
16
|
+
expectations.include_chain_clauses_in_custom_matcher_descriptions = true
|
17
|
+
expectations.syntax = :expect
|
18
|
+
end
|
19
|
+
|
20
|
+
config.mock_with :rspec do |mocks|
|
21
|
+
# Prevents you from mocking or stubbing a method that does not exist on
|
22
|
+
# a real object. This is generally recommended, and will default to
|
23
|
+
# `true` in RSpec 4.
|
24
|
+
mocks.verify_partial_doubles = true
|
25
|
+
end
|
26
|
+
|
27
|
+
# These two settings work together to allow you to limit a spec run
|
28
|
+
# to individual examples or groups you care about by tagging them with
|
29
|
+
# `:focus` metadata. When nothing is tagged with `:focus`, all examples
|
30
|
+
# get run.
|
31
|
+
config.filter_run :focus
|
32
|
+
config.run_all_when_everything_filtered = true
|
33
|
+
|
34
|
+
# Limits the available syntax to the non-monkey patched syntax that is recommended.
|
35
|
+
# For more details, see:
|
36
|
+
# - http://myronmars.to/n/dev-blog/2012/06/rspecs-new-expectation-syntax
|
37
|
+
# - http://teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/
|
38
|
+
# - http://myronmars.to/n/dev-blog/2014/05/notable-changes-in-rspec-3#new__config_option_to_disable_rspeccore_monkey_patching
|
39
|
+
config.disable_monkey_patching!
|
40
|
+
|
41
|
+
# This setting enables warnings. It's recommended, but in some cases may
|
42
|
+
# be too noisy due to issues in dependencies.
|
43
|
+
config.warnings = true
|
44
|
+
|
45
|
+
# Many RSpec users commonly either run the entire suite or an individual
|
46
|
+
# file, and it's useful to allow more verbose output when running an
|
47
|
+
# individual spec file.
|
48
|
+
if config.files_to_run.one?
|
49
|
+
# Use the documentation formatter for detailed output,
|
50
|
+
# unless a formatter has already been configured
|
51
|
+
# (e.g. via a command-line flag).
|
52
|
+
config.default_formatter = 'doc'
|
53
|
+
end
|
54
|
+
|
55
|
+
# Print the 10 slowest examples and example groups at the
|
56
|
+
# end of the spec run, to help surface which specs are running
|
57
|
+
# particularly slow.
|
58
|
+
# config.profile_examples = 10
|
59
|
+
|
60
|
+
# Run specs in random order to surface order dependencies. If you find an
|
61
|
+
# order dependency and want to debug it, you can fix the order by providing
|
62
|
+
# the seed, which is printed after each run.
|
63
|
+
# --seed 1234
|
64
|
+
config.order = :random
|
65
|
+
|
66
|
+
# Seed global randomization in this process using the `--seed` CLI option.
|
67
|
+
# Setting this allows you to use `--seed` to deterministically reproduce
|
68
|
+
# test failures related to randomization by passing the same `--seed` value
|
69
|
+
# as the one that triggered the failure.
|
70
|
+
Kernel.srand config.seed
|
71
|
+
end
|