github.com/myhau/pulumi/pkg/v3@v3.70.2-0.20221116134521-f2775972e587/codegen/python/gen_program_expressions.go (about) 1 // nolint: goconst 2 package python 3 4 import ( 5 "bufio" 6 "bytes" 7 "fmt" 8 "io" 9 "math/big" 10 "strings" 11 12 "github.com/hashicorp/hcl/v2" 13 "github.com/hashicorp/hcl/v2/hclsyntax" 14 "github.com/zclconf/go-cty/cty" 15 16 "github.com/pulumi/pulumi/pkg/v3/codegen/hcl2/model" 17 "github.com/pulumi/pulumi/pkg/v3/codegen/pcl" 18 "github.com/pulumi/pulumi/pkg/v3/codegen/schema" 19 "github.com/pulumi/pulumi/sdk/v3/go/common/util/contract" 20 ) 21 22 type nameInfo int 23 24 func (nameInfo) Format(name string) string { 25 return PyName(name) 26 } 27 28 func (g *generator) lowerExpression(expr model.Expression, typ model.Type) (model.Expression, []*quoteTemp) { 29 // TODO(pdg): diagnostics 30 31 expr = pcl.RewritePropertyReferences(expr) 32 expr, diags := pcl.RewriteApplies(expr, nameInfo(0), false) 33 expr, lowerProxyDiags := g.lowerProxyApplies(expr) 34 expr, convertDiags := pcl.RewriteConversions(expr, typ) 35 expr, quotes, quoteDiags := g.rewriteQuotes(expr) 36 37 diags = diags.Extend(lowerProxyDiags) 38 diags = diags.Extend(convertDiags) 39 diags = diags.Extend(quoteDiags) 40 41 g.diagnostics = g.diagnostics.Extend(diags) 42 43 return expr, quotes 44 } 45 46 func (g *generator) GetPrecedence(expr model.Expression) int { 47 // Precedence is taken from https://docs.python.org/3/reference/expressions.html#operator-precedence. 48 switch expr := expr.(type) { 49 case *model.AnonymousFunctionExpression: 50 return 1 51 case *model.ConditionalExpression: 52 return 2 53 case *model.BinaryOpExpression: 54 switch expr.Operation { 55 case hclsyntax.OpLogicalOr: 56 return 3 57 case hclsyntax.OpLogicalAnd: 58 return 4 59 case hclsyntax.OpGreaterThan, hclsyntax.OpGreaterThanOrEqual, hclsyntax.OpLessThan, hclsyntax.OpLessThanOrEqual, 60 hclsyntax.OpEqual, hclsyntax.OpNotEqual: 61 return 6 62 case hclsyntax.OpAdd, hclsyntax.OpSubtract: 63 return 11 64 case hclsyntax.OpMultiply, hclsyntax.OpDivide, hclsyntax.OpModulo: 65 return 12 66 default: 67 contract.Failf("unexpected binary expression %v", expr) 68 } 69 case *model.UnaryOpExpression: 70 return 13 71 case *model.FunctionCallExpression, *model.IndexExpression, *model.RelativeTraversalExpression, 72 *model.TemplateJoinExpression: 73 return 16 74 case *model.ForExpression, *model.ObjectConsExpression, *model.SplatExpression, *model.TupleConsExpression: 75 return 17 76 case *model.LiteralValueExpression, *model.ScopeTraversalExpression, *model.TemplateExpression: 77 return 18 78 default: 79 contract.Failf("unexpected expression %v of type %T", expr, expr) 80 } 81 return 0 82 } 83 84 func (g *generator) GenAnonymousFunctionExpression(w io.Writer, expr *model.AnonymousFunctionExpression) { 85 g.Fgen(w, "lambda") 86 for i, p := range expr.Signature.Parameters { 87 if i > 0 { 88 g.Fgen(w, ",") 89 } 90 g.Fgenf(w, " %s", p.Name) 91 } 92 93 g.Fgenf(w, ": %.v", expr.Body) 94 } 95 96 func (g *generator) GenBinaryOpExpression(w io.Writer, expr *model.BinaryOpExpression) { 97 opstr, precedence := "", g.GetPrecedence(expr) 98 switch expr.Operation { 99 case hclsyntax.OpAdd: 100 opstr = "+" 101 case hclsyntax.OpDivide: 102 opstr = "/" 103 case hclsyntax.OpEqual: 104 opstr = "==" 105 case hclsyntax.OpGreaterThan: 106 opstr = ">" 107 case hclsyntax.OpGreaterThanOrEqual: 108 opstr = ">=" 109 case hclsyntax.OpLessThan: 110 opstr = "<" 111 case hclsyntax.OpLessThanOrEqual: 112 opstr = "<=" 113 case hclsyntax.OpLogicalAnd: 114 opstr = "and" 115 case hclsyntax.OpLogicalOr: 116 opstr = "or" 117 case hclsyntax.OpModulo: 118 opstr = "%" 119 case hclsyntax.OpMultiply: 120 opstr = "*" 121 case hclsyntax.OpNotEqual: 122 opstr = "!=" 123 case hclsyntax.OpSubtract: 124 opstr = "-" 125 default: 126 opstr, precedence = ",", 0 127 } 128 129 g.Fgenf(w, "%.[1]*[2]v %[3]v %.[1]*[4]o", precedence, expr.LeftOperand, opstr, expr.RightOperand) 130 } 131 132 func (g *generator) GenConditionalExpression(w io.Writer, expr *model.ConditionalExpression) { 133 g.Fgenf(w, "%.2v if %.2v else %.2v", expr.TrueResult, expr.Condition, expr.FalseResult) 134 } 135 136 func (g *generator) GenForExpression(w io.Writer, expr *model.ForExpression) { 137 close := "]" 138 if expr.Key != nil { 139 // Dictionary comprehension 140 // 141 // TODO(pdg): grouping 142 g.Fgenf(w, "{%.v: %.v", expr.Key, expr.Value) 143 close = "}" 144 } else { 145 // List comprehension 146 g.Fgenf(w, "[%.v", expr.Value) 147 } 148 149 if expr.KeyVariable == nil { 150 g.Fgenf(w, " for %v in %.v", expr.ValueVariable.Name, expr.Collection) 151 } else { 152 g.Fgenf(w, " for %v, %v in %.v", expr.KeyVariable.Name, expr.ValueVariable.Name, expr.Collection) 153 } 154 155 if expr.Condition != nil { 156 g.Fgenf(w, " if %.v", expr.Condition) 157 } 158 159 g.Fprint(w, close) 160 } 161 162 func (g *generator) genApply(w io.Writer, expr *model.FunctionCallExpression) { 163 // Extract the list of outputs and the continuation expression from the `__apply` arguments. 164 applyArgs, then := pcl.ParseApplyCall(expr) 165 166 if len(applyArgs) == 1 { 167 // If we only have a single output, just generate a normal `.apply`. 168 g.Fgenf(w, "%.16v.apply(%.v)", applyArgs[0], then) 169 } else { 170 // Otherwise, generate a call to `pulumi.all([]).apply()`. 171 g.Fgen(w, "pulumi.Output.all(") 172 for i, o := range applyArgs { 173 if i > 0 { 174 g.Fgen(w, ", ") 175 } 176 g.Fgenf(w, "%.v", o) 177 } 178 g.Fgenf(w, ").apply(%.v)", then) 179 } 180 } 181 182 // functionName computes the Python package, module, and name for the given function token. 183 func functionName(tokenArg model.Expression) (string, string, string, hcl.Diagnostics) { 184 token := tokenArg.(*model.TemplateExpression).Parts[0].(*model.LiteralValueExpression).Value.AsString() 185 tokenRange := tokenArg.SyntaxNode().Range() 186 187 // Compute the resource type from the Pulumi type token. 188 pkg, module, member, diagnostics := pcl.DecomposeToken(token, tokenRange) 189 return makeValidIdentifier(pkg), strings.Replace(module, "/", ".", -1), title(member), diagnostics 190 } 191 192 var functionImports = map[string][]string{ 193 "fileArchive": {"pulumi"}, 194 "remoteArchive": {"pulumi"}, 195 "assetArchive": {"pulumi"}, 196 "fileAsset": {"pulumi"}, 197 "stringAsset": {"pulumi"}, 198 "remoteAsset": {"pulumi"}, 199 "filebase64": {"base64"}, 200 "filebase64sha256": {"base64", "hashlib"}, 201 "readDir": {"os"}, 202 "toBase64": {"base64"}, 203 "fromBase64": {"base64"}, 204 "toJSON": {"json"}, 205 "sha1": {"hashlib"}, 206 "stack": {"pulumi"}, 207 "project": {"pulumi"}, 208 "cwd": {"os"}, 209 } 210 211 func (g *generator) getFunctionImports(x *model.FunctionCallExpression) []string { 212 if x.Name != pcl.Invoke { 213 return functionImports[x.Name] 214 } 215 216 pkg, _, _, diags := functionName(x.Args[0]) 217 contract.Assert(len(diags) == 0) 218 return []string{"pulumi_" + pkg} 219 } 220 221 func (g *generator) GenFunctionCallExpression(w io.Writer, expr *model.FunctionCallExpression) { 222 switch expr.Name { 223 case pcl.IntrinsicConvert: 224 from := expr.Args[0] 225 to := pcl.LowerConversion(from, expr.Signature.ReturnType) 226 output, isOutput := to.(*model.OutputType) 227 if isOutput { 228 to = output.ElementType 229 } 230 switch to := to.(type) { 231 case *model.EnumType: 232 components := strings.Split(to.Token, ":") 233 contract.Assertf(len(components) == 3, "malformed token %v", to.Token) 234 enum, ok := pcl.GetSchemaForType(to) 235 if !ok { 236 // No schema was provided 237 g.Fgenf(w, "%.v", expr.Args[0]) 238 return 239 } 240 moduleNameOverrides := enum.(*schema.EnumType).Package.Language["python"].(PackageInfo).ModuleNameOverrides 241 pkg := strings.ReplaceAll(components[0], "-", "_") 242 if m := tokenToModule(to.Token, &schema.Package{}, moduleNameOverrides); m != "" { 243 pkg += "." + m 244 } 245 enumName := tokenToName(to.Token) 246 247 if isOutput { 248 g.Fgenf(w, "%.v.apply(lambda x: %s.%s(x))", from, pkg, enumName) 249 } else { 250 pcl.GenEnum(to, from, func(member *schema.Enum) { 251 tag := member.Name 252 if tag == "" { 253 tag = fmt.Sprintf("%v", member.Value) 254 } 255 tag, err := makeSafeEnumName(tag, enumName) 256 contract.AssertNoError(err) 257 g.Fgenf(w, "%s.%s.%s", pkg, enumName, tag) 258 }, func(from model.Expression) { 259 g.Fgenf(w, "%s.%s(%.v)", pkg, enumName, from) 260 }) 261 } 262 default: 263 switch arg := from.(type) { 264 case *model.ObjectConsExpression: 265 g.genObjectConsExpression(w, arg, expr.Type()) 266 default: 267 g.Fgenf(w, "%.v", expr.Args[0]) 268 } 269 270 } 271 case pcl.IntrinsicApply: 272 g.genApply(w, expr) 273 case "element": 274 g.Fgenf(w, "%.16v[%.v]", expr.Args[0], expr.Args[1]) 275 case "entries": 276 g.Fgenf(w, `[{"key": k, "value": v} for k, v in %.v]`, expr.Args[0]) 277 case "fileArchive": 278 g.Fgenf(w, "pulumi.FileArchive(%.v)", expr.Args[0]) 279 case "remoteArchive": 280 g.Fgenf(w, "pulumi.RemoteArchive(%.v)", expr.Args[0]) 281 case "assetArchive": 282 g.Fgenf(w, "pulumi.AssetArchive(%.v)", expr.Args[0]) 283 case "fileAsset": 284 g.Fgenf(w, "pulumi.FileAsset(%.v)", expr.Args[0]) 285 case "stringAsset": 286 g.Fgenf(w, "pulumi.StringAsset(%.v)", expr.Args[0]) 287 case "remoteAsset": 288 g.Fgenf(w, "pulumi.remoteAsset(%.v)", expr.Args[0]) 289 case "filebase64": 290 g.Fgenf(w, "(lambda path: base64.b64encode(open(path).read().encode()).decode())(%.v)", expr.Args[0]) 291 case "filebase64sha256": 292 // Assuming the existence of the following helper method 293 g.Fgenf(w, "computeFilebase64sha256(%v)", expr.Args[0]) 294 case pcl.Invoke: 295 pkg, module, fn, diags := functionName(expr.Args[0]) 296 contract.Assert(len(diags) == 0) 297 if module != "" { 298 module = "." + module 299 } 300 name := fmt.Sprintf("%s%s.%s", pkg, module, PyName(fn)) 301 302 isOut := pcl.IsOutputVersionInvokeCall(expr) 303 if isOut { 304 name = fmt.Sprintf("%s_output", name) 305 } 306 307 if len(expr.Args) == 1 { 308 g.Fprintf(w, "%s()", name) 309 return 310 } 311 312 optionsBag := "" 313 if len(expr.Args) == 3 { 314 var buf bytes.Buffer 315 g.Fgenf(&buf, ", %.v", expr.Args[2]) 316 optionsBag = buf.String() 317 } 318 319 g.Fgenf(w, "%s(", name) 320 321 genFuncArgs := func(objectExpr *model.ObjectConsExpression) { 322 indenter := func(f func()) { f() } 323 if len(objectExpr.Items) > 1 { 324 indenter = g.Indented 325 } 326 indenter(func() { 327 for i, item := range objectExpr.Items { 328 // Ignore non-literal keys 329 key, ok := item.Key.(*model.LiteralValueExpression) 330 if !ok || !key.Value.Type().Equals(cty.String) { 331 continue 332 } 333 keyVal := PyName(key.Value.AsString()) 334 if i == 0 { 335 g.Fgenf(w, "%s=%.v", keyVal, item.Value) 336 } else { 337 g.Fgenf(w, ",\n%s%s=%.v", g.Indent, keyVal, item.Value) 338 } 339 } 340 }) 341 } 342 343 switch arg := expr.Args[1].(type) { 344 case *model.FunctionCallExpression: 345 if argsObject, ok := arg.Args[0].(*model.ObjectConsExpression); ok { 346 genFuncArgs(argsObject) 347 } 348 349 case *model.ObjectConsExpression: 350 genFuncArgs(arg) 351 } 352 353 g.Fgenf(w, "%v)", optionsBag) 354 case "join": 355 g.Fgenf(w, "%.16v.join(%v)", expr.Args[0], expr.Args[1]) 356 case "length": 357 g.Fgenf(w, "len(%.v)", expr.Args[0]) 358 case "lookup": 359 if len(expr.Args) == 3 { 360 g.Fgenf(w, "(lambda v, def: v if v is not None else def)(%.16v[%.v], %.v)", 361 expr.Args[0], expr.Args[1], expr.Args[2]) 362 } else { 363 g.Fgenf(w, "%.16v[%.v]", expr.Args[0], expr.Args[1]) 364 } 365 case "range": 366 g.Fprint(w, "range(") 367 for i, arg := range expr.Args { 368 if i > 0 { 369 g.Fprint(w, ", ") 370 } 371 g.Fgenf(w, "%.v", arg) 372 } 373 g.Fprint(w, ")") 374 case "readFile": 375 g.Fgenf(w, "(lambda path: open(path).read())(%.v)", expr.Args[0]) 376 case "readDir": 377 g.Fgenf(w, "os.listdir(%.v)", expr.Args[0]) 378 case "secret": 379 g.Fgenf(w, "pulumi.secret(%v)", expr.Args[0]) 380 case "split": 381 g.Fgenf(w, "%.16v.split(%.v)", expr.Args[1], expr.Args[0]) 382 case "toBase64": 383 g.Fgenf(w, "base64.b64encode(%.16v.encode()).decode()", expr.Args[0]) 384 case "fromBase64": 385 g.Fgenf(w, "base64.b64decode(%.16v.encode()).decode()", expr.Args[0]) 386 case "toJSON": 387 g.Fgenf(w, "json.dumps(%.v)", expr.Args[0]) 388 case "sha1": 389 g.Fgenf(w, "hashlib.sha1(%v.encode()).hexdigest()", expr.Args[0]) 390 case "project": 391 g.Fgen(w, "pulumi.get_project()") 392 case "stack": 393 g.Fgen(w, "pulumi.get_stack()") 394 case "cwd": 395 g.Fgen(w, "os.getcwd()") 396 default: 397 var rng hcl.Range 398 if expr.Syntax != nil { 399 rng = expr.Syntax.Range() 400 } 401 g.genNYI(w, "FunctionCallExpression: %v (%v)", expr.Name, rng) 402 } 403 } 404 405 func (g *generator) GenIndexExpression(w io.Writer, expr *model.IndexExpression) { 406 g.Fgenf(w, "%.16v[%.v]", expr.Collection, expr.Key) 407 } 408 409 type runeWriter interface { 410 WriteRune(c rune) (int, error) 411 } 412 413 // nolint: errcheck 414 func (g *generator) genEscapedString(w runeWriter, v string, escapeNewlines, escapeBraces bool) { 415 for _, c := range v { 416 switch c { 417 case '\n': 418 if escapeNewlines { 419 w.WriteRune('\\') 420 c = 'n' 421 } 422 case '"', '\\': 423 if escapeNewlines { 424 w.WriteRune('\\') 425 } 426 case '{', '}': 427 if escapeBraces { 428 w.WriteRune(c) 429 } 430 } 431 w.WriteRune(c) 432 } 433 } 434 435 func (g *generator) genStringLiteral(w io.Writer, quotes, v string) { 436 builder := &strings.Builder{} 437 438 builder.WriteString(quotes) 439 escapeNewlines := quotes == `"` || quotes == `'` 440 g.genEscapedString(builder, v, escapeNewlines, false) 441 builder.WriteString(quotes) 442 443 g.Fgenf(w, "%s", builder.String()) 444 } 445 446 func (g *generator) GenLiteralValueExpression(w io.Writer, expr *model.LiteralValueExpression) { 447 typ := expr.Type() 448 if cns, ok := typ.(*model.ConstType); ok { 449 typ = cns.Type 450 } 451 452 switch typ { 453 case model.BoolType: 454 if expr.Value.True() { 455 g.Fgen(w, "True") 456 } else { 457 g.Fgen(w, "False") 458 } 459 case model.NoneType: 460 g.Fgen(w, "None") 461 case model.NumberType: 462 bf := expr.Value.AsBigFloat() 463 if i, acc := bf.Int64(); acc == big.Exact { 464 g.Fgenf(w, "%d", i) 465 } else { 466 f, _ := bf.Float64() 467 g.Fgenf(w, "%g", f) 468 } 469 case model.StringType: 470 quotes := g.quotes[expr] 471 g.genStringLiteral(w, quotes, expr.Value.AsString()) 472 default: 473 contract.Failf("unexpected literal type in GenLiteralValueExpression: %v (%v)", expr.Type(), 474 expr.SyntaxNode().Range()) 475 } 476 } 477 478 func (g *generator) GenObjectConsExpression(w io.Writer, expr *model.ObjectConsExpression) { 479 g.genObjectConsExpression(w, expr, expr.Type()) 480 } 481 482 func (g *generator) genObjectConsExpression(w io.Writer, expr *model.ObjectConsExpression, destType model.Type) { 483 typeName := g.argumentTypeName(expr, destType) // Example: aws.s3.BucketLoggingArgs 484 if typeName != "" { 485 // If a typeName exists, treat this as an Input Class e.g. aws.s3.BucketLoggingArgs(key="value", foo="bar", ...) 486 if len(expr.Items) == 0 { 487 g.Fgenf(w, "%s()", typeName) 488 } else { 489 g.Fgenf(w, "%s(\n", typeName) 490 g.Indented(func() { 491 for _, item := range expr.Items { 492 g.Fgenf(w, "%s", g.Indent) 493 lit := item.Key.(*model.LiteralValueExpression) 494 g.Fprint(w, PyName(lit.Value.AsString())) 495 g.Fgenf(w, "=%.v,\n", item.Value) 496 } 497 }) 498 g.Fgenf(w, "%s)", g.Indent) 499 } 500 } else { 501 // Otherwise treat this as an untyped dictionary e.g. {"key": "value", "foo": "bar", ...} 502 if len(expr.Items) == 0 { 503 g.Fgen(w, "{}") 504 } else { 505 g.Fgen(w, "{") 506 g.Indented(func() { 507 for _, item := range expr.Items { 508 g.Fgenf(w, "\n%s%.v: %.v,", g.Indent, item.Key, item.Value) 509 } 510 }) 511 g.Fgenf(w, "\n%s}", g.Indent) 512 } 513 } 514 } 515 516 func (g *generator) genRelativeTraversal(w io.Writer, traversal hcl.Traversal, parts []model.Traversable) { 517 for _, traverser := range traversal { 518 var key cty.Value 519 switch traverser := traverser.(type) { 520 case hcl.TraverseAttr: 521 key = cty.StringVal(traverser.Name) 522 case hcl.TraverseIndex: 523 key = traverser.Key 524 default: 525 contract.Failf("unexpected traverser of type %T (%v)", traverser, traverser.SourceRange()) 526 } 527 528 switch key.Type() { 529 case cty.String: 530 keyVal := key.AsString() 531 contract.Assert(isLegalIdentifier(keyVal)) 532 g.Fgenf(w, ".%s", keyVal) 533 case cty.Number: 534 idx, _ := key.AsBigFloat().Int64() 535 g.Fgenf(w, "[%d]", idx) 536 default: 537 keyExpr := &model.LiteralValueExpression{Value: key} 538 diags := keyExpr.Typecheck(false) 539 contract.Ignore(diags) 540 541 g.Fgenf(w, "[%v]", keyExpr) 542 } 543 } 544 } 545 546 func (g *generator) GenRelativeTraversalExpression(w io.Writer, expr *model.RelativeTraversalExpression) { 547 g.Fgenf(w, "%.16v", expr.Source) 548 g.genRelativeTraversal(w, expr.Traversal, expr.Parts) 549 } 550 551 func (g *generator) GenScopeTraversalExpression(w io.Writer, expr *model.ScopeTraversalExpression) { 552 rootName := PyName(expr.RootName) 553 if _, ok := expr.Parts[0].(*model.SplatVariable); ok { 554 rootName = "__item" 555 } 556 557 g.Fgen(w, rootName) 558 g.genRelativeTraversal(w, expr.Traversal.SimpleSplit().Rel, expr.Parts) 559 } 560 561 func (g *generator) GenSplatExpression(w io.Writer, expr *model.SplatExpression) { 562 g.Fgenf(w, "[%.v for __item in %.v]", expr.Each, expr.Source) 563 } 564 565 func (g *generator) GenTemplateExpression(w io.Writer, expr *model.TemplateExpression) { 566 quotes := g.quotes[expr] 567 escapeNewlines := quotes == `"` || quotes == `'` 568 569 prefix, escapeBraces := "", false 570 for _, part := range expr.Parts { 571 if lit, ok := part.(*model.LiteralValueExpression); !ok || !model.StringType.AssignableFrom(lit.Type()) { 572 prefix, escapeBraces = "f", true 573 break 574 } 575 } 576 577 b := bufio.NewWriter(w) 578 defer b.Flush() 579 580 g.Fprintf(b, "%s%s", prefix, quotes) 581 for _, expr := range expr.Parts { 582 if lit, ok := expr.(*model.LiteralValueExpression); ok && model.StringType.AssignableFrom(lit.Type()) { 583 g.genEscapedString(b, lit.Value.AsString(), escapeNewlines, escapeBraces) 584 } else { 585 g.Fgenf(b, "{%.v}", expr) 586 } 587 } 588 g.Fprint(b, quotes) 589 } 590 591 func (g *generator) GenTemplateJoinExpression(w io.Writer, expr *model.TemplateJoinExpression) { 592 g.genNYI(w, "TemplateJoinExpression") 593 } 594 595 func (g *generator) GenTupleConsExpression(w io.Writer, expr *model.TupleConsExpression) { 596 switch len(expr.Expressions) { 597 case 0: 598 g.Fgen(w, "[]") 599 case 1: 600 g.Fgenf(w, "[%.v]", expr.Expressions[0]) 601 default: 602 g.Fgen(w, "[") 603 g.Indented(func() { 604 for _, v := range expr.Expressions { 605 g.Fgenf(w, "\n%s%.v,", g.Indent, v) 606 } 607 }) 608 g.Fgen(w, "\n", g.Indent, "]") 609 } 610 } 611 612 func (g *generator) GenUnaryOpExpression(w io.Writer, expr *model.UnaryOpExpression) { 613 opstr, precedence := "", g.GetPrecedence(expr) 614 switch expr.Operation { 615 case hclsyntax.OpLogicalNot: 616 opstr = "not " 617 case hclsyntax.OpNegate: 618 opstr = "-" 619 } 620 g.Fgenf(w, "%[2]v%.[1]*[3]v", precedence, opstr, expr.Operand) 621 }