ebsm 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 +7 -0
- data/Gemfile +17 -0
- data/ebsm.gemspec +30 -0
- data/exe/ebsm +25 -0
- data/lib/ebsm/dsl.rb +191 -0
- data/lib/ebsm/hex.rb +51 -0
- data/lib/ebsm/renderer.rb +139 -0
- data/lib/ebsm/version.rb +9 -0
- data/lib/ebsm.rb +105 -0
- metadata +71 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 6f67cacb0b8f96cb6a3d1772bba3e2b8fb49095e7cd7088127a4c14e080aa792
|
|
4
|
+
data.tar.gz: 4efb427fbed1f5ffba5b13cd4b448223d1bf1a41c6e46873e03c539296e7e0fa
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 672b49abb7e7081fa96ea0077a845a50d6745e792b5b5a4d4923593cd5a1c9c4f165742128b20d345b611b60d6608835ef3b57603d1f771c893e9fdc2238c878
|
|
7
|
+
data.tar.gz: 87aefd8f087c5d27941b64af730fd96083cc8b7f19cc779776ca39a2df4bd4f3c1ee89d6c52b971c6f7833fcd564c9c2a4be5b3a2a5b234f0c3d5e6f90f6aa14
|
data/Gemfile
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
source 'https://rubygems.org'
|
|
4
|
+
|
|
5
|
+
# Specify your gem's dependencies in ebsm.gemspec
|
|
6
|
+
gemspec
|
|
7
|
+
|
|
8
|
+
# Specify development dependencies here and not in the gemspec
|
|
9
|
+
gem 'markspec'
|
|
10
|
+
gem 'rake'
|
|
11
|
+
gem 'rspec'
|
|
12
|
+
gem 'rubocop'
|
|
13
|
+
gem 'rubocop-rake'
|
|
14
|
+
gem 'rubocop-rspec'
|
|
15
|
+
gem 'rubocop-yard'
|
|
16
|
+
gem 'yard'
|
|
17
|
+
gem 'yard-rspec'
|
data/ebsm.gemspec
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'lib/ebsm/version'
|
|
4
|
+
|
|
5
|
+
Gem::Specification.new do |spec|
|
|
6
|
+
spec.name = 'ebsm'
|
|
7
|
+
spec.version = Ebsm::VERSION
|
|
8
|
+
spec.authors = ['David Siaw']
|
|
9
|
+
spec.email = ['874280+davidsiaw@users.noreply.github.com']
|
|
10
|
+
|
|
11
|
+
spec.summary = 'Ebsm gem'
|
|
12
|
+
spec.description = 'Does not do anything'
|
|
13
|
+
spec.homepage = 'https://github.com/davidsiaw/ebsm'
|
|
14
|
+
spec.license = 'MIT'
|
|
15
|
+
spec.required_ruby_version = Gem::Requirement.new('>= 3.0')
|
|
16
|
+
|
|
17
|
+
spec.metadata['allowed_push_host'] = 'https://rubygems.org'
|
|
18
|
+
|
|
19
|
+
spec.metadata['homepage_uri'] = spec.homepage
|
|
20
|
+
spec.metadata['source_code_uri'] = 'https://github.com/davidsiaw/ebsm'
|
|
21
|
+
spec.metadata['changelog_uri'] = 'https://github.com/davidsiaw/ebsm'
|
|
22
|
+
spec.metadata['rubygems_mfa_required'] = 'true'
|
|
23
|
+
|
|
24
|
+
spec.files = Dir['{exe,data,lib}/**/*'] + %w[Gemfile ebsm.gemspec]
|
|
25
|
+
spec.bindir = 'exe'
|
|
26
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
|
27
|
+
spec.require_paths = ['lib']
|
|
28
|
+
|
|
29
|
+
spec.add_dependency 'bsm'
|
|
30
|
+
end
|
data/exe/ebsm
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require 'ebsm'
|
|
5
|
+
require 'bsm'
|
|
6
|
+
|
|
7
|
+
# When set, print the expanded bsm2 literate text (the output of
|
|
8
|
+
# Ebsm::Renderer#render) instead of compiling it to binary. Useful for
|
|
9
|
+
# debugging ebsm scripting: you see exactly what the templating produced
|
|
10
|
+
# before bsm2 turns it into bytes.
|
|
11
|
+
output_bsm2 = ARGV.delete('--bsm') || ARGV.delete('-b')
|
|
12
|
+
|
|
13
|
+
content = ARGF.read
|
|
14
|
+
|
|
15
|
+
begin
|
|
16
|
+
expanded = Ebsm::Renderer.new.render(content)
|
|
17
|
+
if output_bsm2
|
|
18
|
+
print expanded
|
|
19
|
+
else
|
|
20
|
+
print Ebsm::Renderer.new.bsm2_evaluate(content)
|
|
21
|
+
end
|
|
22
|
+
rescue Ebsm::Error => e
|
|
23
|
+
warn e.format
|
|
24
|
+
exit 1
|
|
25
|
+
end
|
data/lib/ebsm/dsl.rb
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'ebsm/hex'
|
|
4
|
+
|
|
5
|
+
module Ebsm
|
|
6
|
+
# The evaluation context for an ebsm template -- the object whose binding an
|
|
7
|
+
# ERB template is evaluated against. Any public method here is available
|
|
8
|
+
# inside a [expr] interpolation or a <% ... %> code block.
|
|
9
|
+
#
|
|
10
|
+
# Helper methods are expected to return strings that the bsm2 lexer can
|
|
11
|
+
# consume on a '; ' data line -- typically space-separated hex byte pairs, so
|
|
12
|
+
# that e.g. [le32 0x12345678] expands to "; 78 56 34 12".
|
|
13
|
+
class Dsl
|
|
14
|
+
def initialize
|
|
15
|
+
@emitted = []
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Source lines of the template being rendered, set by the renderer before
|
|
19
|
+
# evaluation so #eval_at can report the text of an offending line.
|
|
20
|
+
attr_writer :lines
|
|
21
|
+
|
|
22
|
+
# The [source_line, expanded_text] pairs emitted by #line, in order. The
|
|
23
|
+
# renderer reads this to feed each ; line to bsm2 individually so a bsm2
|
|
24
|
+
# error can be reported against the original source line.
|
|
25
|
+
attr_reader :emitted
|
|
26
|
+
|
|
27
|
+
# --- Byte helpers -------------------------------------------------------
|
|
28
|
+
# Each helper emits +value+ as space-separated hex byte pairs, raising
|
|
29
|
+
# Ebsm::Error if +value+ is not an Integer or does not fit the width.
|
|
30
|
+
|
|
31
|
+
# Emit +value+ as a single hex byte pair (0..0xff).
|
|
32
|
+
# @param value [Integer] the byte value, 0..255.
|
|
33
|
+
# @return [String] two lowercase hex digits.
|
|
34
|
+
def byte(value)
|
|
35
|
+
hex.pack_hex(value, 'C', 0xff)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Emit +count+ copies of +value+ as space-separated lowercase hex pairs.
|
|
39
|
+
# Handy for padding/filling, e.g. [repeat 0x00, 16] pads 16 zero bytes.
|
|
40
|
+
# +value+ must be an Integer 0..255; +count+ must be a non-negative Integer.
|
|
41
|
+
# @param value [Integer] the byte to repeat, 0..255.
|
|
42
|
+
# @param count [Integer] non-negative number of copies.
|
|
43
|
+
# @return [String] space-separated hex byte pairs.
|
|
44
|
+
def repeat(value, count)
|
|
45
|
+
unless count.is_a?(Integer) && !count.negative?
|
|
46
|
+
raise Ebsm::Error,
|
|
47
|
+
"#{count.inspect} is not a non-negative Integer"
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
hex_val = hex.hex_pair(value)
|
|
51
|
+
Array.new(count, hex_val).join(' ')
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Emit every byte of +value+ as space-separated lowercase hex pairs.
|
|
55
|
+
# Accepts a String (its bytes) or an Enumerable of Integers (each 0..255).
|
|
56
|
+
# Useful for dumping a whole string or byte array in one call, e.g.
|
|
57
|
+
# [bytes chunk] where chunk is a binary String, or [bytes arr] where arr
|
|
58
|
+
# is an Array of Integers assigned on a # line.
|
|
59
|
+
# @param value [String, Enumerable<Integer>] the bytes to emit.
|
|
60
|
+
# @return [String] space-separated hex byte pairs.
|
|
61
|
+
def bytes(value)
|
|
62
|
+
ints = value.is_a?(String) ? value.bytes : value.to_a
|
|
63
|
+
ints.map { |b| hex.hex_pair(b) }.join(' ')
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Emit +value+ as 2 little-endian hex byte pairs (0..0xffff).
|
|
67
|
+
# @param value [Integer] 0..0xffff.
|
|
68
|
+
# @return [String] two space-separated hex byte pairs.
|
|
69
|
+
def le16(value)
|
|
70
|
+
hex.pack_hex(value, 'v', 0xffff)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Emit +value+ as 2 big-endian hex byte pairs (0..0xffff).
|
|
74
|
+
# @param value [Integer] 0..0xffff.
|
|
75
|
+
# @return [String] two space-separated hex byte pairs.
|
|
76
|
+
def be16(value)
|
|
77
|
+
hex.pack_hex(value, 'n', 0xffff)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Emit +value+ as 4 little-endian hex byte pairs (0..0xffffffff).
|
|
81
|
+
# @param value [Integer] 0..0xffffffff.
|
|
82
|
+
# @return [String] four space-separated hex byte pairs.
|
|
83
|
+
def le32(value)
|
|
84
|
+
hex.pack_hex(value, 'V', 0xffffffff)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Emit +value+ as 4 big-endian hex byte pairs (0..0xffffffff).
|
|
88
|
+
# @param value [Integer] 0..0xffffffff.
|
|
89
|
+
# @return [String] four space-separated hex byte pairs.
|
|
90
|
+
def be32(value)
|
|
91
|
+
hex.pack_hex(value, 'N', 0xffffffff)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Emit +value+ as 8 little-endian hex byte pairs.
|
|
95
|
+
# @param value [Integer] 0..0xffffffffffffffff.
|
|
96
|
+
# @return [String] eight space-separated hex byte pairs.
|
|
97
|
+
def le64(value)
|
|
98
|
+
hex.pack_hex(value, 'Q<', 0xffffffffffffffff)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Emit +value+ as 8 big-endian hex byte pairs.
|
|
102
|
+
# @param value [Integer] 0..0xffffffffffffffff.
|
|
103
|
+
# @return [String] eight space-separated hex byte pairs.
|
|
104
|
+
def be64(value)
|
|
105
|
+
hex.pack_hex(value, 'Q>', 0xffffffffffffffff)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Emit +value+ as a 32-bit (single-precision) little-endian IEEE 754 float.
|
|
109
|
+
# @param value [Float] the float to encode.
|
|
110
|
+
# @return [String] four space-separated hex byte pairs.
|
|
111
|
+
def lef32(value)
|
|
112
|
+
hex.pack_float(value, 'e')
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Emit +value+ as a 32-bit (single-precision) big-endian IEEE 754 float.
|
|
116
|
+
# @param value [Float] the float to encode.
|
|
117
|
+
# @return [String] four space-separated hex byte pairs.
|
|
118
|
+
def bef32(value)
|
|
119
|
+
hex.pack_float(value, 'g')
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Emit +value+ as a 64-bit (double-precision) little-endian IEEE 754 float.
|
|
123
|
+
# @param value [Float] the float to encode.
|
|
124
|
+
# @return [String] eight space-separated hex byte pairs.
|
|
125
|
+
def lef64(value)
|
|
126
|
+
hex.pack_float(value, 'E')
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Emit +value+ as a 64-bit (double-precision) big-endian IEEE 754 float.
|
|
130
|
+
# @param value [Float] the float to encode.
|
|
131
|
+
# @return [String] eight space-separated hex byte pairs.
|
|
132
|
+
def bef64(value)
|
|
133
|
+
hex.pack_float(value, 'G')
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Evaluate a [expr] interpolation, annotating any error with the source
|
|
137
|
+
# position of the bracket so the CLI can draw an arrow/tilde. Ebsm::Error
|
|
138
|
+
# (from helpers) is passed through with position attached; any other
|
|
139
|
+
# StandardError raised by the expression (NoMethodError, ArgumentError,
|
|
140
|
+
# etc.) is wrapped in an Ebsm::Error so it gets the same diagnostic.
|
|
141
|
+
# @param line_number [Integer] 1-indexed source line of the bracket.
|
|
142
|
+
# @param column [Integer] 1-indexed column of the opening bracket.
|
|
143
|
+
# @param char_length [Integer] length of the [...] bracket span.
|
|
144
|
+
# @return [Object] the result of the block.
|
|
145
|
+
# @yieldreturn [Object] the result of the [expr] expression.
|
|
146
|
+
def eval_at(line_number, column, char_length)
|
|
147
|
+
yield
|
|
148
|
+
rescue Ebsm::Error => e
|
|
149
|
+
raise annotate(e, line_number, column, char_length)
|
|
150
|
+
rescue StandardError => e
|
|
151
|
+
raise annotate(Ebsm::Error.new("#{e.message} (#{e.class})"),
|
|
152
|
+
line_number, column, char_length)
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# The binding the renderer evaluates the ERB template against. Methods set
|
|
156
|
+
# or called in <% %> / <%= %> resolve on this object.
|
|
157
|
+
# @return [Binding] this DSL's binding.
|
|
158
|
+
def erb_binding
|
|
159
|
+
binding
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
private
|
|
163
|
+
|
|
164
|
+
# Attach source position to +error+ and return it for re-raising.
|
|
165
|
+
def annotate(error, line_number, column, char_length)
|
|
166
|
+
error.line_number = line_number
|
|
167
|
+
error.column = column
|
|
168
|
+
error.char_length = char_length
|
|
169
|
+
error.line_text = @lines[line_number - 1]
|
|
170
|
+
error.source_lines = @lines
|
|
171
|
+
error
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# Record that a ; data line (the fully-expanded +text+) came from
|
|
175
|
+
# +source_line+, and return +text+ so ERB still emits it. The renderer
|
|
176
|
+
# reads #emitted to bsm2_evaluate each ; line individually.
|
|
177
|
+
# @param source_line [Integer] 1-indexed source line of the ; line.
|
|
178
|
+
# @return [String] the expanded ; line text.
|
|
179
|
+
# @yieldreturn [String] the expanded ; line text.
|
|
180
|
+
def line(source_line)
|
|
181
|
+
text = yield
|
|
182
|
+
@emitted << [source_line, text]
|
|
183
|
+
text
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# @return [Ebsm::Hex] a fresh Hex formatter.
|
|
187
|
+
def hex
|
|
188
|
+
Hex.new
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
end
|
data/lib/ebsm/hex.rb
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ebsm
|
|
4
|
+
# Formats numbers as the hex byte pairs the bsm2 lexer consumes on a '; '
|
|
5
|
+
# data line. Owns the responsibility of validating a number (Integer in range,
|
|
6
|
+
# or Float) and rendering it as lowercase hex pairs, so that Ebsm::Renderer can
|
|
7
|
+
# stay focused on templating.
|
|
8
|
+
class Hex
|
|
9
|
+
# Convert +value+ to a byte sequence via Array#pack using +directive+ and
|
|
10
|
+
# render it as space-separated lowercase hex pairs. Raises Ebsm::Error if
|
|
11
|
+
# +value+ is not an Integer or lies outside 0..+max+.
|
|
12
|
+
# @param value [Integer] the integer to encode.
|
|
13
|
+
# @param directive [String] an Array#pack directive (e.g. 'V', 'N', 'C').
|
|
14
|
+
# @param max [Integer] the inclusive upper bound for +value+.
|
|
15
|
+
# @return [String] space-separated lowercase hex byte pairs.
|
|
16
|
+
def pack_hex(value, directive, max)
|
|
17
|
+
raise Ebsm::Error, "#{value.inspect} is not an Integer" unless value.is_a?(Integer)
|
|
18
|
+
|
|
19
|
+
if value.negative? || value > max
|
|
20
|
+
raise Ebsm::Error, format('0x%<v>s out of range 0..0x%<m>x',
|
|
21
|
+
v: value.to_s(16), m: max)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
[value].pack(directive).bytes.map { |b| format('%02x', b) }.join(' ')
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Render a single Integer byte (0..255) as a two-digit lowercase hex pair,
|
|
28
|
+
# raising Ebsm::Error if it is not an Integer or is out of range.
|
|
29
|
+
# @param byte [Integer] the byte value, 0..255.
|
|
30
|
+
# @return [String] two lowercase hex digits.
|
|
31
|
+
def hex_pair(byte)
|
|
32
|
+
raise Ebsm::Error, "#{byte.inspect} is not an Integer" unless byte.is_a?(Integer)
|
|
33
|
+
|
|
34
|
+
raise Ebsm::Error, format('0x%<v>s out of range 0..0xff', v: byte.to_s(16)) if byte.negative? || byte > 0xff
|
|
35
|
+
|
|
36
|
+
format('%02x', byte)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Convert a Float +value+ to its IEEE 754 binary representation via
|
|
40
|
+
# Array#pack using +directive+ and render it as space-separated lowercase
|
|
41
|
+
# hex pairs. Raises Ebsm::Error if +value+ is not a Float.
|
|
42
|
+
# @param value [Float] the float to encode.
|
|
43
|
+
# @param directive [String] an Array#pack float directive ('e','g','E','G').
|
|
44
|
+
# @return [String] space-separated lowercase hex byte pairs.
|
|
45
|
+
def pack_float(value, directive)
|
|
46
|
+
raise Ebsm::Error, "#{value.inspect} is not a Float" unless value.is_a?(Float)
|
|
47
|
+
|
|
48
|
+
[value].pack(directive).bytes.map { |b| format('%02x', b) }.join(' ')
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'erb'
|
|
4
|
+
require 'ebsm/dsl'
|
|
5
|
+
|
|
6
|
+
module Ebsm
|
|
7
|
+
# Renders ebsm source to bsm2 literate text. Owns the source -> ERB template
|
|
8
|
+
# translation; the byte helpers users call inside [...] live on Ebsm::Dsl,
|
|
9
|
+
# whose binding the template is evaluated against.
|
|
10
|
+
class Renderer
|
|
11
|
+
# Translate ebsm source into an ERB program, evaluate it against a Dsl
|
|
12
|
+
# instance, and return the resulting bsm2 literate text. Each [expr] on a
|
|
13
|
+
# '; ' data line is wrapped so that an Ebsm::Error raised while evaluating
|
|
14
|
+
# it is annotated with its source line, column, and length for diagnostics.
|
|
15
|
+
# @param content [String] the ebsm source.
|
|
16
|
+
# @return [String] the expanded bsm2 literate text.
|
|
17
|
+
def render(content)
|
|
18
|
+
dsl = evaluate(content)
|
|
19
|
+
ERB.new(build_template(content), trim_mode: '-').result(dsl.erb_binding)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Evaluate the template, then feed each emitted ; line to bsm2 individually
|
|
23
|
+
# so a bsm2 lexer error is reported against the original source line (not
|
|
24
|
+
# the expanded text's line number). Returns the concatenated binary bytes.
|
|
25
|
+
# @param content [String] the ebsm source.
|
|
26
|
+
# @return [String] the concatenated binary bytes.
|
|
27
|
+
def bsm2_evaluate(content)
|
|
28
|
+
dsl = evaluate(content)
|
|
29
|
+
ERB.new(build_template(content), trim_mode: '-').result(dsl.erb_binding)
|
|
30
|
+
generator = Bsm::Generator.new
|
|
31
|
+
source_lines = content.lines.map(&:chomp)
|
|
32
|
+
dsl.emitted.each_with_object(+'') do |(source_line, text), output|
|
|
33
|
+
output << generate_line(generator, source_line, text, source_lines)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Compile a single expanded ; line, re-raising a bsm2 error as an
|
|
38
|
+
# Ebsm::Error annotated with the original +source_line+ and source context.
|
|
39
|
+
# @param generator [Bsm::Generator] the bsm2 generator.
|
|
40
|
+
# @param source_line [Integer] 1-indexed source line of the ; line.
|
|
41
|
+
# @param text [String] the expanded ; line text.
|
|
42
|
+
# @param source_lines [Array<String>] all source lines (chomped).
|
|
43
|
+
# @return [String] the compiled binary bytes for this line.
|
|
44
|
+
def generate_line(generator, source_line, text, source_lines)
|
|
45
|
+
generator.generate("#{text}\n")
|
|
46
|
+
rescue Bsm::InvalidInput => e
|
|
47
|
+
raise bsm_error(e, source_line, text, source_lines)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Translate ebsm source into an ERB program. A '# ' line becomes an ERB
|
|
51
|
+
# code block, a '; ' line is wrapped in a #line call (so the renderer can
|
|
52
|
+
# track which source line each expanded ; line came from), anything else
|
|
53
|
+
# passes through verbatim (bsm2 ignores it).
|
|
54
|
+
# @param content [String] the ebsm source.
|
|
55
|
+
# @return [String] the ERB program.
|
|
56
|
+
def build_template(content)
|
|
57
|
+
content.lines.each_with_index.map do |line, index|
|
|
58
|
+
stripped = line.chomp
|
|
59
|
+
if stripped.start_with?('#')
|
|
60
|
+
"<% #{stripped[1..]} -%>\n"
|
|
61
|
+
elsif stripped.start_with?(';')
|
|
62
|
+
"<%= line(#{index + 1}) { #{expand_brackets(stripped, index + 1)} } %>\n"
|
|
63
|
+
else
|
|
64
|
+
line
|
|
65
|
+
end
|
|
66
|
+
end.join
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Build a Ruby double-quoted string expression that evaluates to the
|
|
70
|
+
# expanded ; line, with each [expr] replaced by an `eval_at(N, col, len)`
|
|
71
|
+
# interpolation. Literal text is escaped for a double-quoted Ruby string.
|
|
72
|
+
# An unclosed [ is left literal.
|
|
73
|
+
# @param stripped [String] the chomped ; source line.
|
|
74
|
+
# @param line_number [Integer] 1-indexed source line number.
|
|
75
|
+
# @return [String] a Ruby double-quoted string expression.
|
|
76
|
+
def expand_brackets(stripped, line_number)
|
|
77
|
+
expr = +'"'
|
|
78
|
+
index = 0
|
|
79
|
+
expr, index = expand_segment(expr, stripped, index, line_number) while index < stripped.length
|
|
80
|
+
"#{expr}\""
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Expand one segment of a ; line starting at +index+, returning the updated
|
|
84
|
+
# [expr, next_index]. A [expr] bracket becomes an eval_at interpolation; a
|
|
85
|
+
# literal run is escaped; an unclosed [ ends the walk.
|
|
86
|
+
# @param expr [String] the accumulator expression so far.
|
|
87
|
+
# @param stripped [String] the chomped ; source line.
|
|
88
|
+
# @param index [Integer] the current position in +stripped+.
|
|
89
|
+
# @param line_number [Integer] 1-indexed source line number.
|
|
90
|
+
# @return [Array(String, Integer)] the updated [expr, next_index].
|
|
91
|
+
def expand_segment(expr, stripped, index, line_number)
|
|
92
|
+
if stripped[index] == '['
|
|
93
|
+
close = stripped.index(']', index)
|
|
94
|
+
return [expr, stripped.length] unless close
|
|
95
|
+
|
|
96
|
+
[expr + bracket_expr(stripped, index, close, line_number), close + 1]
|
|
97
|
+
else
|
|
98
|
+
chunk_end = stripped.index('[', index) || stripped.length
|
|
99
|
+
[expr + escape_dq(stripped[index...chunk_end]), chunk_end]
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
private
|
|
104
|
+
|
|
105
|
+
def evaluate(content)
|
|
106
|
+
dsl = Dsl.new
|
|
107
|
+
dsl.lines = content.lines.map(&:chomp)
|
|
108
|
+
dsl
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# The `eval_at(...)` interpolation expression for a [expr] bracket
|
|
112
|
+
# spanning +open+..+close+ in +stripped+.
|
|
113
|
+
def bracket_expr(stripped, open, close, line_number)
|
|
114
|
+
inner = stripped[(open + 1)...close]
|
|
115
|
+
column = open + 1
|
|
116
|
+
length = close - open + 1
|
|
117
|
+
"\#{eval_at(#{line_number}, #{column}, #{length}) { #{inner} }}"
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Escape a literal chunk for a double-quoted Ruby string: backslash,
|
|
121
|
+
# double-quote, and # when it would start an interpolation.
|
|
122
|
+
def escape_dq(chunk)
|
|
123
|
+
chunk.gsub(/[\\"]|\#(?=[{@$])/) { |m| "\\#{m}" }
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Build an Ebsm::Error from a bsm2 error, pointing at +source_line+ with
|
|
127
|
+
# +expanded_text+ as the arrow target and +source_lines+ for context.
|
|
128
|
+
def bsm_error(error, source_line, expanded_text, source_lines)
|
|
129
|
+
result = Ebsm::Error.new(error.message)
|
|
130
|
+
result.line_number = source_line
|
|
131
|
+
result.column = error.column
|
|
132
|
+
result.char_length = error.length
|
|
133
|
+
result.line_text = error.line_text
|
|
134
|
+
result.source_lines = source_lines
|
|
135
|
+
result.expanded_line = expanded_text unless expanded_text == source_lines[source_line - 1]
|
|
136
|
+
result
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
data/lib/ebsm/version.rb
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Ebsm is a literate binary generator: a templating layer over bsm2 that lets
|
|
4
|
+
# you mix prose, Ruby scripting (# lines), and byte data (; lines) in one
|
|
5
|
+
# source file. See Ebsm::Renderer and Ebsm::Dsl.
|
|
6
|
+
module Ebsm
|
|
7
|
+
# The ebsm gem version.
|
|
8
|
+
VERSION = '0.1.0'
|
|
9
|
+
end
|
data/lib/ebsm.rb
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'ebsm/version'
|
|
4
|
+
require 'ebsm/hex'
|
|
5
|
+
require 'ebsm/dsl'
|
|
6
|
+
require 'ebsm/renderer'
|
|
7
|
+
require 'bsm'
|
|
8
|
+
|
|
9
|
+
# Ebsm is a literate binary generator: a templating layer over bsm2 that lets
|
|
10
|
+
# you mix prose, Ruby scripting (# lines), and byte data (; lines) in one
|
|
11
|
+
# source file. See Ebsm::Renderer and Ebsm::Dsl.
|
|
12
|
+
module Ebsm
|
|
13
|
+
# An error raised while evaluating ebsm source. Carries the source position
|
|
14
|
+
# of the offending line plus (for bsm2-stage errors) the expanded bsm2 line
|
|
15
|
+
# the arrow actually points at, so the CLI can show both source and expanded
|
|
16
|
+
# context like bsm2 does.
|
|
17
|
+
class Error < StandardError
|
|
18
|
+
# Source position of the offending line/bracket, filled in by the renderer.
|
|
19
|
+
attr_accessor :line_number, :column, :char_length, :line_text
|
|
20
|
+
# All source lines (chomped), for printing context lines above the error.
|
|
21
|
+
attr_accessor :source_lines
|
|
22
|
+
# The expanded bsm2 line the arrow points at. nil for ebsm-stage helper
|
|
23
|
+
# errors (where the source line is the arrow target).
|
|
24
|
+
attr_accessor :expanded_line
|
|
25
|
+
|
|
26
|
+
# Format this error as a diagnostic with source context (2 lines above)
|
|
27
|
+
# and, for bsm2-stage errors, the expanded line the arrow points at:
|
|
28
|
+
#
|
|
29
|
+
# EBSM: bsm2 error at source line 2 (expanded column 17)
|
|
30
|
+
# ---
|
|
31
|
+
# 1 | # x = 0x12345678
|
|
32
|
+
# > 2 | ; [le32 x] 45 asd
|
|
33
|
+
# expanded:
|
|
34
|
+
# ; 78 56 34 12 45 asd
|
|
35
|
+
# ^ hex byte needs two hex digits
|
|
36
|
+
# @return [String] the formatted diagnostic.
|
|
37
|
+
def format
|
|
38
|
+
body = "#{header}\n---\n"
|
|
39
|
+
body << context_lines
|
|
40
|
+
body << expanded_section if expanded_line
|
|
41
|
+
body << arrow_line
|
|
42
|
+
body
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# The header line, noting whether the column is expanded (bsm2-stage) or
|
|
46
|
+
# source (ebsm-stage).
|
|
47
|
+
# @return [String] the header line.
|
|
48
|
+
def header
|
|
49
|
+
if expanded_line
|
|
50
|
+
"EBSM: bsm2 error at source line #{line_number} (expanded column #{column})"
|
|
51
|
+
else
|
|
52
|
+
"EBSM: error at source line #{line_number} (column #{column})"
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# The "expanded:" label and the expanded bsm2 line -- shown only for
|
|
57
|
+
# bsm2-stage errors where the arrow target differs from source.
|
|
58
|
+
# @return [String] the expanded section.
|
|
59
|
+
def expanded_section
|
|
60
|
+
pad = ' ' * prefix_width
|
|
61
|
+
"#{pad}expanded:\n#{pad}#{expanded_line}\n"
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# The arrow (with leading spaces to align under the displayed line) and the
|
|
65
|
+
# error message.
|
|
66
|
+
# @return [String] the arrow line with the message.
|
|
67
|
+
def arrow_line
|
|
68
|
+
indent = ' ' * (prefix_width + (column - 1))
|
|
69
|
+
"#{indent}#{arrow} #{message}\n"
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# The numbered source lines from 2 above the error up to the error line,
|
|
73
|
+
# with `> ` marking the error line. Line numbers are right-padded to the
|
|
74
|
+
# error line's digit width so the `|` pipes align. When the error is near
|
|
75
|
+
# the top of the file and there are fewer than 2 source lines above, a
|
|
76
|
+
# single `beginning of file` marker fills the gap.
|
|
77
|
+
# @return [String] the numbered context lines.
|
|
78
|
+
def context_lines
|
|
79
|
+
width = line_number.to_s.length
|
|
80
|
+
first = [line_number - 2, 1].max
|
|
81
|
+
bof = line_number < 3 ? [bof_line(width)] : []
|
|
82
|
+
real = (first..line_number).map { |n| context_line(n, width) }
|
|
83
|
+
(bof + real).join
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
private
|
|
87
|
+
|
|
88
|
+
def prefix_width
|
|
89
|
+
"> #{line_number} | ".length
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def arrow
|
|
93
|
+
"^#{'~' * (char_length - 1)}"
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def context_line(number, width)
|
|
97
|
+
prefix = number == line_number ? '> ' : ' '
|
|
98
|
+
"#{prefix}#{number.to_s.rjust(width)} | #{source_lines[number - 1]}\n"
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def bof_line(width)
|
|
102
|
+
" #{' ' * width} | *beginning of file*\n"
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: ebsm
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- David Siaw
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: exe
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-07-22 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: bsm
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - ">="
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '0'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - ">="
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '0'
|
|
27
|
+
description: Does not do anything
|
|
28
|
+
email:
|
|
29
|
+
- 874280+davidsiaw@users.noreply.github.com
|
|
30
|
+
executables:
|
|
31
|
+
- ebsm
|
|
32
|
+
extensions: []
|
|
33
|
+
extra_rdoc_files: []
|
|
34
|
+
files:
|
|
35
|
+
- Gemfile
|
|
36
|
+
- ebsm.gemspec
|
|
37
|
+
- exe/ebsm
|
|
38
|
+
- lib/ebsm.rb
|
|
39
|
+
- lib/ebsm/dsl.rb
|
|
40
|
+
- lib/ebsm/hex.rb
|
|
41
|
+
- lib/ebsm/renderer.rb
|
|
42
|
+
- lib/ebsm/version.rb
|
|
43
|
+
homepage: https://github.com/davidsiaw/ebsm
|
|
44
|
+
licenses:
|
|
45
|
+
- MIT
|
|
46
|
+
metadata:
|
|
47
|
+
allowed_push_host: https://rubygems.org
|
|
48
|
+
homepage_uri: https://github.com/davidsiaw/ebsm
|
|
49
|
+
source_code_uri: https://github.com/davidsiaw/ebsm
|
|
50
|
+
changelog_uri: https://github.com/davidsiaw/ebsm
|
|
51
|
+
rubygems_mfa_required: 'true'
|
|
52
|
+
post_install_message:
|
|
53
|
+
rdoc_options: []
|
|
54
|
+
require_paths:
|
|
55
|
+
- lib
|
|
56
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - ">="
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '3.0'
|
|
61
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
62
|
+
requirements:
|
|
63
|
+
- - ">="
|
|
64
|
+
- !ruby/object:Gem::Version
|
|
65
|
+
version: '0'
|
|
66
|
+
requirements: []
|
|
67
|
+
rubygems_version: 3.5.22
|
|
68
|
+
signing_key:
|
|
69
|
+
specification_version: 4
|
|
70
|
+
summary: Ebsm gem
|
|
71
|
+
test_files: []
|