spitewaste 0.1.001
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/Gemfile +8 -0
- data/README.md +55 -0
- data/Rakefile +10 -0
- data/TUTORIAL.md +125 -0
- data/bin/spw +10 -0
- data/demo/factorial-nicespace.png +0 -0
- data/demo/factorial.asm +47 -0
- data/demo/factorial.png +0 -0
- data/demo/factorial.wsa +5 -0
- data/lib/spitewaste.rb +35 -0
- data/lib/spitewaste/assembler.rb +56 -0
- data/lib/spitewaste/cli.rb +51 -0
- data/lib/spitewaste/cli/asm.rb +10 -0
- data/lib/spitewaste/cli/compile.rb +60 -0
- data/lib/spitewaste/cli/convert.rb +53 -0
- data/lib/spitewaste/cli/exec.rb +43 -0
- data/lib/spitewaste/cli/image.rb +53 -0
- data/lib/spitewaste/emitter.rb +10 -0
- data/lib/spitewaste/emitters/assembly.rb +7 -0
- data/lib/spitewaste/emitters/codegen.rb +72 -0
- data/lib/spitewaste/emitters/image.rb +135 -0
- data/lib/spitewaste/emitters/linefeed.png +0 -0
- data/lib/spitewaste/emitters/schemes.yaml +1143 -0
- data/lib/spitewaste/emitters/whitespace.rb +14 -0
- data/lib/spitewaste/emitters/wsassembly.rb +7 -0
- data/lib/spitewaste/libspw/array.spw +82 -0
- data/lib/spitewaste/libspw/bits.spw +72 -0
- data/lib/spitewaste/libspw/case.spw +32 -0
- data/lib/spitewaste/libspw/fun.spw +42 -0
- data/lib/spitewaste/libspw/io.spw +39 -0
- data/lib/spitewaste/libspw/math.spw +117 -0
- data/lib/spitewaste/libspw/prime.spw +46 -0
- data/lib/spitewaste/libspw/random.spw +10 -0
- data/lib/spitewaste/libspw/stack.spw +84 -0
- data/lib/spitewaste/libspw/string.spw +233 -0
- data/lib/spitewaste/libspw/syntax.spw +2 -0
- data/lib/spitewaste/libspw/test.spw +8 -0
- data/lib/spitewaste/libspw/util.spw +98 -0
- data/lib/spitewaste/parsers/assembly.rb +35 -0
- data/lib/spitewaste/parsers/fucktional.rb +72 -0
- data/lib/spitewaste/parsers/spitewaste.rb +192 -0
- data/lib/spitewaste/parsers/whitespace.rb +60 -0
- data/lib/spitewaste/version.rb +3 -0
- data/spitewaste.gemspec +17 -0
- 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
|
data/spitewaste.gemspec
ADDED
@@ -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: []
|