Compare commits

...

47 Commits

Author SHA1 Message Date
Mike Gerwitz d889aca13a tamer: asg::graph::AsgRelMut: API cleanup
This does two things:

  1. Removes callback; it didn't add anything of practical value.
     The operation will simply be performed as long as no error is provided
     by the callee.
  2. Consolodates three arguments into `ProposedRel`.  This makes blocks in
     `object_rel!` less verbose and boilerplate-y.

I'll probably implement `TplShape::Unknown` via the dynamic `Ident` `Tpl`
edge before continuing with any cleanup.  This is getting pretty close to
reasonable for future implementations.

DEV-13163
2023-07-27 03:33:06 -04:00
Mike Gerwitz 579575a358 tamer: asg::graph::object: ObjectIndex::try_map_obj_inner
Continuing to clean house and make things more concise, not just for `tpl`
but for all future changes.

DEV-13163
2023-07-27 03:07:33 -04:00
Mike Gerwitz 66512bf20d tamer: f: impl_mono_map! macro
This helps to remove some boilerplate.  Testing this out in
`asg::graph::object::tpl` before applying it to other things; really `Map`
can just go away entirely then since it can be implemented in terms of
`TryMap`, but maybe it should stick around for manual impls (implementing
`TryMap` manually is more work).

DEV-13163
2023-07-27 02:56:29 -04:00
Mike Gerwitz 3c9e1add20 tamer: f: Add TryMap
This implements TryMap and utilizes it in `asg::graph::object::tpl`.

DEV-13163
2023-07-27 01:44:12 -04:00
Mike Gerwitz 38c0161257 tamer: f::{Functor=>Map}: It's not really a functor
At least not how most people expect functors to be.  I'm really just using
this as a map with powerful inference properties that make writing code more
pleasent.

And I need fallible methods now too.

DEV-13163
2023-07-26 16:43:09 -04:00
Mike Gerwitz e14854a555 tamer: asg::air::tpl: Resolve shape against inner template application
Things are starting to get interesting, and this shows how caching
information about template shape (rather than having to query the graph any
time we want to discover it) makes it easy to compose shapes.

This does not yet handle the unknown case.  Before I do that, I'll want to
do some refactoring to address duplication in the `tpl` module.

DEV-13163
2023-07-26 16:09:17 -04:00
Mike Gerwitz c19ecba6ef tamer: asg::air::object::tpl: Reject multi-expression shape
This enforces the new constraint that templates expanding into an `Expr`
context must only inline a single `Expr`.

Perhaps in the future we'll support explicit splicing, like `,@` in
Lisp.  But this new restriction is intended for two purposes:

  - To make templates more predictable (if you have a list of expressions
    inlined then they will act differently depending on the type of
    expression that they are inlined into, which means that more defensive
    programming would otherwise be required); and
  - To make expansion easier, since we're going to have to set aside an
    expansion workspace ahead of time to ensure ordering (Petgraph can't
    replace edges in-place).  If we support multi-expansion, we'd have to
    handle associativity in all expression contexts.

This'll become more clear in future commits.

It's nice to see all this hard work coming together now, though; it's easy
now to perform static analysis on the system, and any part of the graph
construction can throw errors with rich diagnostic information and still
recover properly.  And, importantly, the system enforces its own state, and
the compiler helps us with that (the previous commits).

DEV-13163
2023-07-26 04:03:52 -04:00
Mike Gerwitz aec721f4fa tamer: asg::graph: Work AsgRelMut specialization into `object_rel!` macro
This formalizes the previous commit a bit more and adds documentation
explaining why it exists and how it works.  Look there for more
information.

This has been a lot of setup work.  Hopefully things are now easier in the
future.  And now we have nice declarative type-level hooks into the graph!

DEV-13163
2023-07-26 04:03:49 -04:00
Mike Gerwitz 37c962a7ee tamer: asg::graph::object::tpl::TplShape: Introduce template "shapes"
This change is the first to utilize matching on edges to determine the state
of the template (to begin to derive its shape).

But this is notable for my finally caving on `min_specialization`.

The commit contains a bunch of rationale for why I introduced it.  I've been
sitting on trying it for _years_.  I had hoped for further progress in
determining a stabalization path, but that doesn't seem to be happening.

The reason I caved is because _not_ using it is a significant barrier to
utilizing robust types in various scenarios.  I've been having to work
around that with significant efforts to write boilerplate code to match on
types and branch to various static paths accordingly.  It makes it really
expensive to make certain types of changes, and it make the code really
difficult to understand once you start to peel back abstractions that try to
hide it.

I'll see how this goes and, if it goes well, begin to replace old methods
with specialization.

See the next commit for some cleanup.  I purposefully left this a bit of a
mess (at the bottom of `asg::graph::object::tpl`) to emphasize what I'm
doing and why I introduced it.

DEV-13163
2023-07-25 15:28:53 -04:00
Mike Gerwitz 4168c579fd tamer: asg::graph::Asg{Object=>Rel}Mut: Trait-level target type
This allows for a declarative matching on edge targets using the trait
system, rather than having to convert the type to a runtime value to match
on (which doesn't make a whole lot of sense).

See a commit to follow shortly (with Tpl) for an example use case.

DEV-13163
2023-07-25 11:15:41 -04:00
Mike Gerwitz 2ecc143e02 tamer: asg::graph::pre_add_edge: Use pre-narrowed source ObjectIndex
Since we're statically invoking a particular ObjectKind's method, we already
know the source type.  Let's pre-narrow it for their (my) convenience.

DEV-13163
2023-07-25 10:55:08 -04:00
Mike Gerwitz 28b83ad6a3 tamer: asg::graph::AsgObjectMut: Allow objects to assert ownership over relationships
There's a lot to say about this; it's been a bit of a struggle figuring out
what I wanted to do here.

First: this allows objects to use `AsgObjectMut` to control whether an edge
is permitted to be added, or to cache information about an edge that is
about to be added.  But no object does that yet; it just uses the default
trait implementation, and so this _does not change any current
behavior_.  It also is approximately equivalent cycle-count-wise, according
to Valgrind (within ~100 cycles out of hundreds of millions on large package
tests).

Adding edges to the graph is still infallible _after having received
permission_ from an `ObjectIndexRelTo`, but the object is free to reject the
edge with an `AsgError`.

As an example of where this will be useful: the template system needs to
keep track of what is in the body of a template as it is defined.  But the
`TplAirAggregate` parser is sidelined while expressions in the body are
parsed, and edges are added to a dynamic source using
`ObjectIndexRelTo`.  Consequently, we cannot rely on a static API to cache
information; we have to be able to react dynamically.  This will allow `Tpl`
objects to know any time edges are added and, therefore, determine their
shape as the graph is being built, rather than having to traverse the tree
after encountering a close.

(I _could_ change this, but `ObjectIndexRelTo` removes a significant amount
of complexity for the caller, so I'd rather not.)

I did explore other options.  I rejected the first one, then rejected this
one, then rejected the first one again before returning back to this one
after having previously sidelined the entire thing, because of the above
example.  The core point is: I need confidence that the graph isn't being
changed in ways that I forgot about, and because of the complexity of the
system and the heavy refactoring that I do, I need the compiler's help;
otherwise I risk introducing subtle bugs as objects get out of sync with the
actual state of the graph.

(I wish the graph supported these things directly, but that's a project well
outside the scope of my TAMER work.  So I have to make do, as I have been
all this time, by layering atop of Petgraph.)

(...I'm beginning to ramble.)

(...beginning?)

Anyway: my other rejected idea was to provide attestation via the
`ObjectIndex` APIs to force callers to go through those APIs to add an edge
to the graph; it would use sealed objects that are inaccessible to any
modules other than the objects, and assert that the caller is able to
provide a zero-sized object of that sealed type.

The problem with this is...exactly what was mentioned above:
`ObjectIndexRelTo` is dynamic.  We don't always know the source object type
statically, and so we cannot make those static assertions.

I could have tried the same tricks to store attestation at some other time,
but what a confusing mess it would be.

And so here we are.

Most of this work is cleaning up the callers---adding edges is now fallible,
from the `ObjectIndex` API standpoint, and so AIR needed to be set up to
handle those failures.  There _aren't_ any failures yet, but again, since
things are dynamic, they could appear at any moment.  Furthermore, since
ref/def is commutative (things can be defined and referenced in any order),
there could be surprise errors on edge additions in places that might not
otherwise expect it in the future.  We're now ready for that, and I'll be
able to e.g. traverse incoming edges on a `Missing->Transparent` definition
to notify dependents.

This project is going to be the end of me.  As interesting as it is.

I can see why Rust just chose to require macro definitions _before_ use.  So
much less work.

DEV-13163
2023-07-24 16:41:32 -04:00
Mike Gerwitz e414782def tamer: asg::graph: Encapsulate edge additions
AIR is no longer able to explicitly add edges without going through an
object-specific `ObjectIndex` API.  `Asg::add_edge` was already private, but
`ObjectIndex::add_edge_{to,from}` was not.

The problem is that I want to augment the graph with other invariants, such
as caches.  I'd normally have this built into the graph system itself, but I
don't have the time for the engineering effort to extend or replace
Petgraph, so I'm going to build atop of it.

To have confidence in any sort of caching, I need assurances that the graph
can't change out from underneath an object.  This gets _close_ to
accomplishing that, but I'm still uncomfortable:

  - We're one `pub` addition away from breaking these invariants; and
  - Other `Object` types can still manipulates one-anothers' edges.

So this is a first step that at least proves encapsulation within
`asg::graph`, but ideally we'd have the system enforce, statically, that
`Objects` own their _outgoing_ edges, and no other `Object` is able to
manipulate them.  This would ensure that any accidental future changes, or
bugs, will cause compilation failures rather than e.g. allowing caches to
get out of sync with the graph.

DEV-13163
2023-07-21 10:21:57 -04:00
Mike Gerwitz 0f93f3a498 tamer: NIR->xmli interpolation and template param
The fixpoint tests for `meta-interp` are finally working.  I could have
broken this up more, but I'm exhausted with this process, so, you get what
you get.

NIR will now recognize basic `<text>` and `<param-value>` nodes (note the
caveat for `<text>` in the comment, for now), and I finally include abstract
binding in the lowering pipeline.  `xmli` output is also now able to cope
with metavariables with a single lexical association, and continues to
become more of a mess.

DEV-13163
2023-07-18 12:31:28 -04:00
Mike Gerwitz 85b08eb45e tamer: nir::interp: Do not include original specification in generated desc
This is a really obvious problem in retrospect, which makes me feel rather
silly.

The output was useful, but I don't have time to deal with this any further
right now.  The comments in the commit explain the problem---that the output
ends up being interpolated as part of the fixpoint test, in an incorrect
context, and so the code that we generate is invalid.  Also goes to show why
the fixpoint tests are important.

(Yes, they're still disabled for meta-interp, I'm trying to get them
enabled.)

DEV-13163
2023-07-18 11:17:51 -04:00
Mike Gerwitz 507669cb30 tamer: asg::graph::object::ObjectIndexRefined: New narrowing type
The provided documentation provides rationale, and the use case is the
ontree change.  I was uncomfortable without the exhaustive match, and I was
further annoyed by the lack of easy `ObjectIndex` narrowing.

DEV-13163
2023-07-18 10:31:33 -04:00
Mike Gerwitz 5a301c1548 tamer: asg::graph::visit::ontree: Source ordering of ontological tree
This introduces the ability to specify an edge ordering for the ontological
tree traversal.  `tree_reconstruction` will now use a
`SourceCompatibleTreeEdgeOrder`, which will traverse the graph in an order
that will result in a properly ordered source reconstruction.  This is
needed for template headers, because interpolation causes
metavariables (exposed as template params) to be mixed into the body.

There's a lot of information here, including some TODOs on possible
improvements.  I used the unstable `is_sorted` to output how many template
were already sorted, based on one of our very large packages internally that
uses templates extensively, and found that none of the desugared shorthand
template expansions were already ordered.  If I tweak that a bit, then
nearly all templates will already be ordered, reducing the work that needs
to be done, leaving only template definitions with interpolation to be
concerned about, which is infrequent relative to everything else.

DEV-13163
2023-07-18 10:31:31 -04:00
Mike Gerwitz b30018c23b tamer: xmli reconstruction of desugared interpolated metavars
Well, this is both good news and bad news.

The good news is that this finally produces the expected output and
reconstructs sources from interpolated values on the ASG.  Yay!

...the bad news is that it's wrong.  Notice how the fixpoint test is
disabled.

So, my plan was originally to commit it like this first and see if I was
comfortable relaxing the convention that `<param>` nodes had to appear in
the header.  That's nice to do, that's cleaner to do, but would the
XSLT-based compiler really care?  I had to investigate.

Well, turns out that TAMER does care.  Because, well over a decade ago, I
re-used `<param>`, which could represent not only a template param, but also
a global param, or a function param.

So, XML->NIR considers all `<param>` nodes at the head of a template to be
template parameters.  But after the first non-header element, we transition
to another state that allows it to be pretty much anything.

And so, I can't relax that restriction.

And because of that, I can't just stream the tree to the xmli generator,
I'll have to queue up nodes and order them.

Oh well, I tried.

DEV-13163
2023-07-17 14:20:05 -04:00
Mike Gerwitz 85892caeb2 tamer: asg: Root abstract identifiers in active container
I'm not sure how I overlooked this previously, and I didn't notice until
trying to generate xmli output.  I think I distracted myself with the
use of dangling status, which was not appropriate, and that has since
changed so that we have a dedicated concept.

This introduces the term "instantiation", or more specifically "lexical
instantiation".  This is more specific and meaningful than simply
"expansion", which is what occurs during instantiation.  I'll try to adjust
terminology and make things more consistent as I go.

DEV-13163
2023-07-17 14:20:04 -04:00
Mike Gerwitz 760223f0c9 tamer: asg::air: Extract abstract definition into context
This logic ought to live alongside other definition logic...which in turn
needs its own extraction, but that's a separate concern.

This makes the definition of abstract identifiers very similar to
concrete.  But, treating these as dangling, even if that's technically true,
has to change---we still want an edge drawn to the abstract identifier via
e.g. a template since we want the graph to mirror the structure of what it
will expand into concretely.  I didn't notice this problem until trying to
generate the xmli for it.

So, see the commit to follow.

DEV-13163
2023-07-13 11:16:10 -04:00
Mike Gerwitz b4b85a5e85 tamer: asg::air: Support Meta::ConcatList with lexemes and refs
This handles the common cases for meta, which includes what interpolation
desugars into.  Most of this work was in testing and reasoning about the
issue; `asg::graph::visit:ontree::test` has a good summary of the structure
of the graph that results.

The last remaining steps to make this work end-to-end is for NIR->AIR to
lower `Nir::Ref` into `Air::BindIdent`, and then for `asg::graph::xmli` to
reconstruct concatenation lists.  I'll then be able to commit the xmli test
case I've been sitting on, whose errors have been guiding my development.

DEV-13163
2023-07-13 10:48:45 -04:00
Mike Gerwitz d2d29d8957 tamer: asg::air::meta: Use term "metalinguistic" over "metasyntactic"
The term "metasyntactic" made sense literally---it's a variable in a
metalanguage that expands into a context that is able to contribute to the
language's syntax.  But, the term has a different conventional use in
programming that is misleading.

The term "metalinguistic" is used in mathematics, to describe a metalanguage
or schema atop of a language.  This is more fitting.

DEV-13163
2023-07-13 10:48:45 -04:00
Mike Gerwitz b4bbc0d8f0 tamer: asg::air: Use new parse::util::spair function to reduce test ceremony
This makes `SPair` construction more concise, getting rid of the `into`
invocations.  For now I have only made this change in AIR's tests, since
that's what I'm working on and I want to observe how this convention
evolves.  This may also encourage other changes, e.g. placing spans within
the `toks` array, rather than having to jump around the test for them.

The comment for `spair` mentions why this is a test-only function.  But it
also shows how dangerous `impl Into<SymbolId> for &str` can be, since it
seems so innocuous---it uses a global interner.  I'll be interested to see a
year from now if I decided to forego that impl in favor of explicit
internment, since I'm not sure it's worth the convenience anymore.

DEV-13163
2023-07-13 10:48:44 -04:00
Mike Gerwitz 8a10f8bbbe tamer: asg::air: Remove unncessary vec![] usage in tests
This has been bothering me for quite a long time, and is just more test
cleanup before I introduce more.  I suspect this came from habit with the
previous Rust edition where `into_iter()` on arrays was a much more verbose
operation.

To be clear: this change isn't for performance.  It's about not doing
something silly when it's unnecessary, which also sets a bad example for
others.

There are many other tests in other modules that will need updating at some
point.

DEV-13163
2023-07-13 10:48:27 -04:00
Mike Gerwitz 2e33e9e93e tamer: asg::air: Remove `Air::` token variant prefixes from tests
This just removes noise from test, as has become standard in various other
tests in TAMER.

DEV-13163
2023-07-13 10:48:27 -04:00
Mike Gerwitz 24ee041373 tamer: asg::air: Support abstract biding of `Expr`s
This produces a representation of abstract identifiers on the graph, for
`Expr`s at least.  The next step will probably be to get this working
end-to-end in the xmli output before extending it to the other remaining
bindable contexts.

DEV-13163
2023-07-13 10:48:26 -04:00
Mike Gerwitz a144730981 tamer: nir::abstract_bind: Require @-padding of metavariable names
This enforces the naming convention that is utilized to infer whether an
identifier binding must be translated to an abstract binding.

This does not yet place any restrictions on other characters in identifier
names; both the placement of and flexibility of that has yet to be
decided.  This change is sufficient enough to make abstract binding
translation reliable.

DEV-13163
2023-07-10 10:28:01 -04:00
Mike Gerwitz 8449a2b759 tamer: parse::prelude: Include Display, Debug, and Error-related exports
Cut down on the import boilerplate some more for `ParseState`s.

DEV-13163
2023-07-10 10:27:57 -04:00
Mike Gerwitz 8685527feb tamer: nir: New token BindIdentMeta
The previous commit made me uncomfortable; we're already parsing with great
precision (and effort!) the grammar of NIR, and know for certain whether
we're in a metavariable binding context, so it makes no sense to have to try
to guess at another point in the lowering pipeline.

This introduces a new token to retain that information from XIR->NIR
lowering and then re-simplifies the lowering operation that was just
introduced in the previous commit (`AbstractBindTranslate`).

DEV-13163
2023-06-28 09:48:15 -04:00
Mike Gerwitz 7314562671 tamer: nir::abstract_bind: New lowering operation
This builds upon the concepts of the previous commit to translate identifier
binding into an abstract binding if it utilizes a symbol that follows a
metavariable naming convention.

See the provided documentation for more information.

This commit _does not_ integrate this into the lowering pipeline yet, since
the abstract identifiers are still rejected (as TODOs) by AIR.

DEV-13163
2023-06-28 09:48:14 -04:00
Mike Gerwitz 15071a1824 tamer: nir: Interpolate concrete binds into abstract binds
This introduces the notion of an abstract identifier, where the previous
identifiers are concrete.  This serves as a compromise to either introducing
a new object type (another `Ident`), or having every `Ident` name be defined
by a `Meta` edge, which would bloat the graph significantly.

This change causes interpolation within a bind context to desugar into a new
`BindIdentAbstract` token, but AIR will throw an error if it encounters it
for now; that implementation will come soon.

This does not yet handle non-interpolation cases,
e.g. `<classify as="@foo@">`.  This is a well-established shorthand for
`as="{@foo@}"`, but is unfortunately ambiguous in the context of
metavariable definitions (template parameters).  This language ambiguity
will have to be handled here, and will have to fall back to today's behavior
of assuming concrete in that `param/@name` context but abstract every else,
unless of course interpolation is triggered using `{}` to disambiguate (as
in `<param name="{@foo@}"`).

I was going to handle the short-hand meta binding case as part of
interpolation, but I decided it may be appropriate for its own lowering
operation, since it is intended to work regardless of whether interpolation
takes place; it's a _translation_ of a binding into an abstract one, and it
can clearly delineate the awkward syntactic rules that we have to inherit,
as mentioned above.

DEV-13163
2023-06-27 12:48:19 -04:00
Mike Gerwitz 828d8918a3 tamer::asg::graph::object::ident::Ident::name: Produce Option
This prepares to make the name of an `Ident` optional to support abstract
identifiers derived from metavariables.

This is an unfortunate change to have to prepare for, since it complicates
how Idents are interpreted, but the alternative (a new object type) is not
good either.  We'll see how this evolves.

DEV-13163
2023-06-26 15:37:08 -04:00
Mike Gerwitz 6b54eafd70 tamer: asg::air: Hoist metavars in expressions
This is intended to support NIR's lexical interpolation, which expands in
place into metavariables.

This commit does not yet contain the NIR portion (or xmli system test)
because Meta needs to be able to handle concatenation first; that's next.

DEV-13163
2023-06-20 15:14:38 -04:00
Mike Gerwitz d10bf00f5d tamer: Initial template/param support through xmli
This introduces template/param and regenerates it in the xmli output.  Note
that this does not check that applications reference known params; that's a
later phase.

DEV-13163
2023-06-14 16:38:05 -04:00
Mike Gerwitz 9887abd037 tamer: nir::air: Include mention of .experimental file in TODO help
The previous commit introduced support for a `.experimental` file to tigger
`xmlo-experimental`.  This modifies the error message for unsupported
features to make mention of it to help to the user track down the problem.

DEV-13162
2023-06-14 13:03:21 -04:00
Mike Gerwitz a9bbb87612 build-aux/Makefile.am: Introduce .experimental files
If a source file is paired with a `.experimental` file (for example,
`foo.xml` has a silbing `foo.experimental` file), then it will be
precompiled using `--emit xmlo-experimental` instead of `--emit
xmlo`.  Further, the contents of the experimental file may contain
`#`-prefixed comments describing why it exists, as well as additional
options to pass to `tamec`.

For example, if this is an experimental file:

```

--foo
--bar=baz
```

Then the tamec invocation will contain:

  tamec [...] --emit xmlo-experimental --foo --bar=baz -o foo.xmli

This allows for package-level conditional compilation with new features so
that I am able to focus on packages that will provide the most meaningful
benefits to our team, whether they be performance or features.

DEV-13162
2023-06-14 12:02:57 -04:00
Mike Gerwitz 7487bdccc3 tamer: nir::air: Recoverable error instead of panic for TODO tokens
Now that the feature flag for the parser is a command line option, it is
useful to be able to run it on any package and see what errors arise, to use
as a guide for development with the goal of getting a particular package to
compile.

This converts the TODO panic into a recoverable error so that the parser can
spit out as many errors as it can.

DEV-13162
2023-06-14 10:24:50 -04:00
Mike Gerwitz 454f5f4d04 tamer: Initial clarifying pipeline docs
This provides some initial information to help guide a user to discover how
TAMER works, though either the source code or the generated
documentation.  This will improve over time, since all of the high-level
abstractions are still under development.

DEV-13162
2023-06-13 23:43:04 -04:00
Mike Gerwitz 9eeb18bda2 tamer: Replace wip-asg-derived-xmli flag with command line option
This introduces `xmlo-experimental` for `--emit`, allowing the new parser to
be toggled selectively for individual packages.  This has a few notable
benefits:

  1. We'll be able to conditionally compile packages as they are
     supported (TAMER will target specific packages in our system to try to
     achieve certain results more quickly);

  2. This cleans up the code a bit by removing awkward gated logic, allowing
     natural abstractions to form; and

  3. Removing the compile-time feature flag ensures that the new features
     are always built and tested; there are fewer configuration combinations
     to test.

DEV-13162
2023-06-13 23:23:51 -04:00
Mike Gerwitz 341af3fdaf tamer: nir::air: Dynamic configuration in place of static wip-asg-derived-xmli flag
This flag should have never been sprinkled here; it makes the system much
harder to understand.

But, this is working toward a command-line tamec option to toggle NIR
lowering on/off for various packages.

DEV-13162
2023-06-13 15:07:03 -04:00
Mike Gerwitz 61d556c89e tamer: pipeline: Generate recoverable sum error types
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.
2023-06-13 14:49:43 -04:00
Mike Gerwitz 31f6a102eb tamer: pipeline::macro: Partially applied pipeline
This configures the pipeline and returns a closure that can then be provided
with the source and sink.

The next obvious step would be to curry the source and sink.

But I wanted to commit this before I take a different (but equivalent)
approach that makes the pipeline operations more explicit and helps to guide
the user (developer) in developing and composing them.  The FP approach is
less boilerplate, but is also more general and provides less
guidance.  Given that composition at the topmost levels of the system,
especially with all the types involved, is one of the most confusing aspects
of the system---and one of the most important to get right and make clear,
since it's intended to elucidate the entire system at a high level, and
guide the reader.  Well, it does a poor job at that now, but that's the
ultimate goal.

In essence---brutally general abstractions make sense at lower levels, but
the complexity at higher levels benefits from rigid guardrails, even though
it does not necessitate it.

DEV-13162
2023-06-13 10:02:51 -04:00
Mike Gerwitz 26c4076579 tamer: obj::xmlo::reader: Emit token after symbol dependencies
This will allow a tamec xmlo reading pipeline to stop before fragment
loading.

DEV-13162
2023-06-12 12:37:12 -04:00
Mike Gerwitz 0b9e91b936 tamer: obj::xmlo::reader::XmloReader: Remove generics
This cleanup is an interesting one, because I think the present me may
disagree with the past me.

The use of generics here to compose the parser from smaller parsers was due
to how I wrote my object-oriented code in other languages: where a class was
an independently tested unit.  I was trying to reproduce the same here,
utilizing generics in the same way that one would use compoisition via
object constructors in other languages.

But it's been a long time since then, and I've come to settle on different
standards in Rust.  The components of `XmloReader` really are just
implementation details.  As I find myself about to want to modify its
behavior, I don't _want_ to compose `XmloReader` from _different_ parsers;
that may result in an invalid parse.  There's one correct way to parse an
xmlo file.

If I want to parse the file differently, then `XmloReader` ought to expose
a way of doing so.  This is more rigid, but that rigidity buys us confidence
that the system has been explicitly designed to support those
operations.  And that confidence gives us peace of mind knowing that the
system won't compose in ways that we don't intend for it to.

Of course, I _could_ design the system to compose in generic ways.  But
that's an over-generalization that I don't think will be helpful; it's not
only a greater cognitive burden, but it's also a lot more work to ensure
that invariants are properly upheld and to design an API that will ensure
that parsing is always correct.  It's simply not worth it.

So, this makes `XmloReader` consistent with other parsers now, like
`AirAggregate` and nir::parse (ele_parse).  This prepares for a change to
make `XmloReader` configurable to avoid loading fragments from object files,
since that's very wasteful for `tamec`.

DEV-13162
2023-06-12 12:37:12 -04:00
Mike Gerwitz 1bb25b05b3 tamer: Newtypes for all Infallible ParseState errors
More information will be presented in the commit that follows to generalize
these, but this sets the stage.

The recently-introduced pipeline macro takes care of most of the job of a
declarative pipeline, but it's still leaky, since it requires that the
_caller_ create error sum types.  This not only exposes implementation
details and so undermines the goal of making pipelines easy to declare and
compose, but it's also one of the last major components of boilerplate for
the lowering pipeline.

My previous attempts at generating error sum types automatically for
pipelines ran into a problem because of overlapping `impl`s for the various
`<S as ParseState>::Error` types; this resolves that issue via
newtypes.  I had considered other approaches, including explicitly
generating code to `map_err` as part of the lowering pipeline, but in the
end this is the easier way to reason about things that also keeps manual
`Lower` pipelines on the same level of expressiveness as the pipeline macro;
I want to restrict its unique capabilities as much as possible to
elimination of boilerplate and nothing more.

DEV-13162
2023-06-12 12:33:22 -04:00
Mike Gerwitz 672cc54c14 compiler/js.xsl: Derive supplier name from base package name
At or around 00492ace01, I modified packages
to output canonical `@name`s, which contains a leading forward
slash.  Previously, names omitted that slash.  I did not believe that this
caused any problems.

It seems that the XSLT-based `standalones` system utilizes this package name
to derive a supplier name, which is supposed to be the filename of the
package without any path.  Since the package name changed from
`suppliers/foo` to `/suppliers/foo`, for example, this was now producing
"suppliers/name" instead of "name".

Of course, it was never a good idea to strip off only the first path
component.  But, this is how it has been since TAME was originally created
well over a decade ago.

I did not catch this since I was diff'ing the output of the xmle files, not
the final JS files.  I had thought that was sufficient, given what I was
changing, but I was wrong.

DEV-14502
2023-06-08 16:46:18 -04:00
Mike Gerwitz d6e9ec7207 tamer: nightly pin: Describe problems with adt_const_param's ConstParamTy
See commit for description of the problem, describing why I'm not yet
upgrading to a currently nightly version.

DEV-14476
2023-06-06 11:00:44 -04:00
78 changed files with 5755 additions and 1398 deletions

View File

@ -136,7 +136,12 @@ common: $(xmlo_common)
program-ui: ui/package.strip.js ui/Program.js ui/html/index.phtml
# Handle an intermediate step as we transition to the new compiler
# Handle an intermediate step as we transition to the new compiler.
# If a source file is paired with an `*.experimental` file with the same
# stem, then it will trigger compilation using `xmlo-experimental`. The
# file may contain additional arguments to the pass to the compiler.
%.xmli: %.xml %.experimental
$(path_tame)/tamer/target/release/tamec --emit xmlo-experimental $$(grep -v '^#' $*.experimental) -o $@ $<
%.xmli: %.xml
$(path_tame)/tamer/target/release/tamec --emit xmlo -o $@ $<

View File

@ -159,7 +159,7 @@
<!-- make the name of the supplier available -->
<text>/**@expose*/rater.supplier = '</text>
<value-of select="substring-after( $name, '/' )" />
<value-of select="( tokenize( $name, '/' ) )[ last() ]" />
<text>'; </text>
<text>/**@expose*/rater.meta = meta;</text>

View File

@ -53,9 +53,3 @@ unicode-width = "0.1.5"
# This is enabled automatically for the `test` profile.
parser-trace-stderr = []
# Derive `xmli` file from the ASG rather than a XIR token stream. This
# proves that enough information has been added to the graph for the entire
# program to be reconstructed. The `xmli` file will be a new program
# _derived from_ the original, and so will not match exactly.
wip-asg-derived-xmli = []

View File

@ -32,6 +32,24 @@
# }
# }
#
# But edges also support arbitrary code definitions:
#
# object_rel! {
# Source -> {
# tree TargetA {
# fn pre_add_edge(...) {
# // ...
# }
# },
# tree TargetB,
# cross TargetC,
# }
# }
#
# And because of that,
# this script hard-codes the expected level of nesting just to make life
# easier.
#
# This script expects to receive a list of files containing such
# definitions.
# It will output,
@ -81,7 +99,8 @@ BEGINFILE {
/^object_rel! {$/, /^}$/ { in_block = 1 }
# `Foo -> {` line declares the source of the relation.
in_block && /->/ {
# We hard-code the expected depth to simplify parsing.
in_block && /^ \w+ -> \{$/ {
block_src = $1
printf " # `%s` from `%s:%d`\n", block_src, FILENAME, FNR
@ -92,7 +111,7 @@ in_block && /->/ {
# A closing curly brace always means that we've finished with the current
# source relation,
# since we're at the innermost level of nesting.
block_src && /}/ {
block_src && /^ }$/ {
block_src = ""
print ""
}
@ -116,7 +135,12 @@ block_src && /^ *\/\/ empty$/ {
# we must independently define each one.
# But that's okay;
# the output is quite legible.
block_src && $NF ~ /\w+,$/ {
block_src && /^ \w+ \w+(,| \{)$/ {
# Clean up the end of the string _before_ pulling information out of
# fields,
# since the number of fields can vary.
gsub(/(,| \{)$/, "")
# Edge type (cross, tree)
ty = $(NF-1)
@ -139,8 +163,6 @@ block_src && $NF ~ /\w+,$/ {
break;
}
gsub(/,$/, "")
# This may need updating over time as object names in Rust sources
# exceed the fixed-width definition here.
# This output is intended to form a table that is easy to read and

View File

@ -36,7 +36,7 @@
# you will need to modify `configure.ac` / `Makefile.am` to do your bidding.
[toolchain]
channel = "nightly-2023-04-15"
channel = "nightly-2023-04-15"
# The components should be checked in `configure.ac`
# - Note that `cargo-fmt` is `rustfmt`.
@ -50,3 +50,26 @@ components = ["rustfmt", "clippy"]
# another version of Rust already available elsewhere), you may use the
# `TAMER_RUST_TOOLCHAIN` `configure` parameter.
# ¹ TAMER uses the incomplete feature `adt_const_params`. "Incomplete"
# features require a special flag to enable---`incomplete_features`---and
# are described as "incomplete and may not be safe to use and/or cause
# compiler crashes".
#
# The `ConstParamTy` trait was introduced by
# <https://github.com/rust-lang/rust/pull/111670> and merged in early
# June 2023. After this change, const params types must implement this
# trait.
#
# Unfortunately, at the time of writing (2023-06), while the trait is
# implemented on a number of core primitives, it is _not_ implemented on the
# `NonZero*` types. There is an inquiry into this limitation on Zulip, and
# it is expected to be resolved in the future:
# <https://rust-lang.zulipchat.com/#narrow/stream/182449-t-compiler.2Fhelp/topic/ConstParamTy.20for.20NonZero*.20types>
#
# We will sit on this and watch how it evolves over the following
# weeks/months to assess how to best proceed.
#
# Aside from this, there seems to be a rustdoc bug at some point between
# April and June that causes documentation failures. This needs further
# investigation, and possibly a bug report.

View File

@ -36,7 +36,7 @@ use super::{
use crate::{
diagnose::Annotate,
diagnostic_unreachable,
f::Functor,
f::Map,
parse::{prelude::*, StateStack},
span::Span,
sym::SymbolId,
@ -46,6 +46,9 @@ use std::{
fmt::{Debug, Display},
};
#[cfg(test)]
use super::graph::object::ObjectRelTo;
#[macro_use]
mod ir;
use fxhash::FxHashMap;
@ -223,13 +226,14 @@ impl ParseState for AirAggregate {
(PkgTpl(tplst), AirBind(ttok)) => ctx.proxy(tplst, ttok),
(PkgTpl(tplst), AirDoc(ttok)) => ctx.proxy(tplst, ttok),
// Metasyntactic variables (metavariables)
(st @ PkgTpl(_), tok @ AirMeta(..)) => {
// Metavariables
(st @ (PkgTpl(_) | PkgExpr(_)), tok @ AirMeta(..)) => {
ctx.ret_or_transfer(st, tok, AirMetaAggregate::new())
}
(PkgMeta(meta), AirMeta(mtok)) => ctx.proxy(meta, mtok),
(PkgMeta(meta), AirBind(mtok)) => ctx.proxy(meta, mtok),
(PkgMeta(meta), tok @ (AirExpr(..) | AirTpl(..) | AirDoc(..))) => {
(PkgMeta(meta), AirDoc(mtok)) => ctx.proxy(meta, mtok),
(PkgMeta(meta), tok @ (AirExpr(..) | AirTpl(..))) => {
ctx.try_ret_with_lookahead(meta, tok)
}
@ -272,7 +276,7 @@ impl ParseState for AirAggregate {
// TODO: We will need to be more intelligent about this,
// since desugaring will produce metavariables in nested contexts,
// e.g. within an expression within a template.
(st @ (Pkg(..) | PkgExpr(..) | PkgOpaque(..)), AirMeta(tok)) => {
(st @ (Pkg(..) | PkgOpaque(..)), AirMeta(tok)) => {
Transition(st).err(AsgError::UnexpectedMeta(tok.span()))
}
@ -429,6 +433,42 @@ impl AirAggregate {
Root(_) => Filter,
}
}
/// Whether the active context represents an object that can be
/// lexically instantiated.
///
/// Only containers that support _instantiation_ are able to contain
/// abstract identifiers.
/// Instantiation triggers expansion,
/// which resolves metavariables and makes all abstract objects
/// therein concrete.
fn is_lexically_instantiatible(&self) -> bool {
use AirAggregate::*;
match self {
Uninit => false,
// These objects cannot be instantiated,
// and so the abstract identifiers that they own would never
// be able to be made concrete.
Root(_) => false,
Pkg(_) => false,
PkgExpr(_) => false,
// Templates are the metalinguistic abstraction and are,
// at the time of writing,
// the only containers capable of instantiation.
PkgTpl(_) => true,
// Metavariables cannot own identifiers
// (they can only reference them).
PkgMeta(_) => false,
// If an object is opaque to us then we cannot possibly look
// into it to see what needs expansion.
PkgOpaque(_) => false,
}
}
}
/// Behavior of an environment boundary when crossing environment upward
@ -823,7 +863,7 @@ impl AirAggregateCtx {
},
)?;
oi_pkg.root(&mut self.asg);
oi_pkg.root(&mut self.asg)?;
self.ooi_pkg.replace(oi_pkg);
Ok(oi_pkg)
@ -847,11 +887,33 @@ impl AirAggregateCtx {
///
/// A value of [`None`] indicates that no bindings are permitted in the
/// current context.
fn rooting_oi(&self) -> Option<ObjectIndexToTree<Ident>> {
fn rooting_oi(&self) -> Option<(&AirAggregate, ObjectIndexToTree<Ident>)> {
self.stack
.iter()
.rev()
.find_map(|st| st.active_rooting_oi())
.find_map(|st| st.active_rooting_oi().map(|oi| (st, oi)))
}
/// The active container (rooting context) for _abstract_ [`Ident`]s.
///
/// Only containers that support _instantiation_ are able to contain
/// abstract identifiers.
/// Instantiation triggers expansion,
/// which resolves metavariables and makes all abstract objects
/// therein concrete.
///
/// This utilizes [`Self::rooting_oi`] to determine the active rooting
/// context.
/// If that context does not support instantiation,
/// [`None`] is returned.
/// This method will _not_ continue looking further up the stack for a
/// context that is able to be instantiated,
/// since that would change the parent of the binding.
fn instantiable_rooting_oi(
&self,
) -> Option<(&AirAggregate, ObjectIndexToTree<Ident>)> {
self.rooting_oi()
.filter(|(st, _)| st.is_lexically_instantiatible())
}
/// The active dangling expression context for [`Expr`]s.
@ -925,17 +987,77 @@ impl AirAggregateCtx {
})
}
/// Root an identifier using the [`Self::rooting_oi`] atop of the stack.
fn defines(&mut self, name: SPair) -> Result<ObjectIndex<Ident>, AsgError> {
let oi_root = self
/// Root a concrete identifier using the [`Self::rooting_oi`] atop of
/// the stack.
///
/// This definition will index the identifier into the proper
/// environments,
/// giving it scope.
/// If the identifier is abstract,
/// it is important to use [`Self::defines_abstract`] instead so that
/// the metavariable that the identifier references will not be
/// indexed as the binding `name`.
fn defines_concrete(
&mut self,
binding_name: SPair,
) -> Result<ObjectIndex<Ident>, AsgError> {
let (_, oi_root) = self
.rooting_oi()
.ok_or(AsgError::InvalidBindContext(name))?;
.ok_or(AsgError::InvalidBindContext(binding_name))?;
Ok(self.lookup_lexical_or_missing(name).add_edge_from(
self.asg_mut(),
oi_root,
None,
))
self.lookup_lexical_or_missing(binding_name)
.defined_by(self.asg_mut(), oi_root)
}
/// Define an abstract identifier within the context of a container that
/// is able to hold dangling objects.
///
/// If the identifier is concrete,
/// then it is important to use [`Self::defines_concrete`] instead to
/// ensure that the identifier has its scope computed and indexed.
///
/// TODO: This is about to evolve;
/// document further.
fn defines_abstract(
&mut self,
meta_name: SPair,
) -> Result<ObjectIndex<Ident>, AsgError> {
match self.instantiable_rooting_oi() {
// The container cannot be instantiated and so there is no
// chance that this expression will be expanded in the future.
None => {
// Since we do not have an abstract container,
// the nearest container (if any) is presumably concrete,
// so let's reference that in the hope of making the error
// more informative.
// Note that this _does_ re-search the stack,
// but this is an error case that should seldom occur.
// If it's a problem,
// we can have `instantiable_rooting_oi` retain
// information.
let rooting_span = self
.rooting_oi()
.map(|(_, oi)| oi.widen().resolve(self.asg_ref()).span());
// Note that we _discard_ the attempted bind token
// and so remain in a dangling state.
Err(AsgError::InvalidAbstractBindContext(
meta_name,
rooting_span,
))
}
// We root a new identifier in the instantiable container,
// but we do not index it,
// since its name is not known until instantiation.
Some((_, oi_root)) => {
let oi_meta_ident = self.lookup_lexical_or_missing(meta_name);
oi_meta_ident
.new_abstract_ident(self.asg_mut(), meta_name.span())
.and_then(|oi| oi.defined_by(self.asg_mut(), oi_root))
}
}
}
/// Attempt to retrieve an identifier and its scope information from the
@ -966,6 +1088,26 @@ impl AirAggregateCtx {
.map(EnvScopeKind::into_inner)
}
/// Resolve an identifier at the scope of the provided environment and
/// retrieve its definition.
///
/// If the identifier is not in scope or does not have a definition,
/// [`None`] will be returned;
/// the caller cannot distinguish between the two using this method;
/// see [`Self::env_scope_lookup`] if that distinction is important.
#[cfg(test)]
fn env_scope_lookup_ident_dfn<O: ObjectRelatable>(
&self,
env: impl ObjectIndexRelTo<Ident>,
name: SPair,
) -> Option<ObjectIndex<O>>
where
Ident: ObjectRelTo<O>,
{
self.env_scope_lookup::<Ident>(env, name)
.and_then(|oi| oi.definition(self.asg_ref()))
}
/// Attempt to retrieve an identifier from the graph by name relative to
/// the immediate environment `imm_env`.
///
@ -1168,7 +1310,7 @@ impl<T> AsRef<T> for EnvScopeKind<T> {
}
}
impl<T, U> Functor<T, U> for EnvScopeKind<T> {
impl<T, U> Map<T, U> for EnvScopeKind<T> {
type Target = EnvScopeKind<U>;
fn map(self, f: impl FnOnce(T) -> U) -> Self::Target {

View File

@ -30,11 +30,8 @@ use super::{
AirAggregate, AirAggregateCtx,
};
use crate::{
asg::{
graph::object::{ObjectIndexRelTo, ObjectIndexTo},
ObjectKind,
},
f::Functor,
asg::{graph::object::ObjectIndexTo, ObjectKind},
f::Map,
parse::prelude::*,
};
@ -96,8 +93,12 @@ impl ParseState for AirExprAggregate {
}
(BuildingExpr(es, poi), AirExpr(ExprStart(op, span))) => {
let oi = poi.create_subexpr(ctx.asg_mut(), Expr::new(op, span));
Transition(BuildingExpr(es.push(poi), oi)).incomplete()
match poi.create_subexpr(ctx.asg_mut(), Expr::new(op, span)) {
Ok(oi) => {
Transition(BuildingExpr(es.push(poi), oi)).incomplete()
}
Err(e) => Transition(BuildingExpr(es, poi)).err(e),
}
}
(BuildingExpr(es, oi), AirExpr(ExprEnd(end))) => {
@ -123,7 +124,7 @@ impl ParseState for AirExprAggregate {
}
(BuildingExpr(es, oi), AirBind(BindIdent(id))) => {
let result = ctx.defines(id).and_then(|oi_ident| {
let result = ctx.defines_concrete(id).and_then(|oi_ident| {
oi_ident.bind_definition(ctx.asg_mut(), id, oi)
});
@ -138,20 +139,38 @@ impl ParseState for AirExprAggregate {
}
}
(BuildingExpr(es, oi), AirBind(RefIdent(name))) => {
let oi_ident = ctx.lookup_lexical_or_missing(name);
Transition(BuildingExpr(
es,
oi.ref_expr(ctx.asg_mut(), oi_ident),
))
.incomplete()
(BuildingExpr(es, oi), AirBind(BindIdentAbstract(meta_name))) => {
let result =
ctx.defines_abstract(meta_name).and_then(|oi_ident| {
oi_ident.bind_definition(ctx.asg_mut(), meta_name, oi)
});
// It is important that we do not mark this expression as
// reachable unless we successfully bind the identifier.
// Even though the identifier is abstract,
// we want to mimic the concrete structure of the graph.
match result {
Ok(oi_ident) => {
Transition(BuildingExpr(es.reachable_by(oi_ident), oi))
.incomplete()
}
Err(e) => Transition(BuildingExpr(es, oi)).err(e),
}
}
(BuildingExpr(es, oi), AirDoc(DocIndepClause(clause))) => {
oi.desc_short(ctx.asg_mut(), clause);
Transition(BuildingExpr(es, oi)).incomplete()
(BuildingExpr(es, oi), AirBind(RefIdent(name))) => {
let oi_ident = ctx.lookup_lexical_or_missing(name);
oi.ref_expr(ctx.asg_mut(), oi_ident)
.map(|_| ())
.transition(BuildingExpr(es, oi))
}
(BuildingExpr(es, oi), AirDoc(DocIndepClause(clause))) => oi
.add_desc_short(ctx.asg_mut(), clause)
.map(|_| ())
.transition(BuildingExpr(es, oi)),
(BuildingExpr(es, oi), AirDoc(DocText(text))) => Transition(
BuildingExpr(es, oi),
)
@ -188,9 +207,10 @@ impl AirExprAggregate {
oi_root: Option<ObjectIndexTo<Expr>>,
oi_expr: ObjectIndex<Expr>,
) -> Result<(), AsgError> {
oi_root
.ok_or(AsgError::DanglingExpr(oi_expr.resolve(asg).span()))?
.add_edge_to(asg, oi_expr, None);
let oi_container = oi_root
.ok_or(AsgError::DanglingExpr(oi_expr.resolve(asg).span()))?;
oi_expr.held_by(asg, oi_container)?;
Ok(())
}

View File

@ -18,18 +18,23 @@
// along with this program. If not, see <http://www.gnu.org/licenses/>.
use super::*;
use crate::asg::{
air::{
test::{
air_ctx_from_pkg_body_toks, air_ctx_from_toks, parse_as_pkg_body,
pkg_expect_ident_obj, pkg_expect_ident_oi, pkg_lookup,
},
Air, AirAggregate,
},
graph::object::{expr::ExprRel, Doc, ObjectRel},
ExprOp, Ident,
};
use crate::span::dummy::*;
use crate::{
asg::{
air::{
test::{
air_ctx_from_pkg_body_toks, air_ctx_from_toks,
parse_as_pkg_body, pkg_expect_ident_obj, pkg_expect_ident_oi,
pkg_lookup,
},
Air::*,
AirAggregate,
},
graph::object::{expr::ExprRel, Doc, ObjectRel},
ExprOp, Ident,
},
parse::util::spair,
};
use std::assert_matches::assert_matches;
type Sut = AirAggregate;
@ -46,13 +51,13 @@ pub fn collect_subexprs(
#[test]
fn expr_empty_ident() {
let id = SPair("foo".into(), S2);
let id = spair("foo", S2);
#[rustfmt::skip]
let toks = vec![
Air::ExprStart(ExprOp::Sum, S1),
Air::BindIdent(id),
Air::ExprEnd(S3),
let toks = [
ExprStart(ExprOp::Sum, S1),
BindIdent(id),
ExprEnd(S3),
];
let ctx = air_ctx_from_pkg_body_toks(toks);
@ -65,13 +70,13 @@ fn expr_empty_ident() {
#[test]
fn expr_without_pkg() {
let toks = vec![
let toks = [
// No package
// (because we're not parsing with `parse_as_pkg_body` below)
Air::ExprStart(ExprOp::Sum, S1),
ExprStart(ExprOp::Sum, S1),
// RECOVERY
Air::PkgStart(S2, SPair("/pkg".into(), S2)),
Air::PkgEnd(S3),
PkgStart(S2, spair("/pkg", S2)),
PkgEnd(S3),
];
assert_eq!(
@ -88,18 +93,18 @@ fn expr_without_pkg() {
// Note that this can't happen in e.g. NIR / TAME's source XML.
#[test]
fn close_pkg_mid_expr() {
let id = SPair("foo".into(), S4);
let id = spair("foo", S4);
#[rustfmt::skip]
let toks = vec![
Air::PkgStart(S1, SPair("/pkg".into(), S1)),
Air::ExprStart(ExprOp::Sum, S2),
Air::PkgEnd(S3),
let toks = [
PkgStart(S1, spair("/pkg", S1)),
ExprStart(ExprOp::Sum, S2),
PkgEnd(S3),
// RECOVERY: Let's finish the expression first...
Air::BindIdent(id),
Air::ExprEnd(S5),
BindIdent(id),
ExprEnd(S5),
// ...and then try to close again.
Air::PkgEnd(S6),
PkgEnd(S6),
];
assert_eq!(
@ -123,20 +128,20 @@ fn close_pkg_mid_expr() {
#[test]
fn open_pkg_mid_expr() {
let pkg_a = SPair("/pkg".into(), S1);
let pkg_nested = SPair("/pkg-nested".into(), S3);
let id = SPair("foo".into(), S4);
let pkg_a = spair("/pkg", S1);
let pkg_nested = spair("/pkg-nested", S3);
let id = spair("foo", S4);
#[rustfmt::skip]
let toks = vec![
Air::PkgStart(S1, pkg_a),
Air::ExprStart(ExprOp::Sum, S2),
Air::PkgStart(S3, pkg_nested),
let toks = [
PkgStart(S1, pkg_a),
ExprStart(ExprOp::Sum, S2),
PkgStart(S3, pkg_nested),
// RECOVERY: We should still be able to complete successfully.
Air::BindIdent(id),
Air::ExprEnd(S5),
BindIdent(id),
ExprEnd(S5),
// Closes the _original_ package.
Air::PkgEnd(S6),
PkgEnd(S6),
];
assert_eq!(
@ -163,23 +168,23 @@ fn open_pkg_mid_expr() {
#[test]
fn expr_non_empty_ident_root() {
let id_a = SPair("foo".into(), S2);
let id_b = SPair("bar".into(), S2);
let id_a = spair("foo", S2);
let id_b = spair("bar", S2);
#[rustfmt::skip]
let toks = vec![
Air::ExprStart(ExprOp::Sum, S1),
let toks = [
ExprStart(ExprOp::Sum, S1),
// Identifier while still empty...
Air::BindIdent(id_a),
BindIdent(id_a),
Air::ExprStart(ExprOp::Sum, S3),
ExprStart(ExprOp::Sum, S3),
// (note that the inner expression _does not_ have an ident
// binding)
Air::ExprEnd(S4),
ExprEnd(S4),
// ...and an identifier non-empty.
Air::BindIdent(id_b),
Air::ExprEnd(S6),
BindIdent(id_b),
ExprEnd(S6),
];
let ctx = air_ctx_from_pkg_body_toks(toks);
@ -197,20 +202,20 @@ fn expr_non_empty_ident_root() {
// which only becomes reachable at the end.
#[test]
fn expr_non_empty_bind_only_after() {
let id = SPair("foo".into(), S2);
let id = spair("foo", S2);
#[rustfmt::skip]
let toks = vec![
Air::ExprStart(ExprOp::Sum, S1),
let toks = [
ExprStart(ExprOp::Sum, S1),
// Expression root is still dangling at this point.
Air::ExprStart(ExprOp::Sum, S2),
Air::ExprEnd(S3),
ExprStart(ExprOp::Sum, S2),
ExprEnd(S3),
// We only bind an identifier _after_ we've created the expression,
// which should cause the still-dangling root to become
// reachable.
Air::BindIdent(id),
Air::ExprEnd(S5),
BindIdent(id),
ExprEnd(S5),
];
let ctx = air_ctx_from_pkg_body_toks(toks);
@ -225,11 +230,11 @@ fn expr_non_empty_bind_only_after() {
// since they're either mistakes or misconceptions.
#[test]
fn expr_dangling_no_subexpr() {
let toks = vec![
Air::ExprStart(ExprOp::Sum, S1),
let toks = [
ExprStart(ExprOp::Sum, S1),
// No `BindIdent`,
// so this expression is dangling.
Air::ExprEnd(S2),
ExprEnd(S2),
];
// The error span should encompass the entire expression.
@ -251,14 +256,14 @@ fn expr_dangling_no_subexpr() {
#[test]
fn expr_dangling_with_subexpr() {
#[rustfmt::skip]
let toks = vec![
Air::ExprStart(ExprOp::Sum, S1),
let toks = [
ExprStart(ExprOp::Sum, S1),
// Expression root is still dangling at this point.
Air::ExprStart(ExprOp::Sum, S2),
Air::ExprEnd(S3),
ExprStart(ExprOp::Sum, S2),
ExprEnd(S3),
// Still no ident binding,
// so root should still be dangling.
Air::ExprEnd(S4),
ExprEnd(S4),
];
let full_span = S1.merge(S4).unwrap();
@ -280,23 +285,23 @@ fn expr_dangling_with_subexpr() {
#[test]
fn expr_dangling_with_subexpr_ident() {
let id = SPair("foo".into(), S3);
let id = spair("foo", S3);
#[rustfmt::skip]
let toks = vec![
Air::ExprStart(ExprOp::Sum, S1),
let toks = [
ExprStart(ExprOp::Sum, S1),
// Expression root is still dangling at this point.
Air::ExprStart(ExprOp::Sum, S2),
ExprStart(ExprOp::Sum, S2),
// The _inner_ expression receives an identifier,
// but that should have no impact on the dangling status of
// the root,
// especially given that subexpressions are always reachable
// anyway.
Air::BindIdent(id),
Air::ExprEnd(S4),
BindIdent(id),
ExprEnd(S4),
// But the root still has no ident binding,
// and so should still be dangling.
Air::ExprEnd(S5),
ExprEnd(S5),
];
let full_span = S1.merge(S5).unwrap();
@ -323,18 +328,18 @@ fn expr_dangling_with_subexpr_ident() {
// but this also protects against potential future breakages.
#[test]
fn expr_reachable_subsequent_dangling() {
let id = SPair("foo".into(), S2);
let id = spair("foo", S2);
#[rustfmt::skip]
let toks = vec![
let toks = [
// Reachable
Air::ExprStart(ExprOp::Sum, S1),
Air::BindIdent(id),
Air::ExprEnd(S3),
ExprStart(ExprOp::Sum, S1),
BindIdent(id),
ExprEnd(S3),
// Dangling
Air::ExprStart(ExprOp::Sum, S4),
Air::ExprEnd(S5),
ExprStart(ExprOp::Sum, S4),
ExprEnd(S5),
];
// The error span should encompass the entire expression.
@ -362,18 +367,18 @@ fn expr_reachable_subsequent_dangling() {
// Recovery from dangling expression.
#[test]
fn recovery_expr_reachable_after_dangling() {
let id = SPair("foo".into(), S4);
let id = spair("foo", S4);
#[rustfmt::skip]
let toks = vec![
let toks = [
// Dangling
Air::ExprStart(ExprOp::Sum, S1),
Air::ExprEnd(S2),
ExprStart(ExprOp::Sum, S1),
ExprEnd(S2),
// Reachable, after error from dangling.
Air::ExprStart(ExprOp::Sum, S3),
Air::BindIdent(id),
Air::ExprEnd(S5),
ExprStart(ExprOp::Sum, S3),
BindIdent(id),
ExprEnd(S5),
];
// The error span should encompass the entire expression.
@ -415,21 +420,21 @@ fn recovery_expr_reachable_after_dangling() {
#[test]
fn expr_close_unbalanced() {
let id = SPair("foo".into(), S3);
let id = spair("foo", S3);
#[rustfmt::skip]
let toks = vec![
let toks = [
// Close before _any_ open.
Air::ExprEnd(S1),
ExprEnd(S1),
// Should recover,
// allowing for a normal expr.
Air::ExprStart(ExprOp::Sum, S2),
Air::BindIdent(id),
Air::ExprEnd(S4),
ExprStart(ExprOp::Sum, S2),
BindIdent(id),
ExprEnd(S4),
// And now an extra close _after_ a valid expr.
Air::ExprEnd(S5),
ExprEnd(S5),
];
let mut sut = parse_as_pkg_body(toks);
@ -467,26 +472,26 @@ fn expr_close_unbalanced() {
// for non-associative expressions.
#[test]
fn sibling_subexprs_have_ordered_edges_to_parent() {
let id_root = SPair("root".into(), S1);
let id_root = spair("root", S1);
#[rustfmt::skip]
let toks = vec![
Air::ExprStart(ExprOp::Sum, S1),
let toks = [
ExprStart(ExprOp::Sum, S1),
// Identify the root so that it is not dangling.
Air::BindIdent(id_root),
BindIdent(id_root),
// Sibling A
Air::ExprStart(ExprOp::Sum, S3),
Air::ExprEnd(S4),
ExprStart(ExprOp::Sum, S3),
ExprEnd(S4),
// Sibling B
Air::ExprStart(ExprOp::Sum, S5),
Air::ExprEnd(S6),
ExprStart(ExprOp::Sum, S5),
ExprEnd(S6),
// Sibling C
Air::ExprStart(ExprOp::Sum, S7),
Air::ExprEnd(S8),
Air::ExprEnd(S9),
ExprStart(ExprOp::Sum, S7),
ExprEnd(S8),
ExprEnd(S9),
];
let ctx = air_ctx_from_pkg_body_toks(toks);
@ -516,21 +521,21 @@ fn sibling_subexprs_have_ordered_edges_to_parent() {
#[test]
fn nested_subexprs_related_to_relative_parent() {
let id_root = SPair("root".into(), S1);
let id_suba = SPair("suba".into(), S2);
let id_root = spair("root", S1);
let id_suba = spair("suba", S2);
#[rustfmt::skip]
let toks = vec![
Air::ExprStart(ExprOp::Sum, S1), // 0
Air::BindIdent(id_root),
let toks = [
ExprStart(ExprOp::Sum, S1), // 0
BindIdent(id_root),
Air::ExprStart(ExprOp::Sum, S2), // 1
Air::BindIdent(id_suba),
ExprStart(ExprOp::Sum, S2), // 1
BindIdent(id_suba),
Air::ExprStart(ExprOp::Sum, S3), // 2
Air::ExprEnd(S4),
Air::ExprEnd(S5),
Air::ExprEnd(S6),
ExprStart(ExprOp::Sum, S3), // 2
ExprEnd(S4),
ExprEnd(S5),
ExprEnd(S6),
];
let ctx = air_ctx_from_pkg_body_toks(toks);
@ -556,18 +561,18 @@ fn nested_subexprs_related_to_relative_parent() {
fn expr_redefine_ident() {
// Same identifier but with different spans
// (which would be the case in the real world).
let id_first = SPair("foo".into(), S2);
let id_dup = SPair("foo".into(), S3);
let id_first = spair("foo", S2);
let id_dup = spair("foo", S3);
#[rustfmt::skip]
let toks = vec![
Air::ExprStart(ExprOp::Sum, S1),
Air::BindIdent(id_first),
let toks = [
ExprStart(ExprOp::Sum, S1),
BindIdent(id_first),
Air::ExprStart(ExprOp::Sum, S3),
Air::BindIdent(id_dup),
Air::ExprEnd(S4),
Air::ExprEnd(S5),
ExprStart(ExprOp::Sum, S3),
BindIdent(id_dup),
ExprEnd(S4),
ExprEnd(S5),
];
let mut sut = parse_as_pkg_body(toks);
@ -606,32 +611,32 @@ fn expr_redefine_ident() {
fn expr_still_dangling_on_redefine() {
// Same identifier but with different spans
// (which would be the case in the real world).
let id_first = SPair("foo".into(), S2);
let id_dup = SPair("foo".into(), S5);
let id_dup2 = SPair("foo".into(), S8);
let id_second = SPair("bar".into(), S9);
let id_first = spair("foo", S2);
let id_dup = spair("foo", S5);
let id_dup2 = spair("foo", S8);
let id_second = spair("bar", S9);
#[rustfmt::skip]
let toks = vec![
let toks = [
// First expr (OK)
Air::ExprStart(ExprOp::Sum, S1),
Air::BindIdent(id_first),
Air::ExprEnd(S3),
ExprStart(ExprOp::Sum, S1),
BindIdent(id_first),
ExprEnd(S3),
// Second expr should still dangle due to use of duplicate
// identifier
Air::ExprStart(ExprOp::Sum, S4),
Air::BindIdent(id_dup),
Air::ExprEnd(S6),
ExprStart(ExprOp::Sum, S4),
BindIdent(id_dup),
ExprEnd(S6),
// Third expr will error on redefine but then be successful.
// This probably won't happen in practice with TAME's original
// source language,
// but could happen at e.g. a REPL.
Air::ExprStart(ExprOp::Sum, S7),
Air::BindIdent(id_dup2), // fail
Air::BindIdent(id_second), // succeed
Air::ExprEnd(S10),
ExprStart(ExprOp::Sum, S7),
BindIdent(id_dup2), // fail
BindIdent(id_second), // succeed
ExprEnd(S10),
];
let mut sut = parse_as_pkg_body(toks);
@ -691,27 +696,27 @@ fn expr_still_dangling_on_redefine() {
#[test]
fn expr_ref_to_ident() {
let id_foo = SPair("foo".into(), S2);
let id_bar = SPair("bar".into(), S6);
let id_foo = spair("foo", S2);
let id_bar = spair("bar", S6);
#[rustfmt::skip]
let toks = vec![
Air::ExprStart(ExprOp::Sum, S1),
Air::BindIdent(id_foo),
let toks = [
ExprStart(ExprOp::Sum, S1),
BindIdent(id_foo),
// Reference to an as-of-yet-undefined id (okay),
// with a different span than `id_bar`.
Air::RefIdent(SPair("bar".into(), S3)),
Air::ExprEnd(S4),
RefIdent(spair("bar", S3)),
ExprEnd(S4),
//
// Another expression to reference the first
// (we don't handle cyclic references until a topological sort,
// so no point in referencing ourselves;
// it'd work just fine here.)
Air::ExprStart(ExprOp::Sum, S5),
Air::BindIdent(id_bar),
Air::ExprEnd(S7),
ExprStart(ExprOp::Sum, S5),
BindIdent(id_bar),
ExprEnd(S7),
];
let ctx = air_ctx_from_pkg_body_toks(toks);
@ -748,23 +753,23 @@ fn expr_ref_to_ident() {
#[test]
fn idents_share_defining_pkg() {
let id_foo = SPair("foo".into(), S3);
let id_bar = SPair("bar".into(), S5);
let id_baz = SPair("baz".into(), S6);
let id_foo = spair("foo", S3);
let id_bar = spair("bar", S5);
let id_baz = spair("baz", S6);
// An expression nested within another.
#[rustfmt::skip]
let toks = vec![
Air::PkgStart(S1, SPair("/pkg".into(), S1)),
Air::ExprStart(ExprOp::Sum, S2),
Air::BindIdent(id_foo),
let toks = [
PkgStart(S1, spair("/pkg", S1)),
ExprStart(ExprOp::Sum, S2),
BindIdent(id_foo),
Air::ExprStart(ExprOp::Sum, S4),
Air::BindIdent(id_bar),
Air::RefIdent(id_baz),
Air::ExprEnd(S7),
Air::ExprEnd(S8),
Air::PkgEnd(S9),
ExprStart(ExprOp::Sum, S4),
BindIdent(id_bar),
RefIdent(id_baz),
ExprEnd(S7),
ExprEnd(S8),
PkgEnd(S9),
];
let ctx = air_ctx_from_toks(toks);
@ -789,15 +794,15 @@ fn idents_share_defining_pkg() {
#[test]
fn expr_doc_short_desc() {
let id_expr = SPair("foo".into(), S2);
let clause = SPair("short desc".into(), S3);
let id_expr = spair("foo", S2);
let clause = spair("short desc", S3);
#[rustfmt::skip]
let toks = vec![
Air::ExprStart(ExprOp::Sum, S1),
Air::BindIdent(id_expr),
Air::DocIndepClause(clause),
Air::ExprEnd(S4),
let toks = [
ExprStart(ExprOp::Sum, S1),
BindIdent(id_expr),
DocIndepClause(clause),
ExprEnd(S4),
];
let ctx = air_ctx_from_pkg_body_toks(toks);
@ -813,3 +818,73 @@ fn expr_doc_short_desc() {
oi_docs.collect::<Vec<_>>(),
);
}
// Binding an abstract identifier to an expression means that the expression
// may _eventually_ be reachable after expansion,
// but it is not yet.
// They must therefore only be utilized within the context of a container
// that supports dangling expressions,
// like a template.
#[test]
fn abstract_bind_without_dangling_container() {
let id_meta = spair("@foo@", S2);
let id_ok = spair("concrete", S5);
#[rustfmt::skip]
let toks = [
ExprStart(ExprOp::Sum, S1),
// This expression is bound to an _abstract_ identifier,
// which will be expanded at a later time.
// Consequently,
// this expression is still dangling.
BindIdentAbstract(id_meta),
// Since the expression is still dangling,
// attempting to close it will produce an error.
ExprEnd(S3),
// RECOVERY: Since an attempt at identification has been made,
// we shouldn't expect that another attempt will be made.
// The sensible thing to do is to move on to try to find other
// errors,
// leaving the expression alone and unreachable.
ExprStart(ExprOp::Sum, S4),
// This is intended to demonstrate that we can continue on to the
// next expression despite the prior error.
BindIdent(id_ok),
ExprEnd(S6),
];
let mut sut = parse_as_pkg_body(toks);
assert_eq!(
#[rustfmt::skip]
vec![
Ok(Parsed::Incomplete), // PkgStart
Ok(Parsed::Incomplete), // ExprStart
// This provides an _abstract_ identifier,
// which is not permitted in this context.
Err(ParseError::StateError(AsgError::InvalidAbstractBindContext(
id_meta,
Some(S1), // Pkg
))),
// RECOVERY: Ignore the bind and move to close.
// The above identifier was rejected and so we are still dangling.
Err(ParseError::StateError(AsgError::DanglingExpr(
S1.merge(S3).unwrap()
))),
// RECOVERY: This observes that we're able to continue parsing
// the package after the above identification problem.
Ok(Parsed::Incomplete), // ExprStart
Ok(Parsed::Incomplete), // BindIdent (ok)
Ok(Parsed::Incomplete), // ExprEnd
Ok(Parsed::Incomplete), // PkgEnd
],
sut.by_ref().collect::<Vec<_>>(),
);
let _ = sut.finalize().unwrap();
}

View File

@ -542,6 +542,10 @@ sum_ir! {
/// Assign an identifier to the active object.
///
/// The "active" object depends on the current parsing state.
///
/// See also [`Self::BindIdentAbstract`] if the name of the
/// identifier cannot be know until future expansion based on
/// the value of a metavariable.
BindIdent(id: SPair) => {
span: id,
display: |f| write!(
@ -551,6 +555,26 @@ sum_ir! {
),
},
/// Assign an abstract identifier to the active object.
///
/// This differs from [`Self::BindIdent`] in that the name of
/// the identifier will not be known until expansion time.
/// The identifier is bound to a metavariable of the
/// name `meta`,
/// from which its name will eventually be derived.
///
/// If the name is known,
/// use [`Self::BindIdent`] to bind a concrete identifier.
BindIdentAbstract(meta: SPair) => {
span: meta,
display: |f| write!(
f,
"identify active object by the value of the \
metavariable {} during future expansion",
TtQuote::wrap(meta),
),
},
/// Reference another object identified by the given [`SPair`].
///
/// Objects can be referenced before they are declared or defined,
@ -646,7 +670,7 @@ sum_ir! {
/// Subset of [`Air`] tokens for defining [`Tpl`]s.
///
/// Templates serve as containers for objects that reference
/// metasyntactic variables,
/// metavariables,
/// defined by [`AirMeta::MetaStart`].
///
/// Template Application
@ -747,6 +771,18 @@ sum_ir! {
},
}
/// Metalinguistic objects.
///
/// TAME's metalanguage supports the generation of lexemes using
/// metavariables.
/// Those generated lexemes are utilized by the template system
/// (via [`AirTpl`])
/// during expansion,
/// yielding objects as if the user had entered the lexemes
/// statically.
///
/// [`AirBind`] is able to utilize metavariables for
/// dynamically generated bindings.
enum AirMeta {
/// Begin a metavariable definition.
///
@ -769,7 +805,7 @@ sum_ir! {
span: span,
display: |f| write!(
f,
"open definition of metasyntactic variable",
"open definition of metavariable",
),
},
@ -787,7 +823,7 @@ sum_ir! {
span: span,
display: |f| write!(
f,
"close definition of metasyntactic variable",
"close definition of metavariable",
),
},
@ -854,18 +890,7 @@ sum_ir! {
pub sum enum AirBindableTpl = AirTpl | AirBind | AirDoc;
/// Tokens that may be used to define metavariables.
pub sum enum AirBindableMeta = AirMeta | AirBind;
}
impl AirBind {
/// Name of the identifier described by this token.
pub fn name(&self) -> SPair {
use AirBind::*;
match self {
BindIdent(name) | RefIdent(name) => *name,
}
}
pub sum enum AirBindableMeta = AirMeta | AirBind | AirDoc;
}
impl AirIdent {

View File

@ -26,12 +26,9 @@ use super::{
ir::AirBindableMeta,
AirAggregate, AirAggregateCtx,
};
use crate::{
asg::graph::object::Meta, diagnose::Annotate, diagnostic_todo,
parse::prelude::*,
};
use crate::{asg::graph::object::Meta, diagnostic_todo, parse::prelude::*};
/// Metasyntactic variable (metavariable) parser.
/// Metalinguistic variable (metavariable) parser.
#[derive(Debug, PartialEq)]
pub enum AirMetaAggregate {
/// Ready for the start of a metavariable.
@ -64,7 +61,7 @@ impl ParseState for AirMetaAggregate {
tok: Self::Token,
ctx: &mut Self::Context,
) -> TransitionResult<Self::Super> {
use super::ir::{AirBind::*, AirMeta::*};
use super::ir::{AirBind::*, AirDoc::*, AirMeta::*};
use AirBindableMeta::*;
use AirMetaAggregate::*;
@ -78,26 +75,58 @@ impl ParseState for AirMetaAggregate {
Transition(Ready).incomplete()
}
(TplMeta(oi_meta), AirMeta(MetaLexeme(lexeme))) => Transition(
TplMeta(oi_meta.assign_lexeme(ctx.asg_mut(), lexeme)),
)
.incomplete(),
(TplMeta(oi_meta), AirMeta(MetaLexeme(lexeme))) => oi_meta
.append_lexeme(ctx.asg_mut(), lexeme)
.map(|_| ())
.transition(TplMeta(oi_meta)),
(TplMeta(oi_meta), AirBind(BindIdent(name))) => ctx
.defines(name)
.defines_concrete(name)
.and_then(|oi_ident| {
oi_ident.bind_definition(ctx.asg_mut(), name, oi_meta)
})
.map(|_| ())
.transition(TplMeta(oi_meta)),
(TplMeta(..), tok @ AirBind(RefIdent(..))) => {
(TplMeta(oi_meta), AirBind(BindIdentAbstract(meta_name))) => {
diagnostic_todo!(
vec![
oi_meta.note("for this metavariable"),
meta_name.note(
"attempting to bind an abstract identifier with \
this metavariable"
),
],
"attempt to bind abstract identifier to metavariable",
)
}
(TplMeta(oi_meta), AirDoc(DocIndepClause(clause))) => oi_meta
.add_desc_short(ctx.asg_mut(), clause)
.map(|_| ())
.transition(TplMeta(oi_meta)),
// TODO: The user _probably_ meant to use `<text>` in XML NIR,
// so maybe we should have an error to that effect.
(TplMeta(..), tok @ AirDoc(DocText(..))) => {
diagnostic_todo!(
vec![tok.note("this token")],
"AirBind in metavar context (param-value)"
"AirDoc in metavar context \
(is this something we want to support?)"
)
}
// Reference to another metavariable,
// e.g. using `<param-value>` in XML NIR.
(TplMeta(oi_meta), AirBind(RefIdent(name))) => {
let oi_ref = ctx.lookup_lexical_or_missing(name);
oi_meta
.concat_ref(ctx.asg_mut(), oi_ref)
.map(|_| ())
.transition(TplMeta(oi_meta))
}
(TplMeta(..), tok @ AirMeta(MetaStart(..))) => {
diagnostic_todo!(
vec![tok.note("this token")],
@ -119,8 +148,10 @@ impl ParseState for AirMetaAggregate {
)
}
// Maybe the bind can be handled by the parent frame.
(Ready, tok @ AirBind(..)) => Transition(Ready).dead(tok),
// Maybe the token can be handled by the parent frame.
(Ready, tok @ (AirBind(..) | AirDoc(..))) => {
Transition(Ready).dead(tok)
}
}
}
@ -134,3 +165,6 @@ impl AirMetaAggregate {
Self::Ready
}
}
#[cfg(test)]
mod test;

View File

@ -0,0 +1,369 @@
// Tests for ASG IR metavariable parsing
//
// Copyright (C) 2014-2023 Ryan Specialty, LLC.
//
// This file is part of TAME.
//
// 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.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
/// Metavariable parsing tests.
///
/// Note that some metavariable-related tests are in
/// [`super::super::tpl::test`],
/// where the focus is on how they are integrated as template
/// parameters.
/// The tests here focus instead on the particulars of metavariables
/// themselves,
/// with templates being only incidental.
use super::*;
use crate::{
asg::{
air::{
test::{parse_as_pkg_body, pkg_lookup},
Air::{self, *},
},
graph::object::{meta::MetaRel, Meta, Tpl},
},
parse::{util::spair, Parser},
span::{dummy::*, Span},
sym::SymbolId,
};
type Sut = AirAggregate;
// Metavariables can reference the lexical value of other metavariables.
// This does not actually check that the target reference is a metavariable,
// at least not at the time of writing.
// TODO: But the system ought to,
// since if we defer that to expansion-time,
// then we don't have confidence that the template definition is actually
// valid as a part of this compilation unit.
#[test]
fn metavar_ref_only() {
let name_meta = "@foo@";
let name_other = "@other@";
#[rustfmt::skip]
assert_concat_list(
[
MetaStart(S1),
BindIdent(spair(name_meta, S2)),
// Reference to another metavariable,
// effectively creating an alias.
RefIdent(spair(name_other, S3)), // --.
MetaEnd(S4), // |
// |
MetaStart(S5), // |
BindIdent(spair(name_other, S6)), // <-'
MetaEnd(S7),
],
name_meta,
S1.merge(S4),
[
&Meta::Required(S5.merge(S7).unwrap()),
],
);
}
// Similar to above,
// but multiple references.
#[test]
fn metavar_ref_multiple() {
let name_meta = "@foo@";
let name_other_a = "@other-a@";
let name_other_b = "@other-b@";
#[rustfmt::skip]
assert_concat_list(
// Def/ref is commutative,
// so this defines with both orderings to demonstrate that.
[
MetaStart(S1),
BindIdent(spair(name_other_a, S2)), // <-.
MetaEnd(S3), // |
// |
MetaStart(S3), // |
BindIdent(spair(name_meta, S4)), // |
// |
RefIdent(spair(name_other_a, S5)), // --'
RefIdent(spair(name_other_b, S6)), // --.
MetaEnd(S7), // |
// |
MetaStart(S8), // |
BindIdent(spair(name_other_b, S9)), // <-'
MetaEnd(S10),
],
name_meta,
S3.merge(S7),
[
&Meta::Required(S1.merge(S3).unwrap()),
&Meta::Required(S8.merge(S10).unwrap()),
],
);
}
// If a metavariable already has a concrete lexical value,
// then appending a reference to it should convert it into a list.
#[test]
fn metavar_ref_after_lexeme() {
let name_meta = "@foo@";
let value = "foo value";
let name_other = "@other@";
#[rustfmt::skip]
assert_concat_list(
[
MetaStart(S1),
BindIdent(spair(name_meta, S2)),
// At this point,
// we have only a concrete lexeme,
// and so we are not a list.
MetaLexeme(spair(value, S3)),
// Upon encountering this token,
// we should convert into a list.
RefIdent(spair(name_other, S4)), // --.
MetaEnd(S5), // |
// |
MetaStart(S6), // |
BindIdent(spair(name_other, S7)), // <-'
MetaEnd(S8),
],
name_meta,
S1.merge(S5),
// Having been converted into a list,
// we should have a reference to _two_ metavariables:
// one holding the original lexeme and the reference.
[
&Meta::Lexeme(S3, spair(value, S3)),
&Meta::Required(S6.merge(S8).unwrap()),
],
);
}
// If we have a reference,
// then that means we already have a `ConcatList`,
// and therefore should just have to append to it.
// This is the same as the above test,
// with operations reversed.
// It is not commutative,
// though,
// since concatenation is ordered.
#[test]
fn lexeme_after_metavar_ref() {
let name_meta = "@foo@";
let value = "foo value";
let name_other = "@other@";
#[rustfmt::skip]
assert_concat_list(
[
MetaStart(S1),
BindIdent(spair(name_meta, S2)),
// We produce a list here...
RefIdent(spair(name_other, S3)), // --.
// |
// ...and should just append a // |
// `Lexeme` here. // |
MetaLexeme(spair(value, S4)), // |
MetaEnd(S5), // |
// |
MetaStart(S6), // |
BindIdent(spair(name_other, S7)), // <-'
MetaEnd(S8),
],
name_meta,
S1.merge(S5),
// Same as the previous test,
// but concatenation order is reversed.
[
&Meta::Required(S6.merge(S8).unwrap()),
// Note the first span here is not that of the parent
// metavariable,
// since we do not want diagnostics to suggest that this
// object represents the entirety of the parent.
&Meta::Lexeme(S4, spair(value, S4)),
],
);
}
// Multiple lexemes should _also_ produce a list.
// While this could have been replaced by a single lexeme,
// there are still uses:
// maybe this was the result of template expansion,
// or simply a multi-line formatting choice in the provided sources.
#[test]
fn lexeme_after_lexeme() {
let name_meta = "@foo@";
let value_a = "foo value a";
let value_b = "foo value b";
#[rustfmt::skip]
assert_concat_list(
[
MetaStart(S1),
BindIdent(spair(name_meta, S2)),
MetaLexeme(spair(value_a, S3)),
MetaLexeme(spair(value_b, S4)),
MetaEnd(S5),
],
name_meta,
S1.merge(S5),
[
// Since these are Meta objects derived from the original,
// our spans
// (the first value in the tuple)
// are not that of the containing metavariable;
// we don't want diagnostics implying that this represents
// the entirety of that the parent metavariable.
&Meta::Lexeme(S3, spair(value_a, S3)),
&Meta::Lexeme(S4, spair(value_b, S4)),
],
);
}
/////// ///////
/////// Tests above; plumbing begins below ///////
/////// ///////
fn assert_concat_list<'a, IT, IE: 'a>(
toks: IT,
meta_name: impl Into<SymbolId>,
expect_span: Option<Span>,
expect: IE,
) where
IT: IntoIterator<Item = Air>,
IT::IntoIter: Debug,
IE: IntoIterator<Item = &'a Meta>,
IE::IntoIter: Debug + DoubleEndedIterator,
{
let (ctx, oi_tpl) = air_ctx_from_tpl_body_toks(toks);
let asg = ctx.asg_ref();
let oi_meta = ctx
.env_scope_lookup_ident_dfn::<Meta>(
oi_tpl,
spair(meta_name.into(), DUMMY_SPAN),
)
.unwrap();
// Meta references are only supported through lexical concatenation.
assert_eq!(
&Meta::ConcatList(expect_span.expect("expected metavar span is None")),
oi_meta.resolve(asg),
"expected metavariable to be a ConcatList with the provided span",
);
// We `collect()` rather than using `Iterator::eq` so that the failure
// message includes the data.
// If we do this too often,
// we can consider a crate like `itertools` or write our own
// comparison,
// but it's not worth doing for these tests where the cost of
// collection is so low and insignificant.
assert_eq!(
// TODO: We need to modify edge methods to return in the proper
// order (not reversed) without a performance hit,
// which will involve investigating Petgraph further.
expect.into_iter().rev().collect::<Vec<_>>(),
// Each reference is to an Ident whose definition is the other
// metavariable.
oi_meta
.edges(asg)
.filter_map(|rel| match rel {
MetaRel::Meta(oi) => Some(oi),
MetaRel::Ident(oi) => oi.definition::<Meta>(asg),
MetaRel::Doc(_) => None,
})
.map(ObjectIndex::cresolve(asg))
.collect::<Vec<_>>(),
"note: expected tokens are in reverse order in this error message",
);
}
const STUB_TPL_NAME: &str = "__incidental-tpl__";
/// Parse the provided tokens within the context of a template body.
///
/// This allows tests to focus on testing of metavariables instead of
/// mudding tests with setup.
///
/// This creates a package and template that are purely incidental and serve
/// only as scaffolding to put the parser into the necessary state and
/// context.
/// This is an alternative to providing methods to force the parse into a
/// certain context,
/// since we're acting as users of the SUT's public API and are
/// therefore testing real-world situations.
fn parse_as_tpl_body<I: IntoIterator<Item = Air>>(
toks: I,
) -> Parser<Sut, impl Iterator<Item = Air> + Debug>
where
<I as IntoIterator>::IntoIter: Debug,
{
#[rustfmt::skip]
let head = [
TplStart(S1),
BindIdent(spair(STUB_TPL_NAME, S1)),
];
#[rustfmt::skip]
let tail = [
TplEnd(S1),
];
#[rustfmt::skip]
parse_as_pkg_body(
head.into_iter()
.chain(toks.into_iter())
.chain(tail.into_iter())
)
}
fn air_ctx_from_tpl_body_toks<I: IntoIterator<Item = Air>>(
toks: I,
) -> (<Sut as ParseState>::Context, ObjectIndex<Tpl>)
where
I::IntoIter: Debug,
{
let mut sut = parse_as_tpl_body(toks);
assert!(sut.all(|x| x.is_ok()));
let ctx = sut.finalize().unwrap().into_private_context();
let oi_tpl = pkg_lookup(&ctx, spair(STUB_TPL_NAME, S1))
.expect(
"could not locate stub template (did you call \
air_ctx_from_tpl_body_toks without parse_as_tpl_body?)",
)
.definition(ctx.asg_ref())
.expect("missing stub template definition (test setup bug?)");
(ctx, oi_tpl)
}

View File

@ -26,7 +26,6 @@
use super::{super::AsgError, ir::AirIdent, AirAggregate, AirAggregateCtx};
use crate::parse::prelude::*;
use std::fmt::Display;
#[derive(Debug, PartialEq)]
pub enum AirOpaqueAggregate {
@ -74,9 +73,11 @@ impl ParseState for AirOpaqueAggregate {
(Ready, IdentDep(name, dep)) => {
let oi_from = ctx.lookup_lexical_or_missing(name);
let oi_to = ctx.lookup_lexical_or_missing(dep);
oi_from.add_opaque_dep(ctx.asg_mut(), oi_to);
Transition(Ready).incomplete()
oi_from
.add_opaque_dep(ctx.asg_mut(), oi_to)
.map(|_| ())
.transition(Ready)
}
(Ready, IdentFragment(name, text)) => ctx
@ -85,11 +86,11 @@ impl ParseState for AirOpaqueAggregate {
.map(|_| ())
.transition(Ready),
(Ready, IdentRoot(name)) => {
ctx.lookup_lexical_or_missing(name).root(ctx.asg_mut());
Transition(Ready).incomplete()
}
(Ready, IdentRoot(name)) => ctx
.lookup_lexical_or_missing(name)
.root(ctx.asg_mut())
.map(|_| ())
.transition(Ready),
}
}

View File

@ -26,7 +26,7 @@ use super::{
ir::AirLiteratePkg,
AirAggregate, AirAggregateCtx,
};
use crate::{diagnose::Annotate, diagnostic_todo, parse::prelude::*};
use crate::{diagnostic_todo, parse::prelude::*};
/// Package parsing with support for loaded identifiers.
///
@ -112,10 +112,10 @@ impl ParseState for AirPkgAggregate {
)
}
(Toplevel(oi_pkg), AirDoc(DocText(text))) => {
oi_pkg.append_doc_text(ctx.asg_mut(), text);
Transition(Toplevel(oi_pkg)).incomplete()
}
(Toplevel(oi_pkg), AirDoc(DocText(text))) => oi_pkg
.append_doc_text(ctx.asg_mut(), text)
.map(|_| ())
.transition(Toplevel(oi_pkg)),
// Package import
(Toplevel(oi_pkg), AirPkg(PkgImport(namespec))) => oi_pkg

View File

@ -26,11 +26,11 @@ use crate::{
graph::object::{ObjectRel, ObjectRelFrom, ObjectRelatable},
IdentKind, ObjectIndexRelTo, Source, TransitionError,
},
parse::{ParseError, Parsed, Parser},
parse::{util::spair, ParseError, Parsed, Parser},
span::dummy::*,
};
type Sut = AirAggregate;
pub type Sut = AirAggregate;
use Air::*;
use Parsed::Incomplete;
@ -39,7 +39,7 @@ mod scope;
#[test]
fn ident_decl() {
let id = SPair("foo".into(), S2);
let id = spair("foo", S2);
let kind = IdentKind::Tpl;
let src = Source {
src: Some("test/decl".into()),
@ -47,8 +47,8 @@ fn ident_decl() {
};
#[rustfmt::skip]
let toks = vec![
PkgStart(S1, SPair("/pkg".into(), S1)),
let toks = [
PkgStart(S1, spair("/pkg", S1)),
IdentDecl(id, kind.clone(), src.clone()),
// Attempt re-declaration.
IdentDecl(id, kind.clone(), src.clone()),
@ -88,8 +88,8 @@ fn ident_decl() {
#[test]
fn ident_extern_decl() {
let id = SPair("foo".into(), S2);
let re_id = SPair("foo".into(), S3);
let id = spair("foo", S2);
let re_id = spair("foo", S3);
let kind = IdentKind::Tpl;
let different_kind = IdentKind::Meta;
let src = Source {
@ -98,8 +98,8 @@ fn ident_extern_decl() {
};
#[rustfmt::skip]
let toks = vec![
PkgStart(S1, SPair("/pkg".into(), S1)),
let toks = [
PkgStart(S1, spair("/pkg", S1)),
IdentExternDecl(id, kind.clone(), src.clone()),
// Redeclare with a different kind
IdentExternDecl(re_id, different_kind.clone(), src.clone()),
@ -141,12 +141,12 @@ fn ident_extern_decl() {
#[test]
fn ident_dep() {
let id = SPair("foo".into(), S2);
let dep = SPair("dep".into(), S3);
let id = spair("foo", S2);
let dep = spair("dep", S3);
#[rustfmt::skip]
let toks = vec![
PkgStart(S1, SPair("/pkg".into(), S1)),
let toks = [
PkgStart(S1, spair("/pkg", S1)),
IdentDep(id, dep),
PkgEnd(S4),
].into_iter();
@ -174,7 +174,7 @@ fn ident_dep() {
#[test]
fn ident_fragment() {
let id = SPair("frag".into(), S2);
let id = spair("frag", S2);
let kind = IdentKind::Tpl;
let src = Source {
src: Some("test/frag".into()),
@ -183,8 +183,8 @@ fn ident_fragment() {
let frag = "fragment text".into();
#[rustfmt::skip]
let toks = vec![
PkgStart(S1, SPair("/pkg".into(), S1)),
let toks = [
PkgStart(S1, spair("/pkg", S1)),
// Identifier must be declared before it can be given a
// fragment.
IdentDecl(id, kind.clone(), src.clone()),
@ -232,11 +232,11 @@ fn ident_fragment() {
// `Ident::Missing`.
#[test]
fn ident_root_missing() {
let id = SPair("toroot".into(), S2);
let id = spair("toroot", S2);
#[rustfmt::skip]
let toks = vec![
PkgStart(S1, SPair("/pkg".into(), S1)),
let toks = [
PkgStart(S1, spair("/pkg", S1)),
IdentRoot(id),
PkgEnd(S3),
].into_iter();
@ -269,7 +269,7 @@ fn ident_root_missing() {
#[test]
fn ident_root_existing() {
let id = SPair("toroot".into(), S2);
let id = spair("toroot", S2);
let kind = IdentKind::Tpl;
let src = Source {
src: Some("test/root-existing".into()),
@ -281,10 +281,10 @@ fn ident_root_existing() {
assert!(!kind.is_auto_root());
#[rustfmt::skip]
let toks = vec![
PkgStart(S1, SPair("/pkg".into(), S1)),
let toks = [
PkgStart(S1, spair("/pkg", S1)),
IdentDecl(id, kind.clone(), src.clone()),
IdentRoot(SPair(id.symbol(), S3)),
IdentRoot(spair(id, S3)),
PkgEnd(S3),
]
.into_iter();
@ -329,8 +329,8 @@ fn declare_kind_auto_root() {
assert!(auto_kind.is_auto_root());
assert!(!no_auto_kind.is_auto_root());
let id_auto = SPair("auto_root".into(), S2);
let id_no_auto = SPair("no_auto_root".into(), S3);
let id_auto = spair("auto_root", S2);
let id_no_auto = spair("no_auto_root", S3);
let src = Source {
src: Some("src/pkg".into()),
@ -339,7 +339,7 @@ fn declare_kind_auto_root() {
#[rustfmt::skip]
let toks = [
PkgStart(S1, SPair("/pkg".into(), S1)),
PkgStart(S1, spair("/pkg", S1)),
// auto-rooting
IdentDecl(id_auto, auto_kind, src.clone()),
// non-auto-rooting
@ -372,8 +372,8 @@ fn declare_kind_auto_root() {
#[test]
fn pkg_is_rooted() {
#[rustfmt::skip]
let toks = vec![
PkgStart(S1, SPair("/pkg".into(), S1)),
let toks = [
PkgStart(S1, spair("/pkg", S1)),
PkgEnd(S2),
];
@ -394,10 +394,10 @@ fn pkg_is_rooted() {
#[test]
fn close_pkg_without_open() {
let toks = vec![
let toks = [
PkgEnd(S1),
// RECOVERY: Try again.
PkgStart(S2, SPair("/pkg".into(), S2)),
PkgStart(S2, spair("/pkg", S2)),
PkgEnd(S3),
];
@ -414,11 +414,11 @@ fn close_pkg_without_open() {
#[test]
fn nested_open_pkg() {
let name_a = SPair("/pkg-a".into(), S2);
let name_b = SPair("/pkg-b".into(), S4);
let name_a = spair("/pkg-a", S2);
let name_b = spair("/pkg-b", S4);
#[rustfmt::skip]
let toks = vec![
let toks = [
PkgStart(S1, name_a),
// Cannot nest package
PkgStart(S3, name_b),
@ -442,10 +442,10 @@ fn nested_open_pkg() {
#[test]
fn pkg_canonical_name() {
let name = SPair("/foo/bar".into(), S2);
let name = spair("/foo/bar", S2);
#[rustfmt::skip]
let toks = vec![
let toks = [
PkgStart(S1, name),
PkgEnd(S3),
];
@ -477,12 +477,12 @@ fn pkg_canonical_name() {
// filenames.
#[test]
fn pkg_cannot_redeclare() {
let name = SPair("/foo/bar".into(), S2);
let name2 = SPair("/foo/bar".into(), S5);
let namefix = SPair("/foo/fix".into(), S7);
let name = spair("/foo/bar", S2);
let name2 = spair("/foo/bar", S5);
let namefix = spair("/foo/fix", S7);
#[rustfmt::skip]
let toks = vec![
let toks = [
PkgStart(S1, name),
PkgEnd(S3),
@ -527,11 +527,11 @@ fn pkg_cannot_redeclare() {
#[test]
fn pkg_import_canonicalized_against_current_pkg() {
let pkg_name = SPair("/foo/bar".into(), S2);
let pkg_rel = SPair("baz/quux".into(), S3);
let pkg_name = spair("/foo/bar", S2);
let pkg_rel = spair("baz/quux", S3);
#[rustfmt::skip]
let toks = vec![
let toks = [
PkgStart(S1, pkg_name),
PkgImport(pkg_rel),
PkgEnd(S3),
@ -553,18 +553,18 @@ fn pkg_import_canonicalized_against_current_pkg() {
.resolve(&asg);
// TODO
assert_eq!(SPair("/foo/baz/quux".into(), S3), import.canonical_name());
assert_eq!(spair("/foo/baz/quux", S3), import.canonical_name());
}
// Documentation can be mixed in with objects in a literate style.
#[test]
fn pkg_doc() {
let doc_a = SPair("first".into(), S2);
let id_import = SPair("import".into(), S3);
let doc_b = SPair("first".into(), S4);
let doc_a = spair("first", S2);
let id_import = spair("import", S3);
let doc_b = spair("first", S4);
#[rustfmt::skip]
let toks = vec![
let toks = [
DocText(doc_a),
// Some object to place in-between the two
@ -597,19 +597,19 @@ fn pkg_doc() {
// index.
#[test]
fn resume_previous_parsing_context() {
let name_foo = SPair("foo".into(), S2);
let name_bar = SPair("bar".into(), S5);
let name_baz = SPair("baz".into(), S6);
let name_foo = spair("foo", S2);
let name_bar = spair("bar", S5);
let name_baz = spair("baz", S6);
let kind = IdentKind::Tpl;
let src = Source::default();
// We're going to test with opaque objects as if we are the linker.
// This is the first parse.
#[rustfmt::skip]
let toks = vec![
let toks = [
// The first package will reference an identifier from another
// package.
PkgStart(S1, SPair("/pkg-a".into(), S1)),
PkgStart(S1, spair("/pkg-a", S1)),
IdentDep(name_foo, name_bar),
PkgEnd(S3),
];
@ -620,11 +620,11 @@ fn resume_previous_parsing_context() {
// This is the token stream for the second parser,
// which will re-use the above context.
#[rustfmt::skip]
let toks = vec![
let toks = [
// This package will define that identifier,
// which should also find the identifier having been placed into
// the global environment.
PkgStart(S4, SPair("/pkg-b".into(), S4)),
PkgStart(S4, spair("/pkg-b", S4)),
IdentDecl(name_bar, kind.clone(), src.clone()),
// This is a third identifier that is unique to this package.
@ -686,16 +686,23 @@ fn resume_previous_parsing_context() {
pub fn parse_as_pkg_body<I: IntoIterator<Item = Air>>(
toks: I,
) -> Parser<Sut, impl Iterator<Item = Air> + Debug>
where
<I as IntoIterator>::IntoIter: Debug,
{
Sut::parse(as_pkg_body(toks))
}
pub fn as_pkg_body<I: IntoIterator<Item = Air>>(
toks: I,
) -> impl Iterator<Item = Air> + Debug
where
<I as IntoIterator>::IntoIter: Debug,
{
use std::iter;
Sut::parse(
iter::once(PkgStart(S1, SPair("/pkg".into(), S1)))
.chain(toks.into_iter())
.chain(iter::once(PkgEnd(S1))),
)
iter::once(PkgStart(S1, spair("/incidental-pkg", S1)))
.chain(toks.into_iter())
.chain(iter::once(PkgEnd(S1)))
}
pub(super) fn asg_from_pkg_body_toks<I: IntoIterator<Item = Air>>(

View File

@ -51,6 +51,7 @@ use crate::{
visit::{tree_reconstruction, TreeWalkRel},
ExprOp,
},
parse::util::spair,
span::UNKNOWN_SPAN,
};
use std::iter::once;
@ -120,9 +121,9 @@ macro_rules! test_scopes {
test_scopes! {
setup {
let pkg_name = SPair("/pkg".into(), S1);
let outer = SPair("outer".into(), S3);
let inner = SPair("inner".into(), S5);
let pkg_name = spair("/pkg", S1);
let outer = spair("outer", S3);
let inner = spair("inner", S5);
}
air {
@ -166,15 +167,15 @@ test_scopes! {
test_scopes! {
setup {
let pkg_name = SPair("/pkg".into(), S1);
let pkg_name = spair("/pkg", S1);
let tpl_outer = SPair("_tpl-outer_".into(), S3);
let meta_outer = SPair("@param_outer@".into(), S5);
let expr_outer = SPair("exprOuter".into(), S8);
let tpl_outer = spair("_tpl-outer_", S3);
let meta_outer = spair("@param_outer@", S5);
let expr_outer = spair("exprOuter", S8);
let tpl_inner = SPair("_tpl-inner_".into(), S11);
let meta_inner = SPair("@param_inner@".into(), S13);
let expr_inner = SPair("exprInner".into(), S16);
let tpl_inner = spair("_tpl-inner_", S11);
let meta_inner = spair("@param_inner@", S13);
let expr_inner = spair("exprInner", S16);
}
air {
@ -329,18 +330,18 @@ test_scopes! {
test_scopes! {
setup {
let pkg_name = SPair("/pkg".into(), S1);
let pkg_name = spair("/pkg", S1);
let tpl_outer = SPair("_tpl-outer_".into(), S3);
let tpl_inner = SPair("_tpl-inner_".into(), S9);
let tpl_outer = spair("_tpl-outer_", S3);
let tpl_inner = spair("_tpl-inner_", S9);
// Note how these have the _same name_.
let meta_name = "@param@".into();
let meta_same_a = SPair(meta_name, S5);
let meta_same_b = SPair(meta_name, S11);
let meta_name = "@param@";
let meta_same_a = spair(meta_name, S5);
let meta_same_b = spair(meta_name, S11);
// This one will be used for asserting.
let meta_same = SPair(meta_name, S11);
let meta_same = spair(meta_name, S11);
}
air {
@ -433,11 +434,11 @@ test_scopes! {
// From the perspective of the linker (tameld):
test_scopes! {
setup {
let pkg_a = SPair("/pkg/a".into(), S1);
let opaque_a = SPair("opaque_a".into(), S2);
let pkg_a = spair("/pkg/a", S1);
let opaque_a = spair("opaque_a", S2);
let pkg_b = SPair("/pkg/b".into(), S4);
let opaque_b = SPair("opaque_b".into(), S5);
let pkg_b = spair("/pkg/b", S4);
let opaque_b = spair("opaque_b", S5);
}
air {

View File

@ -26,11 +26,7 @@ use super::{
ir::AirBindableTpl,
AirAggregate, AirAggregateCtx,
};
use crate::{
fmt::{DisplayWrapper, TtQuote},
parse::prelude::*,
span::Span,
};
use crate::{fmt::TtQuote, parse::prelude::*, span::Span};
/// Template parser and token aggregator.
///
@ -179,31 +175,47 @@ impl ParseState for AirTplAggregate {
}
(Toplevel(tpl), AirBind(BindIdent(id))) => ctx
.defines(id)
.defines_concrete(id)
.and_then(|oi_ident| {
oi_ident.bind_definition(ctx.asg_mut(), id, tpl.oi())
})
.map(|_| ())
.transition(Toplevel(tpl.identify(id))),
(Toplevel(tpl), AirBind(BindIdentAbstract(meta_name))) => {
diagnostic_todo!(
vec![
tpl.oi().note("for this template"),
meta_name.note(
"attempting to bind an abstract identifier with \
this metavariable"
),
],
"attempt to bind abstract identifier to template",
)
}
(Toplevel(tpl), AirBind(RefIdent(name))) => {
let tpl_oi = tpl.oi();
let ref_oi = ctx.lookup_lexical_or_missing(name);
tpl_oi.apply_named_tpl(ctx.asg_mut(), ref_oi, name.span());
Transition(Toplevel(tpl)).incomplete()
tpl_oi
.apply_named_tpl(ctx.asg_mut(), ref_oi, name.span())
.map(|_| ())
.transition(Toplevel(tpl))
}
(Toplevel(tpl), AirDoc(DocIndepClause(clause))) => {
tpl.oi().desc_short(ctx.asg_mut(), clause);
Transition(Toplevel(tpl)).incomplete()
}
(Toplevel(tpl), AirDoc(DocIndepClause(clause))) => tpl
.oi()
.add_desc_short(ctx.asg_mut(), clause)
.map(|_| ())
.transition(Toplevel(tpl)),
(Toplevel(tpl), AirDoc(DocText(text))) => {
tpl.oi().append_doc_text(ctx.asg_mut(), text);
Transition(Toplevel(tpl)).incomplete()
}
(Toplevel(tpl), AirDoc(DocText(text))) => tpl
.oi()
.append_doc_text(ctx.asg_mut(), text)
.map(|_| ())
.transition(Toplevel(tpl)),
(Toplevel(tpl), AirTpl(TplEnd(span))) => {
tpl.close(ctx.asg_mut(), span).transition(Done)
@ -215,12 +227,12 @@ impl ParseState for AirTplAggregate {
// we are effectively discarding the ref and translating
// into a `TplEnd`.
match ctx.expansion_oi() {
Some(oi_target) => {
tpl.oi().expand_into(ctx.asg_mut(), oi_target);
Transition(Toplevel(tpl.anonymous_reachable()))
.incomplete()
}
Some(oi_target) => tpl
.oi()
.close(ctx.asg_mut(), span)
.expand_into(ctx.asg_mut(), oi_target)
.map(|_| ())
.transition(Toplevel(tpl.anonymous_reachable())),
None => Transition(Toplevel(tpl))
.err(AsgError::InvalidExpansionContext(span)),
}

File diff suppressed because it is too large Load Diff

View File

@ -117,13 +117,20 @@ pub enum AsgError {
/// delimiter.
UnbalancedTpl(Span),
/// Attempted to bind an identifier to an object while not in a context
/// that can receive an identifier binding.
/// Attempted to bind a concrete identifier to an object while not in a
/// context that can receive an identifier binding.
///
/// Note that the user may encounter an error from a higher-level IR
/// instead of this one.
InvalidBindContext(SPair),
/// Attempted to bind an abstract identifier in a context where
/// expansion will not take place.
///
/// This is intended to preempt future errors when we know that the
/// context does not make sense for an abstract binding.
InvalidAbstractBindContext(SPair, Option<Span>),
/// Attempted to reference an identifier while not in a context that can
/// receive an identifier reference.
///
@ -160,8 +167,26 @@ pub enum AsgError {
/// The provided [`Span`] indicates the location of the start of the
/// metavariable definition.
UnexpectedMeta(Span),
/// A template already has a shape of
/// [`TplShape::Expr`](super::graph::object::tpl::TplShape::Expr),
/// but another expression was found that would violate this shape
/// constraint.
///
/// The template name is provided if it is known at the time of the
/// error.
TplShapeExprMulti(Option<SPair>, ErrorOccurrenceSpan, FirstOccurrenceSpan),
}
/// A [`Span`] representing the subject of this error.
type ErrorOccurrenceSpan = Span;
/// A [`Span`] representing the first occurrence of an object related to the
/// subject of this error.
///
/// This should be paired with [`ErrorOccurrenceSpan`].
type FirstOccurrenceSpan = Span;
impl Display for AsgError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
use AsgError::*;
@ -202,8 +227,20 @@ impl Display for AsgError {
),
UnbalancedExpr(_) => write!(f, "unbalanced expression"),
UnbalancedTpl(_) => write!(f, "unbalanced template definition"),
InvalidBindContext(_) => {
write!(f, "invalid identifier binding context")
InvalidBindContext(name) => {
write!(
f,
"invalid identifier binding context for {}",
TtQuote::wrap(name),
)
}
InvalidAbstractBindContext(name, _) => {
write!(
f,
"invalid abstract identifier binding context for \
metavariable {}",
TtQuote::wrap(name),
)
}
InvalidRefContext(ident) => {
write!(
@ -231,6 +268,18 @@ impl Display for AsgError {
UnexpectedMeta(_) => {
write!(f, "unexpected metavariable definition")
}
TplShapeExprMulti(Some(name), _, _) => write!(
f,
"definition of template {} would produce multiple inline \
expressions when expanded",
TtQuote::wrap(name),
),
TplShapeExprMulti(None, _, _) => write!(
f,
"template definition would produce multiple inline \
expressions when expanded"
),
}
}
}
@ -351,6 +400,25 @@ impl Diagnostic for AsgError {
InvalidBindContext(name) => vec![name
.error("an identifier binding is not valid in this context")],
InvalidAbstractBindContext(name, parent_span) => parent_span
.map(|span| {
span.note(
"this definition context does not support metavariable \
expansion"
)
})
.into_iter()
.chain(vec![
name.error("this metavariable will never be expanded"),
name.help(format!(
"this identifier must have its name derived from \
the metavariable {},
but that metavariable will never be expanded here",
TtQuote::wrap(name),
)),
])
.collect(),
InvalidRefContext(ident) => vec![ident.error(
"cannot reference the value of an expression from outside \
of an expression context",
@ -407,6 +475,42 @@ impl Diagnostic for AsgError {
span.help("metavariables are expected to occur in a template context"),
]
}
// TODO: Perhaps we should have a documentation page that can be
// referenced with examples and further rationale,
// like Rust does.
TplShapeExprMulti(oname, err, first) => oname
.map(|name| name.note("for this template"))
.into_iter()
.chain(vec![
first.note(
"this is the first expression that would be inlined \
at an expansion site",
),
err.error(
"template cannot inline more than one expression \
into an expansion site",
),
err.help(
"this restriction is intended to ensure that \
templates expand in ways that are consistent \
given any expansion context; consider either:",
),
err.help(
" - wrapping both expressions in a parent \
expression; or",
),
err.help(
" - giving at least one expression an id to prevent \
inlining",
),
// in case they were wondering
err.help(
"this restriction did not exist in versions of \
TAME prior to TAMER",
),
])
.collect(),
}
}
}

View File

@ -29,7 +29,7 @@ use self::object::{
use super::{AsgError, Object, ObjectIndex, ObjectKind};
use crate::{
diagnose::{panic::DiagnosticPanic, Annotate, AnnotatedSpan},
f::Functor,
f::Map,
global,
span::Span,
};
@ -40,6 +40,9 @@ use petgraph::{
};
use std::{fmt::Debug, result::Result};
#[cfg(doc)]
use object::{ObjectIndexTo, Tpl};
pub mod object;
pub mod visit;
pub mod xmli;
@ -197,17 +200,24 @@ impl Asg {
///
/// For more information on how the ASG's ontology is enforced statically,
/// see [`ObjectRelTo`](object::ObjectRelTo).
fn add_edge<OB: ObjectKind + ObjectRelatable>(
///
/// Callers external to this module should use [`ObjectIndex`] APIs to
/// manipulate the graph;
/// this allows those objects to uphold their own invariants
/// relative to the state of the graph.
fn add_edge<OA: ObjectIndexRelTo<OB>, OB: ObjectKind + ObjectRelatable>(
&mut self,
from_oi: impl ObjectIndexRelTo<OB>,
from_oi: OA,
to_oi: ObjectIndex<OB>,
ctx_span: Option<Span>,
) {
self.graph.add_edge(
from_oi.widen().into(),
to_oi.into(),
(from_oi.src_rel_ty(), OB::rel_ty(), ctx_span),
);
) -> Result<(), AsgError> {
from_oi.pre_add_edge(self, to_oi, ctx_span).map(|()| {
self.graph.add_edge(
from_oi.widen().into(),
to_oi.into(),
(from_oi.src_rel_ty(), OB::rel_ty(), ctx_span),
);
})
}
/// Retrieve an object from the graph by [`ObjectIndex`].
@ -386,5 +396,153 @@ fn diagnostic_node_missing_desc<O: ObjectKind>(
]
}
/// Mutation of an [`Object`]'s relationships (edges) on the [`Asg`].
///
/// This trait is intended to delegate certain responsibilities to
/// [`ObjectKind`]s so that they may enforce their own invariants with
/// respect to their relationships to other objects on the graph.
///
/// TODO: It'd be nice if only [`Asg`] were able to invoke methods on this
/// trait,
/// but the current module structure together with Rust's visibility
/// with sibling modules doesn't seem to make that possible.
///
/// How Does This Work With Trait Specialization?
/// =============================================
/// [`Asg::add_edge`] is provided a [`ObjectIndexRelTo`],
/// which needs narrowing to an appropriate source [`ObjectKind`] so that
/// we can invoke [`<O as AsgRelMut>::pre_add_edge`](AsgRelMut::pre_add_edge).
///
/// At the time of writing,
/// there are two implementors of [`ObjectIndexRelTo`]:
///
/// - [`ObjectIndex<O>`],
/// for which we will know `O: ObjectKind`.
/// - [`ObjectIndexTo<OB>`],
/// for which we only know the _target_ `OB: ObjectKind`.
///
/// The entire purpose of [`ObjectIndexTo`] is to allow for a dynamic
/// source [`ObjectKind`];
/// we do not know what it is statically.
/// So [`ObjectIndexTo::pre_add_edge`] is a method that will dynamically
/// branch to an appropriate static path to invoke the correct
/// [`AsgRelMut::pre_add_edge`].
///
/// And there's the problem.
///
/// We match on each [`ObjectRelTy`] based on
/// [`ObjectIndexTo::src_rel_ty`],
/// and invoke the appropriate [`AsgRelMut::pre_add_edge`].
/// But the trait bound on `OB` for the `ObjectIndexRelTo` `impl` is
/// [`ObjectRelatable`].
/// So it resolves as `AsgRelMut<OB: ObjectRelatable>`.
///
/// But we don't have that implementation.
/// We have implementations for _individual target [`ObjectRelatable`]s,
/// e.g. `impl AsgRelMut<Expr> for Tpl`.
/// So Rust rightfully complains that `AsgRelMut<OB: ObjectRelatable>`
/// is not implemented for [`Tpl`].
/// (Go ahead and remove the generic `impl` block containing `default fn`
/// and see what happens.)
///
/// Of course,
/// _we_ know that there's a trait implemented for every possible
/// [`ObjectRelFrom<Tpl>`],
/// because `object_rel!` does that for us based on the same
/// definition that generates those other types.
/// But Rust does not perform that type of analysis---
/// it does not know that we've accounted for every type.
/// So the `default fn`` uses the unstable `min_specialization` feature to
/// satisfy those more generic trait bounds,
/// making the compiler happy.
///
/// But if Rust is seeing `OB: ObjectRelatable`,
/// why is it not monomorphizing to _this_ one rather than the more
/// specialized implementation?
///
/// That type error described above is contemplating bounds for _any
/// potential caller_.
/// But when we're about to add an edge,
/// we're invoking with a specific type of `OB`.
/// Monomorphization takes place at that point,
/// with the expected type,
/// and uses the appropriate specialization.
///
/// Because of other trait bounds leading up to this point,
/// including those on [`Asg::add_edge`] and [`ObjectIndexRelTo`],
/// this cannot be invoked for any `to_oi` that is not a valid target
/// for `Self`.
/// But we cannot be too strict on that bound _here_,
/// because otherwise it's not general enough for
/// [`ObjectIndexTo::pre_add_edge`].
/// We could do more runtime verification and further refine types,
/// but that is a lot of work for no additional practical benefit,
/// at least at this time.
pub trait AsgRelMut<OB: ObjectRelatable>: ObjectRelatable {
/// Allow an object to handle or reject the creation of an edge from it
/// to another object.
///
/// Objects own both their node on the graph and the edges _from_ that
/// node to another object.
/// Phrased another way:
/// they own their data and their relationships.
///
/// This gives an opportunity for the [`ObjectKind`] associated with the
/// source object to evaluate the proposed relationship.
/// This guarantee allows objects to cache information about these
/// relationships and enforce any associated invariants without
/// worrying about how the object may change out from underneath
/// them.
/// In some cases,
/// this is the only way that an object will know whether an edge has
/// been added,
/// since the [`ObjectIndex`] APIs may not be utilized
/// (e.g. in the case of [`ObjectIndexRelTo`].
///
/// This is invoked by [`Asg::add_edge`].
/// The provided `commit` callback will complete the addition of the
/// edge if provided [`Ok`],
/// and the commit cannot fail.
/// If [`Err`] is provided to `commit`,
/// then [`Asg::add_edge`] will fail with that error.
///
/// Unlike the type of [`Asg::add_edge`],
/// the source [`ObjectIndex`] has been narrowed to the appropriate
/// type for you.
fn pre_add_edge(
asg: &mut Asg,
rel: ProposedRel<Self, OB>,
) -> Result<(), AsgError>;
}
impl<OA: ObjectRelatable, OB: ObjectRelatable> AsgRelMut<OB> for OA {
/// Default edge creation method for all [`ObjectKind`]s.
///
/// This takes the place of a default implementation on the trait itself
/// above.
/// It will be invoked any time there is not a more specialized
/// implementation.
/// Note that `object_rel!` doesn't provide method
/// definitions unless explicitly specified by the user,
/// so this is effective the method called for all edges _unless_
/// overridden for a particular edge for a particular object
/// (see [`object::tpl`] as an example).
default fn pre_add_edge(
_asg: &mut Asg,
_rel: ProposedRel<Self, OB>,
) -> Result<(), AsgError> {
let _ = _rel.ctx_span; // TODO: remove when used (dead_code)
Ok(())
}
}
/// The relationship proposed by [`Asg::add_edge`],
/// requiring approval from [`AsgRelMut::pre_add_edge`].
pub struct ProposedRel<OA: ObjectKind, OB: ObjectKind> {
from_oi: ObjectIndex<OA>,
to_oi: ObjectIndex<OB>,
ctx_span: Option<Span>,
}
#[cfg(test)]
mod test;

View File

@ -112,12 +112,18 @@
//! Since [`ObjectRel`] narrows into an [`ObjectIndex`],
//! the system will produce runtime panics if there is ever any attempt to
//! follow an edge to an unexpected [`ObjectKind`].
//!
//! In addition to these static guarantees,
//! [`AsgRelMut`](super::AsgRelMut) is utilized by [`Asg`] to consult an
//! object before an edge is added _from_ it,
//! allowing objects to assert ownership over their edges and cache
//! information about them as the graph is being built.
use super::Asg;
use super::{Asg, AsgError};
use crate::{
diagnose::{panic::DiagnosticPanic, Annotate, AnnotatedSpan},
diagnostic_panic,
f::Functor,
f::{Map, TryMap},
parse::util::SPair,
span::{Span, UNKNOWN_SPAN},
};
@ -156,7 +162,7 @@ pub use tpl::Tpl;
/// Often-needed exports for [`ObjectKind`]s.
pub mod prelude {
pub use super::{
super::{super::error::AsgError, Asg},
super::{super::error::AsgError, Asg, AsgRelMut},
Object, ObjectIndex, ObjectIndexRelTo, ObjectKind, ObjectRel,
ObjectRelFrom, ObjectRelTo, ObjectRelTy, ObjectRelatable,
ObjectTreeRelTo,
@ -243,6 +249,20 @@ macro_rules! object_gen {
}
}
/// Narrowed [`ObjectIndex`] types for each [`ObjectKind`].
///
/// This allows for converting a dynamic
/// [`ObjectIndex<Object>`](ObjectIndex) into a statically known
/// [`ObjectKind`],
/// while also providing the ability to exhaustively match
/// against all such possibilities.
#[derive(Debug, PartialEq, Eq)]
pub enum ObjectIndexRefined {
$(
$kind(ObjectIndex<$kind>),
)+
}
/// The collection of potential objects of [`Object`].
pub trait ObjectInner {
$(type $kind;)+
@ -363,7 +383,7 @@ object_gen! {
/// A template definition.
Tpl,
/// Metasyntactic variable (metavariable).
/// Metalinguistic variable (metavariable).
Meta,
/// Documentation.
@ -573,23 +593,52 @@ impl<O: ObjectKind> ObjectIndex<O> {
}
}
/// Add an edge from `self` to `to_oi` on the provided [`Asg`].
///
/// Since the only invariant asserted by [`ObjectIndexRelTo`] is that
/// it may be related to `OB`,
/// this method will only permit edges to `OB`;
/// nothing else about the inner object is statically known.
///
/// See also [`Self::add_edge_from`].
///
/// _This method must remain private_,
/// forcing callers to go through APIs for specific operations that
/// allow objects to enforce their own invariants.
/// This is also the reason why this method is defined here rather than
/// on [`ObjectIndexRelTo`].
fn add_edge_to<OB: ObjectRelatable>(
self,
asg: &mut Asg,
to_oi: ObjectIndex<OB>,
ctx_span: Option<Span>,
) -> Result<Self, AsgError>
where
Self: ObjectIndexRelTo<OB>,
{
asg.add_edge(self, to_oi, ctx_span).map(|()| self)
}
/// Add an edge from `from_oi` to `self` on the provided [`Asg`].
///
/// An edge can only be added if ontologically valid;
/// see [`ObjectRelTo`] for more information.
///
/// See also [`Self::add_edge_to`].
pub fn add_edge_from<OF: ObjectIndexRelTo<O>>(
///
/// _This method must remain private_,
/// forcing callers to go through APIs for specific operations that
/// allow objects to enforce their own invariants.
fn add_edge_from<OA: ObjectIndexRelTo<O>>(
self,
asg: &mut Asg,
from_oi: OF,
from_oi: OA,
ctx_span: Option<Span>,
) -> Self
) -> Result<Self, AsgError>
where
O: ObjectRelatable,
{
asg.add_edge(from_oi, self, ctx_span);
self
asg.add_edge(from_oi, self, ctx_span).map(|()| self)
}
/// Create an iterator over the [`ObjectIndex`]es of the outgoing edges
@ -686,6 +735,26 @@ impl<O: ObjectKind> ObjectIndex<O> {
asg.try_map_obj(self, f)
}
/// Resolve the identifier and map over an inner `T` of the resulting
/// [`Object`] narrowed to [`ObjectKind`] `O`,
/// replacing the object on the given [`Asg`].
///
/// This uses [`Self::try_map_obj`] to retrieve the object from
/// the [`Asg`].
///
/// If this operation is [`Infallible`],
/// see [`Self::map_obj_inner`].
pub fn try_map_obj_inner<T, E>(
self,
asg: &mut Asg,
f: impl FnOnce(T) -> <O as TryMap<T>>::FnResult<E>,
) -> Result<Self, E>
where
O: TryMap<T, Result<E> = Result<O, (O, E)>>,
{
self.try_map_obj(asg, O::try_fmap(f))
}
/// Resolve the identifier and infallibly map over the resulting
/// [`Object`] narrowed to [`ObjectKind`] `O`,
/// replacing the object on the given [`Asg`].
@ -702,6 +771,22 @@ impl<O: ObjectKind> ObjectIndex<O> {
}
}
/// Resolve the identifier and map over an inner `T` of the resulting
/// [`Object`] narrowed to [`ObjectKind`] `O`,
/// replacing the object on the given [`Asg`].
///
/// This uses [`Self::map_obj`] to retrieve the object from
/// the [`Asg`].
///
/// If this operation is _not_ [`Infallible`],
/// see [`Self::try_map_obj_inner`].
pub fn map_obj_inner<T>(self, asg: &mut Asg, f: impl FnOnce(T) -> T) -> Self
where
O: Map<T, Target = O>,
{
self.map_obj(asg, O::fmap(f))
}
/// Lift [`Self`] into [`Option`] and [`filter`](Option::filter) based
/// on whether the [`ObjectRelatable::rel_ty`] of [`Self`]'s `O`
/// matches that of `OB`.
@ -736,12 +821,13 @@ impl<O: ObjectKind> ObjectIndex<O> {
/// objects.
/// Forcing objects to be reachable can prevent them from being
/// optimized away if they are not used.
pub fn root(self, asg: &mut Asg) -> Self
pub fn root(self, asg: &mut Asg) -> Result<Self, AsgError>
where
Root: ObjectRelTo<O>,
{
asg.root(self.span()).add_edge_to(asg, self, None);
self
asg.root(self.span())
.add_edge_to(asg, self, None)
.map(|_| self)
}
/// Whether this object has been rooted in the ASG's [`Root`] object.
@ -810,13 +896,23 @@ impl<O: ObjectKind> ObjectIndex<O> {
/// on the graph,
/// like common subexpression elimination,
/// in which case it's best not to rely on following edges in reverse.
pub fn ident<'a>(&self, asg: &'a Asg) -> Option<&'a Ident>
pub fn ident<'a>(&self, asg: &'a Asg) -> Option<ObjectIndex<Ident>>
where
O: ObjectRelFrom<Ident>,
{
self.incoming_edges_filtered(asg)
.map(ObjectIndex::cresolve(asg))
.next()
self.incoming_edges_filtered(asg).next()
}
/// Indicate that the given identifier `oi` is defined by this object.
pub fn defines(
self,
asg: &mut Asg,
oi: ObjectIndex<Ident>,
) -> Result<Self, AsgError>
where
Self: ObjectIndexRelTo<Ident>,
{
oi.defined_by(asg, self).map(|_| self)
}
/// Describe this expression using a short independent clause.
@ -825,13 +921,31 @@ impl<O: ObjectKind> ObjectIndex<O> {
/// simple sentence or as part of a compound sentence.
/// There should only be one such clause for any given object,
/// but that is not enforced here.
pub fn desc_short(&self, asg: &mut Asg, clause: SPair) -> Self
pub fn add_desc_short(
&self,
asg: &mut Asg,
clause: SPair,
) -> Result<Self, AsgError>
where
O: ObjectRelTo<Doc>,
{
let oi_doc = asg.create(Doc::new_indep_clause(clause));
self.add_edge_to(asg, oi_doc, None)
}
/// Retrieve a description of this expression using a short independent
/// clause,
/// if one has been set.
///
/// See [`Self::add_desc_short`] to add such a description.
pub fn desc_short(&self, asg: &Asg) -> Option<SPair>
where
O: ObjectRelTo<Doc>,
{
self.edges_filtered::<Doc>(asg)
.map(ObjectIndex::cresolve(asg))
.find_map(Doc::indep_clause)
}
}
impl ObjectIndex<Object> {
@ -846,7 +960,7 @@ impl ObjectIndex<Object> {
}
}
impl<O: ObjectKind> Functor<Span> for ObjectIndex<O> {
impl<O: ObjectKind> Map<Span> for ObjectIndex<O> {
fn map(self, f: impl FnOnce(Span) -> Span) -> Self {
match self {
Self(index, span, ph) => Self(index, f(span), ph),

View File

@ -19,8 +19,8 @@
//! Expressions on the ASG.
use super::{prelude::*, Doc, Ident, Tpl};
use crate::{f::Functor, num::Dim, span::Span};
use super::{prelude::*, Doc, Ident, ObjectIndexTo, Tpl};
use crate::{f::Map, num::Dim, span::Span};
use std::fmt::Display;
#[cfg(doc)]
@ -52,7 +52,7 @@ impl Expr {
}
}
impl Functor<Span> for Expr {
impl Map<Span> for Expr {
fn map(self, f: impl FnOnce(Span) -> Span) -> Self {
match self {
Self(op, dim, span) => Self(op, dim, f(span)),
@ -233,14 +233,38 @@ impl ObjectIndex<Expr> {
self,
asg: &mut Asg,
expr: Expr,
) -> ObjectIndex<Expr> {
) -> Result<ObjectIndex<Expr>, AsgError> {
let oi_subexpr = asg.create(expr);
oi_subexpr.add_edge_from(asg, self, None)
}
/// Reference the value of the expression identified by `oi_ident` as if
/// it were a subexpression.
pub fn ref_expr(self, asg: &mut Asg, oi_ident: ObjectIndex<Ident>) -> Self {
pub fn ref_expr(
self,
asg: &mut Asg,
oi_ident: ObjectIndex<Ident>,
) -> Result<Self, AsgError> {
self.add_edge_to(asg, oi_ident, Some(oi_ident.span()))
}
/// The expression is held by the container `oi_container`.
///
/// This is intended to convey that an expression would otherwise be
/// dangling (unreachable) were it not for the properties
/// of `oi_container`.
/// If this is not true,
/// consider using:
///
/// 1. [`Self::create_subexpr`] to create and assign ownership of
/// expressions contained within other expressions; or
/// 2. [`ObjectIndex<Ident>::bind_definition`] if this expression is to
/// be assigned to an identifier.
pub fn held_by(
&self,
asg: &mut Asg,
oi_container: ObjectIndexTo<Expr>,
) -> Result<Self, AsgError> {
self.add_edge_from(asg, oi_container, None)
}
}

View File

@ -21,9 +21,9 @@
use super::{prelude::*, Expr, Meta, Pkg, Tpl};
use crate::{
diagnose::{Annotate, Diagnostic},
diagnose::{panic::DiagnosticPanic, Annotate, Diagnostic},
diagnostic_todo,
f::Functor,
f::Map,
fmt::{DisplayWrapper, TtQuote},
num::{Dim, Dtype},
parse::{util::SPair, Token},
@ -143,6 +143,20 @@ pub enum Ident {
/// itself;
/// this is safe since identifiers in TAME are immutable.
Transparent(SPair),
/// The name of the identifier is not yet known and will be determined
/// by the lexical value of a metavariable.
///
/// This is intended for use by identifiers that will be generated as a
/// result of template expansion---
/// it represents the abstract _idea_ of an identifier,
/// to be made concrete at a later time,
/// and is not valid outside of a metalinguistic context.
///
/// The associated span represents the location that the identifier
/// was defined,
/// e.g. within a template body.
Abstract(Span),
}
impl Display for Ident {
@ -165,19 +179,28 @@ impl Display for Ident {
Transparent(id) => {
write!(f, "transparent identifier {}", TtQuote::wrap(id))
}
Abstract(_) => {
write!(f, "pending identifier (to be named during expansion)")
}
}
}
}
impl Ident {
/// Identifier name.
pub fn name(&self) -> SPair {
/// Concrete identifier name.
///
/// Note: This [`Option`] is in preparation for identifiers that may not
/// yet have names,
/// awaiting expansion of a metavariable.
pub fn name(&self) -> Option<SPair> {
match self {
Missing(name)
| Opaque(name, ..)
| Extern(name, ..)
| IdentFragment(name, ..)
| Transparent(name) => *name,
| Transparent(name) => Some(*name),
Abstract(_) => None,
}
}
@ -188,6 +211,8 @@ impl Ident {
| Extern(name, ..)
| IdentFragment(name, ..)
| Transparent(name) => name.span(),
Abstract(span) => *span,
}
}
@ -198,7 +223,7 @@ impl Ident {
/// [`None`] is returned.
pub fn kind(&self) -> Option<&IdentKind> {
match self {
Missing(_) | Transparent(_) => None,
Missing(_) | Transparent(_) | Abstract(_) => None,
Opaque(_, kind, _)
| Extern(_, kind, _)
@ -213,7 +238,7 @@ impl Ident {
/// [`None`] is returned.
pub fn src(&self) -> Option<&Source> {
match self {
Missing(_) | Extern(_, _, _) | Transparent(_) => None,
Missing(_) | Extern(_, _, _) | Transparent(_) | Abstract(_) => None,
Opaque(_, _, src) | IdentFragment(_, _, src, _) => Some(src),
}
@ -225,9 +250,11 @@ impl Ident {
/// [`None`] is returned.
pub fn fragment(&self) -> Option<FragmentText> {
match self {
Missing(_) | Opaque(_, _, _) | Extern(_, _, _) | Transparent(_) => {
None
}
Missing(_)
| Opaque(_, _, _)
| Extern(_, _, _)
| Transparent(_)
| Abstract(_) => None,
IdentFragment(_, _, _, text) => Some(*text),
}
@ -244,6 +271,17 @@ impl Ident {
Missing(ident)
}
/// Create a new abstract identifier at the given location.
///
/// The provided [`Span`] is the only way to uniquely identify this
/// identifier since it does not yet have a name.
/// Note that this just _represents_ an abstract identifier;
/// it is given meaning only when given the proper relationships on
/// the ASG.
pub fn new_abstract<S: Into<Span>>(at: S) -> Self {
Abstract(at.into())
}
/// Attempt to redeclare an identifier with additional information.
///
/// If an existing identifier is an [`Ident::Extern`],
@ -357,12 +395,24 @@ impl Ident {
Missing(name) => Ok(Opaque(name.overwrite(span), kind, src)),
// TODO: Remove guards and catch-all for exhaustiveness check.
_ => {
let err = TransitionError::Redeclare(self.name(), span);
// TODO: Remove guards for better exhaustiveness check
Opaque(name, _, _)
| IdentFragment(name, _, _, _)
| Transparent(name) => {
let err = TransitionError::Redeclare(name, span);
Err((self, err))
}
// This really should never happen at the time of writing,
// since to resolve an identifier it first needs to be located
// on the graph,
// and abstract identifiers do not have an indexed name.
// Does the system now discover identifiers through other means,
// e.g. by trying to pre-draw edges within template bodies?
Abstract(abstract_span) => Err((
self,
TransitionError::ResolveAbstract(abstract_span, span),
)),
}
}
@ -381,7 +431,7 @@ impl Ident {
/// At present,
/// both [`Ident::Missing`] and [`Ident::Extern`] are
/// considered to be unresolved.
pub fn resolved(&self) -> Result<&Ident, UnresolvedError> {
pub fn resolved(&self) -> Result<(&Ident, SPair), UnresolvedError> {
match self {
Missing(name) => Err(UnresolvedError::Missing(*name)),
@ -389,7 +439,11 @@ impl Ident {
Err(UnresolvedError::Extern(*name, kind.clone()))
}
Opaque(..) | IdentFragment(..) | Transparent(..) => Ok(self),
Abstract(span) => Err(UnresolvedError::Abstract(*span)),
Opaque(name, ..)
| IdentFragment(name, ..)
| Transparent(name, ..) => Ok((self, *name)),
}
}
@ -414,22 +468,33 @@ impl Ident {
kind: IdentKind,
src: Source,
) -> TransitionResult<Ident> {
match self.kind() {
None => Ok(Extern(self.name().overwrite(span), kind, src)),
Some(cur_kind) => {
match self {
Missing(name) | Transparent(name) => {
Ok(Extern(name.overwrite(span), kind, src))
}
Opaque(name, ref cur_kind, _)
| Extern(name, ref cur_kind, _)
| IdentFragment(name, ref cur_kind, _, _) => {
if cur_kind != &kind {
let err = TransitionError::ExternResolution(
self.name(),
name,
cur_kind.clone(),
(kind, span),
);
return Err((self, err));
Err((self, err))
} else {
// Resolved successfully, so keep what we already have.
Ok(self)
}
// Resolved successfully, so keep what we already have.
Ok(self)
}
// See notes on `resolve()` for this arm.
Abstract(abstract_span) => Err((
self,
TransitionError::ResolveAbstract(abstract_span, span),
)),
}
}
@ -441,6 +506,11 @@ impl Ident {
/// Note, however, that an identifier's fragment may be cleared under
/// certain circumstances (such as symbol overrides),
/// making way for a new fragment to be set.
///
/// Fragments cannot be attached to abstract identifiers,
/// nor does it make sense to,
/// since fragment code generation only takes place on expanded
/// objects.
pub fn set_fragment(self, text: FragmentText) -> TransitionResult<Ident> {
match self {
Opaque(sym, kind, src) => Ok(IdentFragment(sym, kind, src, text)),
@ -457,6 +527,9 @@ impl Ident {
IdentFragment(_, _, ref src, ..) if src.override_ => Ok(self),
// These represent the prologue and epilogue of maps.
//
// TODO: Is this arm still needed after having eliminated their
// fragments from xmlo files?
IdentFragment(
_,
IdentKind::MapHead
@ -466,10 +539,16 @@ impl Ident {
..,
) => Ok(self),
_ => {
let name = self.name();
Missing(name)
| Extern(name, _, _)
| IdentFragment(name, _, _, _)
| Transparent(name) => {
Err((self, TransitionError::BadFragmentDest(name)))
}
Abstract(span) => {
Err((self, TransitionError::AbstractFragmentDest(span)))
}
}
}
}
@ -503,6 +582,16 @@ pub enum TransitionError {
///
/// See [`Ident::set_fragment`].
BadFragmentDest(SPair),
/// Attempted to resolve an abstract identifier.
///
/// An abstract identifier must be made to be concrete before any
/// resolution can occur.
ResolveAbstract(Span, Span),
/// Like [`Self::BadFragmentDest`] but for abstract identifiers without
/// a name.
AbstractFragmentDest(Span),
}
impl std::fmt::Display for TransitionError {
@ -540,7 +629,15 @@ impl std::fmt::Display for TransitionError {
BadFragmentDest(name) => {
write!(fmt, "bad fragment destination: {}", TtQuote::wrap(name))
},
ResolveAbstract(_, _) => {
write!(fmt, "cannot resolve abstract identifier")
}
AbstractFragmentDest(_) => {
write!(fmt, "cannot attach fragment to abstract identifier")
},
}
}
}
@ -610,6 +707,27 @@ impl Diagnostic for TransitionError {
),
name.help(" object file; this error should never occur."),
],
ResolveAbstract(span, resolve_span) => vec![
span.note("for this abstract identifier"),
resolve_span.internal_error(
"attempted to resolve abstract identifier here",
),
resolve_span.help(
"this is a suspicious error that may represent \
a compiler bug",
),
],
AbstractFragmentDest(span) => vec![
span.internal_error(
"this abstract identifier cannot be assigned a text fragment",
),
span.help(
"the term 'text fragment' refers to compiled code from an \
object file; this error should never occur."
),
],
}
}
}
@ -629,6 +747,13 @@ pub enum UnresolvedError {
/// Expected identifier has not yet been resolved with a concrete
/// definition.
Extern(SPair, IdentKind),
/// The identifier at the given location is pending expansion and is not
/// yet a concrete identifier.
///
/// These identifiers represent a template for the creation of a future
/// identifier during template expansion.
Abstract(Span),
}
impl std::fmt::Display for UnresolvedError {
@ -646,6 +771,8 @@ impl std::fmt::Display for UnresolvedError {
TtQuote::wrap(name),
TtQuote::wrap(kind),
),
Abstract(_) => write!(fmt, "abstract (unexpanded) identifier"),
}
}
}
@ -684,6 +811,16 @@ impl Diagnostic for UnresolvedError {
" later provide a concrete definition for it."
)
],
// This should not occur under normal circumstances;
// the user is likely to hit a more helpful and
// context-specific error before this.
Abstract(span) => vec![
span.error("this identifier has not been expanded"),
span.help(
"are you using a metavariable outside of a template body?",
),
],
}
}
}
@ -980,7 +1117,10 @@ object_rel! {
/// This is a legacy feature expected to be removed in the future;
/// see [`ObjectRel::can_recurse`] for more information.
Ident -> {
tree Ident,
// Could represent an opaque dependency or an abstract identifier's
// metavariable reference.
dyn Ident,
tree Expr,
tree Tpl,
tree Meta,
@ -1098,6 +1238,23 @@ impl ObjectIndex<Ident> {
)
}
// An abstract identifier will become `Transparent` during
// expansion.
// This does not catch multiple definitions,
// but this is hopefully not a problem in practice since there
// is no lookup mechanism in source languages for abstract
// identifiers since this has no name yet and cannot be
// indexed in the usual way.
// Even so,
// multiple definitions can be caught at expansion-time if
// they somehow are able to slip through
// (which would be a compiler bug);
// it is not worth complicating `Ident`'s API or variants
// even further,
// and not worth the cost of a graph lookup here when
// we'll have to do it later anyway.
Abstract(span) => Ok(Abstract(span)),
// We are okay to proceed to add an edge to the `definition`.
// Discard the original span
// (which is the location of the first reference _to_ this
@ -1105,7 +1262,7 @@ impl ObjectIndex<Ident> {
// and use the newly provided `id` and its span.
Missing(_) => Ok(Transparent(id)),
})
.map(|ident_oi| ident_oi.add_edge_to(asg, definition, None))
.and_then(|ident_oi| ident_oi.add_edge_to(asg, definition, None))
}
/// Set the fragment associated with a concrete identifier.
@ -1159,14 +1316,94 @@ impl ObjectIndex<Ident> {
self.incoming_edges_filtered(asg).next()
}
/// Root this identifier into the provided object,
/// as if making the statement "`oi_root` defines `self`".
///
/// This causes `oi_root` to act as an owner of this identifier.
/// An identifier really only ought to have one owner,
/// but this is not enforced here.
pub fn defined_by(
&self,
asg: &mut Asg,
oi_root: impl ObjectIndexRelTo<Ident>,
) -> Result<Self, AsgError> {
self.add_edge_from(asg, oi_root, None)
}
/// Declare that `oi_dep` is an opaque dependency of `self`.
pub fn add_opaque_dep(
&self,
asg: &mut Asg,
oi_dep: ObjectIndex<Ident>,
) -> Self {
) -> Result<Self, AsgError> {
self.add_edge_to(asg, oi_dep, None)
}
/// Retrieve either the concrete name of the identifier or the name of
/// the metavariable that will be used to produce it.
pub fn name_or_meta(&self, asg: &Asg) -> SPair {
let ident = self.resolve(asg);
// It would be nice if this could be built more into the type system
// in the future,
// if it's worth the effort of doing so.
// This is a simple lookup;
// the robust internal diagnostic messages make it look
// more complicated than it is.
ident.name().unwrap_or_else(|| {
let oi_meta_ident =
self.edges_filtered::<Ident>(asg).next().diagnostic_expect(
|| {
vec![
self.internal_error(
"this abstract identifier has no Ident edge",
),
self.help(
"the compiler created an `Ident::Abstract` \
object but did not produce an edge to the \
Ident of the Meta from which its name \
will be derived",
),
]
},
"invalid ASG representation of abstract identifier",
);
oi_meta_ident.resolve(asg).name().diagnostic_expect(
|| {
vec![
self.note(
"while trying to find the Meta name of this abstract \
identifier"
),
oi_meta_ident.internal_error(
"encountered another abstract identifier"
),
oi_meta_ident.help(
"an abstract identifier must reference a concrete \
`Ident`"
),
]
},
"abstract identifier references another abstract identifier",
)
})
}
/// Create a new abstract identifier whose name will be derived from
/// this one during expansion.
///
/// It is expected that `self` defines a [`Meta`],
/// but this is not enforced here and will be checked during
/// expansion.
pub fn new_abstract_ident(
self,
asg: &mut Asg,
at: Span,
) -> Result<ObjectIndex<Ident>, AsgError> {
asg.create(Ident::new_abstract(at))
.add_edge_to(asg, self, Some(at))
}
}
#[cfg(test)]

View File

@ -30,20 +30,20 @@ fn ident_name() {
let name = "name".into();
let spair = SPair(name, S1);
assert_eq!(spair, Ident::Missing(spair).name());
assert_eq!(Some(spair), Ident::Missing(spair).name());
assert_eq!(
spair,
Some(spair),
Ident::Opaque(spair, IdentKind::Meta, Source::default()).name()
);
assert_eq!(
spair,
Some(spair),
Ident::Extern(spair, IdentKind::Meta, Source::default()).name()
);
assert_eq!(
spair,
Some(spair),
Ident::IdentFragment(
spair,
IdentKind::Meta,
@ -182,7 +182,10 @@ fn resolved_on_ident() {
.unwrap()
.resolved()
.unwrap(),
&Ident::Opaque(SPair(sym, S2), kind.clone(), src.clone()),
(
&Ident::Opaque(SPair(sym, S2), kind.clone(), src.clone()),
SPair(sym, S2)
),
);
}
@ -403,7 +406,10 @@ fn resolved_on_fragment() {
assert_eq!(
ident.set_fragment(text.clone()).unwrap().resolved(),
Ok(&Ident::IdentFragment(SPair(sym, S2), kind, src, text)),
Ok((
&Ident::IdentFragment(SPair(sym, S2), kind, src, text),
SPair(sym, S2),
)),
);
}

View File

@ -1,4 +1,4 @@
// Metasyntactic variables represented on the ASG
// Metalinguistic objects represented on the ASG
//
// Copyright (C) 2014-2023 Ryan Specialty, LLC.
//
@ -17,25 +17,33 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//! Metasyntactic variables on the ASG.
//! Metalinguistic objects on the ASG.
//!
//! Metasyntactic variables
//! (sometimes called "metavariables" herein for short)
//! Metalinguistic variables[^w],
//! called "metavariables" for short,
//! have historically been a feature of the template system.
//! The canonical metavariable is the template parameter.
//!
//! [^w]: This term comes from logic; see
//! <https://en.wikipedia.org/wiki/Metavariable_(logic)>.
//! The term "metasyntactic" was originally used with TAMER,
//! but that term generally has a different meaning in programming:
//! <https://en.wikipedia.org/wiki/Metasyntactic_variable>.
use super::{prelude::*, Ident};
use arrayvec::ArrayVec;
use super::{prelude::*, Doc, Ident};
use crate::{
diagnose::Annotate,
diagnostic_todo,
f::Functor,
f::Map,
fmt::{DisplayWrapper, TtQuote},
parse::util::SPair,
parse::{util::SPair, Token},
span::Span,
};
use std::fmt::Display;
/// Metasyntactic variable (metavariable).
/// Metalinguistic variable (metavariable).
///
/// A metavariable is a lexical construct.
/// Its value is a lexeme that represents an [`Ident`],
@ -49,9 +57,26 @@ use std::fmt::Display;
/// the symbol representing that identifier then acts as a metavariable.
#[derive(Debug, PartialEq, Eq)]
pub enum Meta {
/// Metavariable represents a parameter without a value.
///
/// A value must be provided at or before expansion,
/// generally via template application arguments.
Required(Span),
ConcatList(Span),
/// Metavariable has a concrete lexical value.
///
/// This metavariable represents a literal and requires no further
/// reduction or processing.
Lexeme(Span, SPair),
/// Metavariable whose value is to be the concatenation of all
/// referenced metavariables.
///
/// This object has no value on its own;
/// it must contain edges to other metavariables,
/// and the order of those edges on the ASG represents concatenation
/// order.
ConcatList(Span),
}
impl Meta {
@ -93,6 +118,20 @@ impl Meta {
),
}
}
/// Retrieve a concrete lexeme,
/// if any.
///
/// If this metavariable represents a concatenation list,
/// this will return [`None`].
/// This method _does not_ expand metavariables,
/// and does not have the context necessary to do so.
pub fn lexeme(&self) -> Option<SPair> {
match self {
Self::Required(_) | Self::ConcatList(_) => None,
Self::Lexeme(_, lex) => Some(*lex),
}
}
}
impl From<&Meta> for Span {
@ -105,10 +144,10 @@ impl Display for Meta {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
Self::Required(_) => {
write!(f, "metasyntactic parameter with required value")
write!(f, "metalinguistic parameter with required value")
}
Self::ConcatList(_) => {
write!(f, "metasyntactic concatenation list")
write!(f, "metalinguistic concatenation list")
}
Self::Lexeme(_, spair) => {
write!(f, "lexeme {}", TtQuote::wrap(spair))
@ -117,7 +156,7 @@ impl Display for Meta {
}
}
impl Functor<Span> for Meta {
impl Map<Span> for Meta {
fn map(self, f: impl FnOnce(Span) -> Span) -> Self::Target {
match self {
Self::Required(span) => Self::Required(f(span)),
@ -131,14 +170,83 @@ object_rel! {
/// Metavariables contain lexical data and references to other
/// metavariables.
Meta -> {
tree Meta, // TODO: do we need tree?
// References to other metavariables
// (e.g. `<param-value>` in XML-based sources).
cross Ident,
// Owned lexical values.
//
// These differ from the above references because they represent
// inline lexemes that have no identifier.
tree Meta,
// e.g. template paramater description.
tree Doc,
}
}
impl ObjectIndex<Meta> {
pub fn assign_lexeme(self, asg: &mut Asg, lexeme: SPair) -> Self {
self.map_obj(asg, |meta| meta.assign_lexeme(lexeme))
/// Append a lexeme to this metavariable.
///
/// If `self` is [`Meta::Required`],
/// this provides a value and reuses the object already allocated.
///
/// If `self` is a single [`Meta::Lexeme`],
/// it is re-allocated to a separate [`Meta`] object along with the
/// provided `lexeme`,
/// and edges are added to both,
/// indicating concatenation.
///
/// Metavariables with multiple values already represents concatenation
/// and a new edge will be added without changing `self`.
pub fn append_lexeme(
self,
asg: &mut Asg,
lexeme: SPair,
) -> Result<Self, AsgError> {
use Meta::*;
let mut rels = ArrayVec::<SPair, 2>::new();
// We don't have access to `asg` within this closure because of
// `map_obj`;
// the above variable will be mutated by it to return extra
// information to do those operations afterward.
// If we do this often,
// then let's create a `map_obj` that is able to return
// supplemental information or create additional relationships
// (so, a map over a subgraph rather than an object).
self.map_obj(asg, |meta| match meta {
// Storage is already allocated for this lexeme.
Required(span) => Lexeme(span, lexeme),
// We could technically allocate a new symbol and combine the
// lexeme now,
// but let's wait so that we can avoid allocating
// intermediate symbols.
Lexeme(span, first_lexeme) => {
// We're converting from a single lexeme stored on `self` to
// a `Meta` with edges to both individual lexemes.
rels.push(first_lexeme);
rels.push(lexeme);
ConcatList(span)
}
// We're already representing concatenation so we need only add
// an edge to the new lexeme.
ConcatList(span) => {
rels.push(lexeme);
ConcatList(span)
}
});
for rel_lexeme in rels {
let oi = asg.create(Meta::Lexeme(rel_lexeme.span(), rel_lexeme));
self.add_edge_to(asg, oi, None)?;
}
Ok(self)
}
pub fn close(self, asg: &mut Asg, close_span: Span) -> Self {
@ -148,4 +256,53 @@ impl ObjectIndex<Meta> {
})
})
}
// Append a reference to a metavariable identified by `oi_ref`.
//
// The value of the metavariable will not be known until expansion time,
// at which point its lexical value will be concatenated with those of
// any other references,
// in the order that they were added.
//
// It is expected that the value of `oi_ref` was produced via a lookup
// from the reference location and therefore contains the reference
// [`Span`];
// this is used to provide accurate diagnostic information.
pub fn concat_ref(
self,
asg: &mut Asg,
oi_ref: ObjectIndex<Ident>,
) -> Result<Self, AsgError> {
use Meta::*;
// We cannot mutate the ASG within `map_obj` below because of the
// held reference to `asg`,
// so this will be used to store data for later mutation.
let mut pre = None;
// References are only valid for a [`Self::ConcatList`].
self.map_obj(asg, |meta| match meta {
Required(span) | ConcatList(span) => ConcatList(span),
Lexeme(span, lex) => {
// We will move the lexeme into a _new_ object,
// and store a reference to it.
pre.replace(Meta::Lexeme(lex.span(), lex));
ConcatList(span)
}
});
// This represents a lexeme that was extracted into a new `Meta`;
// we must add the edge before appending the ref since
// concatenation will occur during expansion in edge order.
if let Some(orig) = pre {
asg.create(orig).add_edge_from(asg, self, None)?;
}
// Having been guaranteed a `ConcatList` above,
// we now only need to append an edge that references what to
// concatenate.
self.add_edge_to(asg, oi_ref, Some(oi_ref.span()))
}
}

View File

@ -21,7 +21,7 @@
use super::{prelude::*, Doc, Ident, Tpl};
use crate::{
f::Functor,
f::Map,
fmt::{DisplayWrapper, TtQuote},
parse::{util::SPair, Token},
span::Span,
@ -87,7 +87,7 @@ impl Display for Pkg {
}
}
impl Functor<Span> for Pkg {
impl Map<Span> for Pkg {
fn map(self, f: impl FnOnce(Span) -> Span) -> Self::Target {
match self {
Self(span, path) => Self(f(span), path),
@ -134,11 +134,15 @@ impl ObjectIndex<Pkg> {
let parent = self.resolve(asg);
let oi_import = asg.create(Pkg::new_imported(parent, namespec)?);
Ok(self.add_edge_to(asg, oi_import, Some(namespec.span())))
self.add_edge_to(asg, oi_import, Some(namespec.span()))
}
/// Arbitrary text serving as documentation in a literate style.
pub fn append_doc_text(&self, asg: &mut Asg, text: SPair) -> Self {
pub fn append_doc_text(
&self,
asg: &mut Asg,
text: SPair,
) -> Result<Self, AsgError> {
let oi_doc = asg.create(Doc::new_text(text));
self.add_edge_to(asg, oi_doc, None)
}

View File

@ -22,12 +22,15 @@
//! See (parent module)[super] for more information.
use super::{
Doc, Expr, Ident, Meta, Object, ObjectIndex, ObjectKind, OiPairObjectInner,
Pkg, Root,
Doc, Expr, Ident, Meta, Object, ObjectIndex, ObjectIndexRefined,
ObjectKind, OiPairObjectInner, Pkg, Root,
};
use crate::{
asg::{graph::object::Tpl, Asg},
f::Functor,
asg::{
graph::{object::Tpl, AsgRelMut, ProposedRel},
Asg, AsgError,
},
f::Map,
span::Span,
};
use std::{fmt::Display, marker::PhantomData};
@ -55,7 +58,7 @@ macro_rules! object_rel {
(
$(#[$attr:meta])+
$from:ident -> {
$($ety:ident $kind:ident,)*
$($ety:ident $kind:ident $({$($impl:tt)*})?,)*
}
$(can_recurse($rec_obj:ident) if $rec_expr:expr)?
) => {paste::paste! {
@ -168,6 +171,17 @@ macro_rules! object_rel {
}
}
}
// This generates a specialized implementation _per target `$kind`_
// and allows for the caller to override methods on the trait.
// This takes advantage of trait specialization via
// `min_specialization`;
// see `AsgRelMut` for more information.
$(
impl AsgRelMut<$kind> for $from {
$( $($impl)* )?
}
)*
}};
// Static edge types.
@ -270,12 +284,47 @@ impl<S> DynObjectRel<S, ObjectIndex<Object>> {
/// Attempt to narrow the target into the [`ObjectRel`] of `O`.
///
/// See [`ObjectRelatable::new_rel_dyn`] for more information.
///
/// To exhaustively match against all possible [`ObjectKind`]s,
/// see [`Self::refine_target`].
pub fn narrow_target<O: ObjectKind + ObjectRelatable>(
&self,
) -> Option<O::Rel> {
O::new_rel_dyn(self.target_ty(), *self.target())
}
/// Refine the target [`ObjectIndex<Object>`](ObjectIndex) into
/// [`ObjectIndexRefined`] such that the returned variant has a
/// narrowed [`ObjectIndex<O>`] type.
///
/// This allows converting a dynamic [`ObjectIndex`] into a statically
/// known type where `O` is derived from [`Self::target_ty`].
/// This avoids having to manually match on [`Self::target_ty`] and then
/// use [`ObjectIndex::must_narrow_into`] on the matching
/// [`ObjectKind`],
/// since there is a risk of those getting out of sync.
///
/// In contrast to [`Self::narrow_target`],
/// where the caller must specify the expected [`ObjectKind`],
/// this allows for exhaustively matching against all possible objects.
pub fn refine_target(&self) -> ObjectIndexRefined {
macro_rules! narrow_each_rel_ty {
( $($var:ident),+ ) => {
match self.target_ty() {
$(
ObjectRelTy::$var => {
ObjectIndexRefined::$var(
self.target().must_narrow_into()
)
}
)+
}
}
}
narrow_each_rel_ty!(Root, Pkg, Ident, Expr, Tpl, Meta, Doc)
}
/// Attempt to convert [`Self`] into an [`ObjectIndex`] with an
/// [`ObjectKind`] of type `O`.
///
@ -432,7 +481,7 @@ impl DynObjectRel<ObjectIndex<Object>, ObjectIndex<Object>> {
}
}
impl<S, T, U, V> Functor<(S, T), (U, V)> for DynObjectRel<S, T> {
impl<S, T, U, V> Map<(S, T), (U, V)> for DynObjectRel<S, T> {
type Target = DynObjectRel<U, V>;
fn map(self, f: impl FnOnce((S, T)) -> (U, V)) -> Self::Target {
@ -466,7 +515,8 @@ impl<T: Display> Display for DynObjectRel<T> {
/// statically analyzed by the type system to ensure that they only
/// construct graphs that adhere to this schema.
pub trait ObjectRelTo<OB: ObjectKind + ObjectRelatable> =
ObjectRelatable where <Self as ObjectRelatable>::Rel: From<ObjectIndex<OB>>;
ObjectRelatable + AsgRelMut<OB>
where <Self as ObjectRelatable>::Rel: From<ObjectIndex<OB>>;
/// Reverse of [`ObjectRelTo`].
///
@ -791,39 +841,36 @@ pub trait ObjectIndexRelTo<OB: ObjectRelatable>: Sized + Clone + Copy {
/// See [`ObjectIndex::widen`] for more information.
fn widen(&self) -> ObjectIndex<Object>;
/// Add an edge from `self` to `to_oi` on the provided [`Asg`].
/// Request permission to add an edge from `self` to another object.
///
/// Since the only invariant asserted by [`ObjectIndexRelTo`] is that
/// it may be related to `OB`,
/// this method will only permit edges to `OB`;
/// nothing else about the inner object is statically known.
/// To create edges to other types of objects,
/// and for more information about this operation
/// (including `ctx_span`),
/// see [`ObjectIndex::add_edge_to`].
fn add_edge_to(
self,
/// This gives the object ownership over the edges that are created,
/// in addition to the static guarantees provided by
/// [`ObjectIndexRelTo`].
/// Since [`ObjectIndexRelTo` supports dynamic source objects,
/// this allows calling code to be written in a concise manner that is
/// agnostic to the source type,
/// without sacrificing edge ownership.
///
/// For more information,
/// see [`AsgRelMut::pre_add_edge`].
///
/// _This should only be called by [`Asg`]_;
/// `commit` is expected to be a continuation that adds the edge to
/// the graph,
/// and the object represented by `self` may modify itself expecting
/// such an edge to be added.
fn pre_add_edge(
&self,
asg: &mut Asg,
to_oi: ObjectIndex<OB>,
ctx_span: Option<Span>,
) -> Self {
asg.add_edge(self, to_oi, ctx_span);
self
}
) -> Result<(), AsgError>;
/// Check whether an edge exists from `self` to `to_oi`.
fn has_edge_to(&self, asg: &Asg, to_oi: ObjectIndex<OB>) -> bool {
asg.has_edge(*self, to_oi)
}
/// Indicate that the given identifier `oi` is defined by this object.
fn defines(self, asg: &mut Asg, oi: ObjectIndex<Ident>) -> Self
where
Self: ObjectIndexRelTo<Ident>,
{
self.add_edge_to(asg, oi, None)
}
/// Iterate over the [`ObjectIndex`]es of the outgoing edges of `self`
/// that match the [`ObjectKind`] `OB`.
///
@ -875,8 +922,10 @@ pub trait ObjectIndexRelTo<OB: ObjectRelatable>: Sized + Clone + Copy {
Self: ObjectIndexRelTo<Ident>,
{
// Rust fails to infer OB with `self.edges_rel_to` as of 2023-03
ObjectIndexRelTo::<Ident>::edges_rel_to(self, asg)
.find(|oi| oi.resolve(asg).name().symbol() == name.symbol())
ObjectIndexRelTo::<Ident>::edges_rel_to(self, asg).find(|oi| {
oi.resolve(asg).name().map(|name| name.symbol())
== Some(name.symbol())
})
}
}
@ -892,6 +941,22 @@ where
fn widen(&self) -> ObjectIndex<Object> {
ObjectIndex::<O>::widen(*self)
}
fn pre_add_edge(
&self,
asg: &mut Asg,
to_oi: ObjectIndex<OB>,
ctx_span: Option<Span>,
) -> Result<(), AsgError> {
O::pre_add_edge(
asg,
ProposedRel {
from_oi: self.widen().must_narrow_into::<O>(),
to_oi,
ctx_span,
},
)
}
}
impl<OB: ObjectRelatable> ObjectIndexRelTo<OB> for ObjectIndexTo<OB> {
@ -904,6 +969,36 @@ impl<OB: ObjectRelatable> ObjectIndexRelTo<OB> for ObjectIndexTo<OB> {
fn widen(&self) -> ObjectIndex<Object> {
*self.as_ref()
}
fn pre_add_edge(
&self,
asg: &mut Asg,
to_oi: ObjectIndex<OB>,
ctx_span: Option<Span>,
) -> Result<(), AsgError> {
macro_rules! pre_add_edge {
($ty:ident) => {
$ty::pre_add_edge(
asg,
ProposedRel {
from_oi: self.widen().must_narrow_into::<$ty>(),
to_oi,
ctx_span,
},
)
};
}
match self.src_rel_ty() {
ObjectRelTy::Root => pre_add_edge!(Root),
ObjectRelTy::Pkg => pre_add_edge!(Pkg),
ObjectRelTy::Ident => pre_add_edge!(Ident),
ObjectRelTy::Expr => pre_add_edge!(Expr),
ObjectRelTy::Tpl => pre_add_edge!(Tpl),
ObjectRelTy::Meta => pre_add_edge!(Meta),
ObjectRelTy::Doc => pre_add_edge!(Doc),
}
}
}
impl<OB: ObjectRelatable> ObjectIndexRelTo<OB> for ObjectIndexToTree<OB> {
@ -918,6 +1013,17 @@ impl<OB: ObjectRelatable> ObjectIndexRelTo<OB> for ObjectIndexToTree<OB> {
Self(oito) => oito.widen(),
}
}
fn pre_add_edge(
&self,
asg: &mut Asg,
to_oi: ObjectIndex<OB>,
ctx_span: Option<Span>,
) -> Result<(), AsgError> {
match self {
Self(oito) => oito.pre_add_edge(asg, to_oi, ctx_span),
}
}
}
impl<OB: ObjectRelatable> From<ObjectIndexTo<OB>> for ObjectIndex<Object> {

View File

@ -66,7 +66,7 @@ impl ObjectIndex<Root> {
&self,
asg: &mut Asg,
oi: ObjectIndex<Ident>,
) -> ObjectIndex<Ident> {
) -> Result<ObjectIndex<Ident>, AsgError> {
oi.add_edge_from(asg, *self, None)
}
}

View File

@ -22,35 +22,192 @@
use std::fmt::Display;
use super::{prelude::*, Doc, Expr, Ident};
use crate::{f::Functor, parse::util::SPair, span::Span};
use crate::{asg::graph::ProposedRel, f::Map, parse::util::SPair, span::Span};
/// Template with associated name.
#[derive(Debug, PartialEq, Eq)]
pub struct Tpl(Span);
pub struct Tpl(Span, TplShape);
impl Tpl {
pub fn new(span: Span) -> Self {
Self(span, TplShape::default())
}
pub fn span(&self) -> Span {
match self {
Self(span) => *span,
Self(span, _) => *span,
}
}
pub fn new(span: Span) -> Self {
Self(span)
pub fn shape(&self) -> TplShape {
match self {
Self(_, shape) => *shape,
}
}
}
impl Functor<Span> for Tpl {
fn map(self, f: impl FnOnce(Span) -> Span) -> Self::Target {
match self {
Self(span) => Self(f(span)),
}
}
impl_mono_map! {
Span => Tpl(@, shape),
TplShape => Tpl(span, @),
}
impl Display for Tpl {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "template")
let Self(_, shape) = self;
write!(f, "template with {shape}")
}
}
/// The "shape" of a template when expanded into an expression context.
///
/// The shape of a template can be thought of like a puzzle piece.
/// Each application context permits a particular type of puzzle piece,
/// and a compatible template must be expanded into it,
/// or otherwise be made to be compatible.
///
/// Template shapes must be known statically by the time the definition has
/// completed.
/// A definition is not complete until all missing identifier references
/// have been defined.
/// A corollary of this is that templates applied _within_ templates will
/// be able to determine their shape because the shape of the applied
/// template will be known,
/// allowing them to compose without compromising this property.
///
/// Objects that would typically be hoisted out of an expression context do
/// not contribute to the shape of a template.
/// That is---
/// if an object would not typically be parented to the expansion context
/// if manually written at that source location,
/// then it will not be parented by a template expansion,
/// and so will not contribute to its shape.
///
/// Dynamic Inner Template Application
/// ==================================
/// Sometimes the shape of inner applications cannot be known because their
/// application depends on values of metavariables that are provided by
/// the caller.
/// One such example is that the body of the template is conditional
/// depending on what values are provided to the template.
///
/// In this case,
/// it may be necessary for the body of the template to _coerce_ into a
/// statically known shape by wrapping the dynamic application in a known
/// object.
/// For example,
/// if a template's body can conditionally expand into one of a set of
/// [`TplShape::Expr`] templates,
/// then that condition can be wrapped in an [`Expr`] object so that,
/// no matter what the expansion,
/// we'll always have a shape of [`TplShape::Expr`].
///
/// Expansion Ordering
/// ==================
/// By requiring a shape to be available by the time the definition of a
/// template is completed,
/// a system like [`AIR`](crate::asg::air) is able to pre-allocate an
/// [`Object`] at the application site.
/// This ensures that we are able to generate a graph with the proper edge
/// ordering,
/// which is important for non-commutative objects.
#[derive(Debug, PartialEq, Eq, Clone, Copy, Default)]
pub enum TplShape {
/// The template will not inline any objects.
#[default]
Empty,
/// The template is non-[`Empty`](Self::Empty),
/// but its shape cannot yet be determined.
///
/// A template's shape must be known by the time its definition has been
/// completed.
/// Note that a definition is not complete until all missing identifiers
/// have been defined.
Unknown,
/// The template can be expanded inline into a single [`Expr`].
///
/// This allows a template to be expanded into an expression context and
/// provides assurances that it will not take the place of more than a
/// single expression.
///
/// The associated span provides rationale for this shape assertion.
/// The [`ObjectIndex`] is not cached here to avoid having to keep them
/// in sync if the graph changes,
/// in which case this rationale may represent the _original_
/// rationale before any graph rewriting.
Expr(Span),
}
impl TplShape {
/// Attempt to adapt a template shape to that of another.
///
/// If the shape of `other` is a refinement of the shape of `self`,
/// then `other` will be chosen.
/// If the shape of `other` conflicts with `self`,
/// an appropriate [`AsgError`] will describe the problem.
fn try_adapt_to(
self,
other: TplShape,
tpl_name: Option<SPair>,
) -> Result<Self, (Self, AsgError)> {
match (self, other) {
(TplShape::Expr(first_span), TplShape::Expr(bad_span)) => Err((
self,
AsgError::TplShapeExprMulti(tpl_name, bad_span, first_span),
)),
// Higher levels of specificity take precedence.
(shape @ TplShape::Expr(_), TplShape::Empty)
| (TplShape::Empty, shape @ TplShape::Expr(_))
| (shape @ TplShape::Empty, TplShape::Empty) => Ok(shape),
// Unknown is not yet handled.
(
TplShape::Unknown,
TplShape::Empty | TplShape::Unknown | TplShape::Expr(_),
)
| (TplShape::Empty | TplShape::Expr(_), TplShape::Unknown) => {
todo!("TplShape::Unknown")
}
}
}
/// If the shape stores [`Span`] information as evidence of inference,
/// overwrite it with the provided `span`.
///
/// This is most commonly used to encapsulate a previous shape
/// inference.
/// For example,
/// a template application's span may overwrite the inferred shape of
/// its own body.
fn overwrite_span_if_any(self, span: Span) -> Self {
match self {
TplShape::Empty | TplShape::Unknown => self,
TplShape::Expr(_) => TplShape::Expr(span),
}
}
}
/// Attempt to adapt a template shape to that of another.
///
/// This returns a partially applied [`TplShape::try_adapt_to`],
/// where the remaining argument is `self`.
fn try_adapt_to(
other: TplShape,
tpl_name: Option<SPair>,
) -> impl FnOnce(TplShape) -> Result<TplShape, (TplShape, AsgError)> {
move |s| s.try_adapt_to(other, tpl_name)
}
impl Display for TplShape {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
// phrase as "template with ..."
match self {
TplShape::Unknown => write!(f, "unknown shape"),
TplShape::Empty => write!(f, "empty shape"),
TplShape::Expr(_) => write!(f, "shape of a single expression"),
}
}
}
@ -60,14 +217,43 @@ object_rel! {
Tpl -> {
// Expressions must be able to be anonymous to allow templates in
// any `Expr` context.
tree Expr,
tree Expr {
fn pre_add_edge(
asg: &mut Asg,
rel: ProposedRel<Self, Expr>,
) -> Result<(), AsgError> {
let tpl_name = rel.from_oi.name(asg);
let span = rel.to_oi.resolve(asg).span();
rel.from_oi.try_map_obj_inner(
asg,
try_adapt_to(TplShape::Expr(span), tpl_name),
).map(|_| ())
}
},
// Identifiers are used for both references and identifiers that
// will expand into an application site.
dyn Ident,
// Template application.
tree Tpl,
tree Tpl {
fn pre_add_edge(
asg: &mut Asg,
rel: ProposedRel<Self, Tpl>,
) -> Result<(), AsgError> {
let tpl_name = rel.from_oi.name(asg);
let apply = rel.to_oi.resolve(asg);
let apply_shape = apply
.shape()
.overwrite_span_if_any(apply.span());
rel.from_oi.try_map_obj_inner(
asg,
try_adapt_to(apply_shape, tpl_name),
).map(|_| ())
}
},
// Short template description and arbitrary documentation to be
// expanded into the application site.
@ -76,13 +262,23 @@ object_rel! {
}
impl ObjectIndex<Tpl> {
/// Name of template,
/// if any.
///
/// A template may either be anonymous,
/// or it may not yet have a name because it is still under
/// construction.
pub fn name(&self, asg: &Asg) -> Option<SPair> {
self.ident(asg).and_then(|oi| oi.resolve(asg).name())
}
/// Attempt to complete a template definition.
///
/// This updates the span of the template to encompass the entire
/// definition.
pub fn close(self, asg: &mut Asg, close_span: Span) -> Self {
self.map_obj(asg, |tpl| {
tpl.map(|open_span| {
tpl.map(|open_span: Span| {
open_span.merge(close_span).unwrap_or(open_span)
})
})
@ -98,7 +294,7 @@ impl ObjectIndex<Tpl> {
asg: &mut Asg,
oi_apply: ObjectIndex<Ident>,
ref_span: Span,
) -> Self {
) -> Result<Self, AsgError> {
self.add_edge_to(asg, oi_apply, Some(ref_span))
}
@ -116,13 +312,17 @@ impl ObjectIndex<Tpl> {
self,
asg: &mut Asg,
oi_target_parent: OP,
) -> Self {
) -> Result<Self, AsgError> {
self.add_edge_from(asg, oi_target_parent, None)
}
/// Arbitrary text serving as documentation in a literate style,
/// to be expanded into the application site.
pub fn append_doc_text(&self, asg: &mut Asg, text: SPair) -> Self {
pub fn append_doc_text(
&self,
asg: &mut Asg,
text: SPair,
) -> Result<Self, AsgError> {
let oi_doc = asg.create(Doc::new_text(text));
self.add_edge_to(asg, oi_doc, None)
}

View File

@ -29,7 +29,7 @@
//! This is a [depth-first search][w-depth-first-search]
//! visiting all nodes that are _reachable_ from the graph root
//! (see [`Asg::root`]).
//! [`ObjectIndex`]es are emitted in pre-order during the traversal,
//! [`TreeWalkRel`]s are emitted in pre-order during the traversal,
//! and may be emitted more than once if
//! (a) they are the destination of cross edges or
//! (b) they are shared between trees
@ -103,7 +103,7 @@
//!
//! Depth Tracking
//! ==============
//! Each [`ObjectIndex`] emitted by this traversal is accompanied by a
//! Each [`TreeWalkRel`] emitted by this traversal is accompanied by a
//! [`Depth`] representing the length of the current path relative to the
//! [`Asg`] root.
//! Since the ASG root is never emitted,
@ -135,11 +135,34 @@
//! because the [`Depth`] represents the current _path_,
//! the same [`ObjectIndex`] may be emitted multiple times with different
//! [`Depth`]s.
//!
//!
//! Edge Order
//! ==========
//! The order of edges in the tree is important,
//! since there are a number of non-commutative operations in TAME.
//! Ordering is determined by a [`TreeEdgeOrder`] strategy:
//!
//! 1. [`NaturalTreeEdgeOrder`] will traverse in the same order that edges
//! were added to the graph.
//! This ordering is fine for most internal operations,
//! but is not suitable for [`tree_reconstruction`].
//!
//! 2. [`SourceCompatibleTreeEdgeOrder`] traverses edges in an order that
//! will produce a valid source file for [NIR XML](crate::nir).
//! For example,
//! templates require a header and body section,
//! where [`Asg`] permits mixing them.
//! This maintains natural order in all other cases.
use std::fmt::Display;
use std::{fmt::Display, marker::PhantomData};
use super::super::{object::DynObjectRel, Asg, Object, ObjectIndex};
use super::super::{
object::{self, DynObjectRel},
Asg,
};
use crate::{
asg::graph::object::ObjectTy,
parse::{self, Token},
span::{Span, UNKNOWN_SPAN},
};
@ -149,7 +172,9 @@ use crate::{
pub use crate::xir::flat::Depth;
#[cfg(doc)]
use super::super::object::ObjectRel;
use super::super::object::{ObjectIndex, ObjectRel};
pub use order::*;
/// Produce an iterator suitable for reconstructing a source tree based on
/// the contents of the [`Asg`].
@ -160,7 +185,9 @@ use super::super::object::ObjectRel;
///
/// See the [module-level documentation](super) for important information
/// about this traversal.
pub fn tree_reconstruction(asg: &Asg) -> TreePreOrderDfs {
pub fn tree_reconstruction(
asg: &Asg,
) -> TreePreOrderDfs<SourceCompatibleTreeEdgeOrder> {
TreePreOrderDfs::new(asg)
}
@ -170,11 +197,11 @@ pub fn tree_reconstruction(asg: &Asg) -> TreePreOrderDfs {
/// _it does not track visited nodes_,
/// relying instead on the ontology and recognition of cross edges to
/// produce the intended spanning tree.
/// An [`ObjectIndex`] that is the target of a cross edge will be output
/// An object that is the target of a cross edge will be output
/// more than once.
///
/// See [`tree_reconstruction`] for more information.
pub struct TreePreOrderDfs<'a> {
pub struct TreePreOrderDfs<'a, O: TreeEdgeOrder> {
/// Reference [`Asg`].
///
/// Holding a reference to the [`Asg`] allows us to serve conveniently
@ -189,6 +216,8 @@ pub struct TreePreOrderDfs<'a> {
///
/// The traversal ends once the stack becomes empty.
stack: Vec<(DynObjectRel, Depth)>,
_phantom: PhantomData<O>,
}
/// Initial size of the DFS stack for [`TreePreOrderDfs`].
@ -196,34 +225,40 @@ pub struct TreePreOrderDfs<'a> {
/// TODO: Derive a heuristic from our systems.
const TREE_INITIAL_STACK_SIZE: usize = 8;
impl<'a> TreePreOrderDfs<'a> {
impl<'a, O: TreeEdgeOrder> TreePreOrderDfs<'a, O> {
fn new(asg: &'a Asg) -> Self {
let span = UNKNOWN_SPAN;
let mut dfs = Self {
asg,
stack: Vec::with_capacity(TREE_INITIAL_STACK_SIZE),
_phantom: PhantomData,
};
let root = asg.root(span);
dfs.push_edges_of(root.widen(), Depth::root());
let root_rel = DynObjectRel::new(
root.rel_ty(),
root.rel_ty(),
root.widen(),
root.widen(),
None,
);
dfs.push_edges_of(&root_rel, Depth::root());
dfs
}
fn push_edges_of(&mut self, oi: ObjectIndex<Object>, depth: Depth) {
self.asg
.edges_dyn(oi)
.map(|rel| (rel, depth.child_depth()))
.collect_into(&mut self.stack);
fn push_edges_of(&mut self, rel: &DynObjectRel, depth: Depth) {
O::push_edges_of(self.asg, rel, depth, &mut self.stack)
}
}
impl<'a> Iterator for TreePreOrderDfs<'a> {
impl<'a, O: TreeEdgeOrder> Iterator for TreePreOrderDfs<'a, O> {
type Item = TreeWalkRel;
/// Produce the next [`ObjectIndex`] from the traversal in pre-order.
/// Produce the next [`TreeWalkRel`] from the traversal in pre-order.
///
/// An [`ObjectIndex`] may be emitted more than once;
/// An object may be emitted more than once;
/// see [`tree_reconstruction`] for more information.
///
/// Each item contains a corresponding [`Depth`],
@ -233,12 +268,17 @@ impl<'a> Iterator for TreePreOrderDfs<'a> {
/// This depth is the only way to derive the tree structure from this
/// iterator.
fn next(&mut self) -> Option<Self::Item> {
// Note that we pushed edges in the order that `Asg` provided,
// and now pop them,
// which causes us to visit the edges in reverse.
// Because of implementation details (Petgraph),
// this reversal ends up giving us the correct ordering.
let (rel, depth) = self.stack.pop()?;
// We want to output information about references to other trees,
// but we must not traverse into them.
if !rel.is_cross_edge() {
self.push_edges_of(*rel.target(), depth);
self.push_edges_of(&rel, depth);
}
Some(TreeWalkRel(rel, depth))
@ -283,5 +323,196 @@ impl Token for TreeWalkRel {
impl parse::Object for TreeWalkRel {}
mod order {
use crate::asg::graph::object::ObjectIndexRefined;
use super::*;
/// Emit edges in the same order that they were added to the graph.
///
/// Various parts of the system take care in what order edges are
/// added.
/// This ordering is important for operations that are not commutative.
pub struct NaturalTreeEdgeOrder;
/// Emit edges in as close to [`NaturalTreeEdgeOrder`] as possible,
/// sorting only where object ordering would otherwise be
/// syntactically or grammatically invalid for streaming source
/// generation.
///
/// Unless otherwise mentioned below,
/// ordering for objects will be the same as
/// [`NaturalTreeEdgeOrder`].
///
/// Template Headers
/// ----------------
/// For [NIR XML](crate::nir) sources for TAME,
/// templates require that parameters be placed in a header,
/// before all objects representing the body of the template.
/// This is necessary to disambiguate `<param>` nodes in sources,
/// even though no such ambiguity exists on the [`Asg`].
///
/// All metavariables representing template params will be hoisted into
/// the header,
/// immediately after any template description [`Doc`](object::Doc)
/// node.
/// This is a stable partial ordering:
/// the ordering of parameters relative to one-another will not change,
/// nor will the order of any objects in the body of the template.
/// See [`TplOrder`].
pub struct SourceCompatibleTreeEdgeOrder;
/// Order in which tree edges are emitted.
///
/// TAME is sensitive to edge ordering for non-commutative operations.
/// For source lk
pub trait TreeEdgeOrder {
/// Push the edges of `rel` onto the `target` stack.
///
/// The system will pop edges off of `target` to determine what edge
/// to traverse next.
/// This means that edges will be visited in an order that is the
/// reverse of the elements of `target`.
fn push_edges_of(
asg: &Asg,
rel: &DynObjectRel,
depth: Depth,
target: &mut Vec<(DynObjectRel, Depth)>,
);
}
impl TreeEdgeOrder for NaturalTreeEdgeOrder {
fn push_edges_of(
asg: &Asg,
rel: &DynObjectRel,
depth: Depth,
stack: &mut Vec<(DynObjectRel, Depth)>,
) {
let oi = *rel.target();
asg.edges_dyn(oi)
.map(|rel| (rel, depth.child_depth()))
.collect_into(stack);
}
}
/// Ordering of template children for [`SourceCompatibleTreeEdgeOrder`].
///
/// Since these edges are added to a _stack_,
/// larger values will be `pop`'d _before_ smaller ones,
/// as demonstrated by the variant order.
/// That is:
/// we sort in the reverse order that we will visit them.
///
/// ```text
/// ------> Sort direction
/// [ Body, Body, Param, Param, Desc ]
/// <------ visit direction
/// (pop from stack)
/// ```
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Copy, Clone)]
enum TplOrder {
/// Forcing the template description to come first mimics
/// the expected [`NaturalTreeEdgeOrder`].
///
/// We don't want to re-order things before it,
/// since we want to be able to stream source output,
/// e.g. `template/@desc` in XML.
TplDesc = 2,
/// Template parameters must appear in the template
/// "header",
/// which is all the nodes before the body that is to be
/// expanded on application.
Param = 1,
/// The body of the template is anything that is not part of
/// the header.
///
/// The body represents the contents of the template that
/// will be expanded in place of any template application.
Body = 0,
}
impl TreeEdgeOrder for SourceCompatibleTreeEdgeOrder {
fn push_edges_of(
asg: &Asg,
rel: &DynObjectRel,
depth: Depth,
stack: &mut Vec<(DynObjectRel, Depth)>,
) {
// We start by adding edges to the stack in natural order,
// remembering the original stack offset so that we can sort
// just the portion that we added.
let offset = stack.len();
NaturalTreeEdgeOrder::push_edges_of(asg, rel, depth, stack);
use ObjectTy::*;
match rel.target_ty() {
// Templates require partial ordering into a header and a body.
Tpl => {
// This represents the portion of the stack that we just
// contributed to via [`NaturalTreeEdgeOrder`] above.
let part = &mut stack[offset..];
// TODO: Ideally we'd have metadata on the edge itself
// about what type of object an `Ident` points to,
// so that we can use the faster `sort_by_key`.
// With that said,
// initial profiling on large packages with many
// template applications did not yield a
// significant difference between the two methods on
// system-level tests,
// and given the small number of template
// children,
// this consideration may be a micro-optimization.
// An unstable sort is avoided because we wish to
// retain natural ordering as much as possible.
//
// TODO: In practice,
// most of these are template _applications_ resulting
// from `tplshort` desugaring.
// At the time of writing,
// _all_ need sorting because `Ref` is output before
// the params.
// We could recognize them as template applications at
// some point and leave their order alone,
// but at the time of writing we have no imports,
// and so most refs are `Ident::Missing` in
// practice.
// Once template imports are taken care of,
// then _nearly every single `Tpl` in practice` will
// already be ordered and this will rarely have any
// reordering to do
// (just hoisting for interpolation in template
// definitions,
// which are not all that common relative to
// everything else).
use ObjectIndexRefined::*;
part.sort_by_cached_key(|(child_rel, _)| {
match child_rel.refine_target() {
Ident(oi_ident) => {
// This is the (comparatively) expensive lookup,
// requiring a small graph traversal.
match oi_ident.definition::<object::Meta>(asg) {
Some(_) => TplOrder::Param,
None => TplOrder::Body,
}
}
Doc(_) => TplOrder::TplDesc,
Root(_) | Pkg(_) | Expr(_) | Tpl(_) | Meta(_) => {
TplOrder::Body
}
}
});
}
// Leave natural (graph) ordering for everything else.
Root | Pkg | Ident | Expr | Meta | Doc => (),
}
}
}
}
#[cfg(test)]
mod test;

View File

@ -24,8 +24,11 @@ use crate::{
graph::object::ObjectRelTy,
ExprOp,
},
f::Functor,
parse::{util::SPair, ParseState},
f::Map,
parse::{
util::{spair, SPair},
ParseState,
},
span::{dummy::*, Span},
};
use std::fmt::Debug;
@ -204,21 +207,21 @@ fn traverses_ontological_tree_tpl_apply() {
PkgStart(S1, SPair("/pkg".into(), S1)),
// The template that will be applied.
TplStart(S2),
BindIdent(id_tpl),
// This test is light for now,
// until we develop the ASG further.
TplEnd(S4),
// Apply the above template.
TplStart(S5),
RefIdent(ref_tpl),
MetaStart(S7),
BindIdent(id_param),
MetaLexeme(value_param),
MetaEnd(S10),
TplEndRef(S11), // notice the `Ref` at the end
BindIdent(id_tpl), // <-,
// |
// This test is light for now, // |
// until we develop the ASG further. // |
TplEnd(S4), // |
// |
// Apply the above template. // |
TplStart(S5), // |
RefIdent(ref_tpl), // |
// |
MetaStart(S7), // |
BindIdent(id_param), // |
MetaLexeme(value_param), // |
MetaEnd(S10), // |
TplEndRef(S11), // notice the `Ref` at the end --'
PkgEnd(S12),
];
@ -234,9 +237,13 @@ fn traverses_ontological_tree_tpl_apply() {
(d(Pkg, Ident, m(S1, S12), S3, None ), Depth(2)),
(d(Ident, Tpl, S3, m(S2, S4), None ), Depth(3)),
(d(Pkg, Tpl, m(S1, S12), m(S5, S11), None ), Depth(2)),
/*cross*/ (d(Tpl, Ident, m(S5, S11), S3, Some(S6)), Depth(3)),
(d(Tpl, Ident, m(S5, S11), S8, None ), Depth(3)),
(d(Ident, Meta, S8, m(S7, S10), None ), Depth(4)),
/*cross*/ (d(Tpl, Ident, m(S5, S11), S3, Some(S6)), Depth(3)),
// ^
// `- Note that the cross edge was moved to the bottom
// because all template params are moved into the
// template header for `SourceCompatibleTreeEdgeOrder`.
],
tree_reconstruction_report(toks),
);
@ -328,3 +335,147 @@ fn traverses_ontological_tree_tpl_within_template() {
tree_reconstruction_report(toks),
);
}
// Metavariables are used to represent template parameters,
// and are used to perform various lexical manipulations.
// The most fundamental of them is concatenation,
// and in the special case of concatenating a single value,
// assignment.
//
// This asserts that concatenation results in the expected graph and that
// the traversal respects concatenation order.
#[test]
fn traverses_ontological_tree_complex_tpl_meta() {
#[rustfmt::skip]
let toks = vec![
PkgStart(S1, spair("/pkg", S1)),
TplStart(S2),
BindIdent(spair("_tpl_", S3)),
// -- Above this line was setup -- //
MetaStart(S4),
BindIdent(spair("@param@", S5)),
// It will be important to observe that ordering
// is respected during traversal,
// otherwise concatenation order will be wrong.
MetaLexeme(spair("foo", S6)),
RefIdent(spair("@other@", S7)), // --.
MetaLexeme(spair("bar", S8)), // |
MetaEnd(S9), // |
// |
MetaStart(S10), // |
BindIdent(spair("@other@", S11)), // <-'
MetaEnd(S12),
TplEnd(S13),
PkgEnd(S14),
];
// We need more concise expressions for the below table of values.
let d = DynObjectRel::new;
let m = |a: Span, b: Span| a.merge(b).unwrap();
#[rustfmt::skip]
assert_eq!(
// A -|-> B | A span -|-> B span | espan | depth
vec![//-----|-------|-----------|------------|--------|-----------------
(d(Root, Pkg, SU, m(S1, S14), None ), Depth(1)),
(d(Pkg, Ident, m(S1, S14), S3, None ), Depth(2)),
(d(Ident, Tpl, S3, m(S2, S13), None ), Depth(3)),
(d(Tpl, Ident, m(S2, S13), S5, None ), Depth(4)),
(d(Ident, Meta, S5, m(S4, S9), None ), Depth(5)),
(d(Meta, Meta, m(S4, S9), S6, None ), Depth(6)),
/*cross*/ (d(Meta, Ident, m(S4, S9), S11, Some(S7)), Depth(6)),
(d(Meta, Meta, m(S4, S9), S8, None, ), Depth(6)),
(d(Tpl, Ident, m(S2, S13), S11, None ), Depth(4)),
(d(Ident, Meta, S11, m(S10, S12), None ), Depth(5)),
],
tree_reconstruction_report(toks),
);
}
// TAME's grammar expects that template parameters be defined in a header,
// before the template body.
// This is important for disambiguating `<param`> in the sources,
// since they could otherwise refer to other types of parameters.
//
// TAMER generates metavariables during interpolation,
// causing params to be mixed with the body of the template;
// this is reflected in the natural ordering.
// But this would result in a semantically invalid source reconstruction,
// and so we must reorder edges during traversal such that the
// metavariables representing template parameters are visited _before_
// everything else.
#[test]
fn tpl_header_source_order() {
#[rustfmt::skip]
let toks = vec![
PkgStart(S1, spair("/pkg", S1)),
TplStart(S2),
BindIdent(spair("_tpl_", S3)),
// -- Above this line was setup -- //
MetaStart(S4),
BindIdent(spair("@param_before@", S5)),
MetaEnd(S6),
// Dangling (no Ident)
ExprStart(ExprOp::Sum, S7),
ExprEnd(S8),
MetaStart(S9),
BindIdent(spair("@param_after_a@", S10)),
MetaEnd(S11),
MetaStart(S12),
BindIdent(spair("@param_after_b@", S13)),
MetaEnd(S14),
// Reachable (with an Ident)
// (We want to be sure that we're not just hoisting all Idents
// without checking that they're actually Meta
// definitions).
ExprStart(ExprOp::Sum, S15),
BindIdent(spair("sum", S16)),
ExprEnd(S17),
TplEnd(S18),
PkgEnd(S19),
];
// We need more concise expressions for the below table of values.
let d = DynObjectRel::new;
let m = |a: Span, b: Span| a.merge(b).unwrap();
#[rustfmt::skip]
assert_eq!(
// A -|-> B | A span -|-> B span | espan | depth
vec![//-----|-------|-----------|------------|--------|-----------------
(d(Root, Pkg, SU, m(S1, S19), None ), Depth(1)),
(d(Pkg, Ident, m(S1, S19), S3, None ), Depth(2)),
(d(Ident, Tpl, S3, m(S2, S18), None ), Depth(3)),
(d(Tpl, Ident, m(S2, S18), S5, None ), Depth(4)),
(d(Ident, Meta, S5, m(S4, S6), None ), Depth(5)),
// ,-----------------------------------------------------------------------,
(d(Tpl, Ident, m(S2, S18), S10, None ), Depth(4)),
(d(Ident, Meta, S10, m(S9, S11), None ), Depth(5)),
(d(Tpl, Ident, m(S2, S18), S13, None ), Depth(4)),
(d(Ident, Meta, S13, m(S12, S14), None ), Depth(5)),
// '-----------------------------------------------------------------------'
(d(Tpl, Expr, m(S2, S18), m(S7, S8), None ), Depth(4)),
(d(Tpl, Ident, m(S2, S18), S16, None ), Depth(4)),
(d(Ident, Expr, S16, m(S15, S17), None ), Depth(5)),
],
// ^ The enclosed Ident->Meta pairs above have been hoisted out of
// the body and into the header of `Tpl`.
// This is a stable, partial ordering;
// elements do not change poisitions relative to one-another
// with the exception of hoisting.
// This means that all hoisted params retain their order relative
// to other params,
// and all objects in the body retain their positions relative
// to other objects in the body.
tree_reconstruction_report(toks),
);
}

View File

@ -54,7 +54,7 @@ use crate::{
},
};
use arrayvec::ArrayVec;
use std::{convert::Infallible, fmt::Display, marker::PhantomData};
use std::{fmt::Display, marker::PhantomData};
#[derive(Debug, PartialEq, Eq)]
pub enum AsgTreeToXirf<'a> {
@ -75,10 +75,14 @@ impl<'a> Display for AsgTreeToXirf<'a> {
type Xirf = XirfToken<Text>;
diagnostic_infallible! {
pub enum AsgTreeToXirfError {}
}
impl<'a> ParseState for AsgTreeToXirf<'a> {
type Token = TreeWalkRel;
type Object = Xirf;
type Error = Infallible;
type Error = AsgTreeToXirfError;
type Context = TreeContext<'a>;
fn parse_token(
@ -150,6 +154,10 @@ type TokenStack = ArrayVec<Xirf, TOK_STACK_SIZE>;
pub struct TreeContext<'a> {
stack: TokenStack,
asg: &'a Asg,
/// Whether the most recently encountered template has been interpreted
/// as an application.
tpl_apply: Option<ObjectIndex<Tpl>>,
}
impl<'a> TreeContext<'a> {
@ -185,18 +193,30 @@ impl<'a> TreeContext<'a> {
),
},
// Identifiers will be considered in context;
// pass over it for now.
// But we must not skip over its depth,
// otherwise we parent a following sibling at a matching
// depth;
// this close will force the auto-closing system to close
// any siblings in preparation for the object to follow.
Object::Ident((ident, _)) => Some(Xirf::Close(
None,
CloseSpan::without_name_span(ident.span()),
depth,
)),
Object::Ident((ident, oi_ident)) => match paired_rel.source() {
Object::Meta(..) => {
self.emit_tpl_param_value(ident, oi_ident, depth)
}
// All other identifiers will be considered in context;
// pass over it for now.
// But we must not skip over its depth,
// otherwise we parent a following sibling at a matching
// depth;
// this close will force the auto-closing system to
// close any siblings in preparation for the object to
// follow.
Object::Root(..)
| Object::Pkg(..)
| Object::Ident(..)
| Object::Expr(..)
| Object::Tpl(..)
| Object::Doc(..) => Some(Xirf::Close(
None,
CloseSpan::without_name_span(ident.span()),
depth,
)),
},
Object::Expr((expr, oi_expr)) => {
self.emit_expr(expr, *oi_expr, paired_rel.source(), depth)
@ -206,9 +226,10 @@ impl<'a> TreeContext<'a> {
self.emit_template(tpl, *oi_tpl, paired_rel.source(), depth)
}
Object::Meta((meta, oi_meta)) => {
self.emit_tpl_arg(meta, *oi_meta, depth)
}
Object::Meta((meta, oi_meta)) => match self.tpl_apply {
Some(_) => self.emit_tpl_arg(meta, *oi_meta, depth),
None => self.emit_tpl_param(meta, *oi_meta, depth),
},
Object::Doc((doc, oi_doc)) => {
self.emit_doc(doc, *oi_doc, paired_rel.source(), depth)
@ -273,8 +294,9 @@ impl<'a> TreeContext<'a> {
depth: Depth,
) -> Option<Xirf> {
match src {
Object::Ident((ident, _)) => {
self.emit_expr_ident(expr, ident, depth)
Object::Ident((_, oi_ident)) => {
let name = oi_ident.name_or_meta(self.asg);
self.emit_expr_ident(expr, name, depth)
}
Object::Expr((pexpr, _)) => match (pexpr.op(), expr.op()) {
(ExprOp::Conj | ExprOp::Disj, ExprOp::Eq) => {
@ -306,7 +328,7 @@ impl<'a> TreeContext<'a> {
fn emit_expr_ident(
&mut self,
expr: &Expr,
ident: &Ident,
name: SPair,
depth: Depth,
) -> Option<Xirf> {
let (qname, ident_qname) = match expr.op() {
@ -322,8 +344,8 @@ impl<'a> TreeContext<'a> {
}
};
let ispan = ident.span();
self.push(Xirf::attr(ident_qname, ident.name(), (ispan, ispan)));
let span = name.span();
self.push(Xirf::attr(ident_qname, name, (span, span)));
Some(Xirf::open(
qname,
@ -344,21 +366,15 @@ impl<'a> TreeContext<'a> {
let mut edges = oi_expr.edges_filtered::<Ident>(self.asg);
// note: the edges are reversed (TODO?)
let value = edges
.next()
.diagnostic_expect(
|| vec![oi_expr.note("for this match")],
"missing @value ref",
)
.resolve(self.asg);
let value = edges.next().diagnostic_expect(
|| vec![oi_expr.note("for this match")],
"missing @value ref",
);
let on = edges
.next()
.diagnostic_expect(
|| vec![oi_expr.note("for this match")],
"missing @on ref",
)
.resolve(self.asg);
let on = edges.next().diagnostic_expect(
|| vec![oi_expr.note("for this match")],
"missing @on ref",
);
if let Some(unexpected) = edges.next() {
diagnostic_panic!(
@ -367,8 +383,8 @@ impl<'a> TreeContext<'a> {
);
}
self.push(attr_value(value.name()));
self.push(attr_on(on.name()));
self.push(attr_value(value.name_or_meta(self.asg)));
self.push(attr_on(on.name_or_meta(self.asg)));
Xirf::open(QN_MATCH, OpenSpan::without_name_span(expr.span()), depth)
}
@ -382,8 +398,9 @@ impl<'a> TreeContext<'a> {
depth: Depth,
) -> Option<Xirf> {
match src {
Object::Ident((ident, _)) => {
self.push(attr_name(ident.name()));
Object::Ident((_, oi_ident)) => {
self.tpl_apply = None;
self.push(attr_name(oi_ident.name_or_meta(self.asg)));
Some(Xirf::open(
QN_TEMPLATE,
@ -400,6 +417,12 @@ impl<'a> TreeContext<'a> {
// do not have to deal with converting underscore-padded
// template names back into short-hand form.
Object::Pkg(..) | Object::Tpl(..) | Object::Expr(..) => {
// This really ought to be a state transition;
// this is a sheer act of laziness.
// If we introduce states for other things,
// let's convert this as well.
self.tpl_apply = Some(oi_tpl);
// [`Ident`]s are skipped during traversal,
// so we'll handle it ourselves here.
// This also gives us the opportunity to make sure that
@ -422,7 +445,7 @@ impl<'a> TreeContext<'a> {
"cannot derive name of template for application",
);
self.push(attr_name(apply_tpl.resolve(self.asg).name()));
self.push(attr_name(apply_tpl.name_or_meta(self.asg)));
Some(Xirf::open(
QN_APPLY_TEMPLATE,
@ -440,6 +463,99 @@ impl<'a> TreeContext<'a> {
}
}
/// Emit a metavariable as a template parameter.
///
/// For the parent template,
/// see [`Self::emit_template`].
fn emit_tpl_param(
&mut self,
meta: &Meta,
oi_meta: ObjectIndex<Meta>,
depth: Depth,
) -> Option<Xirf> {
if let Some(pname) =
oi_meta.ident(self.asg).map(|oi| oi.name_or_meta(self.asg))
{
// This may have a body that is a single lexeme,
// representing a default value for the parameter.
if let Some(lexeme) = meta.lexeme() {
let open = self.emit_text_node(
lexeme,
lexeme.span(),
depth.child_depth(),
);
self.push(open);
}
// Because of the above,
// we must check here if we have a description rather than
// waiting to encounter it during the traversal;
// otherwise we'd be outputting child nodes before a
// description attribute,
// which would result in invalid XML that is rejected by
// the XIR writer.
if let Some(desc_short) = oi_meta.desc_short(self.asg) {
self.push(attr_desc(desc_short));
}
self.push(attr_name(pname));
Some(Xirf::open(
QN_PARAM,
OpenSpan::without_name_span(meta.span()),
depth,
))
} else if let Some(lexeme) = meta.lexeme() {
Some(self.emit_text_node(lexeme, meta.span(), depth))
} else {
// TODO: Rewrite the above to be an exhaustive match, perhaps,
// so we know what we'll error on.
diagnostic_todo!(
vec![oi_meta.internal_error("unsupported Meta type")],
"xmli output does not yet support this Meta object",
)
}
}
/// Emit a `<text>` node containing a lexeme.
fn emit_text_node(
&mut self,
lexeme: SPair,
node_span: Span,
depth: Depth,
) -> Xirf {
self.push(Xirf::close(
Some(QN_TEXT),
CloseSpan::without_name_span(node_span),
depth,
));
self.push(Xirf::text(
Text(lexeme.symbol(), lexeme.span()),
depth.child_depth(),
));
Xirf::open(QN_TEXT, OpenSpan::without_name_span(node_span), depth)
}
/// Emit a `<param-value>` node assumed to be within a template param
/// body.
fn emit_tpl_param_value(
&mut self,
ident: &Ident,
oi_ident: &ObjectIndex<Ident>,
depth: Depth,
) -> Option<Xirf> {
let name = oi_ident.name_or_meta(self.asg);
self.push(attr_name(name));
Some(Xirf::open(
QN_PARAM_VALUE,
OpenSpan::without_name_span(ident.span()),
depth,
))
}
/// Emit a long-form template argument.
///
/// For the parent template application,
@ -450,7 +566,7 @@ impl<'a> TreeContext<'a> {
oi_meta: ObjectIndex<Meta>,
depth: Depth,
) -> Option<Xirf> {
let pname = oi_meta.ident(self.asg).map(Ident::name)
let pname = oi_meta.ident(self.asg).map(|oi| oi.name_or_meta(self.asg))
.diagnostic_unwrap(|| vec![meta.internal_error(
"anonymous metavariables are not supported as template arguments"
)]);
@ -497,6 +613,14 @@ impl<'a> TreeContext<'a> {
Some(attr_desc(*desc))
}
// template/param/@desc
(Object::Meta(_), Doc::IndepClause(_desc))
if self.tpl_apply.is_none() =>
{
// This is already covered in `emit_tpl_param`
None
}
(_, Doc::Text(_text)) => {
// TODO: This isn't utilized by the XSLT parser and
// `xmllint` for system tests does not format with mixed
@ -560,6 +684,7 @@ impl<'a> From<&'a Asg> for TreeContext<'a> {
TreeContext {
stack: Default::default(),
asg,
tpl_apply: None,
}
}
}

View File

@ -74,7 +74,7 @@ pub use graph::{
ObjectKind,
},
visit,
xmli::AsgTreeToXirf,
xmli::{AsgTreeToXirf, AsgTreeToXirfError},
Asg, AsgResult, IndexType,
};

View File

@ -20,6 +20,7 @@
// Use your judgment;
// a `match` may be more clear within a given context.
#![allow(clippy::single_match)]
#![feature(assert_matches)]
//! This is the TAME compiler.
//!
@ -39,27 +40,49 @@ use std::{
path::Path,
};
use tamer::{
asg::{air::Air, AsgError, DefaultAsg},
asg::DefaultAsg,
diagnose::{
AnnotatedSpan, Diagnostic, FsSpanResolver, Reporter, VisualReporter,
},
nir::{InterpError, Nir, NirToAirError, XirfToNirError},
parse::{lowerable, FinalizeError, ParseError, Token, UnknownToken},
pipeline::parse_package_xml,
xir::{
self,
flat::{RefinedText, XirToXirfError, XirfToken},
reader::XmlXirReader,
DefaultEscaper, Token as XirToken,
},
nir::NirToAirParseType,
parse::{lowerable, FinalizeError, ParseError, Token},
pipeline::{parse_package_xml, LowerXmliError, ParsePackageXmlError},
xir::{self, reader::XmlXirReader, writer::XmlWriter, DefaultEscaper},
};
/// Types of commands
#[derive(Debug, PartialEq)]
enum Command {
Compile(String, String, String),
Compile(String, ObjectFileKind, String),
Usage,
}
/// The type of object file to output.
///
/// While TAMER is under development,
/// object files serve as a transition between the new compiler and the
/// old.
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
enum ObjectFileKind {
/// Produce something akin to an object file.
///
/// During TAME's development,
/// this is an `xmli` file that is passed to the old compiler to pick
/// up where this one left off.
///
/// This is the stable feature set,
/// expected to work with any package.
XmloStable,
/// Enable experimental flag(s),
/// attempting to build the given package with an system that has not
/// yet stabalized and is bound to fail on some packages.
///
/// This is intentionally vague.
/// It should be used only for testing.
XmloExperimental,
}
/// Create a [`XmlXirReader`] for a source file.
///
/// The provided escaper must be shared between all readers and writers in
@ -84,18 +107,15 @@ fn src_reader<'a>(
/// transition period between the XSLT-based TAME and TAMER.
/// Writing XIR proves that the source file is being successfully parsed and
/// helps to evaluate system performance.
#[cfg(not(feature = "wip-asg-derived-xmli"))]
fn copy_xml_to<'e, W: io::Write + 'e>(
mut fout: W,
mut fout: Option<W>,
escaper: &'e DefaultEscaper,
) -> impl FnMut(&Result<XirToken, tamer::xir::Error>) + 'e {
use tamer::xir::writer::XmlWriter;
) -> impl FnMut(&Result<tamer::xir::Token, tamer::xir::Error>) + 'e {
let mut xmlwriter = Default::default();
move |tok_result| match tok_result {
Ok(tok) => {
xmlwriter = tok.write(&mut fout, xmlwriter, escaper).unwrap();
move |tok_result| match (fout.as_mut(), tok_result) {
(Some(mut dest), Ok(tok)) => {
xmlwriter = tok.write(&mut dest, xmlwriter, escaper).unwrap();
}
_ => (),
}
@ -110,16 +130,34 @@ fn compile<R: Reporter>(
src_path: &String,
dest_path: &String,
reporter: &mut R,
kind: ObjectFileKind,
) -> Result<(), UnrecoverableError> {
let dest = Path::new(&dest_path);
#[allow(unused_mut)] // wip-asg-derived-xmli
let mut fout = BufWriter::new(fs::File::create(dest)?);
let (fcopy, fout, parse_type) = match kind {
// Parse XML and re-emit into target verbatim
// (but missing some formatting).
// Tokens will act as no-ops after NIR.
ObjectFileKind::XmloStable => (
Some(BufWriter::new(fs::File::create(dest)?)),
None,
NirToAirParseType::Noop,
),
// Parse sources into ASG and re-generate sources from there.
// This will fail if the source package utilize features that are
// not yet supported.
ObjectFileKind::XmloExperimental => (
None,
Some(BufWriter::new(fs::File::create(dest)?)),
NirToAirParseType::LowerKnownErrorRest,
),
};
let escaper = DefaultEscaper::default();
let mut ebuf = String::new();
let report_err = |result: Result<(), RecoverableError>| {
let report_err = |result: Result<(), ParsePackageXmlError<_>>| {
result.or_else(|e| {
// See below note about buffering.
ebuf.clear();
@ -130,44 +168,26 @@ fn compile<R: Reporter>(
})
};
// TODO: We're just echoing back out XIR,
// which will be the same sans some formatting.
let src = &mut lowerable(src_reader(src_path, &escaper)?.inspect({
#[cfg(not(feature = "wip-asg-derived-xmli"))]
{
copy_xml_to(fout, &escaper)
}
#[cfg(feature = "wip-asg-derived-xmli")]
{
|_| ()
}
}));
let src = &mut lowerable(
src_reader(src_path, &escaper)?.inspect(copy_xml_to(fcopy, &escaper)),
);
// TODO: Determine a good default capacity once we have this populated
// and can come up with some heuristics.
let (air_ctx,) = parse_package_xml(
src,
let (_, air_ctx) = parse_package_xml(
parse_type,
DefaultAsg::with_capacity(1024, 2048),
report_err,
)?;
)(src, report_err)?;
match reporter.has_errors() {
false => {
#[cfg(feature = "wip-asg-derived-xmli")]
{
let asg = air_ctx.finish();
derive_xmli(asg, fout, &escaper)
}
#[cfg(not(feature = "wip-asg-derived-xmli"))]
{
let _ = air_ctx; // unused_variables
Ok(())
}
}
true => Err(UnrecoverableError::ErrorsDuringLowering(
if reporter.has_errors() {
Err(UnrecoverableError::ErrorsDuringLowering(
reporter.error_count(),
)),
))
} else if let Some(dest) = fout {
let asg = air_ctx.finish();
derive_xmli(asg, dest, &escaper)
} else {
Ok(())
}
}
@ -181,16 +201,13 @@ fn compile<R: Reporter>(
/// and must be an equivalent program,
/// but will look different;
/// TAMER reasons about the system using a different paradigm.
#[cfg(feature = "wip-asg-derived-xmli")]
fn derive_xmli(
asg: tamer::asg::Asg,
mut fout: impl std::io::Write,
escaper: &DefaultEscaper,
) -> Result<(), UnrecoverableError> {
use tamer::{
asg::visit::tree_reconstruction,
pipeline,
xir::writer::{WriterState, XmlWriter},
asg::visit::tree_reconstruction, pipeline, xir::writer::WriterState,
};
let src = lowerable(tree_reconstruction(&asg).map(Ok));
@ -198,15 +215,17 @@ fn derive_xmli(
// TODO: Remove bad file?
// Let make do it?
let mut st = WriterState::default();
let (_asg,) = pipeline::lower_xmli(src, &asg, |result| {
let (_asg,) = pipeline::lower_xmli(&asg)(src, |result| {
// Write failures should immediately bail out;
// we can't skip writing portions of the file and
// just keep going!
result.and_then(|tok| {
tok.write(&mut fout, st, escaper)
.map(|newst| st = newst)
.map_err(Into::<UnrecoverableError>::into)
})
result
.map_err(Into::<UnrecoverableError>::into)
.and_then(|tok| {
tok.write(&mut fout, st, escaper)
.map(|newst| st = newst)
.map_err(Into::<UnrecoverableError>::into)
})
})?;
Ok(())
@ -220,10 +239,10 @@ pub fn main() -> Result<(), UnrecoverableError> {
let usage = opts.usage(&format!("Usage: {program} [OPTIONS] INPUT"));
match parse_options(opts, args) {
Ok(Command::Compile(src_path, _, dest_path)) => {
Ok(Command::Compile(src_path, kind, dest_path)) => {
let mut reporter = VisualReporter::new(FsSpanResolver);
compile(&src_path, &dest_path, &mut reporter).map_err(
compile(&src_path, &dest_path, &mut reporter, kind).map_err(
|e: UnrecoverableError| {
// Rendering to a string ensures buffering so that we
// don't interleave output between processes.
@ -285,15 +304,12 @@ fn parse_options(opts: Options, args: Vec<String>) -> Result<Command, Fail> {
let emit = match matches.opt_str("emit") {
Some(m) => match &m[..] {
"xmlo" => m,
_ => {
return Err(Fail::ArgumentMissing(String::from("--emit xmlo")))
}
"xmlo" => Ok(ObjectFileKind::XmloStable),
"xmlo-experimental" => Ok(ObjectFileKind::XmloExperimental),
_ => Err(Fail::ArgumentMissing(String::from("--emit xmlo"))),
},
None => {
return Err(Fail::OptionMissing(String::from("--emit xmlo")));
}
};
None => Err(Fail::OptionMissing(String::from("--emit xmlo"))),
}?;
let output = match matches.opt_str("o") {
Some(m) => m,
@ -309,14 +325,20 @@ fn parse_options(opts: Options, args: Vec<String>) -> Result<Command, Fail> {
///
/// These are errors that will result in aborting execution and exiting with
/// a non-zero status.
/// Contrast this with [`RecoverableError`],
/// Contrast this with recoverable errors in [`tamer::pipeline`],
/// which is reported real-time to the user and _does not_ cause the
/// program to abort until the end of the compilation unit.
///
/// Note that an recoverable error,
/// under a normal compilation strategy,
/// will result in an [`UnrecoverableError::ErrorsDuringLowering`] at the
/// end of the compilation unit.
#[derive(Debug)]
pub enum UnrecoverableError {
Io(io::Error),
Fmt(fmt::Error),
XirWriterError(xir::writer::Error),
LowerXmliError(LowerXmliError<Infallible>),
ErrorsDuringLowering(ErrorCount),
FinalizeError(FinalizeError),
}
@ -327,37 +349,6 @@ pub enum UnrecoverableError {
/// have in your code.
type ErrorCount = usize;
/// An error that occurs during the lowering pipeline that may be recovered
/// from to continue parsing and collection of additional errors.
///
/// This represents the aggregation of all possible errors that can occur
/// during lowering.
/// This cannot include panics,
/// but efforts have been made to reduce panics to situations that
/// represent the equivalent of assertions.
///
/// These errors are distinct from [`UnrecoverableError`],
/// which represents the errors that could be returned to the toplevel
/// `main`,
/// because these errors are intended to be reported to the user _and then
/// recovered from_ so that compilation may continue and more errors may
/// be collected;
/// nobody wants a compiler that reports one error at a time.
///
/// Note that an recoverable error,
/// under a normal compilation strategy,
/// will result in an [`UnrecoverableError::ErrorsDuringLowering`] at the
/// end of the compilation unit.
#[derive(Debug)]
pub enum RecoverableError {
XirParseError(ParseError<UnknownToken, xir::Error>),
XirfParseError(ParseError<XirToken, XirToXirfError>),
NirParseError(ParseError<XirfToken<RefinedText>, XirfToNirError>),
InterpError(ParseError<Nir, InterpError>),
NirToAirError(ParseError<Nir, NirToAirError>),
AirAggregateError(ParseError<Air, AsgError>),
}
impl From<io::Error> for UnrecoverableError {
fn from(e: io::Error) -> Self {
Self::Io(e)
@ -376,15 +367,15 @@ impl From<xir::writer::Error> for UnrecoverableError {
}
}
impl From<FinalizeError> for UnrecoverableError {
fn from(e: FinalizeError) -> Self {
Self::FinalizeError(e)
impl From<LowerXmliError<Infallible>> for UnrecoverableError {
fn from(e: LowerXmliError<Infallible>) -> Self {
Self::LowerXmliError(e)
}
}
impl From<Infallible> for UnrecoverableError {
fn from(_: Infallible) -> Self {
unreachable!("<UnrecoverableError as From<Infallible>>::from")
impl From<FinalizeError> for UnrecoverableError {
fn from(e: FinalizeError) -> Self {
Self::FinalizeError(e)
}
}
@ -396,52 +387,6 @@ impl<T: Token> From<ParseError<T, Infallible>> for UnrecoverableError {
}
}
impl<T: Token> From<ParseError<T, Infallible>> for RecoverableError {
fn from(_: ParseError<T, Infallible>) -> Self {
unreachable!(
"<RecoverableError as From<ParseError<T, Infallible>>>::from"
)
}
}
impl From<ParseError<UnknownToken, xir::Error>> for RecoverableError {
fn from(e: ParseError<UnknownToken, xir::Error>) -> Self {
Self::XirParseError(e)
}
}
impl From<ParseError<XirToken, XirToXirfError>> for RecoverableError {
fn from(e: ParseError<XirToken, XirToXirfError>) -> Self {
Self::XirfParseError(e)
}
}
impl From<ParseError<XirfToken<RefinedText>, XirfToNirError>>
for RecoverableError
{
fn from(e: ParseError<XirfToken<RefinedText>, XirfToNirError>) -> Self {
Self::NirParseError(e)
}
}
impl From<ParseError<Nir, InterpError>> for RecoverableError {
fn from(e: ParseError<Nir, InterpError>) -> Self {
Self::InterpError(e)
}
}
impl From<ParseError<Nir, NirToAirError>> for RecoverableError {
fn from(e: ParseError<Nir, NirToAirError>) -> Self {
Self::NirToAirError(e)
}
}
impl From<ParseError<Air, AsgError>> for RecoverableError {
fn from(e: ParseError<Air, AsgError>) -> Self {
Self::AirAggregateError(e)
}
}
impl Display for UnrecoverableError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
use UnrecoverableError::*;
@ -449,6 +394,7 @@ impl Display for UnrecoverableError {
match self {
Io(e) => Display::fmt(e, f),
Fmt(e) => Display::fmt(e, f),
LowerXmliError(e) => Display::fmt(e, f),
XirWriterError(e) => Display::fmt(e, f),
FinalizeError(e) => Display::fmt(e, f),
@ -460,55 +406,14 @@ impl Display for UnrecoverableError {
}
}
impl Display for RecoverableError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
use RecoverableError::*;
match self {
XirParseError(e) => Display::fmt(e, f),
XirfParseError(e) => Display::fmt(e, f),
NirParseError(e) => Display::fmt(e, f),
InterpError(e) => Display::fmt(e, f),
NirToAirError(e) => Display::fmt(e, f),
AirAggregateError(e) => Display::fmt(e, f),
}
}
}
impl Error for UnrecoverableError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
use UnrecoverableError::*;
match self {
Io(e) => Some(e),
Fmt(e) => Some(e),
XirWriterError(e) => Some(e),
ErrorsDuringLowering(_) => None,
FinalizeError(e) => Some(e),
}
}
}
impl Error for RecoverableError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
use RecoverableError::*;
match self {
XirParseError(e) => Some(e),
XirfParseError(e) => Some(e),
NirParseError(e) => Some(e),
InterpError(e) => Some(e),
NirToAirError(e) => Some(e),
AirAggregateError(e) => Some(e),
}
}
}
impl Error for UnrecoverableError {}
impl Diagnostic for UnrecoverableError {
fn describe(&self) -> Vec<AnnotatedSpan> {
use UnrecoverableError::*;
match self {
LowerXmliError(e) => e.describe(),
FinalizeError(e) => e.describe(),
// Fall back to `Display`
@ -519,24 +424,10 @@ impl Diagnostic for UnrecoverableError {
}
}
impl Diagnostic for RecoverableError {
fn describe(&self) -> Vec<AnnotatedSpan> {
use RecoverableError::*;
match self {
XirParseError(e) => e.describe(),
XirfParseError(e) => e.describe(),
NirParseError(e) => e.describe(),
InterpError(e) => e.describe(),
NirToAirError(e) => e.describe(),
AirAggregateError(e) => e.describe(),
}
}
}
#[cfg(test)]
mod test {
use super::*;
use std::assert_matches::assert_matches;
#[test]
fn parse_options_help() {
@ -670,7 +561,7 @@ mod test {
Ok(Command::Compile(infile, xmlo, outfile)) => {
assert_eq!("foo.xml", infile);
assert_eq!("foo.xmlo", outfile);
assert_eq!("xmlo", xmlo);
assert_eq!(ObjectFileKind::XmloStable, xmlo);
}
_ => panic!("Unexpected result"),
}
@ -696,7 +587,7 @@ mod test {
Ok(Command::Compile(infile, xmlo, outfile)) => {
assert_eq!("foo.xml", infile);
assert_eq!("foo.xmli", outfile);
assert_eq!("xmlo", xmlo);
assert_eq!(ObjectFileKind::XmloStable, xmlo);
}
_ => panic!("Unexpected result"),
}
@ -722,9 +613,31 @@ mod test {
Ok(Command::Compile(infile, xmlo, outfile)) => {
assert_eq!("foo.xml", infile);
assert_eq!("foo.xmli", outfile);
assert_eq!("xmlo", xmlo);
assert_eq!(ObjectFileKind::XmloStable, xmlo);
}
_ => panic!("Unexpected result"),
}
}
#[test]
fn parse_options_xmlo_experimetal() {
let opts = get_opts();
let xmlo = String::from("xmlo-experimental");
let result = parse_options(
opts,
vec![
String::from("program"),
String::from("foo.xml"),
String::from("--emit"),
xmlo,
String::from("--output"),
String::from("foo.xmli"),
],
);
assert_matches!(
result,
Ok(Command::Compile(_, ObjectFileKind::XmloExperimental, _)),
);
}
}

View File

@ -332,3 +332,55 @@ impl<S: Into<Span>> Annotate for S {
AnnotatedSpan(self.into(), level, label)
}
}
/// Generate a variant-less error enum akin to [`Infallible`].
///
/// This is used to create [`Infallible`]-like newtypes where unique error
/// types are beneficial.
/// For example,
/// this can be used so that [`From`] implementations can be exclusively
/// used to widen errors
/// (or lack thereof)
/// into error sum variants,
/// and is especially useful when code generation is involved to avoid
/// generation of overlapping [`From`] `impl`s.
///
/// The generated enum is convertable [`Into`] and [`From`] [`Infallible`].
macro_rules! diagnostic_infallible {
($vis:vis enum $name:ident {}) => {
/// A unique [`Infallible`](std::convert::Infallible) type.
#[derive(Debug, PartialEq)]
$vis enum $name {}
impl std::fmt::Display for $name {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, stringify!($name))
}
}
impl $crate::diagnose::Diagnostic for $name {
fn describe(&self) -> Vec<$crate::diagnose::AnnotatedSpan> {
// This is a unit struct and should not be able to be
// instantiated!
unreachable!(
concat!(
stringify!($name),
" should be unreachable!"
)
)
}
}
impl From<std::convert::Infallible> for $name {
fn from(_: std::convert::Infallible) -> Self {
unreachable!()
}
}
impl From<$name> for std::convert::Infallible {
fn from(_: $name) -> Self {
unreachable!()
}
}
}
}

View File

@ -33,11 +33,12 @@
/// A type providing a `map` function from inner type `T` to `U`.
///
/// In an abuse of terminology,
/// this functor is polymorphic over the entire trait,
/// This used to be called `Functor`,
/// but was renamed because it was an abuse of terminology;
/// this is polymorphic over the entire trait,
/// rather than just the definition of `map`,
/// allowing implementations to provide multiple specialized `map`s
/// without having to define individual `map_*` methods.
/// allowing implementations to provide multiple specialized `map`s
/// without having to define individual `map_*` methods.
/// Rust will often be able to infer the proper types and invoke the
/// intended `map` function within a given context,
/// but methods may still be explicitly disambiguated using the
@ -46,16 +47,16 @@
/// if a functor requires a monomorphic function
/// (so `T = U`),
/// then it's not really a functor.
/// We'll refer to these structures informally as monomorphic functors,
/// since they provide the same type of API as a functor,
/// but cannot change the underlying type.
///
/// This trait also provides a number of convenience methods that can be
/// implemented in terms of [`Functor::map`].
/// implemented in terms of [`Map::map`].
///
/// If a mapping can fail,
/// see [`TryMap`].
///
/// Why a primitive `map` instead of `fmap`?
/// ========================================
/// One of the methods of this trait is [`Functor::fmap`],
/// One of the methods of this trait is [`Map::fmap`],
/// which [is motivated by Haskell][haskell-functor].
/// This trait implements methods in terms of [`map`](Self::map) rather than
/// [`fmap`](Self::fmap) because `map` is a familiar idiom in Rust and
@ -66,8 +67,8 @@
/// which is additional boilerplate relative to `map`.
///
/// [haskell-functor]: https://hackage.haskell.org/package/base/docs/Data-Functor.html
pub trait Functor<T, U = T>: Sized {
/// Type of object resulting from [`Functor::map`] operation.
pub trait Map<T, U = T>: Sized {
/// Type of object resulting from [`Map::map`] operation.
///
/// The term "target" originates from category theory,
/// representing the codomain of the functor.
@ -83,7 +84,7 @@ pub trait Functor<T, U = T>: Sized {
/// all others are implemented in terms of it.
fn map(self, f: impl FnOnce(T) -> U) -> Self::Target;
/// Curried form of [`Functor::map`],
/// Curried form of [`Map::map`],
/// with arguments reversed.
///
/// `fmap` returns a unary closure that accepts an object of type
@ -106,26 +107,153 @@ pub trait Functor<T, U = T>: Sized {
///
/// This is intended for cases where there's a single element that will
/// be replaced,
/// taking advantage of [`Functor`]'s trait-level polymorphism.
/// taking advantage of [`Map`]'s trait-level polymorphism.
fn overwrite(self, value: U) -> Self::Target {
self.map(|_| value)
}
/// Curried form of [`Functor::overwrite`],
/// Curried form of [`Map::overwrite`],
/// with arguments reversed.
fn foverwrite(value: U) -> impl FnOnce(Self) -> Self::Target {
move |x| x.overwrite(value)
}
}
impl<T, U> Functor<T, U> for Option<T> {
impl<T, U> Map<T, U> for Option<T> {
type Target = Option<U>;
fn map(self, f: impl FnOnce(T) -> U) -> <Self as Functor<T, U>>::Target {
fn map(self, f: impl FnOnce(T) -> U) -> <Self as Map<T, U>>::Target {
Option::map(self, f)
}
}
/// A type providing a `try_map` function from inner type `T` to `U`.
///
/// This is a fallible version of [`Map`];
/// see that trait for more information.
pub trait TryMap<T, U = T>: Sized {
/// Type of object resulting from [`TryMap::try_map`] operation.
///
/// The term "target" originates from category theory,
/// representing the codomain of the functor.
type Target = Self;
/// Result of the mapping function.
type FnResult<E> = Result<T, (T, E)>;
/// Result of the entire map operation.
type Result<E> = Result<Self, (Self, E)>;
/// A structure-preserving map between types `T` and `U`.
///
/// This unwraps any number of `T` from `Self` and applies the
/// function `f` to transform it into `U`,
/// wrapping the result back up into [`Self`].
///
/// Since this method takes ownership over `self` rather than a mutable
/// reference,
/// [`Self::FnResult`] is expected to return some version of `T`
/// alongside the error `E`;
/// this is usually the original `self`,
/// but does not have to be.
/// Similarly,
/// [`Self::Result`] will also return [`Self`] in the event of an
/// error.
///
/// This is the only method that needs to be implemented on this trait;
/// all others are implemented in terms of it.
fn try_map<E>(
self,
f: impl FnOnce(T) -> Self::FnResult<E>,
) -> Self::Result<E>;
/// Curried form of [`TryMap::try_map`],
/// with arguments reversed.
///
/// `try_fmap` returns a unary closure that accepts an object of type
/// [`Self`].
/// This is more amenable to function composition and a point-free style.
fn try_fmap<E>(
f: impl FnOnce(T) -> Self::FnResult<E>,
) -> impl FnOnce(Self) -> Self::Result<E> {
move |x| x.try_map(f)
}
}
/// Generate monomorphic [`TryMap`] and [`Map`] implementations for the
/// provided type.
///
/// This macro is suitable for otherwise-boilerplate `impl`s for these
/// traits.
/// If you expect anything more than a generic `map` or `try_map` operation,
/// then you should implement the traits manually.
///
/// Only tuple structs are supported at present.
///
/// For example:
///
/// ```
/// # #[macro_use] extern crate tamer;
/// # use tamer::impl_mono_map;
/// # use tamer::f::Map;
/// # fn main() {
/// #[derive(Debug, PartialEq)]
/// struct Foo(u8, Bar);
///
/// #[derive(Debug, PartialEq)]
/// enum Bar { A, B };
///
/// impl_mono_map! {
/// u8 => Foo(@, bar),
/// Bar => Foo(n, @),
/// }
///
/// assert_eq!(Foo(5, Bar::A).overwrite(Bar::B), Foo(5, Bar::B));
/// # }
/// ```
///
/// Each line above generates a pair of `impl`s,
/// each for `Foo`,
/// where `@` represents the tuple item being mapped over.
#[macro_export] // for doc test above
macro_rules! impl_mono_map {
($($t:ty => $tup:ident( $($pre:ident,)* @ $(, $post:ident),* ),)+) => {
$(
impl $crate::f::TryMap<$t> for $tup {
fn try_map<E>(
self,
f: impl FnOnce($t) -> Self::FnResult<E>,
) -> Self::Result<E> {
match self {
Self($($pre,)* x $(, $post),*) => match f(x) {
Ok(y) => Ok(Self($($pre,)* y $(, $post),*)),
Err((y, e)) => Err((
Self($($pre,)* y $(, $post),*),
e,
)),
},
}
}
}
impl $crate::f::Map<$t> for $tup {
fn map(self, f: impl FnOnce($t) -> $t) -> Self::Target {
use std::convert::Infallible;
use $crate::f::TryMap;
// `unwrap()` requires `E: Debug`,
// so this avoids that bound.
match self.try_map::<Infallible>(|x| Ok(f(x))) {
Ok(y) => y,
// Verbosely emphasize unreachability
Err::<_, (_, Infallible)>(_) => unreachable!(),
}
}
}
)+
}
}
/// A nullary [`Fn`] delaying some computation.
///
/// For the history and usage of this term in computing,

View File

@ -26,30 +26,25 @@ use super::xmle::{
XmleSections,
};
use crate::{
asg::{
air::{Air, AirAggregateCtx},
AsgError, DefaultAsg,
},
asg::{air::AirAggregateCtx, DefaultAsg},
diagnose::{AnnotatedSpan, Diagnostic},
fs::{
Filesystem, FsCanonicalizer, PathFile, VisitOnceFile,
VisitOnceFilesystem,
},
ld::xmle::Sections,
obj::xmlo::{XmloAirContext, XmloAirError, XmloError, XmloToken},
parse::{lowerable, FinalizeError, ParseError, UnknownToken},
pipeline,
obj::xmlo::XmloAirContext,
parse::{lowerable, FinalizeError},
pipeline::{self, LoadXmloError},
sym::{GlobalSymbolResolve, SymbolId},
xir::{
flat::{Text, XirToXirfError, XirfToken},
reader::XmlXirReader,
writer::{Error as XirWriterError, XmlWriter},
DefaultEscaper, Error as XirError, Escaper, Token as XirToken,
DefaultEscaper, Error as XirError, Escaper,
},
};
use fxhash::FxBuildHasher;
use std::{
convert::identity,
error::Error,
fmt::{self, Display},
fs,
@ -106,9 +101,10 @@ fn load_xmlo<P: AsRef<Path>, S: Escaper>(
let src = &mut lowerable(XmlXirReader::new(file, escaper, ctx));
let (mut state, mut air_ctx) = pipeline::load_xmlo::<_, TameldError, _>(
src, state, air_ctx, identity,
)?;
let (mut state, mut air_ctx) =
pipeline::load_xmlo(state, air_ctx)(src, |result| {
result.map_err(TameldError::from)
})?;
let mut dir = path;
dir.pop();
@ -159,11 +155,7 @@ fn output_xmle<'a, X: XmleSections<'a>, S: Escaper>(
pub enum TameldError {
Io(NeqIoError),
SortError(SortError),
XirParseError(ParseError<UnknownToken, XirError>),
XirfParseError(ParseError<XirToken, XirToXirfError>),
XmloParseError(ParseError<XirfToken<Text>, XmloError>),
XmloLowerError(ParseError<XmloToken, XmloAirError>),
AirLowerError(ParseError<Air, AsgError>),
LoadXmloError(LoadXmloError<XirError>),
XirWriterError(XirWriterError),
FinalizeError(FinalizeError),
Fmt(fmt::Error),
@ -206,33 +198,9 @@ impl From<SortError> for TameldError {
}
}
impl From<ParseError<UnknownToken, XirError>> for TameldError {
fn from(e: ParseError<UnknownToken, XirError>) -> Self {
Self::XirParseError(e)
}
}
impl From<ParseError<XirfToken<Text>, XmloError>> for TameldError {
fn from(e: ParseError<XirfToken<Text>, XmloError>) -> Self {
Self::XmloParseError(e)
}
}
impl From<ParseError<XirToken, XirToXirfError>> for TameldError {
fn from(e: ParseError<XirToken, XirToXirfError>) -> Self {
Self::XirfParseError(e)
}
}
impl From<ParseError<XmloToken, XmloAirError>> for TameldError {
fn from(e: ParseError<XmloToken, XmloAirError>) -> Self {
Self::XmloLowerError(e)
}
}
impl From<ParseError<Air, AsgError>> for TameldError {
fn from(e: ParseError<Air, AsgError>) -> Self {
Self::AirLowerError(e)
impl From<LoadXmloError<XirError>> for TameldError {
fn from(e: LoadXmloError<XirError>) -> Self {
Self::LoadXmloError(e)
}
}
@ -259,11 +227,7 @@ impl Display for TameldError {
match self {
Self::Io(e) => Display::fmt(e, f),
Self::SortError(e) => Display::fmt(e, f),
Self::XirParseError(e) => Display::fmt(e, f),
Self::XirfParseError(e) => Display::fmt(e, f),
Self::XmloParseError(e) => Display::fmt(e, f),
Self::XmloLowerError(e) => Display::fmt(e, f),
Self::AirLowerError(e) => Display::fmt(e, f),
Self::LoadXmloError(e) => Display::fmt(e, f),
Self::XirWriterError(e) => Display::fmt(e, f),
Self::FinalizeError(e) => Display::fmt(e, f),
Self::Fmt(e) => Display::fmt(e, f),
@ -271,31 +235,12 @@ impl Display for TameldError {
}
}
impl Error for TameldError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
Self::Io(e) => Some(e),
Self::SortError(e) => Some(e),
Self::XirParseError(e) => Some(e),
Self::XirfParseError(e) => Some(e),
Self::XmloParseError(e) => Some(e),
Self::XmloLowerError(e) => Some(e),
Self::AirLowerError(e) => Some(e),
Self::XirWriterError(e) => Some(e),
Self::FinalizeError(e) => Some(e),
Self::Fmt(e) => Some(e),
}
}
}
impl Error for TameldError {}
impl Diagnostic for TameldError {
fn describe(&self) -> Vec<AnnotatedSpan> {
match self {
Self::XirParseError(e) => e.describe(),
Self::XirfParseError(e) => e.describe(),
Self::XmloParseError(e) => e.describe(),
Self::XmloLowerError(e) => e.describe(),
Self::AirLowerError(e) => e.describe(),
Self::LoadXmloError(e) => e.describe(),
Self::FinalizeError(e) => e.describe(),
Self::SortError(e) => e.describe(),

View File

@ -105,12 +105,13 @@ fn graph_sort() -> SortResult<()> {
let asg = asg_from_toks(toks);
let sections = sort(&asg, StubSections { pushed: Vec::new() })?;
let expected = vec![
let expected = [
// Post-order
name_a_dep_dep,
name_a_dep,
name_a,
]
.map(Some)
.into_iter()
.collect::<Vec<_>>();

View File

@ -113,10 +113,10 @@ impl<'a> XmleSections<'a> for Sections<'a> {
fn push(&mut self, ident: &'a Ident) -> PushResult {
self.deps.push(ident);
let name = ident.name();
let frag = ident.fragment();
let (resolved, name) = ident.resolved()?;
match ident.resolved()?.kind() {
match resolved.kind() {
Some(kind) => match kind {
IdentKind::Cgen(..)
| IdentKind::Gen(..)
@ -156,7 +156,7 @@ impl<'a> XmleSections<'a> for Sections<'a> {
// compiler bug and there is no use in trying to be nice
// about a situation where something went terribly, horribly
// wrong.
return Err(SectionsError::MissingObjectKind(ident.name()));
return Err(SectionsError::MissingObjectKind(name));
}
}

View File

@ -256,7 +256,7 @@ fn test_writes_deps() -> TestResult {
assert_eq!(
attrs.find(QN_NAME).map(|a| a.value()),
Some(ident.name().symbol()),
ident.name().map(|name| name.symbol()),
);
assert_eq!(

View File

@ -23,6 +23,11 @@
//!
//! - [`tamec`](../tamec), the TAME compiler; and
//! - [`tameld`](../tameld), the TAME linker.
//!
//! The [`pipeline`] module contains declarative definitions and
//! documentation for TAMER's _lowering pipelines_;
//! you should start there if you are looking for how a particular
//! component of parsing or code generation is integrated.
// Constant functions are still in their infancy as of the time of writing
// (October 2021).
@ -76,6 +81,8 @@
// If this is not stabalized,
// then we can do without by changing the abstraction;
// this is largely experimentation to see if it's useful.
// See `rust-toolchain.toml` for information on how this blocks more recent
// nightly versions as of 2023-06.
#![allow(incomplete_features)]
#![feature(adt_const_params)]
// Used for traits returning functions,
@ -173,19 +180,88 @@
// which can be inscrutable if you are not very familiar with Rust's
// borrow checker.
#![allow(clippy::needless_lifetimes)]
// Uh oh. Trait specialization, you say?
// This deserves its own section.
//
// Rust has two trait specialization feature flags:
// - min_specialization; and
// - specialization.
//
// Both are unstable,
// but _the latter has soundness holes when it comes to lifetimes_.
// A viable subset of `specialization` was introduced for use in the Rust
// compiler itself,
// dubbed `min_specialization`.
// That hopefully-not-unsound subset is what has been adopted here.
//
// Here's the problem:
// TAMER makes _heavy_ use of the type system for various guarantees,
// operating as proofs.
// This static information means that we're able to determine a lot of
// behavior statically.
// However,
// we also have to support various operations dynamically,
// and marry to the two together.
// The best example of this at the time of writing is AIR,
// which uses static types for graph construction and manipulation
// whenever it can,
// but sometimes has to rely on runtime information to determine which
// types are applicable.
// In that case,
// we have to match on runtime type information and branch into various
// static paths based on that information.
//
// Furthermore,
// this type information often exhibits specialized behavior for certain
// cases,
// and fallback behavior for all others.
//
// This conversion back and fourth in various direction results in either a
// maintenance burden
// (e.g. any time new types or variants are introduced,
// branching code has to be manually updated),
// or complex macros that attempt to generate that code.
// It's all boilerplate,
// and it's messy.
//
// Trait specialization allows for a simple and declarative approach to
// solving these problems without all of the boilerplate;
// the type system can be used to match on relevant types and will fall
// back to specialization in situations where we are not concerned with
// other types.
// In situations where we _do_ want to comprehensively match all types,
// we still have that option in the traditional way.
//
// TAMER will begin to slowly and carefully utilize `min_specialization` in
// isolated areas to experiment with the stability and soundness of the
// system.
// You can search for its uses by searching for `default fn`.
//
// If it is decided to _not_ utilize this feature in the future,
// then specialization must be replaced with burdensome branching code as
// mentioned above.
// It is doable without sacrificing type safety,
// but it makes many changes very time-consuming and therefore very
// expensive.
//
// (At the time of writing,
// there is no clear path to stabalization of this feature.)
#![feature(min_specialization)]
pub mod global;
#[macro_use]
extern crate static_assertions;
#[macro_use]
pub mod f;
#[macro_use]
pub mod diagnose;
#[macro_use]
pub mod xir;
pub mod asg;
pub mod convert;
pub mod diagnose;
pub mod f;
pub mod fmt;
pub mod fs;
pub mod iter;

View File

@ -49,6 +49,7 @@
//! The entry point for NIR in the lowering pipeline is exported as
//! [`XirfToNir`].
mod abstract_bind;
mod air;
mod interp;
mod parse;
@ -56,7 +57,7 @@ mod tplshort;
use crate::{
diagnose::{Annotate, Diagnostic},
f::Functor,
f::Map,
fmt::{DisplayWrapper, TtQuote},
parse::{util::SPair, Object, Token},
span::Span,
@ -72,12 +73,13 @@ use std::{
fmt::{Debug, Display},
};
pub use air::{NirToAir, NirToAirError};
pub use abstract_bind::{AbstractBindTranslate, AbstractBindTranslateError};
pub use air::{NirToAir, NirToAirError, NirToAirParseType};
pub use interp::{InterpError, InterpState as InterpolateNir};
pub use parse::{
NirParseState as XirfToNir, NirParseStateError_ as XirfToNirError,
};
pub use tplshort::TplShortDesugar;
pub use tplshort::{TplShortDesugar, TplShortDesugarError};
/// IR that is "near" the source code.
///
@ -96,12 +98,33 @@ pub enum Nir {
/// Finish definition of a [`NirEntity`] atop of the stack and pop it.
Close(NirEntity, Span),
/// Bind the given name as an identifier for the entity atop of the
/// stack.
/// Bind the given name as a concrete identifier for the entity atop of
/// the stack.
///
/// [`Self::Ref`] references identifiers created using this token.
///
/// See also [`Self::BindIdentAbstract`].
BindIdent(SPair),
/// Bind entity atop of the stack to an abstract identifier whose name
/// will eventually be derived from the metavariable identifier by the
/// given [`SPair`].
///
/// The identifier is intended to become concrete when a lexical value
/// for the metavariable becomes available during expansion,
/// which is outside of the scope of NIR.
///
/// See also [`Self::BindIdent`] for a concrete identifier.
BindIdentAbstract(SPair),
/// The name should be interpreted as a concrete name of a
/// metavariable.
///
/// This is broadly equivalent to [`Self::BindIdent`] but is intended to
/// convey that no NIR operation should ever translate this token into
/// [`Self::BindIdentAbstract`].
BindIdentMeta(SPair),
/// Reference the value of the given identifier as the subject of the
/// current expression.
///
@ -163,15 +186,23 @@ pub enum Nir {
}
impl Nir {
/// Retrieve inner [`SymbolId`] that this token represents,
/// Retrieve a _concrete_ inner [`SymbolId`] that this token represents,
/// if any.
///
/// Not all NIR tokens contain associated symbols;
/// a token's [`SymbolId`] is retained only if it provides additional
/// information over the token itself.
///
/// See also [`Nir::map`] if you wish to change the symbol.
pub fn symbol(&self) -> Option<SymbolId> {
/// An abstract identifier will yield [`None`],
/// since its concrete symbol has yet to be defined;
/// the available symbol instead represents the name of the
/// metavariable from which the concrete symbol will eventually
/// have its value derived.
///
/// See also [`Nir::map`] if you wish to change the symbol,
/// noting however that it does not distinguish between notions of
/// concrete and abstract as this method does.
pub fn concrete_symbol(&self) -> Option<SymbolId> {
use Nir::*;
match self {
@ -180,15 +211,24 @@ impl Nir {
Open(_, _) | Close(_, _) => None,
BindIdent(spair) | RefSubject(spair) | Ref(spair) | Desc(spair)
| Text(spair) | Import(spair) => Some(spair.symbol()),
BindIdent(spair) | BindIdentMeta(spair) | RefSubject(spair)
| Ref(spair) | Desc(spair) | Text(spair) | Import(spair) => {
Some(spair.symbol())
}
// An abstract identifier does not yet have a concrete symbol
// assigned;
// the available symbol represents the metavariable from
// which a symbol will eventually be derived during
// expansion.
BindIdentAbstract(_) => None,
Noop(_) => None,
}
}
}
impl Functor<SymbolId> for Nir {
impl Map<SymbolId> for Nir {
/// Map over a token's [`SymbolId`].
///
/// This allows modifying a token's [`SymbolId`] while retaining the
@ -201,8 +241,7 @@ impl Functor<SymbolId> for Nir {
/// If a token does not contain a symbol,
/// this returns the token unchanged.
///
/// See also [`Nir::symbol`] if you only wish to retrieve the symbol
/// rather than map over it.
/// See also [`Nir::concrete_symbol`].
fn map(self, f: impl FnOnce(SymbolId) -> SymbolId) -> Self {
use Nir::*;
@ -213,6 +252,8 @@ impl Functor<SymbolId> for Nir {
Open(_, _) | Close(_, _) => self,
BindIdent(spair) => BindIdent(spair.map(f)),
BindIdentAbstract(spair) => BindIdentAbstract(spair.map(f)),
BindIdentMeta(spair) => BindIdentMeta(spair.map(f)),
RefSubject(spair) => RefSubject(spair.map(f)),
Ref(spair) => Ref(spair.map(f)),
Desc(spair) => Desc(spair.map(f)),
@ -339,8 +380,14 @@ impl Token for Nir {
Open(_, span) => *span,
Close(_, span) => *span,
BindIdent(spair) | RefSubject(spair) | Ref(spair) | Desc(spair)
| Text(spair) | Import(spair) => spair.span(),
BindIdent(spair)
| BindIdentAbstract(spair)
| BindIdentMeta(spair)
| RefSubject(spair)
| Ref(spair)
| Desc(spair)
| Text(spair)
| Import(spair) => spair.span(),
// A no-op is discarding user input,
// so we still want to know where that is so that we can
@ -363,7 +410,26 @@ impl Display for Nir {
Open(entity, _) => write!(f, "open {entity} entity"),
Close(entity, _) => write!(f, "close {entity} entity"),
BindIdent(spair) => {
write!(f, "bind identifier {}", TtQuote::wrap(spair))
write!(
f,
"bind to concrete identifier {}",
TtQuote::wrap(spair)
)
}
BindIdentAbstract(spair) => {
write!(
f,
"bind to abstract identifier with future value of \
metavariable {}",
TtQuote::wrap(spair)
)
}
BindIdentMeta(spair) => {
write!(
f,
"bind metavariable to concreate identifier {}",
TtQuote::wrap(spair)
)
}
RefSubject(spair) => {
write!(f, "subject ref {}", TtQuote::wrap(spair))

View File

@ -0,0 +1,295 @@
// Abstract binding translation for NIR
//
// Copyright (C) 2014-2023 Ryan Specialty, LLC.
//
// This file is part of TAME.
//
// 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.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//! Translation of non-interpolated abstract bindings for NIR.
//!
//! Metavariables can be used in the context of an identifier binding to
//! produce identifiers dynamically via template expansion.
//! For example:
//!
//! ```xml
//! <classify as="@as@" yields="@yields@">
//! <!-- ^^^^ ^^^^^^^^
//! Ident Ident -->
//!
//! <match on="@match@" />
//! <!-- ~~~~~~~
//! Ref -->
//! </classify>
//! ```
//!
//! In the above example,
//! both `@as@` and `@yields@` represent identifier bindings,
//! but the concrete names are not known until the expansion of the
//! respective metavariables.
//! This is equivalently expressed using interpolation:
//!
//! ```xml
//! <classify as="{@as@}" yields="{@yields@}">
//! <!-- ^^^^^^ ^^^^^^^^^^ -->
//! ```
//!
//! The above interpolation would cause the generation of an abstract
//! identifier via [`super::interp`].
//! However,
//! because metavariables historically have an exclusive naming convention
//! that requires a `@` prefix and suffix,
//! the curly braces can be optionally omitted.
//! This works just fine for the `@match@` _reference_ above,
//! because that reference is unambiguously referring to a metavariable of
//! that name.
//!
//! But binding contexts are different,
//! because they assert that the provided lexical symbol should serve as
//! the name of an identifier.
//! We need an additional and explicit level of indirection,
//! otherwise we run into the following ambiguity:
//!
//! ```xml
//! <template name="_foo_">
//! <param name="@as@" />
//! <!-- ^^^^
//! !
//! vvv -->
//! <classify as="@as@" />
//! </template>
//! ```
//!
//! In this case,
//! if we interpret `@as@` in both contexts to be bindings,
//! then there is a redefinition,
//! which is an error.
//! We instead want the equivalent of this:
//!
//! ```xml
//! <template name="_foo_">
//! <param name="@as@" desc="Name of classification" />
//! <!-- ^^^^ -->
//!
//! <classify as="{@as@}" />
//! <!-- ~~~~ -->
//! </template>
//! ```
//!
//! This creates an awkward ambiguity,
//! because what if we instead want this?
//!
//! ```xml
//! <template name="_foo_">
//! <param name="{@as@}" desc="Name of classification" />
//! <!-- ~~~~ -->
//!
//! <classify as="{@as@}" />
//! <!-- ~~~~ -->
//! </template>
//! ```
//!
//! This was not possible in the XSLT-based TAME.
//! TAMER instead adopts this awkward convention,
//! implemented in this module:
//!
//! 1. Template parameters treat all symbols in binding position
//! (`@name`)
//! as concrete identifiers.
//! This behavior can be overridden using curly braces to trigger
//! interpolation.
//!
//! 2. All other bindings treat symbols matching the `@`-prefix-suffix
//! metavariable naming convention as abstract bindings.
//! This is equivalent to the interpolation behavior.
//!
//! To support this interpretation,
//! this lowering operation requires that all names of
//! [`Nir::BindIdentMeta`] be padded with a single `@` on each side,
//! as shown in the examples above.
//!
//! Lowering Pipeline Ordering
//! ==========================
//! This module is an _optional_ syntactic feature of TAME.
//! If desired,
//! this module could be omitted from the lowering pipeline in favor of
//! explicitly utilizing interpolation for all abstract identifiers.
//!
//! Interpolation via [`super::interp`] its own [`Nir::BindIdentAbstract`]
//! tokens,
//! and shorthand template application via [`super::tplshort`] desugars
//! into both the proper `@`-padded naming convention and
//! [`Nir::BindIdentAbstract`].
//! It should therefore be possible to place this operation in any order
//! relative to those two.
use super::Nir;
use crate::{
fmt::TtQuote, parse::prelude::*, span::Span, sym::GlobalSymbolResolve,
};
use memchr::memchr;
use Nir::*;
#[derive(Debug, PartialEq, Default)]
pub struct AbstractBindTranslate;
impl Display for AbstractBindTranslate {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(
f,
"scanning for abstract binding via metavariable naming convention"
)
}
}
impl ParseState for AbstractBindTranslate {
type Token = Nir;
type Object = Nir;
type Error = AbstractBindTranslateError;
fn parse_token(
self,
tok: Self::Token,
_: &mut Self::Context,
) -> TransitionResult<Self::Super> {
match tok {
BindIdent(name) if needs_translation(name) => {
Transition(self).ok(BindIdentAbstract(name))
}
BindIdentMeta(name) => validate_meta_name(name)
.map(BindIdentMeta)
.map(ParseStatus::Object)
.transition(self),
_ => Transition(self).ok(tok),
}
}
fn is_accepting(&self, _: &Self::Context) -> bool {
true
}
}
/// Validate that the provided name is padded with a single `@` on both
/// sides.
///
/// This check is necessary to ensure that we can properly infer when a
/// metavariable is in use in a bind position without having to rely on
/// interpolation.
///
/// TODO: This does not yet place any other restrictions on the name of a
/// metavariable;
/// we'll take care of that when we decide on an approach for other
/// names.
fn validate_meta_name(
meta: SPair,
) -> Result<SPair, AbstractBindTranslateError> {
let name = meta.symbol().lookup_str();
if !name.starts_with('@') {
Err(AbstractBindTranslateError::MetaNamePadMissing(
meta,
meta.span().slice_head(0),
))
} else if !name.ends_with('@') {
Err(AbstractBindTranslateError::MetaNamePadMissing(
meta,
meta.span().slice_tail(0),
))
} else {
Ok(meta)
}
}
/// Determine whether the given name requires translation into an abstract
/// identifier.
///
/// It's important to understand how the naming convention is utilized;
/// this assumes that:
///
/// 1. Metavariables are `@`-prefixed.
/// The convention is actually to have a suffix too,
/// but since `@` is not permitted at the time of writing for any
/// other types of identifiers,
/// it should be the case that a prefix also implies a suffix,
/// otherwise some other portion of the system will fail.
/// 2. This should not be consulted for metavariable definitions,
/// like template parameters.
fn needs_translation(name: SPair) -> bool {
// Unlike the interpolation module which must check many symbols,
// we assume here that it's not necessary
// (and may be even be determental)
// for a "quick" check version given that this is invoked for
// bindings,
// and bindings will very likely introduce something new.
// It'd be worth verifying this assumption at some point in the future,
// but is unlikely to make a significant different either way.
#[rustfmt::skip]
matches!(
memchr(b'@', name.symbol().lookup_str().as_bytes()),
Some(0),
)
}
#[derive(Debug, PartialEq, Eq)]
pub enum AbstractBindTranslateError {
/// A metavariable does not adhere to the naming convention requiring
/// `@`-padding.
///
/// The provided [`Span`] is the first occurrence of such a violation.
/// If `@` is missing from both the beginning and end of the name,
/// then one of them is chosen.
MetaNamePadMissing(SPair, Span),
}
impl Display for AbstractBindTranslateError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
use AbstractBindTranslateError::*;
match self {
MetaNamePadMissing(name, _) => write!(
f,
"metavariable {} must both begin and end with `@`",
TtQuote::wrap(name),
),
}
}
}
impl Diagnostic for AbstractBindTranslateError {
fn describe(&self) -> Vec<AnnotatedSpan> {
use AbstractBindTranslateError::*;
match self {
MetaNamePadMissing(_, at) => vec![
at.error("missing `@` here"),
at.help(
"metavariables (such as template parameters) must \
have names that both begin and end with the \
character `@`",
),
at.help(
"this naming requirement is necessary to make curly \
braces optional when referencing metavariables \
without requiring interpolation",
),
],
}
}
}
#[cfg(test)]
mod test;

View File

@ -0,0 +1,171 @@
// Test abstract binding translation for NIR
//
// Copyright (C) 2014-2023 Ryan Specialty, LLC.
//
// This file is part of TAME.
//
// 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.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
use super::*;
use crate::span::dummy::{DUMMY_CONTEXT as DC, *};
type Sut = AbstractBindTranslate;
use Parsed::Object as O;
#[test]
fn already_abstract_bind_ignored_despite_metavar_naming() {
// This is named as a metavariable.
let name = "@foo@".into();
assert_tok_translate(
// Identifier is already abstract...
BindIdentAbstract(SPair(name, S1)),
// ...so this acts as an identity operation.
BindIdentAbstract(SPair(name, S1)),
);
}
#[test]
fn concrete_bind_without_metavar_naming_ignored_non_meta() {
// This is _not_ named as a metavariable.
let name = "concrete".into();
assert_tok_translate(
// Identifier is concrete and the name does not follow a
// metavariable naming convention...
BindIdent(SPair(name, S1)),
// ...and so this acts as an identity operation.
BindIdent(SPair(name, S1)),
);
}
#[test]
fn non_meta_concrete_bind_with_metavar_naming_translated_to_abstract_bind() {
// This is named as a metavariable.
let name = "@foo@".into();
assert_tok_translate(
// Identifier is concrete...
BindIdent(SPair(name, S1)),
// ...and so gets translated into an abstract binding.
// Its data are otherwise the same.
BindIdentAbstract(SPair(name, S1)),
);
}
// Metavariable definitions must be left concrete since they produce the
// identifiers that are utilized by other abstract identifiers.
#[test]
fn meta_concrete_bind_with_metavar_naming_ignored() {
// This is named as a metavariable.
let name = "@param@".into();
assert_tok_translate(
// This identifier utilizes a metavariable naming convention,
// but we're in a metavariable definition context.
BindIdentMeta(SPair(name, S2)),
// And so the bind stays concrete.
BindIdentMeta(SPair(name, S2)),
);
}
// This lowering operation utilizes a naming convention to infer user intent
// and lift the requirement for curly braces;
// they go hand-in-hand.
// To utilize this feature,
// we must also require adherence to the naming convention so that we know
// that our assumptions hold.
//
// We can't check for the opposite---
// that non-meta identifiers must _not_ follow that convention---
// because we interpet such occurrences as abstract identifiers.
// In practice,
// users will get an error because the conversion into a reference will
// yield an error when the metavariable does not exist already as a
// reference,
// or a duplicate definition error if it was already defined.
#[test]
fn rejects_metavariable_without_naming_convention() {
let name_a = "@missing-end".into();
// | |
// | 11
// | B
// [----------]
// 0 11
// A
let a_a = DC.span(10, 12);
let a_b = DC.span(22, 0); // _after_ last char
let name_b = "missing-start@".into();
// | |
// 0 |
// A |
// [------------]
// 0 13
// A
let b_a = DC.span(10, 14);
let b_b = DC.span(10, 0); // _before_ first char
let name_c = "missing-both".into();
// | |
// 0 |
// B |
// [----------]
// 0 11
// A
let c_a = DC.span(10, 12);
let c_b = DC.span(10, 0); // _before_ first char
// Each of these will result in slightly different failures.
#[rustfmt::skip]
let toks = [
BindIdentMeta(SPair(name_a, a_a)),
BindIdentMeta(SPair(name_b, b_a)),
BindIdentMeta(SPair(name_c, c_a)),
];
assert_eq!(
#[rustfmt::skip]
vec![
Err(ParseError::StateError(
AbstractBindTranslateError::MetaNamePadMissing(
SPair(name_a, a_a),
a_b,
),
)),
Err(ParseError::StateError(
AbstractBindTranslateError::MetaNamePadMissing(
SPair(name_b, b_a),
b_b,
),
)),
Err(ParseError::StateError(
AbstractBindTranslateError::MetaNamePadMissing(
SPair(name_c, c_a),
c_b,
),
)),
],
Sut::parse(toks.into_iter()).collect::<Vec<Result<_, _>>>(),
);
}
fn assert_tok_translate(tok: Nir, expect: Nir) {
#[rustfmt::skip]
assert_eq!(
Ok(vec![O(expect)]),
Sut::parse([tok].into_iter()).collect()
);
}

View File

@ -21,22 +21,56 @@
use super::Nir;
use crate::{
asg::air::Air,
diagnose::{Annotate, Diagnostic},
fmt::{DisplayWrapper, TtQuote},
asg::{air::Air, ExprOp},
fmt::TtQuote,
nir::{Nir::*, NirEntity::*},
parse::prelude::*,
span::Span,
};
use arrayvec::ArrayVec;
use std::{error::Error, fmt::Display};
// These are also used by the `test` module which imports `super`.
#[cfg(feature = "wip-asg-derived-xmli")]
use crate::{
asg::ExprOp,
nir::{Nir::*, NirEntity::*},
sym::{st::raw::U_TRUE, SymbolId},
};
use arrayvec::ArrayVec;
/// Dynamic [`NirToAir`] parser configuration.
///
/// This acts as a runtime feature flag while this portions of TAMER is
/// under development.
#[derive(Debug, PartialEq, Eq)]
pub enum NirToAirParseType {
/// Discard incoming tokens instead of lowering them.
Noop,
/// Lower known tokens,
/// but produce errors for everything else that is not yet supported.
///
/// It is expected that this will fail on at least some packages;
/// this should be enabled only on packages known to compile with the
/// new system.
LowerKnownErrorRest,
}
impl Display for NirToAirParseType {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
Self::Noop => write!(f, "discarding all tokens (noop)"),
Self::LowerKnownErrorRest => write!(
f,
"lowering only supported tokens and failing on all others"
),
}
}
}
impl From<NirToAirParseType> for (NirToAirParseType, ObjStack) {
fn from(ty: NirToAirParseType) -> Self {
(ty, Default::default())
}
}
impl From<(NirToAirParseType, ObjStack)> for NirToAirParseType {
fn from((ty, _): (NirToAirParseType, ObjStack)) -> Self {
ty
}
}
#[derive(Debug, PartialEq, Eq, Default)]
pub enum NirToAir {
@ -78,37 +112,27 @@ type ObjStack = ArrayVec<Air, 2>;
/// The symbol to use when lexically expanding shorthand notations to
/// compare against values of `1`.
#[cfg(feature = "wip-asg-derived-xmli")]
pub const SYM_TRUE: SymbolId = U_TRUE;
impl ParseState for NirToAir {
type Token = Nir;
type Object = Air;
type Error = NirToAirError;
type Context = ObjStack;
type Context = (NirToAirParseType, ObjStack);
type PubContext = NirToAirParseType;
#[cfg(not(feature = "wip-asg-derived-xmli"))]
fn parse_token(
self,
tok: Self::Token,
_queue: &mut Self::Context,
) -> TransitionResult<Self::Super> {
use NirToAir::*;
let _ = tok; // prevent `unused_variables` warning
Transition(Ready).ok(Air::Todo(crate::span::UNKNOWN_SPAN))
}
#[cfg(feature = "wip-asg-derived-xmli")]
fn parse_token(
self,
tok: Self::Token,
stack: &mut Self::Context,
(parse_type, stack): &mut Self::Context,
) -> TransitionResult<Self::Super> {
use NirToAir::*;
use NirToAirError::*;
use crate::diagnostic_panic;
match parse_type {
NirToAirParseType::Noop => return Transition(Ready).incomplete(),
NirToAirParseType::LowerKnownErrorRest => (),
}
if let Some(obj) = stack.pop() {
return Transition(Ready).ok(obj).with_lookahead(tok);
@ -228,6 +252,12 @@ impl ParseState for NirToAir {
(Ready, Open(TplParam, span)) => {
Transition(Meta(span)).ok(Air::MetaStart(span))
}
(Meta(mspan), BindIdentMeta(spair)) => {
Transition(Meta(mspan)).ok(Air::BindIdent(spair))
}
(Meta(mspan), Ref(spair)) => {
Transition(Meta(mspan)).ok(Air::RefIdent(spair))
}
(Meta(mspan), Text(lexeme)) => {
Transition(Meta(mspan)).ok(Air::MetaLexeme(lexeme))
}
@ -237,10 +267,9 @@ impl ParseState for NirToAir {
// Some of these will be permitted in the future.
(
Meta(mspan),
tok @ (Open(..) | Close(..) | Ref(..) | RefSubject(..)
| Desc(..)),
tok @ (Open(..) | Close(..) | BindIdent(..) | RefSubject(..)),
) => Transition(Meta(mspan))
.err(NirToAirError::UnexpectedMetaToken(mspan, tok)),
.err(NirToAirError::ExpectedMetaToken(mspan, tok)),
(Ready, Text(text)) => Transition(Ready).ok(Air::DocText(text)),
@ -252,15 +281,18 @@ impl ParseState for NirToAir {
),
) => Transition(Ready).ok(Air::ExprEnd(span)),
(st @ (Ready | Meta(_)), BindIdent(spair)) => {
Transition(st).ok(Air::BindIdent(spair))
(Ready, BindIdent(spair)) => {
Transition(Ready).ok(Air::BindIdent(spair))
}
(st @ (Ready | Meta(_)), BindIdentAbstract(spair)) => {
Transition(st).ok(Air::BindIdentAbstract(spair))
}
(Ready, Ref(spair) | RefSubject(spair)) => {
Transition(Ready).ok(Air::RefIdent(spair))
}
(Ready, Desc(clause)) => {
Transition(Ready).ok(Air::DocIndepClause(clause))
(st @ (Ready | Meta(_)), Desc(clause)) => {
Transition(st).ok(Air::DocIndepClause(clause))
}
(Ready, Import(namespec)) => {
@ -272,27 +304,48 @@ impl ParseState for NirToAir {
// This assumption is only valid so long as that's the only
// thing producing NIR.
(st @ Meta(..), tok @ Import(_)) => Transition(st).dead(tok),
(st @ Ready, tok @ BindIdentMeta(_)) => Transition(st).dead(tok),
(_, tok @ (Todo(..) | TodoAttr(..))) => {
crate::diagnostic_todo!(
vec![tok.internal_error(
"this token is not yet supported in TAMER"
)],
"unsupported token: {tok}",
)
// Unsupported tokens yield errors.
// There _is_ a risk that this will put us in a wildly
// inconsistent state,
// yielding nonsense errors in the future.
// This used to panic,
// but yielding errors allows compilation to continue and
// discover further problems,
// so that this new parser can be run on a given package
// (with e.g. `--emit xmlo-experimental`)
// to get some idea of what type of missing features may
// be needed to support the compilation of that package.
// Note also that,
// at the time of writing,
// large numbers of diagnostic spans may be quite slow to
// output on large files because the system does not cache
// newline locations and requires re-calculating from the
// beginning of the file for earlier spans.
(st, tok @ (Todo(..) | TodoAttr(..))) => {
Transition(st).err(NirToAirError::UnsupportedToken(tok))
}
(st, Noop(_)) => Transition(st).incomplete(),
}
}
fn is_accepting(&self, stack: &Self::Context) -> bool {
fn is_accepting(&self, (_, stack): &Self::Context) -> bool {
matches!(self, Self::Ready) && stack.is_empty()
}
}
#[derive(Debug, PartialEq, Eq)]
pub enum NirToAirError {
/// The provided token is not yet supported by TAMER.
///
/// This means that a token was recognized by NIR but it makes no
/// guarantees about _whether_ a token will be supported in the
/// future;
/// explicit rejection is still a future possibility.
UnsupportedToken(Nir),
/// Expected a match subject,
/// but encountered some other token.
///
@ -306,7 +359,7 @@ pub enum NirToAirError {
/// The provided [`Nir`] token of input was unexpected for the body of a
/// metavariable that was opened at the provided [`Span`].
UnexpectedMetaToken(Span, Nir),
ExpectedMetaToken(Span, Nir),
}
impl Display for NirToAirError {
@ -314,6 +367,10 @@ impl Display for NirToAirError {
use NirToAirError::*;
match self {
UnsupportedToken(tok) => {
write!(f, "unsupported token: {tok}")
}
MatchSubjectExpected(_, nir) => {
write!(f, "expected match subject, found {nir}")
}
@ -322,7 +379,7 @@ impl Display for NirToAirError {
write!(f, "match body is not yet supported by TAMER")
}
UnexpectedMetaToken(_, tok) => {
ExpectedMetaToken(_, tok) => {
write!(
f,
"expected lexical token for metavariable, found {tok}"
@ -341,6 +398,19 @@ impl Diagnostic for NirToAirError {
use NirToAirError::*;
match self {
UnsupportedToken(tok) => vec![
tok.span().internal_error("this token is not yet supported in TAMER"),
tok.span().help(
"if this is unexpected, \
are you unintentionally using the `--emit xmlo-experimental` \
command line option?"
),
tok.span().help(
"this package may also have a sibling `.experimental` file \
that triggers `xmlo-experimental`"
),
],
MatchSubjectExpected(ospan, given) => vec![
ospan.note("for this match"),
given
@ -358,7 +428,7 @@ impl Diagnostic for NirToAirError {
// The user should have been preempted by the parent parser
// (e.g. XML->Nir),
// and so shouldn't see this.
UnexpectedMetaToken(mspan, given) => vec![
ExpectedMetaToken(mspan, given) => vec![
mspan.note("while parsing the body of this metavariable"),
given.span().error("expected a lexical token here"),
],
@ -366,5 +436,5 @@ impl Diagnostic for NirToAirError {
}
}
#[cfg(all(test, feature = "wip-asg-derived-xmli"))]
#[cfg(test)]
mod test;

View File

@ -18,12 +18,33 @@
// along with this program. If not, see <http://www.gnu.org/licenses/>.
use super::*;
use crate::{parse::util::SPair, span::dummy::*};
use crate::{
parse::{util::SPair, Parser},
span::dummy::*,
};
type Sut = NirToAir;
use Parsed::{Incomplete, Object as O};
fn sut_parse<I: Iterator<Item = Nir>>(toks: I) -> Parser<Sut, I> {
Sut::parse_with_context(
toks.into_iter(),
NirToAirParseType::LowerKnownErrorRest,
)
}
#[test]
fn ignores_input_when_parse_type_is_noop() {
let toks = vec![Open(Package, S1), Close(Package, S2)];
assert_eq!(
Ok(vec![Incomplete, Incomplete,]),
Sut::parse_with_context(toks.into_iter(), NirToAirParseType::Noop)
.collect(),
);
}
#[test]
fn package_to_pkg() {
let toks = vec![Open(Package, S1), Close(Package, S2)];
@ -33,7 +54,7 @@ fn package_to_pkg() {
O(Air::PkgStart(S1, SPair("/TODO".into(), S1))),
O(Air::PkgEnd(S2)),
]),
Sut::parse(toks.into_iter()).collect(),
sut_parse(toks.into_iter()).collect(),
);
}
@ -55,7 +76,7 @@ fn rate_to_sum_expr() {
O(Air::BindIdent(id)),
O(Air::ExprEnd(S3)),
]),
Sut::parse(toks.into_iter()).collect(),
sut_parse(toks.into_iter()).collect(),
);
}
@ -77,7 +98,7 @@ fn calc_exprs() {
O(Air::ExprEnd(S3)),
O(Air::ExprEnd(S4)),
]),
Sut::parse(toks.into_iter()).collect(),
sut_parse(toks.into_iter()).collect(),
);
}
@ -99,7 +120,7 @@ fn classify_to_conj_expr() {
O(Air::BindIdent(id)),
O(Air::ExprEnd(S3)),
]),
Sut::parse(toks.into_iter()).collect(),
sut_parse(toks.into_iter()).collect(),
);
}
@ -121,7 +142,7 @@ fn logic_exprs() {
O(Air::ExprEnd(S3)),
O(Air::ExprEnd(S4)),
]),
Sut::parse(toks.into_iter()).collect(),
sut_parse(toks.into_iter()).collect(),
);
}
@ -148,7 +169,7 @@ fn desc_as_indep_clause() {
O(Air::DocIndepClause(desc)),
O(Air::ExprEnd(S4)),
]),
Sut::parse(toks.into_iter()).collect(),
sut_parse(toks.into_iter()).collect(),
);
}
@ -170,7 +191,41 @@ fn tpl_with_name() {
O(Air::BindIdent(name)),
O(Air::TplEnd(S3)),
]),
Sut::parse(toks.into_iter()).collect(),
sut_parse(toks.into_iter()).collect(),
);
}
#[test]
fn tpl_with_param() {
let name_tpl = SPair("_tpl_".into(), S2);
let name_param = SPair("@param@".into(), S4);
let desc_param = SPair("param desc".into(), S5);
#[rustfmt::skip]
let toks = vec![
Open(Tpl, S1),
BindIdent(name_tpl),
Open(TplParam, S3),
BindIdentMeta(name_param),
Desc(desc_param),
Close(TplParam, S6),
Close(Tpl, S7),
];
assert_eq!(
#[rustfmt::skip]
Ok(vec![
O(Air::TplStart(S1)),
O(Air::BindIdent(name_tpl)),
O(Air::MetaStart(S3)),
O(Air::BindIdent(name_param)),
O(Air::DocIndepClause(desc_param)),
O(Air::MetaEnd(S6)),
O(Air::TplEnd(S7)),
]),
sut_parse(toks.into_iter()).collect(),
);
}
@ -194,7 +249,7 @@ fn apply_template_long_form_nullary() {
O(Air::RefIdent(name)),
O(Air::TplEndRef(S3)),
]),
Sut::parse(toks.into_iter()).collect(),
sut_parse(toks.into_iter()).collect(),
);
}
@ -212,12 +267,13 @@ fn apply_template_long_form_args() {
RefSubject(name),
Open(TplParam, S3),
BindIdent(p1),
BindIdentMeta(p1),
Text(v1),
Ref(p2),
Close(TplParam, S6),
Open(TplParam, S7),
BindIdent(p2),
BindIdentMeta(p2),
Text(v2),
Close(TplParam, S10),
Close(TplApply, S11),
@ -232,6 +288,7 @@ fn apply_template_long_form_args() {
O(Air::MetaStart(S3)),
O(Air::BindIdent(p1)),
O(Air::MetaLexeme(v1)),
O(Air::RefIdent(p2)),
O(Air::MetaEnd(S6)),
O(Air::MetaStart(S7)),
@ -240,7 +297,7 @@ fn apply_template_long_form_args() {
O(Air::MetaEnd(S10)),
O(Air::TplEndRef(S11)),
]),
Sut::parse(toks.into_iter()).collect(),
sut_parse(toks.into_iter()).collect(),
);
}
@ -274,7 +331,7 @@ fn match_short_no_value() {
O(Air::RefIdent(SPair(SYM_TRUE, S1))),
O(Air::ExprEnd(S3)),
]),
Sut::parse(toks.into_iter()).collect(),
sut_parse(toks.into_iter()).collect(),
);
}
@ -306,7 +363,7 @@ fn match_short_with_value() {
O(Air::RefIdent(value)),
O(Air::ExprEnd(S4)),
]),
Sut::parse(toks.into_iter()).collect(),
sut_parse(toks.into_iter()).collect(),
);
}
@ -345,7 +402,7 @@ fn match_short_value_before_subject_err() {
Ok(O(Air::RefIdent(SPair(SYM_TRUE, S1)))),
Ok(O(Air::ExprEnd(S3))),
],
Sut::parse(toks.into_iter()).collect::<Vec<Result<_, _>>>(),
sut_parse(toks.into_iter()).collect::<Vec<Result<_, _>>>(),
);
}
@ -370,7 +427,7 @@ fn match_no_args_err() {
)),
// RECOVERY: Useless match above discarded.
],
Sut::parse(toks.into_iter()).collect::<Vec<Result<_, _>>>(),
sut_parse(toks.into_iter()).collect::<Vec<Result<_, _>>>(),
);
}
@ -395,6 +452,52 @@ fn text_as_arbitrary_doc() {
O(Air::DocText(text)),
O(Air::PkgEnd(S3)),
]),
Sut::parse(toks.into_iter()).collect(),
sut_parse(toks.into_iter()).collect(),
);
}
// NIR's concept of abstract identifiers exists for the sake of
// disambiguation for AIR.
// While NIR's grammar does not explicitly utilize it,
// interpolation via `nir::interp` will desugar into it.
#[test]
fn abstract_idents_lowered_to_air_equivalent() {
let meta_id = SPair("@foo@".into(), S2);
let meta_meta_id = SPair("@bar@".into(), S5);
#[rustfmt::skip]
let toks = vec![
Open(Rate, S1),
// NIR does not know or care that this metavariable does not
// exist.
BindIdentAbstract(meta_id),
Close(Rate, S3),
// The XSLT-based TAME had a grammatical ambiguity that disallowed
// for this type of construction,
// but there's no reason we can't allow for abstract
// metavariables
// (which would make `meta_meta_id` a meta-metavariable).
// (See `nir::interp` for more information on the handling of
// `TplParam` and abstract identifiers.)
Open(TplParam, S4),
// NIR does not know or care that this metavariable does not
// exist.
BindIdentAbstract(meta_meta_id),
Close(TplParam, S6),
];
assert_eq!(
#[rustfmt::skip]
Ok(vec![
O(Air::ExprStart(ExprOp::Sum, S1)),
O(Air::BindIdentAbstract(meta_id)),
O(Air::ExprEnd(S3)),
O(Air::MetaStart(S4)),
O(Air::BindIdentAbstract(meta_meta_id)),
O(Air::MetaEnd(S6)),
]),
sut_parse(toks.into_iter()).collect(),
);
}

View File

@ -41,7 +41,7 @@
//!
//! ```xml
//! <param name="@___dsgr_01@"
//! desc="Generated from interpolated string `foo{@bar@}baz`">
//! desc="Generated from interpolated string">
//! <text>foo</text>
//! <param-value name="@bar@" />
//! <text>baz</text>
@ -69,6 +69,12 @@
//! then it is interpreted as a literal within the context of the template
//! system and is echoed back unchanged.
//!
//! There is currently no way to escape `{` within a string.
//! Such a feature will be considered in the future,
//! but for the meantime,
//! this can be worked around by using metavariables that expand into the
//! desired literal.
//!
//! Desugared Spans
//! ---------------
//! [`Span`]s for the generated tokens are derived from the specification
@ -102,13 +108,13 @@ use memchr::memchr2;
use super::{Nir, NirEntity};
use crate::{
diagnose::{panic::DiagnosticPanic, Annotate, AnnotatedSpan, Diagnostic},
f::Functor,
f::Map,
fmt::{DisplayWrapper, TtQuote},
parse::{prelude::*, util::SPair, NoContext},
span::Span,
sym::{
st::quick_contains_byte, GlobalSymbolIntern, GlobalSymbolResolve,
SymbolId,
st::{quick_contains_byte, raw::S_GEN_FROM_INTERP},
GlobalSymbolIntern, GlobalSymbolResolve, SymbolId,
},
};
use std::{error::Error, fmt::Display};
@ -266,7 +272,7 @@ impl ParseState for InterpState {
// filter out non-interpolated strings quickly,
// before we start to parse.
// Symbols that require no interpoolation are simply echoed back.
Ready => match tok.symbol() {
Ready => match tok.concrete_symbol() {
Some(sym) if needs_interpolation(sym) => {
Transition(GenIdent(sym))
.ok(Nir::Open(NirEntity::TplParam, span))
@ -282,29 +288,29 @@ impl ParseState for InterpState {
let GenIdentSymbolId(ident_sym) = gen_ident;
Transition(GenDesc(sym, gen_ident))
.ok(Nir::BindIdent(SPair(ident_sym, span)))
.ok(Nir::BindIdentMeta(SPair(ident_sym, span)))
.with_lookahead(tok)
}
// Note: This historically generated a description containing
// the interpolated string,
// which was useful when looking at generated code.
// But this ends up producing output that is not a fixpoint,
// because if you run it back through the compiler,
// it needs interpolation again,
// but now in an incorrect context.
// We can revisit this
// (see commit introducing this comment)
// when we introduce escaping of some form,
// if it's worth doing.
GenDesc(sym, gen_ident) => {
let s = sym.lookup_str();
// Description is not interned since there's no use in
// wasting time hashing something that will not be
// referenced
// (it's just informative for a human).
// Note that this means that tests cannot compare SymbolId.
let gen_desc = format!(
"Generated from interpolated string {}",
TtQuote::wrap(s)
)
.clone_uninterned();
// Begin parsing in a _literal_ context,
// since interpolation is most commonly utilized with literal
// prefixes.
Transition(ParseLiteralAt(s, gen_ident, 0))
.ok(Nir::Desc(SPair(gen_desc, span)))
.ok(Nir::Desc(SPair(S_GEN_FROM_INTERP, span)))
.with_lookahead(tok)
}
@ -458,7 +464,19 @@ impl ParseState for InterpState {
// generated.
// We finally release the lookahead symbol.
FinishSym(_, GenIdentSymbolId(gen_param)) => {
Transition(Ready).ok(tok.map(|_| gen_param))
let replacement = match tok.map(|_| gen_param) {
// `BindIdent` represents a concrete identifier.
// Our interpolation has generated a metavariable,
// meaning that this identifier has become abstract
// since its name will not be known until expansion-time.
Nir::BindIdent(x) => Nir::BindIdentAbstract(x),
// All other tokens only have their symbols replaced by
// the above.
x => x,
};
Transition(Ready).ok(replacement)
}
}
}

View File

@ -22,7 +22,6 @@ use crate::{
nir::NirEntity,
parse::{Parsed, ParsedResult, Parser},
span::dummy::{DUMMY_CONTEXT as DC, *},
sym::GlobalSymbolResolve,
};
use std::assert_matches::assert_matches;
use Parsed::*;
@ -81,7 +80,6 @@ fn does_not_desugar_text() {
fn expect_expanded_header(
sut: &mut Parser<InterpState, std::vec::IntoIter<Nir>>,
given_val: &str,
span: Span,
) -> SymbolId {
let GenIdentSymbolId(expect_name) = gen_tpl_param_ident_at_offset(span);
@ -99,18 +97,104 @@ fn expect_expanded_header(
);
assert_eq!(
sut.next(),
Some(Ok(Object(Nir::BindIdent(SPair(expect_name_sym, span))))),
Some(Ok(Object(Nir::BindIdentMeta(SPair(expect_name_sym, span))))),
);
assert_matches!(
sut.next(),
Some(Ok(Object(Nir::Desc(SPair(desc_str, desc_span)))))
if desc_str.lookup_str().contains(given_val)
&& desc_span == span
Some(Ok(Object(Nir::Desc(SPair(S_GEN_FROM_INTERP, desc_span)))))
if desc_span == span
);
expect_name_sym
}
// This allows for unambiguously requesting desugaring in situations where
// the default is to treat the name as concrete.
#[test]
fn desugars_spec_with_only_var() {
let given_val = "{@foo@}";
// |[---]|
// |1 5|
// | B |
// [-----]
// 0 6
// A
// Non-zero span offset ensures that derived spans properly consider
// parent offset.
let a = DC.span(10, 7);
let b = DC.span(11, 5);
let given_sym = Nir::Ref(SPair(given_val.into(), a));
let toks = vec![given_sym];
let mut sut = Sut::parse(toks.into_iter());
let expect_name = expect_expanded_header(&mut sut, a);
assert_eq!(
Ok(vec![
// This is the actual metavariable reference, pulled out of the
// interpolated portion of the given value.
Object(Nir::Ref(SPair("@foo@".into(), b))),
// This is an object generated from user input, so the closing
// span has to identify what were generated from.
Object(Nir::Close(NirEntity::TplParam, a)),
// Finally,
// we replace the original provided attribute
// (the interpolation specification)
// with a metavariable reference to the generated parameter.
Object(Nir::Ref(SPair(expect_name, a))),
]),
sut.collect(),
);
}
// This is like the above test,
// but with a `BindIdent` instead of a `Ref`,
// which desugars into `BindIdentAbstract`.
// We could handle that translation in a later lowering operation,
// but re-parsing the symbol would be wasteful.
#[test]
fn concrete_bind_ident_desugars_into_abstract_bind_after_interpolation() {
let given_val = "{@bindme@}";
// |[------]|
// |1 8|
// | B |
// [--------]
// 0 9
// A
// Non-zero span offset ensures that derived spans properly consider
// parent offset.
let a = DC.span(10, 10);
let b = DC.span(11, 8);
// This is a bind,
// unlike above.
let given_sym = Nir::BindIdent(SPair(given_val.into(), a));
let toks = vec![given_sym];
let mut sut = Sut::parse(toks.into_iter());
let expect_name = expect_expanded_header(&mut sut, a);
assert_eq!(
Ok(vec![
// The interpolation occurs the same as above.
Object(Nir::Ref(SPair("@bindme@".into(), b))),
Object(Nir::Close(NirEntity::TplParam, a)),
// But at the end,
// instead of keeping the original `BindIdent` token,
// we translate to `BindIdentAbstract`,
// indicating that the name of this identifier depends on the
// value of the metavariable during expansion
Object(Nir::BindIdentAbstract(SPair(expect_name, a))),
]),
sut.collect(),
);
}
// When ending with an interpolated variable,
// the parser should recognize that we've returned to the outer literal
// context and permit successful termination of the specification string.
@ -135,7 +219,7 @@ fn desugars_literal_with_ending_var() {
let mut sut = Sut::parse(toks.into_iter());
let expect_name = expect_expanded_header(&mut sut, given_val, a);
let expect_name = expect_expanded_header(&mut sut, a);
assert_eq!(
Ok(vec![
@ -182,7 +266,7 @@ fn desugars_var_with_ending_literal() {
let mut sut = Sut::parse(toks.into_iter());
let expect_name = expect_expanded_header(&mut sut, given_val, a);
let expect_name = expect_expanded_header(&mut sut, a);
assert_eq!(
Ok(vec![
@ -219,7 +303,7 @@ fn desugars_many_vars_and_literals() {
let mut sut = Sut::parse(toks.into_iter());
let expect_name = expect_expanded_header(&mut sut, given_val, a);
let expect_name = expect_expanded_header(&mut sut, a);
assert_eq!(
Ok(vec![
@ -269,7 +353,7 @@ fn proper_multibyte_handling() {
let mut sut = Sut::parse(toks.into_iter());
let expect_name = expect_expanded_header(&mut sut, given_val, a);
let expect_name = expect_expanded_header(&mut sut, a);
assert_eq!(
Ok(vec![
@ -310,7 +394,7 @@ fn desugars_adjacent_interpolated_vars() {
let mut sut = Sut::parse(toks.into_iter());
let expect_name = expect_expanded_header(&mut sut, given_val, a);
let expect_name = expect_expanded_header(&mut sut, a);
assert_eq!(
Ok(vec![
@ -345,7 +429,7 @@ fn error_missing_closing_interp_delim() {
let mut sut = Sut::parse(toks.into_iter());
let expect_name = expect_expanded_header(&mut sut, given_val, a);
let expect_name = expect_expanded_header(&mut sut, a);
assert_eq!(
vec![
@ -391,7 +475,7 @@ fn error_nested_delim() {
let mut sut = Sut::parse(toks.into_iter());
let expect_name = expect_expanded_header(&mut sut, given_val, a);
let expect_name = expect_expanded_header(&mut sut, a);
assert_eq!(
vec![
@ -440,7 +524,7 @@ fn error_empty_interp() {
let mut sut = Sut::parse(toks.into_iter());
let expect_name = expect_expanded_header(&mut sut, given_val, a);
let expect_name = expect_expanded_header(&mut sut, a);
assert_eq!(
vec![
@ -482,7 +566,7 @@ fn error_close_before_open() {
let mut sut = Sut::parse(toks.into_iter());
let expect_name = expect_expanded_header(&mut sut, given_val, a);
let expect_name = expect_expanded_header(&mut sut, a);
assert_eq!(
vec![

View File

@ -1425,9 +1425,10 @@ ele_parse! {
/// expanded.
TplParamStmt := QN_PARAM(_, ospan) {
@ {
QN_NAME => TodoAttr,
QN_DESC => TodoAttr,
} => Todo(ospan.into()),
QN_NAME => BindIdentMeta,
QN_DESC => Desc,
} => Nir::Open(NirEntity::TplParam, ospan.into()),
/(cspan) => Nir::Close(NirEntity::TplParam, cspan.into()),
TplParamDefault,
};
@ -1457,10 +1458,16 @@ ele_parse! {
/// providing constant values.
/// The result will be as if the user typed the text themselves in the
/// associated template application argument.
///
/// TODO: This just produces a no-op right now and lets the text hander
/// produce text for the inner character data.
/// This is consequently ambiguous with omitting this node entirely;
/// this might be okay,
/// but this needs explicit design.
TplText := QN_TEXT(_, ospan) {
@ {
QN_UNIQUE => TodoAttr,
} => Todo(ospan.into()),
} => Noop(ospan.into()),
};
/// Default the param to the value of another template param,
@ -1474,7 +1481,7 @@ ele_parse! {
/// cumbersome and slow
TplParamValue := QN_PARAM_VALUE(_, ospan) {
@ {
QN_NAME => TodoAttr,
QN_NAME => Ref,
QN_DASH => TodoAttr,
QN_UPPER => TodoAttr,
QN_LOWER => TodoAttr,
@ -1483,7 +1490,7 @@ ele_parse! {
QN_RMUNDERSCORE => TodoAttr,
QN_IDENTIFIER => TodoAttr,
QN_SNAKE => TodoAttr,
} => Todo(ospan.into()),
} => Noop(ospan.into()),
};
/// Inherit a default value from a metavalue.
@ -1696,7 +1703,7 @@ ele_parse! {
/// which gets desugared into this via [`super::tplshort`].
ApplyTemplateParam := QN_WITH_PARAM(_, ospan) {
@ {
QN_NAME => BindIdent,
QN_NAME => BindIdentMeta,
QN_VALUE => Text,
} => Nir::Open(NirEntity::TplParam, ospan.into()),
/(cspan) => Nir::Close(NirEntity::TplParam, cspan.into()),

View File

@ -80,7 +80,7 @@ use arrayvec::ArrayVec;
use super::{Nir, NirEntity};
use crate::{
fmt::{DisplayWrapper, TtQuote},
fmt::TtQuote,
parse::prelude::*,
span::Span,
sym::{
@ -88,7 +88,6 @@ use crate::{
SymbolId,
},
};
use std::convert::Infallible;
use Nir::*;
use NirEntity::*;
@ -130,10 +129,14 @@ impl Display for TplShortDesugar {
}
}
diagnostic_infallible! {
pub enum TplShortDesugarError {}
}
impl ParseState for TplShortDesugar {
type Token = Nir;
type Object = Nir;
type Error = Infallible;
type Error = TplShortDesugarError;
type Context = Stack;
fn parse_token(
@ -162,6 +165,8 @@ impl ParseState for TplShortDesugar {
let tpl_name =
format!("_{}_", qname.local_name().lookup_str()).intern();
// TODO: This should be emitted _after_ params to save work
// for `crate::asg:graph::visit::ontree::SourceCompatibleTreeEdgeOrder`.
let name = SPair(tpl_name, span);
stack.push(Ref(name));
@ -176,7 +181,7 @@ impl ParseState for TplShortDesugar {
// note: reversed (stack)
stack.push(Close(TplParam, span));
stack.push(Text(val));
stack.push(BindIdent(SPair(pname, name.span())));
stack.push(BindIdentMeta(SPair(pname, name.span())));
Transition(DesugaringParams(ospan)).ok(Open(TplParam, span))
}
@ -212,7 +217,7 @@ impl ParseState for TplShortDesugar {
stack.push(Close(TplApply, ospan));
stack.push(Close(TplParam, ospan));
stack.push(Text(SPair(gen_name, ospan)));
stack.push(BindIdent(SPair(L_TPLP_VALUES, ospan)));
stack.push(BindIdentMeta(SPair(L_TPLP_VALUES, ospan)));
// Note that we must have `tok` as lookahead instead of
// pushing directly on the stack in case it's a

View File

@ -86,7 +86,7 @@ fn desugars_unary() {
O(Open(TplParam, S2)),
// Derived from `aname` (by padding)
O(BindIdent(pname)),
O(BindIdentMeta(pname)),
// The value is left untouched.
O(Text(pval)),
// Close is derived from open.
@ -137,7 +137,7 @@ fn desugars_body_into_tpl_with_ref_in_values_param() {
// @values@ remains lexical by referencing the name of a
// template we're about to generate.
O(Open(TplParam, S1)),
O(BindIdent(SPair(L_TPLP_VALUES, S1))),
O(BindIdentMeta(SPair(L_TPLP_VALUES, S1))),
O(Text(SPair(gen_name, S1))), //:-.
O(Close(TplParam, S1)), // |
O(Close(TplApply, S1)), // |
@ -193,7 +193,7 @@ fn desugar_nested_apply() {
// @values@
O(Open(TplParam, S1)),
O(BindIdent(SPair(L_TPLP_VALUES, S1))),
O(BindIdentMeta(SPair(L_TPLP_VALUES, S1))),
O(Text(SPair(gen_name_outer, S1))), //:-.
O(Close(TplParam, S1)), // |
O(Close(TplApply, S1)), // |
@ -227,7 +227,7 @@ fn does_not_desugar_long_form() {
BindIdent(name),
Open(TplParam, S3),
BindIdent(pname),
BindIdentMeta(pname),
Text(pval),
Close(TplParam, S6),
Close(TplApply, S7),
@ -244,7 +244,7 @@ fn does_not_desugar_long_form() {
O(BindIdent(name)),
O(Open(TplParam, S3)),
O(BindIdent(pname)),
O(BindIdentMeta(pname)),
O(Text(pval)),
O(Close(TplParam, S6)),
O(Close(TplApply, S7)),

View File

@ -29,6 +29,7 @@ use fxhash::FxHashSet;
use crate::{
asg::{air::Air, IdentKind, Source},
diagnose::{AnnotatedSpan, Diagnostic},
fmt::{DisplayWrapper, TtQuote},
obj::xmlo::{SymAttrs, SymType},
parse::{util::SPair, ParseState, ParseStatus, Transition, Transitionable},
span::Span,
@ -95,6 +96,7 @@ pub enum XmloToAir {
PackageFound(Span),
Package(PackageSPair),
SymDep(PackageSPair, SPair),
SymDepEnded(PackageSPair, Span),
/// End of header (EOH) reached.
Done(Span),
}
@ -212,13 +214,22 @@ impl ParseState for XmloToAir {
.transition(Package(pkg_name))
}
(Package(pkg_name) | SymDep(pkg_name, _), Fragment(name, text)) => {
(Package(pkg_name) | SymDep(pkg_name, _), SymDepEnd(span)) => {
Transition(SymDepEnded(pkg_name, span)).incomplete()
}
(
Package(pkg_name)
| SymDep(pkg_name, _)
| SymDepEnded(pkg_name, _),
Fragment(name, text),
) => {
Transition(Package(pkg_name)).ok(Air::IdentFragment(name, text))
}
// We don't need to read any further than the end of the
// header (symtable, sym-deps, fragments).
(Package(..) | SymDep(..), Eoh(span)) => {
(Package(..) | SymDep(..) | SymDepEnded(..), Eoh(span)) => {
// It's important to set this _after_ we're done processing,
// otherwise our `first` checks above will be inaccurate.
ctx.first = false;
@ -234,15 +245,36 @@ impl ParseState for XmloToAir {
tok @ (PkgStart(..) | PkgName(..) | Symbol(..)),
) => Transition(st).dead(tok),
(st @ (PackageFound(..) | SymDep(..) | Done(..)), tok) => {
Transition(st).dead(tok)
}
(
st @ (PackageFound(..) | SymDep(..) | SymDepEnded(..)
| Done(..)),
tok,
) => Transition(st).dead(tok),
}
}
fn is_accepting(&self, _: &Self::Context) -> bool {
matches!(*self, Self::Done(_))
}
fn eof_tok(&self, _ctx: &Self::Context) -> Option<Self::Token> {
use XmloToAir::*;
match self {
// We are able to stop parsing immediately after symbol
// dependencies have ended if the caller wishes to ignore
// fragments.
// Pretend that we received an `Eoh` token in this case so that
// we can conclude parsing.
SymDepEnded(_, span) => Some(XmloToken::Eoh(*span)),
Package(_)
| PackageExpected
| PackageFound(_)
| SymDep(_, _)
| Done(_) => None,
}
}
}
impl Display for XmloToAir {
@ -258,6 +290,13 @@ impl Display for XmloToAir {
SymDep(pkg_name, sym) => {
write!(f, "expecting dependency for symbol `/{pkg_name}/{sym}`")
}
SymDepEnded(pkg_name, _) => {
write!(
f,
"expecting fragments or end of header for package {}",
TtQuote::wrap(pkg_name)
)
}
Done(_) => write!(f, "done lowering xmlo into AIR"),
}
}

View File

@ -102,9 +102,10 @@ fn adds_sym_deps() {
PkgName(SPair(name, S2)),
SymDepStart(SPair(sym_from, S3)),
Symbol(SPair(sym_to1, S4)),
Symbol(SPair(sym_to2, S5)),
Eoh(S6),
Symbol(SPair(sym_to1, S4)),
Symbol(SPair(sym_to2, S5)),
SymDepEnd(S6),
Eoh(S7),
];
assert_eq!(
@ -115,7 +116,42 @@ fn adds_sym_deps() {
Incomplete, // SymDepStart
O(Air::IdentDep(SPair(sym_from, S3), SPair(sym_to1, S4))),
O(Air::IdentDep(SPair(sym_from, S3), SPair(sym_to2, S5))),
O(Air::PkgEnd(S6)),
Incomplete, // EndOfDeps
O(Air::PkgEnd(S7)),
]),
Sut::parse(toks.into_iter()).collect(),
);
}
#[test]
fn accepting_state_after_sym_deps() {
let name = "name".into();
let sym_from = "from".into();
let sym_to1 = "to1".into();
#[rustfmt::skip]
let toks = vec![
PkgStart(S1),
PkgName(SPair(name, S2)),
SymDepStart(SPair(sym_from, S3)),
Symbol(SPair(sym_to1, S4)),
SymDepEnd(S5),
// Missing EOH; this should be a valid accepting state so that
// parsing can end early.
];
assert_eq!(
#[rustfmt::skip]
Ok(vec![
Incomplete, // PkgStart
O(Air::PkgStart(S1, SPair(name, S2))),
Incomplete, // SymDepStart
O(Air::IdentDep(SPair(sym_from, S3), SPair(sym_to1, S4))),
Incomplete, // EndOfDeps
// The missing EOH is added automatically.
// TODO: Span of last-encountered token.
O(Air::PkgEnd(S5)),
]),
Sut::parse(toks.into_iter()).collect(),
);

View File

@ -25,8 +25,8 @@ use crate::{
num::{Dim, Dtype},
obj::xmlo::SymType,
parse::{
self, util::SPair, ClosedParseState, EmptyContext, NoContext,
ParseState, Token, Transition, TransitionResult, Transitionable,
self, util::SPair, NoContext, ParseState, Token, Transition,
TransitionResult, Transitionable,
},
span::Span,
sym::{st::raw, GlobalSymbolIntern, GlobalSymbolResolve, SymbolId},
@ -80,6 +80,17 @@ pub enum XmloToken {
/// object file representing the source location of this symbol.
Symbol(SPair),
/// End of symbol dependencies.
///
/// This token indicates that all symbols and their dependencies have
/// been parsed.
/// This is a safe stopping point for subsystems that do not wish to
/// load fragments.
///
/// (This is not named `Eos` because that is not a commonly used
/// initialism and is not clear.)
SymDepEnd(Span),
/// Text (compiled code) fragment for a given symbol.
///
/// This contains the compiler output for a given symbol,
@ -120,6 +131,7 @@ impl Token for XmloToken {
| SymDecl(SPair(_, span), _)
| SymDepStart(SPair(_, span))
| Symbol(SPair(_, span))
| SymDepEnd(span)
| Fragment(SPair(_, span), _)
| Eoh(span) => *span,
}
@ -155,6 +167,7 @@ impl Display for XmloToken {
)
}
Symbol(sym) => write!(f, "symbol {}", TtQuote::wrap(sym)),
SymDepEnd(_) => write!(f, "end of symbol dependencies"),
Fragment(sym, _) => {
write!(f, "symbol {} code fragment", TtQuote::wrap(sym))
}
@ -163,44 +176,30 @@ impl Display for XmloToken {
}
}
/// A parser capable of being composed with [`XmloReader`].
pub trait XmloState =
ClosedParseState<Token = Xirf<Text>, Context = EmptyContext>
where
Self: Default,
<Self as ParseState>::Error: Into<XmloError>,
<Self as ParseState>::Object: Into<XmloToken>;
#[derive(Debug, Default, PartialEq, Eq)]
pub enum XmloReader<
SS: XmloState = SymtableState,
SD: XmloState = SymDepsState,
SF: XmloState = FragmentsState,
> {
pub enum XmloReader {
/// Parser has not yet processed any input.
#[default]
Ready,
/// Processing `package` attributes.
Package(Span),
/// Expecting a symbol declaration or closing `preproc:symtable`.
Symtable(Span, SS),
Symtable(Span, SymtableState),
/// Symbol dependencies are expected next.
SymDepsExpected,
/// Expecting symbol dependency list or closing `preproc:sym-deps`.
SymDeps(Span, SD),
SymDeps(Span, SymDepsState),
/// Compiled text fragments are expected next.
FragmentsExpected,
/// Expecting text fragment or closing `preproc:fragments`.
Fragments(Span, SF),
Fragments(Span, FragmentsState),
/// End of header parsing.
Eoh,
/// `xmlo` file has been fully read.
Done,
}
impl<SS: XmloState, SD: XmloState, SF: XmloState> ParseState
for XmloReader<SS, SD, SF>
{
impl ParseState for XmloReader {
type Token = Xirf<Text>;
type Object = XmloToken;
type Error = XmloError;
@ -249,7 +248,7 @@ impl<SS: XmloState, SD: XmloState, SF: XmloState> ParseState
(Package(_), Xirf::Close(..)) => Transition(Done).incomplete(),
(Package(_), Xirf::Open(QN_P_SYMTABLE, span, ..)) => {
Transition(Symtable(span.tag_span(), SS::default()))
Transition(Symtable(span.tag_span(), SymtableState::default()))
.incomplete()
}
@ -269,14 +268,15 @@ impl<SS: XmloState, SD: XmloState, SF: XmloState> ParseState
),
(SymDepsExpected, Xirf::Open(QN_P_SYM_DEPS, span, _)) => {
Transition(SymDeps(span.tag_span(), SD::default())).incomplete()
Transition(SymDeps(span.tag_span(), SymDepsState::default()))
.incomplete()
}
(SymDeps(_, sd), Xirf::Close(None | Some(QN_P_SYM_DEPS), ..))
if sd.is_accepting(ctx) =>
{
Transition(FragmentsExpected).incomplete()
}
(
SymDeps(_, sd),
Xirf::Close(None | Some(QN_P_SYM_DEPS), cspan, _),
) if sd.is_accepting(ctx) => Transition(FragmentsExpected)
.ok(XmloToken::SymDepEnd(cspan.span())),
(SymDeps(span, sd), tok) => sd.delegate(
tok,
@ -286,8 +286,11 @@ impl<SS: XmloState, SD: XmloState, SF: XmloState> ParseState
),
(FragmentsExpected, Xirf::Open(QN_P_FRAGMENTS, span, _)) => {
Transition(Fragments(span.tag_span(), SF::default()))
.incomplete()
Transition(Fragments(
span.tag_span(),
FragmentsState::default(),
))
.incomplete()
}
(
@ -318,7 +321,7 @@ impl<SS: XmloState, SD: XmloState, SF: XmloState> ParseState
}
fn is_accepting(&self, _: &Self::Context) -> bool {
*self == Self::Eoh || *self == Self::Done
matches!(self, Self::FragmentsExpected | Self::Eoh | Self::Done)
}
}
@ -343,9 +346,7 @@ fn canonical_slash(name: SymbolId) -> SymbolId {
}
}
impl<SS: XmloState, SD: XmloState, SF: XmloState> Display
for XmloReader<SS, SD, SF>
{
impl Display for XmloReader {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
use XmloReader::*;

View File

@ -701,6 +701,7 @@ fn xmlo_composite_parsers_header() {
O(PkgStart(S1)),
O(SymDecl(SPair(sym_name, S3), Default::default(),)),
O(SymDepStart(SPair(symdep_name, S3))),
O(SymDepEnd(S3)),
O(Fragment(SPair(symfrag_id, S4), frag)),
O(Eoh(S3)),
]),
@ -711,3 +712,45 @@ fn xmlo_composite_parsers_header() {
.collect(),
);
}
#[test]
fn xmlo_end_after_sym_deps_before_fragments() {
let sym_name = "sym".into();
let symdep_name = "symdep".into();
#[rustfmt::skip]
let toks_header = [
open(QN_PACKAGE, S1, Depth(0)),
open(QN_P_SYMTABLE, S2, Depth(1)),
open(QN_P_SYM, S3, Depth(2)),
attr(QN_NAME, sym_name, (S2, S3)),
close_empty(S4, Depth(2)),
close(Some(QN_P_SYMTABLE), S4, Depth(1)),
open(QN_P_SYM_DEPS, S2, Depth(1)),
open(QN_P_SYM_DEP, S3, Depth(3)),
attr(QN_NAME, symdep_name, (S2, S3)),
close(Some(QN_P_SYM_DEP), S4, Depth(3)),
close(Some(QN_P_SYM_DEPS), S3, Depth(1)),
// End before fragments.
]
.into_iter();
let sut = Sut::parse(toks_header);
#[rustfmt::skip]
assert_eq!(
Ok(vec![
O(PkgStart(S1)),
O(SymDecl(SPair(sym_name, S3), Default::default(),)),
O(SymDepStart(SPair(symdep_name, S3))),
O(SymDepEnd(S3)),
]),
sut.filter(|parsed| match parsed {
Ok(Incomplete) => false,
_ => true,
})
.collect(),
);
}

View File

@ -32,7 +32,7 @@ pub mod util;
pub use error::{FinalizeError, ParseError};
pub use lower::{
lowerable, terminal, FromParseError, Lower, LowerIter, LowerSource,
ParsedObject,
ParseStateError, ParsedObject,
};
pub use parser::{FinalizedParser, Parsed, ParsedResult, Parser};
pub use state::{
@ -58,8 +58,13 @@ pub mod prelude {
TransitionResult, Transitionable,
};
// Every `Token` must implement `Display`.
pub use std::fmt::Display;
// Every `Token`.
pub use crate::fmt::DisplayWrapper;
pub use std::fmt::{Debug, Display};
// Every `ParseState::Error`.
pub use crate::diagnose::{Annotate, AnnotatedSpan, Diagnostic};
pub use std::error::Error;
}
/// A single datum from a streaming IR with an associated [`Span`].

View File

@ -221,6 +221,16 @@ where
}
}
/// Short-hand [`ParseError`] with types derived from the provided
/// [`ParseState`] `S`.
///
/// The reason that [`ParseError`] does not accept [`ParseState`] is because
/// a [`ParseState`] may carry a lot of additional type baggage---
/// including lifetimes and other generics---
/// that are irrelevant to the error type.
pub type ParseStateError<S> =
ParseError<<S as ParseState>::Token, <S as ParseState>::Error>;
/// A [`Diagnostic`] error type common to both `S` and `LS`.
///
/// This error type must be able to accommodate error variants from all
@ -235,9 +245,17 @@ where
/// which may then decide what to do
/// (e.g. report errors and permit recovery,
/// or terminate at the first sign of trouble).
pub trait WidenedError<S: ParseState, LS: ParseState> = Diagnostic
+ From<ParseError<<S as ParseState>::Token, <S as ParseState>::Error>>
+ From<ParseError<<LS as ParseState>::Token, <LS as ParseState>::Error>>;
///
/// Note that the [`From`] trait bound utilizing `S` is purely a development
/// aid to help guide the user (developer) in deriving the necessary
/// types,
/// since lowering pipelines are deeply complex with all the types
/// involved.
/// It can be safely removed in the future,
/// at least at the time of writing,
/// and have no effect on compilation.
pub trait WidenedError<S: ParseState, LS: ParseState> =
Diagnostic + From<ParseStateError<S>> + From<ParseStateError<LS>>;
/// Convenience trait for converting [`From`] a [`ParseError`] for the
/// provided [`ParseState`] `S`.
@ -246,8 +264,7 @@ pub trait WidenedError<S: ParseState, LS: ParseState> = Diagnostic
/// that is almost certainly already utilized,
/// rather than having to either import more types or use the verbose
/// associated type.
pub trait FromParseError<S: ParseState> =
From<ParseError<<S as ParseState>::Token, <S as ParseState>::Error>>;
pub trait FromParseError<S: ParseState> = From<ParseStateError<S>>;
/// A [`ParsedResult`](super::ParsedResult) with a [`WidenedError`].
pub type WidenedParsedResult<S, E> =

View File

@ -26,7 +26,7 @@
//! to use outside of the domain of the parsing system itself.
use super::{prelude::*, state::TransitionData};
use crate::{f::Functor, span::Span, sym::SymbolId};
use crate::{f::Map, span::Span, sym::SymbolId};
use std::fmt::Display;
/// A [`SymbolId`] with a corresponding [`Span`].
@ -56,6 +56,22 @@ use std::fmt::Display;
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub struct SPair(pub SymbolId, pub Span);
/// More concisely construct an [`SPair`] from [`SymbolId`]- and
/// [`Span`]-like things.
///
/// This is restricted to `cfg(test)` because it can cause unexpected
/// internment if you're not careful.
/// For example,
/// if a [`str`] is assigned to a variable and then that variable is
/// supplied to multiple calls to this function,
/// each call will invoke the internment system.
/// This isn't much of a concern for short-running tests,
/// but is not acceptable elsewhere.
#[cfg(test)]
pub fn spair(sym: impl Into<SymbolId>, span: impl Into<Span>) -> SPair {
SPair(sym.into(), span.into())
}
impl SPair {
/// Retrieve the [`SymbolId`] of this pair.
///
@ -67,7 +83,7 @@ impl SPair {
}
}
impl Functor<SymbolId> for SPair {
impl Map<SymbolId> for SPair {
/// Map over the [`SymbolId`] of the pair while retaining the original
/// associated [`Span`].
///
@ -82,7 +98,7 @@ impl Functor<SymbolId> for SPair {
}
}
impl Functor<Span> for SPair {
impl Map<Span> for SPair {
/// Map over the [`Span`] of the pair while retaining the associated
/// [`SymbolId`].
///

View File

@ -39,15 +39,85 @@
//! The module is responsible for pipeline composition.
//! For information on the lowering pipeline as an abstraction,
//! see [`Lower`].
//!
//! Error Widening
//! ==============
//! Every lowering pipeline will have an associated error sum type generated
//! for it;
//! this is necessary to maintain an appropriate level of encapsulation
//! and keep implementation details away from the caller.
//! All of the individual errors types is otherwise significant source of
//! complexity.
//!
//! Since all [`ParseState`]s in the lowering pipeline are expected to
//! support error recovery,
//! this generated error sum type represents a _recoverable_ error.
//! It is up to the sink to deermine whether the error should be promoted
//! into an unrecoverable error `EU`,
//! which is the error type yielded by the lowering operation.
//! Error reporting and recovery should be utilized whenever it makes sense
//! to present the user with as many errors as possible rather than
//! aborting the process immediately,
//! which would otherwise force the user to correct errors one at a
//! time.
//!
//! [`ParseState`] Requirements
//! ---------------------------
//! Each [`ParseState`] in the pipeline is expected to have its own unique
//! error type,
//! utilizing newtypes if necessary;
//! this ensures that errors are able to be uniquely paired with each
//! [`ParseState`] that produced it without having to perform an
//! explicit mapping at the call site.
//! This uniqueness property allows for generation of [`From`]
//! implementations that will not overlap,
//! and remains compatible with the API of [`Lower`].
//!
//! [`ParseState::Error`] Lifetime Requirements and Workarounds
//! -----------------------------------------------------------
//! Error types in TAMER _never_ have lifetime bounds;
//! this is necessary to allow error types to be propapgated all the way
//! up the stack regardless of dependencies.[^lifetime-alt]
//!
//! [^lifetime-alt]: Rather than utilizing references with lifetimes,
//! TAMER error types may hold symbols representing interned values,
//! or may instead [`Copy`] data that has no interner.
//!
//! However,
//! [`ParseState::Error`] is an associated type on [`ParseState`],
//! which _may_ have lifetimes.[^parse-state-lifetime-ex]
//! At the time of writing,
//! even though the associated error type does not utilize the lifetime
//! bounds of the [`ParseState`],
//! Rust still requires some lifetime specification and will not elide
//! it or allow for anonymous lifetimes.
//!
//! [^parse-state-lifetime-ex]: One example of a [`ParseState`] with
//! an associated lifetime is [`AsgTreeToXirf`].
//!
//! We want to be able to derive error types from the provided
//! [`ParseState`]s along so that the caller does not have to peel back
//! layers of abstraction in order to determine how the error type ought
//! to be specified.
//! To handle this,
//! the `lower_pipeline!` macro will _rewrite all lifetimes to `'static`'_
//! in the provided pipeline types.
//! Since no [`ParseState::Error`] type should have a lifetime,
//! and therefore should not reference the lifetime of its parent
//! [`ParseState`],
//! this should have no practical effect on the error type itself.
use crate::{
asg::{air::AirAggregate, AsgTreeToXirf},
diagnose::Diagnostic,
nir::{InterpolateNir, NirToAir, TplShortDesugar, XirfToNir},
nir::{
AbstractBindTranslate, InterpolateNir, NirToAir, TplShortDesugar,
XirfToNir,
},
obj::xmlo::{XmloReader, XmloToAir, XmloToken},
parse::{
terminal, FinalizeError, Lower, LowerSource, ParseError, ParseState,
Parsed, ParsedObject, UnknownToken,
ParseStateError, Parsed, ParsedObject, UnknownToken,
},
xir::{
autoclose::XirfAutoClose,
@ -61,38 +131,58 @@ use crate::{
mod r#macro;
lower_pipeline! {
/// Load an `xmlo` file represented by `src` into the graph held
/// by `air_ctx`.
/// Parse a source package into the [ASG](crate::asg) using TAME's XML
/// source language.
///
/// Source XML is represented by [XIR](crate::xir).
/// This is parsed into [NIR`](crate::nir),
/// which extracts TAME's high-level source language from the document
/// format (XML).
/// NIR is then desugared in various ways,
/// producing a more verbose NIR than what the user originally
/// entered.
///
/// NIR is then lowered into [AIR](crate::asg::air),
/// which is then aggregated into the [ASG](crate::asg) to await
/// further processing.
/// It is after this point that package imports should be processed and
/// also aggregated into the same ASG so that all needed dependencies
/// definitions are available.
pub parse_package_xml -> ParsePackageXml
|> XirToXirf<64, RefinedText>
|> XirfToNir
|> TplShortDesugar
|> InterpolateNir
|> AbstractBindTranslate
|> NirToAir[nir_air_ty]
|> AirAggregate[air_ctx];
/// Load an `xmlo` file into the graph held by `air_ctx`.
///
/// Loading an object file will result in opaque objects being added to the
/// graph.
/// graph;
/// no sources will be parsed.
///
/// TODO: To re-use this in `tamec` we want to be able to ignore fragments.
///
/// TODO: More documentation once this has been further cleaned up.
pub load_xmlo
/// To parse sources instead,
/// see [`parse_package_xml`].
pub load_xmlo -> LoadXmlo
|> PartialXirToXirf<4, Text>
|> XmloReader
|> XmloToAir[xmlo_ctx], until (XmloToken::Eoh(..))
|> AirAggregate[air_ctx];
/// Parse a source package into the [ASG](crate::asg) using TAME's XML
/// source language.
///
/// TODO: More documentation once this has been further cleaned up.
pub parse_package_xml
|> XirToXirf<64, RefinedText>
|> XirfToNir
|> TplShortDesugar
|> InterpolateNir
|> NirToAir
|> AirAggregate[air_ctx];
/// Lower an [`Asg`](crate::asg::Asg)-derived token stream into an
/// `xmli` file.
///
/// TODO: More documentation once this has been further cleaned up.
pub lower_xmli<'a>
/// After a package has been parsed with [`parse_package_xml`] and
/// further processing has taken place,
/// this pipeline will re-generate TAME sources from the ASG for the
/// purpose of serving as source input to the XSLT-based compiler.
/// This allows us to incrementally replace that compiler's
/// functionality by having the XSLT-based system pick up where we
/// leave off,
/// skipping anything that we have already done.
pub lower_xmli<'a> -> LowerXmli
|> AsgTreeToXirf<'a>[asg]
|> XirfAutoClose
|> XirfToXir<Text>;

View File

@ -25,6 +25,103 @@
//! and to see TAMER's pipelines,
//! see the [parent module](super).
#[cfg(doc)]
use crate::parse::ParseState;
/// Generate an error sum type for a lowering pipeline.
///
/// Given a series of [`ParseState`] types,
/// this derives a sum type capable of representing the associated
/// [`ParseState::Error`] of each.
/// See the [parent module](super) for more information,
/// including the challenges/concerns with this approach.
/// In particular,
/// note that all lifetimes on the [`ParseState`] type are rewritten to be
/// `'static';
/// all associated `Error` types must not contain non-static lifetimes,
/// as is the standard convention in TAMER.
macro_rules! lower_error_sum {
(
$(#[$meta:meta])*
$vis:vis $name:ident = $(
$st:ident $(<$($l:lifetime,)* $($c:literal,)* $($t:ident,)*>)?
)+
) => {
// Pair `'static` with each lifetime so that it may be used to
// replace the respective lifetime in `@gen`
// (we need an iteration token).
lower_error_sum!(
@gen
$(#[$meta])*
$vis $name = $($st$(<$($l: 'static,)* $($c,)* $($t,)*>)?)+
);
};
(
@gen
$(#[$meta:meta])*
$vis:vis $name:ident = $(
$st:ident $(<
$($_:lifetime: $l:lifetime,)*
// ^^ `'static` (see above)
$($c:literal,)*
$($t:ident,)*
>)?
)+
) => {
$(#[$meta])*
#[derive(Debug, PartialEq)]
$vis enum $name<ES: Diagnostic + PartialEq + 'static> {
Src(ParseError<UnknownToken, ES>),
$(
$st(ParseStateError<$st$(<$($l,)* $($c,)* $($t),* >)?>)
// ^^ `'static`
),+
}
impl<ES: Diagnostic + PartialEq + 'static> From<ParseError<UnknownToken, ES>>
for $name<ES>
{
fn from(e: ParseError<UnknownToken, ES>) -> Self {
Self::Src(e)
}
}
$(
impl<ES: Diagnostic + PartialEq + 'static>
From<ParseStateError<$st$(<$($l,)* $($c,)* $($t),*>)?>>
for $name<ES>
{
fn from(e: ParseStateError<$st$(<$($l,)* $($c,)* $($t),*>)?>) -> Self {
Self::$st(e)
}
}
)+
impl<ES: Diagnostic + PartialEq + 'static> std::fmt::Display for $name<ES> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Src(e) => std::fmt::Display::fmt(e, f),
$(
Self::$st(e) => std::fmt::Display::fmt(e, f),
)+
}
}
}
impl<ES: Diagnostic + PartialEq + 'static> Diagnostic for $name<ES> {
fn describe(&self) -> Vec<crate::diagnose::AnnotatedSpan> {
match self {
Self::Src(e) => e.describe(),
$(
Self::$st(e) => e.describe(),
)+
}
}
}
};
}
/// Declaratively define a lowering pipeline.
///
/// A lowering pipeline stitches together parsers such that the objects of
@ -42,9 +139,49 @@
macro_rules! lower_pipeline {
($(
$(#[$meta:meta])*
$vis:vis $fn:ident$(<$l:lifetime>)?
$(|> $lower:ty $([$ctx:ident])? $(, until ($until:pat))?)*;
$vis:vis $fn:ident$(<$l:lifetime>)? -> $struct:ident
$(|>
$lower_name:tt$(<$($lower_t:tt),+>)?
$([$ctx:ident])?
$(, until ($until:pat))?
)*
;
)*) => {$(
paste::paste! {
lower_error_sum! {
/// Recoverable error for
#[doc=concat!("[`", stringify!($fn), "`]")]
/// lowering pipeline.
///
/// This represents an error that occurred from one of the
/// [`ParseState`]s in the lowering pipeline.
/// Since all [`ParseState`]s are expected to attempt
/// recovery on failure,
/// this error represents a _recoverable_ error.
/// Whether or not the error should be treated as
/// recoverable is entirely at the discretion of the sink
/// provided to the pipeline;
/// a sink may choose to promote all errors to
/// unrecoverable.
$vis [<$struct Error>] = $(
$lower_name$(<$($lower_t,)+>)?
)*
}
}
lower_pipeline!(
@pipeline
$(#[$meta])*
$vis $fn$(<$l>)? -> $struct
$(|> $lower_name$(<$($lower_t),+>)? $([$ctx])? $(, until ($until))?)*
);
)*};
(@pipeline
$(#[$meta:meta])*
$vis:vis $fn:ident$(<$l:lifetime>)? -> $struct:ident
$(|> $lower:ty $([$ctx:ident])? $(, until ($until:pat))?)*
) => {paste::paste!{
$(#[$meta])*
///
/// Pipeline Definition
@ -72,47 +209,35 @@ macro_rules! lower_pipeline {
/// of the types of all parsers in the pipeline.
/// It can be understood as:
///
/// 1. A function accepting three classes of arguments:
/// 1. The _source_ token stream,
/// which consists of tokens expected by the first parser
/// in the pipeline;
/// 2. _Context_ for certain parsers that request it,
/// allowing for state to persist between separate
/// pipelines; and
/// 3. A _sink_ that serves as the final destination for the
/// token stream.
/// 2. A [`Result`] consisting of the updated context that was
/// 1. A function accepting _context_ for whatever parsers request
/// it,
/// allowing both for configuration and for state to
/// persist between separate pipelines.
/// This returns a closure representing a configured pipeline.
/// 2. The _source_ token stream is accepted by the closure,
/// which consists of tokens expected by the first parser
/// in the pipeline;
/// 3. A _sink_ serves as the final destination for the token
/// stream.
/// 4. A [`Result`] consisting of the updated context that was
/// originally passed into the function,
/// so that it may be utilized in future pipelines.
/// 3. A _recoverable error_ type `ER` that may be utilized when
/// 5. A _recoverable error_ type
#[doc=concat!("[`", stringify!([<$struct Error>]), "`]")]
/// that may be utilized when
/// compilation should continue despite an error.
/// All parsers are expected to perform their own error
/// recovery in an attempt to continue parsing to discover
/// further errors;
/// as such,
/// this error type `ER` must be able to contain the
/// errors of any parser in the pipeline,
/// which is the reason for the large block of
/// [`From`]s in this function's `where` clause.
/// 4. An _unrecoverable error_ type `EU` that may be yielded by
/// See [`crate::pipeline`] for more information.
/// 6. An _unrecoverable error_ type `EU` that may be yielded by
/// the sink to terminate compilation immediately.
/// This is a component of the [`Result`] type that is
/// ultimately yielded as the result of this function.
$vis fn $fn<$($l,)? ES: Diagnostic, ER: Diagnostic, EU: Diagnostic>(
src: impl LowerSource<
UnknownToken,
lower_pipeline!(@first_tok_ty $($lower),*),
ES
>,
$vis fn $fn<$($l,)? ES: Diagnostic + 'static, EU: Diagnostic, SA, SB>(
$(
// Each parser may optionally receive context from an
// earlier run.
$($ctx: impl Into<<$lower as ParseState>::PubContext>,)?
)*
sink: impl FnMut(
Result<lower_pipeline!(@last_obj_ty $($lower),*), ER>
) -> Result<(), EU>,
) -> Result<
) -> impl FnOnce(SA, SB) -> Result<
(
$(
// Any context that is passed in is also returned so
@ -124,21 +249,6 @@ macro_rules! lower_pipeline {
EU
>
where
// Recoverable errors (ER) are errors that could potentially be
// handled by the sink.
// Parsers are always expected to perform error recovery to the
// best of their ability.
// We need to support widening into this error type from every
// individual ParseState in this pipeline,
// plus the source.
ER: From<ParseError<UnknownToken, ES>>
$(
+ From<ParseError<
<$lower as ParseState>::Token,
<$lower as ParseState>::Error,
>>
)*,
// Unrecoverable errors (EU) are errors that the sink chooses
// not to handle.
// It is constructed explicitly from the sink,
@ -147,17 +257,33 @@ macro_rules! lower_pipeline {
// which is _not_ an error that parsers are expected to
// recover from.
EU: From<FinalizeError>,
{
let lower_pipeline!(@ret_pat $($($ctx)?)*) = lower_pipeline!(
@body_head(src, sink)
$((|> $lower $([$ctx])? $(, until ($until))?))*
)?;
Ok(($(
$($ctx,)?
)*))
SA: LowerSource<
UnknownToken,
lower_pipeline!(@first_tok_ty $($lower),*),
ES
>,
SB: FnMut(
Result<lower_pipeline!(@last_obj_ty $($lower),*), [<$struct Error>]<ES>>
) -> Result<(), EU>
{
move |src, sink| {
// Recoverable error type (for brevity).
#[doc(hidden)]
type ER<T> = [<$struct Error>]<T>;
let lower_pipeline!(@ret_pat $($($ctx)?)*) = lower_pipeline!(
@body_head(src, sink)
$((|> $lower $([$ctx])? $(, until ($until))?))*
)?;
Ok(($(
$($ctx,)?
)*))
}
}
)*};
}};
(@ret_ctx_ty $lower:ty, $_ctx:ident) => {
<$lower as ParseState>::PubContext
@ -200,8 +326,8 @@ macro_rules! lower_pipeline {
Lower::<
ParsedObject<UnknownToken, _, ES>,
$head,
ER,
>::lower::<_, EU>(&mut $src.map(|result| result.map_err(ER::from)), |next| {
ER<ES>,
>::lower::<_, EU>(&mut $src.map(|result| result.map_err(ER::Src)), |next| {
lower_pipeline!(
@body_inner(next, $head, $sink)
$($rest)*
@ -217,9 +343,9 @@ macro_rules! lower_pipeline {
Lower::<
ParsedObject<UnknownToken, _, ES>,
$head,
ER,
ER<ES>,
>::lower_with_context::<_, EU>(
&mut $src.map(|result| result.map_err(ER::from)),
&mut $src.map(|result| result.map_err(ER::Src)),
$ctx,
|next| {
lower_pipeline!(

View File

@ -733,6 +733,8 @@ pub mod st {
URI_LV_TPL: uri "http://www.lovullo.com/rater/apply-template",
URI_LV_WORKSHEET: uri "http://www.lovullo.com/rater/worksheet",
S_GEN_FROM_INTERP: str "Generated from interpolated string",
// Common whitespace.
//
// _This does not represent all forms of whitespace!_

View File

@ -101,12 +101,11 @@ use super::{
CloseSpan, OpenSpan, QName,
};
use crate::{
f::Functor,
f::Map,
parse::prelude::*,
span::{Span, UNKNOWN_SPAN},
xir::EleSpan,
};
use std::{convert::Infallible, fmt::Display};
use XirfAutoClose::*;
@ -138,10 +137,14 @@ impl Display for XirfAutoClose {
}
}
diagnostic_infallible! {
pub enum XirfAutoCloseError {}
}
impl ParseState for XirfAutoClose {
type Token = XirfToken<Text>;
type Object = XirfToken<Text>;
type Error = Infallible;
type Error = XirfAutoCloseError;
type Context = AutoCloseStack;
fn parse_token(

View File

@ -46,20 +46,14 @@ use super::{
CloseSpan, OpenSpan, QName, Token as XirToken, TokenStream,
};
use crate::{
diagnose::{Annotate, AnnotatedSpan, Diagnostic},
f::Functor,
f::Map,
parse::prelude::*,
span::Span,
sym::{st::is_common_whitespace, GlobalSymbolResolve, SymbolId},
xir::EleSpan,
};
use arrayvec::ArrayVec;
use std::{
convert::Infallible,
error::Error,
fmt::{Debug, Display},
marker::PhantomData,
};
use std::marker::PhantomData;
// Used for organization.
pub use accept::*;
@ -254,7 +248,7 @@ impl<T: TextType> From<Attr> for XirfToken<T> {
}
}
impl<T: TextType> Functor<Depth> for XirfToken<T> {
impl<T: TextType> Map<Depth> for XirfToken<T> {
fn map(self, f: impl FnOnce(Depth) -> Depth) -> Self::Target {
use XirfToken::*;
@ -926,10 +920,14 @@ impl<T: TextType> Display for XirfToXir<T> {
}
}
diagnostic_infallible! {
pub enum XirfToXirError {}
}
impl<T: TextType> ParseState for XirfToXir<T> {
type Token = XirfToken<T>;
type Object = XirToken;
type Error = Infallible;
type Error = XirfToXirError;
fn parse_token(
self,

View File

@ -619,10 +619,10 @@ fn xirf_to_xir() {
);
// The lowering pipeline above requires compatible errors.
impl From<ParseError<XirfToken<Text>, Infallible>>
impl From<ParseError<XirfToken<Text>, XirfToXirError>>
for ParseError<XirToken, XirToXirfError>
{
fn from(_value: ParseError<XirfToken<Text>, Infallible>) -> Self {
fn from(_value: ParseError<XirfToken<Text>, XirfToXirError>) -> Self {
unreachable!()
}
}

View File

@ -22,6 +22,10 @@ slightly different representation than the original sources, but it must
express an equivalent program, and the program must be at least as
performant when emitted by TAME XSLT.
# Experimental Features
If a file `is-experimental` exists in a test directory, then
`--emit xmlo-experimental` will be used in place of `--emit xmlo`.
# Running Tests
Test are prefixed with `test-*` and are executable. They must be invoked
with the environment variable `PATH_TAMEC` set to the path of `tamec`

View File

@ -0,0 +1,11 @@
<package xmlns="http://www.lovullo.com/rater"
xmlns:c="http://www.lovullo.com/calc"
xmlns:t="http://www.lovullo.com/rater/apply-template">
This simply verifies that the file is copied to the destination.
This can be removed once more of tamec becomes stable.
<t:foo-bar />
</package>

View File

@ -0,0 +1,11 @@
<?xml version="1.0"?>
<package xmlns="http://www.lovullo.com/rater"
xmlns:c="http://www.lovullo.com/calc"
xmlns:t="http://www.lovullo.com/rater/apply-template">
This simply verifies that the file is copied to the destination.
This can be removed once more of tamec becomes stable.
<t:foo-bar />
</package>

View File

@ -0,0 +1,73 @@
<package xmlns="http://www.lovullo.com/rater"
xmlns:c="http://www.lovullo.com/calc"
xmlns:t="http://www.lovullo.com/rater/apply-template">
<template name="_interp-non-bind_"
desc="Interpolation in non-binding position">
<param name="@___dsgr_335@"
desc="Generated from interpolated string">
<param-value name="@bar@" />
</param>
<param name="@___dsgr_368@"
desc="Generated from interpolated string">
<text>Prefix </text>
<param-value name="@bar@" />
</param>
<param name="@___dsgr_3a3@"
desc="Generated from interpolated string">
<param-value name="@bar@" />
<text> suffix</text>
</param>
<param name="@___dsgr_3da@"
desc="Generated from interpolated string">
<text>Prefix </text>
<param-value name="@bar@" />
<text> suffix</text>
</param>
<classify as="only" desc="@___dsgr_335@"/>
<classify as="prefixed" desc="@___dsgr_368@"/>
<classify as="suffixed" desc="@___dsgr_3a3@"/>
<classify as="both" desc="@___dsgr_3da@" />
</template>
<template name="_with-abstract-ident_"
desc="Metavariable interpolation in binding position">
<param name="@___dsgr_4a8@"
desc="Generated from interpolated string">
<param-value name="@as@" />
</param>
<param name="@___dsgr_4ca@"
desc="Generated from interpolated string">
<text>prefix-</text>
<param-value name="@as@" />
</param>
<param name="@___dsgr_4f4@"
desc="Generated from interpolated string">
<param-value name="@as@" />
<text>-suffix</text>
</param>
<param name="@___dsgr_51e@"
desc="Generated from interpolated string">
<text>prefix-</text>
<param-value name="@as@" />
<text>-suffix</text>
</param>
<classify as="@___dsgr_4a8@" />
<classify as="@___dsgr_4ca@" />
<classify as="@___dsgr_4f4@" />
<classify as="@___dsgr_51e@" />
</template>
</package>

View File

@ -0,0 +1,80 @@
<?xml version="1.0"?>
<package xmlns="http://www.lovullo.com/rater"
xmlns:c="http://www.lovullo.com/calc"
xmlns:t="http://www.lovullo.com/rater/apply-template">
<!-- note: the extra vertical space is for alignment with expected.xml;
open them side-by-side in your editor of choice -->
<!-- because the output contains identifiers derived from spans, this test
is exceptionally fragile; if you add or remove a single byte, you're
bound to break things. If that happens, it is safe to update the
span portion of identifier names. In the future, a tool may be
created to help with this tedious chore. -->
<template name="_interp-non-bind_"
desc="Interpolation in non-binding position">
<!-- note the `{}` here -->
<classify as="only" desc="{@bar@}" />
<classify as="prefixed" desc="Prefix {@bar@}" />
<classify as="suffixed" desc="{@bar@} suffix" />
<classify as="both" desc="Prefix {@bar@} suffix" />
</template>
<template name="_with-abstract-ident_"
desc="Metavariable interpolation in binding position">
<!-- note the `{}` here -->
<classify as="{@as@}" />
<classify as="prefix-{@as@}" />
<classify as="{@as@}-suffix" />
<classify as="prefix-{@as@}-suffix" />
</template>
</package>

View File

@ -20,19 +20,11 @@
<c:sum>
<c:product />
</c:sum>
<c:product>
<c:sum />
</c:product>
</template>
<template name="_with-static-mix_"
desc="Both identified and unidentified that may or may
not be reachable in expansion context">
<c:sum>
<c:product />
</c:sum>
<c:product>
<c:sum />
</c:product>
@ -48,9 +40,9 @@
<rate yields="tplStaticMix" />
<c:sum>
<rate yields="tplStaticMixEnd">
<c:product />
</c:sum>
</rate>
</template>
@ -100,9 +92,9 @@
<template name="_short-hand-nullary-body_" desc="Nullary with body" />
<apply-template name="_short-hand-nullary-body_">
<with-param name="@values@" value="___dsgr-bfe___" />
<with-param name="@values@" value="___dsgr-bb5___" />
</apply-template>
<template name="___dsgr-bfe___"
<template name="___dsgr-bb5___"
desc="Desugared body of shorthand template application of `_short-hand-nullary-body_`">
<c:product>
<c:sum />
@ -113,9 +105,9 @@
<apply-template name="_short-hand-nary-body_">
<with-param name="@bar@" value="baz" />
<with-param name="@baz@" value="quux" />
<with-param name="@values@" value="___dsgr-cb5___" />
<with-param name="@values@" value="___dsgr-c6c___" />
</apply-template>
<template name="___dsgr-cb5___"
<template name="___dsgr-c6c___"
desc="Desugared body of shorthand template application of `_short-hand-nary-body_`">
<c:sum>
<c:product />
@ -125,9 +117,9 @@
<template name="_short-hand-nullary-outer_"
desc="Outer template holding an inner" />
<apply-template name="_short-hand-nullary-outer_">
<with-param name="@values@" value="___dsgr-d99___" />
<with-param name="@values@" value="___dsgr-d50___" />
</apply-template>
<template name="___dsgr-d99___"
<template name="___dsgr-d50___"
desc="Desugared body of shorthand template application of `_short-hand-nullary-outer_`">
<template name="_short-hand-nullary-inner-dfn-inner_"
desc="Inner template applied inner" />
@ -137,9 +129,9 @@
<template name="_short-hand-nullary-inner-dfn-outer_"
desc="Define template outer but apply inner" />
<apply-template name="_short-hand-nullary-outer_">
<with-param name="@values@" value="___dsgr-eed___" />
<with-param name="@values@" value="___dsgr-ea4___" />
</apply-template>
<template name="___dsgr-eed___"
<template name="___dsgr-ea4___"
desc="Desugared body of shorthand template application of `_short-hand-nullary-outer_`">
<apply-template name="_short-hand-nullary-inner-dfn-outer_" />
</template>
@ -148,9 +140,9 @@
desc="Unary with body" />
<apply-template name="_short-hand-unary-with-body_">
<with-param name="@foo@" value="bar" />
<with-param name="@values@" value="___dsgr-fb4___" />
<with-param name="@values@" value="___dsgr-f6b___" />
</apply-template>
<template name="___dsgr-fb4___"
<template name="___dsgr-f6b___"
desc="Desugared body of shorthand template application of `_short-hand-unary-with-body_`">
<template name="_short-hand-unary-with-body-inner_"
desc="Inner template" />
@ -190,5 +182,28 @@
<template name="_match-child_" desc="Template with a match child">
<match on="foo" value="TRUE" />
</template>
<template name="_tpl-param_" desc="Template with a param">
<param name="@foo@" desc="A parameter" />
<param name="@bar@" desc="Another parameter" />
</template>
<template name="_tpl-param_body_"
desc="Template with params with bodies">
<param name="@text@" desc="A param with a literal">
<text>lonely foo</text>
</param>
<param name="@ref@" desc="A param with a ref">
<param-value name="@text@" />
</param>
<param name="@both@" desc="A param with both literal and ref">
<text>foo </text>
<param-value name="@text@" />
<text> bar</text>
</param>
</template>
</package>

View File

@ -20,19 +20,11 @@
<c:sum>
<c:product />
</c:sum>
<c:product>
<c:sum />
</c:product>
</template>
<template name="_with-static-mix_"
desc="Both identified and unidentified that may or may
not be reachable in expansion context">
<c:sum>
<c:product />
</c:sum>
<c:product> <!-- depth N -->
<c:sum /> <!-- depth N+1 -->
</c:product>
@ -48,9 +40,9 @@
<rate yields="tplStaticMix" /> <!-- begins at depth N+1 -->
<c:sum>
<rate yields="tplStaticMixEnd">
<c:product />
</c:sum>
</rate>
</template>
Short-hand template application.
@ -186,8 +178,32 @@
we cannot support the generation of each of those things within
templates.
<template name="_match-child_" desc="Template with a match child">
<match on="foo" />
</template>
<template name="_tpl-param_" desc="Template with a param">
<param name="@foo@" desc="A parameter" />
<param name="@bar@" desc="Another parameter" />
</template>
<template name="_tpl-param_body_"
desc="Template with params with bodies">
<param name="@text@" desc="A param with a literal">
<text>lonely foo</text>
</param>
<param name="@ref@" desc="A param with a ref">
<param-value name="@text@" />
</param>
<param name="@both@" desc="A param with both literal and ref">
<text>foo </text>
<param-value name="@text@" />
<text> bar</text>
</param>
</template>
</package>

View File

@ -11,8 +11,6 @@ mypath=$(dirname "$0")
# Performing this check within `<()` below won't cause a failure.
: "${P_XMLLINT?}" # conf.sh
tamer-flag-or-exit-ok wip-asg-derived-xmli
run-test() {
local name="${1?Missing test name}"
local dir="${2?Missing dir}"
@ -45,8 +43,13 @@ timed-tamec() {
# but it'll be close enough.
local -i start_ns=$(date +%s%N)
local objty=xmlo
if [ -f "$dir/is-experimental" ]; then
objty=xmlo-experimental
fi
command time -f "%F/%Rfault %I/%Oio %Mrss %c/%wctx \n%C" -o "$dir/time.log" \
"${TAMER_PATH_TAMEC?}" -o "$dir/$out" --emit xmlo "$dir/$in" \
"${TAMER_PATH_TAMEC?}" -o "$dir/$out" --emit "$objty" "$dir/$in" \
&> "$dir/tamec-$out.log" \
|| ret=$?
@ -66,7 +69,7 @@ timed-tamec() {
header() {
# allocate enough space based on the path we'll output
local -i mypath_len=${#mypath}
local -i dirlen=$((mypath_len + 12))
local -i dirlen=$((mypath_len + 14))
# newline intentionally omitted
printf "%-${dirlen}s %-20s " "$@"
@ -101,6 +104,11 @@ test-derive-from-src() {
test-fixpoint() {
local dir="${1?Missing directory name}"
if [ -f "$dir/no-fixpoint" ]; then
echo -n '!!!WARNING!!! test skipped: `no-fixpoint` file '
return
fi
timed-tamec "$dir" out.xmli out-2.xmli || return
diff <("$P_XMLLINT" --format "$dir/expected.xml" || echo 'ERR expected.xml') \