User:JayCampbell/weave.rb
weave.rb - The brainfuck forking derivative interpreter
weave.rb is a Ruby interpreter by JayCampbell for the Weave programming language, which allows multiple bf programs to run concurrently, and adds a tape shared by all threads. Every thread that existed at the beginning of each cycle executes one instruction during that cycle.
-Y turns on Brainfork functionality. Threads are spawned every time the Y instruction is run.
-p turn on pbrain functionality. Upon reaching a set of (parenthesis), the innards of that block are saved as a subroutine "named" the value of the current cell. To execute this code later, set the current cell to that same number and run the colon (:) token.
-X turns on braintwist functionality, so the X token "swaps the data and code arrays, making it possible to store code in the data array for later execution." If the Brainfork option is turned on, "the X instruction only swaps the arrays of the thread calling the instruction. As a result, one thread's code array can be used as another's data array."
-j turns on JumpFuck functionality. The ampersand defines a jump point, the percent sign jumps there. Undefined jump points send you to cell zero. See the linked spec for info on jump point IDs and arguments.
-r makes weave.rb act according to Reversible Brainfuck rules.
-E turns on EsoAPI support. The file esoapi.dsk is created if it does not exist, which can be randomly accessed by programs using the correct null-prefixed commands. This program prints the contents of that file: .+++.>[.>]
-R turns on embedded Ruby support. is used as the trigger. Program lines may contain "#ruby" (optionally followed by a number, defaulting to zero) and ruby code. These ruby snippets are collected and triggered by sending the EsoAPI output of zero followed by 10. If a section number was supplied (e.g. #ruby3, or #ruby9) then it could be triggered with a 10-higher EsoAPI call (e.g. 0 3, or 0 19).
-v displays verbose output during the run.
All the switches can be used together. Multiple initial threads can be started using semicolons and/or multiple source files, and embedded Y's in each can cause further forking. Threads do not share pbrain functions, but calls and returns between threads are possible using the shared tape and delay loops.
All files specified at command line create separate program threads. Semicolons separating instructions also cause the source to be broken into threads. Each file's post-exclamation input text is appended (in filename order) to a master input stream available to all threads. If no input text is supplied in the source files, standard input is fetched. (Original Weave spec called for a ! character before threads; the standard use for that character is implemented instead - attaching input text to the end of a script.)
Other notes: Numbers do not wrap, and may be negative or large. Negative pointer references count backward from the end of the rightmost position ever referenced on that particular tape. weave.rb is also an interpreter and debugger for standard brainfuck programs.
Concurrent bf programming
The new instructions introduced by Weave (local/master tape switch, NOP for pausing) means arbitrary segments of the master tape could be reserved as communication channels and common storage for an indefinite number of bf threads. Feedback?
Examples
$ ./weave.rb 99.bf 99.bf 99.bf 999999 bbbooottttttllleeesss ooofff bbbeeeeeerrr ooonnn ttthhheee wwwaaallllll,,, 999999 bbbooottttttllleeesss ooofff bbbeeeeeerrr,,, $ ./weave.rb sange-archive/hello* HHHELLOWelloheallo t HelloW WORWoLrlDd! orld!! worldis! your name?
The following program produces the output GHOSTY:
~+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++; ~@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@..@@@@@@.@@@..@@@@.
The first thread switches to the master tape and starts incrementing. The second thread switches but NOP waits until the first has incremented enough, then prints during the appropriate cycles.
If you save the next few lines to a file called pbrain1.pb and run the command "./weave -p pbrain1.pb",
+([-]<[-]<[>+>+<<-]>>[<<+>>-]) +([-]>[-]+:<+) >>+++++++++++++[<+++++>-] ++: >++: >++: >++: <<<<.>.>.>.>.
... the output is ABCDE. Per the [|pbrain site], "This pbrain program initializes a memory location to 65, the ASCII value of the letter 'A'. It then calls a function for subsequent memory locations to copy the previous location and add one to it. Once a few cells are initialized, it prints all the cells to standard output."
Embedding ruby for debugging
>++++++++++++++++++++++++ # ruby print " hi "
+++++++++++++++++++++++++ # ruby8 print "tape size: #{thread[:tape].size}"
+++++++++++++++++++++++++< # ruby print "there "
>>++++++++++<< .>>.<< # this line triggers ruby0 by outputting 0 10
>..........< .>>.<< >>++++++++<< .>>.<< # run up to 18 for some debug
>>--------<< .>>.<< # go back to 10 and print hi again
$ ./weave.rb -E -R rubytest.b
hi there JJJJJJJJJJ hi there tape size: 3 hi there
weave.rb ruby source code
updated by JayCampbell 08:58, 2 December 2008 (UTC)
#!/usr/bin/ruby
# ./weave.rb [-v] [-Y] [-p] file1.txt file2.txt ...
# A threading debugging interpreter by Jay Campbell 2008
# for a variety of brainfuck derivative esoteric programming language
# http://esoteric.voxelperfect.net/wiki/User:JayCampbell/weave.rb
$sectorsize = 512
class WeaverParser
attr_accessor :threads, :commontape
def initialize
@threads = Array.new
@commontape = String.new
@commontape << 0.chr
@num = 0
end
def addthread( text )
allowed = '[<>+-.,]~@'
allowed << 'Yy' if $brainfork
allowed << '():' if $pbrain
allowed << 'X' if $braintwist
allowed << 'x' if $braintwist2
allowed << '&%' if $jumpfuck
thread = Hash.new
thread[:commands] = text.tr( '^' + allowed, '' )
thread[:codepointer] = 0
thread[:tape] = String.new
thread[:tape] << 0.chr
thread[:pointer] = 0
thread[:otherpointer] = 0
thread[:running] = true
thread[:local] = true
thread[:functions] = Hash.new
thread[:stack] = Array.new
thread[:num] = @num += 1
thread[:jumps] = Hash.new(0) if $jumpfuck
thread[:prevoutput] = String.new
calc_parens( thread )
@threads << thread
return thread
end
def calc_parens( thread )
thread[:leftsquares] = Hash.new
thread[:rightsquares] = Hash.new
thread[:rightrounds] = Hash.new
thread[:leftrounds] = Hash.new
prev_square = Array.new
prev_round = Array.new
(thread[:commandsize] = thread[:commands].size).times do |n|
c = thread[:commands][n].chr
if c == '[' then prev_square.push n
elsif c == ']'
jump = prev_square.unshift.pop
thread[:leftsquares].store(jump, n)
thread[:rightsquares].store(n, jump)
elsif c == '(' then prev_round.push n
elsif c == ')'
jump = prev_round.unshift.pop
thread[:leftrounds].store(jump, n)
thread[:rightrounds].store(n, jump)
end
end
end
def run
while (threads = @threads.select{ |t| t[:running] == true }).size > 0
threads.each do |thread|
if thread[:local] then tape = thread[:tape]; pointer = :pointer
else tape = @commontape ; pointer = :otherpointer end
tape << 0.chr while tape.size <= thread[pointer]
command = thread[:commands][thread[:codepointer]].chr
here = thread[pointer]
value = tape[here]
if command == '~' then thread[:local] = thread[:local] ? false : true
elsif command == '+' then tape[here] += 1
elsif command == '-' then tape[here] -= 1
elsif command == '>' then thread[pointer] += 1
elsif command == '<' then thread[pointer] -= 1
elsif command == ','
if $reversible
if value == 0 then getinput else thread[:running] = false end
else tape[here] = getinput end
elsif command == '.'
if $esoapi and thread[:prevoutput] == 0
if value == 1 then $esofile.seek( $sectorsize, IO::SEEK_CUR )
elsif value == 2 then $esofile.seek( -$sectorsize, IO::SEEK_CUR )
elsif value == 3
tape << 0.chr while tape.size <= (here + $sectorsize)
tape[here + 1 .. here + $sectorsize] = $esofile.read($sectorsize)
elsif value == 4 then
$esofile.syswrite tape[here + 1 .. here + $sectorsize]
elsif value == 5 then $esofile.seek( 0, IO::SEEK_SET )
elsif value == 8 then tape[here] = 0
elsif value > 9 and $ruby[value-10] then eval $ruby[value-10]
else $stdout.putc value end
thread[:prevoutput] = nil
else $stdout.putc value end
thread[:prevoutput] = value
elsif command == '[' and (value == 0 or ($reversible and value != 0))
thread[:codepointer] = thread[:leftsquares][thread[:codepointer]]
elsif command == ']' and value != 0
thread[:codepointer] = thread[:rightsquares][thread[:codepointer]] - 1
elsif $brainfork and command.downcase == 'y'
fork = thread.dup
tape[here] = 0
fork[:tape] = thread[:tape].dup
fork[:codepointer] += 1
fork[:pointer] += 1
fork[:tape] << 0.chr while fork[:tape].size <= fork[:pointer]
fork[:tape][fork[:pointer]] = 1
fork[:num] = @num += 1
@threads << fork
elsif $pbrain and command == '('
thread[:functions][value] = thread[:codepointer]
thread[:codepointer] = thread[:leftrounds][thread[:codepointer]]
elsif $pbrain and command == ')'
thread[:codepointer] = thread[:stack].pop
elsif $pbrain and command == ':' and func_addr = thread[:functions][value]
thread[:stack] << thread[:codepointer]
thread[:codepointer] = func_addr
elsif $braintwist and command == 'X'
tmp = thread[:tape]
thread[:tape] = thread[:commands]
thread[:commands] = tmp
calc_parens thread
tmp = thread[:codepointer]
thread[:codepointer] = thread[:pointer] - 1
thread[:pointer] = tmp
elsif $braintwist and command == 'x'
tmp = tape[here]
tape[here] = thread[:commands][thread[:codepointer]]
thread[:commands][thread[:codepointer]] = tmp
elsif $jumpfuck and command == '&'
thread[:jumps][value] = thread[:codepointer]
tape[here] = thread[:jumparg] || 0
thread[:jumparg] = 0
elsif $jumpfuck and command == '%'
thread[:jumparg] = tape[here + 1] || 0
thread[:codepointer] = thread[:jumps][value]
end
if (thread[:codepointer] += 1) >= thread[:commandsize]
if prev = thread[:stack].pop then thread[:codepointer] = prev
else thread[:running] = false end
end
if thread[:codepointer] < 0 then thread[:codepointer] = 0 end
if $verbose then $stdout.puts "t#{thread[:num]} #{command} #{thread[:codepointer]}/#{thread[:pointer]} #{tape.inspect}" end
end
end
if $verbose then $stderr.puts "\nMaster: "; p @commontape; @threads.each do |t| $stderr.puts "Thread: #{t[:num]}"; p t[:tape] end end
end
def getinput
if $input
if $input.size > 0 then return $input.unshift else return 0 end
else return ($stdin.getc || 0) end
end
end
$input = $verbose = $brainfork = $pbrain = false
files = Array.new
ARGV.each do |arg|
if arg[0].chr == '-'
arg[1..-1].split(//).each do |c|
if c == 'v' then $verbose = true
elsif c == 'y' or arg[1].chr == 'Y' then $brainfork = true
elsif c == 'X' then $braintwist = true
elsif c == 'x' then $braintwist2 = true
elsif c == 'p' then $pbrain = true
elsif c == 'r' then $reversible = true
elsif c == 'j' then $jumpfuck = true
elsif c == 'E' then $esoapi = true
elsif c == 'R' then $ruby = Hash.new
end
end
else files << arg end
end
unless files.size > 0 then $stderr.puts 'Usage: weave.rb [-v] [-Y] filename.txt'; exit end
if $esoapi then begin
$esofile = File.open('esoapi.dsk', File::RDWR|File::CREAT)
rescue
$stderr.puts 'Could not open esoapi.dsk, turning off EsoAPI support'
$esoapi = false
end end
parser = WeaverParser.new
files.each do |filename|
program = String.new
File.open(filename, 'r') do |f|
until f.eof?
line = f.gets.chomp
if line =~ /#\s*ruby\s*(\d+)? (.*)/ then $ruby[$1.to_i] ||= ''; $ruby[$1.to_i] << $2 + "\n" end
program << line.gsub(/#.*/,'')
end
end
# again per bf, bang means input, not new thread
program.split(/;/).each do |commands|
if commands =~ /(.*)!(.*)/ then commands = $1; $input ||= Array.new; $input << $2; $textinput = true end
parser.addthread(commands)
end
end
parser.run