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

Proposal: Alloy syntax conditionals #822

Open
rfratto opened this issue May 10, 2024 · 10 comments
Open

Proposal: Alloy syntax conditionals #822

rfratto opened this issue May 10, 2024 · 10 comments
Labels
proposal A proposal for new functionality. type/syntax Alloy configuration syntax issues

Comments

@rfratto
Copy link
Member

rfratto commented May 10, 2024

Background

Conditionals for the Alloy syntax were initially proposed by @tpaschalis on Dec 20, 2022 (#157).

In Alloy's current state, conditionals would mainly reduce the need for a templating engine when deploying Alloy configs statically. While this is an interesting use case, it's difficult to justify over other priorities, as a templating engine can still be used.

However, when considered alongside other proposals like #156, conditionals become a requirement for dynamic transformation and data routing, as acting on streams of data is not something that can be achieved with templating engines. Imagine a hypothetical prometheus.route component that can inspect incoming metrics and route to different components:

prometheus.scrape "default" {
  targets    = ...
  forward_to = [prometheus.route.prod.receiver]
}

// prometheus.route inspects incoming metrics and uses their 
// labels to determine which component to forward them to. 
prometheus.route "prod" {
  forward_func = func(metric) => (
    if metric.namespace == "prod" then [prometheus.remote_write.prod.receiver]
    else if metric.namespace == "qa" then [prometheus.remote_write.qa.receiver]
    else [prometheus.remote_write.dev.receiver] 
    end 
  )
}

prometheus.remote_write "prod" { ... }
prometheus.remote_write "qa" { ... }
prometheus.remote_write "dev" { ... }

Even though conditionals are difficult to prioritize based on current functionality, adding support for conditionals now will allow us to build a framework for more advanced pipelines and set a precedence for how we can explore syntactical experiments using the --stability.level flag.

This proposal supersedes #157.

Please use reactions to vote on this proposal, with 👍 to signify support and 👎 to signify disapproval. When disapproving, a comment would be appreciated so we can discuss and refine the proposal as needed.

Proposal

This proposal has the following goals:

  • Enable conditionals within an Alloy expression. Conditional statements (such as a conditional block) is out of scope.
  • Balance readability and terseness for long chains of conditionals.

Syntax

Following the exploration in #157, the (ENBF) grammar for conditionals is as follows:

CondExpr       = "if" Expression "then" Expression 
                 { CondElseIfExpr } [ CondElseExpr ] "end"
CondElseIfExpr = "else" "if" Expression "then" Expression 
CondElseExpr   = "else" Expression

// CondExpr is a type of PrimaryExpr, giving it one of the lowest precedences.
PrimaryExpr = LiteralValue | ArrayExpr | ObjectExpr | CondExpr  

Here, the tokens if, then, else, and end are selected to align with natural language for how developers communicate conditionals. This makes conditionals slightly more approachable for non-developers compared to more obscure tokens like ? and : and can aid in making conditionals easier to grok for first-time readers.

While the then keyword is more verbose, it is easier to parse correctly as it strictly divides the "if" part of the expression with the value.

Additionally, terminators are not required within the grammar for conditionals; this allows writers to place newlines arbitrarily for readability.

CondExpr is parsed at the same levels as literals. This allows CondExpr to appear anywhere as part of an expression. In some cases this may hurt readability, such as adding the result of two conditionals together. The formatter will be responsible for producing a readable output; see the Style and Formatting section of this proposal for more information.

The proposed grammar would parse all of these expressions:

// Single if 
if CONDITION then VALUE end 

// If/else 
if CONDITION then VALUE else VALUE end 

// If/Else-if chain 
if CONDITION then VALUE 
else if CONDITION then VALUE  
else if CONDITION then VALUE 
end 

// If/Else-if/Else 
if CONDITION then VALUE 
else if CONDITION then VALUE  
else if CONDITION then VALUE 
else VALUE 
end 

// Alternative form for more readibility 
if CONDITION then 
  VALUE 
else if CONDITION then 
  VALUE 
else if CONDITION then 
  VALUE 
else 
  VALUE 
end 

Evaluation

The syntax evaluator will iterate through the conditionals within the expression and return the first value where the condition is true. If no conditions are true, the else value will be returned. If there is no else value, the expression evaluates to null.

If a conditional in the overall expression is not a boolean value, the entire expression will fail to evaluate:

if a == 5 then 
  a * 2
else if 6 then // INVALID: expected 6 to be a boolean, got number 
  a * 3 
end 

Style and formatting

The formatter will mostly retain the conditional as written by the user, including with newlines. However, if a conditional expression is used as part of a more complex expression (binary operation, etc.), the formatter will surround the conditional in parenthesis for readability (if it is not already).

// Before formatting 
if a > 5 then a * 2 else a end + 10 

// After formatting 
(if a > 5 then a * 2 else a end) + 10 

When the formatter surrounds a conditional in parenthesis, if that conditional crosses multiple lines, the surrounding parenthesis are placed on new lines and the inner conditional is indented:

// Before formatting 
if a > 10 then [0, 1, 2]
else if a > 5 then [3, 4, 5]
else [6, 7, 8]
end [0]  

// After formatting 
(
  if a > 10 then [0, 1, 2]
  else if a > 5 then [3, 4, 5]
  else [6, 7, 8]
  end 
)[0] 

These parenthesis will be injected by the formatter if both of the following are true:

  • The conditional expression is not directly inside a parenthetical expression.
  • The conditional expression is part of a larger expression (such as a binary operator, unary operator, object access, etc.).

Delivery

While it is unlikely support for conditionals will be removed, they should not be released immediately as Generally Available to give us time to iterate and explore the syntax, and potentially make breaking changes if needed. The initial delivery of conditionals will be tagged as public preview and will require passing --stability.level=public-preview or --stability.level=experimental for conditional support.

If the stability level is not set properly, errors will be returned in three places:

  • The scanner will return an error when encountering a token specific to conditionals (if, else, then, end).
  • Additionally, the parser will return an error if a conditional-specific token is scanned.
  • Additionally, the syntax evaluator will return an error if a conditional expression found in the AST.

The bottom two conditions are technically unnecessary, but a defensive implementation is useful towards ensuring that users do not get access to a feature which is not GA yet.

The alloy fmt command disables these checks to allow conditionals to be parsed and formatted regardless of whether the Alloy instance supports them.

Alternatives considered

Refer to #157 for alternative approaches that were considered.

Acknowledgements

Credit to @tpaschalis for the work on the original proposal which formed the basis for this one.

@rfratto rfratto added the proposal A proposal for new functionality. label May 10, 2024
@rfratto rfratto added the type/syntax Alloy configuration syntax issues label May 10, 2024
@mattdurham
Copy link
Collaborator

Are user defined funcs a precursor to conditionals? Granted we can do them separately but I think they would have more limited use unless we defined func beforehand. Or is a better example:

prometheus.remote_write "test" {
  url = if env("ENVIRONMENT") == "QA" then "http://qa" else "http://dev" end,
}

@rfratto
Copy link
Member Author

rfratto commented May 10, 2024

Are user defined funcs a precursor to conditionals? Granted we can do them separately but I think they would have more limited use unless we defined func beforehand.

I touch on this in the proposal:

Even though conditionals are difficult to prioritize based on current functionality, adding support for conditionals now will allow us to build a framework for more advanced pipelines and set a precedence for how we can explore syntactical experiments using the --stability.level flag.

@wildum

This comment was marked as resolved.

@mattdurham
Copy link
Collaborator

Can we change the example to only use functionality that exists today + what you are suggesting? Might give the wrong direction on what this proposal enables.

@rfratto
Copy link
Member Author

rfratto commented May 10, 2024

I believe it's important to acknowledge how conditions will be fundamental in the future. In total isolation I don't think we can prioritize them. This is a bit of a chicken and egg problem; if we do the more powerful stuff first it's hampered because conditionals don't exist, but conditionals on their own aren't extremely useful without the more powerful capabilities.

@rfratto

This comment was marked as resolved.

@mattdurham
Copy link
Collaborator

Do you think we should add an explicit return keyword? Thinking of if we implement variables, it might be helpful and clearer. In general approve of the idea and implementation.

@wildum
Copy link
Contributor

wildum commented May 10, 2024

You can already use custom components as variables. With conditionals this opens the door to many twisted scenarios 😄

@rfratto
Copy link
Member Author

rfratto commented May 10, 2024

@wildum I expanded the grammar above to show the precedence of conditionals, and added a section for formatting to make it clear how the formatter will enhance readability. I'm going to resolve the earlier comments about nesting if expressions now :)

@tpaschalis
Copy link
Member

Thanks for picking this up again! I'm in favor of the proposal as is it presented here and excited to see it move ahead!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
proposal A proposal for new functionality. type/syntax Alloy configuration syntax issues
Projects
None yet
Development

No branches or pull requests

4 participants