diff --git a/zcl/zclsyntax/expression.go b/zcl/zclsyntax/expression.go index dcefbfe..2ffda27 100644 --- a/zcl/zclsyntax/expression.go +++ b/zcl/zclsyntax/expression.go @@ -587,7 +587,152 @@ type ForExpr struct { } func (e *ForExpr) Value(ctx *zcl.EvalContext) (cty.Value, zcl.Diagnostics) { - panic("ForExpr.Value not yet implemented") + var diags zcl.Diagnostics + + collVal, collDiags := e.CollExpr.Value(ctx) + diags = append(diags, collDiags...) + + if collVal.IsNull() { + diags = append(diags, &zcl.Diagnostic{ + Severity: zcl.DiagError, + Summary: "Iteration over null value", + Detail: "A null value cannot be used as the collection in a 'for' expression.", + Subject: e.CollExpr.Range().Ptr(), + Context: &e.SrcRange, + }) + return cty.DynamicVal, diags + } + if !collVal.IsKnown() { + return cty.DynamicVal, diags + } + if !collVal.CanIterateElements() { + diags = append(diags, &zcl.Diagnostic{ + Severity: zcl.DiagError, + Summary: "Iteration over non-iterable value", + Detail: fmt.Sprintf( + "A value of type %s cannot be used as the collection in a 'for' expression.", + collVal.Type().FriendlyName(), + ), + Subject: e.CollExpr.Range().Ptr(), + Context: &e.SrcRange, + }) + return cty.DynamicVal, diags + } + + childCtx := ctx.NewChild() + childCtx.Variables = map[string]cty.Value{} + + if e.KeyExpr != nil { + // Producing an object + vals := map[string]cty.Value{} + + it := collVal.ElementIterator() + + known := true + for it.Next() { + k, v := it.Element() + if e.KeyVar != "" { + childCtx.Variables[e.KeyVar] = k + } + childCtx.Variables[e.ValVar] = v + + if e.CondExpr != nil { + includeRaw, condDiags := e.CondExpr.Value(childCtx) + diags = append(diags, condDiags...) + if includeRaw.IsNull() { + if known { + diags = append(diags, &zcl.Diagnostic{ + Severity: zcl.DiagError, + Summary: "Condition is null", + Detail: "The value of the 'if' clause must not be null.", + Subject: e.CondExpr.Range().Ptr(), + Context: &e.SrcRange, + }) + } + known = false + continue + } + if !includeRaw.IsKnown() { + // We will eventually return DynamicVal, but we'll continue + // iterating in case there are other diagnostics to gather + // for later elements. + known = false + continue + } + + include, err := convert.Convert(includeRaw, cty.Bool) + if err != nil { + if known { + diags = append(diags, &zcl.Diagnostic{ + Severity: zcl.DiagError, + Summary: "Invalid 'for' condition", + Detail: fmt.Sprintf("The 'if' clause value is invalid: %s.", err.Error()), + Subject: e.CondExpr.Range().Ptr(), + Context: &e.SrcRange, + }) + } + known = false + continue + } + + if include.False() { + // Skip this element + continue + } + } + + keyRaw, keyDiags := e.KeyExpr.Value(childCtx) + diags = append(diags, keyDiags...) + if keyRaw.IsNull() { + if known { + diags = append(diags, &zcl.Diagnostic{ + Severity: zcl.DiagError, + Summary: "Invalid object key", + Detail: "Key expression in 'for' expression must not produce a null value.", + Subject: e.KeyExpr.Range().Ptr(), + Context: &e.SrcRange, + }) + } + known = false + continue + } + if !keyRaw.IsKnown() { + known = false + continue + } + + key, err := convert.Convert(keyRaw, cty.String) + if err != nil { + if known { + diags = append(diags, &zcl.Diagnostic{ + Severity: zcl.DiagError, + Summary: "Invalid object key", + Detail: fmt.Sprintf("The key expression produced an invalid result: %s.", err.Error()), + Subject: e.KeyExpr.Range().Ptr(), + Context: &e.SrcRange, + }) + } + known = false + continue + } + + val, valDiags := e.ValExpr.Value(childCtx) + diags = append(diags, valDiags...) + vals[key.AsString()] = val + } + + if !known { + return cty.DynamicVal, diags + } + + return cty.ObjectVal(vals), diags + + } else { + // Producing a tuple + vals := []cty.Value{} + panic("for into a tuple is not yet implemented") + return cty.TupleVal(vals), diags + } } func (e *ForExpr) walkChildNodes(w internalWalkFunc) { diff --git a/zcl/zclsyntax/expression_test.go b/zcl/zclsyntax/expression_test.go index c16e14e..44d6193 100644 --- a/zcl/zclsyntax/expression_test.go +++ b/zcl/zclsyntax/expression_test.go @@ -369,6 +369,71 @@ upper( 0, }, + { + `{for k, v in {hello: "world"}: k => v if k == "hello"}`, + nil, + cty.ObjectVal(map[string]cty.Value{ + "hello": cty.StringVal("world"), + }), + 0, + }, + { + `{for k, v in ["world"]: k => v if k == 0}`, + nil, + cty.ObjectVal(map[string]cty.Value{ + "0": cty.StringVal("world"), + }), + 0, + }, + { + `{for v in ["world"]: v => v}`, + nil, + cty.ObjectVal(map[string]cty.Value{ + "world": cty.StringVal("world"), + }), + 0, + }, + { + `{for k, v in {hello: "world"}: k => v if k == "foo"}`, + nil, + cty.EmptyObjectVal, + 0, + }, + { + `{for k, v in {hello: "world"}: 5 => v}`, + nil, + cty.ObjectVal(map[string]cty.Value{ + "5": cty.StringVal("world"), + }), + 0, + }, + { + `{for k, v in {hello: "world"}: [] => v}`, + nil, + cty.DynamicVal, + 1, // key expression has the wrong type + }, + { + `{for k, v in {hello: "world"}: k => k if k == "hello"}`, + nil, + cty.ObjectVal(map[string]cty.Value{ + "hello": cty.StringVal("hello"), + }), + 0, + }, + { + `{for k, v in {hello: "world"}: k => foo}`, + &zcl.EvalContext{ + Variables: map[string]cty.Value{ + "foo": cty.StringVal("foo"), + }, + }, + cty.ObjectVal(map[string]cty.Value{ + "hello": cty.StringVal("foo"), + }), + 0, + }, + { `["hello"][0]`, nil,