Skip to content

Instantly share code, notes, and snippets.

@tompng
Last active April 24, 2025 15:34
Show Gist options
  • Save tompng/7040fad860403211c2325e4c1eb4fab3 to your computer and use it in GitHub Desktop.
Save tompng/7040fad860403211c2325e4c1eb4fab3 to your computer and use it in GitHub Desktop.
parsercalc
require 'prism'
require 'ripper'
Dir.mkdir 'ref' unless Dir.exist? 'ref'
`cd ref; ruby ../generator.rb; cd ..`
files = [*10.times.map(&:to_s), '+', '=']
files.each { File.unlink _1 rescue nil }
case ARGV[0]
when /^format/
`ruby formatter.rb && ruby output.rb`
when /^gen/
`ruby generator.rb`
when /^min/
`ruby minified.rb`
else
puts "ARGV[0] not in 'format' | 'generator' | 'minified'"
exit
end
files.each do |file|
eq = File.read(file) == File.read('ref/' + file)
puts "#{file}: #{eq ? "\e[32mOK\e[m" : "\e[31mFAIL\e[m]"}"
end
CODES = [*'0'..'9','+', '='].to_h { [_1, File.read(_1)] }
numbers = [*'0'..'9', *'00'..'99']
def defnodes(code)
node = Prism.parse(code).value
defnodes = []
rec = ->n{
defnodes << n if n in Prism::DefNode
n.compact_child_nodes.each(&rec)
}
rec[node]
defnodes
end
def assert_equal(a, b, message)
if a == b
print '|'
else
raise "#{a.inspect} != #{b.inspect} ##{message}"
end
end
def assert_local_variables(code, expected)
actual = defnodes(code.chars.map{CODES[_1]}.join).last.locals
assert_equal expected.map(&:to_s).sort, actual.map(&:to_s).sort - ['__'], code
end
def assert_answer(code, answer)
visible_segments = [
[0,1,2,4,5,6],
[2,5],
[0,2,3,4,6],
[0,2,3,5,6],
[1,2,3,5],
[0,1,3,5,6],
[0,1,3,4,5,6],
[0,2,5],
[0,1,2,3,4,5,6],
[0,1,2,3,5,6]
]
rbcode = code.chars.map{CODES[_1]}.join
raise "invalid code: #{code}" unless Ripper.sexp(rbcode) && Prism.parse_success?(rbcode)
segments = visible_segments.map{[*0..6] - _1}
upper, lower = answer.divmod 10
expected_lvars = segments[upper].map{"a#{_1}"} + segments[lower].map{"b#{_1}"}
# check prism
nodes = defnodes(rbcode)
locals = nodes.reverse.find {|n| !(n.locals & [:nf, :mf]).empty? }.locals.select{_1.match?(/\A[ab][0-6]\z/)}
assert_equal expected_lvars, locals.map(&:to_s).sort - ['__'], code
# check parse.y
exclude_lvars = (0..6).flat_map{["a#{_1}" ,"b#{_1}"]} - expected_lvars
check_code = (expected_lvars.map{|v| "#{v} /'['" } + exclude_lvars.map{|v| "#{v} /1/"}).join(';')
raise 'wrong variable' unless Ripper.sexp(rbcode.gsub('if~exit', '&&(' + check_code + ')'))
end
assert_answer "12+34=", 46
assert_answer "37+28+33=", 98
assert_answer "1+2+3+4+5+6+7+8+9+10=", 55
assert_answer "12+8+17+4+13+9+16+7=", 86
UPPER = [*'a'..'i']
LOWER = [*'j'..'r']
numbers.each do |n|
ans = n.to_i
upper, lower = ans.divmod 10
assert_local_variables "#{n}+", [(UPPER[upper - 1] if upper > 0), (LOWER[lower - 1] if lower > 0)].compact
numbers.each do |m|
ans = n.to_i + m.to_i
upper, lower = ans.divmod 10
next if upper >= 10
assert_local_variables "#{n}+#{m}+", [(UPPER[upper - 1] if upper > 0), (LOWER[lower - 1] if lower > 0)].compact
assert_answer "#{n}+#{m}=", ans
end
end
code = File.read('minified.rb').gsub(/^#.+/, '').gsub(/^ +/, '').gsub(/\n/, '')
NUMBERS = <<EOS
000 1 222 333 4 4 555 666 777 888 999
0 0 1 2 3 4 4 5 6 7 8 8 9 9
0 0 1 222 333 444 555 666 7 888 999
0 0 1 2 3 4 5 6 6 7 8 8 9
000 1 222 333 4 555 666 7 888 999
EOS
W1, H1 = 4, 3
W2, H2 = 6, 4
W = 2 * W1 + W2
H = 3 * H1 + 2 * H2
def shape(n, x, y)
return false unless x >= 0 && y >= 0 && x < W && y < H
if n.is_a? Integer
ix = [w = W1, w += W2, w + W1].index { x < _1 }
iy = [h = H1, h += H2, h += H1, h += H2, h + H1].index { y < _1 }
NUMBERS.lines[iy][n * 4 + ix] != ' '
else
x = (2 * x.fdiv(W - 1) - 1).abs
y = (4 * y.fdiv(H - 1) - 2).abs
if n == '+'
(x<0.35||y<0.4) && y < 1.5
elsif n == '='
(y-0.8).abs < 0.4
end
end
end
WOFFSET = 4
HOFFSET = 2
lines = (H*4+HOFFSET*3).times.map do |y|
(W*3+WOFFSET*2).times.map do |x|
ix = x / (W + WOFFSET)
iy = y / (H + HOFFSET)
n = [[7,8,9],[4,5,6],[1,2,3],[0,'+','=']][iy][ix]
sx = x % (W + WOFFSET)
sy = y % (H + HOFFSET)
shape(n, sx, sy) ? '#' : ' '
end.join.rstrip
end
code2 = 'eval->{%w[' + code.gsub(' ', 'T').gsub('\\','C') + "].join.tr('TRICK',_1)}[+\"\\x2025\"<<-?\\\\]"
chars = code2.chars
output = lines.join($/).gsub('#'){chars.shift||'#'}
puts output
puts
File.write 'output.rb', output + $/
p [lines.join.count('#'), code.size, lines.join($/).size]
puts chars.size
puts output[/[# \n]*\z/].count('#')
# frozen_string_literal: true
# rubocop:disable all
UPPER = [*'a'..'i']
LOWER = [*'j'..'r']
LOWER_ANS = [*'s'..'z', '_']
MARKS = '+-*&|^`!@$'.chars
SEG1 = 10.times.map{|i|"a#{i}"}
SEG2 = 10.times.map{|i|"b#{i}"}
# 000
# 1 2
# 1 2
# 333
# 4 5
# 4 5
# 666
SEGMENTS = [
[3],
[0, 1, 3, 4, 6],
[1, 5],
[1, 4],
[0, 4, 6],
[2, 4],
[2],
[1, 3, 4, 6],
[],
[4],
]
def copy_value_code
herebeg = []
hereend = +"A\n__=<<B"
percs = []
defs = +'))./('
UPPER.each do |v|
herebeg << " #{v} %( <<#{v} ) "
hereend << "\n#{v}\nA\n#{v}=<<B"
end
LOWER_ANS.zip(LOWER, MARKS).each do |s, d, m|
percs << "#{s} /%#{m}/"
defs << "##{m}))./(#{d},"
end
"/)&&def((\n\n<<A;" + (percs.join(';') + defs).gsub(/#?\+/,?#).gsub(/%?[`!@]/){_1[-1].tr('!@', "\"'")} + "\n" + '#{ ' + herebeg.join(' ; ') + ' }' + "\n" + hereend + "\nB\n)="
end
NUMBER_FIRST_FLAG = 'nf'
NUMBER_SECOND_FLAG = 'mf'
NUMBER_FIRST = 'f'
def number_code(n)
code = "(#{NUMBER_FIRST_FLAG} /#{NUMBER_SECOND_FLAG}=l#{n}='/;#{NUMBER_FIRST_FLAG}=#{NUMBER_FIRST}#{n}=?';"
*s = SEGMENTS[n]
code << 'def/('+SEG1.values_at(*s).join(',')+')=('
code + ?x*(150-code.size) + "\n" + segments_view(*SEG1)[..-5]+'))&&'+"\n"
end
def number_lu_code
upper_copy = 10.times.map do |n|
"#{NUMBER_FIRST}#{n} /u#{n}=1./"
end
lower_copy = 10.times.map do |n|
"#{NUMBER_FIRST}#{n} /l#{n}=1./"
end
"#{NUMBER_SECOND_FLAG} /'/;#{lower_copy.join(';')};\"';#{upper_copy.join(';')};?\""
end
def add_lower_code
carry_var = 'ca'
10.times.map do |l|
assigns = +''
branches = +''
closings = []
fallback = "#{LOWER_ANS[l-1]}=" if l > 0
(1..9).each do |i|
v = LOWER[i - 1]
carry = i + l >= 10
if i + l == 10
vars = [carry_var]
elsif carry
vars = [carry_var, LOWER_ANS[i+l-11]]
else
vars = [LOWER_ANS[i+l-1]]
end
m = MARKS[i]
branches << "#{v} /%#{m}/;"
assigns << "#{m};#{vars.join('=')}=%#{m}"
closings << m
end
m = MARKS[0]
closings << m
code = "#{branches}#{fallback}%#{m}#{assigns}#{closings.join(';?')}"
"l#{l} /'/;\"';#{code};?\""
end
end
def add_upper_code
carry_var = 'ca'
carry_code = "#{carry_var} /'/;\"';" + (1..8).map do |n|
"#{UPPER[n-1]} /#{UPPER[n]}=1./;"
end.reverse.join + UPPER[0] + '=1;?"'
carry_code + "\n" + (1..9).map do |u|
code = (1..9).map do |i|
v = UPPER[i - 1]
a = UPPER[u + i - 1]
"#{v} /#{a}=1./;" if a
end
"u#{u} /'/;\"';#{code.reverse.join}#{UPPER[u-1]}=1;?\""
end.join("\n")
end
def formatted_symbol(type)
tokens = ['(' + number_lu_code, add_lower_code, add_upper_code.split("\n")].join(';').split(/([^;]+;)/)
shape = ->x,y{
a = (y-22).abs
b = (x-70).abs
[(a<7&&b<59) || (b<15), (b - 30).abs < 14][type]
}
lines = 44.times.map do |y|
line = ''
while line.size < 150
if shape[line.size, y]
line << (tokens.shift || ';')
else
line += shape[line.size - 8 - type, y] ? ';' : ' '
end
end
line.gsub('; ', ' ').rstrip
end
lines.join("\n").gsub(/(;| )(;;+)(;|$)/){$1+'/'+?x*($2.size-2)+'/'+$3}
end
def segments(vars, svars)
nonzeros = vars.each.with_index(1).map do |v, n|
s = SEGMENTS[n]
vs = svars.values_at(*s)
vs << 1 if vs[0]
"#{v} /#{vs.join('=')}#/;"
end
nonzeros.reverse.join+svars.values_at(*SEGMENTS[0]).join('=')+'='+?x*25
end
def segments_view(a0,a1,a2,a3,a4,a5,a6,*)
# 44 11
# 6 3 0
# 6 3 0
# 55 22
w=67
a=8.times.map do |i|
[[a4, a1], [a5, a2]].map do |a,b|
';' * i + "#{a} /#{?x.*w-i}/x" + ";#{b} /#{?x.*w-i}/x" + ';'*(i+3)
end
end
[
padding = [';'*150]*4,
a.map(&:first).reverse,
[[a6, a3, a0].map{"#{_1} /#{?x.*15}/x"}.join(';'*(w-25))+';;;']*30,
a.map(&:last),
padding
].join("\n").gsub(/(^|;)(;*)(;|$)/){[$1,$3].*?x*$2.size}
end
values = rand(1..10).times.map{(99*rand*rand).round}
values.pop while values.sum >= 100
numbers = values.map do |v|
[
(number_code(v/10) if v >= 10),
number_code(v%10),
].join("\n")
end
10.times do |i|
File.write i.to_s, number_code(i)
end
File.write '+', plus = "\n" * 26 + formatted_symbol(0)[0..-10] + copy_value_code + "\n"
File.write '=', eql = "\n" * 26 + formatted_symbol(1)+"\n"*26+segments(UPPER, SEG1)+"\n"+segments_view(*SEG1)+"\n"+segments(LOWER_ANS, SEG2)+"\n"+segments_view(*SEG2)[..-9]+")if~exit\n"
puts numbers.join(plus)
puts eql
p values
puts values.sum
# frozen_string_literal: true
# rubocop:disable all
u=*?a..?i;
l=*?j..?r;
o=*?s..?z,?_;
m='#-*&|^`!@$'.chars;
s=[
[3],
[0,1,3,4,6],
[1,5],
[1,4],
[0,4,6],
[2,4],
[2],
[1,3,4,6],
[],
[4],
];
a=(0..7).map{[?;*_1+'a4',:a1,?x*(_1+2)]*" /#{?x.*67-_1}/x;"};
v=([
c=[?x*150]*4,
a.reverse,
[[6,3,0].map{"a#{_1} /#{?x*15}/x"}*(?;*42)+';xx']*30,
a.map{_1.tr'14','25'},
c,
]*n=$/).gsub(/(^|;)(;*);/){$1+?x*$2.size+?;};
p,e=[0,1].map{|t|
g=([
"(mf /'/;#{a=(0..9).map{"f#{_1} /l#{_1}=1./"}*?;};\"';#{a.tr'l',?u};?\"",
(0..9).map{|i|
a="l#{i} /'/;\"';";
b="#{o[i-1]+?=if i>0}%+";
(1..9).map{
a<<l[_1-1]+" /%#{c=m[_1]}/;";
b<<c+";#{(d=_1+i)>10?'ca='+o[d-11]:d>9?'ca':o[d-1]}=%"+c
};
a+b+[m[1..],?+,?"]*';?'
},
"ca /'/;\"';"+(1..8).map{u[8-_1]+" /#{u[9-_1]}=1./;"}*''+u[0]+h='=1;?"',
(1..9).map{|i|
"u#{i} /'/;\"';"+(0..8-i).map{u[8-i-_1]+" /#{u[8-_1]}=1./;"}*''+u[i-1]+h
}
]*?;).split(/([^;]+;)/);
((0..43).map{|y|c='';
q=->{
a=(y-22).abs;
b=(c.size+_1-78).abs;
[a<7&&b<59||b<15,(b-30).abs<14][t]
};
110.times{c+=q[8]?g.shift||?;:q[-t]??;:' '};
c.gsub('; ',' ').rstrip
}*n).gsub(/(;| )(;;+)(;|$)/){$1+'/'+?x*($2.size-2)+'/'+$3}
};
F=File;
10.times{|i|
a="(nf /mf=l#{i}='/;nf=f#{i}=?';def/("+s[i].map!{"a#{_1}"}*','+')=(';
F.write"#{i}",a+?x*(150-a.size)+n+v[..-5]+'))&&'+n
};
a,*b="/)&&def((\n\n<<A";
c="))./(";
d=' }',?A,'__=<<B';
u.map{|v|b<<v+" %( <<#{v} ) ";d=d,v,"A\n#{v}=<<B"};
o.zip(l,m){a+=?;+_1+" /%#{_3}/";c+=(_3[?#]||?#+_3)+"))./("+_2+?,};
F.write'+',n*26+[p[..-10]+(a+c).tr('!@',%("')).gsub(/%(['"`])/,){$1},'#{ '+b*'; '+d*n,?B,')=']*n+n;
a,b=[u,o].map{|v|
(0..8).map{t=s[9-_1];t[0]&&t+=[1];v[8-_1]+" /#{t*?=}#/"}*?;+';a3='+?x*25
};
F.write'=',[n*26]*2*e+a+n+v+n+(b+n+v).tr(?a,?b)[..-9]+')if~exit'+n
eval->{%w[u=*? a..?i;l=*?j..? r;o=*?s..?z,?_
;m='#-*&|^`!@$ '.chars;s=[[3] ,[0,1,3,4,6],[
1,5],[1,4],[0, 4,6],[2,4],[2] ,[1,3,4,6],[],
[4], ];a= (0.. 7).m ap{[
?;*_ 1+'a 4',: a1,? x*(_
1+2) ]*"T /#{? x.*6 7-_1
}/x; "};v =([c =[?x *150
]*4, a.reverse,[[6, 3,0].map{"a#{_
1}T/ #{?x*15}/x"}*( ?;*42)+';xx']*
30,a .map{_1.tr'14' ,'25'},c,]*n=$
/).g sub( /(^| ;)(;
*);/ ){$1 +?x* $2.s
ize+ ?;}; p,e= [0,1
].ma p{|t |g=( ["(m
fT/' /;#{a=(0..9).m ap{"f#{_1}T/l#
{_1} =1./"}*?;};C"' ;#{a.tr'l',?u}
;?C" ",(0..9).map{| i|a="l#{i}T/'/
;C"' ;";b ="#{o[i-1]+?=i fTi>0}%+";(1..
9).m ap{a <<l[_1-1]+"T/% #{c=m[_1]}/;";
b<<c +";# {(d=_1+i)>10?' ca='+o[d-11]:d
>9?' ca': o[d- 1]}=
%"+c };a+ b+[m [1..
],?+ ,?"] *';? '},"
caT/ '/;C "';" +(1.
.8).map{u[8-_1 ]+"T/#{u[9-_1] }=1./;"}*''+u[
0]+h='=1;?"',( 1..9).map{|i|" u#{i}T/'/;C"';
"+(0..8-i).map {u[8-i-_1]+"T/ #{u[8-_1]}=1./
;"}* ''+u [i-1 ]+h}
]*?; ).sp lit( /([^
;]+; )/); ((0. .43)
.map {|y| c='' ;q=-
>{a= (y-22).abs;b=( c.size+_1-78).
abs; [a<7&&b<59||b< 15,(b-30).abs<
14][ t]};110.times{ c+=q[8]?g.shif
t||? ;:q[-t]??;:'T' };c.gsub(';T',
'TT' ).rstrip}*n).g sub(/(;|T)(;;+
)(;| $)/){$1+'/'+?x *($2.size-2)+'
/'+$ 3}}; F=Fi
le;1 0.ti mes{
|i|a ="(n fT/m
f=l# {i}= '/;n
f=f# {i}=?';def/("+ s[i].map!{"a#{
_1}" }*','+')=(';F. write"#{i}",a+
?x*( 150-a.size)+n+ v[..-5]+'))&&'
+n}; a,*b ="/)
&&de f((C nCn<
<A"; c=") )./(
";d= 'T}' ,?A,
'__= <<B';u.map{|v| b<<v+"TT%(T<<#
{v}T )TT";d=d,v,"AC n#{v}=<<B"};o.
zip( l,m){a+=?;+_1+ "T/%#{_3}/";c+
=(_3[?#]||?#+_
3)+"))./("+_2+
?,};F.write'+'
,n*2 6+[p [..-
10]+ (a+c ).tr ('!@',%("')).g
sub( /%([ '"`] )/,){$1},'#{TT
T'+b *';T T'+d *n,?B,')=']*n+
n;a, b=[u ,o].map{|v|(0.
.8). map{ t=s[9-_1];t[0]
&&t+ =[1] ;v[8-_1]+"T/#{
t*?= }#/" }*?; +';a3='+?x*25}
;F.w rite '=', [n*26]*2*e+a+n
+v+n +(b+ n+v) .tr(?a,?b)[..-
9]+' )if~ exit
'+n].join.tr('
TRICK',_1)}[+"
\x2025"<<-?\\]

Remarks

Run it with no argument. It will generate 12 files: 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, + and =.

ruby entry.rb

Concat and syntax highlight them.

cat 2 0 + 2 5 = | ruby -run -e colorize
cat 4 + 1 5 + 4 + 1 8 = | ruby -run -e colorize

I confirmed the following implementations/platforms:

  • ruby 3.4.1 (2024-12-25 revision 48d4efcb85) +YJIT +MN +PRISM [arm64-darwin22]
  • ruby 3.3.0 (2023-12-25 revision 5124f9ac75) +YJIT +MN [arm64-darwin22]

Description

Did you know that Ruby syntax can perform additive operations on two-digit numbers without Ruby runtime? This entry demonstrates a syntax level computation of Ruby grammar.

ruby entry.rb will generate 12 files: 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, + and =. These files constitute a calculator system that runs on Ruby parser.

To calculate 6 + 7, concat 6, +, 7 and =.

cat 6 + 7 =

The concatenated output is a Ruby script that does nothing. It is also an ASCII art of █ + █ = ██ rotated 90 degrees. Now, let's try syntax highlighting that code.

cat 6 + 7 = | ruby -run -e colorize

Wow! You can see the calculation result 6 + 7 = 13 as a colorized ASCII art!

This system can also add more than two numbers. All numbers should be one or two digits, and the answer should be less than 100.

cat 3 1 + 4 + 1 5 + 9 = | ruby -run -e colorize
cat 1 + 2 + 4 + 8 + 1 6 + 3 2 = | ruby -run -e colorize
cat 0 + 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 1 0 = | ruby -run -e colorize

If the syntax highlighting is hard to see, use this command to change the terminal color.

printf "\e]11;#000000\a\e]10;#333333\a\e]4;1;#ffaaaa\a"

Internals

To perform calculation, you need a storage and a control flow statement. Local variable existence can be used as a storage. Ruby syntax provides conditional local variable definition and local variable reset with state carry over which can be used as a control flow statement.

Conditional Local Variable Definition

Ruby syntax can define new local variables conditionally.

# Defines x and y if a is defined
a /x = y = 1./
# Defines x and y if a is not defined
a /1#/; x = y = 1
# Defines x or y depend on the existence of local variable a
a /(x=1);'/;(y=1);?'

Local Variables Reset

Local variables can be cleared by creating a new def scope.

x = y = z = 1
def f
# x, y, z are cleared

State Carry Over

Some state should be carried over to the next def scope. There are two tricks to do it.

a /%+/i; b /%-/i; def f(x)# +; def f(y) # -; def f(z)
a %(<<A); b %(<<B); def f
x=<<C
A
y=<<C
B
z=<<C
C

In both examples above, local variable defined in the new scope will be:

x # if both a and b are not defined
y # if a is defined
z # if b is defined

Combining these two tricks, Ruby syntax can carry over two states to the next def scope. In this system, two states represents upper digit and lower digit.

File Structure

# File 0, 1, 2, 3, 4, 5, 6, 7, 8, 9
(code) &&
# File +
(code) && def f(arg)=
# File =
(code) if exit
# cat 1 2 + 3 + 4 5 =
(one) &&
(two) &&
(plus) && def f(arg)=
(three) &&
(plus) && def f(arg)=
(four) &&
(five) &&
(equal) if exit

Limitation

Number to be added must be one or two digits. Answer of the addition must be less than 100.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment