Skip to content

Commit 39f8aa4

Browse files
Instructions ISA manual Appendix (#490)
* Adding phase 1 index Signed-off-by: Afonso Oliveira <Afonso.Oliveira@synopsys.com> * Adding phase 1 index to ./do Signed-off-by: Afonso Oliveira <Afonso.Oliveira@synopsys.com> * Adding phase 1 index: instructions_index object Signed-off-by: Afonso Oliveira <Afonso.Oliveira@synopsys.com> * Generate appendix through UDB. * Changes names and paths file structure Signed-off-by: Afonso Oliveira <Afonso.Oliveira@synopsys.com> * Update template to ensure correct output. Signed-off-by: Afonso Oliveira <Afonso.Oliveira@synopsys.com> * Remove outdated file. Signed-off-by: Afonso Oliveira <Afonso.Oliveira@synopsys.com> * Add appendix generation to regression test. Signed-off-by: Afonso Oliveira <Afonso.Oliveira@synopsys.com> * Set up hexa numbers to input wavedrom diagram. Makes the 0s and 1s be in each own cell Signed-off-by: Afonso Oliveira <Afonso.Oliveira@synopsys.com> --------- Signed-off-by: Afonso Oliveira <Afonso.Oliveira@synopsys.com> Co-authored-by: Derek Hower <134728312+dhower-qc@users.noreply.github.com>
1 parent 815394f commit 39f8aa4

File tree

5 files changed

+289
-0
lines changed

5 files changed

+289
-0
lines changed

.github/workflows/regress.yml

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,34 @@ jobs:
7676
run: ./bin/build_container
7777
- name: Generate HTML ISA manual
7878
run: ./do gen:html_manual
79+
regress-gen-instruction-appendix:
80+
runs-on: ubuntu-latest
81+
env:
82+
SINGULARITY: 1
83+
steps:
84+
- name: Clone Github Repo Action
85+
uses: actions/checkout@v4
86+
- name: Setup apptainer
87+
uses: eWaterCycle/setup-apptainer@v2.0.0
88+
- name: Get container from cache
89+
id: cache-sif
90+
uses: actions/cache@v4
91+
with:
92+
path: .singularity/image.sif
93+
key: ${{ hashFiles('container.def', 'bin/.container-tag') }}
94+
- name: Get gems and node files from cache
95+
id: cache-bundle-npm
96+
uses: actions/cache@v4
97+
with:
98+
path: |
99+
.home/.gems
100+
node_modules
101+
key: ${{ hashFiles('Gemfile.lock') }}-${{ hashFiles('package-lock.json') }}
102+
- if: ${{ steps.cache-sif.outputs.cache-hit != 'true' }}
103+
name: Build container
104+
run: ./bin/build_container
105+
- name: Generate instruction appendix
106+
run: ./do gen:instruction_appendix
79107
regress-cfg-manual:
80108
runs-on: ubuntu-latest
81109
env:

.pre-commit-config.yaml

100644100755
File mode changed.
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
# frozen_string_literal: true
2+
3+
require_relative "../../lib/arch_obj_models/instructions_appendix.rb"
4+
5+
# Define the instructions manual generation directory constant.
6+
INST_MANUAL_GEN_DIR = $root / "gen" / "instructions_appendix"
7+
8+
# Define the path to the merged instructions output.
9+
MERGED_INSTRUCTIONS_FILE = INST_MANUAL_GEN_DIR / "all_instructions.adoc"
10+
11+
# Define the path to the ERB template that renders the merged instructions.
12+
TEMPLATE_FILE = $root / "backends" / "instructions_appendix" / "templates" / "instructions.adoc.erb"
13+
14+
# Declare a file task for the template so Rake knows it exists.
15+
file TEMPLATE_FILE.to_s do
16+
# Nothing to do—this file is assumed to be up-to-date.
17+
end
18+
19+
# File task that generates the merged instructions adoc.
20+
file MERGED_INSTRUCTIONS_FILE.to_s => [__FILE__, TEMPLATE_FILE.to_s] do |t|
21+
cfg_arch = cfg_arch_for("_")
22+
# Use the InstructionIndex helper to aggregate instructions from the entire architecture.
23+
instruction_index = InstructionIndex.new(cfg_arch)
24+
instructions = instruction_index.instructions
25+
26+
# Load and process the template (which renders both an index and details).
27+
erb = ERB.new(File.read(TEMPLATE_FILE), trim_mode: "-")
28+
erb.filename = TEMPLATE_FILE.to_s
29+
30+
FileUtils.mkdir_p(File.dirname(t.name))
31+
File.write(
32+
t.name,
33+
AntoraUtils.resolve_links(cfg_arch.find_replace_links(erb.result(binding)))
34+
)
35+
end
36+
37+
# Define the path to the output PDF file.
38+
MERGED_INSTRUCTIONS_PDF = INST_MANUAL_GEN_DIR / "instructions_appendix.pdf"
39+
40+
# File task to generate the PDF from the merged adoc.
41+
file MERGED_INSTRUCTIONS_PDF.to_s => [MERGED_INSTRUCTIONS_FILE.to_s] do |t|
42+
sh [
43+
"asciidoctor-pdf",
44+
"-a toc",
45+
"-a pdf-theme=#{ENV['THEME'] || "#{$root}/ext/docs-resources/themes/riscv-pdf.yml"}",
46+
"-a pdf-fontsdir=#{$root}/ext/docs-resources/fonts",
47+
"-a imagesdir=#{$root}/ext/docs-resources/images",
48+
"-r asciidoctor-diagram",
49+
"-o #{t.name}",
50+
MERGED_INSTRUCTIONS_FILE.to_s
51+
].join(" ")
52+
53+
puts "SUCCESS: PDF generated at #{t.name}"
54+
end
55+
56+
namespace :gen do
57+
desc "Generate instruction appendix (merged instructions adoc and PDF)"
58+
task :instruction_appendix do
59+
# Generate the merged instructions adoc.
60+
Rake::Task[MERGED_INSTRUCTIONS_FILE.to_s].invoke
61+
# Then generate the PDF.
62+
Rake::Task[MERGED_INSTRUCTIONS_PDF.to_s].invoke
63+
puts "SUCCESS: Instruction appendix generated at '#{MERGED_INSTRUCTIONS_FILE}' and PDF at '#{MERGED_INSTRUCTIONS_PDF}'"
64+
end
65+
end
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
<%
2+
# Helper to substitute problematic entity strings with proper Unicode characters.
3+
def fix_entities(text)
4+
text.to_s.gsub("&ne;", "≠")
5+
.gsub("&pm;", "±")
6+
.gsub("-&infin;", "−∞")
7+
.gsub("+&infin;", "+∞")
8+
end
9+
10+
# Custom JSON converter for wavedrom that handles hexadecimal literals
11+
def json_dump_with_hex_literals(data)
12+
# First convert to standard JSON
13+
json_string = JSON.dump(data)
14+
15+
# Replace string hex values with actual hex literals
16+
json_string.gsub(/"0x([0-9a-fA-F]+)"/) do |match|
17+
# Remove the quotes, leaving just the hex literal
18+
"0x#{$1}"
19+
end.gsub(/"name":/, '"name": ') # Add space after colon for name field
20+
end
21+
22+
# Helper to process wavedrom data
23+
def process_wavedrom(json_data)
24+
result = json_data.dup
25+
26+
# Process reg array if it exists
27+
if result["reg"].is_a?(Array)
28+
result["reg"].each do |item|
29+
# For fields that are likely opcodes or immediates (type 2)
30+
if item["type"] == 2
31+
# Convert to number first (if it's a string)
32+
if item["name"].is_a?(String)
33+
if item["name"].start_with?("0x")
34+
# Already hexadecimal
35+
numeric_value = item["name"].to_i(16)
36+
elsif item["name"] =~ /^[01]+$/
37+
# Binary string without prefix
38+
numeric_value = item["name"].to_i(2)
39+
elsif item["name"] =~ /^\d+$/
40+
# Decimal
41+
numeric_value = item["name"].to_i
42+
else
43+
# Not a number, leave it alone
44+
next
45+
end
46+
else
47+
# Already a number
48+
numeric_value = item["name"]
49+
end
50+
51+
# Convert to hexadecimal string
52+
hex_str = numeric_value.to_s(16).downcase
53+
54+
# Set the name to a specially formatted string that will be converted
55+
# to a hex literal in our custom JSON converter
56+
item["name"] = "0x" + hex_str
57+
end
58+
59+
# Ensure bits is a number
60+
if item["bits"].is_a?(String) && item["bits"] =~ /^\d+$/
61+
item["bits"] = item["bits"].to_i
62+
end
63+
end
64+
end
65+
66+
result
67+
end
68+
%>
69+
= Instruction Appendix
70+
:doctype: book
71+
:wavedrom: <%= $root %>/node_modules/.bin/wavedrom-cli
72+
// Now the document header is complete and the wavedrom attribute is active.
73+
74+
<% instructions.sort_by(&:name).each do |inst| %>
75+
<%= anchor_for_inst(inst.name) %>
76+
== <%= inst.name %>
77+
78+
Synopsis::
79+
<%= inst.long_name %>
80+
81+
Encoding::
82+
<%- if inst.multi_encoding? -%>
83+
[NOTE]
84+
This instruction has different encodings in RV32 and RV64
85+
86+
RV32::
87+
[wavedrom, ,svg,subs='attributes',width="100%"]
88+
....
89+
<%= fix_entities(json_dump_with_hex_literals(process_wavedrom(inst.wavedrom_desc(32)))) %>
90+
....
91+
92+
RV64::
93+
[wavedrom, ,svg,subs='attributes',width="100%"]
94+
....
95+
<%= fix_entities(json_dump_with_hex_literals(process_wavedrom(inst.wavedrom_desc(64)))) %>
96+
....
97+
<%- else -%>
98+
[wavedrom, ,svg,subs='attributes',width="100%"]
99+
....
100+
<%= fix_entities(json_dump_with_hex_literals(process_wavedrom(inst.wavedrom_desc(inst.base.nil? ? 64 : inst.base)))) %>
101+
....
102+
<%- end -%>
103+
104+
Description::
105+
<%= fix_entities(inst.description) %>
106+
107+
Decode Variables::
108+
<%- if inst.multi_encoding? ? (inst.decode_variables(32).empty? && inst.decode_variables(64).empty?) : inst.decode_variables(inst.base.nil? ? 64 : inst.base).empty? -%>
109+
<%= inst.name %> has no decode variables.
110+
<%- else -%>
111+
<%- if inst.multi_encoding? -%>
112+
<%- unless inst.decode_variables(32).empty? -%>
113+
*RV32:*
114+
115+
[width="100%", cols="1,2", options="header"]
116+
|===
117+
|Variable Name |Location
118+
<%- inst.decode_variables(32).each do |d| -%>
119+
|<%= d.name %> |<%= d.extract %>
120+
<%- end -%>
121+
|===
122+
<%- end -%>
123+
<%- unless inst.decode_variables(64).empty? -%>
124+
125+
*RV64:*
126+
127+
[width="100%", cols="1,2", options="header"]
128+
|===
129+
|Variable Name |Location
130+
<%- inst.decode_variables(64).each do |d| -%>
131+
|<%= d.name %> |<%= d.extract %>
132+
<%- end -%>
133+
|===
134+
<%- end -%>
135+
<%- else -%>
136+
[width="100%", cols="1,2", options="header"]
137+
|===
138+
|Variable Name |Location
139+
<%- inst.decode_variables(inst.base.nil? ? 64 : inst.base).each do |d| -%>
140+
|<%= d.name %> |<%= d.extract %>
141+
<%- end -%>
142+
|===
143+
<%- end -%>
144+
<%- end -%>
145+
146+
Included in::
147+
<%- if inst.defined_by_condition.flat? -%>
148+
[options="autowrap,autowidth"]
149+
|===
150+
| Extension | Version
151+
<% inst.defined_by_condition.flat_versions.each do |r| %>
152+
| *<%= r.name %>* | <%= fix_entities(r.requirement_specs_to_s) %>
153+
<% end %>
154+
|===
155+
<%- else -%>
156+
<%= fix_entities(inst.defined_by_condition.to_asciidoc) %>
157+
<%- end -%>
158+
159+
<<<
160+
<% end %>
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# frozen_string_literal: true
2+
3+
require_relative "obj" # Adjust this require if your obj.rb is in the same folder.
4+
5+
# The InstructionIndex class aggregates instructions from the architecture.
6+
# It merges instructions available directly from the architecture (if any)
7+
# with those from each extension.
8+
class InstructionIndex
9+
def initialize(cfg_arch)
10+
@cfg_arch = cfg_arch
11+
end
12+
13+
def instructions
14+
@instructions ||= begin
15+
merged = []
16+
# If the architecture responds to :instructions, add them.
17+
if @cfg_arch.respond_to?(:instructions)
18+
merged.concat(@cfg_arch.instructions)
19+
end
20+
# Also, add instructions from each extension.
21+
if @cfg_arch.respond_to?(:extensions)
22+
@cfg_arch.extensions.each do |ext|
23+
ext_obj = @cfg_arch.extension(ext.name)
24+
if ext_obj && ext_obj.respond_to?(:instructions)
25+
merged.concat(ext_obj.instructions)
26+
end
27+
end
28+
end
29+
merged.uniq { |inst| inst.name }.sort_by { |inst| inst.name }
30+
end
31+
end
32+
33+
def find_instruction(name)
34+
instructions.find { |inst| inst.name == name }
35+
end
36+
end

0 commit comments

Comments
 (0)