spitewaste 0.1.001

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) hide show
  1. checksums.yaml +7 -0
  2. data/Gemfile +8 -0
  3. data/README.md +55 -0
  4. data/Rakefile +10 -0
  5. data/TUTORIAL.md +125 -0
  6. data/bin/spw +10 -0
  7. data/demo/factorial-nicespace.png +0 -0
  8. data/demo/factorial.asm +47 -0
  9. data/demo/factorial.png +0 -0
  10. data/demo/factorial.wsa +5 -0
  11. data/lib/spitewaste.rb +35 -0
  12. data/lib/spitewaste/assembler.rb +56 -0
  13. data/lib/spitewaste/cli.rb +51 -0
  14. data/lib/spitewaste/cli/asm.rb +10 -0
  15. data/lib/spitewaste/cli/compile.rb +60 -0
  16. data/lib/spitewaste/cli/convert.rb +53 -0
  17. data/lib/spitewaste/cli/exec.rb +43 -0
  18. data/lib/spitewaste/cli/image.rb +53 -0
  19. data/lib/spitewaste/emitter.rb +10 -0
  20. data/lib/spitewaste/emitters/assembly.rb +7 -0
  21. data/lib/spitewaste/emitters/codegen.rb +72 -0
  22. data/lib/spitewaste/emitters/image.rb +135 -0
  23. data/lib/spitewaste/emitters/linefeed.png +0 -0
  24. data/lib/spitewaste/emitters/schemes.yaml +1143 -0
  25. data/lib/spitewaste/emitters/whitespace.rb +14 -0
  26. data/lib/spitewaste/emitters/wsassembly.rb +7 -0
  27. data/lib/spitewaste/libspw/array.spw +82 -0
  28. data/lib/spitewaste/libspw/bits.spw +72 -0
  29. data/lib/spitewaste/libspw/case.spw +32 -0
  30. data/lib/spitewaste/libspw/fun.spw +42 -0
  31. data/lib/spitewaste/libspw/io.spw +39 -0
  32. data/lib/spitewaste/libspw/math.spw +117 -0
  33. data/lib/spitewaste/libspw/prime.spw +46 -0
  34. data/lib/spitewaste/libspw/random.spw +10 -0
  35. data/lib/spitewaste/libspw/stack.spw +84 -0
  36. data/lib/spitewaste/libspw/string.spw +233 -0
  37. data/lib/spitewaste/libspw/syntax.spw +2 -0
  38. data/lib/spitewaste/libspw/test.spw +8 -0
  39. data/lib/spitewaste/libspw/util.spw +98 -0
  40. data/lib/spitewaste/parsers/assembly.rb +35 -0
  41. data/lib/spitewaste/parsers/fucktional.rb +72 -0
  42. data/lib/spitewaste/parsers/spitewaste.rb +192 -0
  43. data/lib/spitewaste/parsers/whitespace.rb +60 -0
  44. data/lib/spitewaste/version.rb +3 -0
  45. data/spitewaste.gemspec +17 -0
  46. metadata +88 -0
@@ -0,0 +1,72 @@
1
+ module Spitewaste
2
+ # select() and reject() do almost the exact same thing, differing only in
3
+ # whether they drop or keep the match, so this is a bit of deduplication.
4
+ def self.generate_filter_spw fn, a, b
5
+ ops = ['push -10 dup call dec load swap store', 'pop']
6
+ yes, no = ops[a], ops[b]
7
+ <<SPW
8
+ push -10 dup store
9
+ #{fn}_loop_%1$s:
10
+ push 1 sub dup jn #{fn}_restore_%1$s
11
+ swap dup %2$s jz #{fn}_no_%1$s #{yes}
12
+ jump #{fn}_loop_%1$s
13
+ #{fn}_no_%1$s: #{no} jump #{fn}_loop_%1$s
14
+ #{fn}_restore_%1$s: push 9 sub load
15
+ #{fn}_restore_loop_%1$s:
16
+ dup push 10 add jz #{fn}_done_%1$s
17
+ dup load swap push 1 add
18
+ jump #{fn}_restore_loop_%1$s
19
+ #{fn}_done_%1$s: dup load sub
20
+ SPW
21
+ end
22
+
23
+ FUCKTIONAL = {
24
+ 'map' => <<SPW,
25
+ push -9 push -1 store swap
26
+ map_loop_%1$s:
27
+ push -9 call inc swap dup dup
28
+ push -9 load sub jz map_done_%1$s
29
+ call roll %2$s jump map_loop_%1$s
30
+ map_done_%1$s: pop
31
+ SPW
32
+
33
+ 'reduce' => <<SPW,
34
+ dup dup push 2 sub jn reduce_done_%1$s pop
35
+ push -9 swap push 1 sub store
36
+ reduce_loop_%1$s: %2$s
37
+ push -9 dup load push 1 sub dup jz reduce_done_%1$s
38
+ store jump reduce_loop_%1$s
39
+ reduce_done_%1$s: pop pop
40
+ SPW
41
+
42
+ 'times' => <<SPW,
43
+ push -5 swap push -1 mul store
44
+ times_loop_%1$s: %2$s push -5 dup call inc load jn times_loop_%1$s
45
+ SPW
46
+
47
+ 'find' => <<SPW,
48
+ find_loop_%1$s:
49
+ dup jz find_done_%1$s dup call roll
50
+ dup %2$s jz find_no_%1$s
51
+ swap push 1 sub call nslide push 1 add jump find_done_%1$s
52
+ find_no_%1$s: pop push 1 sub jump find_loop_%1$s
53
+ find_done_%1$s: push 1 sub
54
+ SPW
55
+
56
+ 'maxby' => <<SPW,
57
+ push -3 swap store
58
+ maxby_loop_%1$s:
59
+ copy 1 %2$s copy 1 %2$s sub jn maxby_lesser_%1$s
60
+ maxby_resume_%1$s: pop push -3 dup call dec load push 1 call eq
61
+ jz maxby_loop_%1$s
62
+ jump maxby_done_%1$s
63
+ maxby_lesser_%1$s: swap jump maxby_resume_%1$s
64
+ maxby_done_%1$s:
65
+ SPW
66
+
67
+ 'minby' => 'maxby (%2$s push -1 mul)',
68
+ 'each' => 'dup times (dup call roll %2$s push 1 sub) pop',
69
+ 'select' => generate_filter_spw('select', 0, 1),
70
+ 'reject' => generate_filter_spw('reject', 1, 0),
71
+ }
72
+ end
@@ -0,0 +1,192 @@
1
+ require 'json'
2
+ require 'set'
3
+ require_relative 'fucktional'
4
+
5
+ module Spitewaste
6
+ LIBSPW = File.expand_path '../libspw', __dir__
7
+
8
+ class SpitewasteParser
9
+ NameError = Class.new Exception
10
+
11
+ INSTRUCTIONS = /(\S+):|(\b(#{OPERATORS_M2T.keys * ?|})\s+(-?\d\S*)?)/
12
+ SPECIAL_INSN = /(call|jump|jz|jn)\s+(\S+)/
13
+
14
+ attr_reader :src, :instructions, :error
15
+
16
+ def initialize program, **options
17
+ @src = program.dup
18
+ @macros = {}
19
+ @symbol_file = options['symbol_file']
20
+
21
+ preprocess!
22
+ eliminate_dead_code!
23
+ end
24
+
25
+ def parse
26
+ @instructions = []
27
+
28
+ # first-come, first-served mapping from label names to auto-incrementing
29
+ # indices starting from 0; it would be nice if names could round-trip,
30
+ # but even encoding short ones would result in unpleasantly wide code.
31
+ @symbol_table = Hash.new { |h, k| h[k] = h.size }
32
+ @src.scan(/(\S+):/) { @symbol_table[$1] } # populate label indices
33
+ File.write @symbol_file, @symbol_table.to_json if @symbol_file
34
+
35
+ special = @src.scan SPECIAL_INSN
36
+ @src.scan INSTRUCTIONS do |label, _, operator, arg|
37
+ next @instructions << [:label, @symbol_table[label]] if label
38
+
39
+ if %i[call jump jz jn].include? operator = operator.to_sym
40
+ arg = @symbol_table[special.shift[1]]
41
+ else
42
+ if OPERATORS_M2T.keys.index(operator) < 8 && !arg
43
+ raise "missing arg for #{operator}"
44
+ elsif arg
45
+ begin
46
+ arg = Integer arg
47
+ rescue ArgumentError
48
+ raise "invalid argument '#{arg}' for #{operator}"
49
+ end
50
+ end
51
+ end
52
+
53
+ @instructions << [operator, arg]
54
+ end
55
+ end
56
+
57
+ private
58
+
59
+ def preprocess!
60
+ resolve_imports
61
+ seed_prng if @seen.include? 'random'
62
+ resolve_strings
63
+ remove_comments
64
+ add_sugar
65
+ fucktionalize
66
+ propagate_macros
67
+ end
68
+
69
+ def resolve_imports
70
+ @seen = Set.new
71
+
72
+ # "Recursively" resolve `import L (...)` statements with implicit include
73
+ # guarding. Each chunk of imports is appended to the end of the current
74
+ # source all at once to prevent having to explicitly jump to the actual
75
+ # start of the program. Some care must be taken to ensure that the final
76
+ # routine of one library won't inadvertently "flow" into the next.
77
+ while @src['import']
78
+ imports = []
79
+ @src.gsub!(/import\s+(\S+).*/) {
80
+ imports << resolve($1) if @seen.add? $1
81
+ '' # remove import statement
82
+ }
83
+ @src << imports.join(?\n)
84
+ end
85
+ end
86
+
87
+ def resolve path
88
+ library = path[?/] ? path : File.join(LIBSPW, path)
89
+ File.read "#{library}.spw"
90
+ end
91
+
92
+ def seed_prng
93
+ @src.prepend "push $seed,#{rand 2**31} store $seed = -9001"
94
+ end
95
+
96
+ def resolve_strings
97
+ @src.gsub!(/"([^"]+)"/m) {
98
+ [0, *$1.reverse.bytes] * ' push ' + ' :strpack'
99
+ }
100
+ end
101
+
102
+ def remove_comments
103
+ @src.gsub!(/;.+/, '')
104
+ end
105
+
106
+ def add_sugar
107
+ # `:foo` = `call foo`
108
+ @src.gsub!(/:(\S+)/, 'call \1')
109
+ # character literals
110
+ @src.gsub!(/'(.)'/) { $1.ord }
111
+ # quick push (`push 1,2,3` desugars to individual pushes)
112
+ @src.gsub!(/push \S+/) { _1.split(?,) * ' push ' }
113
+ end
114
+
115
+ def gensym
116
+ (@sym ||= ?`).succ!
117
+ end
118
+
119
+ def fucktionalize
120
+ # Iteratively remove pseudo-fp calls until we can't to allow nesting.
121
+ 1 while @src.gsub!(/(#{FUCKTIONAL.keys * ?|})\s*\((.+)\)/) do
122
+ FUCKTIONAL[$1] % [gensym, $2]
123
+ end
124
+ end
125
+
126
+ def propagate_macros
127
+ @src.gsub!(/(\$\S+)\s*=\s*(.+)/) { @macros[$1] = $2; '' }
128
+ @src.gsub!(/(\$\S+)/) { @macros[$1] || raise("no macro '#{$1}'") }
129
+
130
+ # @src.gsub!(/(\$[^(]+)\s*\((.+)\)\s*{(.+)}/m) { @macros[$1] = $2, $3; '' }
131
+ # @src.gsub!(/(\$[^(]+)\((.+?)\)/m) {
132
+ # unless (params, body = @macros[$1])
133
+ # raise "no macro function '#{$1}'"
134
+ # end
135
+
136
+ # params = params.split(?,).map &:strip
137
+ # args = $2.split ?,
138
+ # body.gsub /\b(#{params * ?|})\b/, params.zip(args).to_h
139
+ # }
140
+ end
141
+
142
+ def eliminate_dead_code!
143
+ tokens = @src.split
144
+
145
+ # We need an entry point from which to begin determining which routines
146
+ # are never invoked, but Whitespace programs aren't required to start
147
+ # with a label. Here, we add an implcit "main" to the beginning of the
148
+ # source unless it already contains an explicit entry point. TODO: better?
149
+ start = tokens[0][/(\S+):/, 1] || 'main'
150
+ tokens.prepend "#{start}:" unless $1
151
+
152
+ # Group the whole program into subroutines to facilitate the discovery
153
+ # of code which can be safely removed without affecting its behavior.
154
+ subroutines = {}
155
+ while label = tokens.shift
156
+ sublen = tokens.index { |t| t[/:$/] } || tokens.size
157
+ subroutines[label.chop] = tokens.shift sublen
158
+ end
159
+
160
+ # A subroutine may indirectly depend on the one immediately after by
161
+ # "flowing" into it; we assume this is the case if the subroutine's final
162
+ # instruction isn't one of {jump, jz, jn, exit, ret}.
163
+ flow = subroutines.each_cons(2).reject { |(_, tokens), _|
164
+ tokens.last(2).any? { |t| %w[jump jz jn exit ret].include? t }
165
+ }.map{ |pair| pair.map &:first }.to_h
166
+
167
+ alive = Set.new
168
+ queue = [start]
169
+ until queue.empty?
170
+ # Bail early if the queue is empty or we've already handled this label.
171
+ next unless label = queue.shift and alive.add? label
172
+
173
+ unless subroutines[label]
174
+ raise NameError, "can't branch to '#{label}', no such label"
175
+ end
176
+
177
+ # Naively(?) assume that subroutines hit all of their branch targets.
178
+ branches = subroutines[label].each_cons(2).select { |insn, _|
179
+ %w[jump jz jn call].include? insn
180
+ }.map &:last
181
+
182
+ # Check dependencies for further dependencies.
183
+ queue.concat branches, [flow[label]]
184
+ end
185
+
186
+ # warn alive.grep_v(/^_/).sort_by(&:upcase).inspect
187
+ @src = subroutines.filter_map { |label, instructions|
188
+ "#{label}: #{instructions * ' '}" if alive.include? label
189
+ } * ?\n + ' ' # trailing space required for regex reasons!
190
+ end
191
+ end
192
+ end
@@ -0,0 +1,60 @@
1
+ module Spitewaste
2
+ class WhitespaceParser
3
+ SyntaxError = Class.new Exception
4
+
5
+ attr_reader :instructions, :error
6
+
7
+ def initialize program, **options
8
+ @tokens = program.delete "^\s\t\n" # Remove comments.
9
+ end
10
+
11
+ def parse
12
+ @instructions = []
13
+ @line = @column = 1
14
+ operator_buffer = ''
15
+ mnemonics = OPERATORS_M2T.keys
16
+
17
+ while token = @tokens.slice!(0)
18
+ operator_buffer << token
19
+ if @operator = OPERATORS_T2M[operator_buffer]
20
+ argument = parse_number if mnemonics.index(@operator) < 8
21
+ @instructions << [@operator, argument]
22
+ operator_buffer.clear
23
+ argument = nil
24
+ end
25
+
26
+ if OPERATORS_T2M.none? { |tokens,| tokens.start_with? operator_buffer }
27
+ @error = [:illegal, operator_buffer, [@line, @column]]
28
+ raise SyntaxError,
29
+ "illegal token sequence: #{operator_buffer.inspect} " +
30
+ "(line #@line, column #@column)"
31
+ end
32
+
33
+ if token == ?\n
34
+ @line, @column = @line + 1, 1
35
+ else
36
+ @column += 1
37
+ end
38
+ end
39
+ end
40
+
41
+ def parse_number
42
+ unless end_of_number = @tokens.index(?\n)
43
+ @column += @tokens.size
44
+ @error = [:number, @operator, [@line, @column]]
45
+ raise SyntaxError,
46
+ "found EOF before end of number for #@operator operator " +
47
+ "(line #@line, column #@column)"
48
+ end
49
+
50
+ digits = @tokens.slice! 0, end_of_number + 1
51
+
52
+ raise SyntaxError,
53
+ "too few digits in number for #@operator operator " +
54
+ "(line #@line, column #@column)" if digits.size <3
55
+
56
+ digits[0] = ?- if digits[0] == ?\t
57
+ digits.tr("\s\t", '01').to_i 2
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,3 @@
1
+ module Spitewaste
2
+ VERSION = '0.1.001'
3
+ end
@@ -0,0 +1,17 @@
1
+ require_relative 'lib/spitewaste/version'
2
+
3
+ Gem::Specification.new do |spec|
4
+ spec.name = 'spitewaste'
5
+ spec.version = Spitewaste::VERSION
6
+ spec.author = 'Collided Scope (collidedscope)'
7
+
8
+ spec.summary = 'Make programming in Whitespace even better.'
9
+ spec.description = 'Spitewaste is a collection of tools that makes it almost too easy to write Whitespace.'
10
+ spec.homepage = 'https://github.com/collidedscope/spitewaste'
11
+ spec.license = 'WTFPL'
12
+
13
+ spec.files = `git ls-files`.split.reject { |f| f[/^test/] }
14
+ spec.bindir = 'bin'
15
+ spec.executables = ['spw']
16
+ spec.require_paths = ['lib']
17
+ end
metadata ADDED
@@ -0,0 +1,88 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: spitewaste
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.001
5
+ platform: ruby
6
+ authors:
7
+ - Collided Scope (collidedscope)
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2020-12-12 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: Spitewaste is a collection of tools that makes it almost too easy to
14
+ write Whitespace.
15
+ email:
16
+ executables:
17
+ - spw
18
+ extensions: []
19
+ extra_rdoc_files: []
20
+ files:
21
+ - Gemfile
22
+ - README.md
23
+ - Rakefile
24
+ - TUTORIAL.md
25
+ - bin/spw
26
+ - demo/factorial-nicespace.png
27
+ - demo/factorial.asm
28
+ - demo/factorial.png
29
+ - demo/factorial.wsa
30
+ - lib/spitewaste.rb
31
+ - lib/spitewaste/assembler.rb
32
+ - lib/spitewaste/cli.rb
33
+ - lib/spitewaste/cli/asm.rb
34
+ - lib/spitewaste/cli/compile.rb
35
+ - lib/spitewaste/cli/convert.rb
36
+ - lib/spitewaste/cli/exec.rb
37
+ - lib/spitewaste/cli/image.rb
38
+ - lib/spitewaste/emitter.rb
39
+ - lib/spitewaste/emitters/assembly.rb
40
+ - lib/spitewaste/emitters/codegen.rb
41
+ - lib/spitewaste/emitters/image.rb
42
+ - lib/spitewaste/emitters/linefeed.png
43
+ - lib/spitewaste/emitters/schemes.yaml
44
+ - lib/spitewaste/emitters/whitespace.rb
45
+ - lib/spitewaste/emitters/wsassembly.rb
46
+ - lib/spitewaste/libspw/array.spw
47
+ - lib/spitewaste/libspw/bits.spw
48
+ - lib/spitewaste/libspw/case.spw
49
+ - lib/spitewaste/libspw/fun.spw
50
+ - lib/spitewaste/libspw/io.spw
51
+ - lib/spitewaste/libspw/math.spw
52
+ - lib/spitewaste/libspw/prime.spw
53
+ - lib/spitewaste/libspw/random.spw
54
+ - lib/spitewaste/libspw/stack.spw
55
+ - lib/spitewaste/libspw/string.spw
56
+ - lib/spitewaste/libspw/syntax.spw
57
+ - lib/spitewaste/libspw/test.spw
58
+ - lib/spitewaste/libspw/util.spw
59
+ - lib/spitewaste/parsers/assembly.rb
60
+ - lib/spitewaste/parsers/fucktional.rb
61
+ - lib/spitewaste/parsers/spitewaste.rb
62
+ - lib/spitewaste/parsers/whitespace.rb
63
+ - lib/spitewaste/version.rb
64
+ - spitewaste.gemspec
65
+ homepage: https://github.com/collidedscope/spitewaste
66
+ licenses:
67
+ - WTFPL
68
+ metadata: {}
69
+ post_install_message:
70
+ rdoc_options: []
71
+ require_paths:
72
+ - lib
73
+ required_ruby_version: !ruby/object:Gem::Requirement
74
+ requirements:
75
+ - - ">="
76
+ - !ruby/object:Gem::Version
77
+ version: '0'
78
+ required_rubygems_version: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ requirements: []
84
+ rubygems_version: 3.1.4
85
+ signing_key:
86
+ specification_version: 4
87
+ summary: Make programming in Whitespace even better.
88
+ test_files: []