base-lang 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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: []