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-13163main
parent
7314562671
commit
8685527feb
|
@ -117,6 +117,14 @@ pub enum Nir {
|
||||||
/// See also [`Self::BindIdent`] for a concrete identifier.
|
/// See also [`Self::BindIdent`] for a concrete identifier.
|
||||||
BindIdentAbstract(SPair),
|
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
|
/// Reference the value of the given identifier as the subject of the
|
||||||
/// current expression.
|
/// current expression.
|
||||||
///
|
///
|
||||||
|
@ -203,8 +211,10 @@ impl Nir {
|
||||||
|
|
||||||
Open(_, _) | Close(_, _) => None,
|
Open(_, _) | Close(_, _) => None,
|
||||||
|
|
||||||
BindIdent(spair) | RefSubject(spair) | Ref(spair) | Desc(spair)
|
BindIdent(spair) | BindIdentMeta(spair) | RefSubject(spair)
|
||||||
| Text(spair) | Import(spair) => Some(spair.symbol()),
|
| Ref(spair) | Desc(spair) | Text(spair) | Import(spair) => {
|
||||||
|
Some(spair.symbol())
|
||||||
|
}
|
||||||
|
|
||||||
// An abstract identifier does not yet have a concrete symbol
|
// An abstract identifier does not yet have a concrete symbol
|
||||||
// assigned;
|
// assigned;
|
||||||
|
@ -243,6 +253,7 @@ impl Functor<SymbolId> for Nir {
|
||||||
|
|
||||||
BindIdent(spair) => BindIdent(spair.map(f)),
|
BindIdent(spair) => BindIdent(spair.map(f)),
|
||||||
BindIdentAbstract(spair) => BindIdentAbstract(spair.map(f)),
|
BindIdentAbstract(spair) => BindIdentAbstract(spair.map(f)),
|
||||||
|
BindIdentMeta(spair) => BindIdentMeta(spair.map(f)),
|
||||||
RefSubject(spair) => RefSubject(spair.map(f)),
|
RefSubject(spair) => RefSubject(spair.map(f)),
|
||||||
Ref(spair) => Ref(spair.map(f)),
|
Ref(spair) => Ref(spair.map(f)),
|
||||||
Desc(spair) => Desc(spair.map(f)),
|
Desc(spair) => Desc(spair.map(f)),
|
||||||
|
@ -371,6 +382,7 @@ impl Token for Nir {
|
||||||
|
|
||||||
BindIdent(spair)
|
BindIdent(spair)
|
||||||
| BindIdentAbstract(spair)
|
| BindIdentAbstract(spair)
|
||||||
|
| BindIdentMeta(spair)
|
||||||
| RefSubject(spair)
|
| RefSubject(spair)
|
||||||
| Ref(spair)
|
| Ref(spair)
|
||||||
| Desc(spair)
|
| Desc(spair)
|
||||||
|
@ -412,6 +424,13 @@ impl Display for Nir {
|
||||||
TtQuote::wrap(spair)
|
TtQuote::wrap(spair)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
BindIdentMeta(spair) => {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"bind metavariable to concreate identifier {}",
|
||||||
|
TtQuote::wrap(spair)
|
||||||
|
)
|
||||||
|
}
|
||||||
RefSubject(spair) => {
|
RefSubject(spair) => {
|
||||||
write!(f, "subject ref {}", TtQuote::wrap(spair))
|
write!(f, "subject ref {}", TtQuote::wrap(spair))
|
||||||
}
|
}
|
||||||
|
|
|
@ -128,33 +128,14 @@
|
||||||
//! At the time of writing,
|
//! At the time of writing,
|
||||||
//! no part of TAMER yet enforces metavariable naming conventions.
|
//! no part of TAMER yet enforces metavariable naming conventions.
|
||||||
|
|
||||||
use super::{Nir, NirEntity};
|
use super::Nir;
|
||||||
use crate::{parse::prelude::*, sym::GlobalSymbolResolve};
|
use crate::{parse::prelude::*, sym::GlobalSymbolResolve};
|
||||||
use memchr::memchr;
|
use memchr::memchr;
|
||||||
|
|
||||||
use Nir::*;
|
use Nir::*;
|
||||||
use NirEntity::*;
|
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Default)]
|
#[derive(Debug, PartialEq, Default)]
|
||||||
pub enum AbstractBindTranslate {
|
pub struct AbstractBindTranslate;
|
||||||
#[default]
|
|
||||||
Ready,
|
|
||||||
|
|
||||||
/// The next bind is in the context of a metavariable definition
|
|
||||||
/// (e.g. template parameter).
|
|
||||||
///
|
|
||||||
/// This should always be accurate since the grammar of NIR,
|
|
||||||
/// at least in XML form,
|
|
||||||
/// both requires binding as part of the attribute list and so before any
|
|
||||||
/// other opening element and may only contain one such attribute.
|
|
||||||
/// If this assumption does not hold in the future,
|
|
||||||
/// then the metavariable identifier will not be defined and will
|
|
||||||
/// result in a reference instead,
|
|
||||||
/// leading to errors.
|
|
||||||
/// Until then,
|
|
||||||
/// this is a simple and straightforward implementation.
|
|
||||||
BindingMeta,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Display for AbstractBindTranslate {
|
impl Display for AbstractBindTranslate {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||||
|
@ -175,18 +156,12 @@ impl ParseState for AbstractBindTranslate {
|
||||||
tok: Self::Token,
|
tok: Self::Token,
|
||||||
_: &mut Self::Context,
|
_: &mut Self::Context,
|
||||||
) -> TransitionResult<Self::Super> {
|
) -> TransitionResult<Self::Super> {
|
||||||
use AbstractBindTranslate::*;
|
match tok {
|
||||||
|
BindIdent(name) if needs_translation(name) => {
|
||||||
match (self, tok) {
|
Transition(self).ok(BindIdentAbstract(name))
|
||||||
(Ready, BindIdent(name)) if needs_translation(name) => {
|
|
||||||
Transition(Ready).ok(BindIdentAbstract(name))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
(Ready, tok @ Open(TplParam, _)) => Transition(BindingMeta).ok(tok),
|
_ => Transition(self).ok(tok),
|
||||||
|
|
||||||
(BindingMeta, tok @ BindIdent(_)) => Transition(Ready).ok(tok),
|
|
||||||
|
|
||||||
(st @ (Ready | BindingMeta), tok) => Transition(st).ok(tok),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -70,35 +70,13 @@ fn non_meta_concrete_bind_with_metavar_naming_translated_to_abstract_bind() {
|
||||||
fn meta_concrete_bind_with_metavar_naming_ignored() {
|
fn meta_concrete_bind_with_metavar_naming_ignored() {
|
||||||
// This is named as a metavariable.
|
// This is named as a metavariable.
|
||||||
let name = "@param@".into();
|
let name = "@param@".into();
|
||||||
let name_outside = "@foo@".into();
|
|
||||||
|
|
||||||
#[rustfmt::skip]
|
assert_tok_translate(
|
||||||
let toks = [
|
// This identifier utilizes a metavariable naming convention,
|
||||||
Open(TplParam, S1),
|
// but we're in a metavariable definition context.
|
||||||
// This identifier utilizes a metavariable naming convention,
|
BindIdentMeta(SPair(name, S2)),
|
||||||
// but we're in a template parameter context,
|
// And so the bind stays concrete.
|
||||||
// which defines a metavariable.
|
BindIdentMeta(SPair(name, S2)),
|
||||||
BindIdent(SPair(name, S2)),
|
|
||||||
Close(TplParam, S3),
|
|
||||||
|
|
||||||
// Back outside of a param context.
|
|
||||||
BindIdent(SPair(name_outside, S4)),
|
|
||||||
];
|
|
||||||
|
|
||||||
#[rustfmt::skip]
|
|
||||||
assert_eq!(
|
|
||||||
Ok(vec![
|
|
||||||
O(Open(TplParam, S1)),
|
|
||||||
// Since we are in a metavariable definition context,
|
|
||||||
// the bind _stays concrete_.
|
|
||||||
O(BindIdent(SPair(name, S2))),
|
|
||||||
O(Close(TplParam, S3)),
|
|
||||||
|
|
||||||
// This one is now outside the metavariable context,
|
|
||||||
// and so we should go back to translating to abstract.
|
|
||||||
O(BindIdentAbstract(SPair(name_outside, S4))),
|
|
||||||
]),
|
|
||||||
Sut::parse(toks.into_iter()).collect()
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -254,6 +254,9 @@ impl ParseState for NirToAir {
|
||||||
(Ready, Open(TplParam, span)) => {
|
(Ready, Open(TplParam, span)) => {
|
||||||
Transition(Meta(span)).ok(Air::MetaStart(span))
|
Transition(Meta(span)).ok(Air::MetaStart(span))
|
||||||
}
|
}
|
||||||
|
(Meta(mspan), BindIdentMeta(spair)) => {
|
||||||
|
Transition(Meta(mspan)).ok(Air::BindIdent(spair))
|
||||||
|
}
|
||||||
(Meta(mspan), Text(lexeme)) => {
|
(Meta(mspan), Text(lexeme)) => {
|
||||||
Transition(Meta(mspan)).ok(Air::MetaLexeme(lexeme))
|
Transition(Meta(mspan)).ok(Air::MetaLexeme(lexeme))
|
||||||
}
|
}
|
||||||
|
@ -263,9 +266,10 @@ impl ParseState for NirToAir {
|
||||||
// Some of these will be permitted in the future.
|
// Some of these will be permitted in the future.
|
||||||
(
|
(
|
||||||
Meta(mspan),
|
Meta(mspan),
|
||||||
tok @ (Open(..) | Close(..) | Ref(..) | RefSubject(..)),
|
tok @ (Open(..) | Close(..) | BindIdent(..) | Ref(..)
|
||||||
|
| RefSubject(..)),
|
||||||
) => Transition(Meta(mspan))
|
) => Transition(Meta(mspan))
|
||||||
.err(NirToAirError::UnexpectedMetaToken(mspan, tok)),
|
.err(NirToAirError::ExpectedMetaToken(mspan, tok)),
|
||||||
|
|
||||||
(Ready, Text(text)) => Transition(Ready).ok(Air::DocText(text)),
|
(Ready, Text(text)) => Transition(Ready).ok(Air::DocText(text)),
|
||||||
|
|
||||||
|
@ -277,8 +281,8 @@ impl ParseState for NirToAir {
|
||||||
),
|
),
|
||||||
) => Transition(Ready).ok(Air::ExprEnd(span)),
|
) => Transition(Ready).ok(Air::ExprEnd(span)),
|
||||||
|
|
||||||
(st @ (Ready | Meta(_)), BindIdent(spair)) => {
|
(Ready, BindIdent(spair)) => {
|
||||||
Transition(st).ok(Air::BindIdent(spair))
|
Transition(Ready).ok(Air::BindIdent(spair))
|
||||||
}
|
}
|
||||||
(st @ (Ready | Meta(_)), BindIdentAbstract(spair)) => {
|
(st @ (Ready | Meta(_)), BindIdentAbstract(spair)) => {
|
||||||
Transition(st).ok(Air::BindIdentAbstract(spair))
|
Transition(st).ok(Air::BindIdentAbstract(spair))
|
||||||
|
@ -300,6 +304,7 @@ impl ParseState for NirToAir {
|
||||||
// This assumption is only valid so long as that's the only
|
// This assumption is only valid so long as that's the only
|
||||||
// thing producing NIR.
|
// thing producing NIR.
|
||||||
(st @ Meta(..), tok @ Import(_)) => Transition(st).dead(tok),
|
(st @ Meta(..), tok @ Import(_)) => Transition(st).dead(tok),
|
||||||
|
(st @ Ready, tok @ BindIdentMeta(_)) => Transition(st).dead(tok),
|
||||||
|
|
||||||
// Unsupported tokens yield errors.
|
// Unsupported tokens yield errors.
|
||||||
// There _is_ a risk that this will put us in a wildly
|
// There _is_ a risk that this will put us in a wildly
|
||||||
|
@ -354,7 +359,7 @@ pub enum NirToAirError {
|
||||||
|
|
||||||
/// The provided [`Nir`] token of input was unexpected for the body of a
|
/// The provided [`Nir`] token of input was unexpected for the body of a
|
||||||
/// metavariable that was opened at the provided [`Span`].
|
/// metavariable that was opened at the provided [`Span`].
|
||||||
UnexpectedMetaToken(Span, Nir),
|
ExpectedMetaToken(Span, Nir),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Display for NirToAirError {
|
impl Display for NirToAirError {
|
||||||
|
@ -374,7 +379,7 @@ impl Display for NirToAirError {
|
||||||
write!(f, "match body is not yet supported by TAMER")
|
write!(f, "match body is not yet supported by TAMER")
|
||||||
}
|
}
|
||||||
|
|
||||||
UnexpectedMetaToken(_, tok) => {
|
ExpectedMetaToken(_, tok) => {
|
||||||
write!(
|
write!(
|
||||||
f,
|
f,
|
||||||
"expected lexical token for metavariable, found {tok}"
|
"expected lexical token for metavariable, found {tok}"
|
||||||
|
@ -423,7 +428,7 @@ impl Diagnostic for NirToAirError {
|
||||||
// The user should have been preempted by the parent parser
|
// The user should have been preempted by the parent parser
|
||||||
// (e.g. XML->Nir),
|
// (e.g. XML->Nir),
|
||||||
// and so shouldn't see this.
|
// and so shouldn't see this.
|
||||||
UnexpectedMetaToken(mspan, given) => vec![
|
ExpectedMetaToken(mspan, given) => vec![
|
||||||
mspan.note("while parsing the body of this metavariable"),
|
mspan.note("while parsing the body of this metavariable"),
|
||||||
given.span().error("expected a lexical token here"),
|
given.span().error("expected a lexical token here"),
|
||||||
],
|
],
|
||||||
|
|
|
@ -207,7 +207,7 @@ fn tpl_with_param() {
|
||||||
BindIdent(name_tpl),
|
BindIdent(name_tpl),
|
||||||
|
|
||||||
Open(TplParam, S3),
|
Open(TplParam, S3),
|
||||||
BindIdent(name_param),
|
BindIdentMeta(name_param),
|
||||||
Desc(desc_param),
|
Desc(desc_param),
|
||||||
Close(TplParam, S6),
|
Close(TplParam, S6),
|
||||||
Close(Tpl, S7),
|
Close(Tpl, S7),
|
||||||
|
@ -267,12 +267,12 @@ fn apply_template_long_form_args() {
|
||||||
RefSubject(name),
|
RefSubject(name),
|
||||||
|
|
||||||
Open(TplParam, S3),
|
Open(TplParam, S3),
|
||||||
BindIdent(p1),
|
BindIdentMeta(p1),
|
||||||
Text(v1),
|
Text(v1),
|
||||||
Close(TplParam, S6),
|
Close(TplParam, S6),
|
||||||
|
|
||||||
Open(TplParam, S7),
|
Open(TplParam, S7),
|
||||||
BindIdent(p2),
|
BindIdentMeta(p2),
|
||||||
Text(v2),
|
Text(v2),
|
||||||
Close(TplParam, S10),
|
Close(TplParam, S10),
|
||||||
Close(TplApply, S11),
|
Close(TplApply, S11),
|
||||||
|
|
|
@ -282,7 +282,7 @@ impl ParseState for InterpState {
|
||||||
let GenIdentSymbolId(ident_sym) = gen_ident;
|
let GenIdentSymbolId(ident_sym) = gen_ident;
|
||||||
|
|
||||||
Transition(GenDesc(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)
|
.with_lookahead(tok)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -99,7 +99,7 @@ fn expect_expanded_header(
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
sut.next(),
|
sut.next(),
|
||||||
Some(Ok(Object(Nir::BindIdent(SPair(expect_name_sym, span))))),
|
Some(Ok(Object(Nir::BindIdentMeta(SPair(expect_name_sym, span))))),
|
||||||
);
|
);
|
||||||
assert_matches!(
|
assert_matches!(
|
||||||
sut.next(),
|
sut.next(),
|
||||||
|
|
|
@ -1425,7 +1425,7 @@ ele_parse! {
|
||||||
/// expanded.
|
/// expanded.
|
||||||
TplParamStmt := QN_PARAM(_, ospan) {
|
TplParamStmt := QN_PARAM(_, ospan) {
|
||||||
@ {
|
@ {
|
||||||
QN_NAME => BindIdent,
|
QN_NAME => BindIdentMeta,
|
||||||
QN_DESC => Desc,
|
QN_DESC => Desc,
|
||||||
} => Nir::Open(NirEntity::TplParam, ospan.into()),
|
} => Nir::Open(NirEntity::TplParam, ospan.into()),
|
||||||
/(cspan) => Nir::Close(NirEntity::TplParam, cspan.into()),
|
/(cspan) => Nir::Close(NirEntity::TplParam, cspan.into()),
|
||||||
|
@ -1697,7 +1697,7 @@ ele_parse! {
|
||||||
/// which gets desugared into this via [`super::tplshort`].
|
/// which gets desugared into this via [`super::tplshort`].
|
||||||
ApplyTemplateParam := QN_WITH_PARAM(_, ospan) {
|
ApplyTemplateParam := QN_WITH_PARAM(_, ospan) {
|
||||||
@ {
|
@ {
|
||||||
QN_NAME => BindIdent,
|
QN_NAME => BindIdentMeta,
|
||||||
QN_VALUE => Text,
|
QN_VALUE => Text,
|
||||||
} => Nir::Open(NirEntity::TplParam, ospan.into()),
|
} => Nir::Open(NirEntity::TplParam, ospan.into()),
|
||||||
/(cspan) => Nir::Close(NirEntity::TplParam, cspan.into()),
|
/(cspan) => Nir::Close(NirEntity::TplParam, cspan.into()),
|
||||||
|
|
|
@ -179,7 +179,7 @@ impl ParseState for TplShortDesugar {
|
||||||
// note: reversed (stack)
|
// note: reversed (stack)
|
||||||
stack.push(Close(TplParam, span));
|
stack.push(Close(TplParam, span));
|
||||||
stack.push(Text(val));
|
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))
|
Transition(DesugaringParams(ospan)).ok(Open(TplParam, span))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -215,7 +215,7 @@ impl ParseState for TplShortDesugar {
|
||||||
stack.push(Close(TplApply, ospan));
|
stack.push(Close(TplApply, ospan));
|
||||||
stack.push(Close(TplParam, ospan));
|
stack.push(Close(TplParam, ospan));
|
||||||
stack.push(Text(SPair(gen_name, 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
|
// Note that we must have `tok` as lookahead instead of
|
||||||
// pushing directly on the stack in case it's a
|
// pushing directly on the stack in case it's a
|
||||||
|
|
|
@ -86,7 +86,7 @@ fn desugars_unary() {
|
||||||
|
|
||||||
O(Open(TplParam, S2)),
|
O(Open(TplParam, S2)),
|
||||||
// Derived from `aname` (by padding)
|
// Derived from `aname` (by padding)
|
||||||
O(BindIdent(pname)),
|
O(BindIdentMeta(pname)),
|
||||||
// The value is left untouched.
|
// The value is left untouched.
|
||||||
O(Text(pval)),
|
O(Text(pval)),
|
||||||
// Close is derived from open.
|
// 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
|
// @values@ remains lexical by referencing the name of a
|
||||||
// template we're about to generate.
|
// template we're about to generate.
|
||||||
O(Open(TplParam, S1)),
|
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(Text(SPair(gen_name, S1))), //:-.
|
||||||
O(Close(TplParam, S1)), // |
|
O(Close(TplParam, S1)), // |
|
||||||
O(Close(TplApply, S1)), // |
|
O(Close(TplApply, S1)), // |
|
||||||
|
@ -193,7 +193,7 @@ fn desugar_nested_apply() {
|
||||||
|
|
||||||
// @values@
|
// @values@
|
||||||
O(Open(TplParam, S1)),
|
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(Text(SPair(gen_name_outer, S1))), //:-.
|
||||||
O(Close(TplParam, S1)), // |
|
O(Close(TplParam, S1)), // |
|
||||||
O(Close(TplApply, S1)), // |
|
O(Close(TplApply, S1)), // |
|
||||||
|
@ -227,7 +227,7 @@ fn does_not_desugar_long_form() {
|
||||||
BindIdent(name),
|
BindIdent(name),
|
||||||
|
|
||||||
Open(TplParam, S3),
|
Open(TplParam, S3),
|
||||||
BindIdent(pname),
|
BindIdentMeta(pname),
|
||||||
Text(pval),
|
Text(pval),
|
||||||
Close(TplParam, S6),
|
Close(TplParam, S6),
|
||||||
Close(TplApply, S7),
|
Close(TplApply, S7),
|
||||||
|
@ -244,7 +244,7 @@ fn does_not_desugar_long_form() {
|
||||||
O(BindIdent(name)),
|
O(BindIdent(name)),
|
||||||
|
|
||||||
O(Open(TplParam, S3)),
|
O(Open(TplParam, S3)),
|
||||||
O(BindIdent(pname)),
|
O(BindIdentMeta(pname)),
|
||||||
O(Text(pval)),
|
O(Text(pval)),
|
||||||
O(Close(TplParam, S6)),
|
O(Close(TplParam, S6)),
|
||||||
O(Close(TplApply, S7)),
|
O(Close(TplApply, S7)),
|
||||||
|
|
Loading…
Reference in New Issue