base-lang 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 352641028b34da47dec624f8cb3e135e6c336e47f854931649104b0c9a29629c
4
+ data.tar.gz: 490c1fa4fd25ec278bf2dd12e67f0cecc1b53da7a3eee848daee81121ddbff4e
5
+ SHA512:
6
+ metadata.gz: b7d1e64864df4c093c56c70c2d212013c926082e213f8aad86ead4120e2f3cb0da5df3153402fa0a70da5716723a71163a1172a23a6a577cee4e23e5337725ed
7
+ data.tar.gz: 054abcc979edead6e60520bba2926a7614899fe73e349f81418ac87b617829de6d196b98c85b7f8156fb27b727a3fc45c0641440a5d8cdbc1667b133be145d54
data/README.md ADDED
@@ -0,0 +1,124 @@
1
+ # Base
2
+
3
+ A simple stack-based assembly programming language and VM, made for learning.
4
+
5
+ ## Using it
6
+
7
+ Enter a simple program:
8
+
9
+ # alphabet.base
10
+
11
+ .main
12
+ push "A"
13
+
14
+ .loop
15
+ # Print letter to the screen
16
+ duplicate
17
+ out
18
+
19
+ # Check whether we've reached Z yet
20
+ duplicate
21
+ push "Z"
22
+ subtract
23
+
24
+ # If we have, we're done
25
+ push done
26
+ betz
27
+
28
+ # Advance to the next letter and loop
29
+ push 1
30
+ add
31
+ push loop
32
+ jump
33
+
34
+ .done
35
+ # Program ends
36
+ discard
37
+ push "\n"
38
+ out
39
+ halt
40
+
41
+ Run it:
42
+
43
+ % gem install base-lang
44
+
45
+ % base alphabet.base
46
+ ABCDEFGHIJKLMNOPQRSTUVWXYZ
47
+
48
+ ## Language
49
+
50
+ All instructions take no arguments except the `push` instruction which takes the value to push to the stack.
51
+
52
+ Values may be an integer, a single character in double quotes, a label, or the special text `ip` which refers to
53
+ the current instruction pointer.
54
+
55
+ Signed integers of arbitrary size can be used. Memory locations start at 0 and by default to up to 1MB (1048575).
56
+
57
+ operation | op code | stack impact | description
58
+ -|-|-|-
59
+ debug | 0 | | enters the debugger
60
+ push (value or label) | 1, value | + | pushes the value onto the stack
61
+ discard | 2 | - | discards the top entry on the stack
62
+ duplicate | 3 | + | duplicates the top entry on the stack
63
+ write | 4 | -- | writes the second entry on the stack to the memory location at the top entry on the stack
64
+ read | 5 | -+ | reads from the memory location on the stack and puts the result on the stack
65
+ add | 6 | --+ | adds the top two entries on the stack and puts the result on the stack
66
+ subtract | 7 | --+ | subtracts the top entry on the stack from the second entry on the stack and puts the result on the stack
67
+ jump | 8 | - | jumps to the location indicated by the top entry on the stack
68
+ bltz | 9 | -- | jumps to the location indicated by the top entry on the stack if the second entry on the stack is less than 0
69
+ bgtz | 10 | -- | jumps to the location indicated by the top entry on the stack if the second entry on the stack is greater than 0
70
+ betz | 11 | -- | jumps to the location indicated by the top entry on the stack if the second entry on the stack is equal to 0
71
+ bnetz | 12 | -- | jumps to the location indicated by the top entry on the stack if the second entry on the stack is not equal to 0
72
+ out | 13 | - | outputs the top entry on the stack to stdout, interpreted as a unicode character
73
+ halt | 13 | | halts the program
74
+
75
+ ## Compiler
76
+
77
+ Any text including and after a `#` in the code is treated as a comment and ignored.
78
+
79
+ ### Labels and data
80
+
81
+ You can use labels to mark a place in the code:
82
+
83
+ # infinite loop
84
+ .marker
85
+ push marker
86
+ jump
87
+
88
+ `.main` is a special label. If specified, code execution will start at this point.
89
+
90
+ You can also use labels to introduce data:
91
+
92
+ .three_bytes 1, 2, 3
93
+
94
+ The data can also be a string, which is interpreted as a list of bytes, or a mix of the two
95
+
96
+ .message "Hello!", 10, 0
97
+
98
+ ### Macros
99
+
100
+ Simple macros can be defined as a comma-separated list of operations (or other macros, as long as they don't recurse):
101
+
102
+ macro increment push 1, add
103
+
104
+ push "A"
105
+ increment
106
+ out # outputs a "B"
107
+
108
+ You'll need to define a macro before you use it in your file.
109
+
110
+ ## Debugger
111
+
112
+ Base has an integrated debugger that allows you to trace through your code, view memory and stack, and disassemble code.
113
+
114
+ You can start it by calling `debug` in your program, or by running `base` with the `--debug` option in which case
115
+ it'll start immediately on program startup.
116
+
117
+ When in the debugger, type `h` for help.
118
+
119
+ ## Licence and contributing
120
+
121
+ MIT licence.
122
+
123
+ Feel free to contribute! It's intentionally simple, so the intention isn't to add any more operations than what is
124
+ there.
data/bin/base ADDED
@@ -0,0 +1,40 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'optparse'
4
+ require_relative "../lib/base"
5
+
6
+ options = {}
7
+ OptionParser.new do |parser|
8
+ parser.banner = "Usage: base [options] filename.base"
9
+
10
+ parser.on("-d", "--debug", "Start debugger immediately") do
11
+ options[:debug] = true
12
+ end
13
+
14
+ parser.on("-c", "--compile", "Compile only and output program code") do
15
+ options[:compile] = true
16
+ end
17
+
18
+ parser.on("-h", "--help", "Prints this help") do
19
+ puts parser
20
+ exit 1
21
+ end
22
+ end.parse!
23
+
24
+ if ARGV.length != 1
25
+ $stderr.puts("one source file must be specified")
26
+ exit 1
27
+ end
28
+
29
+ begin
30
+ program = Base::Compiler.new.compile(ARGV[0])
31
+
32
+ if options[:compile]
33
+ puts program.join(",")
34
+ else
35
+ Base::VM.new(program, debug: options[:debug] || false).run
36
+ end
37
+
38
+ rescue Base::Compiler::Error, Base::VM::Error => e
39
+ $stderr.puts("error: #{e.message}")
40
+ end
@@ -0,0 +1,150 @@
1
+ module Base
2
+ class Compiler
3
+ Error = Class.new(StandardError)
4
+
5
+ def initialize
6
+ @parsing = []
7
+ @memory = []
8
+ @labels = {}
9
+ @macros = {}
10
+ end
11
+
12
+ def compile(file)
13
+ lines = File.read(file).split("\n")
14
+ parse(lines)
15
+
16
+ main = @labels["main"] || 0
17
+ [main] + @memory
18
+ end
19
+
20
+ private
21
+
22
+ def parse(lines)
23
+ lines.each.with_index do |line, index|
24
+ begin
25
+ parse_line(line, index + 1)
26
+ rescue Error => e
27
+ raise Error, "#{e.message} on line #{index + 1}"
28
+ end
29
+ end
30
+
31
+ @parsing.each do |index, line_number|
32
+ begin
33
+ @memory[index] = parse_arg(@memory[index], index)
34
+ rescue Error => e
35
+ raise Error, "#{e.message} on line #{line_number}"
36
+ end
37
+ end
38
+ end
39
+
40
+ def parse_line(line, number, macro_entry = [])
41
+ line = line.gsub(/#(.+)/, '').strip
42
+ case line
43
+ when ""
44
+ nil
45
+
46
+ when /\A\.([a-z_][a-z0-9_]*)(?:\s+(.+))?\z/i
47
+ raise Error, "label already defined" if @labels[$1]
48
+ raise Error, "can't use 'ip' as a label name" if $1 == 'ip'
49
+ @labels[$1] = @memory.length
50
+ @memory += parse_static($2) if $2
51
+
52
+ when /\Apush\s+(.+)\z/
53
+ @memory += [VM::COMMANDS.index("push"), $1]
54
+ @parsing << [@memory.length - 1, number]
55
+
56
+ when /\Amacro\s+(\S+)\s+(.+)/
57
+ raise Error, "label already defined" if @macros[$1]
58
+ raise Error, "macros cannot replace base instructions" if VM::COMMANDS.member?($1)
59
+ @macros[$1] = $2.split(/\s*,\s*/)
60
+
61
+ else
62
+ if op_code = VM::COMMANDS.index(line)
63
+ @memory << op_code
64
+ elsif macro = @macros[line]
65
+ if macro_entry.include?(macro)
66
+ raise Error, "recursive call to macro #{macro}"
67
+ end
68
+
69
+ macro.each do |macro_line|
70
+ parse_line(macro_line, number, macro_entry + [macro])
71
+ end
72
+ else
73
+ raise Error, "unknown command '#{line}'"
74
+ end
75
+ end
76
+ end
77
+
78
+ def parse_arg(arg, location)
79
+ case arg
80
+ when '"\n"'
81
+ "\n".ord
82
+ when /\A"\\?([^"])"\z/
83
+ $1.ord
84
+ when "ip"
85
+ location - 1
86
+ when /\A(-?[0-9]+)\z/
87
+ $1.to_i
88
+ when /\A([a-z_][a-z0-9_]*)\z/i
89
+ @labels[$1] or raise Error, "no such label '#{$1}'"
90
+ else
91
+ raise Error, "unknown argument '#{arg}'"
92
+ end
93
+ end
94
+
95
+ def parse_static(arg)
96
+ chars = arg.chars
97
+ output = []
98
+ buffer = ""
99
+ state = :start
100
+
101
+ loop do
102
+ char = chars.shift
103
+ if char == "-" && state == :start
104
+ buffer = "-"
105
+ state == :number
106
+ elsif ("0".."9").include?(char) && [:start, :number].include?(state)
107
+ buffer += char
108
+ state = :number
109
+ else
110
+ if state == :number
111
+ output << buffer.to_i
112
+ buffer = ""
113
+ state = :start
114
+ end
115
+
116
+ if char.nil?
117
+ if state == :start
118
+ break output
119
+ else
120
+ raise Error, "invalid data"
121
+ end
122
+ end
123
+
124
+ if [',', ' '].include?(char) && state == :start
125
+ nil
126
+ elsif char == '"' && state == :start
127
+ state = :string
128
+ elsif char == '"' && state == :string
129
+ output += buffer.chars.map(&:ord)
130
+ buffer = ""
131
+ state = :start
132
+ elsif char == '\\' && state == :string
133
+ state = :escape
134
+ elsif state == :string
135
+ buffer << char
136
+ elsif state == :escape
137
+ if char == "n"
138
+ buffer << "\n"
139
+ else
140
+ buffer << char
141
+ end
142
+ state = :string
143
+ else
144
+ raise Error, "invalid data"
145
+ end
146
+ end
147
+ end
148
+ end
149
+ end
150
+ end
@@ -0,0 +1,87 @@
1
+ module Base
2
+ class Debugger
3
+ attr_reader :vm
4
+
5
+ PUSH = VM::COMMANDS.index("push")
6
+
7
+ def initialize(vm)
8
+ @vm = vm
9
+ end
10
+
11
+ def debug
12
+ loop do
13
+ next_instruction = memory[ip] == PUSH ? "push #{memory[ip + 1]}" : VM::COMMANDS[memory[ip]] || "???"
14
+ puts "ip=#{ip} stack=#{stack.inner} #{next_instruction}"
15
+ print "> "
16
+
17
+ command = $stdin.gets&.strip
18
+ case command
19
+ when ""
20
+ nil
21
+ when "n"
22
+ return true
23
+ when "c", nil
24
+ return false
25
+ when "q"
26
+ exit
27
+ when /\Am(?:\s+([0-9]+))?(?:\s+([0-9]+))?/
28
+ memory_dump($1 && $1.to_i || 0, $2 && $2.to_i)
29
+ when /\Ad(?:\s+([0-9]+))?(?:\s+([0-9]+))?/
30
+ disassemble($1 && $1.to_i || ip, $2 && $2.to_i)
31
+ else
32
+ puts "n - next instruction"
33
+ puts "c - stop debugging and continue"
34
+ puts "m [start [length]] - dump memory"
35
+ puts "d [start [length]] - disassemble"
36
+ puts "q - quit"
37
+ end
38
+ end
39
+ end
40
+
41
+ private
42
+
43
+ def memory
44
+ vm.memory
45
+ end
46
+
47
+ def ip
48
+ vm.ip
49
+ end
50
+
51
+ def stack
52
+ vm.stack
53
+ end
54
+
55
+ def memory_dump(location, length)
56
+ data = memory[location..(length ? location + length - 1 : -1)]
57
+
58
+ data.each_slice(8).with_index.each do |slice, index|
59
+ print "%8d | " % (location + index * 8)
60
+ slice.each do |byte|
61
+ print "%4d " % byte
62
+ end
63
+ puts
64
+ end
65
+ end
66
+
67
+ def disassemble(location, length)
68
+ end_location = length ? location + length : memory.length
69
+
70
+ while location < end_location && memory[location]
71
+ initial_location = location
72
+ op_code = memory[location]
73
+
74
+ if op_code == PUSH
75
+ value = memory[location + 1]
76
+ instruction = "push #{value}"
77
+ location += 2
78
+ else
79
+ instruction = VM::COMMANDS[op_code] || "#{op_code}???"
80
+ location += 1
81
+ end
82
+
83
+ puts "%8d | %s" % [initial_location, instruction]
84
+ end
85
+ end
86
+ end
87
+ end
data/lib/base/stack.rb ADDED
@@ -0,0 +1,25 @@
1
+ module Base
2
+ class Stack
3
+ Error = Class.new(StandardError)
4
+
5
+ def initialize
6
+ @stack = []
7
+ end
8
+
9
+ def push(value)
10
+ @stack.push(value)
11
+ end
12
+
13
+ def pop
14
+ @stack.pop or raise Error, "pop requested on empty stack"
15
+ end
16
+
17
+ def empty?
18
+ @stack.empty?
19
+ end
20
+
21
+ def inner
22
+ @stack
23
+ end
24
+ end
25
+ end
data/lib/base/vm.rb ADDED
@@ -0,0 +1,122 @@
1
+ module Base
2
+ class VM
3
+ Error = Class.new(StandardError)
4
+
5
+ MAX_MEMORY = 1048576
6
+
7
+ attr_reader :ip, :stack, :memory
8
+
9
+ COMMANDS = %w(debug push discard duplicate write read add subtract jump bltz bgtz betz bnetz out halt)
10
+
11
+ def initialize(program, debug: false)
12
+ @ip = program.shift
13
+ @memory = program
14
+ @debug = debug
15
+ @stack = Stack.new
16
+ end
17
+
18
+ def run
19
+ loop do
20
+ raise Error, "IP reached end of memory" if memory[ip].nil?
21
+
22
+ @debug = Debugger.new(self).debug if @debug
23
+
24
+ begin
25
+ result = execute
26
+ break if result == :halt
27
+ rescue Error => e
28
+ $stderr.puts("error: #{e.message}")
29
+ @debug = true
30
+ end
31
+ end
32
+
33
+ raise Error, "warning: stack not empty at program termination, #{stack.inner}" unless @stack.empty?
34
+ end
35
+
36
+ private
37
+
38
+ def execute
39
+ case COMMANDS[memory[ip]]
40
+ when "debug"
41
+ @debug = true
42
+
43
+ when "halt"
44
+ return :halt
45
+
46
+ when "push"
47
+ @ip += 1
48
+ arg = memory[ip]
49
+ stack.push(arg)
50
+
51
+ when "discard"
52
+ stack.pop
53
+ when "duplicate"
54
+ value = stack.pop
55
+ stack.push(value)
56
+ stack.push(value)
57
+
58
+ when "write"
59
+ location = stack.pop
60
+ value = stack.pop
61
+ write(location, value)
62
+ when "read"
63
+ location = stack.pop
64
+ stack.push(read(location))
65
+
66
+ when "add"
67
+ a = stack.pop
68
+ b = stack.pop
69
+ stack.push(a + b)
70
+ when "subtract"
71
+ a = stack.pop
72
+ b = stack.pop
73
+ stack.push(b - a)
74
+
75
+ when "jump"
76
+ @ip = stack.pop - 1
77
+ when "bltz"
78
+ dest = stack.pop
79
+ test = stack.pop
80
+ @ip = dest - 1 if test < 0
81
+ when "bgtz"
82
+ dest = stack.pop
83
+ test = stack.pop
84
+ @ip = dest - 1 if test > 0
85
+ when "betz"
86
+ dest = stack.pop
87
+ test = stack.pop
88
+ @ip = dest - 1 if test == 0
89
+ when "bnetz"
90
+ dest = stack.pop
91
+ test = stack.pop
92
+ @ip = dest - 1 if test != 0
93
+
94
+ when "out"
95
+ print(stack.pop.chr)
96
+ else
97
+ raise Error, "invalid op code #{memory[ip]}"
98
+ end
99
+
100
+ @ip += 1
101
+ rescue Error, Stack::Error => e
102
+ raise Error, "#{e.message} at IP #{ip}"
103
+ end
104
+
105
+ private
106
+
107
+ def read(location)
108
+ validate_location(location)
109
+ memory[location] || 0
110
+ end
111
+
112
+ def write(location, value)
113
+ validate_location(location)
114
+ memory[location] = value
115
+ end
116
+
117
+ def validate_location(location)
118
+ raise Error, "locations must be non-negative" if location < 0
119
+ raise Error, "locations must be less than #{MAX_MEMORY}" if location >= MAX_MEMORY
120
+ end
121
+ end
122
+ end
data/lib/base.rb ADDED
@@ -0,0 +1,4 @@
1
+ require_relative 'base/stack'
2
+ require_relative 'base/compiler'
3
+ require_relative 'base/vm'
4
+ require_relative 'base/debugger'
metadata ADDED
@@ -0,0 +1,50 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: base-lang
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Mog Nesbitt
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2022-03-26 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: A simple stack-based assembly programming language and VM, made for learning.
14
+ email: mog@seriousorange.com
15
+ executables:
16
+ - base
17
+ extensions: []
18
+ extra_rdoc_files: []
19
+ files:
20
+ - README.md
21
+ - bin/base
22
+ - lib/base.rb
23
+ - lib/base/compiler.rb
24
+ - lib/base/debugger.rb
25
+ - lib/base/stack.rb
26
+ - lib/base/vm.rb
27
+ homepage: https://github.com/mogest/base-lang
28
+ licenses:
29
+ - MIT
30
+ metadata: {}
31
+ post_install_message:
32
+ rdoc_options: []
33
+ require_paths:
34
+ - lib
35
+ required_ruby_version: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '0'
40
+ required_rubygems_version: !ruby/object:Gem::Requirement
41
+ requirements:
42
+ - - ">="
43
+ - !ruby/object:Gem::Version
44
+ version: '0'
45
+ requirements: []
46
+ rubygems_version: 3.1.6
47
+ signing_key:
48
+ specification_version: 4
49
+ summary: A simple assembly language, compiler and VM
50
+ test_files: []