spitewaste 0.1.005 → 0.1.010
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +2 -2
- data/Rakefile +2 -1
- data/lib/spitewaste.rb +2 -0
- data/lib/spitewaste/assembler.rb +51 -6
- data/lib/spitewaste/cli/docs.rb +3 -3
- data/lib/spitewaste/libspw/array.spw +69 -18
- data/lib/spitewaste/libspw/bits.spw +70 -70
- data/lib/spitewaste/libspw/case.spw +34 -28
- data/lib/spitewaste/libspw/docs.json +1 -1
- data/lib/spitewaste/libspw/fun.spw +34 -22
- data/lib/spitewaste/libspw/math.spw +7 -3
- data/lib/spitewaste/libspw/prime.spw +33 -33
- data/lib/spitewaste/libspw/rational.spw +23 -0
- data/lib/spitewaste/libspw/string.spw +20 -19
- data/lib/spitewaste/libspw/test.spw +2 -3
- data/lib/spitewaste/libspw/util.spw +69 -20
- data/lib/spitewaste/parsers/spitewaste.rb +17 -3
- data/lib/spitewaste/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e90a64e56fcb4e3f9d7727b36fbd71c327693bb8b4a4c7f1ff21332e01317215
|
4
|
+
data.tar.gz: 4b0abbf0dd1e86a939014a598d844ba56ee85c67df12938fecb5a0273192e2b7
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 752150cdcefbcf39a836d3551ece95f0abbcd11a078c61b1283710fa8a6c24c391cd02a5ffba7241eaf839ab1d068bb66e716123b4f1f923b434872aea42a824
|
7
|
+
data.tar.gz: e44b79f9bfb77daa2165de8d3a2d2757636af2f6607fd218b12138853cc4c63898105f6ae5d1e6ae7c382a0503058ddbf3eb883ea1a5ea4d4a1d6c6b5c6afd3d
|
data/README.md
CHANGED
@@ -35,8 +35,8 @@ See `spw help` for guidance on invoking the command-line interface, and [this tu
|
|
35
35
|
* {ws, wsa, asm} to {ws, wsa, asm}
|
36
36
|
* all supported formats to C++ and PNG
|
37
37
|
- [x] Execute all supported formats by converting to Whitespace and passing the results to a user-specified interpreter
|
38
|
-
- [
|
39
|
-
- - [
|
38
|
+
- [x] Thoroughly document the standard library and make it searchable (via `spw docs`)
|
39
|
+
- - [x] using a doctest-like approach for free specs
|
40
40
|
- [ ] Write a proper Spitewaste parser for better error reporting (and performance?)
|
41
41
|
- [ ] Support user-specified aliases of the builtin mnemonics in case you wanna say `discard` instead of `pop`, etc.
|
42
42
|
- [ ] Resolve missing identifiers by auto-importing the necessary standard library?
|
data/Rakefile
CHANGED
@@ -14,7 +14,8 @@ task :docs do |t|
|
|
14
14
|
docs = {}
|
15
15
|
|
16
16
|
Dir.chdir('lib/spitewaste/libspw') do |d|
|
17
|
-
|
17
|
+
Dir['*.spw'].each do |path|
|
18
|
+
next if path['random']
|
18
19
|
lib = File.basename path, '.spw'
|
19
20
|
docs[lib] = extract_docs path
|
20
21
|
end
|
data/lib/spitewaste.rb
CHANGED
data/lib/spitewaste/assembler.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
module Spitewaste
|
2
2
|
class Assembler
|
3
|
-
attr_reader :
|
3
|
+
attr_reader :parser
|
4
4
|
|
5
5
|
def initialize program, **options
|
6
6
|
format = options[:format]
|
@@ -9,7 +9,7 @@ module Spitewaste
|
|
9
9
|
raise ArgumentError, "unknown format for parse: #{format}"
|
10
10
|
end
|
11
11
|
|
12
|
-
|
12
|
+
parser =
|
13
13
|
case format
|
14
14
|
when :whitespace
|
15
15
|
WhitespaceParser
|
@@ -19,9 +19,9 @@ module Spitewaste
|
|
19
19
|
SpitewasteParser
|
20
20
|
end
|
21
21
|
|
22
|
-
parser =
|
23
|
-
@
|
24
|
-
@
|
22
|
+
@parser = parser.new(program, **options).tap &:parse
|
23
|
+
@src = @parser.src if format == :spitewaste
|
24
|
+
@instructions = @parser.instructions
|
25
25
|
end
|
26
26
|
|
27
27
|
def assemble! format:, io: STDOUT, **options
|
@@ -42,7 +42,52 @@ module Spitewaste
|
|
42
42
|
else
|
43
43
|
AssemblyEmitter
|
44
44
|
end
|
45
|
-
|
45
|
+
|
46
|
+
# Worthwhile optimizations can only be done if we have a symbol table.
|
47
|
+
optimize! if parser.respond_to? :symbol_table
|
48
|
+
|
49
|
+
src = format == :wsassembly ? @src : @instructions
|
50
|
+
emitter.new(src, **options).emit io: io
|
51
|
+
end
|
52
|
+
|
53
|
+
def optimize_constant_pow
|
54
|
+
pow = parser.symbol_table['pow']
|
55
|
+
|
56
|
+
case @instructions[@ip - 2, 3]
|
57
|
+
in [[:push, base], [:push, exp], [:call, ^pow]]
|
58
|
+
@instructions[@ip - 2, 3] = [[:push, base ** exp]]
|
59
|
+
|
60
|
+
in [[:push, base], [:dup, _], [:call, ^pow]]
|
61
|
+
@instructions[@ip - 2, 3] = [[:push, base ** base]]
|
62
|
+
|
63
|
+
else nil
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def optimize_constant_strpack
|
68
|
+
strpack = parser.symbol_table['strpack']
|
69
|
+
return if @instructions[@ip] != [:call, strpack]
|
70
|
+
|
71
|
+
# grab all the instructions between `push 0` and this `call strpack`
|
72
|
+
start = @instructions[0, @ip].rindex [:push, 0]
|
73
|
+
between = @instructions[start + 1...@ip]
|
74
|
+
|
75
|
+
bytes = []
|
76
|
+
# optimization only applies if all of the intervening ops are pushes
|
77
|
+
return unless between.all? { |op, arg| op == :push && bytes << arg }
|
78
|
+
|
79
|
+
packed = bytes.reverse.zip(0..).sum { |b, e| b * 128 ** e }
|
80
|
+
@instructions[start..@ip] = [[:push, packed]]
|
81
|
+
@ip = start + 1
|
82
|
+
end
|
83
|
+
|
84
|
+
def optimize!
|
85
|
+
@ip = 0
|
86
|
+
while @instructions[@ip]
|
87
|
+
next @ip -= 1 if optimize_constant_pow
|
88
|
+
next if optimize_constant_strpack
|
89
|
+
@ip += 1
|
90
|
+
end
|
46
91
|
end
|
47
92
|
|
48
93
|
def try_elide_main
|
data/lib/spitewaste/cli/docs.rb
CHANGED
@@ -29,7 +29,7 @@ class SpitewasteCLI
|
|
29
29
|
|
30
30
|
docs.each do |lib, fns|
|
31
31
|
fns.each do |fn, data|
|
32
|
-
full = data['full']
|
32
|
+
full = "#{lib}/#{bold under fn} #{data['full']}"
|
33
33
|
ms = terms.count { |t| full[/#{t}/i] }
|
34
34
|
found << [lib, fn, full, ms] if ms >= min
|
35
35
|
end
|
@@ -41,9 +41,9 @@ class SpitewasteCLI
|
|
41
41
|
|
42
42
|
IO.popen('less -R', 'w') do |io|
|
43
43
|
found.each do |lib, fn, full|
|
44
|
-
full.gsub! /#{terms * ?|}
|
44
|
+
full.gsub! /#{terms * ?|}/i, &method(:hi)
|
45
45
|
desc, specs = full.split "\n\n"
|
46
|
-
io.puts "#{?- * 10}\n#{
|
46
|
+
io.puts "#{?- * 10}\n#{desc}\n\n"
|
47
47
|
io.puts specs if options[:show_specs]
|
48
48
|
end
|
49
49
|
end
|
@@ -1,21 +1,57 @@
|
|
1
|
+
;;; Pseudo-arrays
|
2
|
+
|
3
|
+
; An "array" is really just a sequence of elements on the stack followed by
|
4
|
+
; the number of elements, so care must be taken to ensure one knows what kind
|
5
|
+
; of data is currently being operated on.
|
6
|
+
|
1
7
|
import math ; divmod, max, min, pos?
|
2
8
|
import stack ; bury, dig, ncopy, nslide, roll
|
3
9
|
import util ; dec, die!, eq
|
4
10
|
|
5
11
|
$amax = 1000
|
6
12
|
|
13
|
+
; returns the sum of the array A
|
14
|
+
; [A] => [sum]
|
15
|
+
;
|
16
|
+
; [3 1] => [3]
|
17
|
+
; [4 3 2 1 4] => [10]
|
18
|
+
; [-10 -5 7 5 10 5] => [7]
|
7
19
|
arysum: reduce (add) ret
|
8
20
|
|
21
|
+
; duplicates the array at the top of the stack
|
22
|
+
; [A] => [A A]
|
23
|
+
;
|
24
|
+
; [1 2 3 3] => [1 2 3 3 1 2 3 3]
|
25
|
+
; [7 50 10 2] => [7 50 10 2 50 10 2]
|
9
26
|
arydup:
|
10
27
|
dup $++ push -2 copy 1 store
|
11
28
|
times (push -2 load $-- :ncopy) ret
|
12
29
|
|
30
|
+
; removes the element at the end of the array A
|
31
|
+
; [A] => [A']
|
32
|
+
;
|
33
|
+
; [10 1] => [0]
|
34
|
+
; [1 2 3 4 4] => [1 2 3 3]
|
35
|
+
; [7 10 20 30 3] => [7 10 20 2]
|
13
36
|
arypop: slide 1 $-- ret
|
14
37
|
|
38
|
+
; removes the element at the beginning of the array A
|
39
|
+
; [A] => [A']
|
40
|
+
;
|
41
|
+
; [10 1] => [0]
|
42
|
+
; [1 2 3 4 4] => [2 3 4 3]
|
43
|
+
; [7 10 20 30 3] => [7 20 30 2]
|
15
44
|
aryshift: dup :dig $-- ret
|
16
45
|
|
46
|
+
; returns the concatenation of arrays A and B
|
47
|
+
; [A B] => [A+B]
|
48
|
+
;
|
49
|
+
; [1 2 3 3 4 5 6 3] => [1 2 3 4 5 6 6]
|
50
|
+
; [7 10 2 5 4 3 3] => [7 10 5 4 3 5]
|
17
51
|
arycat: dup $++ :roll add ret
|
18
52
|
|
53
|
+
;;;
|
54
|
+
|
19
55
|
arypack:
|
20
56
|
:arydup reduce (:max) $++ push -1 swap store
|
21
57
|
$-- times (push -1 load mul add)
|
@@ -30,36 +66,43 @@ aryunpack:
|
|
30
66
|
|
31
67
|
arylen: push $amax mod ret
|
32
68
|
|
33
|
-
|
34
|
-
|
69
|
+
; returns the index I of element E in array A (or -1 if not found)
|
70
|
+
; [A E] => [I]
|
71
|
+
;
|
72
|
+
; [1 2 3 4 4 2] => [1]
|
73
|
+
; [1 2 3 4 4 4] => [3]
|
74
|
+
; [1 2 3 4 4 5] => [-1]
|
35
75
|
aryindex: copy 1 ; two copies of length, one gets decremented, index is diff
|
36
|
-
_aryindex_loop:
|
37
|
-
dup jn _aryindex_notfound
|
76
|
+
_aryindex_loop: dup jn _aryindex_notfound
|
38
77
|
dup push 2 add :roll copy 2 :eq jz _aryindex_no
|
39
78
|
copy 2 copy 1 sub swap $++ :nslide ret
|
40
|
-
|
41
79
|
_aryindex_no: $-- jump _aryindex_loop
|
42
|
-
|
43
80
|
_aryindex_notfound: slide 1 ret
|
44
81
|
|
45
|
-
|
46
|
-
|
47
|
-
;
|
48
|
-
|
82
|
+
; returns the element E at index I in array A (or dies on out of bounds)
|
83
|
+
; [A I] => [E]
|
84
|
+
;
|
85
|
+
; [1 2 3 4 4 0] => [1]
|
86
|
+
; [1 2 3 4 4 2] => [3]
|
87
|
+
; [1 2 3 4 4 -1] => [4] negative index counts from the end
|
88
|
+
aryat:
|
49
89
|
dup jn _aryat_neg
|
50
90
|
copy 1 swap sub dup :pos? jz _aryat_oob
|
51
91
|
:roll swap $-- :nslide ret
|
52
|
-
|
53
92
|
_aryat_neg: copy 1 add dup jn _aryat_oob jump aryat
|
54
|
-
|
55
93
|
_aryat_oob: push "(aryat) index out of bounds!" :die!
|
56
94
|
|
95
|
+
; returns the minimum and maximum elements of array A
|
96
|
+
; [A] => [min max]
|
97
|
+
;
|
98
|
+
; [4 3 2 1 4] => [1 4]
|
99
|
+
; [6 8 -3 4 0 5] => [-3 8]
|
100
|
+
; [7 1] => [7 7]
|
57
101
|
minmax:
|
58
|
-
:arydup reduce (:max)
|
59
|
-
push -1 swap store
|
102
|
+
:arydup reduce (:max) push -1 swap store
|
60
103
|
reduce (:min) push -1 load ret
|
61
104
|
|
62
|
-
;;;
|
105
|
+
;;; TODO: make sort not atrociously inefficient
|
63
106
|
|
64
107
|
sort: push -3 copy 1 store ; preserve length
|
65
108
|
_sort_loop:
|
@@ -70,17 +113,25 @@ _sort_loop:
|
|
70
113
|
push 0 copy 1 sub jn _sort_loop
|
71
114
|
push 3 sub load ret
|
72
115
|
|
116
|
+
; reverses the array A
|
117
|
+
; [A] => [A']
|
118
|
+
;
|
119
|
+
; [1 2 3 3] => [3 2 1 3]
|
120
|
+
; [7 1] => [7 1]
|
121
|
+
; [5 4 3 2 1 5] => [1 2 3 4 5 5]
|
73
122
|
aryrev: push -3 copy 1 store
|
74
123
|
_aryrev_loop:
|
75
124
|
swap copy 1 :bury $--
|
76
125
|
push 0 copy 1 sub jn _aryrev_loop
|
77
126
|
push 3 sub load ret
|
78
127
|
|
79
|
-
|
80
|
-
|
128
|
+
; returns the array A replicated N times
|
129
|
+
; ! N must be greater than 1
|
130
|
+
; [A N] => [A']
|
131
|
+
;
|
132
|
+
; [3 2 1 3 4] => [3 2 1 3 2 1 3 2 1 3 2 1 12]
|
81
133
|
aryrep: push -1 swap push -3 copy 1 store store
|
82
134
|
_aryrep_loop:
|
83
135
|
push -1 dup :dec load jz _aryrep_done
|
84
136
|
:arydup jump _aryrep_loop
|
85
|
-
|
86
137
|
_aryrep_done: push -3 load $-- times (:arycat) ret
|
@@ -1,74 +1,74 @@
|
|
1
|
-
import math
|
2
|
-
import
|
3
|
-
|
4
|
-
|
5
|
-
;
|
6
|
-
|
7
|
-
;
|
8
|
-
;
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
copy
|
15
|
-
|
16
|
-
|
17
|
-
push
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
;
|
45
|
-
;
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
_bitwise_xor_update: :_bitwise_update jump _bitwise_xor_loop
|
58
|
-
|
59
|
-
; flip all bits in n ; [n]
|
60
|
-
bitwise_not:
|
1
|
+
import math ; ilog, pow
|
2
|
+
import util ; digits
|
3
|
+
|
4
|
+
; The bitwise operators all work very similarly, differing only in how they
|
5
|
+
; map pairs of bits to 0 or 1. For AND, we simply multiply them. For OR, we
|
6
|
+
; sum them, add 1, then divide by 2. For XOR, we only want 1 if they're neq.
|
7
|
+
; We repeatedly halve the inputs while consuming bits and stop when at least
|
8
|
+
; one of them becomes zero. For AND, the answer is in the accumulator and we
|
9
|
+
; are done. For OR and XOR, it's likely the nonzero input still has bits to
|
10
|
+
; contribute, so we also accumulate its product with the current power of 2.
|
11
|
+
$bitfun(op, bit, done) {
|
12
|
+
push -2,1,-1,0 store store
|
13
|
+
_`op`_loop: push -1
|
14
|
+
copy 2 push 2 mod copy 2 push 2 mod `bit`
|
15
|
+
push -1 load swap push -2 load dup push 2 mul
|
16
|
+
push -2 swap store mul add store
|
17
|
+
push 2 div swap push 2 div
|
18
|
+
dup copy 2 mul jz _`op`_done swap jump _`op`_loop
|
19
|
+
_`op`_done: `done` ret
|
20
|
+
}
|
21
|
+
|
22
|
+
; returns the bitwise AND of A and B
|
23
|
+
; [A B] => [A & B]
|
24
|
+
;
|
25
|
+
; [65 47] => [1] [83 25] => [17]
|
26
|
+
; [40 64] => [0] [74 59] => [10]
|
27
|
+
; [31 18] => [18] [86 32] => [0]
|
28
|
+
; [93 79] => [77] [11 79] => [11]
|
29
|
+
band: $bitfun(band, mul, mul $-- load)
|
30
|
+
|
31
|
+
; returns the bitwise OR of A and B
|
32
|
+
; [A B] => [A | B]
|
33
|
+
;
|
34
|
+
; [66 51] => [115] [64 4] => [68]
|
35
|
+
; [61 77] => [125] [93 65] => [93]
|
36
|
+
; [14 87] => [95] [71 37] => [103]
|
37
|
+
; [7 19] => [23] [38 92] => [126]
|
38
|
+
bor: $bitfun(bor, add $++ push 2 div, add push -2 load mul push -1 load add)
|
39
|
+
|
40
|
+
; returns the bitwise XOR of A and B
|
41
|
+
; [A B] => [A ^ B]
|
42
|
+
;
|
43
|
+
; [4 14] => [10] [0 80] => [80]
|
44
|
+
; [51 5] => [54] [97 77] => [44]
|
45
|
+
; [39 65] => [102] [12 26] => [22]
|
46
|
+
; [44 36] => [8] [6 21] => [19]
|
47
|
+
bxor: $bitfun(bxor, :neq, add push -2 load mul push -1 load add)
|
48
|
+
|
49
|
+
; returns the infinite-precision bitwise NOT of N by inverting all of its bits
|
50
|
+
; [N] => [~N]
|
51
|
+
;
|
52
|
+
; [58] => [5] [46] => [17]
|
53
|
+
; [48] => [15] [87] => [40]
|
54
|
+
; [98] => [29] [51] => [12]
|
55
|
+
; [3] => [0] [42] => [21]
|
56
|
+
bnot:
|
61
57
|
dup push 0 swap push 1 add sub
|
62
58
|
swap push 2 :ilog push 2 swap :pow mod ret
|
63
59
|
|
64
|
-
|
65
|
-
|
66
|
-
;
|
60
|
+
; returns the number of bits in the binary representation of N
|
61
|
+
; [N] => [# of bits]
|
62
|
+
;
|
63
|
+
; [0] => [0] [1] => [1]
|
64
|
+
; [7] => [3] [8] => [4]
|
65
|
+
; [255] => [8]
|
66
|
+
blength: dup jz _blength_zero push 2 :ilog $++ ret
|
67
|
+
_blength_zero: ret
|
68
|
+
|
69
|
+
; returns the number of set bits in N
|
70
|
+
; [N] => [popcount]
|
71
|
+
;
|
72
|
+
; [0] => [0] [1] => [1]
|
73
|
+
; [7] => [3] [8] => [1]
|
67
74
|
popcount: push 2 :digits reduce (add) ret
|
68
|
-
|
69
|
-
;;;
|
70
|
-
|
71
|
-
; most significant set bit in n, as a power of 2 ; [n]
|
72
|
-
msb: dup jz _msb_zero
|
73
|
-
push 2 :itos :strlen $-- push 2 swap :pow ret
|
74
|
-
_msb_zero: ret
|
@@ -1,34 +1,40 @@
|
|
1
|
-
import bits ; bitwise_and, bitwise_or, bitwise_xor
|
2
|
-
import string ; isalpha, strcat, strunpack
|
3
|
-
|
4
1
|
;;; Case conversion
|
5
2
|
|
6
|
-
|
7
|
-
|
8
|
-
swap dup jz _upcase_done
|
9
|
-
dup :isalpha jz _upcase_cat
|
10
|
-
push 95 :bitwise_and
|
11
|
-
_upcase_cat:
|
12
|
-
:strcat jump _upcase_loop
|
13
|
-
|
14
|
-
_upcase_done: pop ret
|
3
|
+
import bits ; band, bor, bxor
|
4
|
+
import string ; isalpha, strcat, strunpack
|
15
5
|
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
6
|
+
; The case conversion functions all behave very similarly, differing only in
|
7
|
+
; how they modify the ordinal value V of a character to be kept.
|
8
|
+
; upcase is V & 95, downcase is V | 32, and swapcase is V ^ 32.
|
9
|
+
$casefun(case, op) {
|
10
|
+
:strunpack push 0 ; get characters on stack and initialize accumulator
|
11
|
+
_`case`_loop: swap dup jz _`case`_done dup :isalpha jz _`case`_cat `op`
|
12
|
+
_`case`_cat: :strcat jump _`case`_loop
|
13
|
+
_`case`_done: pop ret
|
14
|
+
}
|
23
15
|
|
24
|
-
|
16
|
+
; returns string S with all alphabetical characters capitalized
|
17
|
+
; [S] => [S']
|
18
|
+
;
|
19
|
+
; [""] => [""]
|
20
|
+
; ["abc"] => ["ABC"]
|
21
|
+
; ["123"] => ["123"]
|
22
|
+
; ["Abc123Def"] => ["ABC123DEF"]
|
23
|
+
upcase: $casefun(upcase, push 0x5F :band)
|
25
24
|
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
25
|
+
; returns string S with all alphabetical characters lower-cased
|
26
|
+
; [S] => [S']
|
27
|
+
;
|
28
|
+
; [""] => [""]
|
29
|
+
; ["ABC"] => ["abc"]
|
30
|
+
; ["123"] => ["123"]
|
31
|
+
; ["aBC123dEF"] => ["abc123def"]
|
32
|
+
downcase: $casefun(downcase, push 0x20 :bor)
|
33
33
|
|
34
|
-
|
34
|
+
; returns string S with the case of all alphabetical characters swapped
|
35
|
+
; [S] => [S']
|
36
|
+
;
|
37
|
+
; [""] => [""]
|
38
|
+
; ["FooBar"] => ["fOObAR"]
|
39
|
+
; ["Abc123deF"] => ["aBC123DEf"]
|
40
|
+
swapcase: $casefun(swapcase, push 0x20 :bxor)
|