├── 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 | [GitHub Workflow](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 | 18 | 20 | 23 | 27 | 31 | 32 | 35 | 41 | 42 | 51 | 54 | 60 | 61 | 62 | 86 | 88 | 89 | 91 | image/svg+xml 92 | 94 | 95 | 96 | 97 | 102 | 105 | 111 | 115 | 121 | 127 | 131 | 135 | 136 | 137 | 138 | --------------------------------------------------------------------------------