spitewaste 0.1.001

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 (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: []