Skip to content

Allow customisation of metaprogramming via scalac flags #12038

Closed
@japgolly

Description

@japgolly

This issue is to allow inline code to customise its output based on user-defined settings provided as scalac flags.

Background

Full detail here: https://contributors.scala-lang.org/t/metaprogramming-configurability/4961

This issue is the first part of the solution I proposed above.

Proposal

Disclaimer: the names used below are just drafts and still need some good bikeshedding.

  • Add a new scalac flag that takes a -E:key=value (similar to how java accepts -Dkey=value args)
  • Add transparent inline def envGet(inline key: String): Option[String] to scala.compiletime
    that provides access to the settings specified with above scalac flags

Usage Example

The very first example in the inlining doc is a logger that can be configured at compile-time, and is "zero-cost" (as they say) at runtime.

  • when logging = true then calls to log() generate println statements
  • when logging = false then calls to log() generate nothing
  • there is never a logging-config check at runtime

Currently, the problems with this are

  1. Logger and its Config are static, and changing the setting is a code change
  2. Config must be pre-configured and defined alongside Logger (else Logger wouldn't compile)
  3. If Logger were a library, downstream users would have no way of configuring it

If this issue were implemented, our zero-cost-ish Logger could be written like this below, to accept a "myLogger.level" setting that downstream users can populate:

import scala.compiletime.*

object Logging {
  private inline val Trace = 0
  private inline val Debug = 1
  private inline val Info  = 2
  private inline val Warn  = 3

  private transparent inline def chosenThreshold: Int =
    inline envGet("myLogger.level") match
      case Some("TRACE") => Trace
      case Some("DEBUG") => Debug
      case Some("INFO")  => Info
      case Some("WARN")  => Warn
      case None          => Warn // let's provide a default out-of-the-box
      case Some(x)       => error("Unsupported logging level: " + x)

  private inline def log(inline lvl: Int, inline msg: String): Unit =
    inline if lvl >= chosenThreshold then println(msg) else ()

  // This is the public API
  inline def trace(inline msg: String) = log(Trace, msg)
  inline def debug(inline msg: String) = log(Debug, msg)
  inline def info (inline msg: String) = log(Info , msg)
  inline def warn (inline msg: String) = log(Warn , msg)
}

And then a downstream user could specify -E:myLogger.level=INFO in their scalac flags so that our toy logging library effectively becomes this for them:

object Logging {
  inline def trace(inline msg: String) = ()
  inline def debug(inline msg: String) = ()
  inline def info (inline msg: String) = println(msg)
  inline def warn (inline msg: String) = println(msg)
}

Reconsidering the three problems above, with this new solution:

  1. Logger can now be configured dynamically, and via config (no code changes required)
  2. Config is no longer necessary. Whether the library author wants to provide it and/or defaults is now something under their control.
  3. Downstream users can now configure the behaviour of Logger

PR?

I'm not asking that the busy Scala 3 team take some time out to implement this. This seemed simple enough for me to try implementing it myself and so I have, and it seems to work well! The above logging example is one of the tests and is confirmed to work as excepted.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions