Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Loops - code generation and emit limit checks #1521

Merged
merged 6 commits into from
Feb 25, 2021

Conversation

majastrz
Copy link
Member

@majastrz majastrz commented Feb 11, 2021

Module and resource loops should now be fully functional. Property loops are supported as well but cannot be nested with other property loops due to runtime limitations. A property loop can be nested inside a resource or module loop, however. This fixes #185.

Additional changes:

  • Accessible symbol completions now include loop variables in the scope of the completion request.
  • Updated language spec to remove brackets following the resource and module keyword when using module/resource loops.
  • The range() function return type is now int[] instead of array. This improves the quality of hovers when using loops with the range.
  • Separated language scope declaration from binding. This fixes a stack overflow with loop expressions like [for thing in thing: thing]. (The change means that the loop variable refs inside the loop body ObjectSyntax continue binding in the loop scope, but the the array expression binds using the parent scope.)

The following limitations remain:

  • No variable or output loops yet. Outputs coming in the next PR. Variables some time later...
  • The index variable syntax [for (thing, index) in things: {...}] is not yet implemented. Coming after this PR.
  • Filtering loops is not yet implemented. Coming soon.
  • No snippet is available to simplify loop authoring. Coming after this PR.
  • We are blocking property loops where the property name is an expression. I can't think of a legitimate scenario for this. We can implement if feedback justifies it.
  • Outside of dependsOn, we are blocking references to a module/resource collection without an indexer. This should be a rare case, but will be relaxed in a future PR.
  • No loop decompilation... yet.

@codecov-io
Copy link

codecov-io commented Feb 11, 2021

Codecov Report

Merging #1521 (1542b15) into main (868828c) will increase coverage by 0.10%.
The diff coverage is 100.00%.

Impacted file tree graph

@@            Coverage Diff             @@
##             main    #1521      +/-   ##
==========================================
+ Coverage   94.94%   95.05%   +0.10%     
==========================================
  Files         356      362       +6     
  Lines       19403    19804     +401     
  Branches       14       13       -1     
==========================================
+ Hits        18422    18824     +402     
+ Misses        981      980       -1     
Flag Coverage Δ
dotnet 95.51% <100.00%> (+0.09%) ⬆️
typescript 27.40% <ø> (+0.20%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Impacted Files Coverage Δ
.../Bicep.Core.UnitTests/TypeSystem/ArrayTypeTests.cs 100.00% <ø> (ø)
.../Bicep.Core.UnitTests/TypeSystem/UnionTypeTests.cs 100.00% <ø> (ø)
src/Bicep.Core/DataFlow/DataFlowAnalyzer.cs 92.30% <ø> (ø)
...icep.Core/DataFlow/LocalSymbolDependencyVisitor.cs 100.00% <ø> (ø)
src/Bicep.Core/Diagnostics/DiagnosticBuilder.cs 100.00% <ø> (ø)
src/Bicep.Core/Emit/EmitHelpers.cs 88.88% <ø> (+22.22%) ⬆️
src/Bicep.Core/Emit/EmitLimitationCalculator.cs 100.00% <ø> (ø)
src/Bicep.Core/Emit/EmitterContext.cs 100.00% <ø> (ø)
src/Bicep.Core/Emit/ExpressionConverter.cs 96.30% <ø> (-0.69%) ⬇️
src/Bicep.Core/Emit/ExpressionEmitter.cs 98.70% <ø> (-0.16%) ⬇️
... and 67 more

@majastrz majastrz force-pushed the majastrz/go-round-and-round branch 3 times, most recently from ac95b81 to 81237ac Compare February 17, 2021 03:20
@majastrz majastrz force-pushed the majastrz/go-round-and-round branch 5 times, most recently from 956934d to b3fe81c Compare February 24, 2021 07:07
@majastrz majastrz changed the title [WIP] Loops - codegen and emit limit checks Loops - code generation and emit limit checks Feb 24, 2021
@majastrz majastrz self-assigned this Feb 24, 2021
@majastrz majastrz marked this pull request as ready for review February 24, 2021 07:19
@majastrz
Copy link
Member Author

Note: Not implemented yet.

I will update this when more loop PRs go in.


Refers to: docs/spec/loops.md:2 in b3fe81c. [](commit_id = b3fe81c, deletion_comment = False)

@majastrz
Copy link
Member Author

majastrz commented Feb 24, 2021

        public ErrorDiagnostic DirectAccessToCollectionNotSupported() => new(

When this restriction is relaxed, this error will become more specific. #ByDesign


Refers to: src/Bicep.Core/Diagnostics/DiagnosticBuilder.cs:823 in 9453dc0. [](commit_id = 9453dc0, deletion_comment = False)

- range() now returns int[] instead of array
- better union type array formatting
- loop variable completions
- removed brackets from loops spec
@majastrz majastrz force-pushed the majastrz/go-round-and-round branch from b3fe81c to 9453dc0 Compare February 24, 2021 07:27
@majastrz
Copy link
Member Author

    /// Works for single resources and resource collections.

I originally had separate methods for single and collection resources, but pretty much everywhere I would have to check if a symbol is a collection and then call one or the other, so it made sense to collapse into one.


Refers to: src/Bicep.Core/Emit/EmitHelpers.cs:14 in 9453dc0. [](commit_id = 9453dc0, deletion_comment = False)

@majastrz
Copy link
Member Author

    public LanguageExpression GetFullyQualifiedResourceId(ResourceSymbol resourceSymbol, SyntaxBase newContext)

I didn't know what to call this.

When we have resource/module symbol references via variable access, we are taking a name expression of a resource to construct a reference() or *resourceId() function expression. When the name expression contains local variables (say, a loop item or index var) and is used to construct something in another context outside the loop body like dependsOn or reference() call in some other resource, the standard replacement with [copyIndex()] produces an incorrect result. If we pretend we moved the expression into a new context, we can use the new DataFlowAnalyzer to figure out which locals become inaccessible.


Refers to: src/Bicep.Core/Emit/ExpressionConverter.cs:312 in 9453dc0. [](commit_id = 9453dc0, deletion_comment = False)

@majastrz
Copy link
Member Author

    public LanguageExpression GetFullyQualifiedResourceId(ResourceSymbol resourceSymbol, SyntaxBase newContext)

This was also the least "viral" option out of a bunch that I tried.


In reply to: 784865454 [](ancestors = 784865454)


Refers to: src/Bicep.Core/Emit/ExpressionConverter.cs:312 in 9453dc0. [](commit_id = 9453dc0, deletion_comment = False)

@majastrz
Copy link
Member Author

majastrz commented Feb 24, 2021

                    var visitor = new LanguageExpressionVisitor

This "solution" predates my immutable converter context solution. I'm going to try to leverage this here as well to remove the hacky expression visitor. #Resolved


Refers to: src/Bicep.Core/Emit/ExpressionEmitter.cs:194 in 9453dc0. [](commit_id = 9453dc0, deletion_comment = False)

@majastrz
Copy link
Member Author

                this.diagnosticWriter.Write(DiagnosticBuilder.ForPosition(syntax).DirectAccessToCollectionNotSupported());

This effectively blocks directly assigning a resource/module collection to a property in a resource body without an indexer. You can use a resource/module collection with an array indexer pretty much anywhere, though. And we do allow the resource/module collection without an indexer in dependsOn of course. I will relax this in another PR because there are cases where I can generate something reasonable when this happens.


Refers to: src/Bicep.Core/Emit/ForSyntaxValidatorVisitor.cs:86 in 9453dc0. [](commit_id = 9453dc0, deletion_comment = False)

@majastrz
Copy link
Member Author

    public LocalVariableSyntax DeclaringLocalVariable => (LocalVariableSyntax) this.DeclaringSyntax;

Just making it consistent with other symbols.


Refers to: src/Bicep.Core/Semantics/LocalVariableSymbol.cs:17 in 9453dc0. [](commit_id = 9453dc0, deletion_comment = False)

@majastrz
Copy link
Member Author

                // the same type may be returned as a child of multiple symbols

Seems like a good argument for types to stop being symbols :)


Refers to: src/Bicep.Core/Semantics/SymbolHierarchy.cs:53 in 9453dc0. [](commit_id = 9453dc0, deletion_comment = False)

@@ -129,7 +129,7 @@ resource secret 'Microsoft.KeyVault/vaults/secrets@2019-09-01' = {
/*
TODO: Replace the first secret workaround above with this once we have loops
Copy link
Member

@anthony-c-martin anthony-c-martin Feb 24, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you uncomment this code now then? #Resolved

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good pt (minus the extra newline).


In reply to: 582064628 [](ancestors = 582064628)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Coming in next commit.


In reply to: 582208509 [](ancestors = 582208509,582064628)

Copy link
Member Author

@majastrz majastrz Feb 25, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. #Resolved

@@ -2,7 +2,7 @@
"profiles": {
Copy link
Member

@anthony-c-martin anthony-c-martin Feb 24, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we just gitignore this file? #Resolved

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I think so. I'll make the change in this PR.


In reply to: 582065438 [](ancestors = 582065438)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Coming in next commit


In reply to: 582209011 [](ancestors = 582209011,582065438)

Copy link
Member Author

@majastrz majastrz Feb 25, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. #Resolved

@@ -5,7 +5,7 @@
"detail": "dependsOn",
"documentation": {
"kind": "markdown",
"value": "Type: `resource | module[]` \nWrite-only property \n"
"value": "Type: `(module[] | resource | module | resource[])[]` \nWrite-only property \n"
Copy link
Member

@anthony-c-martin anthony-c-martin Feb 24, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The ordering is a little weird - presumably this is what's shown to the user. Can we order this somehow? #WontFix

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be able to. I assume you're looking for resource | resource[] | module | module[]?


In reply to: 582066499 [](ancestors = 582066499)

Copy link
Member

@anthony-c-martin anthony-c-martin Feb 24, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Either that or resource | module | resource[] | module[] - both seem like good options. #WontFix

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's actually more complicated than I thought because resource | module part is one member of the union. (Due to how ResourceRef type is implemented.) We can improve this later.


In reply to: 582234791 [](ancestors = 582234791)

Copy link
Member

@anthony-c-martin anthony-c-martin Feb 24, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's actually more complicated than I thought because resource | module part is one member of the union. (Due to how ResourceRef type is implemented.) We can improve this later.

In reply to: 582234791 [](ancestors = 582234791)

So it's really ((resource | module) | resource[] | module[]) flattened into one union? #Resolved

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah unfortunately. I really wish we didn't do it that way now :(


In reply to: 582331000 [](ancestors = 582331000)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I'll add a special case to enclose it in parentheses for now.


In reply to: 582332560 [](ancestors = 582332560,582331000)


private HashSet<LocalVariableSymbol> SymbolDependencies { get; } = new();

public static ISet<LocalVariableSymbol> GetLocalSymbolDependencies(SemanticModel semanticModel, SyntaxBase syntax)
Copy link
Member

@anthony-c-martin anthony-c-martin Feb 24, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Worth making immutable? #Resolved

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah good pt.


In reply to: 582071733 [](ancestors = 582071733)

Copy link
Member Author

@majastrz majastrz Feb 25, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. #Resolved

@@ -792,7 +792,7 @@ public ErrorDiagnostic ArgumentCountMismatch(int argumentCount, int mininumArgum
public ErrorDiagnostic LoopsNotSupported() => new(
Copy link
Member

@anthony-c-martin anthony-c-martin Feb 24, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nit] if we're calling them for-expressions in the message, then would be good to update the name of this function #Resolved

Copy link
Member Author

@majastrz majastrz Feb 25, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. #Resolved

}

public LanguageExpression GetFullyQualifiedResourceId(ModuleSymbol moduleSymbol)
public LanguageExpression GetFullyQualifiedResourceId(ModuleSymbol moduleSymbol, SyntaxBase newContext)
Copy link
Member

@anthony-c-martin anthony-c-martin Feb 24, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is newContext used inside ScopeHelper.FormatFullyQualifiedResourceId()? Seemed like it was being passed recursively but I couldn't see other usages. #Resolved

Copy link
Member Author

@majastrz majastrz Feb 25, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call! I was able to remove a few occurrences of it. #Resolved

Comment on lines 189 to 193
// this is a giant hack
// the named copy index in the serialized expression is incorrect
// because the object syntax here does not match the JSON equivalent due to the presence of { "value": ... } wrappers
// for now, we will manually replace the copy index in the converted expression
// this also will not work for nested property loops
Copy link
Member

@anthony-c-martin anthony-c-martin Feb 24, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the less-hacky solution? Will you create an issue for it? #Resolved

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will try it in this PR - basically use the local symbol replacement logic to do it instead of the ExpressionVisitor.


In reply to: 582201312 [](ancestors = 582201312)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Decided to leave as is for now. Will have to revisit when we support nested property loops.


In reply to: 582207976 [](ancestors = 582207976,582201312)

if (propertyLookup.Contains(true))
{
// we have properties whose value is a for-expression
this.EmitProperty("copy", () =>
Copy link
Member

@anthony-c-martin anthony-c-martin Feb 24, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we block conversion of properties in Bicep named "copy"? Is there any option for codegen if we have a property named "copy"? (I guess that's somewhat orthogonal to this PR) #Resolved

Copy link
Member Author

@majastrz majastrz Feb 24, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch! Expressions are supported in the name property of a property copy loop, so I could emit name: format('copy'), but will need to verify that it works.


In reply to: 582202923 [](ancestors = 582202923)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, I realized the inverse is the problem. A property copy loop for a property called copy should probably be ok because name on the copy object could be set to "copy", but a regular resource with a property called "copy" is going to cause problems. That is a preexisting problem, though.


In reply to: 582219558 [](ancestors = 582219558,582202923)

Comment on lines +174 to +177
this.EmitPropertyWithTransform(
"count",
syntax.Expression,
arrayExpression => new FunctionExpression("length", new[] { arrayExpression }, Array.Empty<LanguageExpression>()));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Neat!

Comment on lines 315 to 324
if (body is IfConditionSyntax ifCondition)
{
body = ifCondition.Body;
emitter.EmitProperty("condition", ifCondition.ConditionExpression);
}
else if (body is ForSyntax @for)
{
body = @for.Body;
emitter.EmitProperty("copy", () => emitter.EmitCopyObject(resourceSymbol.Name, @for, input: null));
}
Copy link
Member

@anthony-c-martin anthony-c-martin Feb 24, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't body be both for & if? #Resolved

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not right now. That's one of the things that is coming right after 0.3. Forgot to mention it in the description.


In reply to: 582212603 [](ancestors = 582212603)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, never mind. It's in the description (the "no filtering" bit).


In reply to: 582231890 [](ancestors = 582231890,582212603)

@@ -18,7 +18,7 @@ public static class BaselineHelper

private const string SetBaseLineSettingName = "SetBaseLine";

public static bool ShouldSetBaseline(TestContext testContext) =>
public static bool ShouldSetBaseline(TestContext testContext) =>
Copy link
Contributor

@shenglol shenglol Feb 24, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nit] Trailing whitespace. #Resolved

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Heh. I've been adding true || to make the tests run in baseline update mode in VS. Will remove in next commit.


In reply to: 582214047 [](ancestors = 582214047)


namespace Bicep.Core.Semantics
{
public class SymbolHierarchy
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does it mean for a symbol to be a parent/child of another?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a hierarchy. FileSymbol is a parent of all the global declarations, for example.


In reply to: 582221724 [](ancestors = 582221724)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After our discussion I started to think more on this. I'm starting to view it as less a hierarchy of symbols but more of a hierarchy of semantically relevant things with some of them being symbols. I'm also starting to realize that too many things are called Symbols in the semantic model.


In reply to: 582234192 [](ancestors = 582234192,582221724)

@@ -14,5 +14,10 @@ public TypedArrayType(ITypeReference itemReference, TypeSymbolValidationFlags va
public override ITypeReference Item { get; }

public override TypeSymbolValidationFlags ValidationFlags { get; }

private static string FormatTypeName(ITypeReference itemReference) =>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

@shenglol
Copy link
Contributor

shenglol commented Feb 24, 2021

Should we handle the case where a variable is used as a ForSyntax body (block it or support it)? This doesn't emit any diagnostics and causes unhandled exception upon transpiling:
image

UPDATE:
Any expression of type object can be used as a ForSyntax body. I wonder if we should perform stricter type checking here:
image

#Resolved

@majastrz
Copy link
Member Author

majastrz commented Feb 24, 2021

    private readonly SymbolHierarchy hierarchy;

Need to move it to the semantic model so we don't initialize it in multiple places. #Resolved


Refers to: src/Bicep.Core/DataFlow/DataFlowAnalyzer.cs:15 in 9453dc0. [](commit_id = 9453dc0, deletion_comment = False)

@majastrz
Copy link
Member Author

majastrz commented Feb 25, 2021

Should we handle the case where a variable is used as a ForSyntax body (block it or support it)? This doesn't emit any diagnostics and causes unhandled exception upon transpiling:
image

UPDATE:
Any expression of type object can be used as a ForSyntax body. I wonder if we should perform stricter type checking here:
image

I ended up blocking this at the parser level for a couple of reasons:

  • The resource loop syntax in the JSON requires an object literal (copy is a property of it)
  • Consistency with how we handle it for conditions and regular resource bodies.

The fix should take care of both issues. #Resolved


resource secret 'Microsoft.KeyVault/vaults/secrets@2019-09-01' = {
name: '${vault.name}/${firstSecretName}'
resource secrets 'Microsoft.KeyVault/vaults/secrets@2018-02-14' = [for secret in secretsObject.secrets: {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this be simplified to:

resource secrets 'Microsoft.KeyVault/vaults/secrets@2018-02-14' = [for secret in secretsObject.secrets: {
  name: '${vault.name}/${secret.secretName}'
  properties: {
    value: secret.secretValue
  }
}]

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will take a look in the next one.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Took a look - yeah can totally be simplified.

Copy link
Member

@anthony-c-martin anthony-c-martin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚀🚀🚀🚀🚀

Copy link
Contributor

@shenglol shenglol left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

:shipit:

@majastrz majastrz merged commit a032b57 into main Feb 25, 2021
@majastrz majastrz deleted the majastrz/go-round-and-round branch February 25, 2021 23:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

implement loops/list comprehension
4 participants