peruby 0.1.0

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.
Files changed (74) hide show
  1. checksums.yaml +7 -0
  2. data/.rubocop.yml +35 -0
  3. data/CHANGELOG.md +15 -0
  4. data/LICENSE.txt +21 -0
  5. data/README.md +51 -0
  6. data/Rakefile +92 -0
  7. data/bin/peruby +9 -0
  8. data/doc/COMPAT.md +54 -0
  9. data/doc/CONTRIBUTING.md +21 -0
  10. data/doc/DESIGN.md +25 -0
  11. data/doc/INCOMPATIBILITIES.md +30 -0
  12. data/doc/PERF.md +51 -0
  13. data/doc/ROADMAP.md +18 -0
  14. data/examples/hello.pl +1 -0
  15. data/examples/json.pl +2 -0
  16. data/examples/object.pl +5 -0
  17. data/examples/word_count.pl +6 -0
  18. data/lib/peruby/cli.rb +224 -0
  19. data/lib/peruby/compile_unit.rb +103 -0
  20. data/lib/peruby/compiler.rb +224 -0
  21. data/lib/peruby/errors.rb +40 -0
  22. data/lib/peruby/lexer/heredoc.rb +8 -0
  23. data/lib/peruby/lexer/keywords.rb +63 -0
  24. data/lib/peruby/lexer/number.rb +32 -0
  25. data/lib/peruby/lexer/quote_like.rb +72 -0
  26. data/lib/peruby/lexer/source_scanner.rb +104 -0
  27. data/lib/peruby/lexer/state.rb +46 -0
  28. data/lib/peruby/lexer/structure_scanner.rb +149 -0
  29. data/lib/peruby/lexer/term_scanner.rb +301 -0
  30. data/lib/peruby/lexer/token.rb +16 -0
  31. data/lib/peruby/lexer.rb +123 -0
  32. data/lib/peruby/node.rb +88 -0
  33. data/lib/peruby/op/assign.rb +128 -0
  34. data/lib/peruby/op/builtin.rb +903 -0
  35. data/lib/peruby/op/call.rb +378 -0
  36. data/lib/peruby/op/control.rb +256 -0
  37. data/lib/peruby/op/element.rb +136 -0
  38. data/lib/peruby/op/expression.rb +342 -0
  39. data/lib/peruby/op/io.rb +113 -0
  40. data/lib/peruby/op/list.rb +102 -0
  41. data/lib/peruby/op/literal.rb +84 -0
  42. data/lib/peruby/op/loop.rb +158 -0
  43. data/lib/peruby/op/regexp.rb +288 -0
  44. data/lib/peruby/op/variable.rb +534 -0
  45. data/lib/peruby/op.rb +47 -0
  46. data/lib/peruby/parser/grammar.rb +5797 -0
  47. data/lib/peruby/parser/grammar.y +576 -0
  48. data/lib/peruby/parser.rb +14 -0
  49. data/lib/peruby/runtime/code.rb +21 -0
  50. data/lib/peruby/runtime/conv.rb +140 -0
  51. data/lib/peruby/runtime/directory_handle.rb +18 -0
  52. data/lib/peruby/runtime/env.rb +129 -0
  53. data/lib/peruby/runtime/glob.rb +24 -0
  54. data/lib/peruby/runtime/interpolation.rb +223 -0
  55. data/lib/peruby/runtime/io_handle.rb +37 -0
  56. data/lib/peruby/runtime/local_stack.rb +90 -0
  57. data/lib/peruby/runtime/match_state.rb +62 -0
  58. data/lib/peruby/runtime/module_loader.rb +133 -0
  59. data/lib/peruby/runtime/mro.rb +94 -0
  60. data/lib/peruby/runtime/perl_array.rb +81 -0
  61. data/lib/peruby/runtime/perl_hash.rb +57 -0
  62. data/lib/peruby/runtime/ref.rb +43 -0
  63. data/lib/peruby/runtime/regexp_compiler.rb +75 -0
  64. data/lib/peruby/runtime/scalar.rb +27 -0
  65. data/lib/peruby/runtime/sprintf.rb +54 -0
  66. data/lib/peruby/runtime/stash.rb +50 -0
  67. data/lib/peruby/runtime/test_builder.rb +47 -0
  68. data/lib/peruby/runtime.rb +325 -0
  69. data/lib/peruby/validator.rb +236 -0
  70. data/lib/peruby/version.rb +5 -0
  71. data/lib/peruby.rb +31 -0
  72. data/t/00-basic.t +5 -0
  73. data/t/lib/MiniTest.pm +22 -0
  74. metadata +130 -0
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Peruby
4
+ class Lexer
5
+ # One lexical token and its source location.
6
+ Token = Data.define(:type, :value, :file, :line, :column) do
7
+ def to_a
8
+ [type, value]
9
+ end
10
+
11
+ def to_s
12
+ "#{file}:#{line}:#{column} #{type} #{value.inspect}"
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,123 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'lexer/token'
4
+ require_relative 'lexer/state'
5
+ require_relative 'lexer/keywords'
6
+ require_relative 'lexer/number'
7
+ require_relative 'lexer/quote_like'
8
+ require_relative 'lexer/heredoc'
9
+ require_relative 'lexer/term_scanner'
10
+ require_relative 'lexer/structure_scanner'
11
+ require_relative 'lexer/source_scanner'
12
+
13
+ module Peruby
14
+ # Stateful Perl lexer. It resolves term/operator ambiguities without parser feedback.
15
+ class Lexer
16
+ OPERATORS = {
17
+ '**=' => :ASSIGNOP, '//=' => :ASSIGNOP, '&&=' => :ASSIGNOP, '||=' => :ASSIGNOP,
18
+ '<<=' => :ASSIGNOP, '>>=' => :ASSIGNOP, '&=' => :ASSIGNOP, '|=' => :ASSIGNOP, '^=' => :ASSIGNOP,
19
+ '...' => :DOTDOTDOT, '**' => :POW,
20
+ '//' => :DORDOR, '&&' => :ANDAND, '||' => :OROR, '=~' => :MATCHOP,
21
+ '!~' => :NOTMATCHOP, '==' => :EQ, '!=' => :NE, '<=' => :LE, '>=' => :GE,
22
+ '<=>' => :CMP, '->' => :ARROW, '=>' => :FATCOMMA, '..' => :DOTDOT,
23
+ '<<' => :LSHIFT, '>>' => :RSHIFT, '++' => :INC, '--' => :DEC,
24
+ '+=' => :ASSIGNOP, '-=' => :ASSIGNOP, '*=' => :ASSIGNOP, '/=' => :ASSIGNOP,
25
+ '%=' => :ASSIGNOP, '.=' => :ASSIGNOP, 'x=' => :ASSIGNOP,
26
+ '=' => :'=', '+' => :+, '-' => :-, '*' => :*, '/' => :/, '%' => :%, '.' => :'.',
27
+ '<' => :<, '>' => :>, '!' => :!, '~' => :~, '&' => :&, '|' => :|, '^' => :^,
28
+ '?' => :'?', ':' => :':', ',' => :',', ';' => :';', '\\' => :REFGEN
29
+ }.freeze
30
+ SORTED_OPERATORS = OPERATORS.keys.sort_by(&:length).reverse.freeze
31
+ SIGILS = { '$' => :SCALAR_VAR, '@' => :ARRAY_VAR, '%' => :HASH_VAR,
32
+ '&' => :CODE_VAR, '*' => :GLOB_VAR }.freeze
33
+ QUOTE_OPERATORS = %w[q qq qw qr m s tr y].freeze
34
+ FILETEST = /\A-[erwxofdszlpSbcugktTBAMC](?!\w)/
35
+
36
+ include TermScanner
37
+ include StructureScanner
38
+ include SourceScanner
39
+
40
+ attr_reader :state, :data
41
+
42
+ def initialize(source, file: '-e', known_subs: {}, implicit_semicolon: false)
43
+ @source = source
44
+ @file = file
45
+ @known_subs = known_subs
46
+ @implicit_semicolon_enabled = implicit_semicolon
47
+ @index = 0
48
+ @line = 1
49
+ @column = 1
50
+ @state = State.new
51
+ @pending_heredocs = []
52
+ @indirect_print = false
53
+ @data = nil
54
+ end
55
+
56
+ def tokens
57
+ each.to_a
58
+ end
59
+
60
+ def each
61
+ return enum_for(__method__) unless block_given?
62
+
63
+ while (token = next_token)
64
+ yield token
65
+ end
66
+ end
67
+
68
+ def next_token
69
+ skip_ignored
70
+ return eof if end?
71
+ return implicit_block_semicolon if implicit_block_semicolon?
72
+
73
+ line = @line
74
+ column = @column
75
+ type, value = scan_token
76
+ @last_token_type = type
77
+ Token.new(type, value, @file, line, column)
78
+ end
79
+
80
+ private
81
+
82
+ def implicit_block_semicolon?
83
+ @implicit_semicolon_enabled && current == '}' && %i[block anon_sub].include?(@state.brackets.last&.kind) &&
84
+ @implicit_block_index != @index &&
85
+ !%i[; {].include?(@last_token_type)
86
+ end
87
+
88
+ def implicit_block_semicolon
89
+ @implicit_block_index = @index
90
+ @last_token_type = :';'
91
+ @indirect_print = false
92
+ @state.statement!
93
+ Token.new(:';', ';', @file, @line, @column)
94
+ end
95
+
96
+ def scan_token
97
+ scanners.each do |scanner|
98
+ token = send(scanner)
99
+ return token if token
100
+ end
101
+ illegal_character
102
+ end
103
+
104
+ def scanners
105
+ %i[scan_end_marker scan_prototype scan_heredoc scan_readline scan_quote scan_number scan_variable scan_filetest
106
+ scan_operator scan_word scan_bracket]
107
+ end
108
+
109
+ def eof
110
+ if @state.brackets.empty?
111
+ return nil unless @implicit_semicolon_enabled
112
+ return nil if @implicit_semicolon || @last_token_type.nil? || @last_token_type == :';'
113
+ return nil if @last_token_type == :'}' && @state.expect != :operator
114
+
115
+ @implicit_semicolon = true
116
+ return Token.new(:';', ';', @file, @line, @column)
117
+ end
118
+
119
+ frame = @state.brackets.last
120
+ raise CompileError, "Unmatched #{frame.open} at #{@file} line #{frame.line}."
121
+ end
122
+ end
123
+ end
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Peruby
4
+ # Syntax tree node types.
5
+ module Node
6
+ # Recursive S-expression conversion shared by all immutable nodes.
7
+ module SExpression
8
+ def to_sexp
9
+ name = self.class.name.split('::').last.gsub(/([A-Z]+)([A-Z][a-z])/, '\\1_\\2')
10
+ .gsub(/([a-z\d])([A-Z])/, '\\1_\\2').downcase.to_sym
11
+ values = members.reject { |member| %i[file line].include?(member) }.map { |member| sexp(public_send(member)) }
12
+ [name, *values]
13
+ end
14
+
15
+ private
16
+
17
+ def sexp(value)
18
+ return value.to_sexp if value.respond_to?(:to_sexp)
19
+ return value.map { |item| sexp(item) } if value.is_a?(Array)
20
+
21
+ value
22
+ end
23
+ end
24
+
25
+ def self.define(name, *members)
26
+ const_set(name, Data.define(*members, :file, :line) { include SExpression })
27
+ end
28
+
29
+ define :Program, :statements
30
+ define :Nop
31
+ define :Literal, :value
32
+ define :Bareword, :value
33
+ define :Variable, :sigil, :name
34
+ define :Unary, :operator, :expression
35
+ define :Binary, :operator, :left, :right
36
+ define :Ternary, :condition, :true_expression, :false_expression
37
+ define :Assign, :operator, :left, :right
38
+ define :List, :items
39
+ define :Block, :statements
40
+ define :BareBlock, :body, :label
41
+ define :If, :condition, :then_block, :else_block
42
+ define :While, :condition, :body, :until_loop, :continue_block, :label
43
+ define :CFor, :initializer, :condition, :step, :body, :continue_block, :label
44
+ define :My, :variable, :initializer
45
+ define :State, :variable, :initializer
46
+ define :Return, :expression
47
+ define :SubDef, :name, :prototype, :body
48
+ define :AnonSub, :prototype, :body
49
+ define :ForEach, :variable, :list, :body, :continue_block, :label, :declaration
50
+ define :LoopJump, :kind, :label
51
+ define :ModifierCondition, :expression, :condition, :kind
52
+ define :Package, :name
53
+ define :PackageBlock, :name, :body
54
+ define :Print, :handle, :arguments
55
+ define :Call, :name, :arguments
56
+ define :CodeCall, :receiver, :arguments
57
+ define :Element, :container, :key, :kind, :dereference
58
+ define :MethodCall, :receiver, :name, :arguments
59
+ define :ArrayLiteral, :items
60
+ define :HashLiteral, :items
61
+ define :Match, :quote
62
+ define :Bind, :subject, :pattern, :negated
63
+ define :Regexp, :quote
64
+ define :Substitute, :quote
65
+ define :Transliterate, :quote
66
+ define :Dereference, :kind, :name
67
+ define :DereferenceExpr, :kind, :expression
68
+ define :Group, :expression
69
+ define :Our, :variable, :initializer
70
+ define :Local, :target, :initializer
71
+ define :Goto, :name
72
+ define :Readline, :handle
73
+ define :FileTest, :operator, :expression
74
+ define :Printf, :handle, :arguments
75
+ define :Say, :handle, :arguments
76
+ define :Postfix, :operator, :expression
77
+ define :Prefix, :operator, :expression
78
+ define :ModifierFor, :expression, :list
79
+ define :Map, :body, :list, :grep
80
+ define :Sort, :comparator, :list, :package
81
+ define :Use, :module_name, :imports, :disable, :package
82
+ define :Require, :expression
83
+ define :Eval, :body, :string
84
+ define :Constant, :name, :value
85
+
86
+ Bareword.define_method(:to_sexp) { [:literal, value] }
87
+ end
88
+ end
@@ -0,0 +1,128 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Peruby
4
+ module Op
5
+ # Scalar plain or compound assignment.
6
+ class Assign < Base
7
+ def initialize(operator, left, right)
8
+ @operator = operator
9
+ @left = left
10
+ @right = right
11
+ end
12
+
13
+ def run(env, context)
14
+ return assign_aggregate(env, context) if @operator == :'=' && @left.respond_to?(:aggregate_assignment?) &&
15
+ @left.aggregate_assignment?
16
+
17
+ target = @left.lvalue(env, :list)
18
+ return assign_list(target, env, context) if target.is_a?(Array)
19
+
20
+ return logical_assignment(target, env, context) if logical_assignment?
21
+
22
+ value = @operator == :'=' ? @right.run(env, :scalar) : assigned_value(target.get, @right.run(env, :scalar))
23
+ target.set(value)
24
+ context == :list ? [target] : value
25
+ end
26
+
27
+ def to_callable
28
+ return super if logical_assignment?
29
+
30
+ right = @right.to_callable
31
+ scalar_right = @right.scalar_callable if @right.respond_to?(:scalar_callable)
32
+ left = @left.respond_to?(:lvalue_callable) ? @left.lvalue_callable : ->(env) { @left.lvalue(env, :list) }
33
+ lambda do |env, context|
34
+ if @operator == :'=' && @left.respond_to?(:aggregate_assignment?) && @left.aggregate_assignment?
35
+ next assign_aggregate(env, context, right.call(env, :list))
36
+ end
37
+
38
+ target = left.call(env)
39
+ next assign_list(target, env, context) if target.is_a?(Array)
40
+
41
+ value = if @operator == :'='
42
+ scalar_right ? scalar_right.call(env) : right.call(env, :scalar)
43
+ else
44
+ compiled_value(target.value, scalar_right ? scalar_right.call(env) : right.call(env, :scalar))
45
+ end
46
+ target.set(value)
47
+ context == :list ? [target] : value
48
+ end
49
+ end
50
+
51
+ def to_void_callable
52
+ return ->(env) { run(env, :void) } if logical_assignment?
53
+
54
+ right = @right.to_callable
55
+ scalar_right = @right.scalar_callable if @right.respond_to?(:scalar_callable)
56
+ left = @left.respond_to?(:lvalue_callable) ? @left.lvalue_callable : ->(env) { @left.lvalue(env, :list) }
57
+ lambda do |env|
58
+ if @operator == :'=' && @left.respond_to?(:aggregate_assignment?) && @left.aggregate_assignment?
59
+ next assign_aggregate(env, :void, right.call(env, :list))
60
+ end
61
+
62
+ target = left.call(env)
63
+ next assign_list(target, env, :void) if target.is_a?(Array)
64
+
65
+ right_value = scalar_right ? scalar_right.call(env) : right.call(env, :scalar)
66
+ value = @operator == :'=' ? right_value : compiled_value(target.value, right_value)
67
+ target.set(value)
68
+ value
69
+ end
70
+ end
71
+
72
+ private
73
+
74
+ def compiled_value(left, right)
75
+ if left.is_a?(Numeric) && right.is_a?(Numeric)
76
+ return Conv.norm_int(left + right) if @operator == :'+='
77
+ return Conv.norm_int(left - right) if @operator == :'-='
78
+ return Conv.norm_int(left * right) if @operator == :'*='
79
+ return Conv.divide(left, right) if @operator == :'/='
80
+ return Conv.modulo(left, right) if @operator == :'%='
81
+ end
82
+ assigned_value(left, right)
83
+ end
84
+
85
+ def assign_aggregate(env, context, cells = @right.run(env, :list))
86
+ values = cells.map(&:get)
87
+ @left.assign_values(env, values)
88
+ context == :list ? values.map { |value| Scalar.new(value) } : values.length
89
+ end
90
+
91
+ def assign_list(targets, env, context)
92
+ raise PerlError, 'Modification of a read-only value attempted' unless @operator == :'='
93
+
94
+ values = @right.run(env, :list).map(&:get)
95
+ targets.each_with_index { |target, index| target.set(values[index]) }
96
+ context == :list ? targets : values.length
97
+ end
98
+
99
+ def assigned_value(left, right) # rubocop:disable Metrics/AbcSize
100
+ case @operator.to_s
101
+ when '+=' then Conv.norm_int(Conv.to_num(left) + Conv.to_num(right))
102
+ when '-=' then Conv.norm_int(Conv.to_num(left) - Conv.to_num(right))
103
+ when '*=' then Conv.norm_int(Conv.to_num(left) * Conv.to_num(right))
104
+ when '/=' then Conv.divide(left, right)
105
+ when '%=' then Conv.modulo(left, right)
106
+ when '.=' then Conv.to_str(left) + Conv.to_str(right)
107
+ when 'x=' then Conv.repeat(left, right)
108
+ when '**=' then Conv.norm_int(Conv.to_num(left)**Conv.to_num(right))
109
+ when '<<=' then Conv.to_num(left).to_i << Conv.to_num(right).to_i
110
+ when '>>=' then Conv.to_num(left).to_i >> Conv.to_num(right).to_i
111
+ when '&=' then Conv.bitwise(:&, left, right)
112
+ when '|=' then Conv.bitwise(:|, left, right)
113
+ when '^=' then Conv.bitwise(:^, left, right)
114
+ end
115
+ end
116
+
117
+ def logical_assignment?(operator = @operator) = %i[&&= ||= //=].include?(operator)
118
+
119
+ def logical_assignment(target, env, context)
120
+ current = target.get
121
+ assign = (@operator == :'&&=' && Conv.truthy?(current)) || (@operator == :'||=' && !Conv.truthy?(current)) ||
122
+ (@operator == :'//=' && current.nil?)
123
+ target.set(@right.run(env, :scalar)) if assign
124
+ context == :list ? [target] : target.get
125
+ end
126
+ end
127
+ end
128
+ end