├── doc
├── lines.md
├── status.md
├── availability.md
├── slogan.md
├── images
│ ├── 16x16.png
│ ├── 48x48.png
│ ├── 128x128.png
│ ├── 200x200.png
│ ├── furore.png
│ ├── github.png
│ ├── 1000x1000.png
│ └── 2400x2400@300dpi.png
├── api
│ └── contextual
│ │ ├── +ContextualError
│ │ └── msg.md
│ │ ├── +Insertion
│ │ ├── embed.md
│ │ ├── T.md
│ │ └── Input.md
│ │ ├── +InterpolationError
│ │ ├── msg.md
│ │ ├── length.md
│ │ └── offset.md
│ │ ├── Interpolator.md
│ │ ├── Substitution.md
│ │ ├── +Interpolator
│ │ ├── Result.md
│ │ ├── initial.md
│ │ ├── insert.md
│ │ ├── parse.md
│ │ ├── expand.md
│ │ ├── complete.md
│ │ ├── skip.md
│ │ ├── Input.md
│ │ ├── State.md
│ │ └── substitute.md
│ │ ├── +Substitution
│ │ ├── T.md
│ │ ├── Input.md
│ │ └── S.md
│ │ ├── InterpolationError.md
│ │ └── Insertion.md
├── logo.md
├── name.md
├── features.md
├── intro.md
├── basics.md
└── logo.svg
├── fury
├── .github
├── workflows
│ └── admin.yml
├── contributing.md
├── license.md
└── readme.md
├── src
├── core
│ ├── contextual.Contextual.scala
│ ├── soundness+contextual-core.scala
│ ├── contextual.Embeddable.scala
│ ├── contextual.Substitution.scala
│ ├── contextual.Insertion.scala
│ ├── contextual.InterpolationError.scala
│ ├── contextual.Verifier.scala
│ └── contextual.Interpolator.scala
└── test
│ └── contextual.Tests.scala
├── .fury
└── etc
└── core
└── pom.xml
/doc/lines.md:
--------------------------------------------------------------------------------
1 | 154
2 |
--------------------------------------------------------------------------------
/doc/status.md:
--------------------------------------------------------------------------------
1 | maturescent
2 |
--------------------------------------------------------------------------------
/doc/availability.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/doc/slogan.md:
--------------------------------------------------------------------------------
1 | Statically-checked string interpolation
2 |
--------------------------------------------------------------------------------
/doc/images/16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/propensive/contextual/HEAD/doc/images/16x16.png
--------------------------------------------------------------------------------
/doc/images/48x48.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/propensive/contextual/HEAD/doc/images/48x48.png
--------------------------------------------------------------------------------
/doc/api/contextual/+ContextualError/msg.md:
--------------------------------------------------------------------------------
1 | the message which describes the cause of the parsing failure
--------------------------------------------------------------------------------
/doc/images/128x128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/propensive/contextual/HEAD/doc/images/128x128.png
--------------------------------------------------------------------------------
/doc/images/200x200.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/propensive/contextual/HEAD/doc/images/200x200.png
--------------------------------------------------------------------------------
/doc/images/furore.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/propensive/contextual/HEAD/doc/images/furore.png
--------------------------------------------------------------------------------
/doc/images/github.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/propensive/contextual/HEAD/doc/images/github.png
--------------------------------------------------------------------------------
/doc/api/contextual/+Insertion/embed.md:
--------------------------------------------------------------------------------
1 | the function to convert the value of type `:T` into the `:Input` type
--------------------------------------------------------------------------------
/doc/api/contextual/+InterpolationError/msg.md:
--------------------------------------------------------------------------------
1 | the message which describes the reason for the parsing failure
--------------------------------------------------------------------------------
/doc/images/1000x1000.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/propensive/contextual/HEAD/doc/images/1000x1000.png
--------------------------------------------------------------------------------
/doc/logo.md:
--------------------------------------------------------------------------------
1 | The logo is of a quote symbol, alluding to Contextual's subject matter of quoted strings.
2 |
--------------------------------------------------------------------------------
/doc/images/2400x2400@300dpi.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/propensive/contextual/HEAD/doc/images/2400x2400@300dpi.png
--------------------------------------------------------------------------------
/doc/name.md:
--------------------------------------------------------------------------------
1 | Contextual takes its name from its ability to provide context-aware substitutions in interpolated strings.
2 |
--------------------------------------------------------------------------------
/doc/api/contextual/Interpolator.md:
--------------------------------------------------------------------------------
1 | a class which defines how an interpolated string can be interpreted at compiletime and evaluated at runtime
--------------------------------------------------------------------------------
/doc/api/contextual/Substitution.md:
--------------------------------------------------------------------------------
1 | extends an `:Insertion` by providing additional type-based disambiguation at compiletime
2 |
3 | While an `:Insertion` allows a
--------------------------------------------------------------------------------
/doc/api/contextual/+Interpolator/Result.md:
--------------------------------------------------------------------------------
1 | the return type of the interpolated string
2 |
3 | This type represents the type an evaluated interpolated string should return.
--------------------------------------------------------------------------------
/doc/api/contextual/+Interpolator/initial.md:
--------------------------------------------------------------------------------
1 | creates an initial `:State` instance for use while interpreting an interpolated string
2 |
3 | This method requires a trivial implementation to return the initial `:State` value that should be used during
4 | interpolation of an interpolated string.
--------------------------------------------------------------------------------
/doc/api/contextual/+Insertion/T.md:
--------------------------------------------------------------------------------
1 | the type which may be inserted into an interpolated string
2 |
3 | The `:Interpolator` does not need to be aware of this type. Inserted values of type `:T` will be converted using
4 | the `.embed` method into instances of `:Input` in order for the interpolator to use them.
--------------------------------------------------------------------------------
/doc/api/contextual/+Substitution/T.md:
--------------------------------------------------------------------------------
1 | the type which may be inserted into an interpolated string
2 |
3 | The `:Interpolator` does not need to be aware of this type. Inserted values of type `:T` will be converted using
4 | the `.embed` method into instances of `:Input` in order for the interpolator to use them.
--------------------------------------------------------------------------------
/doc/api/contextual/InterpolationError.md:
--------------------------------------------------------------------------------
1 | a failure that happens while interpreting an interpolated string
2 |
3 | User code should throw this exception during `.parse`, `.substitute`, `.insert` or `.complete`. Since these
4 | methods are called during macro expansion, it will be caught at compiletime and presented as a compile error.
--------------------------------------------------------------------------------
/doc/features.md:
--------------------------------------------------------------------------------
1 | - user-defined string interpolators
2 | - introduce compiletime failures on invalid values, such as `url"htpt://example.com"`
3 | - compiletime behavior can be defined on _literal_ parts of a string
4 | - runtime behavior can be defined on literal and interpolated parts of a string
5 | - types of interpolated values can be context-dependent
--------------------------------------------------------------------------------
/doc/api/contextual/+InterpolationError/length.md:
--------------------------------------------------------------------------------
1 | the length of the error from the `.offset` position within the current part of the interpolated string
2 |
3 | This field is only relevant for `:InterpolationError`s thrown in the `.parse` method. It will determine the
4 | length of the region of the interpolated string in the source code that is highlighted as erroneous.
--------------------------------------------------------------------------------
/doc/api/contextual/Insertion.md:
--------------------------------------------------------------------------------
1 | a typeclass interface defining how different types may be interpreted by the interpolator
2 |
3 | For a given `:Interpolator`, it may be possible to substitute certain types into the interpolated strings it
4 | parses. Each type will need a corresponding `:Insertion` instance which defines how that type can be transformed
5 | into a value that the interpolator can use.
--------------------------------------------------------------------------------
/doc/intro.md:
--------------------------------------------------------------------------------
1 | __Contextual__ makes it simple to write typesafe, statically-checked interpolated strings.
2 |
3 | Contextual is a Scala library which allows you to define your own string interpolators—prefixes for
4 | interpolated string literals like `url"https://propensive.com/"`—which specify how they should be checked
5 | at compiletime and interpreted at runtime, writing very ordinary user code with no user-defined macros.
6 |
7 |
8 |
--------------------------------------------------------------------------------
/doc/api/contextual/+Interpolator/insert.md:
--------------------------------------------------------------------------------
1 | inserts a known value into the interpolator state at runtime
2 |
3 | Complementary to the `.skip` method, which modifies the interpolation state at compiletime, when the value of a
4 | substitution is not yet known, the `.insert` method should be implemented to modify the `:State` value to
5 | include it, so that interpretation of the interpolated string can continue by calling `.parse` on the next part.
--------------------------------------------------------------------------------
/doc/api/contextual/+Insertion/Input.md:
--------------------------------------------------------------------------------
1 | the resultant input type that this `:Insertion` produces for insertion into an interpolated string
2 |
3 | In general, the `:Interpolator` instance will not be aware of all the types that may be inserted into an
4 | interpolated string, so `:Insertion` instances must be provided to convert downstream types into values that the
5 | interpolator is able to use. These have the type `:Input`, which forms part of the `:Interpolator`'s signature.
--------------------------------------------------------------------------------
/doc/api/contextual/+Substitution/Input.md:
--------------------------------------------------------------------------------
1 | the resultant input type that this `:Substitution` produces for insertion into an interpolated string
2 |
3 | In general, the `:Interpolator` instance will not be aware of all the types that may be inserted into an
4 | interpolated string, so `:Insertion` instances must be provided to convert downstream types into values that the
5 | interpolator is able to use. These have the type `:Input`, which forms part of the `:Interpolator`'s signature.
--------------------------------------------------------------------------------
/doc/api/contextual/+InterpolationError/offset.md:
--------------------------------------------------------------------------------
1 | the offset of the error from the start of the current part of the interpolated string
2 |
3 | This field is only relevant for `:InterpolationError`s thrown in the `.parse` method. It indicates the offset
4 | of the error from the start of the current part of the interpolated string (i.e. from the start of the string,
5 | if the error occurs in the first part, or from the end of the previous substitution, if the error occurs in a
6 | later part).
--------------------------------------------------------------------------------
/fury:
--------------------------------------------------------------------------------
1 | # This is a buildfile for Fury or Wrath.
2 | # More information is available at: https://github.com/propensive/wrath/
3 |
4 | repo propensive/rudiments
5 |
6 | target contextual/core
7 |
8 | project contextual
9 | module core
10 | compiler scala
11 | sources src/core
12 | include rudiments/core
13 |
14 | module test
15 | compiler scala
16 | sources src/test
17 | include probably/cli contextual/core
18 | main contextual.Tests
19 |
--------------------------------------------------------------------------------
/doc/api/contextual/+Interpolator/parse.md:
--------------------------------------------------------------------------------
1 | parses one part of an interpolated string and updates the interpretation state accordingly
2 |
3 | The `.parse` method is invoked first at compiletime and again at runtime, once for each static part of the
4 | string, i.e. each contiguous section of the interpolated string between its start, substitutions and end. Its
5 | implementation should update the `;State` value appropriately to accommodate the relevant details in the
6 | `:String` value passed to it.
--------------------------------------------------------------------------------
/.github/workflows/admin.yml:
--------------------------------------------------------------------------------
1 | name: Repo Admin
2 | on:
3 | push:
4 | branches: [ main ]
5 | paths:
6 | - doc/*
7 | - src/*/*.scala
8 | jobs:
9 | admin:
10 | runs-on: ubuntu-latest
11 | permissions:
12 | contents: write
13 | steps:
14 | - name: Checkout
15 | uses: actions/checkout@v3
16 | - name: Tidy repository
17 | uses: propensive/tumult@0.6.5
18 | - name: Autocommit changes
19 | uses: stefanzweifel/git-auto-commit-action@v4
20 |
--------------------------------------------------------------------------------
/doc/api/contextual/+Interpolator/expand.md:
--------------------------------------------------------------------------------
1 | a macro method for binding an `:Interpolator` to a `:StringContext` in an extension method
2 |
3 | This method should always be called as the right-hand side of a transparent inline extension method to a
4 | `:StringContext`.
5 |
6 | This method should always be called with the following boilerplate,
7 | ```scala
8 | extension (ctx: StringContext)
9 | transparent inline def i(inline parts: Any*): R =
10 | ${I.expand('I, 'ctx, 'parts)}
11 | ```
12 | where the identifiers `I`, `i` and `R` should be replaced with an `:Interpolator` object, the prefix to the
13 | interpolated string, and the return type of the interpolated string, respectively.
--------------------------------------------------------------------------------
/doc/api/contextual/+Interpolator/complete.md:
--------------------------------------------------------------------------------
1 | checks the `:State` value and returns the final `:Result` instance
2 |
3 | This method is executed once during compilation of an interpolated string, and again at runtime to construct the
4 | value from the interpolated string.
5 |
6 | It is passed the final `:State` value that results from the previous steps of interpreting the interpolated
7 | string. Its implementation should check that the `:State` value represents a valid state from which a `:Result`
8 | value may be constructed, and constructs the result.
9 |
10 | At compiletime, this result value is discarded, while at runtime, the value it produces will be returned from
11 | evaluating the interpolated string.
--------------------------------------------------------------------------------
/doc/api/contextual/+Interpolator/skip.md:
--------------------------------------------------------------------------------
1 | modifies the interpolation state at compiletime to interpret an insertion whose value is not yet known
2 |
3 | An interpolated string being interpreted at compiletime may include several substituted expressions whose values
4 | will not be known until runtime. Their presence, even without knowing their values, will usually affect the
5 | compilation state in some way, and the `.skip` method should modify the `:State` value appropriately.
6 |
7 | A common implementation for a `.skip` method is to delegate to the `.parse` method, passing in a dummy value as
8 | a `:String`.
9 |
10 | User code may throw an `:InterpolationError` in this method if an insertion is made in an invalid state.
--------------------------------------------------------------------------------
/src/core/contextual.Contextual.scala:
--------------------------------------------------------------------------------
1 | /*
2 | Contextual, version 0.26.0. Copyright 2025 Jon Pretty, Propensive OÜ.
3 |
4 | The primary distribution site is: https://propensive.com/
5 |
6 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use this
7 | file except in compliance with the License. You may obtain a copy of the License at
8 |
9 | http://www.apache.org/licenses/LICENSE-2.0
10 |
11 | Unless required by applicable law or agreed to in writing, software distributed under the
12 | License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
13 | either express or implied. See the License for the specific language governing permissions
14 | and limitations under the License.
15 | */
16 |
17 |
--------------------------------------------------------------------------------
/src/core/soundness+contextual-core.scala:
--------------------------------------------------------------------------------
1 | /*
2 | Contextual, version 0.26.0. Copyright 2025 Jon Pretty, Propensive OÜ.
3 |
4 | The primary distribution site is: https://propensive.com/
5 |
6 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use this
7 | file except in compliance with the License. You may obtain a copy of the License at
8 |
9 | http://www.apache.org/licenses/LICENSE-2.0
10 |
11 | Unless required by applicable law or agreed to in writing, software distributed under the
12 | License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
13 | either express or implied. See the License for the specific language governing permissions
14 | and limitations under the License.
15 | */
16 |
17 | package soundness
18 |
19 | export contextual.{Insertion, InterpolationError, Interpolator, Verifier, Substitution, Embeddable}
20 |
--------------------------------------------------------------------------------
/src/core/contextual.Embeddable.scala:
--------------------------------------------------------------------------------
1 | /*
2 | Contextual, version 0.26.0. Copyright 2025 Jon Pretty, Propensive OÜ.
3 |
4 | The primary distribution site is: https://propensive.com/
5 |
6 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use this
7 | file except in compliance with the License. You may obtain a copy of the License at
8 |
9 | http://www.apache.org/licenses/LICENSE-2.0
10 |
11 | Unless required by applicable law or agreed to in writing, software distributed under the
12 | License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
13 | either express or implied. See the License for the specific language governing permissions
14 | and limitations under the License.
15 | */
16 |
17 | package contextual
18 |
19 | trait Embeddable:
20 | type Self
21 | type Format
22 | type Operand
23 | def embed(value: Self): Operand
24 |
--------------------------------------------------------------------------------
/src/test/contextual.Tests.scala:
--------------------------------------------------------------------------------
1 | /*
2 | Contextual, version 0.26.0. Copyright 2025 Jon Pretty, Propensive OÜ.
3 |
4 | The primary distribution site is: https://propensive.com/
5 |
6 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use this
7 | file except in compliance with the License. You may obtain a copy of the License at
8 |
9 | http://www.apache.org/licenses/LICENSE-2.0
10 |
11 | Unless required by applicable law or agreed to in writing, software distributed under the
12 | License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
13 | either express or implied. See the License for the specific language governing permissions
14 | and limitations under the License.
15 | */
16 |
17 | package contextual
18 |
19 | import gossamer.*
20 | import probably.*
21 |
22 | object Tests extends Suite(t"Contextual Tests"):
23 | def run(): Unit =
24 | ()
25 |
--------------------------------------------------------------------------------
/doc/api/contextual/+Interpolator/Input.md:
--------------------------------------------------------------------------------
1 | the common type of values substituted into the interpolated string
2 |
3 | Values of different types, including those that are unknown at the time the interpolator is designed, may be
4 | substituted into an interpolated string. But the interpolator must implement a common method to interpret these,
5 | which requires them to be converted to a common type, `:Input`
6 |
7 | A typical choice for `:Input` is `:Text` (or `:String`), since substitutions into a string may often be fully
8 | represented as other strings if only they were known statically. (And the entire interpolated string might thus
9 | be represented as a single string, and parsed as such.)
10 |
11 | Other times, `Text` values will be inadequate, as additional details of the substituted type may affect the
12 | interpretation state in ways that can't be easily determined from a string, and for these use cases, a different
13 | `:Input` type may be chosen.
--------------------------------------------------------------------------------
/doc/api/contextual/+Interpolator/State.md:
--------------------------------------------------------------------------------
1 | a representation of the state of the interpolator while interpreting an interpolated string
2 |
3 | At compiletime, while the interpolator is interpreting an interpolated string, it will evaluate a sequence of
4 | methods (`.parse`, `.skip` and `.complete`), each of which takes a `:State` value as a parameter and returns an
5 | updated `:State` value. The functionality of each method can be dependent on the `:State` value passed to it,
6 |
7 | The `:State` type must carry enough information in order to be able to construct a `:Result` instance in the
8 | `.complete` method, but must also be sufficiently permissive to allow the `.skip` method to modify its state
9 | without a specific substituted value being provided. In practice, however, it is usually sufficient for `.skip`
10 | to use dummy values; since the result constructed in the `.complete` method is discarded at compiletime, it does
11 | not matter if its value is nonsensical due to these dummy values.
--------------------------------------------------------------------------------
/src/core/contextual.Substitution.scala:
--------------------------------------------------------------------------------
1 | /*
2 | Contextual, version 0.26.0. Copyright 2025 Jon Pretty, Propensive OÜ.
3 |
4 | The primary distribution site is: https://propensive.com/
5 |
6 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use this
7 | file except in compliance with the License. You may obtain a copy of the License at
8 |
9 | http://www.apache.org/licenses/LICENSE-2.0
10 |
11 | Unless required by applicable law or agreed to in writing, software distributed under the
12 | License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
13 | either express or implied. See the License for the specific language governing permissions
14 | and limitations under the License.
15 | */
16 |
17 | package contextual
18 |
19 | import language.experimental.captureChecking
20 |
21 | import proscenium.*
22 |
23 | trait Substitution[InputType, -ValueType, SubstitutionType <: Label]
24 | extends Insertion[InputType, ValueType]
25 |
--------------------------------------------------------------------------------
/doc/api/contextual/+Substitution/S.md:
--------------------------------------------------------------------------------
1 | the singleton type which may be used to disambiguate between different substitutions at compiletime
2 |
3 | When a value of some type is inserted into an interpolated string, the `.skip` method will be invoked at
4 | compiletime to modify the interpretation state accordingly. But while the `:Input` type may be used to encode
5 | details of the inserted value and its type at runtime, the value is not available at compiletime, so
6 | substitutions of different types cannot be disambiguated, and using the `.skip` method, the `:State` value must
7 | change in the same way regardless of the type of the substitution.
8 |
9 | The `:S` type allows a singleton `:String` type to be included in a contextual `:Substitution` instance, in
10 | order to disambiguate between differently-typed substitutions at compiletime. This singleton `:String` will be
11 | reified to a `:String` instance, which will be passed to the `.substitute` method, which will be called in place
12 | of `.skip`. Implementations of `.substitute` may have different behavior depending on this `:String` value.
--------------------------------------------------------------------------------
/src/core/contextual.Insertion.scala:
--------------------------------------------------------------------------------
1 | /*
2 | Contextual, version 0.26.0. Copyright 2025 Jon Pretty, Propensive OÜ.
3 |
4 | The primary distribution site is: https://propensive.com/
5 |
6 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use this
7 | file except in compliance with the License. You may obtain a copy of the License at
8 |
9 | http://www.apache.org/licenses/LICENSE-2.0
10 |
11 | Unless required by applicable law or agreed to in writing, software distributed under the
12 | License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
13 | either express or implied. See the License for the specific language governing permissions
14 | and limitations under the License.
15 | */
16 |
17 | package contextual
18 |
19 | import language.experimental.captureChecking
20 |
21 | object Insertion:
22 | given [ValueType: Embeddable] => Substitution[ValueType.Operand, ValueType, "x"] =
23 | ValueType.embed(_)
24 |
25 | trait Insertion[InputType, -ValueType]:
26 | def embed(value: ValueType): InputType
27 |
--------------------------------------------------------------------------------
/src/core/contextual.InterpolationError.scala:
--------------------------------------------------------------------------------
1 | /*
2 | Contextual, version 0.26.0. Copyright 2025 Jon Pretty, Propensive OÜ.
3 |
4 | The primary distribution site is: https://propensive.com/
5 |
6 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use this
7 | file except in compliance with the License. You may obtain a copy of the License at
8 |
9 | http://www.apache.org/licenses/LICENSE-2.0
10 |
11 | Unless required by applicable law or agreed to in writing, software distributed under the
12 | License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
13 | either express or implied. See the License for the specific language governing permissions
14 | and limitations under the License.
15 | */
16 |
17 | package contextual
18 |
19 | import language.experimental.captureChecking
20 |
21 | import scala.compiletime.*
22 |
23 | import fulminate.*
24 | import vacuous.*
25 |
26 | case class InterpolationError
27 | (error: Message, offset: Optional[Int] = Unset, length: Optional[Int] = Unset)
28 | (using Diagnostics)
29 | extends Error(m"$error at ${offset.or(-1)} - ${length.or(-1)}")
30 |
--------------------------------------------------------------------------------
/doc/api/contextual/+Interpolator/substitute.md:
--------------------------------------------------------------------------------
1 | invoked at compiletime when a given `:Substitute` instance is available instead of an `:Insertion` instance
2 |
3 | The `.skip` method will normally be applied to the interpretation `:State` value at compiletime whenever a value
4 | is substituted into an interpolated string. However, if a more precise `:Substitute` instance is available,
5 | which defines an additional `String` singleton type in its signature, the `.substitute` method will be invoked
6 | instead of `.skip`, with the reified `String` singleton value passed as an additional parameter.
7 |
8 | This allows the `.substitute` method's implementation to behave differently for different types of insertion.
9 |
10 | For example, an interpolator for JSON context may be inside an object definition when it sees a substitution,
11 | i.e.:
12 | ```scala
13 | json"""{ $sub ..."""
14 | ```
15 |
16 | At this position, if the type of `sub` were `Text`, it would be sensible to interpret this as a string key
17 | which should be followed by a colon (`:`), whereas if the type were `(Text, Text)` it would be sensible to
18 | interpret it as a key/value pair, which should be followed by a comma (`,`).
--------------------------------------------------------------------------------
/.fury:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | :<< "##"
3 | ┌─────────────────────────────────────────────────────────┐
4 | │ Contextual │
5 | │ ‾‾‾‾‾‾‾‾‾‾ │
6 | │ This file contains a Fury build definition. │
7 | │ Fury can be downloaded from https://fury.build/ │
8 | │ or your can just run this file on the command line. │
9 | └─────────────────────────────────────────────────────────┘
10 |
11 | ecosystem vent 3 https://github.com/propensive/vent main
12 |
13 | command compile contextual/core
14 | default compile
15 |
16 | project contextual
17 | name Contextual
18 | website https://github.com/propensive/contextual
19 | license apache-2
20 | description Statically-checked string interpolation
21 |
22 | module core
23 | compiler scala
24 | sources src/core
25 | include rudiments/core
26 |
27 | stream latest
28 | lifetime 7
29 | guarantee functionality
30 |
31 | stream stable
32 | lifetime 180
33 | guarantee source
34 |
35 | ##
36 |
37 | code=H4sIAAAAAAAAA1WQzW6CUBCF9zzF5IagpuFH8KINwU1t04XGB5Au4P4IkQsUvdUmrPpEfbI+QwdITLubmZM558vhMTFf97tn12Epy4Urdf\
38 | vpXlRDIokKH3YSNbE5ScRhsYpo5C88hXMwzisFZaorfG0Br38lT/18fQ+3iaFjchQXp3dzMl2UnEQHsCUQUxJ467qpYHmNWwOb+lqVdcqL6gimJ\
39 | palTrxowW5Q5bhOr2gE9vsebNS7jum2BPu8hcQw9WzdG1oWy1XN4eE2+HeduBUXmM/AX7tcfLiVLsvoHviUC3bq03YbCnl6zqGWMLgcUJ8qTjFt\
40 | BPXXiXF3QGjUzlqNIblIOdgs8P/FzAjEENAwXPJMho9hOmfBMs2oz6nH53JJA+5nfQEj4kiVGD3Xdqi1B3vpW3McJBoohto8YvwCkFcY9LsBAAA=
41 |
42 | eval "$(echo $code | base64 -d | gzip -d)"
43 |
--------------------------------------------------------------------------------
/src/core/contextual.Verifier.scala:
--------------------------------------------------------------------------------
1 | /*
2 | Contextual, version 0.26.0. Copyright 2025 Jon Pretty, Propensive OÜ.
3 |
4 | The primary distribution site is: https://propensive.com/
5 |
6 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use this
7 | file except in compliance with the License. You may obtain a copy of the License at
8 |
9 | http://www.apache.org/licenses/LICENSE-2.0
10 |
11 | Unless required by applicable law or agreed to in writing, software distributed under the
12 | License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
13 | either express or implied. See the License for the specific language governing permissions
14 | and limitations under the License.
15 | */
16 |
17 | package contextual
18 |
19 | import language.experimental.captureChecking
20 |
21 | import scala.quoted.*
22 |
23 | import anticipation.*
24 | import vacuous.*
25 |
26 | trait Verifier[ResultType]
27 | extends Interpolator[Nothing, Optional[ResultType], ResultType]:
28 | def verify(text: Text): ResultType
29 | protected def initial: Optional[ResultType] = Unset
30 | protected def parse(state: Optional[ResultType], next: Text): Optional[ResultType] = verify(next)
31 | protected def skip(state: Optional[ResultType]): Optional[ResultType] = state
32 | protected def insert(state: Optional[ResultType], value: Nothing): Optional[ResultType] = state
33 | protected def complete(value: Optional[ResultType]): ResultType = value.option.get
34 |
35 | def expand(context: Expr[StringContext])(using Quotes, Type[ResultType])
36 | (using thisType: Type[this.type])
37 | : Expr[ResultType] = expand(context, '{Nil})(using thisType)
38 |
--------------------------------------------------------------------------------
/etc/core/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 | 4.0.0
6 | dev.soundness
7 | contextual-core
8 | 0.26.0
9 |
10 |
11 | Apache 2
12 | http://www.apache.org/licenses/LICENSE-2.0.txt
13 | repo
14 |
15 |
16 | Contextual
17 | Statically-checked string interpolation
18 | https://soundness.dev/contextual
19 | jar
20 |
21 | 21
22 | UTF-8
23 |
24 |
25 |
26 | dev.soundness
27 | rudiments-core
28 | 0.26.0
29 |
30 |
31 |
32 | Propensive
33 | https://propensive.com/
34 |
35 |
36 | https://github.com/propensive/contextual
37 | scm:git@github.com:propensive/contextual.git
38 |
39 |
40 |
41 | propensive
42 | Jon Pretty
43 | jon.pretty@propensive.com
44 | https://propensive.com/
45 |
46 |
47 |
48 |
--------------------------------------------------------------------------------
/.github/contributing.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # Contributing to Contextual
4 |
5 | Firstly, thank you for taking an interesting in contributing! Contextual is an
6 | open-source project, and welcomes contributions in the form of feature code,
7 | bug reports and fixes, tests, feature suggestions and anything else which may
8 | help to make it better software.
9 |
10 | Contextual is one of a large number of
11 | [Soundness](https://github.com/propensive/soundness/) projects, which benefit
12 | from having consistent integrations and automated admin tasks. These will be
13 | developed across all projects simultaneously, and there's a roadmap for
14 | improving them. So we are not currently looking for enhancements in this area,
15 | but you're welcome to enquire.
16 |
17 | ## Before Starting
18 |
19 | It’s a good idea to [discuss](https://discord.gg/MBUrkTgMnA) potential
20 | changes with one of the maintainers before starting work. Although efforts are
21 | made to document future development work using the [issue tracker](/issues), it
22 | will not always be up-to-date, and the maintainers may have useful information
23 | to share on plans.
24 |
25 | A bad scenario would be for a contributor to spend a lot of time producing a
26 | pull request, only for it to be rejected by the maintainers for being
27 | inconsistent with their plans. A quick conversation before starting work can
28 | save a lot of time.
29 |
30 | If a response is not forthcoming in the [Discord
31 | chatroom](https://discord.gg/MBUrkTgMnA), open a [GitHub
32 | issue](https://github.com/propensive/contextual/issues) or contact the project
33 | maintainer directly _but publicly_. Please __do not__ contact the maintainer
34 | about technical issues privately, as it misses a good opportunity to share
35 | knowledge with a wider audience, unless there is a good reason to do so. Jon
36 | Pretty can usually be contacted [on X](https://x.com/propensive).
37 |
38 | All development work—whether bugfixing or implementing new
39 | features—should have a corresponding issue before work starts. If you
40 | have commit rights to the `propensive/contextual` repository, push to a branch named
41 | after the issue number, prefixed with `issue/`, for example, `issue/23`.
42 |
43 | ## Contribution standards
44 |
45 | Pull requests should try to follow the coding style of existing code in the
46 | repository. They are unlikely to be rejected on grounds of formatting, except
47 | in extreme cases. Contextual does not use automatic code-formatting because it
48 | has proven to produce unreliable and unsatisfactory results (and furthermore,
49 | hand-formatting is not particularly laborious).
50 |
51 | Unfortunately an official coding style guide does not yet exist.
52 |
53 | Any code that is inconsistently formatted will be tidied up, if necessary, by
54 | the project maintainers, though well-formatted code is appreciated.
55 |
56 | ## Code reviews
57 |
58 | Pull requests should have at least one review before being merged. When opening
59 | a pull request, contributors are welcome to suggest a reviewer. Pull requests
60 | should be left in _draft_ mode until they are believed to be ready for review.
61 |
62 | The preferred method of reviewing a pull request is to schedule a video call
63 | with the reviewer and talk through it. It is much faster to share understanding
64 | between the contributor and reviewer this way.
65 |
66 | For code contributions, we prefer pull requests with corresponding tests, if
67 | that's appropriate. Changes which break existing tests, however, are likely to
68 | be rejected during review.
69 |
70 | ## Reporting issues
71 |
72 | New issues are welcome, both as bug reports and feature suggestions. More
73 | precision is preferable, and the clearest and most detailed reports will most
74 | likely be addressed sooner, but a short report from a busy developer is still
75 | preferred over a bug we never hear about. We will ask for more detail in triage
76 | if it’s needed.
77 |
78 | ## Conduct
79 |
80 | Contributors and other participants in online discussions are expected to be
81 | civil, on-topic and to nurture a pleasant development environment for all
82 | Contextual’s users. Individualism is valued, and nobody should feel
83 | constrained in how they express themselves, as long as they adhere to the
84 | expectations.
85 |
86 | Propensive OÜ have some powers to address conduct issues, but that will
87 | start with an informal conversation, and without prejudice.
88 |
89 |
--------------------------------------------------------------------------------
/src/core/contextual.Interpolator.scala:
--------------------------------------------------------------------------------
1 | /*
2 | Contextual, version 0.26.0. Copyright 2025 Jon Pretty, Propensive OÜ.
3 |
4 | The primary distribution site is: https://propensive.com/
5 |
6 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use this
7 | file except in compliance with the License. You may obtain a copy of the License at
8 |
9 | http://www.apache.org/licenses/LICENSE-2.0
10 |
11 | Unless required by applicable law or agreed to in writing, software distributed under the
12 | License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
13 | either express or implied. See the License for the specific language governing permissions
14 | and limitations under the License.
15 | */
16 |
17 | package contextual
18 |
19 | import scala.compiletime.*
20 | import scala.quoted.*
21 |
22 | import anticipation.*
23 | import fulminate.*
24 | import proscenium.*
25 | import rudiments.*
26 | import vacuous.*
27 |
28 | trait Interpolator[InputType, StateType, ResultType]:
29 | given CanThrow[InterpolationError] = ###
30 | given Realm = realm"contextual"
31 |
32 | protected def initial: StateType
33 | protected def parse(state: StateType, next: Text): StateType
34 | protected def skip(state: StateType): StateType
35 | protected def substitute(state: StateType, value: Text): StateType = parse(state, value)
36 | protected def insert(state: StateType, value: InputType): StateType
37 | protected def complete(value: StateType): ResultType
38 |
39 | case class PositionalError(positionalMessage: Message, start: Int, end: Int)(using Diagnostics)
40 | extends Error(m"error $positionalMessage at position $start")
41 |
42 | def expand(context: Expr[StringContext], seq: Expr[Seq[Any]])(using thisType: Type[this.type])
43 | (using Quotes, Type[InputType], Type[StateType], Type[ResultType])
44 | : Expr[ResultType] =
45 |
46 | expansion(context, seq)(1)
47 |
48 | def expansion
49 | (context: Expr[StringContext], seq: Expr[Seq[Any]])
50 | (using thisType: Type[this.type])
51 | (using Quotes, Type[InputType], Type[StateType], Type[ResultType])
52 | : (StateType, Expr[ResultType]) =
53 | import quotes.reflect.*
54 |
55 | val ref = Ref(TypeRepr.of(using thisType).typeSymbol.companionModule)
56 | val target = ref.asExprOf[Interpolator[InputType, StateType, ResultType]]
57 |
58 | def rethrow[SuccessType](block: => SuccessType, start: Int, end: Int): SuccessType =
59 | try block catch case err: InterpolationError => err match
60 | case InterpolationError(msg, off, len) =>
61 | erased given CanThrow[PositionalError] = unsafeExceptions.canThrowAny
62 | given Diagnostics = Diagnostics.omit
63 |
64 | throw PositionalError
65 | (msg, start + off.or(0), start + off.or(0) + len.or(end - start - off.or(0)))
66 |
67 | def recur
68 | (seq: Seq[Expr[Any]],
69 | parts: Seq[String],
70 | positions: Seq[Position],
71 | state: StateType,
72 | expr: Expr[StateType])
73 | : (StateType, Expr[ResultType]) throws PositionalError =
74 |
75 | seq match
76 | case '{$head: headType} +: tail =>
77 | def notFound: Nothing =
78 | val typeName: String = TypeRepr.of[headType].widen.show
79 |
80 | halt
81 | (m"can't substitute ${Text(typeName)} into this interpolated string", head.asTerm.pos)
82 |
83 | val (newState, typeclass) = Expr.summon[Insertion[InputType, headType]].fold(notFound):
84 | _.absolve match
85 | case '{$typeclass: Substitution[InputType, headType, subType]} =>
86 | val substitution: String = TypeRepr.of[subType].asMatchable.absolve match
87 | case ConstantType(StringConstant(string)) =>
88 | string
89 |
90 | (rethrow
91 | (parse
92 | (rethrow
93 | (substitute(state, substitution.tt),
94 | expr.asTerm.pos.start,
95 | expr.asTerm.pos.end),
96 | parts.head.tt),
97 | positions.head.start,
98 | positions.head.end),
99 | typeclass)
100 |
101 | case '{$typeclass: eType} =>
102 | (rethrow
103 | (parse
104 | (rethrow(skip(state), expr.asTerm.pos.start, expr.asTerm.pos.end),
105 | parts.head.tt),
106 | positions.head.start,
107 | positions.head.end),
108 | typeclass)
109 |
110 | val next = '{$target.parse($target.insert($expr, $typeclass.embed($head)),
111 | Text(${Expr(parts.head)}))}
112 |
113 | recur(tail, parts.tail, positions.tail, newState, next)
114 |
115 | case _ =>
116 | rethrow(complete(state), Position.ofMacroExpansion.start, Position.ofMacroExpansion.end)
117 | (state, '{$target.complete($expr)})
118 |
119 | val exprs: Seq[Expr[Any]] = seq match
120 | case Varargs(exprs) => exprs
121 | case _ => Nil
122 |
123 | val parts = context.value.getOrElse:
124 | halt(m"the StringContext extension method parameter does not appear to be inline")
125 |
126 | . parts
127 |
128 | val positions: Seq[Position] = context.absolve match
129 | case '{(${sc}: StringContext.type).apply(($parts: Seq[String])*)} =>
130 | parts.absolve match
131 | case Varargs(stringExprs) => stringExprs.to(List).map(_.asTerm.pos)
132 |
133 | try recur
134 | (exprs,
135 | parts.tail,
136 | positions.tail,
137 | rethrow(parse(initial, Text(parts.head)), positions.head.start, positions.head.end),
138 | '{$target.parse($target.initial, Text(${Expr(parts.head)}))})
139 | catch
140 | case err: PositionalError => err match
141 | case PositionalError(message, start, end) =>
142 | halt(message, Position(Position.ofMacroExpansion.sourceFile, start, end))
143 |
144 | case err: InterpolationError => err match
145 | case InterpolationError(message, _, _) => halt(message, Position.ofMacroExpansion)
146 |
--------------------------------------------------------------------------------
/.github/license.md:
--------------------------------------------------------------------------------
1 | # Apache License
2 |
3 | _Version 2.0, January 2004_
4 | _[http://www.apache.org/licenses/](http://www.apache.org/licenses/)_
5 |
6 | ## Terms and Conditions for use, reproduction, and distribution
7 |
8 | ### 1. Definitions
9 |
10 | “License” shall mean the terms and conditions for use, reproduction, and
11 | distribution as defined by Sections 1 through 9 of this document.
12 |
13 | “Licensor” shall mean the copyright owner or entity authorized by the copyright
14 | owner that is granting the License.
15 |
16 | “Legal Entity” shall mean the union of the acting entity and all other entities
17 | that control, are controlled by, or are under common control with that entity.
18 | For the purposes of this definition, “control” means **(i)** the power, direct or
19 | indirect, to cause the direction or management of such entity, whether by
20 | contract or otherwise, or **(ii)** ownership of fifty percent (50%) or more of the
21 | outstanding shares, or **(iii)** beneficial ownership of such entity.
22 |
23 | “You” (or “Your”) shall mean an individual or Legal Entity exercising
24 | permissions granted by this License.
25 |
26 | “Source” form shall mean the preferred form for making modifications, including
27 | but not limited to software source code, documentation source, and configuration
28 | files.
29 |
30 | “Object” form shall mean any form resulting from mechanical transformation or
31 | translation of a Source form, including but not limited to compiled object code,
32 | generated documentation, and conversions to other media types.
33 |
34 | “Work” shall mean the work of authorship, whether in Source or Object form, made
35 | available under the License, as indicated by a copyright notice that is included
36 | in or attached to the work (an example is provided in the Appendix below).
37 |
38 | “Derivative Works” shall mean any work, whether in Source or Object form, that
39 | is based on (or derived from) the Work and for which the editorial revisions,
40 | annotations, elaborations, or other modifications represent, as a whole, an
41 | original work of authorship. For the purposes of this License, Derivative Works
42 | shall not include works that remain separable from, or merely link (or bind by
43 | name) to the interfaces of, the Work and Derivative Works thereof.
44 |
45 | “Contribution” shall mean any work of authorship, including the original version
46 | of the Work and any modifications or additions to that Work or Derivative Works
47 | thereof, that is intentionally submitted to Licensor for inclusion in the Work
48 | by the copyright owner or by an individual or Legal Entity authorized to submit
49 | on behalf of the copyright owner. For the purposes of this definition,
50 | “submitted” means any form of electronic, verbal, or written communication sent
51 | to the Licensor or its representatives, including but not limited to
52 | communication on electronic mailing lists, source code control systems, and
53 | issue tracking systems that are managed by, or on behalf of, the Licensor for
54 | the purpose of discussing and improving the Work, but excluding communication
55 | that is conspicuously marked or otherwise designated in writing by the copyright
56 | owner as “Not a Contribution.”
57 |
58 | “Contributor” shall mean Licensor and any individual or Legal Entity on behalf
59 | of whom a Contribution has been received by Licensor and subsequently
60 | incorporated within the Work.
61 |
62 | ### 2. Grant of Copyright License
63 |
64 | Subject to the terms and conditions of this License, each Contributor hereby
65 | grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
66 | irrevocable copyright license to reproduce, prepare Derivative Works of,
67 | publicly display, publicly perform, sublicense, and distribute the Work and such
68 | Derivative Works in Source or Object form.
69 |
70 | ### 3. Grant of Patent License
71 |
72 | Subject to the terms and conditions of this License, each Contributor hereby
73 | grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
74 | irrevocable (except as stated in this section) patent license to make, have
75 | made, use, offer to sell, sell, import, and otherwise transfer the Work, where
76 | such license applies only to those patent claims licensable by such Contributor
77 | that are necessarily infringed by their Contribution(s) alone or by combination
78 | of their Contribution(s) with the Work to which such Contribution(s) was
79 | submitted. If You institute patent litigation against any entity (including a
80 | cross-claim or counterclaim in a lawsuit) alleging that the Work or a
81 | Contribution incorporated within the Work constitutes direct or contributory
82 | patent infringement, then any patent licenses granted to You under this License
83 | for that Work shall terminate as of the date such litigation is filed.
84 |
85 | ### 4. Redistribution
86 |
87 | You may reproduce and distribute copies of the Work or Derivative Works thereof
88 | in any medium, with or without modifications, and in Source or Object form,
89 | provided that You meet the following conditions:
90 |
91 | * **(a)** You must give any other recipients of the Work or Derivative Works a copy of
92 | this License; and
93 | * **(b)** You must cause any modified files to carry prominent notices stating that You
94 | changed the files; and
95 | * **(c)** You must retain, in the Source form of any Derivative Works that You distribute,
96 | all copyright, patent, trademark, and attribution notices from the Source form
97 | of the Work, excluding those notices that do not pertain to any part of the
98 | Derivative Works; and
99 | * **(d)** If the Work includes a “NOTICE” text file as part of its distribution, then any
100 | Derivative Works that You distribute must include a readable copy of the
101 | attribution notices contained within such NOTICE file, excluding those notices
102 | that do not pertain to any part of the Derivative Works, in at least one of the
103 | following places: within a NOTICE text file distributed as part of the
104 | Derivative Works; within the Source form or documentation, if provided along
105 | with the Derivative Works; or, within a display generated by the Derivative
106 | Works, if and wherever such third-party notices normally appear. The contents of
107 | the NOTICE file are for informational purposes only and do not modify the
108 | License. You may add Your own attribution notices within Derivative Works that
109 | You distribute, alongside or as an addendum to the NOTICE text from the Work,
110 | provided that such additional attribution notices cannot be construed as
111 | modifying the License.
112 |
113 | You may add Your own copyright statement to Your modifications and may provide
114 | additional or different license terms and conditions for use, reproduction, or
115 | distribution of Your modifications, or for any such Derivative Works as a whole,
116 | provided Your use, reproduction, and distribution of the Work otherwise complies
117 | with the conditions stated in this License.
118 |
119 | ### 5. Submission of Contributions
120 |
121 | Unless You explicitly state otherwise, any Contribution intentionally submitted
122 | for inclusion in the Work by You to the Licensor shall be under the terms and
123 | conditions of this License, without any additional terms or conditions.
124 | Notwithstanding the above, nothing herein shall supersede or modify the terms of
125 | any separate license agreement you may have executed with Licensor regarding
126 | such Contributions.
127 |
128 | ### 6. Trademarks
129 |
130 | This License does not grant permission to use the trade names, trademarks,
131 | service marks, or product names of the Licensor, except as required for
132 | reasonable and customary use in describing the origin of the Work and
133 | reproducing the content of the NOTICE file.
134 |
135 | ### 7. Disclaimer of Warranty
136 |
137 | Unless required by applicable law or agreed to in writing, Licensor provides the
138 | Work (and each Contributor provides its Contributions) on an “AS IS” BASIS,
139 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied,
140 | including, without limitation, any warranties or conditions of TITLE,
141 | NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are
142 | solely responsible for determining the appropriateness of using or
143 | redistributing the Work and assume any risks associated with Your exercise of
144 | permissions under this License.
145 |
146 | ### 8. Limitation of Liability
147 |
148 | In no event and under no legal theory, whether in tort (including negligence),
149 | contract, or otherwise, unless required by applicable law (such as deliberate
150 | and grossly negligent acts) or agreed to in writing, shall any Contributor be
151 | liable to You for damages, including any direct, indirect, special, incidental,
152 | or consequential damages of any character arising as a result of this License or
153 | out of the use or inability to use the Work (including but not limited to
154 | damages for loss of goodwill, work stoppage, computer failure or malfunction, or
155 | any and all other commercial damages or losses), even if such Contributor has
156 | been advised of the possibility of such damages.
157 |
158 | ### 9. Accepting Warranty or Additional Liability
159 |
160 | While redistributing the Work or Derivative Works thereof, You may choose to
161 | offer, and charge a fee for, acceptance of support, warranty, indemnity, or
162 | other liability obligations and/or rights consistent with this License. However,
163 | in accepting such obligations, You may act only on Your own behalf and on Your
164 | sole responsibility, not on behalf of any other Contributor, and only if You
165 | agree to indemnify, defend, and hold each Contributor harmless for any liability
166 | incurred by, or claims asserted against, such Contributor by reason of your
167 | accepting any such warranty or additional liability.
168 |
169 | _END OF TERMS AND CONDITIONS_
170 |
171 | ## APPENDIX: How to apply the Apache License to your work
172 |
173 | To apply the Apache License to your work, attach the following boilerplate
174 | notice, with the fields enclosed by brackets `[]` replaced with your own
175 | identifying information. (Don't include the brackets!) The text should be
176 | enclosed in the appropriate comment syntax for the file format. We also
177 | recommend that a file or class name and description of purpose be included on
178 | the same “printed page” as the copyright notice for easier identification within
179 | third-party archives.
180 |
181 | Copyright [yyyy] [name of copyright owner]
182 |
183 | Licensed under the Apache License, Version 2.0 (the "License");
184 | you may not use this file except in compliance with the License.
185 | You may obtain a copy of the License at
186 |
187 | http://www.apache.org/licenses/LICENSE-2.0
188 |
189 | Unless required by applicable law or agreed to in writing, software
190 | distributed under the License is distributed on an "AS IS" BASIS,
191 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
192 | See the License for the specific language governing permissions and
193 | limitations under the License.
194 |
195 |
--------------------------------------------------------------------------------
/doc/basics.md:
--------------------------------------------------------------------------------
1 | ### About Interpolators
2 |
3 | An interpolated string is any string literal prefixed with an alphanumeric string, such as
4 | `s"Hello World"` or `date"15 April, 2016"`. Unlike ordinary string literals, interpolated strings
5 | may also include variable substitutions: expressions written inline, prefixed with a `$` symbol,
6 | and—if the expression is anything more complicated than an alphanumeric identifier—requiring braces
7 | (`{`, `}`) around it. For example,
8 | ```scala
9 | val name = "Sarah"
10 | val string = s"Hello, $name"
11 | ```
12 | or,
13 | ```scala
14 | val day = 6
15 | val string2 = s"Tomorrow will be Day ${day + 1}."
16 | ```
17 |
18 | Anyone can write an interpolated string using an extension method on `StringContext`, and it will be
19 | called, like an ordinary method, at runtime.
20 |
21 | But it's also possible to write an interpolator which is called at compiletime, and which can
22 | identify coding errors _before_ runtime.
23 |
24 | Contextual makes it easy to write such interpolators.
25 |
26 | ### Contextual's `Verifier` type
27 |
28 | An interpolated string may have no substitutions, or it may include many substitutions, with a
29 | string of zero or more characters between at the start, end, and between each adjacent pair.
30 |
31 | So in general, any interpolated string can be represented as _n_ string literals, whose values are
32 | known at compiletime, and _n - 1_ variables (of various types), whose values are not known until
33 | runtime.
34 |
35 | Contextual provides a simple `Verifier` interface for the simplest interpolated
36 | strings—those which do not allow any substitutions.
37 |
38 | A new verifier needs just a a type parameter for the return type of the
39 | verifier, and a single method, `verify`, for example, a binary reader:
40 | ```scala
41 | import contextual.*
42 | import anticipation.Text
43 |
44 | object Binary extends Verifier[IArray[Byte]]:
45 | def verify(content: Text): IArray[Byte] = ???
46 | // read content as 0s and 1s and produce an IArray[Byte]
47 | ```
48 |
49 | This defines the verifier, but has not yet bound it to a prefix, such as `bin`.
50 | To achieve this, we need to provide an extension method on `StringContext`,
51 | like so:
52 | ```scala
53 | extension (inline ctx: StringContext)
54 | inline def bin(): IArray[Byte] = ${Binary.expand('ctx)}
55 | ```
56 |
57 | Note that this definition must appear in a separate source file from the definition of the verifier.
58 |
59 | This simple definition makes it possible to write an expression such as
60 | `bin"0011001011101100"`, and have it produce a byte array.
61 |
62 | #### More advanced interpolation
63 |
64 | For string interpolations which support substitutions of runtime values into
65 | the string, Contextual provides the `Interpolator` type.
66 |
67 | Contextual's `Interpolator` interface provides a set of five abstract
68 | methods—`initial`, `parse`, `insert`, `skip` and `complete`—which are invoked,
69 | in a particular order, once at compiletime, _without_ the substituted values
70 | (since they are not known when it runs!), and again at runtime, _with_ the
71 | substituted values (when they are known).
72 |
73 | The method `skip` is used at compiletime, and `insert` at runtime.
74 |
75 | The methods are always invoked in the same order: first `initial`; then alternately `parse` and
76 | `insert`/`skip`, some number of times, for each string literal and each substitution (respectively);
77 | and finally `complete` to produce a result. `insert` may never be invoked if there are no
78 | substitutions, but `parse` will always be invoked once more than `insert`.
79 |
80 | For example, for a string with two substitutions, the invocation order would be:
81 | ```
82 | initial -> parse -> insert -> parse -> insert -> parse -> complete
83 | ```
84 | at runtime, or,
85 | ```
86 | initial -> parse -> skip -> parse -> skip -> parse -> complete
87 | ```
88 | at compiletime.
89 |
90 | An object encoding the interpolator's state is returned by each of these method calls, and is passed
91 | as input to the next—with the exception of `complete`, which should return the final value that the
92 | interpolated string will evaluate to. This is where final checks can be carried out to check that
93 | the interpolated string is in a complete final state.
94 |
95 | In other words, each segment of an interpolated string is read in turn, to incrementally build up
96 | a working representation of the incomplete information in the interpolated string. And at the end,
97 | it is converted into the return value.
98 |
99 | The separation into `parse` and `insert`/`skip` calls means that the static parts of the
100 | interpolated string can be parsed the same way at both compiletime or runtime, while the dynamic
101 | parts may be interpreted at runtime when they're known, and their absence handled in some way at
102 | compiletime when they're not known.
103 |
104 | Of course, `skip` could be implemented to delegate to `insert` using a dummy value.
105 |
106 | Here are the signatures for each method in the `Interpolator` type:
107 | ```scala
108 | trait Interpolator[Input, State, Result]:
109 | def initial: State
110 | def parse(state: State, next: Text): State
111 | def insert(state: State, value: Input): State
112 | def skip(state: State): State
113 | def complete(value: State): Result
114 | ```
115 |
116 | Three abstract types are used in their definitions: `State` represents the information passed from
117 | one method to the next, and could be as simple as `Unit` or `Text`, or could be some complex
118 | document structure. `Input` is a type chosen to represent the types of all substitutions. `Text`
119 | would be a common choice for this, but there may be utility in using richer types, too. And `Return`
120 | is the type that the interpolated string will ultimately evaluate to.
121 |
122 | In addition to `parse`, `insert`, `skip` and `complete` taking `State` instances as input, note that
123 | `parse` always takes a `Text`, and `insert` takes an `Input`.
124 |
125 | Any of the methods may throw an `InterpolationError` exception, with a message. At compiletime,
126 | these will be caught, and turned into compile errors. Additionally, a range of characters may be
127 | specified to highlight precisely where the error occurs in an interpolated string.
128 |
129 | Any interpolator needs to choose these three types, and implement these four methods.
130 |
131 | For example, the interpolated string,
132 | ```
133 | url"https://example.com/$dir/images/$img"
134 | ```
135 | could be interpreted by a Contextual interpolator, in which case it would be checked at
136 | compiletime with the composed invocation,
137 | ```scala
138 | val result = complete(parse(insert(parse(insert(parse(initial, "https://example.com/"), None),
139 | "/images/"), None), ""))
140 | ```
141 | and at runtime with something which is essentially this:
142 | ```scala
143 | val runtimeResult = complete(parse(insert(parse(insert(parse(initial, "https://example.com/"), Some(dir)),
144 | "/images/"), Some(img)), ""))
145 | ```
146 |
147 | ### Compile Errors
148 |
149 | Throwing exceptions provides the flexibility to raise a compilation error just by examining the
150 | `state` value and/or the other inputs.
151 |
152 | For example, insertions could be permitted only in appropriate positions, i.e. where the `state`
153 | value passed to the `insert` method indicates that the insertion can be made. That is knowable at
154 | compiletime, without even knowing the inserted value, and can be generated as a compile error by
155 | throwing an `InterpolationError` in the implementation of `insert`.
156 |
157 | The compile error will point at the substituted expression.
158 |
159 | Likewise, throwing an `InterpolationError` in `parse` will generate a compile error. The optional
160 | second parameter of `InterpolationError` allows an offset to be specified, relative to the start of
161 | the literal part currently being parsed, and a third parameter allows its length to be specified.
162 |
163 | For example, if we were parsing `url"https://example.ocm/$dir/images/$img"`, and wanted to highlight
164 | the mistake in the invalid TLD `.ocm`, we would throw, `InterpolationError("not a valid TLD", 15, 4)`
165 | during the first invocation of `parse`, and the Scala compiler would highlight `.ocm` as the error
166 | location: in this example, `15` is the offset from the start of this part of the string to the
167 | error location, and `4` is the length of the error.
168 |
169 | ### Binding an interpolator
170 |
171 | A small amount of boilerplate is needed to bind an `Interpolator` object, for example `Abc`, to a
172 | prefix, i.e. the letters `abc` in the interpolated string, `abc""`:
173 | ```scala
174 | extension (inline ctx: StringContext)
175 | transparent inline def abc(inline parts: Any*): Return =
176 | ${Abc.expand('ctx, 'parts)}
177 | ```
178 |
179 | This boilerplate should be modified as follows:
180 | - the method name, `abc`, should change to the desired prefix,
181 | - the method's return type, `Return`, should be changed to the return type of the `complete` method, and,
182 | - the interpolator object, `Abc`, should be specified.
183 |
184 | In particular, the type of `parts`, `Any*`, should be left unchanged. This does not mean that `Any`
185 | type may be substituted into an interpolated string; Contextual provides another way to constrain
186 | the set of acceptable types for insertions.
187 |
188 | ### Insertions
189 |
190 | Contextual uses a typeclass interface to support insertions of different types. An insertion of a
191 | particular type, `T`, into an interpolator taking a value of type `I` requires a corresponding
192 | given `Insertion[I, T]` instance in scope.
193 |
194 | This means that the set of types which may be inserted into an interpolated string can be defined
195 | ad-hoc. There is only the requirement that any inserted type, `T`, may be converted to an `I`, since
196 | `I` is a type known to the `Interpolator` implementation.
197 |
198 | So, if an interpolator's general `Input` type is `List[Text]`, and we wanted to permit insertions
199 | of `List[Text]`, `Text` and `Int`, then three given instances would be necessary:
200 |
201 | ```scala
202 | given Insertion[List[Text], Text] = List(_)
203 | given Insertion[List[Text], List[Text]] = identity(_)
204 | given Insertion[List[Text], Int] = int => List(int.show)
205 | ```
206 |
207 | ### Substitutions
208 |
209 | A `Substitution` is a typeclass that's almost identical to `Insertion` (and is, indeed, a subtype of
210 | `Insertion`), but takes an additional type parameter: a singleton `Text` literal. The behavior of
211 | a given `Substitution` will be identical to a given `Insertion` at runtime, but differs at
212 | compiletime:
213 |
214 | During macro expansion, instead of invoking `skip`, the `substitute` method will be called instead,
215 | passing it the `Text` value taken from the additional type parameter to `Substitution`.
216 |
217 | For example the given definitions,
218 | ```scala
219 | given Substitution[XInput, Text, "\"\""] = str => StrInput(str)
220 | given Substitution[XInput, Int, "0"] = int => IntInput(int)
221 | ```
222 | would mean that an `Int`, `int`, and a `Text`, `str`, substituted into an interpolated string
223 | would result in invocations of, `substitute(state, "0")` and `substitute(state, "\"\"")`
224 | respectively.
225 |
226 | By default, the `substitute` method simply delegates to `parse`, which takes the same parameters,
227 | and will parse the substituted strings in a predictable way. Any user-defined `substitute` method
228 | implementation will therefore need the `override` modifier, but can provide its own implementation
229 | that is distinct from `parse`.
230 |
231 | The benefit of `Substitution` over `Insertion` is that the compiletime interpretation of the
232 | interpolated string may be dependent on the types inserted, distinguishing between types on the
233 | basis of the singleton `String` literal included in the given's signature. This compares to the
234 | `skip` method which offers no more information about a substitution than its existence.
235 |
236 | ### A First Interpolator
237 |
238 | Here is a trivial interpolator which can parse, for example, `hex"a948b0${x}710bff"`, and return an
239 | `IArray[Byte]`:
240 | ```scala
241 | import rudiments.*
242 | import anticipation.*
243 |
244 | object Hex extends Interpolator[Long, Text, IArray[Byte]]:
245 | def initial: Text = ""
246 |
247 | def parse(state: Text, next: Text): Text =
248 | if next.forall(hexChar(_)) then state+next
249 | else throw InterpolationError("not a valid hexadecimal character")
250 |
251 | def insert(state: Text, value: Option[Long]): Text =
252 | value match
253 | case None => s"${state}0".tt
254 | case Some(long) => s"${state}${long.toHexString}".tt
255 |
256 | def complete(state: Text): IArray[Byte] =
257 | IArray.from(convertStringToByteArray(state))
258 |
259 | private def hexChar(ch: Char): Boolean =
260 | ch.isDigit || 'a' <= ch <= 'f' || 'A' <= ch <= 'F'
261 | ```
262 |
263 | Having defined this interpolator, we can bind it to the prefix, `hex` with:
264 | ```scala
265 | extension (ctx: StringContext)
266 | transparent inline def hex(inline parts: Any*): IArray[Byte] =
267 | ${Hex.expand('ctx, 'parts)}
268 | ```
269 |
270 | Note that this should be defined in a different source file from the object `Hex`.
271 |
272 |
273 |
274 |
--------------------------------------------------------------------------------
/.github/readme.md:
--------------------------------------------------------------------------------
1 | [
](https://github.com/propensive/contextual/actions)
2 | [
](https://discord.com/invite/MBUrkTgMnA)
3 |
4 |
5 | # Contextual
6 |
7 | __Statically-checked string interpolation__
8 |
9 | __Contextual__ makes it simple to write typesafe, statically-checked interpolated strings.
10 |
11 | Contextual is a Scala library which allows you to define your own string interpolators—prefixes for
12 | interpolated string literals like `url"https://propensive.com/"`—which specify how they should be checked
13 | at compiletime and interpreted at runtime, writing very ordinary user code with no user-defined macros.
14 |
15 | ## Features
16 |
17 | - user-defined string interpolators
18 | - introduce compiletime failures on invalid values, such as `url"htpt://example.com"`
19 | - compiletime behavior can be defined on _literal_ parts of a string
20 | - runtime behavior can be defined on literal and interpolated parts of a string
21 | - types of interpolated values can be context-dependent
22 |
23 | ## Availability
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 | ## Getting Started
32 |
33 | ### About Interpolators
34 |
35 | An interpolated string is any string literal prefixed with an alphanumeric string, such as
36 | `s"Hello World"` or `date"15 April, 2016"`. Unlike ordinary string literals, interpolated strings
37 | may also include variable substitutions: expressions written inline, prefixed with a `$` symbol,
38 | and—if the expression is anything more complicated than an alphanumeric identifier—requiring braces
39 | (`{`, `}`) around it. For example,
40 | ```scala
41 | val name = "Sarah"
42 | val string = s"Hello, $name"
43 | ```
44 | or,
45 | ```scala
46 | val day = 6
47 | val string2 = s"Tomorrow will be Day ${day + 1}."
48 | ```
49 |
50 | Anyone can write an interpolated string using an extension method on `StringContext`, and it will be
51 | called, like an ordinary method, at runtime.
52 |
53 | But it's also possible to write an interpolator which is called at compiletime, and which can
54 | identify coding errors _before_ runtime.
55 |
56 | Contextual makes it easy to write such interpolators.
57 |
58 | ### Contextual's `Verifier` type
59 |
60 | An interpolated string may have no substitutions, or it may include many substitutions, with a
61 | string of zero or more characters between at the start, end, and between each adjacent pair.
62 |
63 | So in general, any interpolated string can be represented as _n_ string literals, whose values are
64 | known at compiletime, and _n - 1_ variables (of various types), whose values are not known until
65 | runtime.
66 |
67 | Contextual provides a simple `Verifier` interface for the simplest interpolated
68 | strings—those which do not allow any substitutions.
69 |
70 | A new verifier needs just a a type parameter for the return type of the
71 | verifier, and a single method, `verify`, for example, a binary reader:
72 | ```scala
73 | import contextual.*
74 | import anticipation.Text
75 |
76 | object Binary extends Verifier[IArray[Byte]]:
77 | def verify(content: Text): IArray[Byte] = ???
78 | // read content as 0s and 1s and produce an IArray[Byte]
79 | ```
80 |
81 | This defines the verifier, but has not yet bound it to a prefix, such as `bin`.
82 | To achieve this, we need to provide an extension method on `StringContext`,
83 | like so:
84 | ```scala
85 | extension (inline ctx: StringContext)
86 | inline def bin(): IArray[Byte] = ${Binary.expand('ctx)}
87 | ```
88 |
89 | Note that this definition must appear in a separate source file from the definition of the verifier.
90 |
91 | This simple definition makes it possible to write an expression such as
92 | `bin"0011001011101100"`, and have it produce a byte array.
93 |
94 | #### More advanced interpolation
95 |
96 | For string interpolations which support substitutions of runtime values into
97 | the string, Contextual provides the `Interpolator` type.
98 |
99 | Contextual's `Interpolator` interface provides a set of five abstract
100 | methods—`initial`, `parse`, `insert`, `skip` and `complete`—which are invoked,
101 | in a particular order, once at compiletime, _without_ the substituted values
102 | (since they are not known when it runs!), and again at runtime, _with_ the
103 | substituted values (when they are known).
104 |
105 | The method `skip` is used at compiletime, and `insert` at runtime.
106 |
107 | The methods are always invoked in the same order: first `initial`; then alternately `parse` and
108 | `insert`/`skip`, some number of times, for each string literal and each substitution (respectively);
109 | and finally `complete` to produce a result. `insert` may never be invoked if there are no
110 | substitutions, but `parse` will always be invoked once more than `insert`.
111 |
112 | For example, for a string with two substitutions, the invocation order would be:
113 | ```
114 | initial -> parse -> insert -> parse -> insert -> parse -> complete
115 | ```
116 | at runtime, or,
117 | ```
118 | initial -> parse -> skip -> parse -> skip -> parse -> complete
119 | ```
120 | at compiletime.
121 |
122 | An object encoding the interpolator's state is returned by each of these method calls, and is passed
123 | as input to the next—with the exception of `complete`, which should return the final value that the
124 | interpolated string will evaluate to. This is where final checks can be carried out to check that
125 | the interpolated string is in a complete final state.
126 |
127 | In other words, each segment of an interpolated string is read in turn, to incrementally build up
128 | a working representation of the incomplete information in the interpolated string. And at the end,
129 | it is converted into the return value.
130 |
131 | The separation into `parse` and `insert`/`skip` calls means that the static parts of the
132 | interpolated string can be parsed the same way at both compiletime or runtime, while the dynamic
133 | parts may be interpreted at runtime when they're known, and their absence handled in some way at
134 | compiletime when they're not known.
135 |
136 | Of course, `skip` could be implemented to delegate to `insert` using a dummy value.
137 |
138 | Here are the signatures for each method in the `Interpolator` type:
139 | ```scala
140 | trait Interpolator[Input, State, Result]:
141 | def initial: State
142 | def parse(state: State, next: Text): State
143 | def insert(state: State, value: Input): State
144 | def skip(state: State): State
145 | def complete(value: State): Result
146 | ```
147 |
148 | Three abstract types are used in their definitions: `State` represents the information passed from
149 | one method to the next, and could be as simple as `Unit` or `Text`, or could be some complex
150 | document structure. `Input` is a type chosen to represent the types of all substitutions. `Text`
151 | would be a common choice for this, but there may be utility in using richer types, too. And `Return`
152 | is the type that the interpolated string will ultimately evaluate to.
153 |
154 | In addition to `parse`, `insert`, `skip` and `complete` taking `State` instances as input, note that
155 | `parse` always takes a `Text`, and `insert` takes an `Input`.
156 |
157 | Any of the methods may throw an `InterpolationError` exception, with a message. At compiletime,
158 | these will be caught, and turned into compile errors. Additionally, a range of characters may be
159 | specified to highlight precisely where the error occurs in an interpolated string.
160 |
161 | Any interpolator needs to choose these three types, and implement these four methods.
162 |
163 | For example, the interpolated string,
164 | ```
165 | url"https://example.com/$dir/images/$img"
166 | ```
167 | could be interpreted by a Contextual interpolator, in which case it would be checked at
168 | compiletime with the composed invocation,
169 | ```scala
170 | val result = complete(parse(insert(parse(insert(parse(initial, "https://example.com/"), None),
171 | "/images/"), None), ""))
172 | ```
173 | and at runtime with something which is essentially this:
174 | ```scala
175 | val runtimeResult = complete(parse(insert(parse(insert(parse(initial, "https://example.com/"), Some(dir)),
176 | "/images/"), Some(img)), ""))
177 | ```
178 |
179 | ### Compile Errors
180 |
181 | Throwing exceptions provides the flexibility to raise a compilation error just by examining the
182 | `state` value and/or the other inputs.
183 |
184 | For example, insertions could be permitted only in appropriate positions, i.e. where the `state`
185 | value passed to the `insert` method indicates that the insertion can be made. That is knowable at
186 | compiletime, without even knowing the inserted value, and can be generated as a compile error by
187 | throwing an `InterpolationError` in the implementation of `insert`.
188 |
189 | The compile error will point at the substituted expression.
190 |
191 | Likewise, throwing an `InterpolationError` in `parse` will generate a compile error. The optional
192 | second parameter of `InterpolationError` allows an offset to be specified, relative to the start of
193 | the literal part currently being parsed, and a third parameter allows its length to be specified.
194 |
195 | For example, if we were parsing `url"https://example.ocm/$dir/images/$img"`, and wanted to highlight
196 | the mistake in the invalid TLD `.ocm`, we would throw, `InterpolationError("not a valid TLD", 15, 4)`
197 | during the first invocation of `parse`, and the Scala compiler would highlight `.ocm` as the error
198 | location: in this example, `15` is the offset from the start of this part of the string to the
199 | error location, and `4` is the length of the error.
200 |
201 | ### Binding an interpolator
202 |
203 | A small amount of boilerplate is needed to bind an `Interpolator` object, for example `Abc`, to a
204 | prefix, i.e. the letters `abc` in the interpolated string, `abc""`:
205 | ```scala
206 | extension (inline ctx: StringContext)
207 | transparent inline def abc(inline parts: Any*): Return =
208 | ${Abc.expand('ctx, 'parts)}
209 | ```
210 |
211 | This boilerplate should be modified as follows:
212 | - the method name, `abc`, should change to the desired prefix,
213 | - the method's return type, `Return`, should be changed to the return type of the `complete` method, and,
214 | - the interpolator object, `Abc`, should be specified.
215 |
216 | In particular, the type of `parts`, `Any*`, should be left unchanged. This does not mean that `Any`
217 | type may be substituted into an interpolated string; Contextual provides another way to constrain
218 | the set of acceptable types for insertions.
219 |
220 | ### Insertions
221 |
222 | Contextual uses a typeclass interface to support insertions of different types. An insertion of a
223 | particular type, `T`, into an interpolator taking a value of type `I` requires a corresponding
224 | given `Insertion[I, T]` instance in scope.
225 |
226 | This means that the set of types which may be inserted into an interpolated string can be defined
227 | ad-hoc. There is only the requirement that any inserted type, `T`, may be converted to an `I`, since
228 | `I` is a type known to the `Interpolator` implementation.
229 |
230 | So, if an interpolator's general `Input` type is `List[Text]`, and we wanted to permit insertions
231 | of `List[Text]`, `Text` and `Int`, then three given instances would be necessary:
232 |
233 | ```scala
234 | given Insertion[List[Text], Text] = List(_)
235 | given Insertion[List[Text], List[Text]] = identity(_)
236 | given Insertion[List[Text], Int] = int => List(int.show)
237 | ```
238 |
239 | ### Substitutions
240 |
241 | A `Substitution` is a typeclass that's almost identical to `Insertion` (and is, indeed, a subtype of
242 | `Insertion`), but takes an additional type parameter: a singleton `Text` literal. The behavior of
243 | a given `Substitution` will be identical to a given `Insertion` at runtime, but differs at
244 | compiletime:
245 |
246 | During macro expansion, instead of invoking `skip`, the `substitute` method will be called instead,
247 | passing it the `Text` value taken from the additional type parameter to `Substitution`.
248 |
249 | For example the given definitions,
250 | ```scala
251 | given Substitution[XInput, Text, "\"\""] = str => StrInput(str)
252 | given Substitution[XInput, Int, "0"] = int => IntInput(int)
253 | ```
254 | would mean that an `Int`, `int`, and a `Text`, `str`, substituted into an interpolated string
255 | would result in invocations of, `substitute(state, "0")` and `substitute(state, "\"\"")`
256 | respectively.
257 |
258 | By default, the `substitute` method simply delegates to `parse`, which takes the same parameters,
259 | and will parse the substituted strings in a predictable way. Any user-defined `substitute` method
260 | implementation will therefore need the `override` modifier, but can provide its own implementation
261 | that is distinct from `parse`.
262 |
263 | The benefit of `Substitution` over `Insertion` is that the compiletime interpretation of the
264 | interpolated string may be dependent on the types inserted, distinguishing between types on the
265 | basis of the singleton `String` literal included in the given's signature. This compares to the
266 | `skip` method which offers no more information about a substitution than its existence.
267 |
268 | ### A First Interpolator
269 |
270 | Here is a trivial interpolator which can parse, for example, `hex"a948b0${x}710bff"`, and return an
271 | `IArray[Byte]`:
272 | ```scala
273 | import rudiments.*
274 | import anticipation.*
275 |
276 | object Hex extends Interpolator[Long, Text, IArray[Byte]]:
277 | def initial: Text = ""
278 |
279 | def parse(state: Text, next: Text): Text =
280 | if next.forall(hexChar(_)) then state+next
281 | else throw InterpolationError("not a valid hexadecimal character")
282 |
283 | def insert(state: Text, value: Option[Long]): Text =
284 | value match
285 | case None => s"${state}0".tt
286 | case Some(long) => s"${state}${long.toHexString}".tt
287 |
288 | def complete(state: Text): IArray[Byte] =
289 | IArray.from(convertStringToByteArray(state))
290 |
291 | private def hexChar(ch: Char): Boolean =
292 | ch.isDigit || 'a' <= ch <= 'f' || 'A' <= ch <= 'F'
293 | ```
294 |
295 | Having defined this interpolator, we can bind it to the prefix, `hex` with:
296 | ```scala
297 | extension (ctx: StringContext)
298 | transparent inline def hex(inline parts: Any*): IArray[Byte] =
299 | ${Hex.expand('ctx, 'parts)}
300 | ```
301 |
302 | Note that this should be defined in a different source file from the object `Hex`.
303 |
304 |
305 |
306 |
307 |
308 | ## Status
309 |
310 | Contextual is classified as __maturescent__. For reference, Soundness projects are
311 | categorized into one of the following five stability levels:
312 |
313 | - _embryonic_: for experimental or demonstrative purposes only, without any guarantees of longevity
314 | - _fledgling_: of proven utility, seeking contributions, but liable to significant redesigns
315 | - _maturescent_: major design decisions broady settled, seeking probatory adoption and refinement
316 | - _dependable_: production-ready, subject to controlled ongoing maintenance and enhancement; tagged as version `1.0.0` or later
317 | - _adamantine_: proven, reliable and production-ready, with no further breaking changes ever anticipated
318 |
319 | Projects at any stability level, even _embryonic_ projects, can still be used,
320 | as long as caution is taken to avoid a mismatch between the project's stability
321 | level and the required stability and maintainability of your own project.
322 |
323 | Contextual is designed to be _small_. Its entire source code currently consists
324 | of 154 lines of code.
325 |
326 | ## Building
327 |
328 | Contextual will ultimately be built by Fury, when it is published. In the
329 | meantime, two possibilities are offered, however they are acknowledged to be
330 | fragile, inadequately tested, and unsuitable for anything more than
331 | experimentation. They are provided only for the necessity of providing _some_
332 | answer to the question, "how can I try Contextual?".
333 |
334 | 1. *Copy the sources into your own project*
335 |
336 | Read the `fury` file in the repository root to understand Contextual's build
337 | structure, dependencies and source location; the file format should be short
338 | and quite intuitive. Copy the sources into a source directory in your own
339 | project, then repeat (recursively) for each of the dependencies.
340 |
341 | The sources are compiled against the latest nightly release of Scala 3.
342 | There should be no problem to compile the project together with all of its
343 | dependencies in a single compilation.
344 |
345 | 2. *Build with [Wrath](https://github.com/propensive/wrath/)*
346 |
347 | Wrath is a bootstrapping script for building Contextual and other projects in
348 | the absence of a fully-featured build tool. It is designed to read the `fury`
349 | file in the project directory, and produce a collection of JAR files which can
350 | be added to a classpath, by compiling the project and all of its dependencies,
351 | including the Scala compiler itself.
352 |
353 | Download the latest version of
354 | [`wrath`](https://github.com/propensive/wrath/releases/latest), make it
355 | executable, and add it to your path, for example by copying it to
356 | `/usr/local/bin/`.
357 |
358 | Clone this repository inside an empty directory, so that the build can
359 | safely make clones of repositories it depends on as _peers_ of `contextual`.
360 | Run `wrath -F` in the repository root. This will download and compile the
361 | latest version of Scala, as well as all of Contextual's dependencies.
362 |
363 | If the build was successful, the compiled JAR files can be found in the
364 | `.wrath/dist` directory.
365 |
366 | ## Contributing
367 |
368 | Contributors to Contextual are welcome and encouraged. New contributors may like
369 | to look for issues marked
370 | [beginner](https://github.com/propensive/contextual/labels/beginner).
371 |
372 | We suggest that all contributors read the [Contributing
373 | Guide](/contributing.md) to make the process of contributing to Contextual
374 | easier.
375 |
376 | Please __do not__ contact project maintainers privately with questions unless
377 | there is a good reason to keep them private. While it can be tempting to
378 | repsond to such questions, private answers cannot be shared with a wider
379 | audience, and it can result in duplication of effort.
380 |
381 | ## Author
382 |
383 | Contextual was designed and developed by Jon Pretty, and commercial support and
384 | training on all aspects of Scala 3 is available from [Propensive
385 | OÜ](https://propensive.com/).
386 |
387 |
388 |
389 | ## Name
390 |
391 | Contextual takes its name from its ability to provide context-aware substitutions in interpolated strings.
392 |
393 | In general, Soundness project names are always chosen with some rationale,
394 | however it is usually frivolous. Each name is chosen for more for its
395 | _uniqueness_ and _intrigue_ than its concision or catchiness, and there is no
396 | bias towards names with positive or "nice" meanings—since many of the libraries
397 | perform some quite unpleasant tasks.
398 |
399 | Names should be English words, though many are obscure or archaic, and it
400 | should be noted how willingly English adopts foreign words. Names are generally
401 | of Greek or Latin origin, and have often arrived in English via a romance
402 | language.
403 |
404 | ## Logo
405 |
406 | The logo is of a quote symbol, alluding to Contextual's subject matter of quoted strings.
407 |
408 | ## License
409 |
410 | Contextual is copyright © 2025 Jon Pretty & Propensive OÜ, and
411 | is made available under the [Apache 2.0 License](/license.md).
412 |
413 |
--------------------------------------------------------------------------------
/doc/logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
138 |
--------------------------------------------------------------------------------