From 8b4c7dc0d4f3c213daf6ed837898fa7e32ea624e Mon Sep 17 00:00:00 2001 From: Mattias Hansson Date: Sun, 26 Oct 2025 21:06:11 +0100 Subject: [PATCH] Added conststr + tests for handling constant strings. --- internal/compiler/conststr.go | 74 ++++++++++++ internal/compiler/conststr_test.go | 188 +++++++++++++++++++++++++++++ internal/preproc/pragma.go | 5 + 3 files changed, 267 insertions(+) create mode 100644 internal/compiler/conststr.go create mode 100644 internal/compiler/conststr_test.go diff --git a/internal/compiler/conststr.go b/internal/compiler/conststr.go new file mode 100644 index 0000000..7b01d2c --- /dev/null +++ b/internal/compiler/conststr.go @@ -0,0 +1,74 @@ +package compiler + +import "c65gm/internal/preproc" + +const ( + asmStringDeclStart = " !raw " + asmStringDeclEnd = " !8 0" + asmCBMStringDeclStart = " !pet " + asmCBMStringDeclEnd = " !8 0" +) + +type ConstantString struct { + label string + value string + compressable bool + useCBMStrings bool +} + +type ConstantStringHandler struct { + strings []*ConstantString + compressMap map[string]string +} + +func NewConstantStringHandler() *ConstantStringHandler { + return &ConstantStringHandler{ + strings: make([]*ConstantString, 0), + compressMap: make(map[string]string), + } +} + +func (h *ConstantStringHandler) AddConstStr(label, value string, compress bool, ps preproc.PragmaSet) string { + useCBM := ps.GetPragma("_P_USE_CBM_STRINGS") != "" + + if compress { + if existingLabel, exists := h.compressMap[value]; exists { + return existingLabel + } + } + + cs := &ConstantString{ + label: label, + value: value, + compressable: compress, + useCBMStrings: useCBM, + } + + h.strings = append(h.strings, cs) + + if compress { + h.compressMap[value] = label + } + + return label +} + +func (h *ConstantStringHandler) GenerateConstStrDecls() []string { + result := make([]string, 0, len(h.strings)*3) + + for _, cs := range h.strings { + startStr := asmStringDeclStart + endStr := asmStringDeclEnd + + if cs.useCBMStrings { + startStr = asmCBMStringDeclStart + endStr = asmCBMStringDeclEnd + } + + result = append(result, cs.label) + result = append(result, startStr+cs.value) + result = append(result, endStr) + } + + return result +} diff --git a/internal/compiler/conststr_test.go b/internal/compiler/conststr_test.go new file mode 100644 index 0000000..7cca76e --- /dev/null +++ b/internal/compiler/conststr_test.go @@ -0,0 +1,188 @@ +package compiler + +import ( + "c65gm/internal/preproc" + "testing" +) + +func TestAddConstStr_NonCompressable(t *testing.T) { + h := NewConstantStringHandler() + ps := preproc.PragmaSet{} + + label1 := h.AddConstStr("lbl1", "hello", false, ps) + label2 := h.AddConstStr("lbl2", "hello", false, ps) + + if label1 != "lbl1" { + t.Errorf("Expected label1 = lbl1, got %s", label1) + } + if label2 != "lbl2" { + t.Errorf("Expected label2 = lbl2, got %s", label2) + } + if len(h.strings) != 2 { + t.Errorf("Expected 2 strings, got %d", len(h.strings)) + } +} + +func TestAddConstStr_CompressableDedupe(t *testing.T) { + h := NewConstantStringHandler() + ps := preproc.PragmaSet{} + + label1 := h.AddConstStr("lbl1", "hello", true, ps) + label2 := h.AddConstStr("lbl2", "hello", true, ps) + + if label1 != "lbl1" { + t.Errorf("Expected label1 = lbl1, got %s", label1) + } + if label2 != "lbl1" { + t.Errorf("Expected label2 = lbl1 (deduped), got %s", label2) + } + if len(h.strings) != 1 { + t.Errorf("Expected 1 string after dedup, got %d", len(h.strings)) + } +} + +func TestAddConstStr_CompressableNoDedupe(t *testing.T) { + h := NewConstantStringHandler() + ps := preproc.PragmaSet{} + + label1 := h.AddConstStr("lbl1", "hello", true, ps) + label2 := h.AddConstStr("lbl2", "world", true, ps) + + if label1 != "lbl1" { + t.Errorf("Expected label1 = lbl1, got %s", label1) + } + if label2 != "lbl2" { + t.Errorf("Expected label2 = lbl2, got %s", label2) + } + if len(h.strings) != 2 { + t.Errorf("Expected 2 strings (different values), got %d", len(h.strings)) + } +} + +func TestGenerateConstStrDecls_Normal(t *testing.T) { + h := NewConstantStringHandler() + ps := preproc.PragmaSet{} + + h.AddConstStr("str1", "\"hello\"", false, ps) + + result := h.GenerateConstStrDecls() + + expected := []string{ + "str1", + " !raw \"hello\"", + " !8 0", + } + + if len(result) != len(expected) { + t.Fatalf("Expected %d lines, got %d", len(expected), len(result)) + } + + for i, exp := range expected { + if result[i] != exp { + t.Errorf("Line %d: expected %q, got %q", i, exp, result[i]) + } + } +} + +func TestGenerateConstStrDecls_CBM(t *testing.T) { + h := NewConstantStringHandler() + ps := mockPragmaSet(map[string]string{"_P_USE_CBM_STRINGS": "1"}) + + h.AddConstStr("str1", "\"hello\"", false, ps) + + result := h.GenerateConstStrDecls() + + expected := []string{ + "str1", + " !pet \"hello\"", + " !8 0", + } + + if len(result) != len(expected) { + t.Fatalf("Expected %d lines, got %d", len(expected), len(result)) + } + + for i, exp := range expected { + if result[i] != exp { + t.Errorf("Line %d: expected %q, got %q", i, exp, result[i]) + } + } +} + +func TestGenerateConstStrDecls_MixedPragmas(t *testing.T) { + h := NewConstantStringHandler() + psNormal := preproc.PragmaSet{} + psCBM := mockPragmaSet(map[string]string{"_P_USE_CBM_STRINGS": "1"}) + + h.AddConstStr("str1", "\"hello\"", false, psNormal) + h.AddConstStr("str2", "\"world\"", false, psCBM) + + result := h.GenerateConstStrDecls() + + expected := []string{ + "str1", + " !raw \"hello\"", + " !8 0", + "str2", + " !pet \"world\"", + " !8 0", + } + + if len(result) != len(expected) { + t.Fatalf("Expected %d lines, got %d", len(expected), len(result)) + } + + for i, exp := range expected { + if result[i] != exp { + t.Errorf("Line %d: expected %q, got %q", i, exp, result[i]) + } + } +} + +func TestGenerateConstStrDecls_Order(t *testing.T) { + h := NewConstantStringHandler() + ps := preproc.PragmaSet{} + + h.AddConstStr("str1", "\"first\"", false, ps) + h.AddConstStr("str2", "\"second\"", false, ps) + h.AddConstStr("str3", "\"third\"", false, ps) + + result := h.GenerateConstStrDecls() + + if len(result) != 9 { + t.Fatalf("Expected 9 lines, got %d", len(result)) + } + + if result[0] != "str1" || result[3] != "str2" || result[6] != "str3" { + t.Errorf("Labels not in correct order") + } +} + +func TestAddConstStr_CompressOnlyDedupesSameValue(t *testing.T) { + h := NewConstantStringHandler() + ps := preproc.PragmaSet{} + + h.AddConstStr("lbl1", "hello", true, ps) + h.AddConstStr("lbl2", "hello", true, ps) + h.AddConstStr("lbl3", "world", true, ps) + h.AddConstStr("lbl4", "hello", true, ps) + + if len(h.strings) != 2 { + t.Errorf("Expected 2 unique strings, got %d", len(h.strings)) + } + + if len(h.compressMap) != 2 { + t.Errorf("Expected 2 entries in compress map, got %d", len(h.compressMap)) + } + + if h.compressMap["hello"] != "lbl1" { + t.Errorf("Expected hello -> lbl1, got %s", h.compressMap["hello"]) + } + if h.compressMap["world"] != "lbl3" { + t.Errorf("Expected world -> lbl3, got %s", h.compressMap["world"]) + } +} + +func mockPragmaSet(m map[string]string) preproc.PragmaSet { + return preproc.NewPragmaSet(m) +} diff --git a/internal/preproc/pragma.go b/internal/preproc/pragma.go index dcd5966..830a444 100644 --- a/internal/preproc/pragma.go +++ b/internal/preproc/pragma.go @@ -13,6 +13,11 @@ func (ps PragmaSet) GetPragma(name string) string { return "" } +// For mocking +func NewPragmaSet(m map[string]string) PragmaSet { + return PragmaSet{m: m} +} + // Pragma manages an immutable stack of PragmaSet snapshots. type Pragma struct { pragmaSetStack []PragmaSet