loxby 0.0.2 → 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4c24f85fecb8840b0c2b6331b6b4dda6c5a4899e830d81f52ed6ffa12065b432
4
- data.tar.gz: 3b694bcad715f645c52501d192944c207932d71909189f0b9f2dd0eae9e3d392
3
+ metadata.gz: 425095fb701b73736edd3558262a10753ae0b900389f47a5dcf50e77b556ad97
4
+ data.tar.gz: 05c522b5c1d6b099fe73748ddf8d041e6b49cf64335d727c3b5e251beced24c2
5
5
  SHA512:
6
- metadata.gz: 14167ca0449fa15e8b5eba780ab63af4d9b702c9b762c8bb3d2724840549481a25ba1806a4d5702e8ed0b925518872a1ec72ec17db0bc7f8170011d1891e9903
7
- data.tar.gz: 6b8366382f8c51bffdfe71802a0b551aee4938e0880c878012778c8508094544a9f1c02ca1f7dde57dcc1e03919545387b652877dc2ea38266c8422d6ba89b14
6
+ metadata.gz: 4ffca94d2a7d18ce137f7b1fda7fd09ef76530c361e5553df0bfb4764e8d5876f4f442b219c8393bc6708d76e10a4acfc41146c535e13ec89ff9f9bcea4e525a
7
+ data.tar.gz: 570bbfcc023ca9c697717c8a91810411b89c333e1e2f452970cfe500ed4d845c7b094ac8ca9ed2997e79b583c7b236edbb1a9a2f8b49f2bb20003a657ca51523
@@ -12,12 +12,12 @@ jobs:
12
12
 
13
13
  steps:
14
14
  - uses: actions/checkout@v4
15
- - name: Set up Ruby 3.1
15
+ - name: Set up Ruby
16
16
  # To automatically get bug fixes and new Ruby versions for ruby/setup-ruby,
17
17
  # change this to (see https://github.com/ruby/setup-ruby#versioning):
18
18
  uses: ruby/setup-ruby@v1
19
19
  with:
20
- ruby-version: 3.1.3
20
+ ruby-version: 3.3.5
21
21
 
22
22
  - name: Publish to GPR
23
23
  run: |
data/.gitignore ADDED
@@ -0,0 +1,8 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 3.3.5
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2024 Paul Hartman
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.
data/README.md CHANGED
@@ -10,4 +10,7 @@ Loxby is written following the first half of Robert Nystrom's wonderful web-form
10
10
  ```ruby
11
11
  require 'loxby/runner'
12
12
  Lox::Runner.new(ARGV, $stdout, $stderr)
13
- ```
13
+ ```
14
+
15
+ ## License
16
+ This gem is licensed under the [MIT License](https://opensource.org/license/mit). See LICENSE.txt for more.
data/bin/loxby CHANGED
@@ -4,5 +4,5 @@
4
4
  require 'bundler/setup'
5
5
  require 'loxby/runner'
6
6
 
7
- # Start interpreter when run as script
8
- Lox::Runner.new.run(ARGV) if __FILE__ == $PROGRAM_NAME
7
+ # Start interpreter
8
+ Lox::Runner.new.run(ARGV)
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'dry-configurable'
4
+ require_relative 'helpers/errors'
5
+
6
+ class Lox # rubocop:disable Style/Documentation
7
+ extend Dry::Configurable
8
+
9
+ setting :scanner do
10
+ setting :expressions do
11
+ setting :whitespace, default: /\s/
12
+ setting :number_literal, default: /\d/
13
+ setting :identifier, default: /[a-zA-Z_]/
14
+ end
15
+
16
+ setting :keywords,
17
+ default: %w[and class else false for fun if nil or print return super this true var while break]
18
+ end
19
+
20
+ setting :token_types do
21
+ setting :tokens, default: [
22
+ # Single-character tokens.
23
+ :left_paren, :right_paren, :left_brace, :right_brace,
24
+ :comma, :dot, :minus, :plus, :semicolon, :slash, :star,
25
+ :question, :colon,
26
+
27
+ # 1-2 character tokens.
28
+ :bang, :bang_equal,
29
+ :equal, :equal_equal,
30
+ :greater, :greater_equal,
31
+ :less, :less_equal,
32
+
33
+ # Literals.
34
+ :identifier, :string, :number,
35
+
36
+ # Keywords.
37
+ :and, :class, :else, :false, :fun, :for, :if, :nil, :or,
38
+ :print, :return, :super, :this, :true, :var, :while, :break,
39
+
40
+ :eof
41
+ ].freeze
42
+
43
+ setting :single_tokens,
44
+ default: '(){},.-+;/*?:',
45
+ constructor: -> { _1.split('').zip(Lox.config.token_types.tokens).to_h }
46
+ end
47
+
48
+ setting :native_functions do
49
+ setting :clock do
50
+ setting :arity, default: 0
51
+ setting :block, default: ->(_, _) { Time.now.to_i.to_f }
52
+ end
53
+ setting :exit do
54
+ setting :arity, default: 0
55
+ setting :block, default: ->(_, _) { throw :lox_exit }
56
+ end
57
+ end
58
+
59
+ setting :ast do
60
+ setting :expression,
61
+ default: {
62
+ assign: [%i[token name], %i[expr value]],
63
+ binary: [%i[expr left], %i[token operator], %i[expr right]],
64
+ ternary: [%i[expr left], %i[token left_operator], %i[expr center], %i[token right_operator],
65
+ %i[expr right]],
66
+ call: [%i[expr callee], %i[token paren], %i[expr_list arguments]],
67
+ grouping: [%i[expr expression]],
68
+ literal: [%i[object value]],
69
+ logical: [%i[expr left], %i[token operator], %i[expr right]],
70
+ unary: [%i[token operator], %i[expr right]],
71
+ variable: [%i[token name]]
72
+ }
73
+ setting :statement,
74
+ default: {
75
+ block: [%i[stmt_list statements]],
76
+ expression: [%i[expr expression]],
77
+ function: [%i[token name], %i[token_list params], %i[stmt_list body]],
78
+ if: [%i[expr condition], %i[stmt then_branch], %i[stmt else_branch]],
79
+ print: [%i[expr expression]],
80
+ return: [%i[token keyword], %i[expr value]],
81
+ var: [%i[token name], %i[expr initializer]],
82
+ while: [%i[expr condition], %i[stmt body]],
83
+ break: []
84
+ }
85
+ end
86
+
87
+ setting :exit_code do
88
+ setting :interrupt, default: 130
89
+ setting :usage, default: 64
90
+ setting :syntax_error, default: 65
91
+ setting :runtime_error, default: 70
92
+ end
93
+ end
data/lib/loxby/core.rb CHANGED
@@ -4,6 +4,7 @@ require_relative 'scanner'
4
4
  require_relative 'parser'
5
5
  require_relative 'interpreter'
6
6
  require_relative 'helpers/token_type'
7
+ require_relative 'config'
7
8
 
8
9
  # Lox interpreter.
9
10
  # Each interpreter keeps track of its own
@@ -13,36 +14,45 @@ class Lox
13
14
  attr_reader :errored, :interpreter
14
15
 
15
16
  def initialize
17
+ # Whether an error occurred while parsing.
16
18
  @errored = false
19
+ # Whether an error occurred while interpreting
17
20
  @errored_in_runtime = false
18
- @interpreter = Interpreter.new(self) # Make static so REPL sessions reuse it
21
+ # `Lox::Interpreter` instance. Static so interactive sessions reuse it
22
+ @interpreter = Interpreter.new(self)
19
23
  end
20
24
 
21
- # Run from file
25
+ # Parse and run a file
22
26
  def run_file(path)
23
27
  if File.exist? path
24
- run File.read(path)
28
+ catch(:lox_exit) do
29
+ run File.read(path)
30
+ end
25
31
  else
26
32
  report(0, '', "No such file: '#{path}'")
27
33
  end
28
- exit(65) if @errored # Don't execute malformed code
29
- exit(70) if @errored_in_runtime
34
+ exit Lox.config.exit_code.syntax_error if @errored # Don't execute malformed code
35
+ exit Lox.config.exit_code.runtime_error if @errored_in_runtime
30
36
  end
31
37
 
32
- # Run interactively
38
+ # Run interactively, REPL-style
33
39
  def run_prompt
34
- loop do
35
- print '> '
36
- line = gets
37
- break unless line # Trap eof (Ctrl+D unix, Ctrl+Z win)
40
+ catch(:lox_exit) do
41
+ loop do
42
+ print '> '
43
+ line = gets
44
+ break unless line # Trap eof (Ctrl+D unix, Ctrl+Z win)
38
45
 
39
- result = run(line)
40
- puts "=> #{@interpreter.lox_obj_to_str result}" unless @errored
41
- @errored = false # Reset so a mistake doesn't kill the repl
46
+ result = run(line)
47
+ puts "=> #{@interpreter.lox_obj_to_str result}" unless @errored
48
+
49
+ # When run interactively, resets after every prompt so as to not kill the repl
50
+ @errored = false
51
+ end
42
52
  end
43
53
  end
44
54
 
45
- # Run a string
55
+ # Parse and run a string
46
56
  def run(source)
47
57
  tokens = Scanner.new(source, self).scan_tokens
48
58
  parser = Parser.new(tokens, self)
@@ -16,17 +16,22 @@ end
16
16
 
17
17
  class Lox
18
18
  # Interface:
19
+ # ```ruby
19
20
  # Lox::AST.define_ast(
20
21
  # "ASTBaseClass",
21
22
  # {
22
- # :ast_type => [[:field_one_type, :field_one_name], [:field_two_type, :field_two_name]],
23
+ # :ast_type => [
24
+ # [:field_one_type, :field_one_name],
25
+ # [:field_two_type, :field_two_name]
26
+ # ],
23
27
  # :other_ast_type => [[:field_type, :field_name]]
24
28
  # }
25
29
  # )
30
+ # ```
26
31
  #
27
- # This defines Lox::AST::ASTBaseClass, which ::AstType and ::OtherAstType descend from
28
- # and are scoped under. It also defines the Visitor pattern: AstType defines #accept(visitor),
29
- # which calls `visitor.visit_ast_type(self)`
32
+ # This call to `#define_ast` generates `Lox::AST::ASTBaseClass`, as well as `::AstType` and
33
+ # `::OtherAstType` descending from and scoped uner it. Generated classes follow the Visitor
34
+ # pattern: `::AstType` generates with `#accept(visitor)` which calls `visitor.visit_ast_type(self)`.
30
35
  module AST
31
36
  module_function
32
37
 
@@ -43,19 +48,19 @@ class Lox
43
48
  define_class base_name.to_camel_case, base_class
44
49
  end
45
50
 
46
- def define_type(base_class, base_class_name, subtype_name, fields) # rubocop:disable Metrics/MethodLength
51
+ def define_type(base_class, base_class_name, subtype_name, fields) # rubocop:disable Metrics/MethodLength,Metrics/AbcSize
47
52
  subtype = Class.new(base_class)
48
53
  parameters = fields.map { _1[1].to_s }
49
54
 
50
55
  subtype.class_eval <<~RUBY, __FILE__, __LINE__ + 1
51
56
  include Visitable # Visitor pattern
52
- attr_reader #{parameters.map { ":#{_1}" }.join(', ')}
57
+ #{parameters.empty? ? '' : 'attr_reader '}#{parameters.map { ":#{_1}" }.join(', ')}
53
58
  def initialize(#{parameters.map { "#{_1}:" }.join(', ')})
54
- #{parameters.map { "@#{_1}" }.join(', ')} = #{parameters.join ', '}
59
+ #{parameters.map { "@#{_1}" }.join(', ')}#{parameters.empty? ? '' : ' = '}#{parameters.join ', '}
55
60
  end
56
61
 
57
- # Dynamically generated for visitor pattern.
58
- # Expects visitor to define #visit_#{subtype_name}
62
+ # This function was dynamically generated for visitor pattern.
63
+ # Expects visitors to define `#visit_#{subtype_name}_#{base_class_name}`.
59
64
  def accept(visitor)
60
65
  visitor.visit_#{subtype_name}_#{base_class_name}(self)
61
66
  end
@@ -70,25 +75,7 @@ class Lox
70
75
  end
71
76
  end
72
77
 
73
- Lox::AST.define_ast(
74
- :expression,
75
- {
76
- assign: [%i[token name], %i[expr value]],
77
- binary: [%i[expr left], %i[token operator], %i[expr right]],
78
- ternary: [%i[expr left], %i[token left_operator], %i[expr center], %i[token right_operator], %i[expr right]],
79
- grouping: [%i[expr expression]],
80
- literal: [%i[object value]],
81
- unary: [%i[token operator], %i[expr right]],
82
- variable: [%i[token name]]
83
- }
84
- )
85
-
86
- Lox::AST.define_ast(
87
- :statement,
88
- {
89
- block: [%i[stmt_list statements]],
90
- expression: [%i[expr expression]],
91
- print: [%i[expr expression]],
92
- var: [%i[token name], %i[expr initializer]]
93
- }
94
- )
78
+ # Default AST specification for loxby.
79
+ Lox.config.ast.values.each do |name, definition|
80
+ Lox::AST.define_ast(name, definition)
81
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Lox
4
+ # The interface for callable objects
5
+ # in loxby. Currently just functions.
6
+ #
7
+ # To mark a class as callable, simply
8
+ # `include Lox::Callable`.
9
+ module Callable
10
+ def call(_interpreter, _arguments)
11
+ raise NotImplementedError, "#{self.class} has not implemented #call"
12
+ end
13
+
14
+ def arity
15
+ raise NotImplementedError, "#{self.class} has not implemented #arity"
16
+ end
17
+ end
18
+ end
@@ -3,9 +3,8 @@
3
3
  require_relative 'errors'
4
4
 
5
5
  class Lox
6
- # Lox::Environment stores namespace for
7
- # a Lox interpreter. Environments can be
8
- # nested (for scope).
6
+ # Stores namespaces for a Lox interpreter.
7
+ # Environments can be nested (for scope).
9
8
  class Environment
10
9
  def initialize(enclosing = nil)
11
10
  @enclosing = enclosing
@@ -17,16 +16,26 @@ class Lox
17
16
  end
18
17
 
19
18
  def []=(name, value)
20
- @values[name.lexeme] = value
19
+ set name.lexeme, value
21
20
  end
22
21
 
23
- def exists?(name)
22
+ # Used to set a static association. For example:
23
+ # env.set 'static_function_name', static_function
24
+ def set(name, value)
25
+ @values[name] = value
26
+ end
27
+
28
+ def declared?(name)
29
+ # We can't check for a dummy value
30
+ # since loxby uses `nil` as well
24
31
  @values.keys.member? name.lexeme
25
32
  end
26
33
 
27
34
  def [](name)
28
- if exists? name
35
+ if @values[name.lexeme]
29
36
  @values[name.lexeme]
37
+ elsif declared? name
38
+ raise Lox::RunError.new(name, "Declared variable not initialized: '#{name.lexeme}'")
30
39
  elsif @enclosing
31
40
  @enclosing[name]
32
41
  else
@@ -35,7 +44,7 @@ class Lox
35
44
  end
36
45
 
37
46
  def assign(name, value)
38
- if exists? name
47
+ if declared? name
39
48
  self[name] = value
40
49
  elsif @enclosing
41
50
  @enclosing.assign(name, value)
@@ -43,5 +52,7 @@ class Lox
43
52
  raise undefined_variable(name)
44
53
  end
45
54
  end
55
+
56
+ alias define []=
46
57
  end
47
58
  end
@@ -1,9 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class Lox
4
+ # A generic loxby error class raised
5
+ # when a syntax error is found.
4
6
  class ParseError < RuntimeError; end
5
7
 
6
- class RunError < RuntimeError # rubocop:disable Style/Documentation
8
+ # A generic loxby error class raised
9
+ # when a runtime error is found.
10
+ class RunError < RuntimeError
7
11
  attr_reader :token
8
12
 
9
13
  def initialize(token, message)
@@ -12,5 +16,7 @@ class Lox
12
16
  end
13
17
  end
14
18
 
19
+ # A loxby error class raised when
20
+ # a number is divided by zero.
15
21
  class DividedByZeroError < RunError; end
16
22
  end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'callable'
4
+ require_relative 'environment'
5
+ require_relative '../interpreter'
6
+
7
+ class Lox
8
+ # A 'Function' is a loxby function.
9
+ #
10
+ # You could manually instantiate one,
11
+ # but why would you?
12
+ class Function
13
+ include Callable
14
+ attr_reader :declaration, :enclosure
15
+
16
+ def initialize(declaration, closure)
17
+ @declaration = declaration
18
+ @closure = closure
19
+ end
20
+
21
+ def call(interpreter, args)
22
+ env = Environment.new(@closure)
23
+ @declaration.params.zip(args).each do |param, arg|
24
+ env[param] = arg # Environment grabs the lexeme automatically
25
+ end
26
+
27
+ # Interpreter will `throw :return, return_value` to unwind
28
+ # callstack, jumping out of the `catch` block. `catch` then
29
+ # implicitly returns that value.
30
+ catch(:return) do
31
+ interpreter.execute_block @declaration.body, env
32
+ # If we get here, there was no return statement.
33
+ return nil
34
+ end
35
+ end
36
+
37
+ def arity = @declaration.params.size
38
+ def to_s = "<fn #{@declaration.name ? @declaration.name.lexeme : '(anonymous)'}>"
39
+ end
40
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../interpreter'
4
+ require_relative 'callable'
5
+ require_relative '../visitors/base'
6
+
7
+ class Interpreter < Visitor # rubocop:disable Style/Documentation
8
+ # A `NativeFunction` is a loxby function
9
+ # which references a callable Ruby
10
+ # object (block, proc, method, etc.).
11
+ #
12
+ # For example:
13
+ # ```ruby
14
+ # @environment.set(
15
+ # 'clock',
16
+ # NativeFunction.new(0) do |_interpreter, _args|
17
+ # Time.now.to_i.to_f
18
+ # end
19
+ # )
20
+ # ```
21
+ class NativeFunction
22
+ include Lox::Callable
23
+ def initialize(given_arity = 0, &block)
24
+ @block = block
25
+ @arity = given_arity
26
+ end
27
+
28
+ def call(interpreter, args) = @block.call(interpreter, args)
29
+
30
+ def arity
31
+ if @arity.respond_to? :call
32
+ @arity.call
33
+ else
34
+ @arity
35
+ end
36
+ end
37
+
38
+ def to_s
39
+ '<native fn>'
40
+ end
41
+ end
42
+
43
+ def define_native_functions
44
+ Lox.config.native_functions.values.each do |name, func|
45
+ @globals.set name.to_s, NativeFunction.new(func.arity, &func.block)
46
+ end
47
+ end
48
+ end
@@ -1,29 +1,17 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative '../config'
4
+
3
5
  class Lox
6
+ # A single token. Emitted by
7
+ # `Lox::Scanner` and consumed
8
+ # by `Lox::Parser`.
4
9
  class Token
5
- TOKENS = [
6
- # Single-character tokens.
7
- :left_paren, :right_paren, :left_brace, :right_brace,
8
- :comma, :dot, :minus, :plus, :semicolon, :slash, :star,
9
- :question, :colon,
10
-
11
- # 1-2 character tokens.
12
- :bang, :bang_equal,
13
- :equal, :equal_equal,
14
- :greater, :greater_equal,
15
- :less, :less_equal,
16
-
17
- # Literals.
18
- :identifier, :string, :number,
19
-
20
- # Keywords.
21
- :and, :class, :else, :false, :fun, :for, :if, :nil, :or,
22
- :print, :return, :super, :this, :true, :var, :while,
10
+ # List of all token types.
11
+ TOKENS = Lox.config.token_types.tokens
23
12
 
24
- :eof
25
- ].freeze
26
- SINGLE_TOKENS = TOKENS.zip('(){},.-+;/*'.split('')).to_h
13
+ # Map of single-character token types.
14
+ SINGLE_TOKENS = Lox.config.token_types.single_tokens
27
15
 
28
16
  attr_reader :type, :lexeme, :literal, :line
29
17