loxxy 0.3.01 → 0.4.01

Sign up to get free protection for your applications and to get access to all the features.
@@ -25,11 +25,15 @@ module Loxxy
25
25
  # @return [Hash{String => Variable}] Pairs of the kind
26
26
  attr_reader :defns
27
27
 
28
+ # @return [Array<LoxNode>] stack of values needed in evaluating an expression
29
+ attr_reader :expr_stack
30
+
28
31
  # Construct a environment instance.
29
32
  # @param aParent [Environment, NilClass] Parent environment to this one.
30
33
  def initialize(aParent = nil)
31
34
  @enclosing = aParent
32
35
  @defns = {}
36
+ @expr_stack = []
33
37
  end
34
38
 
35
39
  # Add a new variable to the environment.
@@ -7,17 +7,15 @@ module Loxxy
7
7
  module BackEnd
8
8
  # Runtime representation of a Lox class.
9
9
  class LoxClass
10
- # rubocop: disable Style/AccessorGrouping
11
-
12
10
  # @return [String] The name of the class
13
11
  attr_reader :name
14
12
  attr_reader :superclass
15
13
 
16
14
  # @return [Hash{String => LoxFunction}] the list of methods
17
15
  attr_reader :meths
18
- attr_reader :stack
19
16
 
20
- # rubocop: enable Style/AccessorGrouping
17
+ # @return [Loxxy::BackEnd::Engine]
18
+ attr_reader :engine
21
19
 
22
20
  # Create a class with given name
23
21
  # @param aName [String] The name of the class
@@ -28,11 +26,11 @@ module Loxxy
28
26
  theMethods.each do |func|
29
27
  meths[func.name] = func
30
28
  end
31
- @stack = anEngine.stack
29
+ @engine = anEngine
32
30
  end
33
31
 
34
32
  def accept(_visitor)
35
- stack.push self
33
+ engine.expr_stack.push self
36
34
  end
37
35
 
38
36
  def arity
@@ -46,9 +44,9 @@ module Loxxy
46
44
  if initializer
47
45
  constructor = initializer.bind(instance)
48
46
  constructor.call(engine, visitor)
47
+ else
48
+ engine.expr_stack.push(instance)
49
49
  end
50
-
51
- engine.stack.push(instance)
52
50
  end
53
51
 
54
52
  # @param aName [String] the method name to search for
@@ -13,7 +13,7 @@ module Loxxy
13
13
  # @return [Array<>] the parameters
14
14
  attr_reader :parameters
15
15
  attr_reader :body
16
- attr_reader :stack
16
+ attr_reader :engine
17
17
  attr_reader :closure
18
18
  attr_accessor :is_initializer
19
19
 
@@ -23,7 +23,7 @@ module Loxxy
23
23
  @name = aName.dup
24
24
  @parameters = parameterList
25
25
  @body = aBody.kind_of?(Ast::LoxNoopExpr) ? aBody : aBody.subnodes[0]
26
- @stack = anEngine.stack
26
+ @engine = anEngine
27
27
  @closure = anEngine.symbol_table.current_env
28
28
  @is_initializer = false
29
29
  anEngine.symbol_table.current_env.embedding = true
@@ -34,28 +34,34 @@ module Loxxy
34
34
  end
35
35
 
36
36
  def accept(_visitor)
37
- stack.push self
37
+ engine.expr_stack.push self
38
38
  end
39
39
 
40
- def call(engine, aVisitor)
40
+ def call(_engine, aVisitor)
41
41
  new_env = Environment.new(closure)
42
42
  engine.symbol_table.enter_environment(new_env)
43
43
 
44
44
  parameters&.each do |param_name|
45
- local = Variable.new(param_name, stack.pop)
45
+ local = Variable.new(param_name, engine.stack.pop)
46
46
  engine.symbol_table.insert(local)
47
47
  end
48
48
 
49
49
  catch(:return) do
50
- (body.nil? || body.kind_of?(Ast::LoxNoopExpr)) ? Datatype::Nil.instance : body.accept(aVisitor)
51
- throw(:return)
50
+ body.accept(aVisitor) unless body.nil? || body.kind_of?(Ast::LoxNoopExpr)
51
+ # implicit return at end of function...
52
+ engine.stack.push(Datatype::Nil.instance) unless is_initializer
52
53
  end
54
+ # Compensate for deeply nested return
55
+ engine.symbol_table.leave_environment while engine.current_env != new_env
56
+
53
57
  if is_initializer
54
58
  enclosing_env = engine.symbol_table.current_env.enclosing
55
59
  engine.stack.push(enclosing_env.defns['this'].value)
56
60
  end
57
61
 
58
62
  engine.symbol_table.leave_environment
63
+ # engine.expr_stack.clear
64
+ engine.expr_stack.push(engine.stack.pop) unless engine.stack.empty?
59
65
  end
60
66
 
61
67
  def bind(anInstance)
@@ -22,8 +22,16 @@ module Loxxy
22
22
  @fields = {}
23
23
  end
24
24
 
25
- def accept(_visitor)
26
- engine.stack.push self
25
+ # In Lox, only false and Nil have false value...
26
+ # @return [FalseClass]
27
+ def falsey?
28
+ false # Default implementation
29
+ end
30
+
31
+ # Any instance is truthy
32
+ # @return [TrueClass]
33
+ def truthy?
34
+ true # Default implementation
27
35
  end
28
36
 
29
37
  # Text representation of a Lox instance
@@ -31,6 +39,10 @@ module Loxxy
31
39
  "#{klass.to_str} instance"
32
40
  end
33
41
 
42
+ def accept(_visitor)
43
+ engine.expr_stack.push self
44
+ end
45
+
34
46
  # Look up the value of property with given name
35
47
  # aName [String] name of object property
36
48
  def get(aName)
@@ -133,10 +133,16 @@ module Loxxy
133
133
  end
134
134
 
135
135
  def after_set_expr(aSetExpr, aVisitor)
136
+ aSetExpr.value.accept(aVisitor)
136
137
  # Evaluate object part
137
138
  aSetExpr.object.accept(aVisitor)
138
139
  end
139
140
 
141
+ def after_logical_expr(aLogicalExpr, aVisitor)
142
+ # Force the visit of second operand (resolver should ignore shortcuts)
143
+ aLogicalExpr.operands.last.accept(aVisitor)
144
+ end
145
+
140
146
  # Variable expressions require their variables resolved
141
147
  def before_variable_expr(aVarExpr)
142
148
  var_name = aVarExpr.name
@@ -47,11 +47,10 @@ module Loxxy
47
47
 
48
48
  def validated_value(aValue)
49
49
  unless aValue.is_a?(String)
50
- raise StandardError, "Invalid number value #{aValue}"
50
+ raise StandardError, "Invalid string value #{aValue}"
51
51
  end
52
52
 
53
- # Remove double quotes delimiter
54
- aValue.gsub(/(^")|("$)/, '')
53
+ aValue
55
54
  end
56
55
  end # class
57
56
  end # module
@@ -7,9 +7,9 @@ module Loxxy
7
7
  module FrontEnd
8
8
  ########################################
9
9
  # Grammar for Lox language
10
- # Authoritave grammar at:
10
+ # Authoritative grammar at:
11
11
  # https://craftinginterpreters.com/appendix-i.html
12
- builder = Rley::Syntax::GrammarBuilder.new do
12
+ builder = Rley::grammar_builder do
13
13
  # Punctuators, separators...
14
14
  add_terminals('LEFT_PAREN', 'RIGHT_PAREN', 'LEFT_BRACE', 'RIGHT_BRACE')
15
15
  add_terminals('COMMA', 'DOT', 'MINUS', 'PLUS')
@@ -26,30 +26,21 @@ module Loxxy
26
26
  add_terminals('EOF')
27
27
 
28
28
  # Top-level rule that matches an entire Lox program
29
- rule('program' => 'EOF').as 'null_program'
30
- rule('program' => 'declaration_plus EOF').as 'lox_program'
29
+ rule('program' => 'declaration* EOF').as 'lox_program'
31
30
 
32
31
  # Declarations: bind an identifier to something
33
- rule('declaration_plus' => 'declaration_plus declaration').as 'declaration_plus_more'
34
- rule('declaration_plus' => 'declaration').as 'declaration_plus_end'
35
32
  rule('declaration' => 'classDecl')
36
33
  rule('declaration' => 'funDecl')
37
34
  rule('declaration' => 'varDecl')
38
35
  rule('declaration' => 'stmt')
39
36
 
40
37
  rule('classDecl' => 'CLASS classNaming class_body').as 'class_decl'
41
- rule('classNaming' => 'IDENTIFIER LESS IDENTIFIER').as 'class_subclassing'
42
- rule('classNaming' => 'IDENTIFIER').as 'class_name'
43
- rule('class_body' => 'LEFT_BRACE methods_opt RIGHT_BRACE').as 'class_body'
44
- rule('methods_opt' => 'method_plus')
45
- rule('methods_opt' => [])
46
- rule('method_plus' => 'method_plus function').as 'method_plus_more'
47
- rule('method_plus' => 'function').as 'method_plus_end'
38
+ rule('classNaming' => 'IDENTIFIER (LESS IDENTIFIER)?').as 'class_naming'
39
+ rule('class_body' => 'LEFT_BRACE function* RIGHT_BRACE').as 'class_body'
48
40
 
49
41
  rule('funDecl' => 'FUN function').as 'fun_decl'
50
42
 
51
- rule('varDecl' => 'VAR IDENTIFIER SEMICOLON').as 'var_declaration'
52
- rule('varDecl' => 'VAR IDENTIFIER EQUAL expression SEMICOLON').as 'var_initialization'
43
+ rule('varDecl' => 'VAR IDENTIFIER (EQUAL expression)? SEMICOLON').as 'var_declaration'
53
44
 
54
45
  # Statements: produce side effects, but don't introduce bindings
55
46
  rule('stmt' => 'statement')
@@ -65,12 +56,11 @@ module Loxxy
65
56
  rule('exprStmt' => 'expression SEMICOLON').as 'exprStmt'
66
57
 
67
58
  rule('forStmt' => 'FOR LEFT_PAREN forControl RIGHT_PAREN statement').as 'for_stmt'
68
- rule('forControl' => 'forInitialization forTest forUpdate').as 'for_control'
59
+ rule('forControl' => 'forInitialization forTest expression?').as 'for_control'
69
60
  rule('forInitialization' => 'varDecl')
70
61
  rule('forInitialization' => 'exprStmt')
71
62
  rule('forInitialization' => 'SEMICOLON').as 'empty_for_initialization'
72
- rule('forTest' => 'expression_opt SEMICOLON').as 'for_test'
73
- rule('forUpdate' => 'expression_opt')
63
+ rule('forTest' => 'expression? SEMICOLON').as 'for_test'
74
64
 
75
65
  rule('ifStmt' => 'IF ifCondition statement ELSE statement').as 'if_else_stmt'
76
66
  rule('unbalancedStmt' => 'IF ifCondition stmt').as 'if_stmt'
@@ -78,19 +68,14 @@ module Loxxy
78
68
  rule('ifCondition' => 'LEFT_PAREN expression RIGHT_PAREN').as 'keep_symbol2'
79
69
 
80
70
  rule('printStmt' => 'PRINT expression SEMICOLON').as 'print_stmt'
81
- rule('returnStmt' => 'RETURN expression_opt SEMICOLON').as 'return_stmt'
71
+ rule('returnStmt' => 'RETURN expression? SEMICOLON').as 'return_stmt'
82
72
  rule('whileStmt' => 'WHILE LEFT_PAREN expression RIGHT_PAREN statement').as 'while_stmt'
83
- rule('block' => 'LEFT_BRACE declaration_plus RIGHT_BRACE').as 'block_stmt'
84
- rule('block' => 'LEFT_BRACE RIGHT_BRACE').as 'block_empty'
73
+ rule('block' => 'LEFT_BRACE declaration* RIGHT_BRACE').as 'block_stmt'
85
74
 
86
75
  # Expressions: produce values
87
- rule('expression_opt' => 'expression')
88
- rule('expression_opt' => [])
89
76
  rule('expression' => 'assignment')
90
- rule('assignment' => 'owner_opt IDENTIFIER EQUAL assignment').as 'assign_expr'
77
+ rule('assignment' => '(call DOT)? IDENTIFIER EQUAL assignment').as 'assign_expr'
91
78
  rule('assignment' => 'logic_or')
92
- rule('owner_opt' => 'call DOT').as 'set_expr'
93
- rule('owner_opt' => [])
94
79
  rule('logic_or' => 'logic_and')
95
80
  rule('logic_or' => 'logic_and disjunct_plus').as 'logical_expr'
96
81
  rule('disjunct_plus' => 'disjunct_plus OR logic_and').as 'binary_plus_more'
@@ -130,10 +115,8 @@ module Loxxy
130
115
  rule('unaryOp' => 'BANG')
131
116
  rule('unaryOp' => 'MINUS')
132
117
  rule('call' => 'primary')
133
- rule('call' => 'primary refinement_plus').as 'call_expr'
134
- rule('refinement_plus' => 'refinement_plus refinement').as 'refinement_plus_more'
135
- rule('refinement_plus' => 'refinement').as 'refinement_plus_end'
136
- rule('refinement' => 'LEFT_PAREN arguments_opt RIGHT_PAREN').as 'call_arglist'
118
+ rule('call' => 'primary refinement+').as 'call_expr'
119
+ rule('refinement' => 'LEFT_PAREN arguments? RIGHT_PAREN').as 'call_arglist'
137
120
  rule('refinement' => 'DOT IDENTIFIER').as 'get_expr'
138
121
  rule('primary' => 'TRUE').as 'literal_expr'
139
122
  rule('primary' => 'FALSE').as 'literal_expr'
@@ -146,15 +129,9 @@ module Loxxy
146
129
  rule('primary' => 'SUPER DOT IDENTIFIER').as 'super_expr'
147
130
 
148
131
  # Utility rules
149
- rule('function' => 'IDENTIFIER LEFT_PAREN params_opt RIGHT_PAREN block').as 'function'
150
- rule('params_opt' => 'parameters')
151
- rule('params_opt' => [])
152
- rule('parameters' => 'parameters COMMA IDENTIFIER').as 'parameters_plus_more'
153
- rule('parameters' => 'IDENTIFIER').as 'parameters_plus_end'
154
- rule('arguments_opt' => 'arguments')
155
- rule('arguments_opt' => [])
156
- rule('arguments' => 'arguments COMMA expression').as 'arguments_plus_more'
157
- rule('arguments' => 'expression').as 'arguments_plus_end'
132
+ rule('function' => 'IDENTIFIER LEFT_PAREN parameters? RIGHT_PAREN block').as 'function'
133
+ rule('parameters' => 'IDENTIFIER (COMMA IDENTIFIER)*').as 'parameters'
134
+ rule('arguments' => 'expression (COMMA expression)*').as 'arguments'
158
135
  end
159
136
 
160
137
  unless defined?(Grammar)
@@ -61,6 +61,20 @@ module Loxxy
61
61
  print return super this true var while
62
62
  ].map { |x| [x, x] }.to_h
63
63
 
64
+ # Single character that have a special meaning when escaped
65
+ # @return [{Char => String}]
66
+ @@escape_chars = {
67
+ ?a => "\a",
68
+ ?b => "\b",
69
+ ?e => "\e",
70
+ ?f => "\f",
71
+ ?n => "\n",
72
+ ?r => "\r",
73
+ ?s => "\s",
74
+ ?t => "\t",
75
+ ?v => "\v"
76
+ }.freeze
77
+
64
78
  # Constructor. Initialize a tokenizer for Lox input.
65
79
  # @param source [String] Lox text to tokenize.
66
80
  def initialize(source = nil)
@@ -91,7 +105,6 @@ module Loxxy
91
105
 
92
106
  private
93
107
 
94
- # rubocop: disable Lint/DuplicateBranch
95
108
  def _next_token
96
109
  skip_intertoken_spaces
97
110
  curr_ch = scanner.peek(1)
@@ -105,18 +118,14 @@ module Loxxy
105
118
  elsif (lexeme = scanner.scan(/[!=><]=?/))
106
119
  # One or two special character tokens
107
120
  token = build_token(@@lexeme2name[lexeme], lexeme)
121
+ elsif scanner.scan(/"/) # Start of string detected...
122
+ token = build_string_token
108
123
  elsif (lexeme = scanner.scan(/\d+(?:\.\d+)?/))
109
124
  token = build_token('NUMBER', lexeme)
110
- elsif (lexeme = scanner.scan(/"(?:\\"|[^"])*"/))
111
- token = build_token('STRING', lexeme)
112
125
  elsif (lexeme = scanner.scan(/[a-zA-Z_][a-zA-Z_0-9]*/))
113
126
  keyw = @@keywords[lexeme]
114
127
  tok_type = keyw ? keyw.upcase : 'IDENTIFIER'
115
128
  token = build_token(tok_type, lexeme)
116
- elsif scanner.scan(/"(?:\\"|[^"])*\z/)
117
- # Error: unterminated string...
118
- col = scanner.pos - @line_start + 1
119
- raise ScanError, "Error: [line #{lineno}:#{col}]: Unterminated string."
120
129
  else # Unknown token
121
130
  col = scanner.pos - @line_start + 1
122
131
  _erroneous = curr_ch.nil? ? '' : scanner.scan(/./)
@@ -125,7 +134,6 @@ module Loxxy
125
134
 
126
135
  return token
127
136
  end
128
- # rubocop: enable Lint/DuplicateBranch
129
137
 
130
138
  def build_token(aSymbolName, aLexeme)
131
139
  begin
@@ -155,8 +163,6 @@ module Loxxy
155
163
  value = Datatype::Nil.instance
156
164
  when 'NUMBER'
157
165
  value = Datatype::Number.new(aLexeme)
158
- when 'STRING'
159
- value = Datatype::LXString.new(unescape_string(aLexeme))
160
166
  when 'TRUE'
161
167
  value = Datatype::True.instance
162
168
  else
@@ -166,27 +172,47 @@ module Loxxy
166
172
  return [value, symb]
167
173
  end
168
174
 
169
- # Replace any sequence sequence by their "real" value.
170
- def unescape_string(aText)
171
- result = +''
172
- previous = nil
173
-
174
- aText.each_char do |ch|
175
- if previous
176
- if ch == ?n
177
- result << "\n"
178
- else
179
- result << ch
180
- end
181
- previous = nil
182
- elsif ch == '\\'
183
- previous = ?\
175
+ # precondition: current position at leading quote
176
+ def build_string_token
177
+ scan_pos = scanner.pos
178
+ line = @lineno
179
+ column_start = scan_pos - @line_start
180
+ literal = +''
181
+ loop do
182
+ substr = scanner.scan(/[^"\\\r\n]*/)
183
+ if scanner.eos?
184
+ pos_start = "line #{line}:#{column_start}"
185
+ raise ScanError, "Error: [#{pos_start}]: Unterminated string."
184
186
  else
185
- result << ch
187
+ literal << substr
188
+ special = scanner.scan(/["\\\r\n]/)
189
+ case special
190
+ when '"' # Terminating quote found
191
+ break
192
+ when "\r"
193
+ next_line
194
+ special << scanner.scan(/./) if scanner.match?(/\n/)
195
+ literal << special
196
+ when "\n"
197
+ next_line
198
+ literal << special
199
+ when '\\'
200
+ ch = scanner.scan(/./)
201
+ next unless ch
202
+
203
+ escaped = @@escape_chars[ch]
204
+ if escaped
205
+ literal << escaped
206
+ else
207
+ literal << ch
208
+ end
209
+ end
186
210
  end
187
211
  end
188
-
189
- result
212
+ pos = Rley::Lexical::Position.new(line, column_start)
213
+ lox_string = Datatype::LXString.new(literal)
214
+ lexeme = scanner.string[scan_pos - 1..scanner.pos - 1]
215
+ Literal.new(lox_string, lexeme, 'STRING', pos)
190
216
  end
191
217
 
192
218
  # Skip non-significant whitespaces and comments.
data/lib/loxxy/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Loxxy
4
- VERSION = '0.3.01'
4
+ VERSION = '0.4.01'
5
5
  end
data/loxxy.gemspec CHANGED
@@ -48,7 +48,7 @@ Gem::Specification.new do |spec|
48
48
  DESCR_END
49
49
  spec.homepage = 'https://github.com/famished-tiger/loxxy'
50
50
  spec.license = 'MIT'
51
- spec.required_ruby_version = '~> 2.4'
51
+ spec.required_ruby_version = '~> 2.5'
52
52
 
53
53
  spec.bindir = 'bin'
54
54
  spec.executables = ['loxxy']
@@ -58,7 +58,7 @@ Gem::Specification.new do |spec|
58
58
  PkgExtending.pkg_documentation(spec)
59
59
 
60
60
  # Runtime dependencies
61
- spec.add_dependency 'rley', '~> 0.7.06'
61
+ spec.add_dependency 'rley', '~> 0.8.01'
62
62
 
63
63
  # Development dependencies
64
64
  spec.add_development_dependency 'bundler', '~> 2.0'