Files
jak-project/test/common/test_demacro.cpp
T
2026-08-31 15:10:16 -07:00

395 lines
14 KiB
C++

#include "common/demacro/demacro.h"
#include "gtest/gtest.h"
TEST(Demacro, CapturesAndSequences) {
const auto rules = demacro::parse_rules(R"RULES(
{
"rules": [
{
"name": "two-sets",
"match": [
"(set! (-> $dst x) (-> $src x))",
"(set! (-> $dst y) (-> $src y))"
],
"rewrite": "(copy-xy! $dst $src)"
}
]
}
)RULES");
const std::string source = R"((defun test ((a foo) (b foo))
(set! (-> a x) (-> b x))
(set! (-> a y) (-> b y))
a)
)";
const auto result = demacro::rewrite(source, rules);
EXPECT_EQ(result.rewrite_count(), 1);
EXPECT_EQ(result.source, R"((defun test ((a foo) (b foo))
(copy-xy! a b)
a)
)");
}
TEST(Demacro, RestCapturesHandleMergedLets) {
const auto rules = demacro::parse_rules(R"RULES(
{
"rules": [
{
"name": "merged-let",
"match": "(let ($*before ($temp $value) $*after) $*body-before (use $temp) $*body-after)",
"rewrite": "(let ($*before $*after) $*body-before (use-macro $value) $*body-after)"
}
]
}
)RULES");
const auto result = demacro::rewrite(
"(let ((keep value) (temp (compute)) (later other)) (first keep) (use temp) (last later))",
rules);
EXPECT_EQ(result.rewrite_count(), 1);
EXPECT_EQ(result.source,
"(let ((keep value) (later other)) (first keep) (use-macro (compute)) (last later))");
}
TEST(Demacro, PreservesCommentsInsideReplacementRange) {
const auto rules = demacro::parse_rules(R"RULES(
{
"rules": [
{
"name": "two-sets",
"match": ["(set! $dst 1)", "(set! $dst 2)"],
"rewrite": "(set-twice! $dst)"
}
]
}
)RULES");
const std::string source = R"((begin
(set! value 1)
;; Keep this explanation.
(set! value 2))
)";
const auto result = demacro::rewrite(source, rules);
EXPECT_EQ(result.rewrite_count(), 1);
EXPECT_EQ(result.source, R"((begin
;; Keep this explanation.
(set-twice! value))
)");
}
TEST(Demacro, RepeatedCapturesMustAgree) {
const auto rules = demacro::parse_rules(R"RULES(
{
"rules": [
{
"name": "two-sets",
"match": ["(set! $dst 1)", "(set! $dst 2)"],
"rewrite": "(set-twice! $dst)"
}
]
}
)RULES");
const auto result = demacro::rewrite("(begin (set! a 1) (set! b 2))", rules);
EXPECT_EQ(result.rewrite_count(), 0);
}
TEST(Demacro, ExpandsPatternTables) {
const auto rules = demacro::parse_rules(R"RULES(
{
"tables": {
"kind": [
{"value": "0", "symbol": "first"},
{"value": "1", "symbol": "second"}
]
},
"rules": [
{
"name": "kind-{{symbol}}",
"for_each": "kind",
"match": "(expanded-kind {{value}})",
"rewrite": "(kind {{symbol}})"
}
]
}
)RULES");
const auto result = demacro::rewrite("(begin (expanded-kind 0) (expanded-kind 1))", rules);
EXPECT_EQ(result.rewrite_count(), 2);
EXPECT_EQ(result.source, "(begin (kind first) (kind second))");
}
TEST(Demacro, Jak1PreservesMemUsageNameSemantics) {
const auto rules =
demacro::load_rules(file_util::get_file_path({"decompiler/config/jak1/demacro.jsonc"}));
const std::string source = R"((begin
(set! (-> usage length) (max 1 (-> usage length)))
(set! (-> usage data 0 name) "drawable-group")
(+! (-> usage data 0 count) 2)
(let ((literal-bytes 32))
(+! (-> usage data 0 used) literal-bytes)
(+! (-> usage data 0 total) (logand -16 (+ literal-bytes 15))))
(set! (-> other length) (max 1 (-> other length)))
(set! (-> other data 0 name) (symbol->string 'drawable-group))
(+! (-> other data 0 count) 3)
(let ((symbol-bytes 48))
(+! (-> other data 0 used) symbol-bytes)
(+! (-> other data 0 total) (logand -16 (+ symbol-bytes 15)))))
)";
const auto result = demacro::rewrite(source, rules);
EXPECT_EQ(result.rewrite_count(), 2);
EXPECT_EQ(result.source, R"((begin
(mem-usage-add! usage drawable-group 2 32)
(mem-usage-add-symbol! other drawable-group 3 48))
)");
}
TEST(Demacro, Jak1RecognizesPerfStatAndDmaMemUsageMacros) {
const auto rules =
demacro::load_rules(file_util::get_file_path({"decompiler/config/jak1/demacro.jsonc"}));
const std::string source = R"((begin
(let* ((stat (-> *perf-stats* data 1))
(ctrl (-> stat ctrl)))
(+! (-> stat count) 1)
(b! (zero? ctrl) reset-done :delay (nop!))
(.mtc0 Perf 0)
(.sync.l)
(.sync.p)
(.mtpc pcr0 0)
(.mtpc pcr1 0)
(.sync.l)
(.sync.p)
(.mtc0 Perf ctrl))
(.sync.l)
(.sync.p)
(label reset-done)
0
(let ((stat (-> *perf-stats* data 1)))
(b! (zero? (-> stat ctrl)) read-done :delay (nop!))
(.mtc0 Perf 0)
(.sync.l)
(.sync.p)
(.mfpc counter0 pcr0)
(+! (-> stat accum0) counter0)
(.mfpc counter1 pcr1)
(+! (-> stat accum1) counter1))
(label read-done)
0
(let ((usage *dma-mem-usage*))
(when (nonzero? usage)
(set! (-> usage length) (max 87 (-> usage length)))
(set! (-> usage data 86 name) "pris-generic")
(+! (-> usage data 86 count) 1)
(+! (-> usage data 86 used) bytes)
(set! (-> usage data 86 total) (-> usage data 86 used))))
(let ((line (-> dma-buf base)))
(.sync.l)
(.cache dxwbin line 0)
(.sync.l)
(.cache dxwbin line 1))
(.sync.l)
0)
)";
const auto result = demacro::rewrite(source, rules);
EXPECT_EQ(result.rewrite_count(), 4);
EXPECT_EQ(result.source, R"((begin
(reset! (-> *perf-stats* data 1))
(read! (-> *perf-stats* data 1))
(dma-mem-usage-add! pris-generic 1 bytes)
(invalidate-cache-line (-> dma-buf base)))
)");
}
TEST(Demacro, Jak1RecognizesCachedEngineIteration) {
const auto rules =
demacro::load_rules(file_util::get_file_path({"decompiler/config/jak1/demacro.jsonc"}));
const std::string source = R"((let ((node (-> *collide-player-list* alive-list next0)))
*collide-player-list*
(let ((next-node (-> node next0)))
(while (!= node (-> *collide-player-list* alive-list-end))
;; Body comments must survive the collapsed traversal.
(visit (-> (the-as connection node) param1))
(set! node next-node)
*collide-player-list*
(set! next-node (-> next-node next0)))))
)";
const auto result = demacro::rewrite(source, rules);
EXPECT_EQ(result.rewrite_count(), 1);
EXPECT_EQ(result.source, R"(;; Body comments must survive the collapsed traversal.
(iterate-engine-connections (node *collide-player-list*) (visit (-> (the-as connection node) param1)))
)");
}
TEST(Demacro, Jak1RecognizesMergedCachedEngineIterations) {
const auto rules =
demacro::load_rules(file_util::get_file_path({"decompiler/config/jak1/demacro.jsonc"}));
const std::string source = R"((begin
(let ((node (-> first-engine alive-list next0)))
first-engine
(let ((next-node (-> node next0)))
(while (!= node (-> first-engine alive-list-end))
(visit-first node)
(set! node next-node)
first-engine
(set! next-node (-> next-node next0)))
(set! node (-> second-engine alive-list next0))
second-engine
(set! next-node (-> node next0))
(while (!= node (-> second-engine alive-list-end))
(visit-second node)
(set! node next-node)
second-engine
(set! next-node (-> next-node next0)))))
(set! node (-> assigned-engine alive-list next0))
assigned-engine
(set! next-node (-> node next0))
(while (!= node (-> assigned-engine alive-list-end))
(visit-assigned node)
(set! node next-node)
assigned-engine
(set! next-node (-> next-node next0)))
(let ((keep value)
(node (-> bound-engine alive-list next0)))
bound-engine
(let ((next-node (-> node next0)))
(while (!= node (-> bound-engine alive-list-end))
(visit-bound node)
(set! node next-node)
bound-engine
(set! next-node (-> next-node next0))))))
)";
const auto result = demacro::rewrite(source, rules);
EXPECT_EQ(result.rewrite_count(), 4);
EXPECT_EQ(result.source, R"((begin
(iterate-engine-connections (node first-engine) (visit-first node))
(iterate-engine-connections (node second-engine) (visit-second node))
(iterate-engine-connections (node assigned-engine) (visit-assigned node))
(let ((keep value)) (iterate-engine-connections (node bound-engine) (visit-bound node))))
)");
}
TEST(Demacro, Jak1RecognizesDmaBucketConstruction) {
const auto rules =
demacro::load_rules(file_util::get_file_path({"decompiler/config/jak1/demacro.jsonc"}));
const std::string source = R"((let* ((buf (-> (current-frame) debug-buf))
(start (-> buf base)))
;; Keep the packet-building body.
(emit-packet buf)
(let ((edge (-> buf base)))
(let ((packet (the-as dma-packet (-> buf base))))
(set! (-> packet dma) (new 'static 'dma-tag :id (dma-tag-id next)))
(set! (-> packet vif0) (new 'static 'vif-tag))
(set! (-> packet vif1) (new 'static 'vif-tag))
(set! (-> buf base) (&+ (the-as pointer packet) 16)))
(dma-bucket-insert-tag buckets bucket start (the-as (pointer dma-tag) edge))))
)";
const auto result = demacro::rewrite(source, rules);
EXPECT_EQ(result.rewrite_count(), 1);
EXPECT_EQ(result.source, R"(;; Keep the packet-building body.
(with-dma-buffer-add-bucket ((buf (-> (current-frame) debug-buf)) bucket) :bucket-group buckets (emit-packet buf))
)");
}
TEST(Demacro, Jak1RecognizesDmaCntPacketsWithExplicitQwc) {
const auto rules =
demacro::load_rules(file_util::get_file_path({"decompiler/config/jak1/demacro.jsonc"}));
const std::string source = R"((begin
(let* ((dma-state dma-buf)
(mask-packet (the-as object (-> dma-state base))))
(set! (-> (the-as dma-packet mask-packet) dma)
(new 'static 'dma-tag :qwc #x3 :id (dma-tag-id cnt)))
(set! (-> (the-as dma-packet mask-packet) vif0)
(new 'static 'vif-tag :cmd (vif-cmd stmask)))
(set! (-> (the-as dma-packet mask-packet) vif1) (new 'static 'vif-tag))
(set! (-> dma-state base) (&+ (the-as pointer mask-packet) 16)))
(let* ((dma-state dma-buf)
(packet (the-as dma-packet (-> dma-state base))))
(set! (-> packet dma) (new 'static 'dma-tag :qwc qwc :id (dma-tag-id cnt)))
(set! (-> packet vif0) vif0)
(set! (-> packet vif1) vif1)
(set! (-> dma-state base) (&+ (the-as pointer packet) 16))))
)";
const auto result = demacro::rewrite(source, rules);
EXPECT_EQ(result.rewrite_count(), 2);
EXPECT_EQ(result.source, R"((begin
(dma-buffer-add-cnt-vif2 dma-buf #x3 (new 'static 'vif-tag :cmd (vif-cmd stmask)) (new 'static 'vif-tag))
(dma-buffer-add-cnt-vif2 dma-buf qwc vif0 vif1))
)");
}
TEST(Demacro, Jak1RecognizesDirectDmaCntAndRefPackets) {
const auto rules =
demacro::load_rules(file_util::get_file_path({"decompiler/config/jak1/demacro.jsonc"}));
const std::string source = R"((begin
(let ((packet (the-as object (-> dma-buf base))))
(set! (-> (the-as dma-packet packet) dma) (new 'static 'dma-tag :id (dma-tag-id cnt)))
(set! (-> (the-as dma-packet packet) vif0) vif0)
(set! (-> (the-as dma-packet packet) vif1) vif1)
(set! (-> dma-buf base) (&+ (the-as pointer packet) 16)))
(let* ((dma-state dma-buf)
(packet (the-as object (-> dma-state base))))
(set! (-> (the-as dma-packet packet) dma)
(new 'static 'dma-tag :id (dma-tag-id cnt) :qwc qwc))
(set! (-> (the-as dma-packet packet) vif0) vif0)
(set! (-> (the-as dma-packet packet) vif1) vif1)
(set! (-> dma-state base) (&+ (the-as pointer packet) 16)))
(let ((packet (the-as object (-> dma-buf base))))
(set! (-> (the-as dma-packet packet) dma)
(new 'static 'dma-tag :qwc qwc :id (dma-tag-id ref) :addr addr))
(set! (-> (the-as dma-packet packet) vif0) vif0)
(set! (-> (the-as dma-packet packet) vif1) vif1)
(set! (-> dma-buf base) (&+ (the-as pointer packet) 16))))
)";
const auto result = demacro::rewrite(source, rules);
EXPECT_EQ(result.rewrite_count(), 3);
EXPECT_EQ(result.source, R"((begin
(dma-buffer-add-cnt-vif2 dma-buf 0 vif0 vif1)
(dma-buffer-add-cnt-vif2 dma-buf qwc vif0 vif1)
(dma-buffer-add-ref-vif2 dma-buf qwc addr vif0 vif1))
)");
}
TEST(Demacro, Jak1RecognizesDmaNextPackets) {
const auto rules =
demacro::load_rules(file_util::get_file_path({"decompiler/config/jak1/demacro.jsonc"}));
const std::string source = R"((begin
(let ((packet (the-as object (-> dma-buf base))))
(set! (-> (the-as dma-packet packet) dma)
(new 'static 'dma-tag :id (dma-tag-id next) :addr next-address))
(set! (-> (the-as dma-packet packet) vif0) vif0)
(set! (-> (the-as dma-packet packet) vif1) vif1)
(set! (-> dma-buf base) (&+ (the-as pointer packet) 16)))
(let ((packet (the-as dma-packet (-> dma-buf base))))
(set! (-> packet dma) (new 'static 'dma-tag :id (dma-tag-id next)))
(set! (-> packet vif0) (new 'static 'vif-tag))
(set! (-> packet vif1) (new 'static 'vif-tag))
(set! (-> dma-buf base) (&+ (the-as pointer packet) 16))))
)";
const auto result = demacro::rewrite(source, rules);
EXPECT_EQ(result.rewrite_count(), 2);
EXPECT_EQ(result.source, R"((begin
(dma-buffer-add-next-vif2 dma-buf 0 next-address vif0 vif1)
(dma-buffer-add-next-vif2 dma-buf 0 0 (new 'static 'vif-tag) (new 'static 'vif-tag)))
)");
}
TEST(Demacro, Jak1RecognizesInlinedFontEnumSetters) {
const auto rules =
demacro::load_rules(file_util::get_file_path({"decompiler/config/jak1/demacro.jsonc"}));
const std::string source = R"((begin
(set! (-> font flags) (font-flags shadow kerning large))
;; Keep the color choice with its reconstructed call.
(let ((selected-font font))
(set! (-> selected-font color) (font-color progress-selected)))
(let ((option-font font))
(set! (-> option-font color)
(if selected? (font-color progress-selected) (font-color default))))
(set! (-> water flags) (water-flag active)))
)";
const auto result = demacro::rewrite(source, rules);
EXPECT_EQ(result.rewrite_count(), 3);
EXPECT_EQ(result.source, R"((begin
(set-flags! font (font-flags shadow kerning large))
;; Keep the color choice with its reconstructed call.
(set-color! font (font-color progress-selected))
(set-color! font (if selected? (font-color progress-selected) (font-color default)))
(set! (-> water flags) (water-flag active)))
)");
}