diff --git a/go/both_ref_test.go b/go/both_ref_test.go index ab1f525..eae1f19 100644 --- a/go/both_ref_test.go +++ b/go/both_ref_test.go @@ -46,6 +46,18 @@ func bothRefEqual(a, b any) bool { return false } } + // Meta: treat nil and empty map as equal + if len(av.Meta) != 0 || len(bv.Meta) != 0 { + if len(av.Meta) != len(bv.Meta) { + return false + } + for k, v := range av.Meta { + bval, exists := bv.Meta[k] + if !exists || !bothRefEqual(v, bval) { + return false + } + } + } return true case ListRef: bv, ok := b.(ListRef) @@ -63,6 +75,18 @@ func bothRefEqual(a, b any) bool { return false } } + // Meta: treat nil and empty map as equal + if len(av.Meta) != 0 || len(bv.Meta) != 0 { + if len(av.Meta) != len(bv.Meta) { + return false + } + for k, v := range av.Meta { + bval, exists := bv.Meta[k] + if !exists || !bothRefEqual(v, bval) { + return false + } + } + } return true case map[string]any: bv, ok := b.(map[string]any) @@ -467,3 +491,97 @@ func TestBothRefMapWithNullAndList(t *testing.T) { // {a:null,b:[1]} → MapRef with null value and ListRef value expectBothRef(t, "{a:null,b:[1]}", bmr(false, "a", nil, "b", blr(false, 1.0))) } + +// --- Meta map --- + +func TestMapRefMetaInitialized(t *testing.T) { + // MapRef.Meta should be initialized as an empty map when MapRef is enabled. + j := Make(Options{MapRef: boolPtr(true)}) + got, err := j.Parse("{a:1}") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + m, ok := got.(MapRef) + if !ok { + t.Fatalf("expected MapRef, got %T: %#v", got, got) + } + if m.Meta == nil { + t.Errorf("expected Meta to be initialized (non-nil), got nil") + } + if len(m.Meta) != 0 { + t.Errorf("expected Meta to be empty, got %#v", m.Meta) + } +} + +func TestListRefMetaInitialized(t *testing.T) { + // ListRef.Meta should be initialized as an empty map when ListRef is enabled. + j := Make(Options{ListRef: boolPtr(true)}) + got, err := j.Parse("[1,2]") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + lr, ok := got.(ListRef) + if !ok { + t.Fatalf("expected ListRef, got %T: %#v", got, got) + } + if lr.Meta == nil { + t.Errorf("expected Meta to be initialized (non-nil), got nil") + } + if len(lr.Meta) != 0 { + t.Errorf("expected Meta to be empty, got %#v", lr.Meta) + } +} + +func TestMapRefMetaAvailableInBOPhase(t *testing.T) { + // Verify that Meta is available during parsing (set in BO, accessible in BC). + j := Make(Options{MapRef: boolPtr(true)}) + + // Add a custom BO action that writes to Meta + j.Rule("map", func(rs *RuleSpec) { + rs.AddBO(func(r *Rule, ctx *Context) { + if mr, ok := r.Node.(MapRef); ok { + mr.Meta["created_in"] = "bo" + r.Node = mr + } + }) + }) + + got, err := j.Parse("{a:1}") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + m, ok := got.(MapRef) + if !ok { + t.Fatalf("expected MapRef, got %T: %#v", got, got) + } + if m.Meta["created_in"] != "bo" { + t.Errorf("expected Meta[\"created_in\"] = \"bo\", got %#v", m.Meta) + } +} + +func TestListRefMetaAvailableInBOPhase(t *testing.T) { + // Verify that Meta is available during parsing (set in BO, accessible in BC). + j := Make(Options{ListRef: boolPtr(true)}) + + // Add a custom BO action that writes to Meta + j.Rule("list", func(rs *RuleSpec) { + rs.AddBO(func(r *Rule, ctx *Context) { + if lr, ok := r.Node.(ListRef); ok { + lr.Meta["created_in"] = "bo" + r.Node = lr + } + }) + }) + + got, err := j.Parse("[1,2]") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + lr, ok := got.(ListRef) + if !ok { + t.Fatalf("expected ListRef, got %T: %#v", got, got) + } + if lr.Meta["created_in"] != "bo" { + t.Errorf("expected Meta[\"created_in\"] = \"bo\", got %#v", lr.Meta) + } +} diff --git a/go/grammar.go b/go/grammar.go index 1cd9aae..79ccb6b 100644 --- a/go/grammar.go +++ b/go/grammar.go @@ -173,10 +173,17 @@ func Grammar(rsm map[string]*RuleSpec, cfg *LexConfig) { // BO callbacks (JSON then Jsonic): mapSpec.BO = []StateAction{ - // JSON: create empty map + // JSON: create empty map (or MapRef if enabled) func(r *Rule, ctx *Context) { _ = ctx - r.Node = make(map[string]any) + if cfg.MapRef { + r.Node = MapRef{ + Val: make(map[string]any), + Meta: make(map[string]any), + } + } else { + r.Node = make(map[string]any) + } }, // Jsonic: increment dmap depth func(r *Rule, ctx *Context) { @@ -191,13 +198,14 @@ func Grammar(rsm map[string]*RuleSpec, cfg *LexConfig) { // BC callbacks: mapSpec.BC = []StateAction{ - // Wrap map in MapRef if option is enabled. + // Set Implicit on MapRef if option is enabled. func(r *Rule, ctx *Context) { _ = ctx if cfg.MapRef { implicit := !(r.O0 != NoToken && r.O0.Tin == TinOB) - if m, ok := r.Node.(map[string]any); ok { - r.Node = MapRef{Val: m, Implicit: implicit} + if mr, ok := r.Node.(MapRef); ok { + mr.Implicit = implicit + r.Node = mr } } }, @@ -240,10 +248,17 @@ func Grammar(rsm map[string]*RuleSpec, cfg *LexConfig) { // BO callbacks (JSON then Jsonic): listSpec.BO = []StateAction{ - // JSON: create empty list + // JSON: create empty list (or ListRef if enabled) func(r *Rule, ctx *Context) { _ = ctx - r.Node = make([]any, 0) + if cfg.ListRef { + r.Node = ListRef{ + Val: make([]any, 0), + Meta: make(map[string]any), + } + } else { + r.Node = make([]any, 0) + } }, // Jsonic: increment dlist depth, handle implist func(r *Rule, ctx *Context) { @@ -256,13 +271,11 @@ func Grammar(rsm map[string]*RuleSpec, cfg *LexConfig) { // If previous rule was an implicit list, adopt its node if r.Prev != NoRule && r.Prev != nil { if implist, ok := r.Prev.U["implist"]; ok && implist == true { - arr := r.Node.([]any) prevNode := r.Prev.Node if IsUndefined(prevNode) { prevNode = nil } - arr = append(arr, prevNode) - r.Node = arr + r.Node = nodeListAppend(r.Node, prevNode) r.Prev.Node = r.Node } } @@ -271,17 +284,17 @@ func Grammar(rsm map[string]*RuleSpec, cfg *LexConfig) { // BC callbacks: listSpec.BC = []StateAction{ - // Wrap list in ListRef if option is enabled. + // Set Implicit and Child on ListRef if option is enabled. func(r *Rule, ctx *Context) { _ = ctx if cfg.ListRef { implicit := !(r.O0 != NoToken && r.O0.Tin == TinOS) - if arr, ok := r.Node.([]any); ok { - var child any + if lr, ok := r.Node.(ListRef); ok { + lr.Implicit = implicit if c, ok := r.U["child$"]; ok { - child = c + lr.Child = c } - r.Node = ListRef{Val: arr, Implicit: implicit, Child: child} + r.Node = lr } } }, @@ -424,8 +437,8 @@ func Grammar(rsm map[string]*RuleSpec, cfg *LexConfig) { _ = ctx done, _ := r.U["done"].(bool) if !done && !IsUndefined(r.Child.Node) { - if arr, ok := r.Node.([]any); ok { - r.Node = append(arr, r.Child.Node) + if _, ok := nodeListVal(r.Node); ok { + r.Node = nodeListAppend(r.Node, r.Child.Node) // Propagate updated slice to parent list rule // (Go slices may reallocate on append, unlike JS arrays which are reference types) if r.Parent != NoRule && r.Parent != nil { @@ -447,8 +460,8 @@ func Grammar(rsm map[string]*RuleSpec, cfg *LexConfig) { val = nil } pairObj := map[string]any{key: val} - if arr, ok := r.Node.([]any); ok { - r.Node = append(arr, pairObj) + if _, ok := nodeListVal(r.Node); ok { + r.Node = nodeListAppend(r.Node, pairObj) if r.Parent != NoRule && r.Parent != nil { r.Parent.Node = r.Node } @@ -495,8 +508,8 @@ func Grammar(rsm map[string]*RuleSpec, cfg *LexConfig) { U: map[string]any{"done": true}, A: func(r *Rule, ctx *Context) { _ = ctx - if arr, ok := r.Node.([]any); ok { - r.Node = append(arr, nil) + if _, ok := nodeListVal(r.Node); ok { + r.Node = nodeListAppend(r.Node, nil) // Propagate to parent if r.Parent != NoRule && r.Parent != nil { r.Parent.Node = r.Node @@ -508,8 +521,8 @@ func Grammar(rsm map[string]*RuleSpec, cfg *LexConfig) { U: map[string]any{"done": true}, A: func(r *Rule, ctx *Context) { _ = ctx - if arr, ok := r.Node.([]any); ok { - r.Node = append(arr, nil) + if _, ok := nodeListVal(r.Node); ok { + r.Node = nodeListAppend(r.Node, nil) // Propagate to parent if r.Parent != NoRule && r.Parent != nil { r.Parent.Node = r.Node @@ -563,6 +576,39 @@ func Grammar(rsm map[string]*RuleSpec, cfg *LexConfig) { rsm["elem"] = elemSpec } +// nodeListAppend appends a value to a list node (plain []any or ListRef). +// Returns the updated node (must be reassigned since slices may reallocate). +func nodeListAppend(node any, val any) any { + if lr, ok := node.(ListRef); ok { + lr.Val = append(lr.Val, val) + return lr + } + if arr, ok := node.([]any); ok { + return append(arr, val) + } + return node +} + +// nodeListVal extracts the []any from a list node (plain []any or ListRef). +func nodeListVal(node any) ([]any, bool) { + if lr, ok := node.(ListRef); ok { + return lr.Val, true + } + if arr, ok := node.([]any); ok { + return arr, true + } + return nil, false +} + +// nodeListSetVal updates the []any inside a list node, returning the updated node. +func nodeListSetVal(node any, arr []any) any { + if lr, ok := node.(ListRef); ok { + lr.Val = arr + return lr + } + return arr +} + // nodeMapSet sets a key on a map node. func nodeMapSet(node any, key any, val any) { k, _ := key.(string) diff --git a/go/listref_test.go b/go/listref_test.go index aaa72bc..59e276d 100644 --- a/go/listref_test.go +++ b/go/listref_test.go @@ -45,6 +45,18 @@ func listRefEqual(a, b any) bool { return false } } + // Meta: treat nil and empty map as equal + if len(av.Meta) != 0 || len(bv.Meta) != 0 { + if len(av.Meta) != len(bv.Meta) { + return false + } + for k, v := range av.Meta { + bval, exists := bv.Meta[k] + if !exists || !listRefEqual(v, bval) { + return false + } + } + } return true case map[string]any: bv, ok := b.(map[string]any) diff --git a/go/mapref_test.go b/go/mapref_test.go index 96471f0..dfbd407 100644 --- a/go/mapref_test.go +++ b/go/mapref_test.go @@ -46,6 +46,18 @@ func mapRefEqual(a, b any) bool { return false } } + // Meta: treat nil and empty map as equal + if len(av.Meta) != 0 || len(bv.Meta) != 0 { + if len(av.Meta) != len(bv.Meta) { + return false + } + for k, v := range av.Meta { + bval, exists := bv.Meta[k] + if !exists || !mapRefEqual(v, bval) { + return false + } + } + } return true case map[string]any: bv, ok := b.(map[string]any) diff --git a/go/text.go b/go/text.go index f9993f7..a7510b5 100644 --- a/go/text.go +++ b/go/text.go @@ -16,6 +16,8 @@ type Text struct { // ListRef wraps a list value with metadata about how it was created. // When the ListRef option is enabled, list values in the output are // returned as ListRef instead of plain []any slices. +// ListRef is created early (in the BO phase) so that custom parsers +// can store additional information in the Meta map during parsing. type ListRef struct { // Val is the list contents. Val []any @@ -31,11 +33,18 @@ type ListRef struct { // Multiple child values are merged (deep merge if Map.Extend is true). // Nil when no child value is present. Child any + + // Meta is a map for custom parsers to attach additional information + // during parsing. It is initialized when the ListRef is created in + // the BO (before-open) phase. + Meta map[string]any } // MapRef wraps a map value with metadata about how it was created. // When the MapRef option is enabled, map values in the output are // returned as MapRef instead of plain map[string]any. +// MapRef is created early (in the BO phase) so that custom parsers +// can store additional information in the Meta map during parsing. type MapRef struct { // Val is the map contents. Val map[string]any @@ -44,4 +53,9 @@ type MapRef struct { // (e.g. key:value pairs without braces), // and false when braces were used explicitly. Implicit bool + + // Meta is a map for custom parsers to attach additional information + // during parsing. It is initialized when the MapRef is created in + // the BO (before-open) phase. + Meta map[string]any } diff --git a/go/utility.go b/go/utility.go index f855b49..bef3416 100644 --- a/go/utility.go +++ b/go/utility.go @@ -64,7 +64,8 @@ func deepMerge(base, over any) any { } // Preserve MapRef wrapper if the over value was a MapRef. if overIsMR { - return MapRef{Val: result, Implicit: overMR.Implicit} + meta := mergeMeta(baseMR.Meta, overMR.Meta) + return MapRef{Val: result, Implicit: overMR.Implicit, Meta: meta} } return result } @@ -96,7 +97,8 @@ func deepMerge(base, over any) any { } else if baseIsLR { child = deepClone(baseLR.Child) } - return ListRef{Val: result, Implicit: overLR.Implicit, Child: child} + meta := mergeMeta(baseLR.Meta, overLR.Meta) + return ListRef{Val: result, Implicit: overLR.Implicit, Child: child, Meta: meta} } return result } @@ -108,6 +110,33 @@ func deepMerge(base, over any) any { return deepClone(over) } +// cloneMeta creates a shallow copy of a Meta map. +func cloneMeta(meta map[string]any) map[string]any { + if meta == nil { + return nil + } + result := make(map[string]any, len(meta)) + for k, v := range meta { + result[k] = v + } + return result +} + +// mergeMeta merges two Meta maps. The over map's values take precedence. +func mergeMeta(base, over map[string]any) map[string]any { + if base == nil && over == nil { + return nil + } + result := make(map[string]any) + for k, v := range base { + result[k] = v + } + for k, v := range over { + result[k] = v + } + return result +} + // deepClone creates a deep copy of a value. func deepClone(val any) any { if val == nil { @@ -131,13 +160,13 @@ func deepClone(val any) any { for i, val := range v.Val { result[i] = deepClone(val) } - return ListRef{Val: result, Implicit: v.Implicit, Child: deepClone(v.Child)} + return ListRef{Val: result, Implicit: v.Implicit, Child: deepClone(v.Child), Meta: cloneMeta(v.Meta)} case MapRef: result := make(map[string]any) for k, val := range v.Val { result[k] = deepClone(val) } - return MapRef{Val: result, Implicit: v.Implicit} + return MapRef{Val: result, Implicit: v.Implicit, Meta: cloneMeta(v.Meta)} default: return v } diff --git a/test/utility.js b/test/utility.js index 149eab9..ddb86ea 100644 --- a/test/utility.js +++ b/test/utility.js @@ -17,7 +17,7 @@ function loadTSV(name) { throw new Error('spec file not found: ' + specPath) } - const lines = readFileSync(specPath, 'utf8').split('\n').filter(Boolean) + const lines = readFileSync(specPath, 'utf8').split(/\r?\n/).filter(Boolean) return lines.slice(1).map((line, i) => { const cols = line.split('\t').map(unescape) return { cols, row: i + 1 }