Repository: brimsec/zq (go)

Issue:
Dot-containing field name in expression casues "invalid expression on lhs of assignment"
Repro is with `zq` commit 3475ad3.

I was trying to make some enhancements to the Z shaper that merged with #2300. As part of this, I was attempting a data transport on a Zeek field _before_ the `put .=unflatten(.)` call. This means working with the field as if its name contained a literal `.` as opposed to treating it like the nested record it would normally be after the `unflatten()` call.

@mccanne helped me find the correct Z syntax to work with the field despite the special characters in its name, but it currently doesn't work, with the following error:

```
$ echo '{"certificate.not_valid_before":"2011-09-10T19:15:02.000000Z"}' | zq -z 'put .["certificate.not_valid_before"]=iso(.["certificate.not_valid_before"])' -
invalid expression on lhs of assignment
```

Pull request description:
allow record index by string as l-value
This commit fixes a bug where l-values could not be index operations
with fields referenced as string, e.g., put .["x"] = 123.
The fix is to detect this case in the semantic pass and extend
the field-reference path with the string constant.

Fixes #2326 



Patch:
diff --git a/compiler/semantic/expr.go b/compiler/semantic/expr.go
index c0761f679..b9384f7b2 100644
--- a/compiler/semantic/expr.go
+++ b/compiler/semantic/expr.go
@@ -161,6 +161,13 @@ func semBinary(scope *Scope, e *ast.BinaryExpr) (ast.Expr, error) {
 	if err != nil {
 		return nil, err
 	}
+	// If we index a root record with a string constant, then just
+	// extend the path.
+	if op == "[" {
+		if path := isRootIndex(scope, lhs, rhs); path != nil {
+			return path, nil
+		}
+	}
 	return &ast.BinaryExpr{
 		Kind: "BinaryExpr",
 		Op:   e.Op,
@@ -169,6 +176,28 @@ func semBinary(scope *Scope, e *ast.BinaryExpr) (ast.Expr, error) {
 	}, nil
 }
 
+func isRootIndex(scope *Scope, lhs, rhs ast.Expr) *ast.Path {
+	if path, ok := lhs.(*ast.Path); ok && len(path.Name) == 0 {
+		if s, ok := isStringConst(scope, rhs); ok {
+			path.Name = append(path.Name, s)
+			return path
+		}
+	}
+	return nil
+}
+
+func isStringConst(scope *Scope, e ast.Expr) (string, bool) {
+	if p, ok := e.(*ast.Primitive); ok && p.Type == "string" {
+		return p.Text, true
+	}
+	if ref, ok := e.(*ast.Ref); ok {
+		if c, ok := scope.Lookup(ref.Name).(*ast.Const); ok {
+			return isStringConst(scope, c.Expr)
+		}
+	}
+	return "", false
+}
+
 func semSlice(scope *Scope, slice *ast.BinaryExpr) (*ast.BinaryExpr, error) {
 	sliceFrom, err := semExprNullable(scope, slice.LHS)
 	if err != nil {
@@ -370,6 +399,9 @@ func semField(scope *Scope, e ast.Expr) (ast.Expr, error) {
 			if err != nil {
 				return nil, err
 			}
+			if path := isRootIndex(scope, lhs, rhs); path != nil {
+				return path, nil
+			}
 			return &ast.BinaryExpr{
 				Kind: "BinaryExpr",
 				Op:   "[",
diff --git a/zfmt/canon.go b/zfmt/canon.go
index eb6bc8c1a..7603a95e5 100644
--- a/zfmt/canon.go
+++ b/zfmt/canon.go
@@ -433,10 +433,17 @@ func (c *canon) fieldpath(path []string) {
 		return
 	}
 	for k, s := range path {
-		if k != 0 {
-			c.write(".")
+		if zng.IsIdentifier(s) {
+			if k != 0 {
+				c.write(".")
+			}
+			c.write(s)
+		} else {
+			if k == 0 {
+				c.write(".")
+			}
+			c.write("[%q]", s)
 		}
-		c.write(s)
 	}
 }
 
diff --git a/zson/lexer.go b/zson/lexer.go
index d5fa002de..e6e31c1e8 100644
--- a/zson/lexer.go
+++ b/zson/lexer.go
@@ -14,7 +14,7 @@ import (
 
 var ErrBufferOverflow = errors.New("zson scanner buffer size exceeded")
 
-const primitiveRE = `^(([0-9a-fA-Fx_\$\-\+:eEnumsh./TZµ]+)|true|false|null|-Inf|-inf|\+Inf|\+inf|NaN|nan|(([.0-9]+(ns|us|ms|s|m|h|d|w|y))+))`
+const primitiveRE = `^(([0-9a-fA-Fx_\$\-\+:eEnumsh./TZµ]+)|true|false|null|-Inf|-inf|\+Inf|\+inf|NaN|nan|(([-.0-9]+(ns|us|ms|s|m|h|d|w|y))+))`
 const indentationRE = `\n\s*`
 
 type Lexer struct {


Test patch:
diff --git a/compiler/ztests/path-index.yaml b/compiler/ztests/path-index.yaml
new file mode 100644
index 000000000..f48af9ef9
--- /dev/null
+++ b/compiler/ztests/path-index.yaml
@@ -0,0 +1,7 @@
+zql: 'put .["x.y"]=to_lower(.["x.y"])'
+
+input: |
+  {"x.y":"Hello, World"}
+
+output: |
+  {"x.y":"hello, world"}
diff --git a/zson/ztests/negative-duration.yaml b/zson/ztests/negative-duration.yaml
new file mode 100644
index 000000000..1e8bdb38c
--- /dev/null
+++ b/zson/ztests/negative-duration.yaml
@@ -0,0 +1,6 @@
+zql: '*'
+
+input: &input |
+  {duration:-292y171d23h47m16.854775807s}
+
+output: *input