{"id":16964970,"url":"https://github.com/propensive/contextual","last_synced_at":"2025-05-16T01:07:28.766Z","repository":{"id":47438880,"uuid":"74711877","full_name":"propensive/contextual","owner":"propensive","description":"Statically-checked string interpolation in Scala","archived":false,"fork":false,"pushed_at":"2025-01-26T12:13:13.000Z","size":4886,"stargazers_count":251,"open_issues_count":5,"forks_count":23,"subscribers_count":13,"default_branch":"main","last_synced_at":"2025-05-12T06:43:13.199Z","etag":null,"topics":["compiletime","parsing","scala","string-interpolation","string-literals"],"latest_commit_sha":null,"homepage":"https://soundness.dev/contextual/","language":"Scala","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/propensive.png","metadata":{"files":{"readme":".github/readme.md","changelog":null,"contributing":".github/contributing.md","funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2016-11-24T23:52:50.000Z","updated_at":"2025-01-26T12:13:16.000Z","dependencies_parsed_at":"2024-02-19T20:29:21.683Z","dependency_job_id":"6581a440-8fce-44ca-8207-314b93dd4f2c","html_url":"https://github.com/propensive/contextual","commit_stats":null,"previous_names":[],"tags_count":27,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/propensive%2Fcontextual","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/propensive%2Fcontextual/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/propensive%2Fcontextual/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/propensive%2Fcontextual/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/propensive","download_url":"https://codeload.github.com/propensive/contextual/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":254448579,"owners_count":22072764,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["compiletime","parsing","scala","string-interpolation","string-literals"],"created_at":"2024-10-13T23:44:43.400Z","updated_at":"2025-05-16T01:07:23.745Z","avatar_url":"https://github.com/propensive.png","language":"Scala","readme":"[\u003cimg alt=\"GitHub Workflow\" src=\"https://img.shields.io/github/actions/workflow/status/propensive/contextual/main.yml?style=for-the-badge\" height=\"24\"\u003e](https://github.com/propensive/contextual/actions)\n[\u003cimg src=\"https://img.shields.io/discord/633198088311537684?color=8899f7\u0026label=DISCORD\u0026style=for-the-badge\" height=\"24\"\u003e](https://discord.com/invite/MBUrkTgMnA)\n\u003cimg src=\"/doc/images/github.png\" valign=\"middle\"\u003e\n\n# Contextual\n\n__Statically-checked string interpolation__\n\n__Contextual__ makes it simple to write typesafe, statically-checked interpolated strings.\n\nContextual is a Scala library which allows you to define your own string interpolators—prefixes for\ninterpolated string literals like `url\"https://propensive.com/\"`—which specify how they should be checked\nat compiletime and interpreted at runtime, writing very ordinary user code with no user-defined macros.\n\n## Features\n\n- user-defined string interpolators\n- introduce compiletime failures on invalid values, such as `url\"htpt://example.com\"`\n- compiletime behavior can be defined on _literal_ parts of a string\n- runtime behavior can be defined on literal and interpolated parts of a string\n- types of interpolated values can be context-dependent\n\n## Availability\n\n\n\n\n\n\n\n## Getting Started\n\n### About Interpolators\n\nAn interpolated string is any string literal prefixed with an alphanumeric string, such as\n`s\"Hello World\"` or `date\"15 April, 2016\"`. Unlike ordinary string literals, interpolated strings\nmay also include variable substitutions: expressions written inline, prefixed with a `$` symbol,\nand—if the expression is anything more complicated than an alphanumeric identifier—requiring braces\n(`{`, `}`) around it. For example,\n```scala\nval name = \"Sarah\"\nval string = s\"Hello, $name\"\n```\nor,\n```scala\nval day = 6\nval string2 = s\"Tomorrow will be Day ${day + 1}.\"\n```\n\nAnyone can write an interpolated string using an extension method on `StringContext`, and it will be\ncalled, like an ordinary method, at runtime.\n\nBut it's also possible to write an interpolator which is called at compiletime, and which can\nidentify coding errors _before_ runtime.\n\nContextual makes it easy to write such interpolators.\n\n### Contextual's `Verifier` type\n\nAn interpolated string may have no substitutions, or it may include many substitutions, with a\nstring of zero or more characters between at the start, end, and between each adjacent pair.\n\nSo in general, any interpolated string can be represented as _n_ string literals, whose values are\nknown at compiletime, and _n - 1_ variables (of various types), whose values are not known until\nruntime.\n\nContextual provides a simple `Verifier` interface for the simplest interpolated\nstrings—those which do not allow any substitutions.\n\nA new verifier needs just a a type parameter for the return type of the\nverifier, and a single method, `verify`, for example, a binary reader:\n```scala\nimport contextual.*\nimport anticipation.Text\n\nobject Binary extends Verifier[IArray[Byte]]:\n  def verify(content: Text): IArray[Byte] = ???\n    // read content as 0s and 1s and produce an IArray[Byte]\n```\n\nThis defines the verifier, but has not yet bound it to a prefix, such as `bin`.\nTo achieve this, we need to provide an extension method on `StringContext`,\nlike so:\n```scala\nextension (inline ctx: StringContext)\n  inline def bin(): IArray[Byte] = ${Binary.expand('ctx)}\n```\n\nNote that this definition must appear in a separate source file from the definition of the verifier.\n\nThis simple definition makes it possible to write an expression such as\n`bin\"0011001011101100\"`, and have it produce a byte array.\n\n#### More advanced interpolation\n\nFor string interpolations which support substitutions of runtime values into\nthe string, Contextual provides the `Interpolator` type.\n\nContextual's `Interpolator` interface provides a set of five abstract\nmethods—`initial`, `parse`, `insert`, `skip` and `complete`—which are invoked,\nin a particular order, once at compiletime, _without_ the substituted values\n(since they are not known when it runs!), and again at runtime, _with_ the\nsubstituted values (when they are known).\n\nThe method `skip` is used at compiletime, and `insert` at runtime.\n\nThe methods are always invoked in the same order: first `initial`; then alternately `parse` and\n`insert`/`skip`, some number of times, for each string literal and each substitution (respectively);\nand finally `complete` to produce a result. `insert` may never be invoked if there are no\nsubstitutions, but `parse` will always be invoked once more than `insert`.\n\nFor example, for a string with two substitutions, the invocation order would be:\n```\ninitial -\u003e parse -\u003e insert -\u003e parse -\u003e insert -\u003e parse -\u003e complete\n```\nat runtime, or,\n```\ninitial -\u003e parse -\u003e skip -\u003e parse -\u003e skip -\u003e parse -\u003e complete\n```\nat compiletime.\n\nAn object encoding the interpolator's state is returned by each of these method calls, and is passed\nas input to the next—with the exception of `complete`, which should return the final value that the\ninterpolated string will evaluate to. This is where final checks can be carried out to check that\nthe interpolated string is in a complete final state.\n\nIn other words, each segment of an interpolated string is read in turn, to incrementally build up\na working representation of the incomplete information in the interpolated string. And at the end,\nit is converted into the return value.\n\nThe separation into `parse` and `insert`/`skip` calls means that the static parts of the\ninterpolated string can be parsed the same way at both compiletime or runtime, while the dynamic\nparts may be interpreted at runtime when they're known, and their absence handled in some way at\ncompiletime when they're not known.\n\nOf course, `skip` could be implemented to delegate to `insert` using a dummy value.\n\nHere are the signatures for each method in the `Interpolator` type:\n```scala\ntrait Interpolator[Input, State, Result]:\n  def initial: State\n  def parse(state: State, next: Text): State\n  def insert(state: State, value: Input): State\n  def skip(state: State): State\n  def complete(value: State): Result\n```\n\nThree abstract types are used in their definitions: `State` represents the information passed from\none method to the next, and could be as simple as `Unit` or `Text`, or could be some complex\ndocument structure. `Input` is a type chosen to represent the types of all substitutions. `Text`\nwould be a common choice for this, but there may be utility in using richer types, too. And `Return`\nis the type that the interpolated string will ultimately evaluate to.\n\nIn addition to `parse`, `insert`, `skip` and `complete` taking `State` instances as input, note that\n`parse` always takes a `Text`, and `insert` takes an `Input`.\n\nAny of the methods may throw an `InterpolationError` exception, with a message. At compiletime,\nthese will be caught, and turned into compile errors. Additionally, a range of characters may be\nspecified to highlight precisely where the error occurs in an interpolated string.\n\nAny interpolator needs to choose these three types, and implement these four methods.\n\nFor example, the interpolated string,\n```\nurl\"https://example.com/$dir/images/$img\"\n```\ncould be interpreted by a Contextual interpolator, in which case it would be checked at\ncompiletime with the composed invocation,\n```scala\nval result = complete(parse(insert(parse(insert(parse(initial, \"https://example.com/\"), None),\n    \"/images/\"), None), \"\"))\n```\nand at runtime with something which is essentially this:\n```scala\nval runtimeResult = complete(parse(insert(parse(insert(parse(initial, \"https://example.com/\"), Some(dir)),\n    \"/images/\"), Some(img)), \"\"))\n```\n\n### Compile Errors\n\nThrowing exceptions provides the flexibility to raise a compilation error just by examining the\n`state` value and/or the other inputs.\n\nFor example, insertions could be permitted only in appropriate positions, i.e. where the `state`\nvalue passed to the `insert` method indicates that the insertion can be made. That is knowable at\ncompiletime, without even knowing the inserted value, and can be generated as a compile error by\nthrowing an `InterpolationError` in the implementation of `insert`.\n\nThe compile error will point at the substituted expression.\n\nLikewise, throwing an `InterpolationError` in `parse` will generate a compile error. The optional\nsecond parameter of `InterpolationError` allows an offset to be specified, relative to the start of\nthe literal part currently being parsed, and a third parameter allows its length to be specified.\n\nFor example, if we were parsing `url\"https://example.ocm/$dir/images/$img\"`, and wanted to highlight\nthe mistake in the invalid TLD `.ocm`, we would throw, `InterpolationError(\"not a valid TLD\", 15, 4)`\nduring the first invocation of `parse`, and the Scala compiler would highlight `.ocm` as the error\nlocation: in this example, `15` is the offset from the start of this part of the string to the\nerror location, and `4` is the length of the error.\n\n### Binding an interpolator\n\nA small amount of boilerplate is needed to bind an `Interpolator` object, for example `Abc`, to a\nprefix, i.e. the letters `abc` in the interpolated string, `abc\"\"`:\n```scala\nextension (inline ctx: StringContext)\n  transparent inline def abc(inline parts: Any*): Return =\n    ${Abc.expand('ctx, 'parts)}\n```\n\nThis boilerplate should be modified as follows:\n - the method name, `abc`, should change to the desired prefix,\n - the method's return type, `Return`, should be changed to the return type of the `complete` method, and,\n - the interpolator object, `Abc`, should be specified.\n\nIn particular, the type of `parts`, `Any*`, should be left unchanged. This does not mean that `Any`\ntype may be substituted into an interpolated string; Contextual provides another way to constrain\nthe set of acceptable types for insertions.\n\n### Insertions\n\nContextual uses a typeclass interface to support insertions of different types. An insertion of a\nparticular type, `T`, into an interpolator taking a value of type `I` requires a corresponding\ngiven `Insertion[I, T]` instance in scope.\n\nThis means that the set of types which may be inserted into an interpolated string can be defined\nad-hoc. There is only the requirement that any inserted type, `T`, may be converted to an `I`, since\n`I` is a type known to the `Interpolator` implementation.\n\nSo, if an interpolator's general `Input` type is `List[Text]`, and we wanted to permit insertions\nof `List[Text]`, `Text` and `Int`, then three given instances would be necessary:\n\n```scala\ngiven Insertion[List[Text], Text] = List(_)\ngiven Insertion[List[Text], List[Text]] = identity(_)\ngiven Insertion[List[Text], Int] = int =\u003e List(int.show)\n```\n\n### Substitutions\n\nA `Substitution` is a typeclass that's almost identical to `Insertion` (and is, indeed, a subtype of\n`Insertion`), but takes an additional type parameter: a singleton `Text` literal. The behavior of\na given `Substitution` will be identical to a given `Insertion` at runtime, but differs at\ncompiletime:\n\nDuring macro expansion, instead of invoking `skip`, the `substitute` method will be called instead,\npassing it the `Text` value taken from the additional type parameter to `Substitution`.\n\nFor example the given definitions,\n```scala\ngiven Substitution[XInput, Text, \"\\\"\\\"\"] = str =\u003e StrInput(str)\ngiven Substitution[XInput, Int, \"0\"] = int =\u003e IntInput(int)\n```\nwould mean that an `Int`, `int`, and a `Text`, `str`, substituted into an interpolated string\nwould result in invocations of, `substitute(state, \"0\")` and `substitute(state, \"\\\"\\\"\")`\nrespectively.\n\nBy default, the `substitute` method simply delegates to `parse`, which takes the same parameters,\nand will parse the substituted strings in a predictable way. Any user-defined `substitute` method\nimplementation will therefore need the `override` modifier, but can provide its own implementation\nthat is distinct from `parse`.\n\nThe benefit of `Substitution` over `Insertion` is that the compiletime interpretation of the\ninterpolated string may be dependent on the types inserted, distinguishing between types on the\nbasis of the singleton `String` literal included in the given's signature. This compares to the\n`skip` method which offers no more information about a substitution than its existence.\n\n### A First Interpolator\n\nHere is a trivial interpolator which can parse, for example, `hex\"a948b0${x}710bff\"`, and return an\n`IArray[Byte]`:\n```scala\nimport rudiments.*\nimport anticipation.*\n\nobject Hex extends Interpolator[Long, Text, IArray[Byte]]:\n  def initial: Text = \"\"\n\n  def parse(state: Text, next: Text): Text =\n    if next.forall(hexChar(_)) then state+next\n    else throw InterpolationError(\"not a valid hexadecimal character\")\n  \n  def insert(state: Text, value: Option[Long]): Text =\n    value match\n      case None       =\u003e s\"${state}0\".tt\n      case Some(long) =\u003e s\"${state}${long.toHexString}\".tt\n  \n  def complete(state: Text): IArray[Byte] =\n    IArray.from(convertStringToByteArray(state))\n  \n  private def hexChar(ch: Char): Boolean =\n    ch.isDigit || 'a' \u003c= ch \u003c= 'f' || 'A' \u003c= ch \u003c= 'F'\n```\n\nHaving defined this interpolator, we can bind it to the prefix, `hex` with:\n```scala\nextension (ctx: StringContext)\n  transparent inline def hex(inline parts: Any*): IArray[Byte] =\n    ${Hex.expand('ctx, 'parts)}\n```\n\nNote that this should be defined in a different source file from the object `Hex`.\n\n\n\n\n\n## Status\n\nContextual is classified as __maturescent__. For reference, Soundness projects are\ncategorized into one of the following five stability levels:\n\n- _embryonic_: for experimental or demonstrative purposes only, without any guarantees of longevity\n- _fledgling_: of proven utility, seeking contributions, but liable to significant redesigns\n- _maturescent_: major design decisions broady settled, seeking probatory adoption and refinement\n- _dependable_: production-ready, subject to controlled ongoing maintenance and enhancement; tagged as version `1.0.0` or later\n- _adamantine_: proven, reliable and production-ready, with no further breaking changes ever anticipated\n\nProjects at any stability level, even _embryonic_ projects, can still be used,\nas long as caution is taken to avoid a mismatch between the project's stability\nlevel and the required stability and maintainability of your own project.\n\nContextual is designed to be _small_. Its entire source code currently consists\nof 154 lines of code.\n\n## Building\n\nContextual will ultimately be built by Fury, when it is published. In the\nmeantime, two possibilities are offered, however they are acknowledged to be\nfragile, inadequately tested, and unsuitable for anything more than\nexperimentation. They are provided only for the necessity of providing _some_\nanswer to the question, \"how can I try Contextual?\".\n\n1. *Copy the sources into your own project*\n   \n   Read the `fury` file in the repository root to understand Contextual's build\n   structure, dependencies and source location; the file format should be short\n   and quite intuitive. Copy the sources into a source directory in your own\n   project, then repeat (recursively) for each of the dependencies.\n\n   The sources are compiled against the latest nightly release of Scala 3.\n   There should be no problem to compile the project together with all of its\n   dependencies in a single compilation.\n\n2. *Build with [Wrath](https://github.com/propensive/wrath/)*\n\n   Wrath is a bootstrapping script for building Contextual and other projects in\n   the absence of a fully-featured build tool. It is designed to read the `fury`\n   file in the project directory, and produce a collection of JAR files which can\n   be added to a classpath, by compiling the project and all of its dependencies,\n   including the Scala compiler itself.\n   \n   Download the latest version of\n   [`wrath`](https://github.com/propensive/wrath/releases/latest), make it\n   executable, and add it to your path, for example by copying it to\n   `/usr/local/bin/`.\n\n   Clone this repository inside an empty directory, so that the build can\n   safely make clones of repositories it depends on as _peers_ of `contextual`.\n   Run `wrath -F` in the repository root. This will download and compile the\n   latest version of Scala, as well as all of Contextual's dependencies.\n\n   If the build was successful, the compiled JAR files can be found in the\n   `.wrath/dist` directory.\n\n## Contributing\n\nContributors to Contextual are welcome and encouraged. New contributors may like\nto look for issues marked\n[beginner](https://github.com/propensive/contextual/labels/beginner).\n\nWe suggest that all contributors read the [Contributing\nGuide](/contributing.md) to make the process of contributing to Contextual\neasier.\n\nPlease __do not__ contact project maintainers privately with questions unless\nthere is a good reason to keep them private. While it can be tempting to\nrepsond to such questions, private answers cannot be shared with a wider\naudience, and it can result in duplication of effort.\n\n## Author\n\nContextual was designed and developed by Jon Pretty, and commercial support and\ntraining on all aspects of Scala 3 is available from [Propensive\nO\u0026Uuml;](https://propensive.com/).\n\n\n\n## Name\n\nContextual takes its name from its ability to provide context-aware substitutions in interpolated strings.\n\nIn general, Soundness project names are always chosen with some rationale,\nhowever it is usually frivolous. Each name is chosen for more for its\n_uniqueness_ and _intrigue_ than its concision or catchiness, and there is no\nbias towards names with positive or \"nice\" meanings—since many of the libraries\nperform some quite unpleasant tasks.\n\nNames should be English words, though many are obscure or archaic, and it\nshould be noted how willingly English adopts foreign words. Names are generally\nof Greek or Latin origin, and have often arrived in English via a romance\nlanguage.\n\n## Logo\n\nThe logo is of a quote symbol, alluding to Contextual's subject matter of quoted strings.\n\n## License\n\nContextual is copyright \u0026copy; 2025 Jon Pretty \u0026 Propensive O\u0026Uuml;, and\nis made available under the [Apache 2.0 License](/license.md).\n\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpropensive%2Fcontextual","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fpropensive%2Fcontextual","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpropensive%2Fcontextual/lists"}