From fd79fd18c6c32884e757e91b8629c87af4cbf34e Mon Sep 17 00:00:00 2001
From: Charlie Stanton <charlie@shtanton.xyz>
Date: Sat, 30 Mar 2024 21:09:32 +0000
Subject: Add map destructure

---
 subex/filter.go     |  6 ++++++
 subex/main_test.go  | 35 +++++++++++++++++++++++++++++++++++
 subex/parse.go      | 13 +++++++++++++
 subex/subexast.go   | 29 +++++++++++++++++++++++++++++
 subex/subexstate.go | 25 +++++++++++++++++++++++++
 5 files changed, 108 insertions(+)

diff --git a/subex/filter.go b/subex/filter.go
index dce0f0e..ae4b8ab 100644
--- a/subex/filter.go
+++ b/subex/filter.go
@@ -38,6 +38,12 @@ func (_ anyArrayFilter) valueFilter(value walk.Value) bool {
 	return isArray
 }
 
+type anyMapFilter struct {}
+func (_ anyMapFilter) valueFilter(value walk.Value) bool {
+	_, isMap := value.(walk.MapValue)
+	return isMap
+}
+
 type anyStringFilter struct {}
 func (_ anyStringFilter) valueFilter(value walk.Value) bool {
 	_, isString := value.(walk.StringValue)
diff --git a/subex/main_test.go b/subex/main_test.go
index 673b807..9c1819a 100644
--- a/subex/main_test.go
+++ b/subex/main_test.go
@@ -293,6 +293,41 @@ func TestSubexMain(t *testing.T) {
 				},
 			},
 		},
+		{
+			subex: "#(.(.$_){-0}):",
+			input: []walk.Value {
+				walk.MapValue {
+					{
+						Key: "a",
+						Value: walk.NullValue{},
+					},
+					{
+						Key: "b",
+						Value: walk.NumberValue(4),
+					},
+					{
+						Key: "c",
+						Value: walk.StringValue("hello"),
+					},
+				},
+			},
+			expected: []walk.Value {
+				walk.ArrayValue {
+					{
+						Index: 0,
+						Value: walk.StringValue("a"),
+					},
+					{
+						Index: 0,
+						Value: walk.StringValue("b"),
+					},
+					{
+						Index: 0,
+						Value: walk.StringValue("c"),
+					},
+				},
+			},
+		},
 	}
 
 	for i, test := range tests {
diff --git a/subex/parse.go b/subex/parse.go
index 98821fd..1e17bb3 100644
--- a/subex/parse.go
+++ b/subex/parse.go
@@ -35,6 +35,7 @@ const (
 	StringStructure
 	ArrayStructure
 	ArrayValuesStructure
+	MapStructure
 )
 func (s Structure) String() string {
 	switch s {
@@ -46,6 +47,8 @@ func (s Structure) String() string {
 		return "@"
 	case ArrayValuesStructure:
 		return ":"
+	case MapStructure:
+		return "#"
 	default:
 		panic("Invalid structure")
 	}
@@ -329,6 +332,9 @@ func parseDestructure(l RuneReader, destructure Structure, inType Type) (lhs Sub
 	case ArrayValuesStructure:
 		innerInType = ValueType
 		expectedInType = ValueType
+	case MapStructure:
+		innerInType = ValueType
+		expectedInType = ValueType
 	default:
 		panic("Invalid structure")
 	}
@@ -356,6 +362,9 @@ func parseDestructure(l RuneReader, destructure Structure, inType Type) (lhs Sub
 	case ':':
 		structure = ArrayValuesStructure
 		expectedInnerOutType = ValueType
+	case '#':
+		structure = MapStructure
+		expectedInnerOutType = ValueType
 	default:
 		panic("Missing matching destructure")
 	}
@@ -371,6 +380,8 @@ func parseDestructure(l RuneReader, destructure Structure, inType Type) (lhs Sub
 		outType = ValueType
 	case ArrayValuesStructure:
 		outType = ValueType
+	case MapStructure:
+		outType = ValueType
 	}
 
 	lhs = SubexASTDestructure {
@@ -400,6 +411,8 @@ func parseSubex(l RuneReader, minPower int, inType Type) (lhs SubexAST, outType
 			lhs, outType = parseDestructure(l, ArrayStructure, inType)
 		case ':':
 			lhs, outType = parseDestructure(l, ArrayValuesStructure, inType)
+		case '#':
+			lhs, outType = parseDestructure(l, MapStructure, inType)
 		// TODO
 		// case '[':
 		// 	rangeParts := parseRangeSubex(l)
diff --git a/subex/subexast.go b/subex/subexast.go
index a2c3675..2685925 100644
--- a/subex/subexast.go
+++ b/subex/subexast.go
@@ -489,6 +489,11 @@ func (ast SubexASTDestructure) compileWith(next SubexState, slotMap *SlotMap, in
 		construct = &SubexConstructArrayValuesState {
 			next: next,
 		}
+	case MapStructure:
+		innerOutType = ValueType
+		construct = &SubexConstructMapState {
+			next: next,
+		}
 	default:
 		panic("Invalid ast structure")
 	}
@@ -523,6 +528,14 @@ func (ast SubexASTDestructure) compileWith(next SubexState, slotMap *SlotMap, in
 				next: construct,
 			},
 		}
+	case MapStructure:
+		innerInType = ValueType
+		destructFooter = &SubexDiscardTerminalState {
+			terminal: walk.MapEnd,
+			next: &SubexDecrementNestState {
+				next: construct,
+			},
+		}
 	default:
 		panic("Invalid ast destructure")
 	}
@@ -550,6 +563,10 @@ func (ast SubexASTDestructure) compileWith(next SubexState, slotMap *SlotMap, in
 		beginConstruct = &SubexCaptureBeginState {
 			next: inner,
 		}
+	case MapStructure:
+		beginConstruct = &SubexCaptureBeginState {
+			next: inner,
+		}
 	default:
 		panic("Invalid ast structure")
 	}
@@ -593,6 +610,18 @@ func (ast SubexASTDestructure) compileWith(next SubexState, slotMap *SlotMap, in
 				},
 			},
 		}
+	case MapStructure:
+		return &SubexCaptureBeginState {
+			next: &SubexCopyState {
+				filter: anyMapFilter{},
+				next: &SubexDiscardState {
+					next: &SubexIncrementNestState {
+						keys: true,
+						next: beginConstruct,
+					},
+				},
+			},
+		}
 	default:
 		panic("Invalid destructure in ast")
 	}
diff --git a/subex/subexstate.go b/subex/subexstate.go
index 45b5d00..26d7347 100644
--- a/subex/subexstate.go
+++ b/subex/subexstate.go
@@ -373,6 +373,31 @@ func (state SubexConstructArrayValuesState) epsilon(aux auxiliaryState) []SubexB
 	}}
 }
 
+type SubexConstructMapState struct {
+	next SubexState
+}
+func (state SubexConstructMapState) epsilon(aux auxiliaryState) []SubexBranch {
+	values, aux := aux.popOutput()
+	var m walk.MapValue
+	if len(values) % 2 != 0 {
+		panic("Tried to construct array with odd length input")
+	}
+	for i := 0; i < len(values); i += 2 {
+		key, isNum := values[i].(walk.StringValue)
+		if !isNum {
+			panic("Tried to construct array with non-numeric index")
+		}
+		m = append(m, walk.MapElement {
+			Key: string(key),
+			Value: values[i + 1],
+		})
+	}
+	return []SubexBranch {{
+		state: state.next,
+		aux: aux.topAppend([]walk.Value {m}),
+	}}
+}
+
 type SubexConstructStringState struct {
 	next SubexState
 }
-- 
cgit v1.2.3