Mike Gerwitz
61d556c89e
This was a significant undertaking, with a few thrown-out approaches. The documentation describes what approach was taken, but I'd also like to provide some insight into the approaches that I rejected for various reasons, or because they simply didn't work. The problem that this commit tries to solve is encapsulation of error types. Prior to the introduction of the lowering pipeline macro `lower_pipeline!`, all pipelines were written by hand using `Lower` and specifying the applicable types. This included creating sum types to accommodate each of the errors so that `Lower` could widen automatically. The introduction of the `lower_pipeline!` macro resolved the boilerplate and type complexity concerns for the parsers by allowing the pipeline to be concisely declared. However, it still accepted an error sum type `ER` for recoverable errors, which meant that we had to break a level of encapsulation, peering into the pipeline to know both what parsers were in play and what their error types were. These error sum types were also the source of a lot of tedious boilerplate that made adding new parsers to the pipeline unnecessarily unpleasant; the purpose of the macro is to make composition both easy and clear, and error types were undermining it. Another benefit of sum types per pipeline is that callers need only aggregate those pipeline types, if they care about them, rather than every error type used as a component of the pipeline. So, this commit generates the error types. Doing so was non-trivial. Associated Types and Lifetimes ------------------------------ Error types are associated with their `ParseState` as `ParseState::Error`. As described in this commit, TAMER's approach to errors is that they never contain non-static lifetimes; interning and copying are used to that effect. And, indeed, no errors in TAMER have lifetimes. But, some `ParseState`s may. In this case, `AsgTreeToXirf`: ``` impl<'a> ParseState for AsgTreeToXirf<'a> { // [...] type Error = AsgTreeToXirfError; // [...] } ``` Even though `AsgTreeToXirfError` does not have a lifetime, the `ParseState` it is associated with _does_`. So to reference that type, we must use `<AsgTreeToXirf<'a> as ParseState>::Error`. So if we have a sum type: ``` enum Sum<'a> { // ^^ oh no! vv AsgTreeToXirfError(<AsgTreeToXirf<'a> as ParseState>::Error), } ``` There's no way to elide or make anonymous that lifetime, since it's not used, at the time of writing. `for<'a>` also cannot be used in this context. The solution in this commit is to use a macro (`lower_error_sum`) to rewrite lifetimes: to `'static`: ``` enum Sum { AsgTreeToXirfError(<AsgTreeToXirf<'static> as ParseState>::Error), } ``` The `Error` associated type will resolve to `AsgTreeToXirfError` all the same either way, since it has no lifetimes of its own, letalone any referencing trait bounds. That's not to say that we _couldn't_ support lifetimes as long as they're attached to context, but we have no need to at the moment, and it adds significant cognitive overhead. Further, the diagnostic system doesn't deal in lifetimes, and so would need reworking as well. Not worth it. An alternative solution to this that was rejected is an explicitly `Error` type in the macro application: ``` // in the lowering pipeline |> AsgTreeToXirf<'a> { // lifetime type Error = AsgTreeToXirfError; // no lifetime } ``` But this requires peeling back the `ParseState` to see what its error is and _duplicate_ it here. Silly, and it breaks encapsulation, since the lowering pipeline is supposed to return its own error type. Yet another option considered was to standardize a submodule convention whereby each `ParseState` would have a module exporting `Error`, among other types. This would decouple it from the parent type. However, we still have the duplication between that and an associated type. Further, there's no way to enforce this convention (effectively a module API)---the macro would just fail in obscure ways, at least with `macro_rules!`. It would have been an ugly kluge. Overlapping Error Types ----------------------- Another concern with generating the sum type, resolved in a previous commit, was overlapping error types, which prohibited `impl From<E> for ER` generation. The problem with that a number of `ParseState`s used `Infallible` as their `Error` type. This was resolved in a previous commit by creating Infallible-like newtypes (variantless enums). This was not the only option. `From` fits naturally into how TAMER handles sum types, and fits naturally into `Lower`'s `WidenedError`. The alternative is generating explicit `map_err`s in `lower_pipeline!`. This would have allowed for overlapping error types because the _caller_ knows what the correct target variant is in the sum type. The problem with an explicit `map_err` is that it places more power in `lower_pipeline!`, which is _supposed_ to be a macro that simply removes boilerplate; it's not supposed to increase expressiveness. It's also not fun dealing with complexity in macros; they're much more confusing that normal code. With the decided-upon approach (newtypes + `From`), hand-written `Lower` pipelines are just as expressive---just more verbose---as `lower_pipeline!`, and handles widening for you. Rust's type system will also handle the complexity of widening automatically for us without us having to reason about it in the macro. This is not always desirable, but in this case, I feel that it is. |
||
---|---|---|
bin | ||
build-aux | ||
core | ||
design/tpl | ||
doc | ||
progtest | ||
rater | ||
src | ||
tamer | ||
test | ||
tools | ||
.gitignore | ||
.gitlab-ci.yml | ||
.gitmodules | ||
.rev-xmle | ||
.rev-xmlo | ||
COPYING | ||
COPYING.FDL | ||
HACKING | ||
Makefile.am | ||
README.md | ||
RELEASES.md | ||
VERSION.in | ||
bootstrap | ||
c1map.xsd | ||
configure.ac | ||
package-lock.json |
README.md
TAME
TAME is The Algebraic Metalanguage, a programming language and system of tools designed to aid in the development, understanding, and maintenance of systems performing numerous calculations on a complex graph of dependencies, conditions, and a large number of inputs.
This system was developed at Ryan Specialty Group (formerly LoVullo Associates) to handle the complexity of comparative insurance rating systems. It is a domain-specific language (DSL) that itself encourages, through the use of templates, the creation of sub-DSLs. TAME itself is at heart a calculator—processing only numerical input and output—driven by quantifiers as predicates. Calculations and quantifiers are written declaratively without concern for order of execution.
The system has powerful dependency resolution and data flow capabilities.
TAME consists of a macro processor (implementing a metalanguage), numerous compilers for various targets (JavaScript, HTML documentation and debugging environment, LaTeX, and others), linkers, and supporting tools. The input grammar is XML, and the majority of the project (including the macro processor, compilers, and linkers) is written in a combination of XSLT and Rust.
TAMER
Due to performance requirements, this project is currently being reimplemented in Rust. That project can be found in the tamer/ directory.
Documentation
Compiled documentation for the latest release is available via our GitLab mirror, which uses the same build pipeline as we do on our internal GitLab instance. Available formats are:
Getting Started
To get started, make sure Saxon version 9 or later is available and its path
set as SAXON_CP
; that the path to hoxsl is set via HOXSL
; and then run
the bootstrap
script:
$ export SAXON_CP=/path/to/saxon9he.jar
$ export HOXSL=/path/to/hoxsl/root
$ ./boostrap
Running Test Cases
To run the test cases, invoke make check
(or its alias, make test
).
Testing Core Features
In order to run tests located at core/test/core/**
, a supporting environment
is required. (e.g. mega rater). Inside a supporting rater, either check out a
submodule containing the core tests, or temporarily add them into the
submodule.
Build the core test suite summary page using:
$ make rater/core/test/core/suite.html
Visit the summary page in a web browser and click the Calculate Premium button. If all test cases pass, it will yield a value of $1.
Hacking
Information for TAME developers can be found in the file HACKING
.
License
This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.