Ruby 2 - 540 447 420 characters
Run as " ruby2.0 jumper.rb 'instructions' 'initialization data' ". 1.x Ruby won't work (no String.bytes method).
Added multi-line commands and comments and improved my putting.
i=$*[0].gsub(/\([^)]*\)/m,' ').scan(/(\??)\s*([#=:><+-])\s*(\d*)/m).map{|a|[a[0]!='?',a[1],a[2]==''?/[#=:]/=~a[1]?0:1:a[2].to_i]}
N=i.size
d=$*[1].bytes
r=p=0
while p<N
u,o,x=i[p]
p+=1
d[r]=0 if d[r].nil?
case o
when'#';r=x
when'>';r+=x
when'<';r-=x
when/[=+-]/;eval "d[r]#{o.tr'=',''}=x";d[r]%=256
when':';p=x;abort'Error'if p>=N
end if u||d[r]>0
abort'Error'if r<0
end
printf"%s\n",d.take_while{|v|v&&v!=0}.pack('C*')
Here's a test suite with some scatter-shot tests. The easiest way to use it is to stuff the code into t/jumper.t and run "perl t/jumper.t".
#/usr/bin/perl
use strict;
use warnings;
# timestamp: 2014 August 3, 19:00
#
# - Assume program takes machine code and initialization string as command
# line options.
# - Assume all required errors reported as "Error\n".
# - Go with the flow and suffix output with \n. Merged terminal newlines are
# unacceptable [I'm talkin' to YOU Ruby puts()!].
# - As per OP - jumping to > end-of-program must be an error.
use Test::More qw(no_plan);
# use Test::More tests => 4;
my $jumper = "jumper.rb";
#
# "happy" path
#
# starter tests provided by OP
is( `$jumper '=72>=101>=108>=108>=111>=32>=119>=111>=114>=108>=100>=33>=' '' 2>&1`, "Hello world!\n", "hello world (from user2992539)");
is( `$jumper '?:2 :4 >1 :0 =33 >1 =0' 'a' 2>&1`, "a!\n", 'append !, #1 (from user2992539)');
# simple variations
is( `$jumper '?:2 :4 >1 :0 =33 >1 =0' '' 2>&1`, "!\n", 'append !, #2');
is( `$jumper '?:2 :4 >1 :0 =33' '' 2>&1`, "!\n", 'append !, #3, no NUL');
# comment delimiters don't nest
is( `$jumper "(()=" 'oops' 2>&1`, "\n", "() don't nest");
# comments and termination
is( `$jumper '(start with a comment)?(comment w/ trailing sp) # (comment w/ surrounding sp) 1 =98' 'a' 2>&1`, "ab\n", 'walk to exit');
is( `$jumper '(start with a comment)? (comment w/ leading sp)= (comment w/ surrounding sp) 97()' '' 2>&1`, "\n", 'skip to exit');
is( `$jumper '#1=0 (actually two instructions, but it scans well) :5 #=(truncate further if not jumped over)' 'a b' 2>&1`, "Error\n", 'truncate & jump to exit');
# is RAM pointer initialized to 0?
is( `$jumper '-103(g-g) ?:1025(exit) =103 #4=10' 'good' 2>&1`, "good\n\n", 'intial string in right place?');
# TBD, do jumps work?
# TBD, do conditional jumps work?
# jump right to a harder case, copy byte 0 to byte 3 and format, e.g. input="Y" output="Y=>Y"
is( `$jumper '#1=61#2=62#4=0#3=#10=#(11:)?:13:20(13:)#3+#10+#0-:11(20:)#10(21:)?:23:28(23:)#0+#10-:21(28:)#' 'Y' 2>&1`, "Y=>Y\n", 'copy a byte');
# test memory allocation by dropping 255s at increasingly large intervals
is( `$jumper '#16=511 #64=511 #256=511 #1024=511 #4096=511 #16384=511 #65536=511 #262144=511 #1048576=511 #65536-255 (20:)?:23(exit) #=' 'wrong' 2>&1`, "\n", 'test alloc()');
# upcase by subtraction
is( `$jumper '-32' 't' 2>&1`, "T\n", 'upcase via subtraction');
# 2 nested loops to upcase a character, like so: #0=2; do { #0--; #1=16; do { #1--; #2--; } while (#1); } while (#0);
is( `$jumper '#=2 (2:)#- #1=16 (6:)#1- #2- #1?:6 #0?:2 #=32 #1=32' ' t' 2>&1`, " T\n", 'upcase via loops');
# downcase by addition
is( `$jumper '+32' 'B' 2>&1`, "b\n", 'downcase via addition');
# same thing with a loop, adjusted to walk the plank instead of jumping off it
is( `$jumper '#1 ?:3 :7 -<+ :0 #' 'B ' 2>&1`, "b\n", 'downcase via adder (from Sieg)');
# base 10 adder with carry
is( `$jumper '#0-48#10=9#11=#5=#0(9:)?:11:22(11:)#10?:14:22(14:)-#11+#5+#0-:9(22:)#0?:110#11(25:)?:27:32(27:)#0+#11-:25(32:)#0+48>-43?:110=43>-48#10=9#11=#2(45:)?:47:58(47:)#10?:50:58(50:)-#11+#5+#2-:45(58:)#2?:110#11(61:)?:63:68(63:)#2+#11-:61(68:)#2+48>-61?:110=61>?:110=32#10=9#11=#5-10(83:)?:85:94(85:)#10?:88:94(88:)-#11+#5-:83(94:)#5?:99#4=49:100(99:)+10(100:)#11(101:)?:103:108(103:)#5+#11-:101(108:)#5+48' '1+1=' 2>&1`, "1+1= 2\n", 'base 10 adder, #1');
is( `$jumper '#0-48#10=9#11=#5=#0(9:)?:11:22(11:)#10?:14:22(14:)-#11+#5+#0-:9(22:)#0?:110#11(25:)?:27:32(27:)#0+#11-:25(32:)#0+48>-43?:110=43>-48#10=9#11=#2(45:)?:47:58(47:)#10?:50:58(50:)-#11+#5+#2-:45(58:)#2?:110#11(61:)?:63:68(63:)#2+#11-:61(68:)#2+48>-61?:110=61>?:110=32#10=9#11=#5-10(83:)?:85:94(85:)#10?:88:94(88:)-#11+#5-:83(94:)#5?:99#4=49:100(99:)+10(100:)#11(101:)?:103:108(103:)#5+#11-:101(108:)#5+48' '9+9=' 2>&1`, "9+9=18\n", 'base 10 adder, #2');
# order of assignment shouldn't affect order of print
is( `$jumper '#1=98 #0=97' '' 2>&1`, "ab\n", 'print order != assignment order');
# are chars modulo 256?
is( `$jumper '#10(#10 defaults to 0) +255+(#10 += 256) ?#(skip if #10==0) =' 'good' 2>&1`, "good\n", 'memory values limited to 0<x<255');
# go for the cycle;
is( `$jumper '(0:)+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ (256:)#4=10' 'BCID' 2>&1`, "ACID\n\n", 'cycle character less 1, PC>255');
# same thing with a loop;
is( `$jumper '#4=255(#4 = 255) (2:)#1+(#1++) #4-(#4--) ?:2(loop 255 times) #4=10(#4 = NL)' 'ADID' 2>&1`, "ACID\n\n", 'cycle character less 1, PC>255');
# Exercise the program counter.
# PC > 255;
is( `$jumper '(0:)= (1:)############################################################################################################################################################################################################################################################### (256:)?:259 (257:)+ (258:):1 (259:)=97#3=10' 'a==' 2>&1`, "a==\n\n", 'program counter range >255');
#
# "sad" path
#
# Error checking required by the specification.
#
# simplest test case of PC going out of bounds
is( `$jumper ':2' '' 2>&1`, "Error\n", 'program counter too big by 1');
is( `$jumper ':1024' '' 2>&1`, "Error\n", 'program counter in space');
is( `$jumper ':1073741824' '' 2>&1`, "Error\n", 'program counter in hyperspace');
# try to drive program counter negative, if 32-bit signed integer
is( `$jumper ':2147483648(exit)' 'ridiculous speed' 2>&1`, "Error\n", 'program counter goes negative?, #1');
# try to drive program counter negative, if 64-bit signed integer
is( `$jumper ':9223372036854775808 (exit)' 'ludicrous speed' 2>&1`, "Error\n", 'program counter goes negative?, #2');
# spaces not allowed in operand; error or silently ignore (my choice)
isnt(`$jumper '#= #= #= #= #= +1 4 ' 'aops' 2>&1`, "oops\n", 'do not accept spaces in operands');
# ditto w/ a comment ; error or silently ignore (my choice)
isnt(`$jumper '#= #= #= #= #= +1(not valid)4 ' 'aops' 2>&1`, "oops\n", 'do not accept spaces in operands');
# RAM pointer error-checking; "Error" or "" are OK
isnt( `$jumper '<>=' 'oops' 2>&1 | grep -v Error`, "oops\n", 'unused negative RAM pointer behavior unspecified');
# RAM pointer negative and use it
is( `$jumper '<=' '' 2>&1`, "Error\n", 'cannot use negative RAM pointer, #1');
# check for RAM pointer wrap-around
is( `$jumper '<=' '0123456789' 2>&1`, "Error\n", 'cannot use negative RAM pointer, #2');
# The way I read this
# "Commands and arguments may be delimited with spaces or new lines but
# not necessary."
# multi-line commands are legit.
is( `$jumper "#4#?\n=" 'oops' 2>&1`, "\n", 'multi-line commands allowed');
# Multi-line comments would be consistent with multi-line commands, but I can't
# find something I can translate into a "must" or "must not" requirement in
# "Program can have comments between (). ... Comments can be placed
# anywhere."
# Until uncertainty resolved, no test case.
#
# "bad" path
#
# These tests violate the assumption that the instruction stream is wellll-farmed.
#
# characters not in the language; error or (my choice) silently skip
isnt(`$jumper 'x =' 'oops' 2>&1`, "oops\n", 'opcode discrimination');
# is ? accepted as an operator (vs operation modifier); error or (my choice) silently skip
is(`$jumper '(bad 0, good 0:)??0 (bad 1, good 0:):3 (bad 2, good 1:)#0' '' 2>&1`, "Error\n", '? not accepted as an opcode');
exit 0;
Ungolfed version.
#
# Turing Machine Mach 2.0.
# Tape? Tape? We don't need no stinkin' tape! We gots RAM!
#
# dM = data memory
# iM = instruction memory
# pC = program counter
# rP = RAM pointer
# u, o, x = current instruction being executed
#
# N = number of instructions in instruction memory
#
# instruction decoder
iM = $*[0].gsub(/\([^)]*\)/m,' ').scan(/(\??)\s*([#=:><+-])\s*(\d*)/m).map { |a|
[
a[0] != '?',
a[1],
(a[2] == '') ? (/[#=:]/ =~ a[1] ? 0 : 1) : a[2].to_i
]
}
pC = 0
N = iM.size
dM = $*[1].bytes
rP = 0
while pC < N do
# u, unconditional instruction, execute if true || (dM[rP] > 0)
# skip if false && (dM[rP] == 0)
# o, operator
# x, operand
(u, o, x) = iM[pC]
pC += 1
dM[rP] = 0 if dM[rP].nil?
if u || (dM[rP] > 0)
case o
when '#'
rP = x
when '>'
rP += x
when '<'
rP -= x
when /[=+-]/
eval "dM[rP]#{o.tr'=',''}=x"
dM[rP] %= 256
when ':'
pC = x
abort 'Error' if pC >= N
end
end
abort 'Error' if rP < 0
end
printf "%s\n", dM.take_while{|v|v&&v!=0}.pack('C*')
A quickie proto-assembler.
#
# Jumper "assembler" - symbolic goto labels.
#
# what it does:
# - translates labels/targets into absolute position
# @label ?:good_exit
# ...
# :label
#
# - a label is [a-zA-Z][a-zA-Z0-9_]*
# - a target is @label
# - one special label:
# - "hyperspace" is last instruction index + 1
# - strips out user comments
# - everything from "//" to EOL is stripped
# - jumper comments are stripped
# - adds "label" comments of the form "(ddd:)"
# limitations & bugs:
# - multi-line jumper comments aren't alway handled gracefully
# - a target not followed by an instruction will reference
# the previous instruction. this can only happen
# at the end of the program. recommended idiom to
# avoid this:
# @good_exit #
# what it doesn't do:
# - TBD, simple error checking
# - labels defined and not used
# - TBD, symbolic memory names
#
# Example:
#
# input -
# (
# adder from Sieg
# )
# @loop_head # 1 // while (*(1)) {
# ?:continue
# :good_exit
#
# @continue - // *(1) -= 1;
# <- // *(0) += 1;
# +
# :loop_head // }
# @good_exit #
#
# output -
# (0:) #1 ?:3 :7 (3:) - < + :0 (7:)#
rawSource = ARGF.map do |line|
line.gsub(/\([^)]*\)/, ' ') # eat intra-line jumper comments
.gsub(/\/\/.*/, ' ') # eat C99 comments
.gsub(/^/, "#{$<.filename}@#{$<.file.lineno}\n") # add line ID
end.join
rawSource.gsub! /\([^)]*\)/m, '' # eat multi-line jumper comments
#
# Using example from above
#
# rawSource =
# "sieg.ja@1\n \n" +
# "sieg.ja@4\n@loop_head # 1\n"
# ...
# "sieg.ja@12\n@good_exit # \n"
instructionPattern = %r{
(?<label> [[:alpha:]]\w* ){0}
(?<operator> \??\s*[#=:><+-]) {0}
(?<operand> \d+|[[:alpha:]]\w* ){0}
\G\s*(@\g<label>\s*)?(\g<operator>\s*)?(\g<operand>)?
}x
FAIL = [nil, nil, nil]
instructionOffset = 0
iStream = Array.new
target = Hash.new
targetComment = nil
for a in rawSource.lines.each_slice(2) do
# only parse non-empty lines
if /\S/ =~ a[1]
m = nil
catch( :parseError ) do
chopped = a[1]
while m = instructionPattern.match(chopped)
if m.captures.eql?(FAIL) || (!m[:operator] && m[:operand])
m = nil
throw :parseError
end
if m[:label]
if target.has_key?(m[:label].to_sym)
printf $stderr, a[0].chomp + ": error: label '#{m[:label]}' is already defined"
abort a[1]
end
target[ m[:label].to_sym ] = instructionOffset
targetComment = "(#{instructionOffset}:)"
end
if m[:operator]
iStream[instructionOffset] = [
targetComment,
m[:operator],
/\A[[:alpha:]]/.match(m[:operand]) ? m[:operand].to_sym : m[:operand]
]
targetComment = nil
instructionOffset += 1
end
chopped = m.post_match
if /\A\s*\Z/ =~ chopped
# nothing parseable left
break
end
end
end
if !m
printf $stderr, a[0].chomp + ": error: parse failure"
abort a[1]
end
end
end
# inject hyperspace label
target[:hyperspace] = instructionOffset
# replace operands that are labels
iStream.each do |instruction|
if instruction[2]
if !(/\A\d/ =~ instruction[2]) # its a label
if target.has_key?(instruction[2])
instruction[2] = target[instruction[2]]
else
abort "error: label '@#{instruction[2]}' is used but not defined"
end
end
end
puts instruction.join
end
How is "Aborted" or "Aborted (core dumped)"? – S.S. Anne – 2020-02-05T00:50:52.587
I will write some test programs in Jumper after some time and own interpreter. – Somnium – 2014-07-24T16:55:36.463
"to first zero byte excluding" So if there still come other bytes after the first zero byte, we shouldn't output them? – ProgramFOX – 2014-07-24T17:15:51.550
Yes, we shouldn't. That is done to not to clear whole used memory, but only copy output to beginning. – Somnium – 2014-07-24T17:24:11.943
5Could you give us some sample programs and their outputs? – arshajii – 2014-07-24T17:47:41.053
@arshajii Some test programs added. Will be more. It's a bit hard to think in Jumper but fun) – Somnium – 2014-07-25T08:48:34.280
Simple language, thus fun challenge. Thanks! – seequ – 2014-07-25T20:26:25.800
May comments be placed between the operator and its argument? – Oberon – 2014-07-25T22:19:27.013
@Oberon comments may be placed anywhere. – Somnium – 2014-07-25T22:32:29.123
@user2992539 Darn, that breaks my interpreter. I thought comments would count as whitespace. Will fix later. – seequ – 2014-07-26T12:55:04.377
1Could you specify the exact error message for negative indices? I think the difference between the two currently leading answers is less than the difference in their error messages, so I think it would be fairer if this was precisely specified. – Martin Ender – 2014-07-26T13:16:07.460
Also, what about invalid arguments for the go-to command? And can the program crash on any invalid syntax? And could you please add test cases which use the remaining commands as well (and also one where the RAM size increases)? – Martin Ender – 2014-07-26T20:49:14.167
If the input is invalid syntax, what should happen? – soktinpk – 2014-07-26T22:25:20.863
You should leave comments on the answers that don't comply with your changed spec regarding invalid syntax (for instance, that'll cost my submission at least 40 characters, I think) – Martin Ender – 2014-07-27T19:32:48.000
Q 1 - What constitutes "program ends executing"? Assumption 1 - Terminate any time the program counter goes out of range. Q 2 - Should post-termination printing add a newline? Assuption 2 - No. – Scott Leadley – 2014-07-28T02:05:00.480
@ScottLeadley Q1 - I mean successfully ends executing - after last command is executed without error. If
:
command send pointer out of range - it is error. Q2 - it's not necessary, but if it doesn't cost you bytes it's preferable. – Somnium – 2014-07-28T07:00:55.170It is definitely more difficult to write something useful in Jumper than in Brainfuck. A most devious language. – Fors – 2014-07-28T16:25:05.157
The 2nd chunk of code in my submission is a test harness. Most submissions (except mine, of course :-) fail. If you care to point out blatantly wrong test results {comment at my post so we don't clutter the OP comment thread}, I'll fix them if I have time. Don't pester me over the newline vs. no-newline suffixing. You can change that yourself. – Scott Leadley – 2014-07-28T17:23:04.443
@user2992539 After rereading your answer to Q1, I believe you're saying the interpreter should display "Error" unless a program falls off the end by advancing the program counter by 1. Jumping to the same address, end-of-program+1, is an error. Is this correct? If so, I'm going to need to make some changes. – Scott Leadley – 2014-07-30T23:48:02.060
Feature request for Jumper 2.0: The token
*
may be used as an argument (in place of a number). The contents of the current cell are used as the argument value. – aschepler – 2014-07-31T00:11:56.690Jumper is an elegantly minimal toy. A more modern Turing machine. Although :* and #* would give Jumper a useful subroutine mechanism and an accumulator would be nice, I think adding more to Jumper would detract from it's value as a teaching tool. If you're looking for realism, there are plenty of PDP-8, 4040, 6502, 680x and 808x emulators available to play with. – Scott Leadley – 2014-07-31T02:03:51.340
@user2992539 As further exploration of your answer to Q1, I believe you're saying that "#" exits successfully and ":1" throws an error. – Scott Leadley – 2014-07-31T02:07:23.327
@ScottLeadley end-of-program+1 I thought to be an error. If
#
and:1
you mean as complete program, then you are right. – Somnium – 2014-07-31T08:29:16.367Upon reflection, Jumper 2.0 with more features seems like a good idea. Incrementally connecting the mathematical model to more realistic systems has a lot of pedagogical value. – Scott Leadley – 2014-08-01T02:55:27.687
@user2992539 Should the interpreter throw an error as soon as the RAM pointer goes negative (a underflow/wrap-around condition with real hardware), or only when a =+-?, i.e. memory load/store operation, occurs? Assumption, I've implemented it as throwing an error as soon as the RAM pointer goes negative. See the test suite attached to my solution for more details. – Scott Leadley – 2014-08-01T03:15:09.603