Skip to content

Commit

Permalink
further iteration on the oterator
Browse files Browse the repository at this point in the history
  • Loading branch information
robertDurst committed Dec 29, 2023
1 parent 0b39b91 commit 6eb352b
Show file tree
Hide file tree
Showing 8 changed files with 142 additions and 72 deletions.
2 changes: 2 additions & 0 deletions .rubocop.yml
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
require: rubocop-rspec
AllCops:
NewCops: enable
38 changes: 19 additions & 19 deletions spec/zodiac/character_helpers_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,129 +9,129 @@
describe '.symbol?' do
context 'when symbol' do
it 'returns true' do
expect(symbol?('.')).to eq(true)
expect(symbol?('.')).to be(true)
end
end

context 'when not symbol' do
it 'returns false' do
expect(symbol?('a')).to eq(false)
expect(symbol?('a')).to be(false)
end
end
end

describe '.string_start?' do
context 'when string start' do
it 'returns true' do
expect(string_start?('"')).to eq(true)
expect(string_start?('"')).to be(true)
end
end

context 'when not string start' do
it 'returns false' do
expect(string_start?('a')).to eq(false)
expect(string_start?('a')).to be(false)
end
end
end

describe '.double_symbol?' do
context 'when double symbol' do
it 'returns true' do
expect(double_symbol?('*')).to eq(true)
expect(double_symbol?('*')).to be(true)
end
end

context 'when not double symbol' do
it 'returns false' do
expect(double_symbol?('-')).to eq(false)
expect(double_symbol?('-')).to be(false)
end
end
end

describe '.alpha_num?' do
context 'when letter' do
it 'returns true' do
expect(alpha_num?('a')).to eq(true)
expect(alpha_num?('a')).to be(true)
end
end

context 'when number' do
it 'returns true' do
expect(alpha_num?('1')).to eq(true)
expect(alpha_num?('1')).to be(true)
end
end

context 'when symbol' do
it 'returns false' do
expect(alpha_num?(':')).to eq(false)
expect(alpha_num?(':')).to be(false)
end
end
end

describe '.contains_equal_sign?' do
context 'when contains equal sign' do
it 'returns true' do
expect(contains_equal_sign?('a=b')).to eq(true)
expect(contains_equal_sign?('a=b')).to be(true)
end
end

context 'when does not contain equal sign' do
it 'returns false' do
expect(contains_equal_sign?('a')).to eq(false)
expect(contains_equal_sign?('a')).to be(false)
end
end
end

describe '.letter?' do
context 'when letter' do
it 'returns true' do
expect(letter?('a')).to eq(true)
expect(letter?('a')).to be(true)
end
end

context 'when number' do
it 'returns false' do
expect(letter?('1')).to eq(false)
expect(letter?('1')).to be(false)
end
end

context 'when symbol' do
it 'returns false' do
expect(letter?(':')).to eq(false)
expect(letter?(':')).to be(false)
end
end
end

describe '.number?' do
context 'when letter' do
it 'returns false' do
expect(number?('a')).to eq(false)
expect(number?('a')).to be(false)
end
end

context 'when number' do
it 'returns true' do
expect(number?('1')).to eq(true)
expect(number?('1')).to be(true)
end
end

context 'when symbol' do
it 'returns false' do
expect(number?(':')).to eq(false)
expect(number?(':')).to be(false)
end
end
end

describe '.underscore?' do
context 'when underscore' do
it 'returns true' do
expect(underscore?('_')).to eq(true)
expect(underscore?('_')).to be(true)
end
end

context 'when not underscore' do
it 'returns false' do
expect(underscore?('a')).to eq(false)
expect(underscore?('a')).to be(false)
end
end
end
Expand Down
11 changes: 10 additions & 1 deletion src/zodiac/character_helpers.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
module Zodiac
# Character helper methods common to parsing within the Zodiac language compiler.
module CharacterHelpers
def comment?(value)
value == '#'
end

def string_start?(value)
['"', "'", '`'].include?(value)
end
Expand All @@ -20,7 +24,8 @@ def double_symbol?(value)
end

def complex_symbol?(value, next_value)
!next_value.nil? && %w(+@ -@ [] =~).include?(value + next_value) || (double_symbol?(value) && value == next_value)
(!next_value.nil? && %w(+@ -@ []
=~).include?(value + next_value)) || (double_symbol?(value) && value == next_value)
end

def contains_equal_sign?(value)
Expand All @@ -31,6 +36,10 @@ def alpha_num?(value)
letter?(value) || number?(value) || underscore?(value)
end

def alpha?(value)
letter?(value) || underscore?(value)
end

def letter?(value)
value.match?(/[a-zA-Z]/)
end
Expand Down
92 changes: 46 additions & 46 deletions src/zodiac/lexer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
require './src/zodiac/character_helpers'
require './src/zodiac/lex_error'
require './src/zodiac/string_character_iterator'
require './src/zodiac/lexer_evaluator'
require './src/zodiac/lexer_evaluator_engine'

module Zodiac
# Base lexing class for the Zodiac language.
Expand All @@ -18,45 +20,49 @@ class Lexer

def initialize(raw_string)
@input_iterator = StringCharacterIterator.new(raw_string)
@lexer_evaluator_engine = LexerEvaluatorEngine.new(
[comment_lexer, op_assign_lexer, symbol_lexer, identifier_lexer, string_lexer, number_lexer],
proc { @input_iterator.iterate }
)
end

def lex
tokens = []

tokens << lex_next while @input_iterator.not_finished?
tokens << @lexer_evaluator_engine.execute(@input_iterator.peek) while @input_iterator.not_finished?

tokens.compact
tokens.compact.select { |token| token.is_a?(Hash) }
end

private

def lexers
[
{ token_kind: 'COMMENT', lexer: 'lex_comment', condition: proc { |top| top == '#' } },
{ token_kind: 'OP_ASGN', lexer: 'lex_op_assign', condition: proc { |_top| @input_iterator.op_assign_peek? } },
{ token_kind: 'SYMBOL', lexer: 'lex_symbol', condition: proc { |top| symbol?(top) } },
{ token_kind: 'IDENTIFIER', lexer: 'lex_identifier', condition: proc { |top|
letter?(top) || underscore?(top)
} },
{ token_kind: 'STRING', lexer: 'lex_string', condition: proc { |top| string_start?(top) } },
{ token_kind: 'NUMBER', lexer: 'lex_number', condition: proc { |top| number?(top) } }
]
### Comment lexing ###

def comment_lexer
LexerEvaluator.new('COMMENT', method(:comment?), method(:lex_comment))
end

def lex_next
lexers.each do |lexer|
next unless lexer[:condition].call(@input_iterator.peek)
def lex_comment
@input_iterator.take_until(proc { |val| val == "\n" })
end

return { kind: lexer[:token_kind], value: send(lexer[:lexer]) }
end
### Operator Assignment lexing ###
def lex_op_assign
@input_iterator.take_until(proc { |val| val == '=' }, after: 1)
end

# if we get here, we didn't lex anything, i.e. unrecognized character pattern
@input_iterator.iterate
def op_assign_lexer
LexerEvaluator.new('OP_ASGN', method(:op_assign?), method(:lex_op_assign))
end

nil
def op_assign?(_value)
@input_iterator.op_assign_peek?
end

### lexers ###
### Symbol lexing ###
def symbol_lexer
LexerEvaluator.new('SYMBOL', method(:symbol?), method(:lex_symbol))
end

def lex_symbol
word = @input_iterator.peek
Expand All @@ -70,41 +76,35 @@ def lex_symbol
word
end

def lex_op_assign
take_until(after: 1) { @input_iterator.peek != '=' }
### Identifier lexing ###
def identifier_lexer
LexerEvaluator.new('IDENTIFIER', method(:alpha?), method(:lex_identifier))
end

def lex_string
raise LexError, 'String not terminated' unless @input_iterator.rest_includes?(@input_iterator.peek)

take_until(before: 1, after: 1) { !string_start?(@input_iterator.peek) }
def lex_identifier
@input_iterator.take_until_not(method(:alpha_num?))
end

def lex_number
word = take_until { number?(@input_iterator.peek) }

return word += take_until(before: 1) { number?(@input_iterator.peek) } if @input_iterator.peek == '.'

word
### String lexing ###
def string_lexer
LexerEvaluator.new('STRING', method(:string_start?), method(:lex_string))
end

def lex_identifier
take_until { alpha_num?(@input_iterator.peek) }
end
def lex_string
raise LexError, 'String not terminated' unless @input_iterator.rest_includes?(@input_iterator.peek)

def lex_comment
take_until { @input_iterator.peek != "\n" }
@input_iterator.take_until(method(:string_start?), before: 1, after: 1)
end

### Helpers ###
def take_until(before: 0, after: 0)
word = ''

before.times { word += @input_iterator.iterate }
### Number lexing ###
def number_lexer
LexerEvaluator.new('NUMBER', method(:number?), method(:lex_number))
end

word += @input_iterator.iterate while @input_iterator.not_finished? && yield
def lex_number
word = @input_iterator.take_until_not(method(:number?))

after.times { word += @input_iterator.iterate }
word += @input_iterator.take_until_not(method(:number?), before: 1) if @input_iterator.peek == '.'

word
end
Expand Down
18 changes: 18 additions & 0 deletions src/zodiac/lexer_evaluator.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# frozen_string_literal: true

module Zodiac
# Encapsulates lexer evaluation logic for a specific token kind.
class LexerEvaluator
def initialize(token_kind, condition, evaluator)
@token_kind = token_kind
@condition = condition
@evaluator = evaluator
end

def evaluate(input)
return nil unless @condition.call(input)

{ kind: @token_kind, value: @evaluator.call }
end
end
end
23 changes: 23 additions & 0 deletions src/zodiac/lexer_evaluator_engine.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# frozen_string_literal: true

module Zodiac
# Evaluates a list of lexers and returns the first token that matches.
class LexerEvaluatorEngine
def initialize(lexers, default_evaluator)
@lexers = lexers
@default_evaluator = default_evaluator
end

def execute(input)
@lexers.each do |lexer|
token = lexer.evaluate(input)

return token if token
end

@default_evaluator.call

nil
end
end
end
6 changes: 0 additions & 6 deletions src/zodiac/main.rb

This file was deleted.

Loading

0 comments on commit 6eb352b

Please sign in to comment.