Browse Source

Initial commit

master
Dylan Baker 5 years ago
commit
0db77c281e

+ 11
- 0
.gitignore View File

@@ -0,0 +1,11 @@
1
+/.bundle/
2
+/.yardoc
3
+/_yardoc/
4
+/coverage/
5
+/doc/
6
+/pkg/
7
+/spec/reports/
8
+/tmp/
9
+
10
+# rspec failure tracking
11
+.rspec_status

+ 3
- 0
.rspec View File

@@ -0,0 +1,3 @@
1
+--format documentation
2
+--color
3
+--require spec_helper

+ 6
- 0
Gemfile View File

@@ -0,0 +1,6 @@
1
+source "https://rubygems.org"
2
+
3
+git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
4
+
5
+# Specify your gem's dependencies in ahem.gemspec
6
+gemspec

+ 35
- 0
Gemfile.lock View File

@@ -0,0 +1,35 @@
1
+PATH
2
+  remote: .
3
+  specs:
4
+    ahem (0.1.0)
5
+
6
+GEM
7
+  remote: https://rubygems.org/
8
+  specs:
9
+    diff-lcs (1.3)
10
+    rake (10.5.0)
11
+    rspec (3.8.0)
12
+      rspec-core (~> 3.8.0)
13
+      rspec-expectations (~> 3.8.0)
14
+      rspec-mocks (~> 3.8.0)
15
+    rspec-core (3.8.0)
16
+      rspec-support (~> 3.8.0)
17
+    rspec-expectations (3.8.2)
18
+      diff-lcs (>= 1.2.0, < 2.0)
19
+      rspec-support (~> 3.8.0)
20
+    rspec-mocks (3.8.0)
21
+      diff-lcs (>= 1.2.0, < 2.0)
22
+      rspec-support (~> 3.8.0)
23
+    rspec-support (3.8.0)
24
+
25
+PLATFORMS
26
+  ruby
27
+
28
+DEPENDENCIES
29
+  ahem!
30
+  bundler (~> 1.17)
31
+  rake (~> 10.0)
32
+  rspec (~> 3.0)
33
+
34
+BUNDLED WITH
35
+   1.17.3

+ 21
- 0
LICENSE View File

@@ -0,0 +1,21 @@
1
+The MIT License (MIT)
2
+
3
+Copyright (c) 2019 Dylan Baker
4
+
5
+Permission is hereby granted, free of charge, to any person obtaining a copy
6
+of this software and associated documentation files (the "Software"), to deal
7
+in the Software without restriction, including without limitation the rights
8
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+copies of the Software, and to permit persons to whom the Software is
10
+furnished to do so, subject to the following conditions:
11
+
12
+The above copyright notice and this permission notice shall be included in
13
+all copies or substantial portions of the Software.
14
+
15
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+THE SOFTWARE.

+ 39
- 0
README.md View File

@@ -0,0 +1,39 @@
1
+# Ahem
2
+
3
+Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/ahem`. To experiment with that code, run `bin/console` for an interactive prompt.
4
+
5
+TODO: Delete this and the text above, and describe your gem
6
+
7
+## Installation
8
+
9
+Add this line to your application's Gemfile:
10
+
11
+```ruby
12
+gem 'ahem'
13
+```
14
+
15
+And then execute:
16
+
17
+    $ bundle
18
+
19
+Or install it yourself as:
20
+
21
+    $ gem install ahem
22
+
23
+## Usage
24
+
25
+TODO: Write usage instructions here
26
+
27
+## Development
28
+
29
+After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
30
+
31
+To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
32
+
33
+## Contributing
34
+
35
+Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/ahem.
36
+
37
+## License
38
+
39
+The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).

+ 6
- 0
Rakefile View File

@@ -0,0 +1,6 @@
1
+require "bundler/gem_tasks"
2
+require "rspec/core/rake_task"
3
+
4
+RSpec::Core::RakeTask.new(:spec)
5
+
6
+task :default => :spec

+ 33
- 0
ahem.gemspec View File

@@ -0,0 +1,33 @@
1
+lib = File.expand_path("../lib", __FILE__)
2
+$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
+require "ahem/version"
4
+
5
+Gem::Specification.new do |spec|
6
+  spec.name          = "ahem"
7
+  spec.version       = Ahem::VERSION
8
+  spec.authors       = ["Dylan Baker"]
9
+  spec.email         = ["dylan@simulacrum.party"]
10
+
11
+  spec.summary       = %q{A little language}
12
+  spec.homepage      = "https://git.sr.ht/~simulacrumparty/ahem"
13
+  spec.license       = "MIT"
14
+
15
+  if spec.respond_to?(:metadata)
16
+    spec.metadata["homepage_uri"] = spec.homepage
17
+    spec.metadata["source_code_uri"] = spec.homepage
18
+  else
19
+    raise "RubyGems 2.0 or newer is required to protect against " \
20
+      "public gem pushes."
21
+  end
22
+
23
+  spec.files         = Dir.chdir(File.expand_path('..', __FILE__)) do
24
+    `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
25
+  end
26
+  spec.bindir        = "exe"
27
+  spec.executables   = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
28
+  spec.require_paths = ["lib"]
29
+
30
+  spec.add_development_dependency "bundler", "~> 1.17"
31
+  spec.add_development_dependency "rake", "~> 10.0"
32
+  spec.add_development_dependency "rspec", "~> 3.0"
33
+end

+ 4
- 0
bin/ahem View File

@@ -0,0 +1,4 @@
1
+#!/usr/bin/env ruby
2
+require 'ahem'
3
+
4
+CLI::run

+ 14
- 0
bin/console View File

@@ -0,0 +1,14 @@
1
+#!/usr/bin/env ruby
2
+
3
+require "bundler/setup"
4
+require "ahem"
5
+
6
+# You can add fixtures and/or initialization code here to make experimenting
7
+# with your gem easier. You can also use a different console, if you like.
8
+
9
+# (If you use this, don't forget to add pry to your Gemfile!)
10
+# require "pry"
11
+# Pry.start
12
+
13
+require "irb"
14
+IRB.start(__FILE__)

+ 8
- 0
bin/setup View File

@@ -0,0 +1,8 @@
1
+#!/usr/bin/env bash
2
+set -euo pipefail
3
+IFS=$'\n\t'
4
+set -vx
5
+
6
+bundle install
7
+
8
+# Do any other automated setup that you need to do here

+ 9
- 0
lib/ahem.rb View File

@@ -0,0 +1,9 @@
1
+require 'ahem/ast'
2
+require 'ahem/cli'
3
+require 'ahem/lexer'
4
+require 'ahem/parser'
5
+require 'ahem/token'
6
+require 'ahem/token_kinds'
7
+require 'ahem/version'
8
+
9
+module Ahem; end

+ 18
- 0
lib/ahem/ast.rb View File

@@ -0,0 +1,18 @@
1
+module AST
2
+  require 'ahem/ast/array'
3
+  require 'ahem/ast/binary'
4
+  require 'ahem/ast/block'
5
+  require 'ahem/ast/boolean'
6
+  require 'ahem/ast/branch'
7
+  require 'ahem/ast/class_definition'
8
+  require 'ahem/ast/conditional'
9
+  require 'ahem/ast/function_call'
10
+  require 'ahem/ast/function_definition'
11
+  require 'ahem/ast/identifier'
12
+  require 'ahem/ast/member'
13
+  require 'ahem/ast/null'
14
+  require 'ahem/ast/number'
15
+  require 'ahem/ast/operators'
16
+  require 'ahem/ast/string'
17
+  require 'ahem/ast/variable_declaration'
18
+end

+ 11
- 0
lib/ahem/ast/array.rb View File

@@ -0,0 +1,11 @@
1
+class AST::Array
2
+  attr_reader :elements
3
+
4
+  def initialize(elements)
5
+    @elements = elements
6
+  end
7
+
8
+  def ==(other)
9
+    other.is_a?(AST::Array) && other.elements == @elements
10
+  end
11
+end

+ 14
- 0
lib/ahem/ast/binary.rb View File

@@ -0,0 +1,14 @@
1
+class AST::Binary
2
+  attr_reader :operation, :left, :right
3
+
4
+  def initialize(operation, left, right)
5
+    @operation = operation
6
+    @left = left
7
+    @right = right
8
+  end
9
+
10
+  def ==(other)
11
+    other.operation == @operation && other.left == @left &&
12
+      other.right == @right
13
+  end
14
+end

+ 11
- 0
lib/ahem/ast/block.rb View File

@@ -0,0 +1,11 @@
1
+class AST::Block
2
+  attr_reader :statements
3
+
4
+  def initialize(statements)
5
+    @statements = statements
6
+  end
7
+
8
+  def ==(other)
9
+    other.is_a?(AST::Block) && other.statements == @statements
10
+  end
11
+end

+ 11
- 0
lib/ahem/ast/boolean.rb View File

@@ -0,0 +1,11 @@
1
+class AST::Boolean
2
+  attr_reader :value
3
+
4
+  def initialize(value)
5
+    @value = value
6
+  end
7
+
8
+  def ==(other)
9
+    other.is_a?(AST::Boolean) && other.value == @value
10
+  end
11
+end

+ 13
- 0
lib/ahem/ast/branch.rb View File

@@ -0,0 +1,13 @@
1
+class AST::Branch
2
+  attr_reader :condition, :block
3
+
4
+  def initialize(condition, block)
5
+    @condition = condition
6
+    @block = block
7
+  end
8
+
9
+  def ==(other)
10
+    other.is_a?(AST::Branch) && other.condition == @condition &&
11
+      other.block == @block
12
+  end
13
+end

+ 15
- 0
lib/ahem/ast/class_definition.rb View File

@@ -0,0 +1,15 @@
1
+class AST::ClassDefinition
2
+  attr_reader :name, :members, :methods
3
+
4
+  def initialize(name, members, methods)
5
+    @name = name
6
+    @members = members
7
+    @methods = methods
8
+  end
9
+
10
+  def ==(other)
11
+    other.is_a?(AST::ClassDefinition) && other.name == @name &&
12
+      other.members == @members &&
13
+      other.methods == @methods
14
+  end
15
+end

+ 11
- 0
lib/ahem/ast/conditional.rb View File

@@ -0,0 +1,11 @@
1
+class AST::Conditional
2
+  attr_reader :branches
3
+
4
+  def initialize(branches)
5
+    @branches = branches
6
+  end
7
+
8
+  def ==(other)
9
+    other.is_a?(AST::Conditional) && other.branches == @branches
10
+  end
11
+end

+ 13
- 0
lib/ahem/ast/function_call.rb View File

@@ -0,0 +1,13 @@
1
+class AST::FunctionCall
2
+  attr_reader :function, :arguments
3
+
4
+  def initialize(function, arguments)
5
+    @function = function
6
+    @arguments = arguments
7
+  end
8
+
9
+  def ==(other)
10
+    other.is_a?(AST::FunctionCall) && other.function == @function &&
11
+      other.arguments == @arguments
12
+  end
13
+end

+ 15
- 0
lib/ahem/ast/function_definition.rb View File

@@ -0,0 +1,15 @@
1
+class AST::FunctionDefinition
2
+  attr_reader :name, :parameters, :body
3
+
4
+  def initialize(name, parameters, body)
5
+    @name = name
6
+    @parameters = parameters
7
+    @body = body
8
+  end
9
+
10
+  def ==(other)
11
+    other.is_a?(AST::FunctionDefinition) && other.name == @name &&
12
+      other.parameters == @parameters &&
13
+      other.body == @body
14
+  end
15
+end

+ 11
- 0
lib/ahem/ast/identifier.rb View File

@@ -0,0 +1,11 @@
1
+class AST::Identifier
2
+  attr_reader :name
3
+
4
+  def initialize(name)
5
+    @name = name
6
+  end
7
+
8
+  def ==(other)
9
+    other.is_a?(AST::Identifier) && other.name == @name
10
+  end
11
+end

+ 13
- 0
lib/ahem/ast/member.rb View File

@@ -0,0 +1,13 @@
1
+class AST::Member
2
+  attr_reader :name, :is_public
3
+
4
+  def initialize(name, is_public)
5
+    @name = name
6
+    @is_public = is_public
7
+  end
8
+
9
+  def ==(other)
10
+    other.is_a?(AST::Member) && other.name == @name &&
11
+      other.is_public == @is_public
12
+  end
13
+end

+ 5
- 0
lib/ahem/ast/null.rb View File

@@ -0,0 +1,5 @@
1
+class AST::Null
2
+  def ==(other)
3
+    other.is_a?(AST::Null)
4
+  end
5
+end

+ 11
- 0
lib/ahem/ast/number.rb View File

@@ -0,0 +1,11 @@
1
+class AST::Number
2
+  attr_reader :value
3
+
4
+  def initialize(value)
5
+    @value = value
6
+  end
7
+
8
+  def ==(other)
9
+    other.is_a?(AST::Number) && other.value == @value
10
+  end
11
+end

+ 12
- 0
lib/ahem/ast/operators.rb View File

@@ -0,0 +1,12 @@
1
+module AST::Operators
2
+  ADD = :+
3
+  SUBTRACT = :-
4
+  MULTIPLY = :*
5
+  DIVIDE = :/
6
+  LESS_THAN = :<
7
+  GREATER_THAN = :>
8
+  LESS_THAN_OR_EQUAL = :<=
9
+  GREATER_THAN_OR_EQUAL = :>=
10
+  OR = :or
11
+  AND = :and
12
+end

+ 11
- 0
lib/ahem/ast/string.rb View File

@@ -0,0 +1,11 @@
1
+class AST::String
2
+  attr_reader :value
3
+
4
+  def initialize(value)
5
+    @value = value
6
+  end
7
+
8
+  def ==(other)
9
+    other.is_a?(AST::String) && other.value == @value
10
+  end
11
+end

+ 13
- 0
lib/ahem/ast/variable_declaration.rb View File

@@ -0,0 +1,13 @@
1
+class AST::VariableDeclaration
2
+  attr_reader :name, :value
3
+
4
+  def initialize(name, value)
5
+    @name = name
6
+    @value = value
7
+  end
8
+
9
+  def ==(other)
10
+    other.is_a?(AST::VariableDeclaration) && other.name == @name &&
11
+      other.value == @value
12
+  end
13
+end

+ 9
- 0
lib/ahem/cli.rb View File

@@ -0,0 +1,9 @@
1
+require 'pp'
2
+
3
+class CLI
4
+  def self.run
5
+    file = ARGV.first
6
+    source = File.read(file).strip
7
+    Parser.new(Lexer.new(source)).parse.each { |node| pp node }
8
+  end
9
+end

+ 145
- 0
lib/ahem/lexer.rb View File

@@ -0,0 +1,145 @@
1
+class Lexer
2
+  def initialize(source)
3
+    @source = source.split("\n").join(' ')
4
+    @position = 0
5
+  end
6
+
7
+  def get_token
8
+    return Token.new(TokenKinds::EOF) if at_end
9
+
10
+    @position += 1 while !at_end && @source.slice(@position).match(/\s/)
11
+
12
+    source = @source.slice(@position..-1)
13
+    if source.match(/^null/)
14
+      @position += 4
15
+      Token.new(TokenKinds::NULL)
16
+    elsif source.match(/^true/)
17
+      @position += 4
18
+      Token.new(TokenKinds::BOOLEAN, true)
19
+    elsif source.match(/^false/)
20
+      @position += 5
21
+      Token.new(TokenKinds::BOOLEAN, false)
22
+    elsif source.match(/^if/)
23
+      @position += 2
24
+      Token.new(TokenKinds::IF)
25
+    elsif source.match(/^elseif/)
26
+      @position += 6
27
+      Token.new(TokenKinds::ELSEIF)
28
+    elsif source.match(/^else/)
29
+      @position += 4
30
+      Token.new(TokenKinds::ELSE)
31
+    elsif source.match(/^\d+(\.\d+)?/)
32
+      number = source.match(/^\d+(\.\d+)?/)[0]
33
+      @position += number.size
34
+      Token.new(TokenKinds::NUMBER, number.to_f)
35
+    elsif source.match(/^"(.*)"/)
36
+      string = source.match(/^"([^"]*)"/)[1]
37
+      @position += (string.size + 2)
38
+      Token.new(TokenKinds::STRING, string)
39
+    elsif source.match(/^\+/)
40
+      @position += 1
41
+      Token.new(TokenKinds::OPERATOR, :+)
42
+    elsif source.match(/^\-/)
43
+      @position += 1
44
+      Token.new(TokenKinds::OPERATOR, :-)
45
+    elsif source.match(/^\*/)
46
+      @position += 1
47
+      Token.new(TokenKinds::OPERATOR, :*)
48
+    elsif source.match(%r{^\/})
49
+      @position += 1
50
+      Token.new(TokenKinds::OPERATOR, :/)
51
+    elsif source.match(/^\{/)
52
+      @position += 1
53
+      Token.new(TokenKinds::LBRACE)
54
+    elsif source.match(/^\}/)
55
+      @position += 1
56
+      Token.new(TokenKinds::RBRACE)
57
+    elsif source.match(/^\(/)
58
+      @position += 1
59
+      Token.new(TokenKinds::LPAREN)
60
+    elsif source.match(/^\)/)
61
+      @position += 1
62
+      Token.new(TokenKinds::RPAREN)
63
+    elsif source.match(/^\[/)
64
+      @position += 1
65
+      Token.new(TokenKinds::LBRACKET)
66
+    elsif source.match(/^\]/)
67
+      @position += 1
68
+      Token.new(TokenKinds::RBRACKET)
69
+    elsif source.match(/^\;/)
70
+      @position += 1
71
+      Token.new(TokenKinds::SEMICOLON)
72
+    elsif source.match(/^,/)
73
+      @position += 1
74
+      Token.new(TokenKinds::COMMA)
75
+    elsif source.match(/^\./)
76
+      @position += 1
77
+      Token.new(TokenKinds::DOT)
78
+    elsif source.match(/^\<=/)
79
+      @position += 2
80
+      Token.new(TokenKinds::OPERATOR, :<=)
81
+    elsif source.match(/^\>\=/)
82
+      @position += 2
83
+      Token.new(TokenKinds::OPERATOR, :>=)
84
+    elsif source.match(/^\</)
85
+      @position += 1
86
+      Token.new(TokenKinds::OPERATOR, :<)
87
+    elsif source.match(/^\>/)
88
+      @position += 1
89
+      Token.new(TokenKinds::OPERATOR, :>)
90
+    elsif source.match(/^and/)
91
+      @position += 3
92
+      Token.new(TokenKinds::OPERATOR, :and)
93
+    elsif source.match(/^or/)
94
+      @position += 2
95
+      Token.new(TokenKinds::OPERATOR, :or)
96
+    elsif source.match(/^\=/)
97
+      @position += 1
98
+      Token.new(TokenKinds::EQUALS)
99
+    elsif source.match(/^let/)
100
+      @position += 3
101
+      Token.new(TokenKinds::LET)
102
+    elsif source.match(/^function/)
103
+      @position += 8
104
+      Token.new(TokenKinds::FUNCTION)
105
+    elsif source.match(/^class/)
106
+      @position += 5
107
+      Token.new(TokenKinds::CLASS)
108
+    elsif source.match(/^public/)
109
+      @position += 6
110
+      Token.new(TokenKinds::PUBLIC)
111
+    elsif source.match(/^private/)
112
+      @position += 7
113
+      Token.new(TokenKinds::PRIVATE)
114
+    elsif source.match(/^[a-z][a-zA-Z0-9_]*/)
115
+      identifier = source.match(/^[a-z][a-zA-Z0-9_]*/)[0]
116
+      @position += identifier.size
117
+      Token.new(TokenKinds::IDENTIFIER, identifier)
118
+    elsif source.match(/^[A-Z][a-zA-Z0-9_]*/)
119
+      class_name = source.match(/^[A-Z][a-zA-Z0-9_]*/)[0]
120
+      @position += class_name.size
121
+      Token.new(TokenKinds::CLASS_NAME, class_name)
122
+    else
123
+      throw "Unrecognized character #{source[0]}"
124
+    end
125
+  end
126
+
127
+  def scan_all
128
+    tokens = Array.new
129
+    until at_end
130
+      if @source.slice(@position).match(/\s/)
131
+        @position += 1
132
+      else
133
+        tokens << get_token
134
+      end
135
+    end
136
+    tokens << Token.new(TokenKinds::EOF)
137
+    tokens
138
+  end
139
+
140
+  private
141
+
142
+  def at_end
143
+    @position == @source.size
144
+  end
145
+end

+ 299
- 0
lib/ahem/parser.rb View File

@@ -0,0 +1,299 @@
1
+class Parser
2
+  def initialize(lexer)
3
+    @lexer = lexer
4
+    @current_token = @lexer.get_token
5
+  end
6
+
7
+  def parse
8
+    tree = Array.new
9
+
10
+    tree << statement until at_end
11
+
12
+    tree
13
+  end
14
+
15
+  def statement
16
+    if @current_token.type == TokenKinds::IF
17
+      conditional
18
+    elsif @current_token.type == TokenKinds::LET
19
+      variable_declaration
20
+    elsif @current_token.type == TokenKinds::FUNCTION
21
+      function_definition
22
+    elsif @current_token.type == TokenKinds::CLASS
23
+      class_definition
24
+    else
25
+      expr = expression
26
+      eat(TokenKinds::SEMICOLON)
27
+      expr
28
+    end
29
+  end
30
+
31
+  def variable_declaration
32
+    eat(TokenKinds::LET)
33
+    name = identifier
34
+    eat(TokenKinds::EQUALS)
35
+    value = expression
36
+    eat(TokenKinds::SEMICOLON)
37
+    AST::VariableDeclaration.new(name, value)
38
+  end
39
+
40
+  def function_definition
41
+    eat(TokenKinds::FUNCTION)
42
+    name = identifier
43
+    params = parameters
44
+    body = block
45
+    AST::FunctionDefinition.new(name, params, body)
46
+  end
47
+
48
+  def class_definition
49
+    eat(TokenKinds::CLASS)
50
+
51
+    class_name = AST::Identifier.new(eat(TokenKinds::CLASS_NAME).value)
52
+
53
+    eat(TokenKinds::LBRACE)
54
+
55
+    members = Array.new
56
+
57
+    while [TokenKinds::PUBLIC, TokenKinds::PRIVATE].include?(
58
+      @current_token.type
59
+    )
60
+      is_public =
61
+        if @current_token.type == TokenKinds::PUBLIC
62
+          eat(TokenKinds::PUBLIC)
63
+          true
64
+        elsif @current_token.type == TokenKinds::PRIVATE
65
+          eat(TokenKinds::PRIVATE)
66
+          false
67
+        end
68
+
69
+      while !at_end && true
70
+        name = identifier
71
+        members << AST::Member.new(name, is_public)
72
+        if @current_token.type == TokenKinds::COMMA
73
+          eat(TokenKinds::COMMA)
74
+        else
75
+          break
76
+        end
77
+      end
78
+
79
+      eat(TokenKinds::SEMICOLON)
80
+    end
81
+
82
+    methods = Array.new
83
+    while @current_token.type == TokenKinds::FUNCTION
84
+      methods << function_definition
85
+    end
86
+
87
+    eat(TokenKinds::RBRACE)
88
+
89
+    AST::ClassDefinition.new(class_name, members, methods)
90
+  end
91
+
92
+  def conditional
93
+    branches = Array.new
94
+    eat(TokenKinds::IF)
95
+    condition = expression
96
+    block = self.block
97
+    branches << AST::Branch.new(condition, block)
98
+
99
+    while @current_token.type == TokenKinds::ELSEIF
100
+      eat(TokenKinds::ELSEIF)
101
+      condition = expression
102
+      block = self.block
103
+      branches << AST::Branch.new(condition, block)
104
+    end
105
+
106
+    if @current_token.type == TokenKinds::ELSE
107
+      eat(TokenKinds::ELSE)
108
+      block = self.block
109
+      branches << AST::Branch.new(AST::Boolean.new(true), block)
110
+    end
111
+
112
+    AST::Conditional.new(branches)
113
+  end
114
+
115
+  def parameters
116
+    params = Array.new
117
+    eat(TokenKinds::LPAREN)
118
+
119
+    while @current_token.type == TokenKinds::IDENTIFIER
120
+      params << identifier
121
+
122
+      if @current_token.type == TokenKinds::COMMA
123
+        eat(TokenKinds::COMMA)
124
+      else
125
+        break
126
+      end
127
+    end
128
+
129
+    eat(TokenKinds::RPAREN)
130
+
131
+    params
132
+  end
133
+
134
+  def block
135
+    statements = Array.new
136
+
137
+    eat(TokenKinds::LBRACE)
138
+    statements << statement until @current_token.type == TokenKinds::RBRACE
139
+    eat(TokenKinds::RBRACE)
140
+
141
+    AST::Block.new(statements)
142
+  end
143
+
144
+  def expression
145
+    expr = binary
146
+    if @current_token.type == TokenKinds::LPAREN
147
+      args = arguments
148
+      AST::FunctionCall.new(expr, args)
149
+    else
150
+      expr
151
+    end
152
+  end
153
+
154
+  private
155
+
156
+  def binary
157
+    left = comparison
158
+
159
+    if @current_token.type == TokenKinds::OPERATOR
160
+      operator = @current_token.value
161
+      advance
162
+      right = comparison
163
+      AST::Binary.new(operator, left, right)
164
+    else
165
+      left
166
+    end
167
+  end
168
+
169
+  def comparison
170
+    left = multiplication
171
+
172
+    if @current_token.type == TokenKinds::OPERATOR &&
173
+       %i[< > <= >=].include?(@current_token.type)
174
+      operator = @current_token.value
175
+      advance
176
+      right = multiplication
177
+      AST::Binary.new(operator, left, right)
178
+    else
179
+      left
180
+    end
181
+  end
182
+
183
+  def addition
184
+    left = multiplication
185
+
186
+    if @current_token.type == TokenKinds::OPERATOR &&
187
+       %i[+ -].include?(@current_token.value)
188
+      operator = @current_token.value
189
+      advance
190
+      right = multiplication
191
+      AST::Binary.new(operator, left, right)
192
+    else
193
+      left
194
+    end
195
+  end
196
+
197
+  def multiplication
198
+    left = primary
199
+
200
+    if @current_token.type == TokenKinds::OPERATOR &&
201
+       %i[* /].include?(@current_token.value)
202
+      operator = @current_token.value
203
+      advance
204
+      right = primary
205
+      AST::Binary.new(operator, left, right)
206
+    else
207
+      left
208
+    end
209
+  end
210
+
211
+  def primary
212
+    token = @current_token
213
+    case token.type
214
+    when TokenKinds::NULL
215
+      advance
216
+      AST::Null.new
217
+    when TokenKinds::BOOLEAN
218
+      advance
219
+      AST::Boolean.new(token.value)
220
+    when TokenKinds::NUMBER
221
+      advance
222
+      AST::Number.new(token.value)
223
+    when TokenKinds::STRING
224
+      advance
225
+      AST::String.new(token.value)
226
+    when TokenKinds::IDENTIFIER
227
+      identifier
228
+    when TokenKinds::LBRACKET
229
+      array
230
+    else
231
+      throw "Unexpected token #{token.type}"
232
+    end
233
+  end
234
+
235
+  def identifier
236
+    token = eat(TokenKinds::IDENTIFIER)
237
+    AST::Identifier.new(token.value)
238
+  end
239
+
240
+  def array
241
+    elements = Array.new
242
+    eat(TokenKinds::LBRACKET)
243
+
244
+    until @current_token.type == TokenKinds::RBRACKET
245
+      elements << expression
246
+
247
+      if @current_token.type == TokenKinds::COMMA
248
+        eat(TokenKinds::COMMA)
249
+      else
250
+        break
251
+      end
252
+    end
253
+
254
+    eat(TokenKinds::RBRACKET)
255
+
256
+    AST::Array.new(elements)
257
+  end
258
+
259
+  def arguments
260
+    args = Array.new
261
+    eat(TokenKinds::LPAREN)
262
+
263
+    until @current_token.type == TokenKinds::RPAREN
264
+      args << expression
265
+
266
+      if @current_token.type == TokenKinds::COMMA
267
+        eat(TokenKinds::COMMA)
268
+      else
269
+        break
270
+      end
271
+    end
272
+
273
+    eat(TokenKinds::RPAREN)
274
+
275
+    args
276
+  end
277
+
278
+  def eat(type)
279
+    token = @current_token
280
+    if token.type == type
281
+      advance
282
+      token
283
+    else
284
+      if token.nil?
285
+        throw "Unexpected #{token.type} - expected #{type}"
286
+      else
287
+        throw "Unexpected #{token.type} - expected #{type}"
288
+      end
289
+    end
290
+  end
291
+
292
+  def advance
293
+    @current_token = @lexer.get_token
294
+  end
295
+
296
+  def at_end
297
+    @current_token.type == TokenKinds::EOF
298
+  end
299
+end

+ 12
- 0
lib/ahem/token.rb View File

@@ -0,0 +1,12 @@
1
+class Token
2
+  attr_reader :type, :value
3
+
4
+  def initialize(type, value = nil)
5
+    @type = type
6
+    @value = value
7
+  end
8
+
9
+  def ==(other)
10
+    @type == other.type && @value == other.value
11
+  end
12
+end

+ 28
- 0
lib/ahem/token_kinds.rb View File

@@ -0,0 +1,28 @@
1
+module TokenKinds
2
+  BOOLEAN = :boolean
3
+  CLASS = :class
4
+  CLASS_NAME = :class_name
5
+  COMMA = :comma
6
+  DOT = :dot
7
+  ELSEIF = :elseif
8
+  ELSE = :else
9
+  EOF = :eof
10
+  EQUALS = :equals
11
+  FUNCTION = :function
12
+  IDENTIFIER = :identifier
13
+  IF = :if
14
+  LBRACE = :lbrace
15
+  LBRACKET = :lbracket
16
+  LPAREN = :lparen
17
+  LET = :let
18
+  NULL = :null
19
+  NUMBER = :number
20
+  OPERATOR = :operator
21
+  PRIVATE = :private
22
+  PUBLIC = :public
23
+  RBRACE = :rbrace
24
+  RBRACKET = :rbracket
25
+  RPAREN = :rparen
26
+  SEMICOLON = :semicolon
27
+  STRING = :string
28
+end

+ 3
- 0
lib/ahem/version.rb View File

@@ -0,0 +1,3 @@
1
+module Ahem
2
+  VERSION = '0.1.0'
3
+end

+ 125
- 0
spec/lexer_spec.rb View File

@@ -0,0 +1,125 @@
1
+RSpec.describe Lexer do
2
+  it 'lexes null' do
3
+    expect(Lexer.new('null').scan_all).to eq(
4
+      [Token.new(TokenKinds::NULL), Token.new(TokenKinds::EOF)]
5
+    )
6
+  end
7
+
8
+  it 'lexes booleans' do
9
+    expect(Lexer.new('true false').scan_all).to eq(
10
+      [
11
+        Token.new(TokenKinds::BOOLEAN, true),
12
+        Token.new(TokenKinds::BOOLEAN, false),
13
+        Token.new(TokenKinds::EOF)
14
+      ]
15
+    )
16
+  end
17
+
18
+  it 'lexes numbers' do
19
+    expect(Lexer.new('1 2.3').scan_all).to eq(
20
+      [
21
+        Token.new(TokenKinds::NUMBER, 1.0),
22
+        Token.new(TokenKinds::NUMBER, 2.3),
23
+        Token.new(TokenKinds::EOF)
24
+      ]
25
+    )
26
+  end
27
+
28
+  it 'lexes strings' do
29
+    expect(Lexer.new('"hello world"').scan_all).to eq(
30
+      [Token.new(TokenKinds::STRING, 'hello world'), Token.new(TokenKinds::EOF)]
31
+    )
32
+  end
33
+
34
+  it 'lexes operators' do
35
+    expect(Lexer.new('+-*/<<=>>= and or').scan_all).to eq(
36
+      [
37
+        Token.new(TokenKinds::OPERATOR, :+),
38
+        Token.new(TokenKinds::OPERATOR, :-),
39
+        Token.new(TokenKinds::OPERATOR, :*),
40
+        Token.new(TokenKinds::OPERATOR, :/),
41
+        Token.new(TokenKinds::OPERATOR, :<),
42
+        Token.new(TokenKinds::OPERATOR, :<=),
43
+        Token.new(TokenKinds::OPERATOR, :>),
44
+        Token.new(TokenKinds::OPERATOR, :>=),
45
+        Token.new(TokenKinds::OPERATOR, :and),
46
+        Token.new(TokenKinds::OPERATOR, :or),
47
+        Token.new(TokenKinds::EOF)
48
+      ]
49
+    )
50
+  end
51
+
52
+  it 'lexes punctuation' do
53
+    expect(Lexer.new('{}()[];,.').scan_all).to eq(
54
+      [
55
+        Token.new(TokenKinds::LBRACE),
56
+        Token.new(TokenKinds::RBRACE),
57
+        Token.new(TokenKinds::LPAREN),
58
+        Token.new(TokenKinds::RPAREN),
59
+        Token.new(TokenKinds::LBRACKET),
60
+        Token.new(TokenKinds::RBRACKET),
61
+        Token.new(TokenKinds::SEMICOLON),
62
+        Token.new(TokenKinds::COMMA),
63
+        Token.new(TokenKinds::DOT),
64
+        Token.new(TokenKinds::EOF)
65
+      ]
66
+    )
67
+  end
68
+
69
+  it 'lexes if elseif else' do
70
+    expect(Lexer.new('if elseif else').scan_all).to eq(
71
+      [
72
+        Token.new(TokenKinds::IF),
73
+        Token.new(TokenKinds::ELSEIF),
74
+        Token.new(TokenKinds::ELSE),
75
+        Token.new(TokenKinds::EOF)
76
+      ]
77
+    )
78
+  end
79
+
80
+  it 'lexes =' do
81
+    expect(Lexer.new('=').scan_all).to eq(
82
+      [Token.new(TokenKinds::EQUALS), Token.new(TokenKinds::EOF)]
83
+    )
84
+  end
85
+
86
+  it 'lexes let' do
87
+    expect(Lexer.new('let').scan_all).to eq(
88
+      [Token.new(TokenKinds::LET), Token.new(TokenKinds::EOF)]
89
+    )
90
+  end
91
+
92
+  it 'lexes identifiers' do
93
+    expect(Lexer.new('x').scan_all).to eq(
94
+      [Token.new(TokenKinds::IDENTIFIER, 'x'), Token.new(TokenKinds::EOF)]
95
+    )
96
+  end
97
+
98
+  it 'lexes function' do
99
+    expect(Lexer.new('function').scan_all).to eq(
100
+      [Token.new(TokenKinds::FUNCTION), Token.new(TokenKinds::EOF)]
101
+    )
102
+  end
103
+
104
+  it 'lexes class' do
105
+    expect(Lexer.new('class').scan_all).to eq(
106
+      [Token.new(TokenKinds::CLASS), Token.new(TokenKinds::EOF)]
107
+    )
108
+  end
109
+
110
+  it 'lexes class names' do
111
+    expect(Lexer.new('Class').scan_all).to eq(
112
+      [Token.new(TokenKinds::CLASS_NAME, 'Class'), Token.new(TokenKinds::EOF)]
113
+    )
114
+  end
115
+
116
+  it 'lexes public and private' do
117
+    expect(Lexer.new('public private').scan_all).to eq(
118
+      [
119
+        Token.new(TokenKinds::PUBLIC),
120
+        Token.new(TokenKinds::PRIVATE),
121
+        Token.new(TokenKinds::EOF)
122
+      ]
123
+    )
124
+  end
125
+end

+ 309
- 0
spec/parser_spec.rb View File

@@ -0,0 +1,309 @@
1
+RSpec.describe Parser do
2
+  def parse(source)
3
+    Parser.new(Lexer.new(source)).parse
4
+  end
5
+
6
+  it 'parses null' do
7
+    expect(parse('null;')).to eq([AST::Null.new])
8
+  end
9
+
10
+  it 'parses booleans' do
11
+    expect(parse('true;')).to eq([AST::Boolean.new(true)])
12
+    expect(parse('false;')).to eq([AST::Boolean.new(false)])
13
+  end
14
+
15
+  it 'parses numbers' do
16
+    expect(parse('5;')).to eq([AST::Number.new(5.0)])
17
+  end
18
+
19
+  it 'parses strings' do
20
+    expect(parse('"hello world";')).to eq([AST::String.new('hello world')])
21
+  end
22
+
23
+  it 'parses addition' do
24
+    expect(parse('1 + 2;')).to eq(
25
+      [
26
+        AST::Binary.new(
27
+          AST::Operators::ADD,
28
+          AST::Number.new(1.0),
29
+          AST::Number.new(2.0)
30
+        )
31
+      ]
32
+    )
33
+  end
34
+
35
+  it 'parses subtraction' do
36
+    expect(parse('1 - 2;')).to eq(
37
+      [
38
+        AST::Binary.new(
39
+          AST::Operators::SUBTRACT,
40
+          AST::Number.new(1.0),
41
+          AST::Number.new(2.0)
42
+        )
43
+      ]
44
+    )
45
+  end
46
+
47
+  it 'parses multiplication' do
48
+    expect(parse('1 * 2;')).to eq(
49
+      [
50
+        AST::Binary.new(
51
+          AST::Operators::MULTIPLY,
52
+          AST::Number.new(1.0),
53
+          AST::Number.new(2.0)
54
+        )
55
+      ]
56
+    )
57
+  end
58
+
59
+  it 'parses division' do
60
+    expect(parse('1 / 2;')).to eq(
61
+      [
62
+        AST::Binary.new(
63
+          AST::Operators::DIVIDE,
64
+          AST::Number.new(1.0),
65
+          AST::Number.new(2.0)
66
+        )
67
+      ]
68
+    )
69
+  end
70
+
71
+  it 'gives multiplication higher precedence than addition' do
72
+    expect(parse('1 + 2 * 3;')).to eq(
73
+      [
74
+        AST::Binary.new(
75
+          AST::Operators::ADD,
76
+          AST::Number.new(1.0),
77
+          AST::Binary.new(
78
+            AST::Operators::MULTIPLY,
79
+            AST::Number.new(2.0),
80
+            AST::Number.new(3.0)
81
+          )
82
+        )
83
+      ]
84
+    )
85
+  end
86
+
87
+  it 'gives multiplication higher precedence than subtraction' do
88
+    expect(parse('1 - 2 / 3;')).to eq(
89
+      [
90
+        AST::Binary.new(
91
+          AST::Operators::SUBTRACT,
92
+          AST::Number.new(1.0),
93
+          AST::Binary.new(
94
+            AST::Operators::DIVIDE,
95
+            AST::Number.new(2.0),
96
+            AST::Number.new(3.0)
97
+          )
98
+        )
99
+      ]
100
+    )
101
+  end
102
+
103
+  it 'parses if statement' do
104
+    expect(parse('if true { "true"; }')).to eq(
105
+      [
106
+        AST::Conditional.new(
107
+          [
108
+            AST::Branch.new(
109
+              AST::Boolean.new(true),
110
+              AST::Block.new([AST::String.new('true')])
111
+            )
112
+          ]
113
+        )
114
+      ]
115
+    )
116
+  end
117
+
118
+  it 'parses if else statement' do
119
+    expect(parse('if true { "true"; } else { "false"; }')).to eq(
120
+      [
121
+        AST::Conditional.new(
122
+          [
123
+            AST::Branch.new(
124
+              AST::Boolean.new(true),
125
+              AST::Block.new([AST::String.new('true')])
126
+            ),
127
+            AST::Branch.new(
128
+              AST::Boolean.new(true),
129
+              AST::Block.new([AST::String.new('false')])
130
+            )
131
+          ]
132
+        )
133
+      ]
134
+    )
135
+  end
136
+
137
+  it 'parses if elseif else statement' do
138
+    expect(
139
+      parse('if true { "true"; } elseif false { "false"; } else { "neither"; }')
140
+    ).to eq(
141
+      [
142
+        AST::Conditional.new(
143
+          [
144
+            AST::Branch.new(
145
+              AST::Boolean.new(true),
146
+              AST::Block.new([AST::String.new('true')])
147
+            ),
148
+            AST::Branch.new(
149
+              AST::Boolean.new(false),
150
+              AST::Block.new([AST::String.new('false')])
151
+            ),
152
+            AST::Branch.new(
153
+              AST::Boolean.new(true),
154
+              AST::Block.new([AST::String.new('neither')])
155
+            )
156
+          ]
157
+        )
158
+      ]
159
+    )
160
+  end
161
+
162
+  it 'parses comparisons' do
163
+    expect(parse('1 < 2;')).to eq(
164
+      [
165
+        AST::Binary.new(
166
+          AST::Operators::LESS_THAN,
167
+          AST::Number.new(1.0),
168
+          AST::Number.new(2.0)
169
+        )
170
+      ]
171
+    )
172
+    expect(parse('1 > 2;')).to eq(
173
+      [
174
+        AST::Binary.new(
175
+          AST::Operators::GREATER_THAN,
176
+          AST::Number.new(1.0),
177
+          AST::Number.new(2.0)
178
+        )
179
+      ]
180
+    )
181
+    expect(parse('1 <= 2;')).to eq(
182
+      [
183
+        AST::Binary.new(
184
+          AST::Operators::LESS_THAN_OR_EQUAL,
185
+          AST::Number.new(1.0),
186
+          AST::Number.new(2.0)
187
+        )
188
+      ]
189
+    )
190
+    expect(parse('1 >= 2;')).to eq(
191
+      [
192
+        AST::Binary.new(
193
+          AST::Operators::GREATER_THAN_OR_EQUAL,
194
+          AST::Number.new(1.0),
195
+          AST::Number.new(2.0)
196
+        )
197
+      ]
198
+    )
199
+    expect(parse('1 or 2;')).to eq(
200
+      [
201
+        AST::Binary.new(
202
+          AST::Operators::OR,
203
+          AST::Number.new(1.0),
204
+          AST::Number.new(2.0)
205
+        )
206
+      ]
207
+    )
208
+    expect(parse('1 and 2;')).to eq(
209
+      [
210
+        AST::Binary.new(
211
+          AST::Operators::AND,
212
+          AST::Number.new(1.0),
213
+          AST::Number.new(2.0)
214
+        )
215
+      ]
216
+    )
217
+  end
218
+
219
+  it 'parses identifier' do
220
+    expect(parse('x;')).to eq([AST::Identifier.new('x')])
221
+  end
222
+
223
+  it 'parses variable declaration' do
224
+    expect(parse('let x = 1;')).to eq(
225
+      [
226
+        AST::VariableDeclaration.new(
227
+          AST::Identifier.new('x'),
228
+          AST::Number.new(1.0)
229
+        )
230
+      ]
231
+    )
232
+  end
233
+
234
+  it 'parses function definition' do
235
+    result = [
236
+      AST::FunctionDefinition.new(
237
+        AST::Identifier.new('add_one'),
238
+        [AST::Identifier.new('x')],
239
+        AST::Block.new(
240
+          [
241
+            AST::Binary.new(
242
+              AST::Operators::ADD,
243
+              AST::Identifier.new('x'),
244
+              AST::Number.new(1.0)
245
+            )
246
+          ]
247
+        )
248
+      )
249
+    ]
250
+
251
+    expect(parse('function add_one(x) { x + 1; }')).to eq(result)
252
+    expect(parse('function add_one(x,) { x + 1; }')).to eq(result)
253
+  end
254
+
255
+  it 'parses function call' do
256
+    result = [
257
+      AST::FunctionCall.new(
258
+        AST::Identifier.new('func'),
259
+        [AST::Number.new(1.0), AST::Number.new(2.0)]
260
+      )
261
+    ]
262
+
263
+    expect(parse('func(1, 2);')).to eq(result)
264
+    expect(parse('func(1, 2,);')).to eq(result)
265
+  end
266
+
267
+  it 'parses array literal' do
268
+    result = [
269
+      AST::Array.new(
270
+        [AST::Number.new(1.0), AST::Number.new(2.0), AST::Number.new(3.0)]
271
+      )
272
+    ]
273
+
274
+    expect(parse('[1, 2, 3];')).to eq(result)
275
+    expect(parse('[1, 2, 3,];')).to eq(result)
276
+  end
277
+
278
+  it 'parses a class definition do' do
279
+    expect(
280
+      parse(<<~CLASS)
281
+          class Class {
282
+            public foo, bar;
283
+            private baz;
284
+            function method() {
285
+              "method";
286
+            }
287
+          }
288
+        CLASS
289
+    ).to eq(
290
+      [
291
+        AST::ClassDefinition.new(
292
+          AST::Identifier.new('Class'),
293
+          [
294
+            AST::Member.new(AST::Identifier.new('foo'), true),
295
+            AST::Member.new(AST::Identifier.new('bar'), true),
296
+            AST::Member.new(AST::Identifier.new('baz'), false)
297
+          ],
298
+          [
299
+            AST::FunctionDefinition.new(
300
+              AST::Identifier.new('method'),
301
+              [],
302
+              AST::Block.new([AST::String.new('method')])
303
+            )
304
+          ]
305
+        )
306
+      ]
307
+    )
308
+  end
309
+end

+ 15
- 0
spec/spec_helper.rb View File

@@ -0,0 +1,15 @@
1
+require 'bundler/setup'
2
+require 'ahem'
3
+
4
+RSpec.configure do |config|
5
+  # Enable flags like --only-failures and --next-failure
6
+  config.example_status_persistence_file_path =
7
+    '.rspec_status'
8
+
9
+  # Disable RSpec exposing methods globally on `Module` and `main`
10
+  config.disable_monkey_patching!
11
+
12
+  config.expect_with :rspec do |c|
13
+    c.syntax = :expect
14
+  end
15
+end

Loading…
Cancel
Save