tamer: asg::graph::object::xir: Initial rate element reconstruction
This extends the POC a bit by beginning to reconstruct rate blocks (note that NIR isn't producing sub-expressions yet). Importantly, this also adds the first system tests, now that we have an end-to-end system. This not only gives me confidence that the system is producing the expected output, but serves as a compromise: writing unit or integration tests for this program derivation would be a great deal of work, and wouldn't even catch the bugs I'm worried most about; the lowering operation can be written in such a way as to give me high confidence in its correctness without those more granular tests, or in conjunction with unit or integration tests for a smaller portion. DEV-13708main
parent
95272c4593
commit
82915f11af
|
@ -61,3 +61,5 @@ fi
|
||||||
|
|
||||||
declare -r TAMER_PATH_TAMEC="$TAMER_PATH_BIN/tamec"
|
declare -r TAMER_PATH_TAMEC="$TAMER_PATH_BIN/tamec"
|
||||||
declare -r TAMER_PATH_TAMELD="$TAMER_PATH_BIN/tameld"
|
declare -r TAMER_PATH_TAMELD="$TAMER_PATH_BIN/tameld"
|
||||||
|
|
||||||
|
declare -r P_XMLLINT="@XMLLINT@"
|
||||||
|
|
|
@ -123,6 +123,10 @@ test -z "$FEATURES" || {
|
||||||
FEATURES="--features $FEATURES"
|
FEATURES="--features $FEATURES"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Other programs used by scripts
|
||||||
|
AC_CHECK_PROGS(XMLLINT, [xmllint])
|
||||||
|
test -n "$XMLLINT" || AC_MSG_ERROR([xmllint not found])
|
||||||
|
|
||||||
AC_CONFIG_FILES([Makefile conf.sh])
|
AC_CONFIG_FILES([Makefile conf.sh])
|
||||||
|
|
||||||
AC_OUTPUT
|
AC_OUTPUT
|
||||||
|
|
|
@ -53,6 +53,12 @@ impl Expr {
|
||||||
Expr(_, _, span) => *span,
|
Expr(_, _, span) => *span,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn op(&self) -> ExprOp {
|
||||||
|
match self {
|
||||||
|
Expr(op, _, _) => *op,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Functor<Span> for Expr {
|
impl Functor<Span> for Expr {
|
||||||
|
@ -82,7 +88,7 @@ impl Display for Expr {
|
||||||
/// TODO: Ideally this will be replaced with arbitrary binary (dyadic)
|
/// TODO: Ideally this will be replaced with arbitrary binary (dyadic)
|
||||||
/// functions defined within the language of TAME itself,
|
/// functions defined within the language of TAME itself,
|
||||||
/// as was the original plan with TAMER.
|
/// as was the original plan with TAMER.
|
||||||
#[derive(Debug, PartialEq, Eq)]
|
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
|
||||||
pub enum ExprOp {
|
pub enum ExprOp {
|
||||||
Sum,
|
Sum,
|
||||||
Product,
|
Product,
|
||||||
|
@ -284,4 +290,12 @@ impl ObjectIndex<Expr> {
|
||||||
let identi = asg.lookup_or_missing(ident);
|
let identi = asg.lookup_or_missing(ident);
|
||||||
self.add_edge_to(asg, identi, Some(ident.span()))
|
self.add_edge_to(asg, identi, Some(ident.span()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The [`Ident`] bound to this expression,
|
||||||
|
/// if any.
|
||||||
|
pub fn ident(self, asg: &Asg) -> Option<&Ident> {
|
||||||
|
self.incoming_edges_filtered(asg)
|
||||||
|
.map(ObjectIndex::cresolve(asg))
|
||||||
|
.next()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,18 +29,24 @@
|
||||||
//! but may be useful in the future for concrete code suggestions/fixes,
|
//! but may be useful in the future for concrete code suggestions/fixes,
|
||||||
//! or observing template expansions.
|
//! or observing template expansions.
|
||||||
|
|
||||||
use super::ObjectRelTy;
|
use super::{DynObjectRel, Expr, Object, ObjectIndex, ObjectRelTy, Pkg};
|
||||||
use crate::{
|
use crate::{
|
||||||
asg::{visit::TreeWalkRel, Asg},
|
asg::{
|
||||||
diagnose::Annotate,
|
visit::{Depth, TreeWalkRel},
|
||||||
diagnostic_unreachable,
|
Asg, ExprOp,
|
||||||
parse::prelude::*,
|
},
|
||||||
sym::st::raw::URI_LV_RATER,
|
diagnose::{panic::DiagnosticPanic, Annotate},
|
||||||
|
diagnostic_panic, diagnostic_unreachable,
|
||||||
|
parse::{prelude::*, util::SPair},
|
||||||
|
span::{Span, UNKNOWN_SPAN},
|
||||||
|
sym::{
|
||||||
|
st::{URI_LV_CALC, URI_LV_RATER, URI_LV_TPL},
|
||||||
|
UriStaticSymbolId,
|
||||||
|
},
|
||||||
xir::{
|
xir::{
|
||||||
attr::Attr,
|
|
||||||
flat::{Text, XirfToken},
|
flat::{Text, XirfToken},
|
||||||
st::qname::{QN_PACKAGE, QN_XMLNS},
|
st::qname::*,
|
||||||
OpenSpan,
|
OpenSpan, QName,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
use arrayvec::ArrayVec;
|
use arrayvec::ArrayVec;
|
||||||
|
@ -63,55 +69,145 @@ impl<'a> Display for AsgTreeToXirf<'a> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Xirf = XirfToken<Text>;
|
||||||
|
|
||||||
impl<'a> ParseState for AsgTreeToXirf<'a> {
|
impl<'a> ParseState for AsgTreeToXirf<'a> {
|
||||||
type Token = TreeWalkRel;
|
type Token = TreeWalkRel;
|
||||||
type Object = XirfToken<Text>;
|
type Object = Xirf;
|
||||||
type Error = Infallible;
|
type Error = Infallible;
|
||||||
type Context = TreeContext<'a>;
|
type Context = TreeContext<'a>;
|
||||||
|
|
||||||
fn parse_token(
|
fn parse_token(
|
||||||
self,
|
self,
|
||||||
tok: Self::Token,
|
tok: Self::Token,
|
||||||
TreeContext(tok_stack, asg): &mut TreeContext,
|
TreeContext(toks, asg): &mut TreeContext,
|
||||||
) -> TransitionResult<Self::Super> {
|
) -> TransitionResult<Self::Super> {
|
||||||
use ObjectRelTy as Ty;
|
if let Some(emit) = toks.pop() {
|
||||||
|
|
||||||
if let Some(emit) = tok_stack.pop() {
|
|
||||||
return Transition(self).ok(emit).with_lookahead(tok);
|
return Transition(self).ok(emit).with_lookahead(tok);
|
||||||
}
|
}
|
||||||
|
|
||||||
let tok_span = tok.span();
|
let tok_span = tok.span();
|
||||||
let TreeWalkRel(dyn_rel, depth) = tok;
|
let TreeWalkRel(dyn_rel, depth) = tok;
|
||||||
|
|
||||||
|
if depth == Depth(0) {
|
||||||
|
return Transition(self).incomplete();
|
||||||
|
}
|
||||||
|
|
||||||
let obj = dyn_rel.target().resolve(asg);
|
let obj = dyn_rel.target().resolve(asg);
|
||||||
let obj_span = obj.span();
|
|
||||||
|
|
||||||
match dyn_rel.target_ty() {
|
match obj {
|
||||||
Ty::Pkg => {
|
Object::Pkg(pkg) => {
|
||||||
tok_stack.push(XirfToken::Attr(Attr::new(
|
let span = pkg.span();
|
||||||
QN_XMLNS,
|
|
||||||
URI_LV_RATER,
|
|
||||||
(obj_span, obj_span),
|
|
||||||
)));
|
|
||||||
|
|
||||||
Transition(self).ok(XirfToken::Open(
|
toks.push(ns(QN_XMLNS_T, URI_LV_TPL, span));
|
||||||
QN_PACKAGE,
|
toks.push(ns(QN_XMLNS_C, URI_LV_CALC, span));
|
||||||
OpenSpan::without_name_span(obj_span),
|
toks.push(ns(QN_XMLNS, URI_LV_RATER, span));
|
||||||
depth,
|
|
||||||
))
|
Transition(self).ok(package(pkg, depth))
|
||||||
}
|
}
|
||||||
|
|
||||||
Ty::Ident | Ty::Expr => Transition(self).incomplete(),
|
// Identifiers will be considered in context;
|
||||||
|
// pass over it for now.
|
||||||
|
Object::Ident(..) => Transition(self).incomplete(),
|
||||||
|
|
||||||
Ty::Root => diagnostic_unreachable!(
|
Object::Expr(expr) => match dyn_rel.source_ty() {
|
||||||
|
ObjectRelTy::Ident => {
|
||||||
|
// We were just told an ident exists,
|
||||||
|
// so this should not fail.
|
||||||
|
let ident = dyn_rel
|
||||||
|
.must_narrow_into::<Expr>()
|
||||||
|
.ident(asg)
|
||||||
|
.diagnostic_unwrap(|| {
|
||||||
|
vec![expr.internal_error(
|
||||||
|
"missing ident for this expression",
|
||||||
|
)]
|
||||||
|
});
|
||||||
|
|
||||||
|
toks.push(yields(ident.name(), expr.span()));
|
||||||
|
|
||||||
|
Transition(Self::Ready(Default::default()))
|
||||||
|
.ok(stmt(expr, depth))
|
||||||
|
}
|
||||||
|
_ => todo!("non-ident expr"),
|
||||||
|
},
|
||||||
|
|
||||||
|
Object::Root(_) => diagnostic_unreachable!(
|
||||||
vec![tok_span.error("unexpected Root")],
|
vec![tok_span.error("unexpected Root")],
|
||||||
"tree walk is not expected to emit Root",
|
"tree walk is not expected to emit Root",
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_accepting(&self, _ctx: &Self::Context) -> bool {
|
fn is_accepting(&self, TreeContext(toks, _): &Self::Context) -> bool {
|
||||||
true
|
toks.is_empty()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn eof_tok(
|
||||||
|
&self,
|
||||||
|
TreeContext(toks, _): &Self::Context,
|
||||||
|
) -> Option<Self::Token> {
|
||||||
|
// If the stack is not empty on EOF,
|
||||||
|
// yield a dummy token just to invoke `parse_token` to finish
|
||||||
|
// emptying it.
|
||||||
|
(!toks.is_empty()).then_some(TreeWalkRel(
|
||||||
|
DynObjectRel::new(
|
||||||
|
ObjectRelTy::Root,
|
||||||
|
ObjectRelTy::Root,
|
||||||
|
ObjectIndex::new(0.into(), UNKNOWN_SPAN),
|
||||||
|
None,
|
||||||
|
),
|
||||||
|
// This is the only part that really matters;
|
||||||
|
// the tree walk will never yield a depth of 0.
|
||||||
|
Depth(0),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn package(pkg: &Pkg, depth: Depth) -> Xirf {
|
||||||
|
Xirf::open(QN_PACKAGE, OpenSpan::without_name_span(pkg.span()), depth)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ns(qname: QName, uri: UriStaticSymbolId, span: Span) -> Xirf {
|
||||||
|
Xirf::attr(qname, uri, (span, span))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn stmt(expr: &Expr, depth: Depth) -> Xirf {
|
||||||
|
match expr.op() {
|
||||||
|
ExprOp::Sum => {
|
||||||
|
Xirf::open(QN_RATE, OpenSpan::without_name_span(expr.span()), depth)
|
||||||
|
}
|
||||||
|
|
||||||
|
_ => todo!("stmt: {expr:?}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn yields(name: SPair, span: Span) -> Xirf {
|
||||||
|
Xirf::attr(QN_YIELDS, name, (span, name))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct TreeContext<'a>(TokenStack, &'a Asg);
|
||||||
|
|
||||||
|
// Custom `Debug` impl to omit ASG rendering,
|
||||||
|
// since it's large and already included while rendering other parts of
|
||||||
|
// the lowering pipeline.
|
||||||
|
// Of course,
|
||||||
|
// that's assuming this is part of the lowering pipeline.
|
||||||
|
impl<'a> std::fmt::Debug for TreeContext<'a> {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||||
|
f.debug_tuple("TreeContext")
|
||||||
|
.field(&self.0)
|
||||||
|
.field(&AsgElided)
|
||||||
|
.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Used a placeholder for [`TreeContext`]'s [`Debug`].
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct AsgElided;
|
||||||
|
|
||||||
|
impl<'a> From<&'a Asg> for TreeContext<'a> {
|
||||||
|
fn from(asg: &'a Asg) -> Self {
|
||||||
|
Self(Default::default(), asg)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -132,14 +228,39 @@ const TOK_STACK_SIZE: usize = 8;
|
||||||
/// This need only be big enough to accommodate [`AsgTreeToXirf`]'s
|
/// This need only be big enough to accommodate [`AsgTreeToXirf`]'s
|
||||||
/// implementation;
|
/// implementation;
|
||||||
/// the size is independent of user input.
|
/// the size is independent of user input.
|
||||||
type TokenStack<'a> =
|
#[derive(Debug, Default)]
|
||||||
ArrayVec<<AsgTreeToXirf<'a> as ParseState>::Object, TOK_STACK_SIZE>;
|
struct TokenStack(ArrayVec<Xirf, TOK_STACK_SIZE>);
|
||||||
|
|
||||||
#[derive(Debug)]
|
impl TokenStack {
|
||||||
pub struct TreeContext<'a>(TokenStack<'a>, &'a Asg);
|
fn push(&mut self, tok: Xirf) {
|
||||||
|
match self {
|
||||||
|
Self(stack) => {
|
||||||
|
if stack.is_full() {
|
||||||
|
diagnostic_panic!(
|
||||||
|
vec![tok.internal_error(
|
||||||
|
"while emitting a token for this object"
|
||||||
|
)],
|
||||||
|
"token stack exhausted (increase TOK_STACK_SIZE)",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
impl<'a> From<&'a Asg> for TreeContext<'a> {
|
stack.push(tok)
|
||||||
fn from(asg: &'a Asg) -> Self {
|
}
|
||||||
Self(Default::default(), asg)
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn pop(&mut self) -> Option<Xirf> {
|
||||||
|
match self {
|
||||||
|
Self(stack) => stack.pop(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_empty(&self) -> bool {
|
||||||
|
match self {
|
||||||
|
Self(stack) => stack.is_empty(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// System tests covering this functionality can be found in
|
||||||
|
// `tamer/tests/xir/`.
|
||||||
|
|
|
@ -132,6 +132,12 @@ impl From<SPair> for (SymbolId, Span) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<SPair> for SymbolId {
|
||||||
|
fn from(spair: SPair) -> Self {
|
||||||
|
spair.symbol()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq)]
|
#[derive(Debug, PartialEq, Eq)]
|
||||||
pub struct EchoParseState<S: ClosedParseState>(S);
|
pub struct EchoParseState<S: ClosedParseState>(S);
|
||||||
|
|
||||||
|
|
|
@ -146,6 +146,48 @@ pub enum XirfToken<T: TextType> {
|
||||||
CData(SymbolId, Span, Depth),
|
CData(SymbolId, Span, Depth),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<T: TextType> XirfToken<T> {
|
||||||
|
pub fn open(
|
||||||
|
qname: impl Into<QName>,
|
||||||
|
span: impl Into<OpenSpan>,
|
||||||
|
depth: Depth,
|
||||||
|
) -> Self {
|
||||||
|
Self::Open(qname.into(), span.into(), depth)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn close(
|
||||||
|
qname: Option<impl Into<QName>>,
|
||||||
|
span: impl Into<CloseSpan>,
|
||||||
|
depth: Depth,
|
||||||
|
) -> Self {
|
||||||
|
Self::Close(qname.map(Into::into), span.into(), depth)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn attr(
|
||||||
|
qname: impl Into<QName>,
|
||||||
|
value: impl Into<SymbolId>,
|
||||||
|
span: (impl Into<Span>, impl Into<Span>),
|
||||||
|
) -> Self {
|
||||||
|
Self::Attr(Attr::new(
|
||||||
|
qname.into(),
|
||||||
|
value.into(),
|
||||||
|
(span.0.into(), span.1.into()),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn comment(
|
||||||
|
comment: impl Into<SymbolId>,
|
||||||
|
span: impl Into<Span>,
|
||||||
|
depth: Depth,
|
||||||
|
) -> Self {
|
||||||
|
Self::Comment(comment.into(), span.into(), depth)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn text(text: impl Into<T>, depth: Depth) -> Self {
|
||||||
|
Self::Text(text.into(), depth)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl<T: TextType> Token for XirfToken<T> {
|
impl<T: TextType> Token for XirfToken<T> {
|
||||||
fn ir_name() -> &'static str {
|
fn ir_name() -> &'static str {
|
||||||
"XIRF"
|
"XIRF"
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
out.xmli
|
|
@ -0,0 +1,29 @@
|
||||||
|
# XMLI System Test
|
||||||
|
The `xmli` file is an intermediate file that serves as a handoff between
|
||||||
|
TAMER and the XSLT-based compiler:
|
||||||
|
|
||||||
|
```
|
||||||
|
xml -> (TAMER) -> xmli -> (TAME XSLT) -> xmlo
|
||||||
|
```
|
||||||
|
|
||||||
|
TAMER gets the first shot at processing, and then the compilation process
|
||||||
|
continues with the XSLT-based compiler. This allows TAMER to incrementally
|
||||||
|
augment and manipulate the source file and remove responsibilities from
|
||||||
|
TAME XSLT.
|
||||||
|
|
||||||
|
Tests in this directory ensure that this process is working as
|
||||||
|
intended. TAMER's failure to perform a proper handoff will cause TAME XSLT
|
||||||
|
to compile sources incorrectly, since TAMER will have rewritten them to
|
||||||
|
something else.
|
||||||
|
|
||||||
|
This handoff is more than just echoing tokens back into a file---it
|
||||||
|
_derives_ a new program from the state of the ASG. This program may have a
|
||||||
|
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.
|
||||||
|
|
||||||
|
# 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`
|
||||||
|
relative to the working directory.
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
<package xmlns="http://www.lovullo.com/rater"
|
||||||
|
xmlns:c="http://www.lovullo.com/calc"
|
||||||
|
xmlns:t="http://www.lovullo.com/rater/apply-template">
|
||||||
|
|
||||||
|
<rate yields="rateFoo" />
|
||||||
|
<rate yields="rateBar" />
|
||||||
|
</package>
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
<?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 is the source package to be read by `tamec`.
|
||||||
|
The output `out.xmli` is asserted against `expected.xml`.
|
||||||
|
|
||||||
|
<rate yields="rateFoo" />
|
||||||
|
<rate yields="rateBar" />
|
||||||
|
</package>
|
||||||
|
|
|
@ -0,0 +1,35 @@
|
||||||
|
#!/bin/bash
|
||||||
|
# Assert that a program can be derived from the ASG as expected.
|
||||||
|
#
|
||||||
|
# See `./README.md` for more information.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
mypath=$(dirname "$0")
|
||||||
|
. "$mypath/../../conf.sh"
|
||||||
|
|
||||||
|
tamer-flag-or-exit-ok wip-asg-derived-xmli
|
||||||
|
|
||||||
|
main() {
|
||||||
|
"${TAMER_PATH_TAMEC?}" -o "$mypath/out.xmli" --emit xmlo "$mypath/src.xml"
|
||||||
|
|
||||||
|
# Performing this check within `<()` below won't cause a failure.
|
||||||
|
: "${P_XMLLINT?}" # conf.sh
|
||||||
|
|
||||||
|
diff <("$P_XMLLINT" --format "$mypath/expected.xml" || echo 'ERR expected.xml') \
|
||||||
|
<("$P_XMLLINT" --format "$mypath/out.xmli" || echo 'ERR out.xmli') \
|
||||||
|
|| {
|
||||||
|
cat << EOF
|
||||||
|
!!! TEST FAILED
|
||||||
|
tamec: $TAMER_PATH_TAMEC
|
||||||
|
|
||||||
|
note: The compiler output and diff between \`expected.xml\` and \`out.xmli\`
|
||||||
|
are above. Both files are formatted with \`xmllint\` automatically.
|
||||||
|
EOF
|
||||||
|
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$@"
|
||||||
|
|
Loading…
Reference in New Issue