Compare commits
146 Commits
a5b03e8790
...
6769f0c280
Author | SHA1 | Date |
---|---|---|
Mike Gerwitz | 6769f0c280 | |
Mike Gerwitz | 1706c55645 | |
Mike Gerwitz | 93cc0d2ce1 | |
Mike Gerwitz | 65c1b2d083 | |
Mike Gerwitz | 0e0f3e658d | |
Mike Gerwitz | 3800109530 | |
Mike Gerwitz | 6a99ee3cb3 | |
Mike Gerwitz | 109ba5f797 | |
Mike Gerwitz | 9c6b00a124 | |
Mike Gerwitz | f34f2644e9 | |
Mike Gerwitz | 0f20f22cfb | |
Mike Gerwitz | 2bf3122402 | |
Mike Gerwitz | b5187de5dc | |
Mike Gerwitz | 896fb3a0e5 | |
Mike Gerwitz | 1f2315436c | |
Mike Gerwitz | 9e8b809c14 | |
Mike Gerwitz | 57a805b495 | |
Mike Gerwitz | ea6259570e | |
Mike Gerwitz | 4ac8bf5981 | |
Mike Gerwitz | 294caaa35a | |
Mike Gerwitz | 19a5ec1e0f | |
Mike Gerwitz | ac9b7f620e | |
Mike Gerwitz | e8335c57d4 | |
Mike Gerwitz | c12bf439ae | |
Mike Gerwitz | da4d7f83ea | |
Mike Gerwitz | 434365543e | |
Mike Gerwitz | e940fc5aa0 | |
Mike Gerwitz | 7857460c1d | |
Mike Gerwitz | 92214c7e05 | |
Mike Gerwitz | e266d42c48 | |
Mike Gerwitz | 8b3dfe9149 | |
Mike Gerwitz | 94bbc2d725 | |
Mike Gerwitz | 716e217c9f | |
Mike Gerwitz | 92eb991df3 | |
Mike Gerwitz | b61e1ce952 | |
Mike Gerwitz | 79fa10f26b | |
Mike Gerwitz | b238366bee | |
Mike Gerwitz | bfbaa6e528 | |
Mike Gerwitz | ba38a3c1ba | |
Mike Gerwitz | 1cf5488756 | |
Mike Gerwitz | 33f34bf244 | |
Mike Gerwitz | 9fb2169a06 | |
Mike Gerwitz | 00ff660008 | |
Mike Gerwitz | dd6a6dd196 | |
Mike Gerwitz | 7a6aef00b2 | |
Mike Gerwitz | 4510de38ed | |
Mike Gerwitz | ebdae9ac38 | |
Mike Gerwitz | 5f275fb801 | |
Mike Gerwitz | 4ec4857360 | |
Mike Gerwitz | 572337505c | |
Mike Gerwitz | 00492ace01 | |
Mike Gerwitz | 13bac8382f | |
Mike Gerwitz | 48bcb0cdab | |
Mike Gerwitz | a9d0f43684 | |
Mike Gerwitz | e3a68aaf9e | |
Mike Gerwitz | 670c5d3a5d | |
Mike Gerwitz | 799f2c6d96 | |
Mike Gerwitz | 7cfe6a6f8d | |
Mike Gerwitz | 92c9c9ba2f | |
Mike Gerwitz | 56ab671363 | |
Mike Gerwitz | 068804b397 | |
Mike Gerwitz | 77ada079e1 | |
Mike Gerwitz | 78c1a9136e | |
Mike Gerwitz | 9b53a5e176 | |
Mike Gerwitz | c2c1434afe | |
Mike Gerwitz | e3094e0bad | |
Mike Gerwitz | be05fbb833 | |
Mike Gerwitz | 42aa5bd407 | |
Mike Gerwitz | 48d9bca3b7 | |
Mike Gerwitz | 34dad122fd | |
Mike Gerwitz | 1f371b6ba6 | |
Mike Gerwitz | dd47fc564d | |
Mike Gerwitz | e13817c203 | |
Mike Gerwitz | 6f68292df5 | |
Mike Gerwitz | f183600c3a | |
Mike Gerwitz | 46551ee298 | |
Mike Gerwitz | 778e90c81d | |
Mike Gerwitz | c367666d8e | |
Mike Gerwitz | 590c4b7b06 | |
Mike Gerwitz | 5dd77e7b41 | |
Mike Gerwitz | 2325eb1b2f | |
Mike Gerwitz | b7aae207c2 | |
Mike Gerwitz | af43e35567 | |
Mike Gerwitz | e88800af42 | |
Mike Gerwitz | 647e0ccbbd | |
Mike Gerwitz | acafe91ab9 | |
Mike Gerwitz | 9cb6195046 | |
Mike Gerwitz | 0163391498 | |
Mike Gerwitz | f4653790da | |
Mike Gerwitz | 82e228009d | |
Mike Gerwitz | 1f2ead7f9b | |
Mike Gerwitz | 7b2acb65c5 | |
Mike Gerwitz | e8371c452e | |
Mike Gerwitz | c0e5b1d750 | |
Mike Gerwitz | daa8c6967b | |
Mike Gerwitz | a738a05461 | |
Mike Gerwitz | 3660c15d5a | |
Mike Gerwitz | f1495f8cf4 | |
Mike Gerwitz | 02dba0d63a | |
Mike Gerwitz | 5b0a4561a2 | |
Mike Gerwitz | 6d35e8776c | |
Mike Gerwitz | e3d60750a9 | |
Mike Gerwitz | a33d0c4ea5 | |
Mike Gerwitz | 0e0b72ff5f | |
Mike Gerwitz | 558f1c96b1 | |
Mike Gerwitz | f29e3cfce1 | |
Mike Gerwitz | e6c6028b37 | |
Mike Gerwitz | 11a4fdfb26 | |
Mike Gerwitz | d091103983 | |
Mike Gerwitz | c59b92370c | |
Mike Gerwitz | 68e2d5d10e | |
Mike Gerwitz | 15fd2de437 | |
Mike Gerwitz | 26ddb2ae9d | |
Mike Gerwitz | 525adb8a6c | |
Mike Gerwitz | 755c91e04a | |
Mike Gerwitz | a5b4eda369 | |
Mike Gerwitz | 1ef1290ee9 | |
Mike Gerwitz | 2ae33a1dfa | |
Mike Gerwitz | eebacb52cc | |
Mike Gerwitz | b1ce7aaf29 | |
Mike Gerwitz | fc569f7551 | |
Mike Gerwitz | e1c8e371d5 | |
Mike Gerwitz | 6581c9946c | |
Mike Gerwitz | e595698309 | |
Mike Gerwitz | 975f60bff9 | |
Mike Gerwitz | 120f5bdfef | |
Mike Gerwitz | 9c0e20e58c | |
Mike Gerwitz | fcd25d581c | |
Mike Gerwitz | 1c7df894ea | |
Mike Gerwitz | 25121c1086 | |
Mike Gerwitz | bef68e1634 | |
Mike Gerwitz | 3dcb2cb03c | |
Mike Gerwitz | a686855e9d | |
Mike Gerwitz | 3e9f407527 | |
Mike Gerwitz | 669302700a | |
Mike Gerwitz | 893da0ed20 | |
Mike Gerwitz | e132f108e8 | |
Mike Gerwitz | 9d50157f8e | |
Mike Gerwitz | aa229b827c | |
Mike Gerwitz | 03b46ebeff | |
Mike Gerwitz | d930b26487 | |
Mike Gerwitz | be81878dd7 | |
Mike Gerwitz | 9e5958d89e | |
Mike Gerwitz | 0e42788dcc | |
Mike Gerwitz | 2233c69bbf | |
Mike Gerwitz | 18fa910e0f |
|
@ -20,7 +20,7 @@ build:
|
|||
- export SAXON_CP=/usr/share/ant/lib/saxon9/saxon9he.jar
|
||||
- export HOXSL=hoxsl
|
||||
- ./bootstrap
|
||||
- make all check info pdf html
|
||||
- make clean all check info pdf html
|
||||
artifacts:
|
||||
paths:
|
||||
- doc/
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
# This number is incremented for every linker change to force rebuilding
|
||||
# of xmle files.
|
||||
4
|
||||
5 # Removal of {ret,}map:___{head,tail}
|
||||
|
||||
|
|
|
@ -150,7 +150,7 @@
|
|||
</xs:annotation>
|
||||
|
||||
<xs:restriction base="xs:string">
|
||||
<xs:pattern value="_[a-zA-Z0-9@\{\}-]+_|@[a-z][a-zA-Z0-9]*@" />
|
||||
<xs:pattern value="_+[a-zA-Z0-9@\{\}-]+_+|@[a-z][a-zA-Z0-9]*@" />
|
||||
</xs:restriction>
|
||||
</xs:simpleType>
|
||||
|
||||
|
|
|
@ -127,27 +127,12 @@
|
|||
$pkg-with-symtable/preproc:sym-deps" />
|
||||
|
||||
<preproc:fragments>
|
||||
<!-- special fragment to be output as the head -->
|
||||
<preproc:fragment id=":map:___head">
|
||||
<!-- use a callback just in case we need to make portions of this async in the
|
||||
future -->
|
||||
<text>function( input, callback ) {</text>
|
||||
<text>var output = {};</text>
|
||||
</preproc:fragment>
|
||||
|
||||
<!-- compile mapped -->
|
||||
<apply-templates select="./lvm:*" mode="lvmc:compile">
|
||||
<with-param name="rater" select="$rater" />
|
||||
<with-param name="type" select="'map'" />
|
||||
<with-param name="symtable" select="$pkg-with-symtable/preproc:symtable"
|
||||
tunnel="yes"/>
|
||||
</apply-templates>
|
||||
|
||||
<!-- special fragment to be output as the foot -->
|
||||
<preproc:fragment id=":map:___tail">
|
||||
<text>callback(output);</text>
|
||||
<text>};</text>
|
||||
</preproc:fragment>
|
||||
</preproc:fragments>
|
||||
|
||||
<sequence select="$pkg-with-symtable/preproc:sym-deps/following-sibling::*" />
|
||||
|
@ -204,27 +189,12 @@
|
|||
$pkg-with-symtable/preproc:sym-deps" />
|
||||
|
||||
<preproc:fragments>
|
||||
<!-- special fragment to be output as the head -->
|
||||
<preproc:fragment id=":retmap:___head">
|
||||
<!-- use a callback just in case we need to make portions of this async in the
|
||||
future -->
|
||||
<text>function( input, callback ) {</text>
|
||||
<text>var output = {};</text>
|
||||
</preproc:fragment>
|
||||
|
||||
<!-- compile mapped -->
|
||||
<apply-templates select="./lvm:*" mode="lvmc:compile">
|
||||
<with-param name="rater" select="$rater" />
|
||||
<with-param name="type" select="'retmap'" />
|
||||
<with-param name="symtable" select="$pkg-with-symtable/preproc:symtable"
|
||||
tunnel="yes"/>
|
||||
</apply-templates>
|
||||
|
||||
<!-- special fragment to be output as the foot -->
|
||||
<preproc:fragment id=":retmap:___tail">
|
||||
<text>callback(output);</text>
|
||||
<text>};</text>
|
||||
</preproc:fragment>
|
||||
</preproc:fragments>
|
||||
|
||||
<sequence select="$pkg-with-symtable/preproc:sym-deps/following-sibling::*" />
|
||||
|
@ -236,19 +206,6 @@
|
|||
<param name="type-prefix" select="'map'" />
|
||||
|
||||
<preproc:symtable>
|
||||
<!-- purposely non-polluting. @ignore-dup is intended to be
|
||||
temporary until static generation of these names is resolved;
|
||||
this will not cause problems, since the code is always the
|
||||
same (future bug pending!) -->
|
||||
<preproc:sym name=":{$type-prefix}:___head"
|
||||
type="{$type-prefix}:head"
|
||||
pollute="true"
|
||||
ignore-dup="true"
|
||||
no-deps="true" />
|
||||
<preproc:sym name=":{$type-prefix}:___tail"
|
||||
type="{$type-prefix}:tail"
|
||||
ignore-dup="true"
|
||||
no-deps="true" />
|
||||
</preproc:symtable>
|
||||
</template>
|
||||
|
||||
|
|
|
@ -657,10 +657,29 @@
|
|||
select="lv:param-meta" />
|
||||
|
||||
<variable name="varname" select="@name" />
|
||||
<variable name="param" select="$params[ @name=$varname ]" />
|
||||
|
||||
<!-- determine the nodes to copy -->
|
||||
<variable name="copy"
|
||||
select="$params[ @name=$varname ]/*" />
|
||||
<variable name="copy" as="node()*">
|
||||
<choose>
|
||||
<!-- TAMER desugared @values@ application convention (see tplshort.rs) -->
|
||||
<when test="$param/@value">
|
||||
<!-- the value is the name of a template to copy the body from -->
|
||||
<variable name="dsgr" select="$param/@value" />
|
||||
|
||||
<!-- the template is always positioned as the immeditely-following
|
||||
sibling of the `lv:apply-template` node -->
|
||||
<sequence select="$params/parent::lv:apply-template
|
||||
/following-sibling::lv:template[ @name=$dsgr ]
|
||||
/*" />
|
||||
</when>
|
||||
|
||||
<!-- old applicatication convention has child nodes within
|
||||
`with-param` -->
|
||||
<otherwise>
|
||||
<sequence select="$param/*" />
|
||||
</otherwise>
|
||||
</choose>
|
||||
</variable>
|
||||
|
||||
<choose>
|
||||
<!-- if the @expand flag is set, then immediately begin expanding any
|
||||
|
|
|
@ -148,7 +148,7 @@
|
|||
</xs:annotation>
|
||||
|
||||
<xs:restriction base="xs:string">
|
||||
<xs:pattern value="_[a-zA-Z0-9@\{\}-]+_|@[a-z][a-zA-Z0-9]*@" />
|
||||
<xs:pattern value="_+[a-zA-Z0-9@\{\}-]+_+|@[a-z][a-zA-Z0-9]*@" />
|
||||
</xs:restriction>
|
||||
</xs:simpleType>
|
||||
|
||||
|
|
|
@ -75,34 +75,16 @@
|
|||
<variable name="retmap" select="/lv:package/l:retmap-exec" />
|
||||
|
||||
<!-- store a reference to the mapper in rater.fromMap() -->
|
||||
<text>rater.fromMap = </text>
|
||||
<choose>
|
||||
<when test="/lv:package/l:dep/preproc:sym[@type='map'][1]">
|
||||
<value-of disable-output-escaping="yes" select="$map/text()" />
|
||||
</when>
|
||||
|
||||
<!-- no map available -->
|
||||
<otherwise>
|
||||
<!-- simply invoke the conintuation with the provided data -->
|
||||
<text>function(d,c){c(d);}</text>
|
||||
</otherwise>
|
||||
</choose>
|
||||
<text>; </text>
|
||||
<text>rater.fromMap=function(input,callback){</text>
|
||||
<text>var output={};</text>
|
||||
<value-of disable-output-escaping="yes" select="$map/text()" />
|
||||
<text>callback(output);}; </text>
|
||||
|
||||
<!-- return map -->
|
||||
<text>rater._retmap = </text>
|
||||
<choose>
|
||||
<when test="/lv:package/l:dep/preproc:sym[@type='retmap'][1]">
|
||||
<value-of disable-output-escaping="yes" select="$retmap/text()" />
|
||||
</when>
|
||||
|
||||
<!-- no map available -->
|
||||
<otherwise>
|
||||
<!-- simply invoke the conintuation with the provided data -->
|
||||
<text>function(d,c){c(d);}</text>
|
||||
</otherwise>
|
||||
</choose>
|
||||
<text>; </text>
|
||||
<text>rater._retmap=function(input,callback){</text>
|
||||
<text>var output={};</text>
|
||||
<value-of disable-output-escaping="yes" select="$retmap/text()" />
|
||||
<text>callback(output);}; </text>
|
||||
|
||||
<!-- we'll export a version that automatically performs the mapping -->
|
||||
<text>module.exports = function( args_base, _canterm ) { </text>
|
||||
|
|
|
@ -116,6 +116,7 @@ dependencies = [
|
|||
"arrayvec",
|
||||
"bumpalo",
|
||||
"exitcode",
|
||||
"fixedbitset",
|
||||
"fxhash",
|
||||
"getopts",
|
||||
"memchr",
|
||||
|
|
|
@ -26,6 +26,7 @@ lto = true
|
|||
arrayvec = ">= 0.7.1"
|
||||
bumpalo = ">= 2.6.0"
|
||||
exitcode = "1.1.2"
|
||||
fixedbitset = ">= 0.4.1" # also used by petgraph
|
||||
fxhash = ">= 0.2.1"
|
||||
getopts = "0.2"
|
||||
memchr = ">= 2.3.4" # quick-xml expects =2.3.4 at the time
|
||||
|
@ -52,15 +53,9 @@ unicode-width = "0.1.5"
|
|||
# This is enabled automatically for the `test` profile.
|
||||
parser-trace-stderr = []
|
||||
|
||||
# Lowering NIR into AIR will begin adding objects to the graph. Since
|
||||
# loweirng is not complete, this may result in errors. This flag can be
|
||||
# removed when we are able to confidently state that this lowering will not
|
||||
# cause issues in production TAME code.
|
||||
wip-nir-to-air = []
|
||||
|
||||
# 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 = ["wip-nir-to-air"]
|
||||
wip-asg-derived-xmli = []
|
||||
|
||||
|
|
|
@ -52,7 +52,7 @@ rustdoc:
|
|||
RUSTFLAGS="$(RUSTFLAGS)" @CARGO@ +@RUST_TC@ @CARGO_FLAGS@ @CARGO_DOC_FLAGS@ doc --document-private-items @FEATURES@
|
||||
|
||||
$(ontviz_svg): FORCE
|
||||
tools/asg-ontviz | $(DOT) -Tsvg > $@
|
||||
build-aux/asg-ontviz | $(DOT) -Tsvg > $@
|
||||
|
||||
# note that 'cargo check' is something else; see 'cargo --help'
|
||||
test: check
|
||||
|
@ -64,7 +64,7 @@ check-cargo:
|
|||
|
||||
.PHONY: check-docgen
|
||||
check-docgen:
|
||||
tools/asg-ontviz >/dev/null
|
||||
build-aux/asg-ontviz >/dev/null
|
||||
|
||||
.PHONY: check-system
|
||||
check-system: bin
|
||||
|
@ -72,7 +72,7 @@ check-system: bin
|
|||
|
||||
.PHONY: check-lint
|
||||
check-lint:
|
||||
@CARGO@ +@RUST_TC@ @CARGO_FLAGS@ clippy
|
||||
RUSTFLAGS="$(RUSTFLAGS)" @CARGO@ +@RUST_TC@ @CARGO_FLAGS@ clippy @FEATURES@
|
||||
|
||||
.PHONY: check-fmt
|
||||
check-fmt:
|
||||
|
|
|
@ -1,471 +0,0 @@
|
|||
// Abstract semantic graph benchmarks
|
||||
//
|
||||
// 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/>.
|
||||
//
|
||||
// Note that the baseline tests have a _suffix_ rather than a prefix so that
|
||||
// they are still grouped with the associated test in the output, since it's
|
||||
// sorted lexically by function name.
|
||||
|
||||
#![feature(test)]
|
||||
|
||||
extern crate tamer;
|
||||
extern crate test;
|
||||
|
||||
use test::Bencher;
|
||||
|
||||
mod base {
|
||||
use super::*;
|
||||
use tamer::{
|
||||
asg::{DefaultAsg, IdentKind, Source},
|
||||
parse::util::SPair,
|
||||
span::UNKNOWN_SPAN,
|
||||
sym::GlobalSymbolIntern,
|
||||
};
|
||||
|
||||
type Sut = DefaultAsg;
|
||||
|
||||
fn interned_n(n: u16) -> Vec<SPair> {
|
||||
(0..n)
|
||||
.map(|i| SPair(i.to_string().intern(), UNKNOWN_SPAN))
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn declare_1_000(bench: &mut Bencher) {
|
||||
let mut sut = Sut::new();
|
||||
let xs = interned_n(1_000);
|
||||
|
||||
bench.iter(|| {
|
||||
xs.iter()
|
||||
.map(|i| sut.declare(*i, IdentKind::Meta, Source::default()))
|
||||
.for_each(drop);
|
||||
});
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn declare_1_000_full_inital_capacity(bench: &mut Bencher) {
|
||||
let mut sut = Sut::with_capacity(1024, 1024);
|
||||
let xs = interned_n(1_000);
|
||||
|
||||
bench.iter(|| {
|
||||
xs.iter()
|
||||
.map(|i| sut.declare(*i, IdentKind::Meta, Source::default()))
|
||||
.for_each(drop);
|
||||
});
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn declare_1_000_prog_ident_size(bench: &mut Bencher) {
|
||||
let mut sut = Sut::new();
|
||||
let xs = interned_n(1_000);
|
||||
|
||||
bench.iter(|| {
|
||||
xs.iter()
|
||||
.map(|i| sut.declare(*i, IdentKind::Meta, Source::default()))
|
||||
.for_each(drop);
|
||||
});
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn declare_extern_1_000(bench: &mut Bencher) {
|
||||
let mut sut = Sut::new();
|
||||
let xs = interned_n(1_000);
|
||||
|
||||
bench.iter(|| {
|
||||
xs.iter()
|
||||
.map(|i| {
|
||||
sut.declare_extern(*i, IdentKind::Meta, Source::default())
|
||||
})
|
||||
.for_each(drop);
|
||||
});
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn resolve_extern_1_000(bench: &mut Bencher) {
|
||||
let mut sut = Sut::new();
|
||||
let xs = interned_n(1_000);
|
||||
|
||||
xs.iter().for_each(|sym| {
|
||||
let _ =
|
||||
sut.declare_extern(*sym, IdentKind::Meta, Source::default());
|
||||
});
|
||||
|
||||
// Bench only the resolution, not initial declare.
|
||||
bench.iter(|| {
|
||||
xs.iter()
|
||||
.map(|sym| {
|
||||
sut.declare(*sym, IdentKind::Meta, Source::default())
|
||||
})
|
||||
.for_each(drop);
|
||||
});
|
||||
}
|
||||
|
||||
// N.B.: This benchmark isn't easily comparable to the others because
|
||||
// `set_fragment` takes ownership over a string, and so we have to clone
|
||||
// strings for each call.
|
||||
#[bench]
|
||||
fn set_fragment_1_000_with_new_str(bench: &mut Bencher) {
|
||||
let mut sut = Sut::new();
|
||||
let xs = interned_n(1_000);
|
||||
|
||||
xs.iter().for_each(|sym| {
|
||||
sut.declare(*sym, IdentKind::Meta, Source::default())
|
||||
.unwrap();
|
||||
});
|
||||
|
||||
// Bench only the resolution, not initial declare.
|
||||
bench.iter(|| {
|
||||
xs.iter()
|
||||
.map(|sym| sut.set_fragment(*sym, "".into())) // see N.B.
|
||||
.for_each(drop);
|
||||
});
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn lookup_1_000(bench: &mut Bencher) {
|
||||
let mut sut = Sut::new();
|
||||
let xs = interned_n(1_000);
|
||||
|
||||
xs.iter().for_each(|sym| {
|
||||
let _ = sut.declare(*sym, IdentKind::Meta, Source::default());
|
||||
});
|
||||
|
||||
bench.iter(|| {
|
||||
xs.iter()
|
||||
.map(|sym| sut.lookup(*sym).unwrap())
|
||||
.for_each(drop);
|
||||
});
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn get_1_000(bench: &mut Bencher) {
|
||||
let mut sut = Sut::new();
|
||||
let xs = interned_n(1_000);
|
||||
|
||||
let orefs = xs
|
||||
.iter()
|
||||
.map(|sym| {
|
||||
sut.declare(*sym, IdentKind::Meta, Source::default())
|
||||
.unwrap()
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
bench.iter(|| {
|
||||
orefs
|
||||
.iter()
|
||||
.map(|oref| sut.get(*oref).unwrap())
|
||||
.for_each(drop);
|
||||
});
|
||||
}
|
||||
|
||||
// All dependencies on a single node. Petgraph does poorly with
|
||||
// supernodes at the time of writing, relatively speaking.
|
||||
#[bench]
|
||||
fn add_dep_1_000_to_single_node(bench: &mut Bencher) {
|
||||
let mut sut = Sut::new();
|
||||
let xs = interned_n(1_000);
|
||||
|
||||
let orefs = xs
|
||||
.iter()
|
||||
.map(|sym| {
|
||||
sut.declare(*sym, IdentKind::Meta, Source::default())
|
||||
.unwrap()
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let root = orefs[0];
|
||||
|
||||
// Note that this adds all edges to one node
|
||||
bench.iter(|| {
|
||||
orefs
|
||||
.iter()
|
||||
.map(|oref| sut.add_dep(root, *oref))
|
||||
.for_each(drop);
|
||||
});
|
||||
}
|
||||
|
||||
// Same as above but only one edge per node.
|
||||
#[bench]
|
||||
fn add_dep_1_000_one_edge_per_node(bench: &mut Bencher) {
|
||||
let mut sut = Sut::new();
|
||||
let xs = interned_n(1_000);
|
||||
|
||||
let orefs = xs
|
||||
.iter()
|
||||
.map(|sym| {
|
||||
sut.declare(*sym, IdentKind::Meta, Source::default())
|
||||
.unwrap()
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
bench.iter(|| {
|
||||
orefs
|
||||
.iter()
|
||||
.zip(orefs.iter().cycle().skip(1))
|
||||
.map(|(from, to)| sut.add_dep(*from, *to))
|
||||
.for_each(drop);
|
||||
});
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn has_dep_1_000_single_node(bench: &mut Bencher) {
|
||||
let mut sut = Sut::new();
|
||||
let xs = interned_n(1_000);
|
||||
|
||||
let orefs = xs
|
||||
.iter()
|
||||
.map(|sym| {
|
||||
sut.declare(*sym, IdentKind::Meta, Source::default())
|
||||
.unwrap()
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let root = orefs[0];
|
||||
|
||||
orefs.iter().for_each(|oref| {
|
||||
sut.add_dep(root, *oref);
|
||||
});
|
||||
|
||||
bench.iter(|| {
|
||||
orefs
|
||||
.iter()
|
||||
.map(|oref| sut.has_dep(root, *oref))
|
||||
.for_each(drop);
|
||||
});
|
||||
}
|
||||
|
||||
// Same as above but only one edge per node.
|
||||
#[bench]
|
||||
fn has_dep_1_000_one_edge_per_node(bench: &mut Bencher) {
|
||||
let mut sut = Sut::new();
|
||||
let xs = interned_n(1_000);
|
||||
|
||||
let orefs = xs
|
||||
.iter()
|
||||
.map(|sym| {
|
||||
sut.declare(*sym, IdentKind::Meta, Source::default())
|
||||
.unwrap()
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
orefs.iter().zip(orefs.iter().cycle().skip(1)).for_each(
|
||||
|(from, to)| {
|
||||
sut.add_dep(*from, *to);
|
||||
},
|
||||
);
|
||||
|
||||
bench.iter(|| {
|
||||
orefs
|
||||
.iter()
|
||||
.zip(orefs.iter().cycle().skip(1))
|
||||
.map(|(from, to)| sut.has_dep(*from, *to))
|
||||
.for_each(drop);
|
||||
});
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn add_dep_lookup_1_000_missing_one_edge_per_node(bench: &mut Bencher) {
|
||||
let mut sut = Sut::new();
|
||||
let xs = interned_n(1_000);
|
||||
|
||||
bench.iter(|| {
|
||||
xs.iter()
|
||||
.zip(xs.iter().cycle().skip(1))
|
||||
.map(|(from, to)| sut.add_dep_lookup(*from, *to))
|
||||
.for_each(drop);
|
||||
});
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn add_dep_lookup_1_000_existing_one_edge_per_node(bench: &mut Bencher) {
|
||||
let mut sut = Sut::new();
|
||||
let xs = interned_n(1_000);
|
||||
|
||||
xs.iter().for_each(|sym| {
|
||||
let _ = sut.declare(*sym, IdentKind::Meta, Source::default());
|
||||
});
|
||||
|
||||
bench.iter(|| {
|
||||
xs.iter()
|
||||
.zip(xs.iter().cycle().skip(1))
|
||||
.map(|(from, to)| sut.add_dep_lookup(*from, *to))
|
||||
.for_each(drop);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
mod object {
|
||||
use super::*;
|
||||
|
||||
mod ident {
|
||||
use super::*;
|
||||
use tamer::{
|
||||
asg::{Ident, IdentKind, Source},
|
||||
parse::util::SPair,
|
||||
span::UNKNOWN_SPAN as S0,
|
||||
};
|
||||
|
||||
type Sut = Ident;
|
||||
|
||||
#[bench]
|
||||
fn declare_1_000(bench: &mut Bencher) {
|
||||
let sym = SPair("sym".into(), S0);
|
||||
|
||||
bench.iter(|| {
|
||||
(0..1000).map(|_| Sut::declare(sym)).for_each(drop);
|
||||
});
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn resolve_1_000_missing(bench: &mut Bencher) {
|
||||
let sym = SPair("sym".into(), S0);
|
||||
|
||||
bench.iter(|| {
|
||||
(0..1000)
|
||||
.map(|_| {
|
||||
Sut::declare(sym).resolve(
|
||||
S0,
|
||||
IdentKind::Meta,
|
||||
Source::default(),
|
||||
)
|
||||
})
|
||||
.for_each(drop);
|
||||
});
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn extern_1_000_missing(bench: &mut Bencher) {
|
||||
let sym = SPair("sym".into(), S0);
|
||||
|
||||
bench.iter(|| {
|
||||
(0..1000)
|
||||
.map(|_| {
|
||||
Sut::declare(sym).extern_(
|
||||
S0,
|
||||
IdentKind::Meta,
|
||||
Source::default(),
|
||||
)
|
||||
})
|
||||
.for_each(drop);
|
||||
});
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn resolve_1_000_extern(bench: &mut Bencher) {
|
||||
let sym = SPair("sym".into(), S0);
|
||||
|
||||
bench.iter(|| {
|
||||
(0..1000)
|
||||
.map(|_| {
|
||||
Sut::declare(sym)
|
||||
.extern_(S0, IdentKind::Meta, Source::default())
|
||||
.unwrap()
|
||||
.resolve(S0, IdentKind::Meta, Source::default())
|
||||
})
|
||||
.for_each(drop);
|
||||
});
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn resolve_1_000_override(bench: &mut Bencher) {
|
||||
let sym = SPair("sym".into(), S0);
|
||||
|
||||
bench.iter(|| {
|
||||
(0..1000)
|
||||
.map(|_| {
|
||||
Sut::declare(sym)
|
||||
.resolve(
|
||||
S0,
|
||||
IdentKind::Meta,
|
||||
Source {
|
||||
virtual_: true,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.unwrap()
|
||||
.resolve(
|
||||
S0,
|
||||
IdentKind::Meta,
|
||||
Source {
|
||||
override_: true,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
})
|
||||
.for_each(drop);
|
||||
});
|
||||
}
|
||||
|
||||
// Override encountered before virtual
|
||||
#[bench]
|
||||
fn resolve_1_000_override_virt_after_override(bench: &mut Bencher) {
|
||||
let sym = SPair("sym".into(), S0);
|
||||
|
||||
bench.iter(|| {
|
||||
(0..1000)
|
||||
.map(|_| {
|
||||
Sut::declare(sym)
|
||||
.resolve(
|
||||
S0,
|
||||
IdentKind::Meta,
|
||||
Source {
|
||||
override_: true,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.unwrap()
|
||||
.resolve(
|
||||
S0,
|
||||
IdentKind::Meta,
|
||||
Source {
|
||||
virtual_: true,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
})
|
||||
.for_each(drop);
|
||||
});
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn set_fragment_1_000_resolved_with_new_str(bench: &mut Bencher) {
|
||||
let sym = SPair("sym".into(), S0);
|
||||
|
||||
bench.iter(|| {
|
||||
(0..1000)
|
||||
.map(|_| {
|
||||
Sut::declare(sym)
|
||||
.resolve(S0, IdentKind::Meta, Source::default())
|
||||
.unwrap()
|
||||
.set_fragment("".into())
|
||||
})
|
||||
.for_each(drop);
|
||||
});
|
||||
}
|
||||
|
||||
// No need to do all of the others, since they're all the same thing.
|
||||
#[bench]
|
||||
fn declared_name_1_000(bench: &mut Bencher) {
|
||||
let sym = SPair("sym".into(), S0);
|
||||
|
||||
bench.iter(|| {
|
||||
(0..1000).map(|_| Sut::declare(sym).name()).for_each(drop);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,109 +0,0 @@
|
|||
// Benchmarking of sorting of ASG into xmle sections
|
||||
//
|
||||
// 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/>.
|
||||
|
||||
#![feature(test)]
|
||||
|
||||
extern crate tamer;
|
||||
extern crate test;
|
||||
|
||||
use test::Bencher;
|
||||
|
||||
pub(crate) use tamer::{
|
||||
asg::{DefaultAsg, IdentKind, Source},
|
||||
ld::xmle::{lower::sort, Sections},
|
||||
num::Dtype,
|
||||
parse::util::SPair,
|
||||
span::UNKNOWN_SPAN,
|
||||
sym::GlobalSymbolIntern,
|
||||
};
|
||||
|
||||
type TestAsg = DefaultAsg;
|
||||
|
||||
fn interned_n(n: u16) -> Vec<SPair> {
|
||||
(0..n)
|
||||
.map(|i| SPair(i.to_string().intern(), UNKNOWN_SPAN))
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn sort_1_with_1_000_existing_supernode(bench: &mut Bencher) {
|
||||
let mut sut = TestAsg::new();
|
||||
let xs = interned_n(1_000);
|
||||
|
||||
let orefs = xs
|
||||
.iter()
|
||||
.map(|sym| {
|
||||
sut.declare(
|
||||
*sym,
|
||||
IdentKind::Rate(Dtype::Integer),
|
||||
Source::default(),
|
||||
)
|
||||
.unwrap()
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let root = orefs[0];
|
||||
|
||||
// All edges from a single node.
|
||||
orefs.iter().skip(1).for_each(|to| {
|
||||
sut.add_dep(root, *to);
|
||||
});
|
||||
|
||||
sut.add_root(root);
|
||||
|
||||
bench.iter(|| {
|
||||
drop(sort(&sut, Sections::new()));
|
||||
});
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn sort_1_with_1_000_existing_one_edge_per_node_one_path(bench: &mut Bencher) {
|
||||
let mut sut = TestAsg::new();
|
||||
let xs = interned_n(1_000);
|
||||
|
||||
let orefs = xs
|
||||
.iter()
|
||||
.map(|sym| {
|
||||
sut.declare(
|
||||
*sym,
|
||||
IdentKind::Rate(Dtype::Integer),
|
||||
Source::default(),
|
||||
)
|
||||
.unwrap()
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// Note that there's no `cycle` call on the iterator, like the
|
||||
// above tests, to make sure we don't create a cycle on the
|
||||
// graph.
|
||||
orefs
|
||||
.iter()
|
||||
.zip(orefs.iter().skip(1))
|
||||
.for_each(|(from, to)| {
|
||||
sut.add_dep(*from, *to);
|
||||
});
|
||||
|
||||
let root = orefs[0];
|
||||
|
||||
sut.add_root(root);
|
||||
|
||||
bench.iter(|| {
|
||||
drop(sort(&sut, Sections::new()));
|
||||
});
|
||||
}
|
|
@ -24,6 +24,12 @@
|
|||
|
||||
set -euo pipefail
|
||||
|
||||
# Invoke cargo both to output version information to the log and to trigger
|
||||
# rustup based on `rust-toolchain.toml`, if the user happens to use rustup.
|
||||
# This will allow `cargo` commands to work in the usual way, though `make`
|
||||
# should still be preferred.
|
||||
which cargo &>/dev/null && cargo --version
|
||||
|
||||
# This will apply a default configuration, which will be used to perform a
|
||||
# fetch of the dependencies. You are of course free to reconfigure after.
|
||||
./autogen.sh && ./configure && make fetch
|
||||
|
|
|
@ -14,7 +14,7 @@ cd "$(dirname "$0")"
|
|||
#
|
||||
# So, to be clear:
|
||||
# do _not_ do something like `grep -rl 'object_rel!' ../src/asg`.
|
||||
find ../src/asg/graph/object/ -name '*.rs' \
|
||||
find ../src/asg/graph/object/ -maxdepth 1 -name '*.rs' \
|
||||
-a \! -name 'rel.rs' \
|
||||
-a \! -name 'test.rs' \
|
||||
| xargs awk -f asg-ontviz.awk
|
|
@ -73,9 +73,6 @@ BEGINFILE {
|
|||
found_rels = 0
|
||||
}
|
||||
|
||||
# Skip comments.
|
||||
/^ *\/+/ { next }
|
||||
|
||||
# Predicates will be reset for each line,
|
||||
# allowing the remainder of the script to be read more declaratively.
|
||||
{ in_block = in_block_subexpr = 0 }
|
||||
|
@ -100,6 +97,15 @@ block_src && /}/ {
|
|||
print ""
|
||||
}
|
||||
|
||||
# "// empty" means that the lack of edges is intentional.
|
||||
block_src && /^ *\/\/ empty$/ {
|
||||
# Suppress error from the edge check below.
|
||||
found_rels++
|
||||
}
|
||||
|
||||
# Skip comments.
|
||||
/^ *\/+/ { next }
|
||||
|
||||
# For each target object,
|
||||
# output a relation.
|
||||
#
|
||||
|
@ -114,9 +120,23 @@ block_src && $NF ~ /\w+,$/ {
|
|||
# Edge type (cross, tree)
|
||||
ty = $(NF-1)
|
||||
|
||||
# Dashed is visually in-between solid and dotted,
|
||||
# and `dyn` is either `tree` or `cross`,
|
||||
# determined at runtime.
|
||||
# But we may need some other representation if `dotted` is too visually
|
||||
# sparse and difficult/annoying to see;
|
||||
# let's see where the ASG visualization ends up first.
|
||||
attrs = ""
|
||||
if (ty == "cross") {
|
||||
attrs="[style=dashed]"
|
||||
switch (ty) {
|
||||
case "tree":
|
||||
attrs="[style=solid,arrowhead=normal]";
|
||||
break;
|
||||
case "cross":
|
||||
attrs="[style=dotted,arrowhead=open]";
|
||||
break;
|
||||
case "dyn":
|
||||
attrs="[style=dashed,arrowhead=normal]";
|
||||
break;
|
||||
}
|
||||
|
||||
gsub(/,$/, "")
|
||||
|
@ -129,7 +149,7 @@ block_src && $NF ~ /\w+,$/ {
|
|||
# the source object is right-aligned and target is left-aligned,
|
||||
# so that `Src -> Target` can be easily read regardless of the width
|
||||
# of the objects involved.
|
||||
printf " %5s -> %-5s %14s; # %s edge\n", block_src, $NF, attrs, ty
|
||||
printf " %5s -> %-5s %32s; # %s edge\n", block_src, $NF, attrs, ty
|
||||
|
||||
found_rels++
|
||||
}
|
|
@ -43,28 +43,50 @@ AC_SUBST(SUFFIX, m4_argn(4, ver_split))
|
|||
AC_ARG_VAR([CARGO], [Rust Cargo executable])
|
||||
AC_CHECK_PROGS(CARGO, [cargo])
|
||||
|
||||
# Rust toolchain (stable/nightly) is hard-coded for now, since decisions
|
||||
# regarding whether to use it are nuanced.
|
||||
AC_SUBST([RUST_TC], nightly)
|
||||
|
||||
test -n "$CARGO" || AC_MSG_ERROR([cargo not found])
|
||||
|
||||
# Derive the expected nightly version from `rust-toolchain.toml` and
|
||||
# configure the build system to use that version explicitly for each cargo
|
||||
# invoation in the Makefile.
|
||||
#
|
||||
# See rust-toolchain.toml for information about this process. Note that
|
||||
# this file will only trigger an auto-update if it is actually used to
|
||||
# derive the version (if TAMER_RUST_TOOLCHAIN is _not_ used).
|
||||
AC_ARG_VAR([TAMER_RUST_TOOLCHAIN],
|
||||
[Rust toolchain channel to use in place of rust-toolchain.toml contents])
|
||||
AC_MSG_CHECKING([toolchain channel])
|
||||
AS_IF([test -n "$TAMER_RUST_TOOLCHAIN"],
|
||||
[AC_MSG_RESULT([$TAMER_RUST_TOOLCHAIN (from TAMER_RUST_TOOLCHAIN)])
|
||||
AC_SUBST(RUST_TC, [$TAMER_RUST_TOOLCHAIN])],
|
||||
[rust_channel=$($AWK -F '[[ "]]' '$1=="channel" { print $4 }' rust-toolchain.toml)
|
||||
AS_IF([test -n "$rust_channel"],
|
||||
[AC_MSG_RESULT([$rust_channel (from rust-toolchain.toml)])
|
||||
AC_SUBST([RUST_TC], [$rust_channel])],
|
||||
[AC_MSG_RESULT([missing (from both rust-toolchain.toml and TAMER_RUST_TOOLCHAIN)])
|
||||
AC_MSG_ERROR([rust-toolchain.toml is missing or does not contain channel])])
|
||||
AC_SUBST([CONFIG_STATUS_DEPENDENCIES], rust-toolchain.toml)])
|
||||
|
||||
# There is no reason the build should _ever_ access the network.
|
||||
# This both helps with reproducibility and helps to mitigate supply chain
|
||||
# attacks by requiring developers to explicitly indicate their intent to
|
||||
# fetch a network resource (by invoking cargo manually).
|
||||
AC_SUBST([CARGO_FLAGS], "--frozen --offline")
|
||||
|
||||
# This is a nightly version at the time of writing
|
||||
rustc_ver_req=1.68
|
||||
# This is the version of Rust that would be required if we were not
|
||||
# utilizing nightly features. This serves as a lower version bound, whereas
|
||||
# the nightly check above serves as an upper bound.
|
||||
rustc_ver_req=1.70
|
||||
|
||||
AC_CHECK_PROGS(RUSTC, [rustc])
|
||||
AC_MSG_CHECKING([rustc $RUST_TC version >= $rustc_ver_req])
|
||||
rustc_version=$("$RUSTC" "+$RUST_TC" --version | cut -d' ' -f2)
|
||||
AS_IF([test -z "$rustc_version"],
|
||||
[AC_MSG_ERROR([
|
||||
failed to execute rustc with toolchain $RUST_TC (see above for error message)
|
||||
- if the above toolchain is not found, try running `bootstrap`])])
|
||||
AX_COMPARE_VERSION([$rustc_version], [ge], [$rustc_ver_req],
|
||||
[AC_MSG_RESULT([yes ($rustc_version)])],
|
||||
[AC_MSG_RESULT([no ($rustc_version)])
|
||||
AC_MSG_ERROR([If using rustup, run `rustup update'])])
|
||||
[AC_MSG_RESULT([no ($rustc_version)])])
|
||||
|
||||
AC_ARG_VAR([CARGO_BUILD_FLAGS],
|
||||
[Flags to be passed to `cargo build' when invoked via Make])
|
||||
|
|
|
@ -0,0 +1,52 @@
|
|||
# See <https://rust-lang.github.io/rustup/overrides.html?highlight=rust-toolchain#the-toolchain-file>
|
||||
#
|
||||
# This file is used to pin the version of Rust used to build TAMER. This is
|
||||
# not at all an ideal situation, but is necessary because TAMER uses nightly
|
||||
# features, and those features will occaisonally make
|
||||
# backwards-incompatabile changes.
|
||||
#
|
||||
# The default behavior caused by this file is not ideal: If the user uses
|
||||
# rustup, it'll cause the toolchain to be downloaded during build,
|
||||
# _ignoring_ any `--offline` flag provided to Cargo. Projects ought to be
|
||||
# able to build without network access; the `bootstrap` script is intended
|
||||
# to install necessary dependencies on a system prior to building.
|
||||
#
|
||||
# By explicitly specifying the toolchain via `cargo +toolchain`, we bypass
|
||||
# the system utilizing this file. It becomes clear that TAMER is being
|
||||
# compiled with a particular toolchain version---the invoked command (by
|
||||
# `make`) explicitly says so. This also avoids automatically downloading
|
||||
# the toolchain if it's missing, relying instead on any prior bootstrapping
|
||||
# or system-installed toolchain.
|
||||
#
|
||||
# Why have `rust-toolchain.toml` at all, then? Because then `cargo` will
|
||||
# work when invoked independently, if someone chooses not to use `make` for
|
||||
# any reason. It is still recommended that `make` be utilized so that all
|
||||
# the other build configuration takes effect.
|
||||
#
|
||||
# `configure.ac` will extract the channel from this file and utilize it to
|
||||
# configure the build to explicitly pass the channel via each cargo
|
||||
# invocation via the `Makefile`. Changing this file will will cause
|
||||
# reconfiguration automatically.
|
||||
#
|
||||
# Alternatively, `TAMER_RUST_TOOLCHAIN` may be used to explicitly specify
|
||||
# the channel for the toolchain. If you do this, then modifying
|
||||
# `rust-toolchain.toml` will _not_ trigger a reconfiguration automatically.
|
||||
#
|
||||
# If you are building an earlier commit that does not have this file, then
|
||||
# you will need to modify `configure.ac` / `Makefile.am` to do your bidding.
|
||||
|
||||
[toolchain]
|
||||
channel = "nightly-2023-04-15"
|
||||
|
||||
# The components should be checked in `configure.ac`
|
||||
# - Note that `cargo-fmt` is `rustfmt`.
|
||||
components = ["rustfmt", "clippy"]
|
||||
|
||||
# A note on the above version: it's not likely to be the case that _this
|
||||
# specific version_ is necessary to build TAMER. Instead, this is an upper
|
||||
# bound, whereas the Rust version in `configure.ac` is a lower bound. Some,
|
||||
# or all, of the versions within that bound are likely to work. If you know
|
||||
# another version that works (e.g. you are packaging TAMER and want to use
|
||||
# another version of Rust already available elsewhere), you may use the
|
||||
# `TAMER_RUST_TOOLCHAIN` `configure` parameter.
|
||||
|
1309
tamer/src/asg/air.rs
1309
tamer/src/asg/air.rs
File diff suppressed because it is too large
Load Diff
|
@ -27,30 +27,20 @@ use super::{
|
|||
Asg, AsgError, ObjectIndex,
|
||||
},
|
||||
ir::AirBindableExpr,
|
||||
AirAggregate, AirAggregateCtx,
|
||||
};
|
||||
use crate::{
|
||||
asg::{graph::object::ObjectRelTo, Ident, ObjectKind},
|
||||
asg::{
|
||||
graph::object::{ObjectIndexRelTo, ObjectIndexTo},
|
||||
ObjectKind,
|
||||
},
|
||||
f::Functor,
|
||||
parse::prelude::*,
|
||||
};
|
||||
use std::marker::PhantomData;
|
||||
|
||||
#[cfg(doc)]
|
||||
use StackEdge::{Dangling, Reachable};
|
||||
|
||||
/// Parse and aggregate [`Reachable`] [`Expr`]s into the graph,
|
||||
/// with expression roots bound to their associated [`Ident`]s.
|
||||
///
|
||||
/// See [`ReachableOnly`] for more information.
|
||||
pub type AirExprAggregateReachable<O> = AirExprAggregate<O, ReachableOnly<O>>;
|
||||
|
||||
/// Parse and aggregate both [`Reachable`] and [`Dangling`] [`Expr`]s into
|
||||
/// the graph.
|
||||
///
|
||||
/// See [`StoreDangling`] for more information.
|
||||
pub type AirExprAggregateStoreDangling<O> =
|
||||
AirExprAggregate<O, StoreDangling<O>>;
|
||||
|
||||
/// Parse an AIR expression with binding support.
|
||||
///
|
||||
/// Expressions are composable,
|
||||
|
@ -61,108 +51,120 @@ pub type AirExprAggregateStoreDangling<O> =
|
|||
/// handles each of its tokens and performs error recovery on invalid
|
||||
/// state transitions.
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum AirExprAggregate<O: ObjectKind, S: RootStrategy<O>> {
|
||||
pub enum AirExprAggregate {
|
||||
/// Ready for an expression;
|
||||
/// expression stack is empty.
|
||||
Ready(S, ExprStack<Dormant>, PhantomData<O>),
|
||||
Ready(ExprStack<Dormant>),
|
||||
|
||||
/// Building an expression.
|
||||
BuildingExpr(S, ExprStack<Active>, ObjectIndex<Expr>),
|
||||
BuildingExpr(ExprStack<Active>, ObjectIndex<Expr>),
|
||||
}
|
||||
|
||||
impl<O: ObjectKind, S: RootStrategy<O>> Display for AirExprAggregate<O, S> {
|
||||
impl Display for AirExprAggregate {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Ready(_, es, _) => {
|
||||
Self::Ready(es) => {
|
||||
write!(f, "ready for expression with {es}")
|
||||
}
|
||||
Self::BuildingExpr(_, es, _) => {
|
||||
Self::BuildingExpr(es, _) => {
|
||||
write!(f, "building expression with {es}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<O: ObjectKind, S: RootStrategy<O>> ParseState for AirExprAggregate<O, S> {
|
||||
impl ParseState for AirExprAggregate {
|
||||
type Token = AirBindableExpr;
|
||||
type Object = ();
|
||||
type Error = AsgError;
|
||||
type Context = Asg;
|
||||
type Context = AirAggregateCtx;
|
||||
type Super = AirAggregate;
|
||||
|
||||
fn parse_token(
|
||||
self,
|
||||
tok: Self::Token,
|
||||
asg: &mut Self::Context,
|
||||
ctx: &mut Self::Context,
|
||||
) -> crate::parse::TransitionResult<Self::Super> {
|
||||
use super::ir::{AirBind::*, AirExpr::*};
|
||||
use super::ir::{AirBind::*, AirDoc::*, AirExpr::*};
|
||||
use AirBindableExpr::*;
|
||||
use AirExprAggregate::*;
|
||||
|
||||
match (self, tok) {
|
||||
(Ready(root, es, _), AirExpr(ExprOpen(op, span))) => {
|
||||
let oi = asg.create(Expr::new(op, span));
|
||||
Transition(BuildingExpr(root, es.activate(), oi)).incomplete()
|
||||
(Ready(es), AirExpr(ExprStart(op, span))) => {
|
||||
let oi = ctx.asg_mut().create(Expr::new(op, span));
|
||||
Transition(BuildingExpr(es.activate(), oi)).incomplete()
|
||||
}
|
||||
|
||||
(BuildingExpr(root, es, poi), AirExpr(ExprOpen(op, span))) => {
|
||||
let oi = poi.create_subexpr(asg, Expr::new(op, span));
|
||||
Transition(BuildingExpr(root, es.push(poi), oi)).incomplete()
|
||||
(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()
|
||||
}
|
||||
|
||||
(BuildingExpr(root, es, oi), AirExpr(ExprClose(end))) => {
|
||||
let _ = oi.map_obj(asg, |expr| {
|
||||
(BuildingExpr(es, oi), AirExpr(ExprEnd(end))) => {
|
||||
let _ = oi.map_obj(ctx.asg_mut(), |expr| {
|
||||
expr.map(|span| span.merge(end).unwrap_or(span))
|
||||
});
|
||||
|
||||
let dangling = es.is_dangling();
|
||||
let oi_root = ctx.dangling_expr_oi();
|
||||
|
||||
match (es.pop(), dangling) {
|
||||
((es, Some(poi)), _) => {
|
||||
Transition(BuildingExpr(root, es, poi)).incomplete()
|
||||
Transition(BuildingExpr(es, poi)).incomplete()
|
||||
}
|
||||
((es, None), true) => {
|
||||
root.hold_dangling(asg, oi).transition(Ready(
|
||||
root,
|
||||
es.done(),
|
||||
PhantomData::default(),
|
||||
))
|
||||
Self::hold_dangling(ctx.asg_mut(), oi_root, oi)
|
||||
.transition(Ready(es.done()))
|
||||
}
|
||||
((es, None), false) => {
|
||||
Transition(Ready(es.done())).incomplete()
|
||||
}
|
||||
((es, None), false) => Transition(Ready(
|
||||
root,
|
||||
es.done(),
|
||||
PhantomData::default(),
|
||||
))
|
||||
.incomplete(),
|
||||
}
|
||||
}
|
||||
|
||||
(BuildingExpr(root, es, oi), AirBind(BindIdent(id))) => {
|
||||
let oi_ident = root.defines(asg, id);
|
||||
(BuildingExpr(es, oi), AirBind(BindIdent(id))) => {
|
||||
let result = ctx.defines(id).and_then(|oi_ident| {
|
||||
oi_ident.bind_definition(ctx.asg_mut(), id, oi)
|
||||
});
|
||||
|
||||
// It is important that we do not mark this expression as
|
||||
// reachable unless we successfully bind the identifier.
|
||||
match oi_ident.bind_definition(asg, id, oi) {
|
||||
Ok(_) => Transition(BuildingExpr(
|
||||
root,
|
||||
es.reachable_by(oi_ident),
|
||||
oi,
|
||||
))
|
||||
.incomplete(),
|
||||
Err(e) => Transition(BuildingExpr(root, es, oi)).err(e),
|
||||
match result {
|
||||
Ok(oi_ident) => {
|
||||
Transition(BuildingExpr(es.reachable_by(oi_ident), oi))
|
||||
.incomplete()
|
||||
}
|
||||
Err(e) => Transition(BuildingExpr(es, oi)).err(e),
|
||||
}
|
||||
}
|
||||
|
||||
(BuildingExpr(root, es, oi), AirBind(RefIdent(ident))) => {
|
||||
Transition(BuildingExpr(root, es, oi.ref_expr(asg, ident)))
|
||||
.incomplete()
|
||||
(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()
|
||||
}
|
||||
|
||||
(st @ Ready(..), AirExpr(ExprClose(span))) => {
|
||||
(BuildingExpr(es, oi), AirDoc(DocIndepClause(clause))) => {
|
||||
oi.desc_short(ctx.asg_mut(), clause);
|
||||
Transition(BuildingExpr(es, oi)).incomplete()
|
||||
}
|
||||
|
||||
(BuildingExpr(es, oi), AirDoc(DocText(text))) => Transition(
|
||||
BuildingExpr(es, oi),
|
||||
)
|
||||
.err(AsgError::InvalidDocContextExpr(oi.span(), text.span())),
|
||||
|
||||
(st @ Ready(..), AirExpr(ExprEnd(span))) => {
|
||||
Transition(st).err(AsgError::UnbalancedExpr(span))
|
||||
}
|
||||
|
||||
// The binding may refer to a parent context.
|
||||
(st @ Ready(..), tok @ AirBind(..)) => Transition(st).dead(tok),
|
||||
// Token may refer to a parent context.
|
||||
(st @ Ready(..), tok @ (AirBind(..) | AirDoc(..))) => {
|
||||
Transition(st).dead(tok)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -171,13 +173,38 @@ impl<O: ObjectKind, S: RootStrategy<O>> ParseState for AirExprAggregate<O, S> {
|
|||
}
|
||||
}
|
||||
|
||||
impl<O: ObjectKind, S: RootStrategy<O>> AirExprAggregate<O, S> {
|
||||
pub(super) fn new_in(oi: ObjectIndex<O>) -> Self {
|
||||
Self::Ready(
|
||||
S::new_root(oi),
|
||||
ExprStack::default(),
|
||||
PhantomData::default(),
|
||||
)
|
||||
impl AirExprAggregate {
|
||||
pub(super) fn new() -> Self {
|
||||
Self::Ready(ExprStack::default())
|
||||
}
|
||||
|
||||
/// Hold or reject a [`Dangling`] root [`Expr`].
|
||||
///
|
||||
/// A [`Dangling`] expression is not reachable by any other object.
|
||||
/// If there is no context able to handle such an expression,
|
||||
/// then an [`AsgError::DanglingExpr`] will be returned.
|
||||
fn hold_dangling(
|
||||
asg: &mut Asg,
|
||||
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);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The [`ObjectIndex`] of the active expression being built,
|
||||
/// if any.
|
||||
///
|
||||
/// This will return the deepest active subexpression.
|
||||
pub(super) fn active_expr_oi(&self) -> Option<ObjectIndex<Expr>> {
|
||||
use AirExprAggregate::*;
|
||||
|
||||
match self {
|
||||
Ready(_) => None,
|
||||
BuildingExpr(_, oi) => Some(*oi),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -371,125 +398,5 @@ impl Display for ExprStack<Active> {
|
|||
}
|
||||
}
|
||||
|
||||
pub use root::*;
|
||||
mod root {
|
||||
use super::*;
|
||||
use std::fmt::Debug;
|
||||
|
||||
/// The rooting strategy to employ after an [`Expr`] construction.
|
||||
///
|
||||
/// The method [`Self::defines`] roots an identifier,
|
||||
/// stating that the object associated with [`Self`] is responsible
|
||||
/// for the definition associated with this identifier.
|
||||
/// An identified expression will be rooted in [`Self`] even if it is a
|
||||
/// sub-expression.
|
||||
pub trait RootStrategy<O: ObjectKind>: Debug + PartialEq {
|
||||
/// Declare `oi` as the root of all accepted [`Expr`]s produced by
|
||||
/// the parser.
|
||||
fn new_root(oi: ObjectIndex<O>) -> Self;
|
||||
|
||||
/// Look up the provided identifier `id` on the [`Asg`] and indicate
|
||||
/// that its definition is associated with [`Self`]'s root.
|
||||
///
|
||||
/// This is invoked for _all_ identifiers,
|
||||
/// including sub-expressions.
|
||||
fn defines(&self, asg: &mut Asg, id: SPair) -> ObjectIndex<Ident>;
|
||||
|
||||
/// Hold or reject a [`Dangling`] root [`Expr`].
|
||||
///
|
||||
/// A [`Dangling`] expression is not reachable by any other object,
|
||||
/// so this strategy must decide whether to root it in [`Self`] or
|
||||
/// reject it.
|
||||
fn hold_dangling(
|
||||
&self,
|
||||
asg: &mut Asg,
|
||||
oi_expr: ObjectIndex<Expr>,
|
||||
) -> Result<(), AsgError>;
|
||||
}
|
||||
|
||||
/// Accept and root only [`Reachable`] root expressions.
|
||||
///
|
||||
/// Note that a root expresion is still [`Dangling`]
|
||||
/// (and therefore not [`Reachable`])
|
||||
/// even if one of its sub-expressions has been bound to an
|
||||
/// identifier.
|
||||
/// In that case,
|
||||
/// the sub-expression will be rooted in [`Self`],
|
||||
/// but the [`Dangling`] root expression will still be rejected.
|
||||
///
|
||||
/// See [`RootStrategy`] for more information.
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub struct ReachableOnly<O: ObjectKind>(ObjectIndex<O>)
|
||||
where
|
||||
O: ObjectRelTo<Ident>;
|
||||
|
||||
impl<O: ObjectKind> RootStrategy<O> for ReachableOnly<O>
|
||||
where
|
||||
O: ObjectRelTo<Ident>,
|
||||
{
|
||||
fn new_root(oi: ObjectIndex<O>) -> Self {
|
||||
Self(oi)
|
||||
}
|
||||
|
||||
fn defines(&self, asg: &mut Asg, id: SPair) -> ObjectIndex<Ident> {
|
||||
match self {
|
||||
Self(oi_root) => {
|
||||
asg.lookup_or_missing(id).add_edge_from(asg, *oi_root, None)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn hold_dangling(
|
||||
&self,
|
||||
asg: &mut Asg,
|
||||
oi_expr: ObjectIndex<Expr>,
|
||||
) -> Result<(), AsgError> {
|
||||
Err(AsgError::DanglingExpr(oi_expr.resolve(asg).span()))
|
||||
}
|
||||
}
|
||||
|
||||
/// Accept both [`Reachable`] and [`Dangling`] expressions.
|
||||
///
|
||||
/// A [`Dangling`] expression will have the [`Expr`] rooted instead of
|
||||
/// an [`Ident`].
|
||||
///
|
||||
/// Sub-expressions can be thought of as utilizing this strategy with an
|
||||
/// implicit parent [`ObjectIndex<Expr>`](ObjectIndex).
|
||||
///
|
||||
/// See [`RootStrategy`] for more information.
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub struct StoreDangling<O: ObjectKind>(ObjectIndex<O>)
|
||||
where
|
||||
O: ObjectRelTo<Ident> + ObjectRelTo<Expr>;
|
||||
|
||||
impl<O: ObjectKind> RootStrategy<O> for StoreDangling<O>
|
||||
where
|
||||
O: ObjectRelTo<Ident> + ObjectRelTo<Expr>,
|
||||
{
|
||||
fn new_root(oi: ObjectIndex<O>) -> Self {
|
||||
Self(oi)
|
||||
}
|
||||
|
||||
fn defines(&self, asg: &mut Asg, id: SPair) -> ObjectIndex<Ident> {
|
||||
// We are a superset of `ReachableOnly`'s behavior,
|
||||
// so delegate to avoid duplication.
|
||||
match self {
|
||||
Self(oi_root) => ReachableOnly(*oi_root).defines(asg, id),
|
||||
}
|
||||
}
|
||||
|
||||
fn hold_dangling(
|
||||
&self,
|
||||
asg: &mut Asg,
|
||||
oi_expr: ObjectIndex<Expr>,
|
||||
) -> Result<(), AsgError> {
|
||||
let Self(oi_root) = self;
|
||||
|
||||
oi_root.add_edge_to(asg, oi_expr, None);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod test;
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -18,6 +18,8 @@
|
|||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! AIR token definitions.
|
||||
//!
|
||||
//! See [`Air`] for more information.
|
||||
|
||||
use super::super::{ExprOp, FragmentText, IdentKind, Source};
|
||||
use crate::{
|
||||
|
@ -334,6 +336,7 @@ macro_rules! sum_ir {
|
|||
// subtypes.
|
||||
$(
|
||||
$(#[$sumattr])*
|
||||
#[allow(clippy::enum_variant_names)] // intentional consistency
|
||||
#[derive(Debug, PartialEq)]
|
||||
$sumvis enum $sumsub {
|
||||
$(
|
||||
|
@ -405,6 +408,20 @@ sum_ir! {
|
|||
/// populating the ASG with the raw data that that will be
|
||||
/// subsequently analyzed and rewritten.
|
||||
///
|
||||
/// Terminology
|
||||
/// ===========
|
||||
/// AIR uses the terms _start_ and _end_ to refer to tokens that act as
|
||||
/// delimiters,
|
||||
/// which is in contrast to other IRs of this system.
|
||||
/// This is to avoid confusing terminology conflicts with the term
|
||||
/// _closed_—an
|
||||
/// object is _closed_ if it contains no free variables.
|
||||
/// A variable is _free_ in some object if it has no value.
|
||||
/// For example,
|
||||
/// a template is closed iff all of its parameters have been bound to
|
||||
/// values
|
||||
/// (have received arguments or have assumed their defaults).
|
||||
///
|
||||
/// Implementation Notes
|
||||
/// ====================
|
||||
/// [`Air`] is a public token type;
|
||||
|
@ -449,16 +466,37 @@ sum_ir! {
|
|||
/// within a given package,
|
||||
/// but we have no such restriction.
|
||||
///
|
||||
/// TODO: The package needs a name,
|
||||
/// and we'll need to determine how to best represent that relative to
|
||||
/// the project root and be considerate of symlinks.
|
||||
PkgOpen(span: Span) => {
|
||||
/// Packages are assigned unique names in a similar way to
|
||||
/// identifiers.
|
||||
/// This name is generally expected to be generated from the
|
||||
/// path to the package on the host filesystem,
|
||||
/// so no guarantees are made as to the [`Span`] associated
|
||||
/// with the provided `name`.
|
||||
/// For clarity,
|
||||
/// the first [`Span`] is intended to represent the start of
|
||||
/// the package declaration,
|
||||
/// however that is represented by the source stream;
|
||||
/// the `name` [`Span`] may or may not duplicate it.
|
||||
PkgStart(span: Span, name: SPair) => {
|
||||
span: span,
|
||||
display: |f| write!(f, "open package"),
|
||||
},
|
||||
|
||||
/// Import a package identified by the provided namespec.
|
||||
///
|
||||
/// This is similar to [`AirBind::RefIdent`],
|
||||
/// except that this is used for package references whereas
|
||||
/// the latter is used for identifiers.
|
||||
/// Having a token to uniquely represent imports allows package
|
||||
/// parsers to omit support for [`AirBind`] entirely rather
|
||||
/// than supporting that subset only to use the one token.
|
||||
PkgImport(namespec: SPair) => {
|
||||
span: namespec,
|
||||
display: |f| write!(f, "import package {}", TtQuote::wrap(namespec)),
|
||||
},
|
||||
|
||||
/// Complete processing of the current package.
|
||||
PkgClose(span: Span) => {
|
||||
PkgEnd(span: Span) => {
|
||||
span: span,
|
||||
display: |f| write!(f, "close package"),
|
||||
},
|
||||
|
@ -485,14 +523,14 @@ sum_ir! {
|
|||
/// [`Air::BindIdent`].
|
||||
///
|
||||
/// Expressions are composed of references to other expressions.
|
||||
ExprOpen(op: ExprOp, span: Span) => {
|
||||
ExprStart(op: ExprOp, span: Span) => {
|
||||
span: span,
|
||||
display: |f| write!(f, "open {op} expression"),
|
||||
},
|
||||
|
||||
/// Complete the expression atop of the expression stack and pop it from
|
||||
/// the stack.
|
||||
ExprClose(span: Span) => {
|
||||
ExprEnd(span: Span) => {
|
||||
span: span,
|
||||
display: |f| write!(f, "close expression"),
|
||||
},
|
||||
|
@ -606,10 +644,56 @@ sum_ir! {
|
|||
}
|
||||
|
||||
/// Subset of [`Air`] tokens for defining [`Tpl`]s.
|
||||
///
|
||||
/// Templates serve as containers for objects that reference
|
||||
/// metasyntactic variables,
|
||||
/// defined by [`AirMeta::MetaStart`].
|
||||
///
|
||||
/// Template Application
|
||||
/// ====================
|
||||
/// _Application_ is triggered by a [`Air::RefIdent`] to a [`Tpl`]
|
||||
/// or a [`Air::TplEndRef`] and will re-bind an inner template to
|
||||
/// the metavariables in the current context and expand the
|
||||
/// template in place.
|
||||
/// A metavariable is _free_ if it has no bound value.
|
||||
/// A template is _closed_ if it has no free metavariables.
|
||||
/// Application _expands_ into its context,
|
||||
/// and application of a closed template is equivalent to the
|
||||
/// notion of template application in NIR / TAME's source language.
|
||||
///
|
||||
/// Let α be the current template definition context
|
||||
/// (via [`Air::TplStart`])
|
||||
/// and let β be the inner template.
|
||||
/// All free metavariables in β that contain default values in α
|
||||
/// (via [`Air::MetaStart`])
|
||||
/// corresponding to the same [`Ident`] will be _bound_ to
|
||||
/// that value.
|
||||
/// The body of the inner template β will be expanded into the
|
||||
/// body of α.
|
||||
/// The result is a template α which is the application of its
|
||||
/// parameters to β.
|
||||
///
|
||||
/// Partial application is not yet supported,
|
||||
/// but can be added if it is worth the effort of doing so.
|
||||
/// This simplifies the semantics of this operation:
|
||||
///
|
||||
/// - All metavariables that are still free in β after binding
|
||||
/// will assume their default values, if any; and
|
||||
/// - All metavariables that are still free in β after
|
||||
/// applying defaults will result in an error.
|
||||
///
|
||||
/// Consequently,
|
||||
/// the template α will always be closed after this operation.
|
||||
/// This can be thought of like a lexical thunk.
|
||||
///
|
||||
/// α can then be expanded in place using [`Air::TplEndRef`] if
|
||||
/// it is anonymous.
|
||||
/// If α is named,
|
||||
/// [`Air::RefIdent`] can be used to trigger expansion.
|
||||
enum AirTpl {
|
||||
/// Create a new [`Tpl`] on the graph and switch to template parsing.
|
||||
///
|
||||
/// Until [`Self::TplClose`] is found,
|
||||
/// Until [`Self::TplEnd`] is found,
|
||||
/// all parsed objects will be parented to the [`Tpl`] rather than the
|
||||
/// parent [`Pkg`].
|
||||
/// Template parsing also recognizes additional nodes that can appear
|
||||
|
@ -617,24 +701,184 @@ sum_ir! {
|
|||
///
|
||||
/// The active expression stack will be restored after template
|
||||
/// parsing has concluded.
|
||||
TplOpen(span: Span) => {
|
||||
TplStart(span: Span) => {
|
||||
span: span,
|
||||
display: |f| write!(f, "open template"),
|
||||
},
|
||||
|
||||
/// Close the active [`Tpl`] and exit template parsing.
|
||||
/// Complete the active [`Tpl`] and exit template parsing.
|
||||
///
|
||||
/// The expression stack will be restored to its prior state.
|
||||
TplClose(span: Span) => {
|
||||
///
|
||||
/// If the template is anonymous,
|
||||
/// then this will result in an error,
|
||||
/// since nothing will be able to reference the template to
|
||||
/// utilize it.
|
||||
/// See [`Self::TplEndRef`] if you wish to apply an anonymous
|
||||
/// template.
|
||||
TplEnd(span: Span) => {
|
||||
span: span,
|
||||
display: |f| write!(f, "close template"),
|
||||
display: |f| write!(f, "end template definition"),
|
||||
},
|
||||
|
||||
/// Complete the active _closed_ [`Tpl`] just as [`Self::TplEnd`],
|
||||
/// but reference its value,
|
||||
/// with the effect of expanding it in place.
|
||||
///
|
||||
/// If the active template is not closed,
|
||||
/// this will result in an error.
|
||||
///
|
||||
/// This additional token is not ideal;
|
||||
/// ideally [`Air`] would have a means by which to manipulate
|
||||
/// anonymous objects.
|
||||
/// However,
|
||||
/// until such a thing is derived,
|
||||
/// this is the only current use case,
|
||||
/// allowing us to avoid having to generate identifiers for
|
||||
/// templates just for the sake of expansion.
|
||||
///
|
||||
/// If the active template is identified as τ,
|
||||
/// then this has the same behavior as first completing its
|
||||
/// definition with [`Self::TplEnd`] and then referencing τ as
|
||||
/// in [`Air::RefIdent(SPair(τ, …))`](Air::RefIdent).
|
||||
TplEndRef(span: Span) => {
|
||||
span: span,
|
||||
display: |f| write!(f, "end template definition and expand it"),
|
||||
},
|
||||
}
|
||||
|
||||
enum AirMeta {
|
||||
/// Begin a metavariable definition.
|
||||
///
|
||||
/// A metavariable is anonymous unless identified via
|
||||
/// [`AirBind::BindIdent`] before [`Self::MetaEnd`].
|
||||
///
|
||||
/// Metavariables may contain default values,
|
||||
/// making their specification during application optional.
|
||||
/// A metavariable may contain an ordered mixture of references
|
||||
/// to another metavariables via [`AirBind::RefIdent`] and
|
||||
/// literals via [`Self::MetaLexeme`].
|
||||
/// Once all metavariable references have been satisfied during
|
||||
/// application,
|
||||
/// all children will be combined into a single lexeme to
|
||||
/// serve as a final identifier.
|
||||
///
|
||||
/// The interpretation of a metavariable depends solely on the
|
||||
/// context in which it is referenced.
|
||||
MetaStart(span: Span) => {
|
||||
span: span,
|
||||
display: |f| write!(
|
||||
f,
|
||||
"open definition of metasyntactic variable",
|
||||
),
|
||||
},
|
||||
|
||||
/// A lexeme to be interpreted in the context of a template
|
||||
/// expansion.
|
||||
MetaLexeme(lex: SPair) => {
|
||||
span: lex,
|
||||
display: |f| write!(f, "lexeme {}", TtQuote::wrap(lex)),
|
||||
},
|
||||
|
||||
/// Complete a metavariable definition.
|
||||
///
|
||||
/// See [`Self::MetaStart`] for more information.
|
||||
MetaEnd(span: Span) => {
|
||||
span: span,
|
||||
display: |f| write!(
|
||||
f,
|
||||
"close definition of metasyntactic variable",
|
||||
),
|
||||
},
|
||||
|
||||
}
|
||||
|
||||
enum AirDoc {
|
||||
/// Describe the active object using an independent clause.
|
||||
///
|
||||
/// This is like a "subject line",
|
||||
/// but is intended to be used when generating documentation
|
||||
/// in various different contexts.
|
||||
/// Users should think of this as taking place of an identifier
|
||||
/// name when used in a sentence,
|
||||
/// and phrase these clauses relative to the semantic
|
||||
/// properties of the object being described.
|
||||
/// The description should be able to stand on its own as a
|
||||
/// simple sentence, and should be able to be used to make
|
||||
/// compound sentences.
|
||||
///
|
||||
/// For example,
|
||||
/// predicates should make sense when being used to describe
|
||||
/// other objects,
|
||||
/// and should make sense when concatenated together using
|
||||
/// conjunctives and disjunctives.
|
||||
/// Calculations should make sense with and without those
|
||||
/// predicates.
|
||||
DocIndepClause(text: SPair) => {
|
||||
span: text,
|
||||
display: |f| write!(
|
||||
f,
|
||||
"documentation describing the active object as a subject \
|
||||
in a sentence",
|
||||
),
|
||||
},
|
||||
|
||||
/// Arbitrary documentation text.
|
||||
///
|
||||
/// TAMER hopes to eventually provide structured documentation,
|
||||
/// but until then,
|
||||
/// this is just some arbitrary block of text.
|
||||
/// Historically,
|
||||
/// the convention was LaTeX,
|
||||
/// but the approach has fallen out of favor;
|
||||
/// TAMER should provide its own documentation format that
|
||||
/// it can reason about.
|
||||
DocText(text: SPair) => {
|
||||
span: text,
|
||||
display: |f| write!(f, "documentation text"),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Package definitions interspersed with documentation in a
|
||||
/// literate style.
|
||||
pub sum enum AirLiteratePkg = AirPkg | AirDoc;
|
||||
|
||||
/// Expressions that are able to be bound to identifiers.
|
||||
///
|
||||
/// This is the primary token set when parsing packages,
|
||||
/// since most everything in TAMER is an expression.
|
||||
pub sum enum AirBindableExpr = AirExpr | AirBind;
|
||||
pub sum enum AirBindableExpr = AirExpr | AirBind | AirDoc;
|
||||
|
||||
/// Tokens that may be used to define or apply templates.
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AirIdent {
|
||||
/// Name of the identifier described by this token.
|
||||
pub fn name(&self) -> SPair {
|
||||
use AirIdent::*;
|
||||
|
||||
match self {
|
||||
IdentDecl(name, _, _)
|
||||
| IdentExternDecl(name, _, _)
|
||||
| IdentDep(name, _)
|
||||
| IdentFragment(name, _)
|
||||
| IdentRoot(name) => *name,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,136 @@
|
|||
// 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/>.
|
||||
|
||||
//! AIR metavariable parser.
|
||||
//!
|
||||
//! See the [parent module](super) for more information.
|
||||
|
||||
use super::{
|
||||
super::{AsgError, ObjectIndex},
|
||||
ir::AirBindableMeta,
|
||||
AirAggregate, AirAggregateCtx,
|
||||
};
|
||||
use crate::{
|
||||
asg::graph::object::Meta, diagnose::Annotate, diagnostic_todo,
|
||||
parse::prelude::*,
|
||||
};
|
||||
|
||||
/// Metasyntactic variable (metavariable) parser.
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum AirMetaAggregate {
|
||||
/// Ready for the start of a metavariable.
|
||||
Ready,
|
||||
|
||||
/// Defining a metavariable.
|
||||
TplMeta(ObjectIndex<Meta>),
|
||||
}
|
||||
|
||||
impl Display for AirMetaAggregate {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
use AirMetaAggregate::*;
|
||||
|
||||
match self {
|
||||
Ready => write!(f, "ready for metavariable"),
|
||||
TplMeta(_) => write!(f, "defining metavariable"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ParseState for AirMetaAggregate {
|
||||
type Token = AirBindableMeta;
|
||||
type Object = ();
|
||||
type Error = AsgError;
|
||||
type Context = AirAggregateCtx;
|
||||
type Super = AirAggregate;
|
||||
|
||||
fn parse_token(
|
||||
self,
|
||||
tok: Self::Token,
|
||||
ctx: &mut Self::Context,
|
||||
) -> TransitionResult<Self::Super> {
|
||||
use super::ir::{AirBind::*, AirMeta::*};
|
||||
use AirBindableMeta::*;
|
||||
use AirMetaAggregate::*;
|
||||
|
||||
match (self, tok) {
|
||||
(Ready, AirMeta(MetaStart(span))) => {
|
||||
let oi_meta = ctx.asg_mut().create(Meta::new_required(span));
|
||||
Transition(TplMeta(oi_meta)).incomplete()
|
||||
}
|
||||
(TplMeta(oi_meta), AirMeta(MetaEnd(cspan))) => {
|
||||
oi_meta.close(ctx.asg_mut(), cspan);
|
||||
Transition(Ready).incomplete()
|
||||
}
|
||||
|
||||
(TplMeta(oi_meta), AirMeta(MetaLexeme(lexeme))) => Transition(
|
||||
TplMeta(oi_meta.assign_lexeme(ctx.asg_mut(), lexeme)),
|
||||
)
|
||||
.incomplete(),
|
||||
|
||||
(TplMeta(oi_meta), AirBind(BindIdent(name))) => ctx
|
||||
.defines(name)
|
||||
.and_then(|oi_ident| {
|
||||
oi_ident.bind_definition(ctx.asg_mut(), name, oi_meta)
|
||||
})
|
||||
.map(|_| ())
|
||||
.transition(TplMeta(oi_meta)),
|
||||
|
||||
(TplMeta(..), tok @ AirBind(RefIdent(..))) => {
|
||||
diagnostic_todo!(
|
||||
vec![tok.note("this token")],
|
||||
"AirBind in metavar context (param-value)"
|
||||
)
|
||||
}
|
||||
|
||||
(TplMeta(..), tok @ AirMeta(MetaStart(..))) => {
|
||||
diagnostic_todo!(
|
||||
vec![tok.note("this token")],
|
||||
"AirMeta variant"
|
||||
)
|
||||
}
|
||||
|
||||
(Ready, tok @ AirMeta(MetaEnd(..))) => {
|
||||
diagnostic_todo!(
|
||||
vec![tok.note("this token")],
|
||||
"unbalanced meta"
|
||||
)
|
||||
}
|
||||
|
||||
(Ready, tok @ AirMeta(MetaLexeme(..))) => {
|
||||
diagnostic_todo!(
|
||||
vec![tok.note("this token")],
|
||||
"unexpected lexeme"
|
||||
)
|
||||
}
|
||||
|
||||
// Maybe the bind can be handled by the parent frame.
|
||||
(Ready, tok @ AirBind(..)) => Transition(Ready).dead(tok),
|
||||
}
|
||||
}
|
||||
|
||||
fn is_accepting(&self, _: &Self::Context) -> bool {
|
||||
matches!(self, Self::Ready)
|
||||
}
|
||||
}
|
||||
|
||||
impl AirMetaAggregate {
|
||||
pub(super) fn new() -> Self {
|
||||
Self::Ready
|
||||
}
|
||||
}
|
|
@ -0,0 +1,105 @@
|
|||
// ASG IR opaque object 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/>.
|
||||
|
||||
//! AIR opaque object parser.
|
||||
//!
|
||||
//! This parser exists primarily to ensure that the parent frame can be held
|
||||
//! on the stack and considered in lexical scoping operations.
|
||||
//!
|
||||
//! See the [parent module](super) for more information.
|
||||
|
||||
use super::{super::AsgError, ir::AirIdent, AirAggregate, AirAggregateCtx};
|
||||
use crate::parse::prelude::*;
|
||||
use std::fmt::Display;
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum AirOpaqueAggregate {
|
||||
Ready,
|
||||
}
|
||||
|
||||
impl Display for AirOpaqueAggregate {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Ready => {
|
||||
write!(f, "ready for opaque object")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ParseState for AirOpaqueAggregate {
|
||||
type Token = AirIdent;
|
||||
type Object = ();
|
||||
type Error = AsgError;
|
||||
type Context = AirAggregateCtx;
|
||||
type Super = AirAggregate;
|
||||
|
||||
fn parse_token(
|
||||
self,
|
||||
tok: Self::Token,
|
||||
ctx: &mut Self::Context,
|
||||
) -> crate::parse::TransitionResult<Self::Super> {
|
||||
use super::ir::AirIdent::*;
|
||||
use AirOpaqueAggregate::*;
|
||||
|
||||
match (self, tok) {
|
||||
(Ready, IdentDecl(name, kind, src)) => ctx
|
||||
.lookup_lexical_or_missing(name)
|
||||
.declare(ctx.asg_mut(), name, kind, src)
|
||||
.map(|_| ())
|
||||
.transition(Ready),
|
||||
|
||||
(Ready, IdentExternDecl(name, kind, src)) => ctx
|
||||
.lookup_lexical_or_missing(name)
|
||||
.declare_extern(ctx.asg_mut(), name, kind, src)
|
||||
.map(|_| ())
|
||||
.transition(Ready),
|
||||
|
||||
(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()
|
||||
}
|
||||
|
||||
(Ready, IdentFragment(name, text)) => ctx
|
||||
.lookup_lexical_or_missing(name)
|
||||
.set_fragment(ctx.asg_mut(), text)
|
||||
.map(|_| ())
|
||||
.transition(Ready),
|
||||
|
||||
(Ready, IdentRoot(name)) => {
|
||||
ctx.lookup_lexical_or_missing(name).root(ctx.asg_mut());
|
||||
|
||||
Transition(Ready).incomplete()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn is_accepting(&self, _: &Self::Context) -> bool {
|
||||
matches!(self, Self::Ready)
|
||||
}
|
||||
}
|
||||
|
||||
impl AirOpaqueAggregate {
|
||||
pub(super) fn new() -> Self {
|
||||
Self::Ready
|
||||
}
|
||||
}
|
|
@ -0,0 +1,159 @@
|
|||
// ASG IR package 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/>.
|
||||
|
||||
//! AIR package parser.
|
||||
//!
|
||||
//! See the [parent module](super) for more information.
|
||||
|
||||
use super::{
|
||||
super::{graph::object::Pkg, AsgError, ObjectIndex},
|
||||
ir::AirLiteratePkg,
|
||||
AirAggregate, AirAggregateCtx,
|
||||
};
|
||||
use crate::{diagnose::Annotate, diagnostic_todo, parse::prelude::*};
|
||||
|
||||
/// Package parsing with support for loaded identifiers.
|
||||
///
|
||||
/// This supports non-nested package definitions of source files,
|
||||
/// as well as declaring opaque identifiers loaded from object files via
|
||||
/// [`AirIdent`](super::ir::AirIdent).
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum AirPkgAggregate {
|
||||
/// Ready for an expression;
|
||||
/// expression stack is empty.
|
||||
Ready,
|
||||
|
||||
/// Expecting a package-level token.
|
||||
Toplevel(ObjectIndex<Pkg>),
|
||||
}
|
||||
|
||||
impl Display for AirPkgAggregate {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
use AirPkgAggregate::*;
|
||||
|
||||
match self {
|
||||
Ready => {
|
||||
write!(f, "expecting package definition")
|
||||
}
|
||||
Toplevel(_) => {
|
||||
write!(f, "expecting package header or an expression")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ParseState for AirPkgAggregate {
|
||||
type Token = AirLiteratePkg;
|
||||
type Object = ();
|
||||
type Error = AsgError;
|
||||
type Context = AirAggregateCtx;
|
||||
type Super = AirAggregate;
|
||||
|
||||
fn parse_token(
|
||||
self,
|
||||
tok: Self::Token,
|
||||
ctx: &mut Self::Context,
|
||||
) -> crate::parse::TransitionResult<Self::Super> {
|
||||
use super::ir::{AirDoc::*, AirPkg::*};
|
||||
use AirLiteratePkg::*;
|
||||
use AirPkgAggregate::*;
|
||||
|
||||
match (self, tok) {
|
||||
(st @ (Ready | Toplevel(..)), AirPkg(PkgStart(span, name))) => {
|
||||
if let Some(first) =
|
||||
ctx.pkg_oi().map(|oi| oi.resolve(ctx.asg_ref()))
|
||||
{
|
||||
let first_span = first.span();
|
||||
let first_name = first.canonical_name();
|
||||
|
||||
Transition(st).err(AsgError::NestedPkgStart(
|
||||
(span, name),
|
||||
(first_span, first_name),
|
||||
))
|
||||
} else {
|
||||
match ctx.pkg_begin(span, name) {
|
||||
Ok(oi_pkg) => Transition(Toplevel(oi_pkg)).incomplete(),
|
||||
Err(e) => Transition(Ready).err(e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
(Toplevel(oi_pkg), AirPkg(PkgEnd(span))) => {
|
||||
oi_pkg.close(ctx.asg_mut(), span);
|
||||
ctx.pkg_clear();
|
||||
Transition(Ready).incomplete()
|
||||
}
|
||||
|
||||
(Toplevel(oi_pkg), tok @ AirDoc(DocIndepClause(..))) => {
|
||||
diagnostic_todo!(
|
||||
vec![
|
||||
oi_pkg.note("for this package"),
|
||||
tok.internal_error(
|
||||
"this package description is not yet supported"
|
||||
)
|
||||
],
|
||||
"package-level short description is not yet supported by TAMER",
|
||||
)
|
||||
}
|
||||
|
||||
(Toplevel(oi_pkg), AirDoc(DocText(text))) => {
|
||||
oi_pkg.append_doc_text(ctx.asg_mut(), text);
|
||||
Transition(Toplevel(oi_pkg)).incomplete()
|
||||
}
|
||||
|
||||
// Package import
|
||||
(Toplevel(oi_pkg), AirPkg(PkgImport(namespec))) => oi_pkg
|
||||
.import(ctx.asg_mut(), namespec)
|
||||
.map(|_| ())
|
||||
.transition(Toplevel(oi_pkg)),
|
||||
|
||||
(Ready, AirPkg(PkgImport(namespec))) => {
|
||||
Transition(Ready).err(AsgError::InvalidPkgImport(namespec))
|
||||
}
|
||||
|
||||
(Ready, AirPkg(PkgEnd(span))) => {
|
||||
Transition(Ready).err(AsgError::InvalidPkgEndContext(span))
|
||||
}
|
||||
|
||||
// Token may refer to a parent context.
|
||||
(st @ Ready, tok @ AirDoc(..)) => Transition(st).dead(tok),
|
||||
}
|
||||
}
|
||||
|
||||
fn is_accepting(&self, _: &Self::Context) -> bool {
|
||||
matches!(self, Self::Ready)
|
||||
}
|
||||
}
|
||||
|
||||
impl AirPkgAggregate {
|
||||
pub fn new() -> Self {
|
||||
Self::Ready
|
||||
}
|
||||
|
||||
/// The [`ObjectIndex`] of the package being parsed,
|
||||
/// if any.
|
||||
pub fn active_pkg_oi(&self) -> Option<ObjectIndex<Pkg>> {
|
||||
use AirPkgAggregate::*;
|
||||
|
||||
match self {
|
||||
Ready => None,
|
||||
Toplevel(oi_pkg) => Some(*oi_pkg),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -22,115 +22,159 @@
|
|||
|
||||
use super::{super::Ident, *};
|
||||
use crate::{
|
||||
asg::{IdentKind, Source},
|
||||
asg::{
|
||||
graph::object::{ObjectRel, ObjectRelFrom, ObjectRelatable},
|
||||
IdentKind, ObjectIndexRelTo, Source, TransitionError,
|
||||
},
|
||||
parse::{ParseError, Parsed, Parser},
|
||||
span::dummy::*,
|
||||
};
|
||||
use std::assert_matches::assert_matches;
|
||||
|
||||
type Sut = AirAggregate;
|
||||
|
||||
use Air::*;
|
||||
use Parsed::Incomplete;
|
||||
|
||||
mod scope;
|
||||
|
||||
#[test]
|
||||
fn ident_decl() {
|
||||
let id = SPair("foo".into(), S1);
|
||||
let id = SPair("foo".into(), S2);
|
||||
let kind = IdentKind::Tpl;
|
||||
let src = Source {
|
||||
src: Some("test/decl".into()),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let toks = vec![Air::IdentDecl(id, kind.clone(), src.clone())].into_iter();
|
||||
#[rustfmt::skip]
|
||||
let toks = vec![
|
||||
PkgStart(S1, SPair("/pkg".into(), S1)),
|
||||
IdentDecl(id, kind.clone(), src.clone()),
|
||||
// Attempt re-declaration.
|
||||
IdentDecl(id, kind.clone(), src.clone()),
|
||||
PkgEnd(S3),
|
||||
].into_iter();
|
||||
|
||||
let mut sut = Sut::parse(toks);
|
||||
|
||||
assert_eq!(Some(Ok(Parsed::Incomplete)), sut.next());
|
||||
assert_eq!(
|
||||
#[rustfmt::skip]
|
||||
vec![
|
||||
Ok(Incomplete), // PkgStart
|
||||
Ok(Incomplete), // IdentDecl
|
||||
// Redeclare identifier
|
||||
Err(ParseError::StateError(AsgError::IdentTransition(
|
||||
TransitionError::Redeclare(id, S2)
|
||||
))),
|
||||
// RECOVERY: Ignore redeclaration
|
||||
Ok(Incomplete), // PkgEnd
|
||||
],
|
||||
sut.by_ref().collect::<Vec<Result<Parsed<()>, _>>>(),
|
||||
);
|
||||
|
||||
let asg = sut.finalize().unwrap().into_context();
|
||||
let ctx = sut.finalize().unwrap().into_private_context();
|
||||
|
||||
let ident_node = asg.lookup(id).expect("identifier was not added to graph");
|
||||
let ident = asg.get(ident_node).unwrap();
|
||||
let ident_node =
|
||||
root_lookup(&ctx, id).expect("identifier was not added to graph");
|
||||
let ident = ctx.asg_ref().get(ident_node).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
Ok(ident),
|
||||
Ident::declare(id)
|
||||
.resolve(S1, kind.clone(), src.clone())
|
||||
.resolve(S2, kind.clone(), src.clone())
|
||||
.as_ref(),
|
||||
);
|
||||
|
||||
// Re-instantiate the parser and test an error by attempting to
|
||||
// redeclare the same identifier.
|
||||
let bad_toks =
|
||||
vec![Air::IdentDecl(SPair(id.symbol(), S2), kind, src)].into_iter();
|
||||
let mut sut = Sut::parse_with_context(bad_toks, asg);
|
||||
|
||||
assert_matches!(
|
||||
sut.next(),
|
||||
Some(Err(ParseError::StateError(AsgError::IdentTransition(_)))),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ident_extern_decl() {
|
||||
let id = SPair("foo".into(), S1);
|
||||
let id = SPair("foo".into(), S2);
|
||||
let re_id = SPair("foo".into(), S3);
|
||||
let kind = IdentKind::Tpl;
|
||||
let different_kind = IdentKind::Meta;
|
||||
let src = Source {
|
||||
src: Some("test/decl-extern".into()),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let toks =
|
||||
vec![Air::IdentExternDecl(id, kind.clone(), src.clone())].into_iter();
|
||||
#[rustfmt::skip]
|
||||
let toks = vec![
|
||||
PkgStart(S1, SPair("/pkg".into(), S1)),
|
||||
IdentExternDecl(id, kind.clone(), src.clone()),
|
||||
// Redeclare with a different kind
|
||||
IdentExternDecl(re_id, different_kind.clone(), src.clone()),
|
||||
PkgEnd(S4),
|
||||
].into_iter();
|
||||
|
||||
let mut sut = Sut::parse(toks);
|
||||
|
||||
assert_eq!(Some(Ok(Parsed::Incomplete)), sut.next());
|
||||
assert_eq!(
|
||||
#[rustfmt::skip]
|
||||
vec![
|
||||
Ok(Incomplete), // PkgStart
|
||||
Ok(Incomplete), // IdentDecl
|
||||
// Redeclare identifier with a different kind
|
||||
Err(ParseError::StateError(AsgError::IdentTransition(
|
||||
TransitionError::ExternResolution(
|
||||
id,
|
||||
kind.clone(),
|
||||
(different_kind, S3)
|
||||
)
|
||||
))),
|
||||
// RECOVERY: Ignore redeclaration
|
||||
Ok(Incomplete), // PkgEnd
|
||||
],
|
||||
sut.by_ref().collect::<Vec<Result<Parsed<()>, _>>>(),
|
||||
);
|
||||
|
||||
let asg = sut.finalize().unwrap().into_context();
|
||||
let ctx = sut.finalize().unwrap().into_private_context();
|
||||
|
||||
let ident_node = asg.lookup(id).expect("identifier was not added to graph");
|
||||
let ident = asg.get(ident_node).unwrap();
|
||||
let ident_node =
|
||||
root_lookup(&ctx, id).expect("identifier was not added to graph");
|
||||
let ident = ctx.asg_ref().get(ident_node).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
Ok(ident),
|
||||
Ident::declare(id).extern_(S1, kind, src.clone()).as_ref(),
|
||||
);
|
||||
|
||||
// Re-instantiate the parser and test an error by attempting to
|
||||
// redeclare with a different kind.
|
||||
let different_kind = IdentKind::Meta;
|
||||
let bad_toks = vec![Air::IdentExternDecl(
|
||||
SPair(id.symbol(), S2),
|
||||
different_kind,
|
||||
src,
|
||||
)]
|
||||
.into_iter();
|
||||
let mut sut = Sut::parse_with_context(bad_toks, asg);
|
||||
|
||||
assert_matches!(
|
||||
sut.next(),
|
||||
Some(Err(ParseError::StateError(AsgError::IdentTransition(_)))),
|
||||
Ident::declare(id).extern_(S2, kind, src.clone()).as_ref(),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ident_dep() {
|
||||
let id = SPair("foo".into(), S1);
|
||||
let dep = SPair("dep".into(), S2);
|
||||
let id = SPair("foo".into(), S2);
|
||||
let dep = SPair("dep".into(), S3);
|
||||
|
||||
#[rustfmt::skip]
|
||||
let toks = vec![
|
||||
PkgStart(S1, SPair("/pkg".into(), S1)),
|
||||
IdentDep(id, dep),
|
||||
PkgEnd(S4),
|
||||
].into_iter();
|
||||
|
||||
let toks = vec![Air::IdentDep(id, dep)].into_iter();
|
||||
let mut sut = Sut::parse(toks);
|
||||
|
||||
assert_eq!(Some(Ok(Parsed::Incomplete)), sut.next());
|
||||
assert_eq!(
|
||||
#[rustfmt::skip]
|
||||
Ok(vec![
|
||||
Incomplete, // PkgStart
|
||||
Incomplete, // IdentDep
|
||||
Incomplete, // PkgEnd
|
||||
]),
|
||||
sut.by_ref().collect(),
|
||||
);
|
||||
|
||||
let asg = sut.finalize().unwrap().into_context();
|
||||
let ctx = sut.finalize().unwrap().into_private_context();
|
||||
|
||||
let ident_node = asg.lookup(id).expect("identifier was not added to graph");
|
||||
let dep_node = asg.lookup(dep).expect("dep was not added to graph");
|
||||
let ident_node =
|
||||
root_lookup(&ctx, id).expect("identifier was not added to graph");
|
||||
let dep_node = root_lookup(&ctx, dep).expect("dep was not added to graph");
|
||||
|
||||
assert!(asg.has_dep(ident_node, dep_node));
|
||||
assert!(ident_node.has_edge_to(ctx.asg_ref(), dep_node));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ident_fragment() {
|
||||
let id = SPair("frag".into(), S1);
|
||||
let id = SPair("frag".into(), S2);
|
||||
let kind = IdentKind::Tpl;
|
||||
let src = Source {
|
||||
src: Some("test/frag".into()),
|
||||
|
@ -138,72 +182,94 @@ fn ident_fragment() {
|
|||
};
|
||||
let frag = "fragment text".into();
|
||||
|
||||
#[rustfmt::skip]
|
||||
let toks = vec![
|
||||
// Identifier must be declared before it can be given a
|
||||
// fragment.
|
||||
Air::IdentDecl(id, kind.clone(), src.clone()),
|
||||
Air::IdentFragment(id, frag),
|
||||
]
|
||||
.into_iter();
|
||||
PkgStart(S1, SPair("/pkg".into(), S1)),
|
||||
// Identifier must be declared before it can be given a
|
||||
// fragment.
|
||||
IdentDecl(id, kind.clone(), src.clone()),
|
||||
IdentFragment(id, frag),
|
||||
// Reset fragment (error)
|
||||
IdentFragment(id, frag),
|
||||
// RECOVERY: Ignore reset
|
||||
PkgEnd(S4),
|
||||
] .into_iter();
|
||||
|
||||
let mut sut = Sut::parse(toks);
|
||||
|
||||
assert_eq!(Some(Ok(Parsed::Incomplete)), sut.next()); // IdentDecl
|
||||
assert_eq!(Some(Ok(Parsed::Incomplete)), sut.next()); // IdentFragment
|
||||
assert_eq!(
|
||||
#[rustfmt::skip]
|
||||
vec![
|
||||
Ok(Incomplete), // PkgStart
|
||||
Ok(Incomplete), // IdentDecl
|
||||
Ok(Incomplete), // IdentFragment
|
||||
// Reset fragment
|
||||
Err(ParseError::StateError(AsgError::IdentTransition(
|
||||
TransitionError::BadFragmentDest(id)
|
||||
))),
|
||||
// RECOVERY: Ignore reset
|
||||
Ok(Incomplete), // PkgEnd
|
||||
],
|
||||
sut.by_ref().collect::<Vec<Result<Parsed<()>, _>>>(),
|
||||
);
|
||||
|
||||
let asg = sut.finalize().unwrap().into_context();
|
||||
let ctx = sut.finalize().unwrap().into_private_context();
|
||||
|
||||
let ident_node = asg.lookup(id).expect("identifier was not added to graph");
|
||||
let ident = asg.get(ident_node).unwrap();
|
||||
let ident_node =
|
||||
root_lookup(&ctx, id).expect("identifier was not added to graph");
|
||||
let ident = ctx.asg_ref().get(ident_node).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
Ok(ident),
|
||||
Ident::declare(id)
|
||||
.resolve(S1, kind.clone(), src.clone())
|
||||
.resolve(S2, kind.clone(), src.clone())
|
||||
.and_then(|resolved| resolved.set_fragment(frag))
|
||||
.as_ref(),
|
||||
);
|
||||
|
||||
// Re-instantiate the parser and test an error by attempting to
|
||||
// re-set the fragment.
|
||||
let bad_toks = vec![Air::IdentFragment(id, frag)].into_iter();
|
||||
let mut sut = Sut::parse_with_context(bad_toks, asg);
|
||||
|
||||
assert_matches!(
|
||||
sut.next(),
|
||||
Some(Err(ParseError::StateError(AsgError::IdentTransition(_)))),
|
||||
);
|
||||
}
|
||||
|
||||
// Adding a root before the identifier exists should add a
|
||||
// `Ident::Missing`.
|
||||
#[test]
|
||||
fn ident_root_missing() {
|
||||
let id = SPair("toroot".into(), S1);
|
||||
let id = SPair("toroot".into(), S2);
|
||||
|
||||
#[rustfmt::skip]
|
||||
let toks = vec![
|
||||
PkgStart(S1, SPair("/pkg".into(), S1)),
|
||||
IdentRoot(id),
|
||||
PkgEnd(S3),
|
||||
].into_iter();
|
||||
|
||||
let toks = vec![Air::IdentRoot(id)].into_iter();
|
||||
let mut sut = Sut::parse(toks);
|
||||
|
||||
assert_eq!(Some(Ok(Parsed::Incomplete)), sut.next());
|
||||
assert_eq!(
|
||||
#[rustfmt::skip]
|
||||
Ok(vec![
|
||||
Incomplete, // PkgStart
|
||||
Incomplete, // IdentRoot
|
||||
Incomplete, // PkgEnd
|
||||
]),
|
||||
sut.by_ref().collect(),
|
||||
);
|
||||
|
||||
let asg = sut.finalize().unwrap().into_context();
|
||||
let ctx = sut.finalize().unwrap().into_private_context();
|
||||
|
||||
let ident_node = asg
|
||||
.lookup(id)
|
||||
.expect("identifier was not added to the graph");
|
||||
let ident = asg.get(ident_node).unwrap();
|
||||
let ident_node =
|
||||
root_lookup(&ctx, id).expect("identifier was not added to the graph");
|
||||
let ident = ctx.asg_ref().get(ident_node).unwrap();
|
||||
|
||||
// The identifier did not previously exist,
|
||||
// and so a missing node is created as a placeholder.
|
||||
assert_eq!(&Ident::Missing(id), ident);
|
||||
|
||||
// And that missing identifier should be rooted.
|
||||
assert!(asg.is_rooted(ident_node));
|
||||
assert!(ident_node.is_rooted(ctx.asg_ref()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ident_root_existing() {
|
||||
let id = SPair("toroot".into(), S1);
|
||||
let id = SPair("toroot".into(), S2);
|
||||
let kind = IdentKind::Tpl;
|
||||
let src = Source {
|
||||
src: Some("test/root-existing".into()),
|
||||
|
@ -214,44 +280,107 @@ fn ident_root_existing() {
|
|||
// otherwise we won't be testing the right thing.
|
||||
assert!(!kind.is_auto_root());
|
||||
|
||||
#[rustfmt::skip]
|
||||
let toks = vec![
|
||||
Air::IdentDecl(id, kind.clone(), src.clone()),
|
||||
Air::IdentRoot(SPair(id.symbol(), S2)),
|
||||
PkgStart(S1, SPair("/pkg".into(), S1)),
|
||||
IdentDecl(id, kind.clone(), src.clone()),
|
||||
IdentRoot(SPair(id.symbol(), S3)),
|
||||
PkgEnd(S3),
|
||||
]
|
||||
.into_iter();
|
||||
|
||||
let mut sut = Sut::parse(toks);
|
||||
|
||||
assert_eq!(Some(Ok(Parsed::Incomplete)), sut.next()); // IdentDecl
|
||||
assert_eq!(Some(Ok(Parsed::Incomplete)), sut.next()); // IdentRoot
|
||||
assert_eq!(
|
||||
#[rustfmt::skip]
|
||||
Ok(vec![
|
||||
Incomplete, // PkgStart
|
||||
Incomplete, // IdentDecl
|
||||
Incomplete, // IdentRoot
|
||||
Incomplete, // PkgEnd
|
||||
]),
|
||||
sut.by_ref().collect(),
|
||||
);
|
||||
|
||||
let asg = sut.finalize().unwrap().into_context();
|
||||
let ctx = sut.finalize().unwrap().into_private_context();
|
||||
|
||||
let ident_node = asg
|
||||
.lookup(id)
|
||||
.expect("identifier was not added to the graph");
|
||||
let ident = asg.get(ident_node).unwrap();
|
||||
let ident_node =
|
||||
root_lookup(&ctx, id).expect("identifier was not added to the graph");
|
||||
let ident = ctx.asg_ref().get(ident_node).unwrap();
|
||||
|
||||
// The previously-declared identifier...
|
||||
assert_eq!(
|
||||
Ok(ident),
|
||||
Ident::declare(id)
|
||||
.resolve(S1, kind.clone(), src.clone())
|
||||
.resolve(S2, kind.clone(), src.clone())
|
||||
.as_ref()
|
||||
);
|
||||
|
||||
// ...should have been subsequently rooted.
|
||||
assert!(asg.is_rooted(ident_node));
|
||||
assert!(ident_node.is_rooted(ctx.asg_ref()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn declare_kind_auto_root() {
|
||||
let auto_kind = IdentKind::Worksheet;
|
||||
let no_auto_kind = IdentKind::Tpl;
|
||||
|
||||
// Sanity check, in case this changes.
|
||||
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 src = Source {
|
||||
src: Some("src/pkg".into()),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
#[rustfmt::skip]
|
||||
let toks = [
|
||||
PkgStart(S1, SPair("/pkg".into(), S1)),
|
||||
// auto-rooting
|
||||
IdentDecl(id_auto, auto_kind, src.clone()),
|
||||
// non-auto-rooting
|
||||
IdentDecl(id_no_auto, no_auto_kind, src),
|
||||
PkgEnd(S4),
|
||||
].into_iter();
|
||||
|
||||
let mut sut = Sut::parse(toks);
|
||||
|
||||
assert_eq!(
|
||||
#[rustfmt::skip]
|
||||
Ok(vec![
|
||||
Incomplete, // PkgStart
|
||||
Incomplete, // IdentDecl
|
||||
Incomplete, // IdentDecl
|
||||
Incomplete, // PkgEnd
|
||||
]),
|
||||
sut.by_ref().collect(),
|
||||
);
|
||||
|
||||
let ctx = sut.finalize().unwrap().into_private_context();
|
||||
|
||||
let oi_auto = root_lookup(&ctx, id_auto).unwrap();
|
||||
let oi_no_auto = root_lookup(&ctx, id_no_auto).unwrap();
|
||||
|
||||
assert!(oi_auto.is_rooted(ctx.asg_ref()));
|
||||
assert!(!oi_no_auto.is_rooted(ctx.asg_ref()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pkg_is_rooted() {
|
||||
let toks = vec![Air::PkgOpen(S1), Air::PkgClose(S2)];
|
||||
#[rustfmt::skip]
|
||||
let toks = vec![
|
||||
PkgStart(S1, SPair("/pkg".into(), S1)),
|
||||
PkgEnd(S2),
|
||||
];
|
||||
|
||||
let mut sut = Sut::parse(toks.into_iter());
|
||||
assert!(sut.all(|x| x.is_ok()));
|
||||
|
||||
let asg = sut.finalize().unwrap().into_context();
|
||||
let asg = sut.finalize().unwrap().into_context().finish();
|
||||
|
||||
let oi_root = asg.root(S3);
|
||||
let pkg = oi_root
|
||||
|
@ -266,18 +395,18 @@ fn pkg_is_rooted() {
|
|||
#[test]
|
||||
fn close_pkg_without_open() {
|
||||
let toks = vec![
|
||||
Air::PkgClose(S1),
|
||||
PkgEnd(S1),
|
||||
// RECOVERY: Try again.
|
||||
Air::PkgOpen(S2),
|
||||
Air::PkgClose(S3),
|
||||
PkgStart(S2, SPair("/pkg".into(), S2)),
|
||||
PkgEnd(S3),
|
||||
];
|
||||
|
||||
assert_eq!(
|
||||
vec![
|
||||
Err(ParseError::StateError(AsgError::InvalidPkgCloseContext(S1))),
|
||||
Err(ParseError::StateError(AsgError::InvalidPkgEndContext(S1))),
|
||||
// RECOVERY
|
||||
Ok(Parsed::Incomplete), // PkgOpen
|
||||
Ok(Parsed::Incomplete), // PkgClose
|
||||
Ok(Incomplete), // PkgStart
|
||||
Ok(Incomplete), // PkgEnd
|
||||
],
|
||||
Sut::parse(toks.into_iter()).collect::<Vec<_>>(),
|
||||
);
|
||||
|
@ -285,24 +414,274 @@ 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);
|
||||
|
||||
#[rustfmt::skip]
|
||||
let toks = vec![
|
||||
Air::PkgOpen(S1),
|
||||
Air::PkgOpen(S2),
|
||||
// RECOVERY
|
||||
Air::PkgClose(S3),
|
||||
PkgStart(S1, name_a),
|
||||
// Cannot nest package
|
||||
PkgStart(S3, name_b),
|
||||
// RECOVERY
|
||||
PkgEnd(S5),
|
||||
];
|
||||
|
||||
assert_eq!(
|
||||
#[rustfmt::skip]
|
||||
vec![
|
||||
Ok(Parsed::Incomplete), // PkgOpen
|
||||
Err(ParseError::StateError(AsgError::NestedPkgOpen(S2, S1))),
|
||||
// RECOVERY
|
||||
Ok(Parsed::Incomplete), // PkgClose
|
||||
Ok(Incomplete), // PkgStart
|
||||
Err(ParseError::StateError(AsgError::NestedPkgStart(
|
||||
(S3, name_b), (S1, name_a),
|
||||
))),
|
||||
// RECOVERY
|
||||
Ok(Incomplete), // PkgEnd
|
||||
],
|
||||
Sut::parse(toks.into_iter()).collect::<Vec<_>>(),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pkg_canonical_name() {
|
||||
let name = SPair("/foo/bar".into(), S2);
|
||||
|
||||
#[rustfmt::skip]
|
||||
let toks = vec![
|
||||
PkgStart(S1, name),
|
||||
PkgEnd(S3),
|
||||
];
|
||||
|
||||
let ctx = air_ctx_from_toks(toks);
|
||||
|
||||
let oi_root = ctx.asg_ref().root(S1);
|
||||
let oi_pkg = oi_root
|
||||
.edges_filtered::<Pkg>(ctx.asg_ref())
|
||||
.next()
|
||||
.expect("cannot find package from root");
|
||||
|
||||
assert_eq!(name, oi_pkg.resolve(ctx.asg_ref()).canonical_name());
|
||||
|
||||
// We should be able to find the same package by its index.
|
||||
let oi_pkg_indexed = ctx
|
||||
.env_scope_lookup_raw(oi_root, name)
|
||||
.map(|eoi| eoi.into_inner());
|
||||
|
||||
assert_eq!(
|
||||
Some(oi_pkg),
|
||||
oi_pkg_indexed,
|
||||
"package was not indexed at Root"
|
||||
);
|
||||
}
|
||||
|
||||
// This isn't supposed to happen in practice,
|
||||
// especially with normal usage of TAME where names are generated from
|
||||
// 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);
|
||||
|
||||
#[rustfmt::skip]
|
||||
let toks = vec![
|
||||
PkgStart(S1, name),
|
||||
PkgEnd(S3),
|
||||
|
||||
// Attempt to define a package of the same name.
|
||||
PkgStart(S4, name2),
|
||||
|
||||
// RECOVERY: Use a proper name.
|
||||
PkgStart(S6, namefix),
|
||||
PkgEnd(S8),
|
||||
];
|
||||
|
||||
let mut sut = Sut::parse(toks.into_iter());
|
||||
|
||||
assert_eq!(
|
||||
#[rustfmt::skip]
|
||||
vec![
|
||||
Ok(Incomplete), // PkgStart
|
||||
Ok(Incomplete), // PkgEnd
|
||||
|
||||
Err(ParseError::StateError(
|
||||
AsgError::PkgRedeclare(name, name2)
|
||||
)),
|
||||
|
||||
// RECOVERY: Retry with a proper name
|
||||
Ok(Incomplete), // PkgStart
|
||||
Ok(Incomplete), // PkgEnd
|
||||
],
|
||||
sut.by_ref().collect::<Vec<_>>(),
|
||||
);
|
||||
|
||||
let ctx = sut.finalize().unwrap().into_private_context();
|
||||
|
||||
// The second package should be available under the recovery name.
|
||||
let oi_root = ctx.asg_ref().root(S1);
|
||||
let oi_pkg = ctx
|
||||
.env_scope_lookup_raw::<Pkg>(oi_root, namefix)
|
||||
.map(|eoi| eoi.into_inner())
|
||||
.expect("failed to locate package by its recovery name");
|
||||
|
||||
assert_eq!(S6.merge(S8).unwrap(), oi_pkg.resolve(ctx.asg_ref()).span());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pkg_import_canonicalized_against_current_pkg() {
|
||||
let pkg_name = SPair("/foo/bar".into(), S2);
|
||||
let pkg_rel = SPair("baz/quux".into(), S3);
|
||||
|
||||
#[rustfmt::skip]
|
||||
let toks = vec![
|
||||
PkgStart(S1, pkg_name),
|
||||
PkgImport(pkg_rel),
|
||||
PkgEnd(S3),
|
||||
];
|
||||
|
||||
let mut sut = Sut::parse(toks.into_iter());
|
||||
assert!(sut.all(|x| x.is_ok()));
|
||||
|
||||
let asg = sut.finalize().unwrap().into_context().finish();
|
||||
|
||||
let import = asg
|
||||
.root(S1)
|
||||
.edges_filtered::<Pkg>(&asg)
|
||||
.next()
|
||||
.expect("cannot find package from root")
|
||||
.edges_filtered::<Pkg>(&asg)
|
||||
.next()
|
||||
.expect("cannot find imported package")
|
||||
.resolve(&asg);
|
||||
|
||||
// TODO
|
||||
assert_eq!(SPair("/foo/baz/quux".into(), 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);
|
||||
|
||||
#[rustfmt::skip]
|
||||
let toks = vec![
|
||||
DocText(doc_a),
|
||||
|
||||
// Some object to place in-between the two
|
||||
// documentation blocks.
|
||||
PkgImport(id_import),
|
||||
|
||||
DocText(doc_b),
|
||||
];
|
||||
|
||||
let asg = asg_from_pkg_body_toks(toks);
|
||||
|
||||
let oi_pkg = asg
|
||||
.root(S1)
|
||||
.edges_filtered::<Pkg>(&asg)
|
||||
.next()
|
||||
.expect("cannot find package from root");
|
||||
|
||||
assert_eq!(
|
||||
vec![S4, S3, S2], // (edges reversed by Petgraph)
|
||||
oi_pkg
|
||||
.edges(&asg)
|
||||
.map(|rel| rel.widen().resolve(&asg).span())
|
||||
.collect::<Vec<_>>(),
|
||||
);
|
||||
}
|
||||
|
||||
// Package imports will trigger parsing,
|
||||
// but the intent is to re-use the previous parsing context so that we can
|
||||
// continue to accumulate into the same graph along with the same scope
|
||||
// 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 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![
|
||||
// The first package will reference an identifier from another
|
||||
// package.
|
||||
PkgStart(S1, SPair("/pkg-a".into(), S1)),
|
||||
IdentDep(name_foo, name_bar),
|
||||
PkgEnd(S3),
|
||||
];
|
||||
|
||||
let ctx = air_ctx_from_toks(toks);
|
||||
|
||||
// We consumed the parser above and retrieved its context.
|
||||
// This is the token stream for the second parser,
|
||||
// which will re-use the above context.
|
||||
#[rustfmt::skip]
|
||||
let toks = vec![
|
||||
// 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)),
|
||||
IdentDecl(name_bar, kind.clone(), src.clone()),
|
||||
|
||||
// This is a third identifier that is unique to this package.
|
||||
// This is intended to catch the following situation,
|
||||
// where `P` is the ParseState and `S` is the stack.
|
||||
//
|
||||
// 1. P:Uninit S:[]
|
||||
// 2. P:Root S:[]
|
||||
// 3. P:Pkg S:[Root]
|
||||
// ---- next parser ---
|
||||
// 4. P:Uninit S:[Root]
|
||||
// 5. P:Root S:[Root] <-- new Root
|
||||
// 6. P:Pkg S:[Root, Root]
|
||||
// ^ ^
|
||||
// `-----\
|
||||
// Would try to index at oi_root
|
||||
// _twice_, which would panic.
|
||||
//
|
||||
// AirAggregate is designed to resume from the top of the stack
|
||||
// when initializing to avoid this scenario.
|
||||
// So here's what it's expected to do instead:
|
||||
//
|
||||
// [...]
|
||||
// ---- next parser ---
|
||||
// 4. P:Uninit S:[Root]
|
||||
// 5. P:Root S:[] <-- pop existing Root
|
||||
// 6. P:Pkg S:[Root]
|
||||
IdentDecl(name_baz, kind.clone(), src),
|
||||
PkgEnd(S7),
|
||||
];
|
||||
|
||||
// We _resume_ parsing with the previous context.
|
||||
let mut sut = Sut::parse_with_context(toks.into_iter(), ctx);
|
||||
assert!(sut.all(|x| x.is_ok()));
|
||||
|
||||
// The ASG should have been constructed from _both_ of the previous
|
||||
// individual parsers,
|
||||
// having used the shared context.
|
||||
let ctx = sut.finalize().unwrap().into_private_context();
|
||||
|
||||
// Both should have been added to the same graph.
|
||||
let oi_foo = root_lookup(&ctx, name_foo).expect("missing foo");
|
||||
let oi_bar = root_lookup(&ctx, name_bar).expect("missing bar");
|
||||
|
||||
assert!(oi_foo.has_edge_to(ctx.asg_ref(), oi_bar));
|
||||
|
||||
// And it should have been resolved via the _second_ package,
|
||||
// which is parsed separately,
|
||||
// as part of the same graph and with the same indexed identifiers.
|
||||
// If there were not a shared index between the two parsers,
|
||||
// then it would have retained an original `Missing` Ident and created
|
||||
// a new resolved one.
|
||||
assert_eq!(Some(&kind), oi_bar.resolve(ctx.asg_ref()).kind());
|
||||
}
|
||||
|
||||
/////// Tests above; plumbing begins below ///////
|
||||
|
||||
/// Parse using [`Sut`] when the test does not care about the outer package.
|
||||
pub fn parse_as_pkg_body<I: IntoIterator<Item = Air>>(
|
||||
toks: I,
|
||||
|
@ -313,17 +692,95 @@ where
|
|||
use std::iter;
|
||||
|
||||
Sut::parse(
|
||||
iter::once(Air::PkgOpen(S1))
|
||||
iter::once(PkgStart(S1, SPair("/pkg".into(), S1)))
|
||||
.chain(toks.into_iter())
|
||||
.chain(iter::once(Air::PkgClose(S1))),
|
||||
.chain(iter::once(PkgEnd(S1))),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn asg_from_toks<I: IntoIterator<Item = Air>>(toks: I) -> Asg
|
||||
pub(super) fn asg_from_pkg_body_toks<I: IntoIterator<Item = Air>>(
|
||||
toks: I,
|
||||
) -> Asg
|
||||
where
|
||||
I::IntoIter: Debug,
|
||||
{
|
||||
// Equivalent to `into_{private_=>}context` in this function.
|
||||
air_ctx_from_pkg_body_toks(toks).finish()
|
||||
}
|
||||
|
||||
pub(super) fn air_ctx_from_pkg_body_toks<I: IntoIterator<Item = Air>>(
|
||||
toks: I,
|
||||
) -> <Sut as ParseState>::Context
|
||||
where
|
||||
I::IntoIter: Debug,
|
||||
{
|
||||
let mut sut = parse_as_pkg_body(toks);
|
||||
assert!(sut.all(|x| x.is_ok()));
|
||||
sut.finalize().unwrap().into_context()
|
||||
sut.finalize().unwrap().into_private_context()
|
||||
}
|
||||
|
||||
/// Create and yield a new [`Asg`] from an [`Air`] token stream.
|
||||
pub fn asg_from_toks<I: IntoIterator<Item = Air>>(toks: I) -> Asg
|
||||
where
|
||||
I::IntoIter: Debug,
|
||||
{
|
||||
// Equivalent to `into_{private_=>}context` in this function.
|
||||
air_ctx_from_toks(toks).finish()
|
||||
}
|
||||
|
||||
/// Create and yield a new [`Asg`] from an [`Air`] token stream.
|
||||
pub(super) fn air_ctx_from_toks<I: IntoIterator<Item = Air>>(
|
||||
toks: I,
|
||||
) -> <Sut as ParseState>::Context
|
||||
where
|
||||
I::IntoIter: Debug,
|
||||
{
|
||||
let mut sut = Sut::parse(toks.into_iter());
|
||||
assert!(sut.all(|x| x.is_ok()));
|
||||
sut.finalize().unwrap().into_private_context()
|
||||
}
|
||||
|
||||
fn root_lookup(
|
||||
ctx: &<AirAggregate as ParseState>::Context,
|
||||
name: SPair,
|
||||
) -> Option<ObjectIndex<Ident>> {
|
||||
ctx.env_scope_lookup_raw(ctx.asg_ref().root(S1), name)
|
||||
.map(|eoi| eoi.into_inner())
|
||||
}
|
||||
|
||||
pub fn pkg_lookup(
|
||||
ctx: &<AirAggregate as ParseState>::Context,
|
||||
name: SPair,
|
||||
) -> Option<ObjectIndex<Ident>> {
|
||||
let oi_pkg = ctx
|
||||
.asg_ref()
|
||||
.root(S1)
|
||||
.edges_filtered::<Pkg>(ctx.asg_ref())
|
||||
.next()
|
||||
.expect("missing rooted package");
|
||||
|
||||
ctx.env_scope_lookup(oi_pkg, name)
|
||||
}
|
||||
|
||||
pub fn pkg_expect_ident_oi<O: ObjectRelatable + ObjectRelFrom<Ident>>(
|
||||
ctx: &<AirAggregate as ParseState>::Context,
|
||||
name: SPair,
|
||||
) -> ObjectIndex<O> {
|
||||
// Duplicates logic of `pkg_get_ident_oi`,
|
||||
// but in doing so,
|
||||
// provides better assertion messages.
|
||||
pkg_lookup(ctx, name)
|
||||
.expect(&format!("missing ident: `{name}`"))
|
||||
.edges(ctx.asg_ref())
|
||||
.next()
|
||||
.expect(&format!("missing definition for ident `{name}`"))
|
||||
.narrow()
|
||||
.expect(&format!("ident `{name}` was not of expected ObjectKind"))
|
||||
}
|
||||
|
||||
pub fn pkg_expect_ident_obj<O: ObjectRelatable + ObjectRelFrom<Ident>>(
|
||||
ctx: &<AirAggregate as ParseState>::Context,
|
||||
name: SPair,
|
||||
) -> &O {
|
||||
pkg_expect_ident_oi(ctx, name).resolve(ctx.asg_ref())
|
||||
}
|
||||
|
|
|
@ -0,0 +1,544 @@
|
|||
// Scope tests for ASG IR
|
||||
//
|
||||
// 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/>.
|
||||
|
||||
//! Scoping tests.
|
||||
//!
|
||||
//! These tests verify that identifiers are scoped to the proper
|
||||
//! environments.
|
||||
//! These may duplicate portions of other tests,
|
||||
//! but having concrete examples all in one place helps to develop, debug,
|
||||
//! and understand a system that can be quite confusing in the abstract.
|
||||
//!
|
||||
//! These tests _do not_ assert that identifiers are properly assigned to
|
||||
//! their corresponding definitions;
|
||||
//! those tests exist elsewhere.
|
||||
//!
|
||||
//! The core abstraction for these tests is [`assert_scope`],
|
||||
//! which allows for declarative assertions of identifier scope against
|
||||
//! the graph;
|
||||
//! it is key to creating tests that can be both easily created and
|
||||
//! easily understood.
|
||||
//!
|
||||
//! If You Are Here Due To A Test Failure
|
||||
//! =====================================
|
||||
//! If there are failing tests in parent or sibling modules,
|
||||
//! check those first;
|
||||
//! these tests are potentially fragile given that they test only a
|
||||
//! subset of behavior without first asserting against other system
|
||||
//! invariants,
|
||||
//! as described above.
|
||||
|
||||
use super::*;
|
||||
use crate::{
|
||||
asg::{
|
||||
graph::object::{self, ObjectTy},
|
||||
visit::{tree_reconstruction, TreeWalkRel},
|
||||
ExprOp,
|
||||
},
|
||||
span::UNKNOWN_SPAN,
|
||||
};
|
||||
use std::iter::once;
|
||||
|
||||
use EnvScopeKind::*;
|
||||
|
||||
const S0: Span = UNKNOWN_SPAN;
|
||||
|
||||
fn m(a: Span, b: Span) -> Span {
|
||||
a.merge(b).unwrap()
|
||||
}
|
||||
|
||||
/// Assert that the scope of the identifier named `name` is that of the
|
||||
/// provided environment list `expected`.
|
||||
///
|
||||
/// The inner value of `$kind` is the span of the identifier that is
|
||||
/// expected to have been indexed at that environment.
|
||||
/// This distinction comes into play for local identifiers that may have
|
||||
/// overlapping shadows.
|
||||
macro_rules! test_scopes {
|
||||
(
|
||||
setup { $($setup:tt)* }
|
||||
air $toks:block
|
||||
|
||||
#[test]
|
||||
$name:ident == [
|
||||
$( ($obj:ident, $span:expr, $kind:expr), )*
|
||||
];
|
||||
|
||||
$( $rest:tt )*
|
||||
) => {
|
||||
#[test]
|
||||
fn $name() {
|
||||
$($setup)*
|
||||
let ctx = air_ctx_from_toks($toks);
|
||||
|
||||
let given = derive_scopes_from_asg(&ctx, $name);
|
||||
let expected = [
|
||||
$( (ObjectTy::$obj, $span, $kind), )*
|
||||
];
|
||||
|
||||
// Collection allows us to see the entire expected and given
|
||||
// lists on assertion failure.
|
||||
// Asserting within the macro itself ensures that panics
|
||||
// will reference the test function rather than a utility
|
||||
// function.
|
||||
assert_eq!(
|
||||
given.collect::<Vec<_>>(),
|
||||
expected.into_iter().collect::<Vec<_>>(),
|
||||
"left: given, right: expected",
|
||||
);
|
||||
}
|
||||
|
||||
test_scopes! {
|
||||
setup { $($setup)* }
|
||||
air $toks
|
||||
|
||||
$($rest)*
|
||||
}
|
||||
};
|
||||
|
||||
(
|
||||
setup { $($setup:tt)* }
|
||||
air $toks:block
|
||||
) => {}
|
||||
}
|
||||
|
||||
test_scopes! {
|
||||
setup {
|
||||
let pkg_name = SPair("/pkg".into(), S1);
|
||||
let outer = SPair("outer".into(), S3);
|
||||
let inner = SPair("inner".into(), S5);
|
||||
}
|
||||
|
||||
air {
|
||||
[
|
||||
// ENV: 0 global lexical scoping boundaries (envs)
|
||||
PkgStart(S1, pkg_name), //- -.
|
||||
// ENV: 1 pkg // :
|
||||
ExprStart(ExprOp::Sum, S2), // :
|
||||
// ENV: 1 pkg // :
|
||||
BindIdent(outer), // v :v
|
||||
// :
|
||||
ExprStart(ExprOp::Sum, S4), // 1: 0
|
||||
// ENV: 1 pkg // :
|
||||
BindIdent(inner), // v :v
|
||||
ExprEnd(S6), // :
|
||||
ExprEnd(S7), // :
|
||||
PkgEnd(S8), //- -'
|
||||
]
|
||||
}
|
||||
|
||||
#[test]
|
||||
outer == [
|
||||
// The identifier is not local,
|
||||
// and so its scope should extend into the global environment.
|
||||
(Root, S0, Visible(S3)),
|
||||
|
||||
// Expr does not introduce a new environment,
|
||||
// and so the innermost environment in which we should be able to
|
||||
// find the identifier is the Pkg.
|
||||
(Pkg, m(S1, S8), Visible(S3)),
|
||||
];
|
||||
|
||||
#[test]
|
||||
inner == [
|
||||
// Same as above since the environment is the same;
|
||||
// `Expr` does not introduce a new environment.
|
||||
(Root, S0, Visible(S5)),
|
||||
(Pkg, m(S1, S8), Visible(S5)),
|
||||
];
|
||||
}
|
||||
|
||||
test_scopes! {
|
||||
setup {
|
||||
let pkg_name = SPair("/pkg".into(), 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_inner = SPair("_tpl-inner_".into(), S11);
|
||||
let meta_inner = SPair("@param_inner@".into(), S13);
|
||||
let expr_inner = SPair("exprInner".into(), S16);
|
||||
}
|
||||
|
||||
air {
|
||||
[
|
||||
// ENV: 0 global lexical scoping boundaries (envs)
|
||||
PkgStart(S1, pkg_name), //- - - - -.
|
||||
// ENV: 1 pkg // :
|
||||
TplStart(S2), //–-----. :
|
||||
// ENV: 2 tpl // | :
|
||||
BindIdent(tpl_outer), // |v :v
|
||||
// | :
|
||||
MetaStart(S4), // | :
|
||||
BindIdent(meta_outer), // vl|s :
|
||||
MetaEnd(S6), // | :
|
||||
// | :
|
||||
ExprStart(ExprOp::Sum, S7), // | :
|
||||
BindIdent(expr_outer), // vd|s :
|
||||
ExprEnd(S9), // | :
|
||||
// | :
|
||||
TplStart(S10), //---. | :
|
||||
// ENV: 3 tpl // | | :
|
||||
BindIdent(tpl_inner), // |v |s :
|
||||
// | | :
|
||||
MetaStart(S12), // | | :
|
||||
BindIdent(meta_inner), // vl|s |s :
|
||||
MetaEnd(S14), // | | :
|
||||
// 3| 2| 1: 0
|
||||
ExprStart(ExprOp::Sum, S15), // | | :
|
||||
BindIdent(expr_inner), // vd|s |s :
|
||||
ExprEnd(S17), // | | :
|
||||
TplEnd(S18), //---' | : v,s = EnvScopeKind
|
||||
TplEnd(S19), //–-----' : |
|
||||
PkgEnd(S20), //- - - - -' |`- l = local
|
||||
] // ^ `- d = defer
|
||||
// observe: - (l)ocal shadows until root
|
||||
// - (d)efer shadows until root
|
||||
// - visual >|> shadow
|
||||
// - visual >:> visual (pool)
|
||||
// - shadow >|> shadow
|
||||
// - shadow >:> (no visual/pool)
|
||||
}
|
||||
|
||||
#[test]
|
||||
tpl_outer == [
|
||||
// The template is defined at the package level,
|
||||
// and so is incorporated into the global environment.
|
||||
(Root, S0, Visible(S3)),
|
||||
|
||||
// Definition environment.
|
||||
(Pkg, m(S1, S20), Visible(S3)),
|
||||
];
|
||||
|
||||
#[test]
|
||||
meta_outer == [
|
||||
// The metavariable is local to the template,
|
||||
// and so is not scoped outside of it.
|
||||
// It does not contribute to the global scope,
|
||||
// however we must introduce shadow records so that we're able to
|
||||
// provide an error if shadowing would occur due to another
|
||||
// identifier of the same name,
|
||||
// such as a template within another template.
|
||||
// Root never contains shadow records since it is not part of a
|
||||
// hierarchy,
|
||||
// so it is omitted from the metavariable's scope.
|
||||
(Pkg, m(S1, S20), Shadow (S5)),
|
||||
(Tpl, m(S2, S19), Visible(S5)),
|
||||
];
|
||||
|
||||
#[test]
|
||||
expr_outer == [
|
||||
// Expressions defined within templates will eventually be scoped to
|
||||
// their _expansion site_.
|
||||
// Since the future scope cannot possibly be known until the point
|
||||
// of expansion,
|
||||
// we don't know what its parent environment will be.
|
||||
//
|
||||
// Why, then, does it shadow?
|
||||
//
|
||||
// Templates in TAMER
|
||||
// (unlike in the original XSLT-based TAME)
|
||||
// are designed to _close_ over their definition environment.
|
||||
// If a template references a value defined within the scope of its
|
||||
// definition
|
||||
// (e.g. an identifier imported into the package into which the
|
||||
// template itself was defined),
|
||||
// the intent is to be able to utilize that identifier at the
|
||||
// expansion site without having to break encapsulation by
|
||||
// having to know implementation details of the template;
|
||||
// this awkward problem is the reason for `import/@export`,
|
||||
// so that packages templates could re-export their symbols
|
||||
// to avoid this trap,
|
||||
// which is far too heavy-handed of an approach and is
|
||||
// easily forgotten.
|
||||
// In that sense,
|
||||
// templates act more like how one would expect functions to
|
||||
// operate.
|
||||
//
|
||||
// Because of that lexical capture,
|
||||
// it is important that identifiers shadow to ensure that we do
|
||||
// not rebind an identifier without the user realizing it.
|
||||
// The intent is that the system should just do the right thing
|
||||
// unless there happens to be a problem.
|
||||
// If a user references an identifier from the outer scope,
|
||||
// the intent is almost certainly to have it be lexically captured
|
||||
// and available at the expansion site.
|
||||
// If an identifier is unknown,
|
||||
// perhaps the intent is to have it defined by another template,
|
||||
// or to be defined at the expansion site.
|
||||
// And if the situation changes from the second to the first because
|
||||
// of the introduction of an import or a duplicate identifier,
|
||||
// we want to help the user at the earliest possible moment.
|
||||
(Pkg, m(S1, S20), Shadow (S8)),
|
||||
(Tpl, m(S2, S19), Visible(S8)),
|
||||
];
|
||||
|
||||
#[test]
|
||||
tpl_inner == [
|
||||
// This is similar to `expr_outer` above.
|
||||
// Even though the template is entirely scoped within the parent
|
||||
// `tpl_outer` such that it isn't even defined until it is expanded,
|
||||
// at which point it is defined within its expansion context,
|
||||
// we still want shadow records so that any _references_ to this
|
||||
// template can be resolved unambiguously in ways that are
|
||||
// helpful to the user
|
||||
// (see `expr_outer` above for more information).
|
||||
(Pkg, m(S1, S20), Shadow (S11)),
|
||||
(Tpl, m(S2, S19), Visible(S11)),
|
||||
];
|
||||
|
||||
#[test]
|
||||
meta_inner == [
|
||||
// Just as the previous metavariable,
|
||||
// we need to cast a shadow all the way up to the package level to
|
||||
// ensure that we do not permit identifier shadowing.
|
||||
// See `meta_outer` above for more information.
|
||||
(Pkg, m(S1, S20), Shadow (S13)),
|
||||
(Tpl, m(S2, S19), Shadow (S13)),
|
||||
(Tpl, m(S10, S18), Visible(S13)),
|
||||
];
|
||||
|
||||
#[test]
|
||||
expr_inner == [
|
||||
// Just the same as the previous expression.
|
||||
// Note the intended consequence of this:
|
||||
// if `tpl_outer` contains an identifier,
|
||||
// it cannot be shadowed by `tpl_inner`.
|
||||
(Pkg, m(S1, S20), Shadow (S16)),
|
||||
(Tpl, m(S2, S19), Shadow (S16)),
|
||||
(Tpl, m(S10, S18), Visible(S16)),
|
||||
];
|
||||
}
|
||||
|
||||
test_scopes! {
|
||||
setup {
|
||||
let pkg_name = SPair("/pkg".into(), S1);
|
||||
|
||||
let tpl_outer = SPair("_tpl-outer_".into(), S3);
|
||||
let tpl_inner = SPair("_tpl-inner_".into(), 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);
|
||||
|
||||
// This one will be used for asserting.
|
||||
let meta_same = SPair(meta_name, S11);
|
||||
}
|
||||
|
||||
air {
|
||||
// Note that,
|
||||
// unlike the above set of tests,
|
||||
// these templates are _siblings_.
|
||||
[
|
||||
// ENV: 0 global lexical scoping boundaries (envs)
|
||||
PkgStart(S1, pkg_name), //- - - -.
|
||||
// ENV: 1 pkg // :
|
||||
TplStart(S2), //----. :
|
||||
// ENV: 2 tpl // | :
|
||||
BindIdent(tpl_outer), // |~ :
|
||||
// | :
|
||||
MetaStart(S4), // | :
|
||||
BindIdent(meta_same_a), // vl|s : <--.
|
||||
MetaEnd(S6), // | : |
|
||||
TplEnd(S7), //----' : |
|
||||
// : |s
|
||||
TplStart(S8), //----. : |a
|
||||
// ENV: 3 tpl // | : |m
|
||||
BindIdent(tpl_inner), // |~ : |e
|
||||
// | : |
|
||||
MetaStart(S10), // | : |
|
||||
BindIdent(meta_same_b), // vl|s : <--'
|
||||
MetaEnd(S12), // | :
|
||||
TplEnd(S13), //----' : ~ = ignored for
|
||||
PkgEnd(S14), //- - - -' these tests
|
||||
]
|
||||
}
|
||||
|
||||
// Detailed information on metavariables is present in previous tests.
|
||||
// We focus here only on the fact that these definitions were permitted
|
||||
// to occur since identifiers of the same name have overlapping
|
||||
// shadows.
|
||||
|
||||
// Keep in mind that this test is a filtering of an ontological tree,
|
||||
// so this is ordered as such and does not contain duplicate objects.
|
||||
#[test]
|
||||
meta_same == [
|
||||
// A shadow is cast by both `meta_same_a` and `meta_same_b`.
|
||||
// When they intersect,
|
||||
// we must make a choice:
|
||||
//
|
||||
// (a) Index both of them; or
|
||||
// (b) Keep only one of them.
|
||||
//
|
||||
// At the time of writing,
|
||||
// the choice was (b).
|
||||
// But by keeping only one identifier indexed,
|
||||
// we do lose information and therefore will only be able to
|
||||
// present one diagnostic error in the event that we later
|
||||
// discover that the metavariables shadow another identifier
|
||||
// that is visible in scope.
|
||||
// This would present only one error at a time to the user,
|
||||
// depending on how they chose to resolve it,
|
||||
// and so is not ideal;
|
||||
// the solution may eventually move to (a) to retain this
|
||||
// important information.
|
||||
//
|
||||
// Since (b) was chosen,
|
||||
// which do we keep?
|
||||
// This choice is not important,
|
||||
// since in the event of a future error we'll still be providing
|
||||
// correct
|
||||
// (albeit incomplete)
|
||||
// information to the user,
|
||||
// but it is defined by implementation details,
|
||||
// and so is subject to change.
|
||||
// At the time of writing,
|
||||
// because of how indexing is carried out,
|
||||
// we retain the shadow of the _first_ encountered identifier.
|
||||
(Pkg, m(S1, S14), Shadow (S5 )),
|
||||
|
||||
// The first identifier of this name is found in the first
|
||||
// template.
|
||||
// Its shadow is discussed above.
|
||||
(Tpl, m(S2, S7), Visible(S5 )),
|
||||
|
||||
// And the second identifier in the second template,
|
||||
// with its shadow also discussed above.
|
||||
// As noted atop this test,
|
||||
// we do not have duplicate objects in this test data,
|
||||
// and so the `Pkg` object above is not duplicated despite two
|
||||
// shadows being cast.
|
||||
(Tpl, m(S8, S13), Visible(S11)),
|
||||
];
|
||||
}
|
||||
|
||||
// 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_b = SPair("/pkg/b".into(), S4);
|
||||
let opaque_b = SPair("opaque_b".into(), S5);
|
||||
}
|
||||
|
||||
air {
|
||||
[
|
||||
// ENV: 0 global lexical scoping boundaries (envs)
|
||||
PkgStart(S1, pkg_a), //- -.
|
||||
// ENV: 1 pkg // :
|
||||
IdentDecl( // v :v
|
||||
opaque_a, // :
|
||||
IdentKind::Meta, // :
|
||||
Default::default(), // :
|
||||
), // 1:
|
||||
PkgEnd(S3), //- -'
|
||||
// 0
|
||||
PkgStart(S4, pkg_b), //- -.
|
||||
// ENV: 1 pkg // 1:
|
||||
IdentDecl( // v :v
|
||||
opaque_b, // :
|
||||
IdentKind::Meta, // :
|
||||
Default::default(), // :
|
||||
), // :
|
||||
PkgEnd(S6), //- -'
|
||||
]
|
||||
}
|
||||
|
||||
#[test]
|
||||
opaque_a == [
|
||||
(Root, S0, Visible(S2)),
|
||||
(Pkg, m(S1, S3), Visible(S2)),
|
||||
];
|
||||
|
||||
#[test]
|
||||
opaque_b == [
|
||||
(Root, S0, Visible(S5)),
|
||||
(Pkg, m(S4, S6), Visible(S5)),
|
||||
];
|
||||
}
|
||||
|
||||
///// Tests end above this line, plumbing below /////
|
||||
|
||||
/// Independently derive identifier scopes from the graph.
|
||||
///
|
||||
/// This will search the graph for all environments in which `name` has been
|
||||
/// indexed,
|
||||
/// gather information about those environments,
|
||||
/// and compare them against `expected`.
|
||||
/// The environment listing is expected to be in ontological order,
|
||||
/// as by [`tree_reconstruction`].
|
||||
///
|
||||
/// This function is essential to providing easily understood,
|
||||
/// declarative scope test definitions,
|
||||
/// which make it easy to form and prove hypotheses about the behavior of
|
||||
/// TAMER's scoping system.
|
||||
fn derive_scopes_from_asg<'a>(
|
||||
ctx: &'a <AirAggregate as ParseState>::Context,
|
||||
name: SPair,
|
||||
) -> impl Iterator<Item = (ObjectTy, Span, EnvScopeKind<Span>)> + 'a {
|
||||
// We use what was most convenient at the time of writing to gather
|
||||
// environments representing the scope of `name`.
|
||||
// This is not the most efficient,
|
||||
// but our test graphs are quite small,
|
||||
// and so that won't matter.
|
||||
//
|
||||
// The reason that this works is because the traversal will visit
|
||||
// objects following the graph's ontology,
|
||||
// which will produce a tree in the expected order.
|
||||
// We filter on index lookup,
|
||||
// which discards the portions of the tree
|
||||
// (the graph)
|
||||
// that we are not interested in.
|
||||
//
|
||||
// This also means that a failure to extend the scope of `name` to a
|
||||
// particular environment will cause it to be omitted from this
|
||||
// iterator,
|
||||
// but that is okay;
|
||||
// it'll result in a test failure that should be easy enough to
|
||||
// understand.
|
||||
let given_without_root = tree_reconstruction(ctx.asg_ref()).filter_map(
|
||||
move |TreeWalkRel(dynrel, _)| {
|
||||
dynrel.target_oi_rel_to_dyn::<object::Ident>().map(|oi_to| {
|
||||
(
|
||||
dynrel.target_ty(),
|
||||
dynrel.target().resolve(ctx.asg_ref()).span(),
|
||||
ctx.env_scope_lookup_raw(oi_to, name),
|
||||
)
|
||||
})
|
||||
},
|
||||
);
|
||||
|
||||
// `tree_reconstruction` omits root,
|
||||
// so we'll have to add it ourselves.
|
||||
let oi_root = ctx.asg_ref().root(name);
|
||||
|
||||
once((ObjectTy::Root, S0, ctx.env_scope_lookup_raw(oi_root, name)))
|
||||
.chain(given_without_root)
|
||||
.filter_map(|(ty, span, oeoi)| {
|
||||
oeoi.map(|eoi| {
|
||||
(ty, span, eoi.map(ObjectIndex::cresolve(ctx.asg_ref())))
|
||||
})
|
||||
})
|
||||
// We discard the inner ObjectIndex since it is not relevant for the
|
||||
// test assertion.
|
||||
.map(|(ty, span, eid)| (ty, span, eid.map(|id| id.span())))
|
||||
}
|
|
@ -22,16 +22,14 @@
|
|||
//! See the [parent module](super) for more information.
|
||||
|
||||
use super::{
|
||||
super::{
|
||||
graph::object::{Pkg, Tpl},
|
||||
Asg, AsgError, ObjectIndex,
|
||||
},
|
||||
expr::AirExprAggregateStoreDangling,
|
||||
Air, AirExprAggregate,
|
||||
super::{graph::object::Tpl, Asg, AsgError, ObjectIndex},
|
||||
ir::AirBindableTpl,
|
||||
AirAggregate, AirAggregateCtx,
|
||||
};
|
||||
use crate::{
|
||||
fmt::{DisplayWrapper, TtQuote},
|
||||
parse::prelude::*,
|
||||
span::Span,
|
||||
};
|
||||
|
||||
/// Template parser and token aggregator.
|
||||
|
@ -40,13 +38,12 @@ use crate::{
|
|||
///
|
||||
/// - Metadata about the template,
|
||||
/// including its parameters; and
|
||||
/// - A collection of [`Air`] tokens representing the body of the
|
||||
/// template that will be expanded into the application site when the
|
||||
/// template is applied.
|
||||
/// - A collection of objects representing the body of the template that
|
||||
/// will be expanded into the application site when the template is
|
||||
/// applied.
|
||||
///
|
||||
/// This contains an embedded [`AirExprAggregate`] parser for handling
|
||||
/// expressions just the same as [`super::AirAggregate`] does with
|
||||
/// packages.
|
||||
/// The superstate is expected to preempt this parser for expression
|
||||
/// parsing.
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum AirTplAggregate {
|
||||
/// Ready for a template,
|
||||
|
@ -57,177 +54,213 @@ pub enum AirTplAggregate {
|
|||
/// AIR has no restrictions on when template header tokens are
|
||||
/// provided,
|
||||
/// which simplifies AIR generation.
|
||||
Ready(ObjectIndex<Pkg>),
|
||||
Ready,
|
||||
|
||||
Toplevel(
|
||||
ObjectIndex<Pkg>,
|
||||
ObjectIndex<Tpl>,
|
||||
AirExprAggregateStoreDangling<Tpl>,
|
||||
Option<SPair>,
|
||||
),
|
||||
/// Toplevel template context.
|
||||
///
|
||||
/// Conceptually,
|
||||
/// tokens that are received in this state are interpreted as directly
|
||||
/// applying to the template itself,
|
||||
/// or creating an object directly owned by the template.
|
||||
Toplevel(TplState),
|
||||
|
||||
/// Aggregating tokens into a template.
|
||||
TplExpr(
|
||||
ObjectIndex<Pkg>,
|
||||
ObjectIndex<Tpl>,
|
||||
AirExprAggregateStoreDangling<Tpl>,
|
||||
Option<SPair>,
|
||||
),
|
||||
/// A template has been completed.
|
||||
///
|
||||
/// This is used to determine whether the next token of input ought to
|
||||
/// result in a dead state transition or a transition back to
|
||||
/// [`Self::Ready`].
|
||||
Done,
|
||||
}
|
||||
|
||||
impl Display for AirTplAggregate {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Ready(_) => write!(f, "ready for template definition"),
|
||||
Self::Ready => write!(f, "ready for template definition"),
|
||||
Self::Toplevel(tpl) => write!(f, "building {tpl} at toplevel"),
|
||||
Self::Done => write!(f, "completed building template"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Self::Toplevel(_, expr, _, None)
|
||||
| Self::TplExpr(_, expr, _, None) => {
|
||||
write!(f, "building anonymous template with {expr}")
|
||||
/// The current reachability status of the template.
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
|
||||
pub enum TplState {
|
||||
/// Template is dangling and cannot be referenced by anything else.
|
||||
Dangling(ObjectIndex<Tpl>),
|
||||
|
||||
/// Template is anonymous and is not reachable by an identifier,
|
||||
/// but is reachable in the current context.
|
||||
AnonymousReachable(ObjectIndex<Tpl>),
|
||||
|
||||
/// Template is reachable via an identifier.
|
||||
///
|
||||
/// This uses an [`SPair`] as evidence for that assertion rather than an
|
||||
/// [`ObjectIndex`] so that it provides useful output via [`Display`]
|
||||
/// in parser traces.
|
||||
Identified(ObjectIndex<Tpl>, SPair),
|
||||
}
|
||||
|
||||
impl TplState {
|
||||
fn oi(&self) -> ObjectIndex<Tpl> {
|
||||
match self {
|
||||
TplState::Dangling(oi)
|
||||
| TplState::AnonymousReachable(oi)
|
||||
| TplState::Identified(oi, _) => *oi,
|
||||
}
|
||||
}
|
||||
|
||||
fn identify(self, id: SPair) -> Self {
|
||||
Self::Identified(self.oi(), id)
|
||||
}
|
||||
|
||||
fn anonymous_reachable(self) -> Self {
|
||||
Self::AnonymousReachable(self.oi())
|
||||
}
|
||||
|
||||
/// Attempt to complete a template definition.
|
||||
///
|
||||
/// If `self` is [`Self::Dangling`],
|
||||
/// then an [`AsgError::DanglingTpl`] will be returned.
|
||||
///
|
||||
/// This updates the span of the template to encompass the entire
|
||||
/// definition,
|
||||
/// even if an error occurs.
|
||||
fn close(self, asg: &mut Asg, close_span: Span) -> Result<(), AsgError> {
|
||||
let oi = self.oi().close(asg, close_span);
|
||||
|
||||
match self {
|
||||
Self::Dangling(_) => {
|
||||
Err(AsgError::DanglingTpl(oi.resolve(asg).span()))
|
||||
}
|
||||
Self::AnonymousReachable(..) | Self::Identified(..) => Ok(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Self::Toplevel(_, expr, _, Some(name))
|
||||
| Self::TplExpr(_, expr, _, Some(name)) => {
|
||||
write!(
|
||||
f,
|
||||
"building named template {} with {expr}",
|
||||
TtQuote::wrap(name)
|
||||
)
|
||||
impl Display for TplState {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
match self {
|
||||
TplState::Dangling(_) => write!(f, "anonymous dangling template"),
|
||||
TplState::AnonymousReachable(_) => {
|
||||
write!(f, "anonymous reachable template")
|
||||
}
|
||||
TplState::Identified(_, id) => {
|
||||
write!(f, "identified template {}", TtQuote::wrap(id))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ParseState for AirTplAggregate {
|
||||
type Token = Air;
|
||||
type Token = AirBindableTpl;
|
||||
type Object = ();
|
||||
type Error = AsgError;
|
||||
type Context = Asg;
|
||||
type Context = AirAggregateCtx;
|
||||
type Super = AirAggregate;
|
||||
|
||||
fn parse_token(
|
||||
self,
|
||||
tok: Self::Token,
|
||||
asg: &mut Self::Context,
|
||||
ctx: &mut Self::Context,
|
||||
) -> TransitionResult<Self::Super> {
|
||||
use super::ir::{AirBind::*, AirSubsets::*, AirTodo::*, AirTpl::*};
|
||||
use super::ir::{AirBind::*, AirDoc::*, AirTpl::*};
|
||||
use AirBindableTpl::*;
|
||||
use AirTplAggregate::*;
|
||||
|
||||
match (self, tok.into()) {
|
||||
(st, AirTodo(Todo(_))) => Transition(st).incomplete(),
|
||||
match (self, tok) {
|
||||
(Ready | Done, AirTpl(TplStart(span))) => {
|
||||
let oi_tpl = ctx.asg_mut().create(Tpl::new(span));
|
||||
|
||||
(Ready(oi_pkg), AirTpl(TplOpen(span))) => {
|
||||
let oi_tpl = asg.create(Tpl::new(span));
|
||||
|
||||
Transition(Toplevel(
|
||||
oi_pkg,
|
||||
oi_tpl,
|
||||
AirExprAggregate::new_in(oi_tpl),
|
||||
None,
|
||||
))
|
||||
.incomplete()
|
||||
Transition(Toplevel(TplState::Dangling(oi_tpl))).incomplete()
|
||||
}
|
||||
|
||||
(Toplevel(..), AirTpl(TplOpen(_span))) => todo!("nested tpl open"),
|
||||
|
||||
(Toplevel(oi_pkg, oi_tpl, expr, _), AirBind(BindIdent(name))) => {
|
||||
asg.lookup_or_missing(name)
|
||||
.bind_definition(asg, name, oi_tpl)
|
||||
.map(|oi_ident| oi_pkg.defines(asg, oi_ident))
|
||||
.map(|_| ())
|
||||
.transition(Toplevel(oi_pkg, oi_tpl, expr, Some(name)))
|
||||
(st @ Toplevel(..), tok @ AirTpl(TplStart(_))) => {
|
||||
ctx.ret_or_transfer(st, tok, AirTplAggregate::new())
|
||||
}
|
||||
|
||||
(Toplevel(..), AirBind(RefIdent(_))) => {
|
||||
todo!("tpl Toplevel RefIdent")
|
||||
(Toplevel(tpl), AirBind(BindIdent(id))) => ctx
|
||||
.defines(id)
|
||||
.and_then(|oi_ident| {
|
||||
oi_ident.bind_definition(ctx.asg_mut(), id, tpl.oi())
|
||||
})
|
||||
.map(|_| ())
|
||||
.transition(Toplevel(tpl.identify(id))),
|
||||
|
||||
(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()
|
||||
}
|
||||
|
||||
(
|
||||
Toplevel(oi_pkg, oi_tpl, _expr_done, _),
|
||||
AirTpl(TplClose(span)),
|
||||
) => {
|
||||
oi_tpl.close(asg, span);
|
||||
Transition(Ready(oi_pkg)).incomplete()
|
||||
(Toplevel(tpl), AirDoc(DocIndepClause(clause))) => {
|
||||
tpl.oi().desc_short(ctx.asg_mut(), clause);
|
||||
Transition(Toplevel(tpl)).incomplete()
|
||||
}
|
||||
|
||||
(TplExpr(oi_pkg, oi_tpl, expr, name), AirTpl(TplClose(span))) => {
|
||||
// TODO: duplicated with AirAggregate
|
||||
match expr.is_accepting(asg) {
|
||||
true => {
|
||||
// TODO: this is duplicated with the above
|
||||
oi_tpl.close(asg, span);
|
||||
Transition(Ready(oi_pkg)).incomplete()
|
||||
(Toplevel(tpl), AirDoc(DocText(text))) => {
|
||||
tpl.oi().append_doc_text(ctx.asg_mut(), text);
|
||||
Transition(Toplevel(tpl)).incomplete()
|
||||
}
|
||||
|
||||
(Toplevel(tpl), AirTpl(TplEnd(span))) => {
|
||||
tpl.close(ctx.asg_mut(), span).transition(Done)
|
||||
}
|
||||
|
||||
(Toplevel(tpl), AirTpl(TplEndRef(span))) => {
|
||||
// Note that we utilize lookahead in either case,
|
||||
// but in the case of an error,
|
||||
// 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()
|
||||
}
|
||||
false => Transition(TplExpr(oi_pkg, oi_tpl, expr, name))
|
||||
.err(AsgError::InvalidTplCloseContext(span)),
|
||||
None => Transition(Toplevel(tpl))
|
||||
.err(AsgError::InvalidExpansionContext(span)),
|
||||
}
|
||||
.with_lookahead(AirTpl(TplEnd(span)))
|
||||
}
|
||||
|
||||
(Toplevel(..) | TplExpr(..), AirPkg(_)) => {
|
||||
todo!("template cannot define packages")
|
||||
// If we just finished a template then this end may represent
|
||||
// the closing of a parent template.
|
||||
(Done, tok @ AirTpl(TplEnd(_) | TplEndRef(_))) => {
|
||||
Transition(Done).dead(tok)
|
||||
}
|
||||
// Otherwise we have an unbalanced close
|
||||
// (we just started parsing to receive an end token).
|
||||
(Ready, AirTpl(TplEnd(span) | TplEndRef(span))) => {
|
||||
Transition(Ready).err(AsgError::UnbalancedTpl(span))
|
||||
}
|
||||
|
||||
(Toplevel(..) | TplExpr(..), AirIdent(_)) => {
|
||||
todo!("linker token cannot be used in templates")
|
||||
(st @ (Ready | Done), tok @ (AirBind(..) | AirDoc(..))) => {
|
||||
Transition(st).dead(tok)
|
||||
}
|
||||
|
||||
(
|
||||
Toplevel(oi_pkg, oi_tpl, expr, name)
|
||||
| TplExpr(oi_pkg, oi_tpl, expr, name),
|
||||
AirExpr(etok),
|
||||
) => Self::delegate_expr(asg, oi_pkg, oi_tpl, expr, name, etok),
|
||||
|
||||
(TplExpr(oi_pkg, oi_tpl, expr, name), AirBind(etok)) => {
|
||||
Self::delegate_expr(asg, oi_pkg, oi_tpl, expr, name, etok)
|
||||
}
|
||||
|
||||
(TplExpr(..), AirTpl(TplOpen(_))) => {
|
||||
todo!("nested template (template-generated template)")
|
||||
}
|
||||
|
||||
(st @ Ready(..), AirTpl(TplClose(span))) => {
|
||||
Transition(st).err(AsgError::UnbalancedTpl(span))
|
||||
}
|
||||
|
||||
(
|
||||
st @ Ready(..),
|
||||
tok @ (AirPkg(..) | AirExpr(..) | AirBind(..) | AirIdent(..)),
|
||||
) => Transition(st).dead(tok.into()),
|
||||
}
|
||||
}
|
||||
|
||||
fn is_accepting(&self, _: &Self::Context) -> bool {
|
||||
matches!(self, Self::Ready(..))
|
||||
matches!(self, Self::Ready | Self::Done)
|
||||
}
|
||||
}
|
||||
|
||||
impl AirTplAggregate {
|
||||
pub(super) fn new_in_pkg(oi_pkg: ObjectIndex<Pkg>) -> Self {
|
||||
Self::Ready(oi_pkg)
|
||||
pub(super) fn new() -> Self {
|
||||
Self::Ready
|
||||
}
|
||||
|
||||
/// Delegate to the expression parser [`AirExprAggregate`].
|
||||
// TODO: Sir, this argument count is out of control.
|
||||
fn delegate_expr(
|
||||
asg: &mut <Self as ParseState>::Context,
|
||||
oi_pkg: ObjectIndex<Pkg>,
|
||||
oi_tpl: ObjectIndex<Tpl>,
|
||||
expr: AirExprAggregateStoreDangling<Tpl>,
|
||||
name: Option<SPair>,
|
||||
etok: impl Into<<AirExprAggregateStoreDangling<Tpl> as ParseState>::Token>,
|
||||
) -> TransitionResult<Self> {
|
||||
let tok = etok.into();
|
||||
pub(super) fn active_tpl_oi(&self) -> Option<ObjectIndex<Tpl>> {
|
||||
use AirTplAggregate::*;
|
||||
|
||||
expr.parse_token(tok, asg).branch_dead::<Self, _>(
|
||||
|expr, ()| {
|
||||
Transition(Self::Toplevel(oi_pkg, oi_tpl, expr, name))
|
||||
.incomplete()
|
||||
},
|
||||
|expr, result, ()| {
|
||||
result
|
||||
.map(ParseStatus::reflexivity)
|
||||
.transition(Self::TplExpr(oi_pkg, oi_tpl, expr, name))
|
||||
},
|
||||
(),
|
||||
)
|
||||
match self {
|
||||
Ready | Done => None,
|
||||
Toplevel(tplst) => Some(tplst.oi()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -21,38 +21,40 @@ use super::*;
|
|||
use crate::asg::{
|
||||
air::{
|
||||
expr::test::collect_subexprs,
|
||||
test::{asg_from_toks, parse_as_pkg_body},
|
||||
Air, AirAggregate,
|
||||
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,
|
||||
},
|
||||
graph::object::{Doc, Meta, ObjectRel},
|
||||
Expr, ExprOp, Ident,
|
||||
};
|
||||
use crate::span::dummy::*;
|
||||
|
||||
type Sut = AirAggregate;
|
||||
|
||||
// A template is defined by the package containing it,
|
||||
// like an expression.
|
||||
#[test]
|
||||
fn tpl_defining_pkg() {
|
||||
let id_tpl = SPair("_tpl_".into(), S3);
|
||||
|
||||
#[rustfmt::skip]
|
||||
let toks = vec![
|
||||
Air::PkgOpen(S1),
|
||||
// This also tests tpl as a transition away from the package header.
|
||||
Air::TplOpen(S2),
|
||||
Air::BindIdent(id_tpl),
|
||||
Air::TplClose(S4),
|
||||
Air::PkgClose(S5),
|
||||
Air::PkgStart(S1, SPair("/pkg".into(), S1)),
|
||||
// This also tests tpl as a transition away from the package header.
|
||||
Air::TplStart(S2),
|
||||
Air::BindIdent(id_tpl),
|
||||
Air::TplEnd(S4),
|
||||
Air::PkgEnd(S5),
|
||||
];
|
||||
|
||||
let mut sut = Sut::parse(toks.into_iter());
|
||||
assert!(sut.all(|x| x.is_ok()));
|
||||
let asg = sut.finalize().unwrap().into_context();
|
||||
let ctx = air_ctx_from_toks(toks);
|
||||
let asg = ctx.asg_ref();
|
||||
|
||||
let tpl = asg.expect_ident_obj::<Tpl>(id_tpl);
|
||||
let tpl = pkg_expect_ident_obj::<Tpl>(&ctx, id_tpl);
|
||||
assert_eq!(S2.merge(S4).unwrap(), tpl.span());
|
||||
|
||||
let oi_id_tpl = asg.lookup(id_tpl).unwrap();
|
||||
let oi_id_tpl = pkg_lookup(&ctx, id_tpl).unwrap();
|
||||
assert_eq!(
|
||||
S1.merge(S5),
|
||||
oi_id_tpl.src_pkg(&asg).map(|pkg| pkg.resolve(&asg).span()),
|
||||
|
@ -66,25 +68,23 @@ fn tpl_after_expr() {
|
|||
|
||||
#[rustfmt::skip]
|
||||
let toks = vec![
|
||||
Air::PkgOpen(S1),
|
||||
Air::PkgStart(S1, SPair("/pkg".into(), S1)),
|
||||
// This expression is incidental to this test;
|
||||
// it need only parse.
|
||||
Air::ExprOpen(ExprOp::Sum, S2),
|
||||
Air::ExprStart(ExprOp::Sum, S2),
|
||||
Air::BindIdent(id_expr),
|
||||
Air::ExprClose(S4),
|
||||
Air::ExprEnd(S4),
|
||||
|
||||
// Open after an expression.
|
||||
Air::TplOpen(S5),
|
||||
Air::TplStart(S5),
|
||||
Air::BindIdent(id_tpl),
|
||||
Air::TplClose(S7),
|
||||
Air::PkgClose(S8),
|
||||
Air::TplEnd(S7),
|
||||
Air::PkgEnd(S8),
|
||||
];
|
||||
|
||||
let mut sut = Sut::parse(toks.into_iter());
|
||||
assert!(sut.all(|x| x.is_ok()));
|
||||
let asg = sut.finalize().unwrap().into_context();
|
||||
let ctx = air_ctx_from_toks(toks);
|
||||
|
||||
let tpl = asg.expect_ident_obj::<Tpl>(id_tpl);
|
||||
let tpl = pkg_expect_ident_obj::<Tpl>(&ctx, id_tpl);
|
||||
assert_eq!(S5.merge(S7).unwrap(), tpl.span());
|
||||
}
|
||||
|
||||
|
@ -105,41 +105,40 @@ fn tpl_within_expr() {
|
|||
|
||||
#[rustfmt::skip]
|
||||
let toks = vec![
|
||||
Air::PkgOpen(S1),
|
||||
Air::ExprOpen(ExprOp::Sum, S2),
|
||||
Air::PkgStart(S1, SPair("/pkg".into(), S1)),
|
||||
Air::ExprStart(ExprOp::Sum, S2),
|
||||
Air::BindIdent(id_expr),
|
||||
|
||||
// Child expression before the template to ensure that the
|
||||
// context is properly restored after template parsing.
|
||||
Air::ExprOpen(ExprOp::Sum, S4),
|
||||
Air::ExprClose(S5),
|
||||
Air::ExprStart(ExprOp::Sum, S4),
|
||||
Air::ExprEnd(S5),
|
||||
|
||||
// Template _within_ an expression.
|
||||
// This will not be present in the final expression,
|
||||
// as if it were hoisted out.
|
||||
Air::TplOpen(S6),
|
||||
Air::TplStart(S6),
|
||||
Air::BindIdent(id_tpl),
|
||||
Air::TplClose(S8),
|
||||
Air::TplEnd(S8),
|
||||
|
||||
// Child expression _after_ the template for the same reason.
|
||||
Air::ExprOpen(ExprOp::Sum, S9),
|
||||
Air::ExprClose(S10),
|
||||
Air::ExprClose(S11),
|
||||
Air::PkgClose(S12),
|
||||
Air::ExprStart(ExprOp::Sum, S9),
|
||||
Air::ExprEnd(S10),
|
||||
Air::ExprEnd(S11),
|
||||
Air::PkgEnd(S12),
|
||||
];
|
||||
|
||||
let mut sut = Sut::parse(toks.into_iter());
|
||||
assert!(sut.all(|x| x.is_ok()));
|
||||
let asg = sut.finalize().unwrap().into_context();
|
||||
let ctx = air_ctx_from_toks(toks);
|
||||
let asg = ctx.asg_ref();
|
||||
|
||||
// The inner template.
|
||||
let tpl = asg.expect_ident_obj::<Tpl>(id_tpl);
|
||||
let tpl = pkg_expect_ident_obj::<Tpl>(&ctx, id_tpl);
|
||||
assert_eq!(S6.merge(S8).unwrap(), tpl.span());
|
||||
|
||||
// The expression that was produced on the graph ought to be equivalent
|
||||
// to the expression without the template being present at all
|
||||
// (noting that the spans are of course not adjusted).
|
||||
let oi_expr = asg.expect_ident_oi::<Expr>(id_expr);
|
||||
let oi_expr = pkg_expect_ident_oi::<Expr>(&ctx, id_expr);
|
||||
let expr = oi_expr.resolve(&asg);
|
||||
assert_eq!(S2.merge(S11).unwrap(), expr.span());
|
||||
assert_eq!(
|
||||
|
@ -156,23 +155,80 @@ fn tpl_within_expr() {
|
|||
);
|
||||
}
|
||||
|
||||
// Like the above test,
|
||||
// but now we're _applying_ a template.
|
||||
#[test]
|
||||
fn tpl_apply_within_expr() {
|
||||
let id_expr = SPair("expr".into(), S3);
|
||||
let id_tpl = SPair("_tpl_".into(), S5);
|
||||
let ref_tpl = SPair("_tpl_".into(), S8);
|
||||
|
||||
#[rustfmt::skip]
|
||||
let toks = vec![
|
||||
Air::ExprStart(ExprOp::Sum, S2),
|
||||
Air::BindIdent(id_expr),
|
||||
|
||||
// This will not be present in the final expression,
|
||||
// as if it were hoisted out.
|
||||
Air::TplStart(S4),
|
||||
Air::BindIdent(id_tpl),
|
||||
Air::TplEnd(S6),
|
||||
|
||||
// But the application will remain.
|
||||
Air::TplStart(S7),
|
||||
Air::RefIdent(ref_tpl),
|
||||
Air::TplEndRef(S9),
|
||||
Air::ExprEnd(S10),
|
||||
];
|
||||
|
||||
let ctx = air_ctx_from_pkg_body_toks(toks);
|
||||
let asg = ctx.asg_ref();
|
||||
|
||||
// The inner template.
|
||||
let tpl = pkg_expect_ident_obj::<Tpl>(&ctx, id_tpl);
|
||||
assert_eq!(S4.merge(S6).unwrap(), tpl.span());
|
||||
|
||||
// The expression that was produced on the graph ought to be equivalent
|
||||
// to the expression without the template being present at all,
|
||||
// but retaining the _application_.
|
||||
let oi_expr = pkg_expect_ident_oi::<Expr>(&ctx, id_expr);
|
||||
let expr = oi_expr.resolve(&asg);
|
||||
assert_eq!(S2.merge(S10).unwrap(), expr.span());
|
||||
assert_eq!(
|
||||
#[rustfmt::skip]
|
||||
vec![
|
||||
S7.merge(S9).unwrap(),
|
||||
],
|
||||
oi_expr
|
||||
.edges(&asg)
|
||||
.map(|rel| rel.widen().resolve(&asg).span())
|
||||
.collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn close_tpl_without_open() {
|
||||
let id_tpl = SPair("_tpl_".into(), S3);
|
||||
|
||||
#[rustfmt::skip]
|
||||
let toks = vec![
|
||||
Air::TplClose(S1),
|
||||
Air::TplEnd(S1),
|
||||
// RECOVERY: Try again.
|
||||
Air::TplOpen(S2),
|
||||
Air::TplClose(S3),
|
||||
Air::TplStart(S2),
|
||||
Air::BindIdent(id_tpl),
|
||||
Air::TplEnd(S4),
|
||||
];
|
||||
|
||||
assert_eq!(
|
||||
#[rustfmt::skip]
|
||||
vec![
|
||||
Ok(Parsed::Incomplete), // PkgOpen
|
||||
Err(ParseError::StateError(AsgError::UnbalancedTpl(S1))),
|
||||
// RECOVERY
|
||||
Ok(Parsed::Incomplete), // TplOpen
|
||||
Ok(Parsed::Incomplete), // TplClose
|
||||
Ok(Parsed::Incomplete), // PkgClose
|
||||
Ok(Parsed::Incomplete), // PkgStart
|
||||
Err(ParseError::StateError(AsgError::UnbalancedTpl(S1))),
|
||||
// RECOVERY
|
||||
Ok(Parsed::Incomplete), // TplStart
|
||||
Ok(Parsed::Incomplete), // BindIdent
|
||||
Ok(Parsed::Incomplete), // TplEnd
|
||||
Ok(Parsed::Incomplete), // PkgEnd
|
||||
],
|
||||
parse_as_pkg_body(toks).collect::<Vec<_>>(),
|
||||
);
|
||||
|
@ -186,47 +242,66 @@ fn tpl_with_reachable_expression() {
|
|||
|
||||
#[rustfmt::skip]
|
||||
let toks = vec![
|
||||
Air::TplOpen(S1),
|
||||
Air::TplStart(S1),
|
||||
Air::BindIdent(id_tpl),
|
||||
|
||||
Air::ExprOpen(ExprOp::Sum, S3),
|
||||
Air::ExprStart(ExprOp::Sum, S3),
|
||||
// Must not be cached in the global env.
|
||||
Air::BindIdent(id_expr_a),
|
||||
Air::ExprClose(S5),
|
||||
Air::ExprEnd(S5),
|
||||
|
||||
Air::ExprOpen(ExprOp::Sum, S6),
|
||||
Air::ExprStart(ExprOp::Sum, S6),
|
||||
// Must not be cached in the global env.
|
||||
Air::BindIdent(id_expr_b),
|
||||
Air::ExprClose(S8),
|
||||
Air::TplClose(S9),
|
||||
Air::ExprEnd(S8),
|
||||
Air::TplEnd(S9),
|
||||
];
|
||||
|
||||
let asg = asg_from_toks(toks);
|
||||
let ctx = air_ctx_from_pkg_body_toks(toks);
|
||||
let asg = ctx.asg_ref();
|
||||
|
||||
let oi_tpl = asg.expect_ident_oi::<Tpl>(id_tpl);
|
||||
let oi_tpl = pkg_expect_ident_oi::<Tpl>(&ctx, id_tpl);
|
||||
let tpl = oi_tpl.resolve(&asg);
|
||||
assert_eq!(S1.merge(S9).unwrap(), tpl.span());
|
||||
|
||||
// The inner expressions are reachable,
|
||||
// but the intent is to expand them into the template's eventual
|
||||
// application site.
|
||||
// They have identifiers,
|
||||
// but those identifiers _must not_ be cached in the global
|
||||
// environment;
|
||||
// such a determination will be made at expansion-time.
|
||||
// Given that,
|
||||
// they should be defined by the template...
|
||||
assert_eq!(
|
||||
vec![
|
||||
asg.lookup(id_expr_b).unwrap(),
|
||||
asg.lookup(id_expr_a).unwrap(),
|
||||
// At the time of writing,
|
||||
// this is implemented using the same `edges_filtered`,
|
||||
// but the point is that we want to ensure that the
|
||||
// identifiers bound to this template are only these.
|
||||
oi_tpl.lookup_local_linear(&asg, id_expr_b),
|
||||
oi_tpl.lookup_local_linear(&asg, id_expr_a),
|
||||
],
|
||||
oi_tpl.edges_filtered::<Ident>(&asg).collect::<Vec<_>>()
|
||||
oi_tpl
|
||||
.edges_filtered::<Ident>(&asg)
|
||||
.map(Some)
|
||||
.collect::<Vec<_>>()
|
||||
);
|
||||
|
||||
// ...but not by the package containing the template.
|
||||
let oi_pkg = asg.lookup(id_tpl).unwrap().src_pkg(&asg).unwrap();
|
||||
let oi_pkg = pkg_lookup(&ctx, id_tpl).unwrap().src_pkg(&asg).unwrap();
|
||||
assert_eq!(
|
||||
vec![
|
||||
// The only identifier on the package should be the template itself.
|
||||
asg.lookup(id_tpl).unwrap(),
|
||||
pkg_lookup(&ctx, id_tpl).unwrap(),
|
||||
],
|
||||
oi_pkg.edges_filtered::<Ident>(&asg).collect::<Vec<_>>()
|
||||
);
|
||||
|
||||
// Verify the above claim that these are not cached in the global
|
||||
// environment.
|
||||
assert_eq!(None, pkg_lookup(&ctx, id_expr_a));
|
||||
assert_eq!(None, pkg_lookup(&ctx, id_expr_b));
|
||||
}
|
||||
|
||||
// Templates can expand into many contexts,
|
||||
|
@ -240,21 +315,23 @@ fn tpl_holds_dangling_expressions() {
|
|||
|
||||
#[rustfmt::skip]
|
||||
let toks = vec![
|
||||
Air::TplOpen(S1),
|
||||
Air::TplStart(S1),
|
||||
Air::BindIdent(id_tpl),
|
||||
|
||||
// Dangling
|
||||
Air::ExprOpen(ExprOp::Sum, S3),
|
||||
Air::ExprClose(S4),
|
||||
Air::ExprStart(ExprOp::Sum, S3),
|
||||
Air::ExprEnd(S4),
|
||||
|
||||
// Dangling
|
||||
Air::ExprOpen(ExprOp::Sum, S5),
|
||||
Air::ExprClose(S6),
|
||||
Air::TplClose(S7),
|
||||
Air::ExprStart(ExprOp::Sum, S5),
|
||||
Air::ExprEnd(S6),
|
||||
Air::TplEnd(S7),
|
||||
];
|
||||
|
||||
let asg = asg_from_toks(toks);
|
||||
let oi_tpl = asg.expect_ident_oi::<Tpl>(id_tpl);
|
||||
let ctx = air_ctx_from_pkg_body_toks(toks);
|
||||
let asg = ctx.asg_ref();
|
||||
|
||||
let oi_tpl = pkg_expect_ident_oi::<Tpl>(&ctx, id_tpl);
|
||||
|
||||
assert_eq!(
|
||||
vec![S5.merge(S6).unwrap(), S3.merge(S4).unwrap(),],
|
||||
|
@ -268,35 +345,342 @@ fn tpl_holds_dangling_expressions() {
|
|||
|
||||
#[test]
|
||||
fn close_tpl_mid_open() {
|
||||
let id_expr = SPair("expr".into(), S3);
|
||||
let id_tpl = SPair("_tpl_".into(), S2);
|
||||
let id_expr = SPair("expr".into(), S4);
|
||||
|
||||
#[rustfmt::skip]
|
||||
let toks = vec![
|
||||
Air::TplOpen(S1),
|
||||
Air::ExprOpen(ExprOp::Sum, S2),
|
||||
Air::BindIdent(id_expr),
|
||||
Air::TplStart(S1),
|
||||
Air::BindIdent(id_tpl),
|
||||
|
||||
Air::ExprStart(ExprOp::Sum, S3),
|
||||
Air::BindIdent(id_expr),
|
||||
// This is misplaced.
|
||||
Air::TplClose(S4),
|
||||
Air::TplEnd(S5),
|
||||
// RECOVERY: Close the expression and try again.
|
||||
Air::ExprClose(S5),
|
||||
Air::TplClose(S6),
|
||||
Air::ExprEnd(S6),
|
||||
Air::TplEnd(S7),
|
||||
];
|
||||
|
||||
assert_eq!(
|
||||
#[rustfmt::skip]
|
||||
vec![
|
||||
Ok(Parsed::Incomplete), // PkgOpen
|
||||
Ok(Parsed::Incomplete), // TplOpen
|
||||
Ok(Parsed::Incomplete), // ExprOpen
|
||||
Ok(Parsed::Incomplete), // PkgStart
|
||||
Ok(Parsed::Incomplete), // TplStart
|
||||
Ok(Parsed::Incomplete), // BindIdent
|
||||
Ok(Parsed::Incomplete), // ExprStart
|
||||
Ok(Parsed::Incomplete), // BindIdent
|
||||
Err(ParseError::StateError(
|
||||
AsgError::InvalidTplCloseContext(S4))
|
||||
AsgError::UnbalancedTpl(S5))
|
||||
),
|
||||
// RECOVERY
|
||||
Ok(Parsed::Incomplete), // ExprClose
|
||||
Ok(Parsed::Incomplete), // TplClose
|
||||
Ok(Parsed::Incomplete), // PkgClose
|
||||
Ok(Parsed::Incomplete), // ExprEnd
|
||||
Ok(Parsed::Incomplete), // TplEnd
|
||||
Ok(Parsed::Incomplete), // PkgEnd
|
||||
],
|
||||
parse_as_pkg_body(toks).collect::<Vec<_>>(),
|
||||
);
|
||||
}
|
||||
|
||||
// If a template is ended with `TplEnd` and was not assigned a name,
|
||||
// then it isn't reachable on the graph.
|
||||
//
|
||||
// ...that's technically not entirely true in a traversal sense
|
||||
// (see following test),
|
||||
// but the context would be all wrong.
|
||||
// It _is_ true from a practical sense,
|
||||
// with how NIR and AIR have been constructed at the time of writing,
|
||||
// but may not be true in the future.
|
||||
#[test]
|
||||
fn unreachable_anonymous_tpl() {
|
||||
let id_ok = SPair("_tpl_".into(), S4);
|
||||
|
||||
#[rustfmt::skip]
|
||||
let toks = vec![
|
||||
Air::TplStart(S1),
|
||||
// No BindIdent
|
||||
Air::TplEnd(S2),
|
||||
|
||||
// Recovery should ignore the above template
|
||||
// (it's lost to the void)
|
||||
// and allow continuing.
|
||||
Air::TplStart(S3),
|
||||
Air::BindIdent(id_ok),
|
||||
Air::TplEnd(S5),
|
||||
];
|
||||
|
||||
let mut sut = parse_as_pkg_body(toks);
|
||||
|
||||
assert_eq!(
|
||||
#[rustfmt::skip]
|
||||
vec![
|
||||
Ok(Parsed::Incomplete), // PkgStart
|
||||
Ok(Parsed::Incomplete), // TplStart
|
||||
Err(ParseError::StateError(AsgError::DanglingTpl(
|
||||
S1.merge(S2).unwrap()
|
||||
))),
|
||||
// RECOVERY
|
||||
Ok(Parsed::Incomplete), // TplStart
|
||||
Ok(Parsed::Incomplete), // TplBindIdent
|
||||
Ok(Parsed::Incomplete), // TplEnd
|
||||
Ok(Parsed::Incomplete), // PkgEnd
|
||||
],
|
||||
sut.by_ref().collect::<Vec<_>>(),
|
||||
);
|
||||
|
||||
let ctx = sut.finalize().unwrap().into_private_context();
|
||||
|
||||
// Let's make sure that the template created after recovery succeeded.
|
||||
pkg_expect_ident_obj::<Tpl>(&ctx, id_ok);
|
||||
}
|
||||
|
||||
// Normally we cannot reference objects without an identifier using AIR
|
||||
// (at the time of writing at least),
|
||||
// but `TplEndRef` is an exception.
|
||||
#[test]
|
||||
fn anonymous_tpl_immediate_ref() {
|
||||
#[rustfmt::skip]
|
||||
let toks = vec![
|
||||
Air::TplStart(S1),
|
||||
// No BindIdent
|
||||
// But ended with `TplEndRef`,
|
||||
// so the missing identifier is okay.
|
||||
// This would fail if it were `TplEnd`.
|
||||
Air::TplEndRef(S2),
|
||||
];
|
||||
|
||||
let mut sut = parse_as_pkg_body(toks);
|
||||
assert!(sut.all(|x| x.is_ok()));
|
||||
|
||||
// TODO: More to come.
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tpl_with_param() {
|
||||
let id_tpl = SPair("_tpl_".into(), S2);
|
||||
|
||||
let id_param1 = SPair("@param1@".into(), S4);
|
||||
let pval1 = SPair("value1".into(), S5);
|
||||
let id_param2 = SPair("@param2@".into(), S8);
|
||||
|
||||
#[rustfmt::skip]
|
||||
let toks = vec![
|
||||
Air::TplStart(S1),
|
||||
Air::BindIdent(id_tpl),
|
||||
|
||||
// Metavariable with a value.
|
||||
Air::MetaStart(S3),
|
||||
Air::BindIdent(id_param1),
|
||||
Air::MetaLexeme(pval1),
|
||||
Air::MetaEnd(S6),
|
||||
|
||||
// Required metavariable (no value).
|
||||
Air::MetaStart(S7),
|
||||
Air::BindIdent(id_param2),
|
||||
Air::MetaEnd(S9),
|
||||
Air::TplEnd(S10),
|
||||
];
|
||||
|
||||
let ctx = air_ctx_from_pkg_body_toks(toks);
|
||||
let asg = ctx.asg_ref();
|
||||
|
||||
let oi_tpl = pkg_expect_ident_oi::<Tpl>(&ctx, id_tpl);
|
||||
|
||||
// The template should have an edge to each identifier for each
|
||||
// metavariable.
|
||||
let params = [id_param1, id_param2]
|
||||
.iter()
|
||||
.map(|id| {
|
||||
oi_tpl
|
||||
.lookup_local_linear(&asg, *id)
|
||||
.and_then(|oi| oi.edges_filtered::<Meta>(&asg).next())
|
||||
.map(ObjectIndex::cresolve(&asg))
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
assert_eq!(params[0], Some(&Meta::Lexeme(S3.merge(S6).unwrap(), pval1)));
|
||||
assert_eq!(params[1], Some(&Meta::Required(S7.merge(S9).unwrap())));
|
||||
}
|
||||
|
||||
// A template definition nested within another creates a
|
||||
// template-defining-template.
|
||||
// The inner template and its identifier should be entirely contained within
|
||||
// the outer (parent) template so that it will expand into a template
|
||||
// definition in the context of the expansion site.
|
||||
#[test]
|
||||
fn tpl_nested() {
|
||||
let id_tpl_outer = SPair("_tpl-outer_".into(), S2);
|
||||
let id_tpl_inner = SPair("_tpl-inner_".into(), S4);
|
||||
|
||||
#[rustfmt::skip]
|
||||
let toks = vec![
|
||||
Air::TplStart(S1),
|
||||
Air::BindIdent(id_tpl_outer),
|
||||
|
||||
// Inner template
|
||||
Air::TplStart(S3),
|
||||
Air::BindIdent(id_tpl_inner),
|
||||
Air::TplEnd(S5),
|
||||
Air::TplEnd(S6),
|
||||
];
|
||||
|
||||
let ctx = air_ctx_from_pkg_body_toks(toks);
|
||||
let asg = ctx.asg_ref();
|
||||
|
||||
// The outer template should be defined globally,
|
||||
// but not the inner,
|
||||
// since it hasn't been expanded yet.
|
||||
let oi_tpl_outer = pkg_expect_ident_oi::<Tpl>(&ctx, id_tpl_outer);
|
||||
assert_eq!(None, pkg_lookup(&ctx, id_tpl_inner));
|
||||
assert_eq!(S1.merge(S6).unwrap(), oi_tpl_outer.resolve(&asg).span());
|
||||
|
||||
// The identifier for the inner template should be local to the outer
|
||||
// template.
|
||||
let oi_tpl_inner = oi_tpl_outer.lookup_local_linear(&asg, id_tpl_inner);
|
||||
assert_eq!(
|
||||
S3.merge(S5),
|
||||
oi_tpl_inner
|
||||
.and_then(|oi| oi.definition::<Tpl>(&asg))
|
||||
.map(|oi| oi.resolve(&asg).span())
|
||||
);
|
||||
}
|
||||
|
||||
// A template application within another template can be interpreted as
|
||||
// either applying the template as much as possible into the body of the
|
||||
// template definition,
|
||||
// or as expanding the template application into the expansion site and
|
||||
// _then_ expanding the inner template at the expansion site.
|
||||
// Both will yield equivalent results,
|
||||
// but in either case,
|
||||
// it all starts the same.
|
||||
#[test]
|
||||
fn tpl_apply_nested() {
|
||||
let id_tpl_outer = SPair("_tpl-outer_".into(), S2);
|
||||
|
||||
#[rustfmt::skip]
|
||||
let toks = vec![
|
||||
Air::TplStart(S1),
|
||||
Air::BindIdent(id_tpl_outer),
|
||||
|
||||
// Inner template application
|
||||
Air::TplStart(S3),
|
||||
Air::TplEndRef(S4),
|
||||
Air::TplEnd(S5),
|
||||
];
|
||||
|
||||
let ctx = air_ctx_from_pkg_body_toks(toks);
|
||||
let asg = ctx.asg_ref();
|
||||
|
||||
let oi_tpl_outer = pkg_expect_ident_oi::<Tpl>(&ctx, id_tpl_outer);
|
||||
assert_eq!(S1.merge(S5).unwrap(), oi_tpl_outer.resolve(&asg).span());
|
||||
|
||||
// The inner template,
|
||||
// being a template application,
|
||||
// should be a direct child of the outer template.
|
||||
let inners = oi_tpl_outer
|
||||
.edges_filtered::<Tpl>(&asg)
|
||||
.map(|oi| oi.resolve(&asg).span());
|
||||
|
||||
assert_eq!(vec![S3.merge(S4).unwrap()], inners.collect::<Vec<_>>(),);
|
||||
}
|
||||
|
||||
// Template application should resolve all the same regardless of order of
|
||||
// ref/def.
|
||||
#[test]
|
||||
fn tpl_apply_nested_missing() {
|
||||
let id_tpl_outer = SPair("_tpl-outer_".into(), S2);
|
||||
let tpl_inner = "_tpl-inner_".into();
|
||||
let id_tpl_inner = SPair(tpl_inner, S7);
|
||||
|
||||
let ref_tpl_inner_pre = SPair(tpl_inner, S4);
|
||||
let ref_tpl_inner_post = SPair(tpl_inner, S10);
|
||||
|
||||
#[rustfmt::skip]
|
||||
let toks = vec![
|
||||
Air::TplStart(S1),
|
||||
Air::BindIdent(id_tpl_outer),
|
||||
|
||||
// Inner template application (Missing)
|
||||
Air::TplStart(S3),
|
||||
Air::RefIdent(ref_tpl_inner_pre),
|
||||
Air::TplEndRef(S5),
|
||||
|
||||
// Define the template above
|
||||
Air::TplStart(S6),
|
||||
Air::BindIdent(id_tpl_inner),
|
||||
Air::TplEnd(S8),
|
||||
|
||||
// Apply again,
|
||||
// this time _after_ having been defined.
|
||||
Air::TplStart(S9),
|
||||
Air::RefIdent(ref_tpl_inner_post),
|
||||
Air::TplEndRef(S11),
|
||||
Air::TplEnd(S12),
|
||||
];
|
||||
|
||||
let ctx = air_ctx_from_pkg_body_toks(toks);
|
||||
let asg = ctx.asg_ref();
|
||||
|
||||
let oi_tpl_outer = pkg_expect_ident_oi::<Tpl>(&ctx, id_tpl_outer);
|
||||
assert_eq!(S1.merge(S12).unwrap(), oi_tpl_outer.resolve(&asg).span());
|
||||
|
||||
// The inner template should be contained within the outer and so not
|
||||
// globally resolvable.
|
||||
assert!(pkg_lookup(&ctx, id_tpl_inner).is_none());
|
||||
|
||||
// But it is accessible as a local on the outer template.
|
||||
let oi_tpl_inner = oi_tpl_outer
|
||||
.lookup_local_linear(&asg, id_tpl_inner)
|
||||
.expect("could not locate inner template as a local")
|
||||
.definition::<Tpl>(&asg)
|
||||
.expect("could not resolve inner template ref to Tpl");
|
||||
|
||||
// We should have two inner template applications.
|
||||
let inners = oi_tpl_outer.edges_filtered::<Tpl>(&asg).collect::<Vec<_>>();
|
||||
assert_eq!(
|
||||
vec![S9.merge(S11).unwrap(), S3.merge(S5).unwrap()],
|
||||
inners
|
||||
.iter()
|
||||
.map(|oi| oi.resolve(&asg).span())
|
||||
.collect::<Vec<_>>(),
|
||||
);
|
||||
|
||||
// Each of those inner template applications should have resolved to the
|
||||
// same template,
|
||||
// despite their varying ref/def ordering.
|
||||
assert_eq!(
|
||||
vec![oi_tpl_inner, oi_tpl_inner],
|
||||
inners
|
||||
.iter()
|
||||
.flat_map(|oi| oi.edges_filtered::<Ident>(&asg))
|
||||
.filter_map(|oi| oi.definition(&asg))
|
||||
.collect::<Vec<_>>(),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tpl_doc_short_desc() {
|
||||
let id_tpl = SPair("foo".into(), S2);
|
||||
let clause = SPair("short desc".into(), S3);
|
||||
|
||||
#[rustfmt::skip]
|
||||
let toks = vec![
|
||||
Air::TplStart(S1),
|
||||
Air::BindIdent(id_tpl),
|
||||
Air::DocIndepClause(clause),
|
||||
Air::TplEnd(S4),
|
||||
];
|
||||
|
||||
let ctx = air_ctx_from_pkg_body_toks(toks);
|
||||
let asg = ctx.asg_ref();
|
||||
|
||||
let oi_expr = pkg_expect_ident_oi::<Tpl>(&ctx, id_tpl);
|
||||
let oi_docs = oi_expr
|
||||
.edges_filtered::<Doc>(&asg)
|
||||
.map(ObjectIndex::cresolve(&asg));
|
||||
|
||||
assert_eq!(
|
||||
vec![&Doc::new_indep_clause(clause)],
|
||||
oi_docs.collect::<Vec<_>>(),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -31,7 +31,9 @@ use crate::{
|
|||
span::Span,
|
||||
};
|
||||
|
||||
use super::TransitionError;
|
||||
use super::{
|
||||
graph::object::pkg::CanonicalNameError, visit::Cycle, TransitionError,
|
||||
};
|
||||
|
||||
/// An error from an ASG operation.
|
||||
///
|
||||
|
@ -61,20 +63,33 @@ pub enum AsgError {
|
|||
/// whereas _declaring_ an identifier provides metadata about it.
|
||||
IdentRedefine(SPair, Span),
|
||||
|
||||
/// Error while processing the canonical name for a package.
|
||||
PkgCanonicalName(CanonicalNameError),
|
||||
|
||||
/// A package of this same name has already been defined.
|
||||
///
|
||||
/// The [`SPair`]s represent the original and redefinition names
|
||||
/// respectively.
|
||||
PkgRedeclare(SPair, SPair),
|
||||
|
||||
/// Attempted to open a package while defining another package.
|
||||
///
|
||||
/// Packages cannot be nested.
|
||||
/// The first span represents the location of the second package open,
|
||||
/// and the second span represents the location of the package already
|
||||
/// being defined.
|
||||
NestedPkgOpen(Span, Span),
|
||||
/// The [`SPair`]s are the respective package names.
|
||||
NestedPkgStart((Span, SPair), (Span, SPair)),
|
||||
|
||||
/// Attempted to close a package when not in a package toplevel context.
|
||||
InvalidPkgCloseContext(Span),
|
||||
InvalidPkgEndContext(Span),
|
||||
|
||||
/// Attempted to open an expression in an invalid context.
|
||||
PkgExpected(Span),
|
||||
|
||||
/// Requested package import in a non-package context.
|
||||
InvalidPkgImport(SPair),
|
||||
|
||||
/// An expresion is not reachable by any other expression or
|
||||
/// identifier.
|
||||
///
|
||||
|
@ -88,6 +103,12 @@ pub enum AsgError {
|
|||
/// The span should encompass the entirety of the expression.
|
||||
DanglingExpr(Span),
|
||||
|
||||
/// A template is not reachable by any other object.
|
||||
///
|
||||
/// See [`Self::DanglingExpr`] for more information on the concept of
|
||||
/// dangling objects.
|
||||
DanglingTpl(Span),
|
||||
|
||||
/// Attempted to close an expression with no corresponding opening
|
||||
/// delimiter.
|
||||
UnbalancedExpr(Span),
|
||||
|
@ -96,22 +117,49 @@ pub enum AsgError {
|
|||
/// delimiter.
|
||||
UnbalancedTpl(Span),
|
||||
|
||||
/// Attempted to bind the an identifier to an expression while not in an
|
||||
/// expression context.
|
||||
/// Attempted to bind an 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.
|
||||
InvalidExprBindContext(SPair),
|
||||
InvalidBindContext(SPair),
|
||||
|
||||
/// Attempted to reference an identifier as part of an expression while
|
||||
/// not in an expression context.
|
||||
/// Attempted to reference an identifier while not in a context that can
|
||||
/// receive an identifier reference.
|
||||
///
|
||||
/// Ideally this situation is syntactically invalid in a source IR.
|
||||
InvalidExprRefContext(SPair),
|
||||
InvalidRefContext(SPair),
|
||||
|
||||
/// Attempted to close a template when not in a template toplevel
|
||||
/// context.
|
||||
InvalidTplCloseContext(Span),
|
||||
/// Attempted to expand a template into a context that does not support
|
||||
/// expansion.
|
||||
InvalidExpansionContext(Span),
|
||||
|
||||
/// Documentation text is not valid in an expression context.
|
||||
///
|
||||
/// This historical limitation existed because the author was unsure how
|
||||
/// to go about rendering an equation with literate documentation
|
||||
/// interspersed.
|
||||
/// The plan is to lift this limitation in the future.
|
||||
///
|
||||
/// The spans represent the expression and the documentation text
|
||||
/// respectively.
|
||||
InvalidDocContextExpr(Span, Span),
|
||||
|
||||
/// A circular dependency was found where it is not permitted.
|
||||
///
|
||||
/// A cycle almost always means that computing the value of an object
|
||||
/// depends on first having computed itself,
|
||||
/// which is not possible.
|
||||
UnsupportedCycle(Cycle),
|
||||
|
||||
/// An opaque identifier was declared in an invalid context.
|
||||
UnexpectedOpaqueIdent(SPair),
|
||||
|
||||
/// A metavariable is being defined in an invalid context.
|
||||
///
|
||||
/// The provided [`Span`] indicates the location of the start of the
|
||||
/// metavariable definition.
|
||||
UnexpectedMeta(Span),
|
||||
}
|
||||
|
||||
impl Display for AsgError {
|
||||
|
@ -119,33 +167,69 @@ impl Display for AsgError {
|
|||
use AsgError::*;
|
||||
|
||||
match self {
|
||||
IdentTransition(err) => Display::fmt(&err, f),
|
||||
IdentTransition(e) => Display::fmt(&e, f),
|
||||
IdentRedefine(spair, _) => {
|
||||
write!(f, "cannot redefine {}", TtQuote::wrap(spair))
|
||||
}
|
||||
NestedPkgOpen(_, _) => write!(f, "cannot nest packages"),
|
||||
InvalidPkgCloseContext(_) => {
|
||||
PkgCanonicalName(e) => Display::fmt(&e, f),
|
||||
PkgRedeclare(orig, _) => write!(
|
||||
f,
|
||||
"attempted to redeclare or redefine package {}",
|
||||
TtQuote::wrap(orig),
|
||||
),
|
||||
NestedPkgStart((_, child), (_, parent)) => write!(
|
||||
f,
|
||||
"cannot define package {} while defining package {}",
|
||||
TtQuote::wrap(child),
|
||||
TtQuote::wrap(parent),
|
||||
),
|
||||
InvalidPkgEndContext(_) => {
|
||||
write!(f, "invalid context for package close",)
|
||||
}
|
||||
PkgExpected(_) => write!(f, "expected package definition"),
|
||||
InvalidPkgImport(namespec) => write!(
|
||||
f,
|
||||
"unexpected package import {}",
|
||||
TtQuote::wrap(namespec)
|
||||
),
|
||||
DanglingExpr(_) => write!(
|
||||
f,
|
||||
"dangling expression (anonymous expression has no parent)"
|
||||
),
|
||||
DanglingTpl(_) => write!(
|
||||
f,
|
||||
"dangling template (anonymous template cannot be referenced)"
|
||||
),
|
||||
UnbalancedExpr(_) => write!(f, "unbalanced expression"),
|
||||
UnbalancedTpl(_) => write!(f, "unbalanced template definition"),
|
||||
InvalidExprBindContext(_) => {
|
||||
write!(f, "invalid expression identifier binding context")
|
||||
InvalidBindContext(_) => {
|
||||
write!(f, "invalid identifier binding context")
|
||||
}
|
||||
InvalidExprRefContext(ident) => {
|
||||
InvalidRefContext(ident) => {
|
||||
write!(
|
||||
f,
|
||||
"invalid context for expression identifier {}",
|
||||
TtQuote::wrap(ident)
|
||||
)
|
||||
}
|
||||
InvalidTplCloseContext(_) => {
|
||||
write!(f, "invalid context for template close",)
|
||||
InvalidExpansionContext(_) => {
|
||||
write!(f, "invalid template expansion context",)
|
||||
}
|
||||
InvalidDocContextExpr(_, _) => {
|
||||
write!(f, "document text is not permitted within expressions")
|
||||
}
|
||||
UnsupportedCycle(cycle) => {
|
||||
write!(f, "circular dependency: {cycle}")
|
||||
}
|
||||
UnexpectedOpaqueIdent(name) => {
|
||||
write!(
|
||||
f,
|
||||
"unexpected opaque identifier {} declaration",
|
||||
TtQuote::wrap(name)
|
||||
)
|
||||
}
|
||||
UnexpectedMeta(_) => {
|
||||
write!(f, "unexpected metavariable definition")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -154,8 +238,14 @@ impl Display for AsgError {
|
|||
impl Error for AsgError {}
|
||||
|
||||
impl From<TransitionError> for AsgError {
|
||||
fn from(err: TransitionError) -> Self {
|
||||
Self::IdentTransition(err)
|
||||
fn from(e: TransitionError) -> Self {
|
||||
Self::IdentTransition(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<CanonicalNameError> for AsgError {
|
||||
fn from(e: CanonicalNameError) -> Self {
|
||||
Self::PkgCanonicalName(e)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -188,16 +278,25 @@ impl Diagnostic for AsgError {
|
|||
.help(" defined and its definition cannot be changed."),
|
||||
],
|
||||
|
||||
NestedPkgOpen(second, first) => vec![
|
||||
first.note("this package is still being defined"),
|
||||
second.error("attempted to open another package here"),
|
||||
second.help(
|
||||
"close the package to complete its definition before \
|
||||
attempting to open another",
|
||||
),
|
||||
PkgCanonicalName(e) => e.describe(),
|
||||
|
||||
PkgRedeclare(orig, redef) => vec![
|
||||
orig.note("package originally declared here"),
|
||||
redef.error("attempting to redeclare or redefine package here"),
|
||||
],
|
||||
|
||||
InvalidPkgCloseContext(span) => vec![
|
||||
NestedPkgStart((second, sname), (first, fname)) => vec![
|
||||
first.note("this package is still being defined"),
|
||||
second.error("attempted to open another package here"),
|
||||
second.help(format!(
|
||||
"end the package {} complete its definition before \
|
||||
attempting to start the definition of {}",
|
||||
TtQuote::wrap(fname),
|
||||
TtQuote::wrap(sname),
|
||||
)),
|
||||
],
|
||||
|
||||
InvalidPkgEndContext(span) => vec![
|
||||
span.error("package close was not expected here"),
|
||||
span.help(
|
||||
"a package must be closed at the same level of nesting \
|
||||
|
@ -209,6 +308,15 @@ impl Diagnostic for AsgError {
|
|||
vec![span.error("a package definition was expected here")]
|
||||
}
|
||||
|
||||
InvalidPkgImport(namespec) => {
|
||||
vec![
|
||||
namespec.error("this package cannot be imported here"),
|
||||
namespec.help(
|
||||
"imports must appear in the context of a package",
|
||||
),
|
||||
]
|
||||
}
|
||||
|
||||
DanglingExpr(span) => vec![
|
||||
span.error(
|
||||
"this expression is unreachable and its value \
|
||||
|
@ -221,6 +329,17 @@ impl Diagnostic for AsgError {
|
|||
span.help(" its value cannot referenced."),
|
||||
],
|
||||
|
||||
DanglingTpl(span) => vec![
|
||||
span.error(
|
||||
"this template is unreachable and can never be used",
|
||||
),
|
||||
span.help(
|
||||
"a template may only be anonymous if it is ephemeral ",
|
||||
),
|
||||
span.help(" (immediately expanded)."),
|
||||
span.help("alternatively, assign this template an identifier."),
|
||||
],
|
||||
|
||||
UnbalancedExpr(span) => {
|
||||
vec![span.error("there is no open expression to close here")]
|
||||
}
|
||||
|
@ -229,28 +348,65 @@ impl Diagnostic for AsgError {
|
|||
vec![span.error("there is no open template to close here")]
|
||||
}
|
||||
|
||||
InvalidExprBindContext(span) => vec![
|
||||
span.error(
|
||||
"there is no active expression to bind this identifier to",
|
||||
),
|
||||
span.help(
|
||||
"an identifier must be bound to an expression before \
|
||||
the expression is closed",
|
||||
),
|
||||
],
|
||||
InvalidBindContext(name) => vec![name
|
||||
.error("an identifier binding is not valid in this context")],
|
||||
|
||||
InvalidExprRefContext(ident) => vec![ident.error(
|
||||
InvalidRefContext(ident) => vec![ident.error(
|
||||
"cannot reference the value of an expression from outside \
|
||||
of an expression context",
|
||||
)],
|
||||
InvalidExpansionContext(span) => {
|
||||
vec![span.error("cannot expand a template here")]
|
||||
}
|
||||
|
||||
InvalidTplCloseContext(span) => vec![
|
||||
span.error("template close was not expected here"),
|
||||
InvalidDocContextExpr(expr_span, span) => vec![
|
||||
expr_span.note("in this expression"),
|
||||
span.error("documentation text is not permitted here"),
|
||||
span.help(
|
||||
"a template must be closed at the same level of nesting \
|
||||
that it was opened",
|
||||
"this is a historical limitation that will \
|
||||
likely be lifted in the future",
|
||||
),
|
||||
],
|
||||
UnsupportedCycle(cycle) => {
|
||||
// The cycle description clearly describes the cycle,
|
||||
// but in neutral terms,
|
||||
// since cycles may not necessarily be errors.
|
||||
let mut desc = cycle.describe();
|
||||
|
||||
// (this will always be non-empty)
|
||||
if let Some(obj) = cycle.path_rev().last() {
|
||||
// But in this context,
|
||||
// this _is_ a problem,
|
||||
// so make clear why we're pointing this out.
|
||||
// TODO: Include an identifier name,
|
||||
// once `Cycle` supports it.
|
||||
desc.extend(
|
||||
[
|
||||
obj.help(
|
||||
"the value cannot be computed because its",
|
||||
),
|
||||
obj.help(
|
||||
" definition requires first computing itself.",
|
||||
),
|
||||
]
|
||||
.into_iter(),
|
||||
);
|
||||
}
|
||||
|
||||
desc
|
||||
}
|
||||
// TODO: This doesn't seem all that helpful.
|
||||
// What are the circumstances under which this can be hit,
|
||||
// and what additional information can we provide?
|
||||
UnexpectedOpaqueIdent(name) => vec![name.error(
|
||||
"an opaque identifier declaration was not expected here",
|
||||
)],
|
||||
UnexpectedMeta(span) => {
|
||||
vec![
|
||||
span.error("this metavariable cannot occur here"),
|
||||
span.help("metavariables are expected to occur in a template context"),
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,21 +22,16 @@
|
|||
//! ![Visualization of ASG ontology](../ontviz.svg)
|
||||
|
||||
use self::object::{
|
||||
DynObjectRel, ObjectRelFrom, ObjectRelTy, ObjectRelatable, Root,
|
||||
DynObjectRel, ObjectIndexRelTo, ObjectRelFrom, ObjectRelTy,
|
||||
ObjectRelatable, Root,
|
||||
};
|
||||
|
||||
use super::{
|
||||
AsgError, FragmentText, Ident, IdentKind, Object, ObjectIndex, ObjectKind,
|
||||
Source, TransitionResult,
|
||||
};
|
||||
use super::{AsgError, Object, ObjectIndex, ObjectKind};
|
||||
use crate::{
|
||||
diagnose::{panic::DiagnosticPanic, Annotate, AnnotatedSpan},
|
||||
f::Functor,
|
||||
fmt::{DisplayWrapper, TtQuote},
|
||||
global,
|
||||
parse::{util::SPair, Token},
|
||||
span::Span,
|
||||
sym::SymbolId,
|
||||
};
|
||||
use petgraph::{
|
||||
graph::{DiGraph, Graph, NodeIndex},
|
||||
|
@ -49,7 +44,7 @@ pub mod object;
|
|||
pub mod visit;
|
||||
pub mod xmli;
|
||||
|
||||
use object::{ObjectContainer, ObjectRelTo};
|
||||
use object::ObjectContainer;
|
||||
|
||||
/// Datatype representing node and edge indexes.
|
||||
pub trait IndexType = petgraph::graph::IndexType;
|
||||
|
@ -83,65 +78,47 @@ type Ix = global::ProgSymSize;
|
|||
///
|
||||
/// This implementation is currently based on [`petgraph`].
|
||||
///
|
||||
/// Identifiers are cached by name for `O(1)` lookup.
|
||||
/// Since [`SymbolId`][crate::sym::SymbolId] is used for this purpose,
|
||||
/// the index may contain more entries than nodes and may contain gaps.
|
||||
///
|
||||
/// This IR focuses on the definition and manipulation of objects and their
|
||||
/// dependencies.
|
||||
/// See [`Ident`]for a summary of valid identifier object state
|
||||
/// transitions.
|
||||
///
|
||||
/// Objects are never deleted from the graph,
|
||||
/// so [`ObjectIndex`]s will remain valid for the lifetime of the ASG.
|
||||
///
|
||||
/// For more information,
|
||||
/// see the [module-level documentation][self].
|
||||
pub struct Asg {
|
||||
// TODO: private; see `ld::xmle::lower`.
|
||||
/// Directed graph on which objects are stored.
|
||||
pub graph: DiGraph<Node, AsgEdge, Ix>,
|
||||
|
||||
/// Map of [`SymbolId`][crate::sym::SymbolId] to node indexes.
|
||||
///
|
||||
/// This allows for `O(1)` lookup of identifiers in the graph.
|
||||
/// Note that,
|
||||
/// while we store [`NodeIndex`] internally,
|
||||
/// the public API encapsulates it within an [`ObjectIndex`].
|
||||
index: Vec<NodeIndex<Ix>>,
|
||||
|
||||
/// Empty node indicating that no object exists for a given index.
|
||||
empty_node: NodeIndex<Ix>,
|
||||
graph: DiGraph<Node, AsgEdge, Ix>,
|
||||
|
||||
/// The root node used for reachability analysis and topological
|
||||
/// sorting.
|
||||
root_node: NodeIndex<Ix>,
|
||||
}
|
||||
|
||||
impl Debug for Asg {
|
||||
/// Trimmed-down Asg [`Debug`] output.
|
||||
///
|
||||
/// This primarily hides the large `self.index` that takes up so much
|
||||
/// space in parser traces,
|
||||
/// but also hides irrelevant information.
|
||||
///
|
||||
/// The better option in the future may be to create a newtype for
|
||||
/// `index` if it sticks around in its current form,
|
||||
/// which in turn can encapsulate `self.empty_node`.
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
f.debug_struct("Asg")
|
||||
.field("root_node", &self.root_node)
|
||||
.field("graph", &self.graph)
|
||||
.finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Asg {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for Asg {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
// The ASG provides far too much information even on modestly size
|
||||
// tests,
|
||||
// letalone real-world graphs with tens to hundreds of thousands
|
||||
// of nodes and edges.
|
||||
// Outputting the graph also brings Parser trace generation to a
|
||||
// crawl,
|
||||
// making tracing half-useless.
|
||||
// So this provides a simple summary.
|
||||
// If we need a graph representation,
|
||||
// a visualization or ability to query it is far more appropriate.
|
||||
write!(
|
||||
f,
|
||||
"[ASG: {} objects, {} edges]",
|
||||
self.object_count(),
|
||||
self.graph.edge_count(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Asg {
|
||||
/// Create a new ASG.
|
||||
///
|
||||
|
@ -163,24 +140,13 @@ impl Asg {
|
|||
/// edge to another object.
|
||||
pub fn with_capacity(objects: usize, edges: usize) -> Self {
|
||||
let mut graph = Graph::with_capacity(objects, edges);
|
||||
let mut index = Vec::with_capacity(objects);
|
||||
|
||||
// Exhaust the first index to be used as a placeholder
|
||||
// (its value does not matter).
|
||||
let empty_node = graph.add_node(Object::Root(Root).into());
|
||||
index.push(empty_node);
|
||||
|
||||
// Automatically add the root which will be used to determine what
|
||||
// identifiers ought to be retained by the final program.
|
||||
// This is not indexed and is not accessable by name.
|
||||
let root_node = graph.add_node(Object::Root(Root).into());
|
||||
|
||||
Self {
|
||||
graph,
|
||||
index,
|
||||
empty_node,
|
||||
root_node,
|
||||
}
|
||||
Self { graph, root_node }
|
||||
}
|
||||
|
||||
/// Get the underlying Graph
|
||||
|
@ -188,104 +154,13 @@ impl Asg {
|
|||
self.graph
|
||||
}
|
||||
|
||||
/// Index the provided symbol `name` as representing the identifier `node`.
|
||||
/// Number of [`Object`]s on the graph.
|
||||
///
|
||||
/// This index permits `O(1)` identifier lookups.
|
||||
///
|
||||
/// After an identifier is indexed it is not expected to be reassigned
|
||||
/// to another node.
|
||||
/// Debug builds contain an assertion that will panic in this instance.
|
||||
///
|
||||
/// Panics
|
||||
/// ======
|
||||
/// Will panic if unable to allocate more space for the index.
|
||||
fn index_identifier(&mut self, name: SymbolId, node: NodeIndex<Ix>) {
|
||||
let i = name.as_usize();
|
||||
|
||||
if i >= self.index.len() {
|
||||
// If this is ever a problem we can fall back to usize max and
|
||||
// re-compare before panicing
|
||||
let new_size = (i + 1)
|
||||
.checked_next_power_of_two()
|
||||
.expect("internal error: cannot allocate space for ASG index");
|
||||
|
||||
self.index.resize(new_size, self.empty_node);
|
||||
}
|
||||
|
||||
// We should never overwrite indexes
|
||||
debug_assert!(self.index[i] == self.empty_node);
|
||||
|
||||
self.index[i] = node;
|
||||
}
|
||||
|
||||
/// Lookup `ident` or add a missing identifier to the graph and return a
|
||||
/// reference to it.
|
||||
///
|
||||
/// The provided span is necessary to seed the missing identifier with
|
||||
/// some sort of context to aid in debugging why a missing identifier
|
||||
/// was introduced to the graph.
|
||||
/// The provided span will be used even if an identifier exists on the
|
||||
/// graph,
|
||||
/// which can be used for retaining information on the location that
|
||||
/// requested the identifier.
|
||||
/// To retrieve the span of a previously declared identifier,
|
||||
/// you must resolve the [`Ident`] object and inspect it.
|
||||
///
|
||||
/// See [`Ident::declare`] for more information.
|
||||
pub(super) fn lookup_or_missing(
|
||||
&mut self,
|
||||
ident: SPair,
|
||||
) -> ObjectIndex<Ident> {
|
||||
self.lookup(ident).unwrap_or_else(|| {
|
||||
let index = self.graph.add_node(Ident::declare(ident).into());
|
||||
|
||||
self.index_identifier(ident.symbol(), index);
|
||||
ObjectIndex::new(index, ident.span())
|
||||
})
|
||||
}
|
||||
|
||||
/// Perform a state transition on an identifier by name.
|
||||
///
|
||||
/// Look up `ident` or add a missing identifier if it does not yet exist
|
||||
/// (see [`Self::lookup_or_missing`]).
|
||||
/// Then invoke `f` with the located identifier and replace the
|
||||
/// identifier on the graph with the result.
|
||||
///
|
||||
/// This will safely restore graph state to the original identifier
|
||||
/// value on transition failure.
|
||||
fn with_ident_lookup<F>(
|
||||
&mut self,
|
||||
name: SPair,
|
||||
f: F,
|
||||
) -> AsgResult<ObjectIndex<Ident>>
|
||||
where
|
||||
F: FnOnce(Ident) -> TransitionResult<Ident>,
|
||||
{
|
||||
let identi = self.lookup_or_missing(name);
|
||||
self.with_ident(identi, f)
|
||||
}
|
||||
|
||||
/// Perform a state transition on an identifier by [`ObjectIndex`].
|
||||
///
|
||||
/// Invoke `f` with the located identifier and replace the identifier on
|
||||
/// the graph with the result.
|
||||
///
|
||||
/// This will safely restore graph state to the original identifier
|
||||
/// value on transition failure.
|
||||
fn with_ident<F>(
|
||||
&mut self,
|
||||
identi: ObjectIndex<Ident>,
|
||||
f: F,
|
||||
) -> AsgResult<ObjectIndex<Ident>>
|
||||
where
|
||||
F: FnOnce(Ident) -> TransitionResult<Ident>,
|
||||
{
|
||||
let container = self.graph.node_weight_mut(identi.into()).unwrap();
|
||||
|
||||
container
|
||||
.try_replace_with(f)
|
||||
.map(|()| identi)
|
||||
.map_err(Into::into)
|
||||
/// This is equivalent to the number of nodes on the graph at the time
|
||||
/// of writing,
|
||||
/// but that may not always be the case.
|
||||
fn object_count(&self) -> usize {
|
||||
self.graph.node_count()
|
||||
}
|
||||
|
||||
/// Root object.
|
||||
|
@ -296,123 +171,8 @@ impl Asg {
|
|||
/// The `witness` is used in the returned [`ObjectIndex`] and is
|
||||
/// intended for diagnostic purposes to highlight the source entity that
|
||||
/// triggered the request of the root.
|
||||
pub fn root(&self, witness: Span) -> ObjectIndex<Root> {
|
||||
ObjectIndex::new(self.root_node, witness)
|
||||
}
|
||||
|
||||
/// Add an object as a root.
|
||||
///
|
||||
/// Roots are always included during a topological sort and any
|
||||
/// reachability analysis.
|
||||
///
|
||||
/// Ideally,
|
||||
/// roots would be minimal and dependencies properly organized such
|
||||
/// that objects will be included if they are a transitive dependency
|
||||
/// of some included subsystem.
|
||||
///
|
||||
/// See also [`IdentKind::is_auto_root`].
|
||||
pub fn add_root(&mut self, identi: ObjectIndex<Ident>) {
|
||||
self.graph.add_edge(
|
||||
self.root_node,
|
||||
identi.into(),
|
||||
(ObjectRelTy::Root, ObjectRelTy::Ident, None),
|
||||
);
|
||||
}
|
||||
|
||||
/// Whether an object is rooted.
|
||||
///
|
||||
/// See [`Asg::add_root`] for more information about roots.
|
||||
#[cfg(test)]
|
||||
pub(super) fn is_rooted(&self, identi: ObjectIndex<Ident>) -> bool {
|
||||
self.graph.contains_edge(self.root_node, identi.into())
|
||||
}
|
||||
|
||||
/// Declare a concrete identifier.
|
||||
///
|
||||
/// An identifier declaration is similar to a declaration in a header
|
||||
/// file in a language like C,
|
||||
/// describing the structure of the identifier.
|
||||
/// Once declared,
|
||||
/// this information cannot be changed.
|
||||
///
|
||||
/// Identifiers are uniquely identified by a [`SymbolId`] `name`.
|
||||
/// If an identifier of the same `name` already exists,
|
||||
/// then the provided declaration is compared against the existing
|
||||
/// declaration---should
|
||||
/// they be incompatible,
|
||||
/// then the operation will fail;
|
||||
/// otherwise,
|
||||
/// the existing identifier will be returned.
|
||||
///
|
||||
/// If a concrete identifier has already been declared (see
|
||||
/// [`Asg::declare`]),
|
||||
/// then extern declarations will be compared and,
|
||||
/// if compatible,
|
||||
/// the identifier will be immediately _resolved_ and the object
|
||||
/// on the graph will not be altered.
|
||||
/// Resolution will otherwise fail in error.
|
||||
///
|
||||
/// For more information on state transitions that can occur when
|
||||
/// redeclaring an identifier that already exists,
|
||||
/// see [`Ident::resolve`].
|
||||
///
|
||||
/// A successful declaration will add an identifier to the graph
|
||||
/// and return an [`ObjectIndex`] reference.
|
||||
pub fn declare(
|
||||
&mut self,
|
||||
name: SPair,
|
||||
kind: IdentKind,
|
||||
src: Source,
|
||||
) -> AsgResult<ObjectIndex<Ident>> {
|
||||
let is_auto_root = kind.is_auto_root();
|
||||
|
||||
self.with_ident_lookup(name, |obj| obj.resolve(name.span(), kind, src))
|
||||
.map(|node| {
|
||||
is_auto_root.then(|| self.add_root(node));
|
||||
node
|
||||
})
|
||||
}
|
||||
|
||||
/// Declare an abstract identifier.
|
||||
///
|
||||
/// An _extern_ declaration declares an identifier the same as
|
||||
/// [`Asg::declare`],
|
||||
/// but omits source information.
|
||||
/// Externs are identifiers that are expected to be defined somewhere
|
||||
/// else ("externally"),
|
||||
/// and are resolved at [link-time][crate::ld].
|
||||
///
|
||||
/// If a concrete identifier has already been declared (see
|
||||
/// [`Asg::declare`]),
|
||||
/// then the declarations will be compared and,
|
||||
/// if compatible,
|
||||
/// the identifier will be immediately _resolved_ and the object
|
||||
/// on the graph will not be altered.
|
||||
/// Resolution will otherwise fail in error.
|
||||
///
|
||||
/// See [`Ident::extern_`] and
|
||||
/// [`Ident::resolve`] for more information on
|
||||
/// compatibility related to extern resolution.
|
||||
pub fn declare_extern(
|
||||
&mut self,
|
||||
name: SPair,
|
||||
kind: IdentKind,
|
||||
src: Source,
|
||||
) -> AsgResult<ObjectIndex<Ident>> {
|
||||
self.with_ident_lookup(name, |obj| obj.extern_(name.span(), kind, src))
|
||||
}
|
||||
|
||||
/// Set the fragment associated with a concrete identifier.
|
||||
///
|
||||
/// Fragments are intended for use by the [linker][crate::ld].
|
||||
/// For more information,
|
||||
/// see [`Ident::set_fragment`].
|
||||
pub fn set_fragment(
|
||||
&mut self,
|
||||
name: SPair,
|
||||
text: FragmentText,
|
||||
) -> AsgResult<ObjectIndex<Ident>> {
|
||||
self.with_ident_lookup(name, |obj| obj.set_fragment(text))
|
||||
pub fn root<S: Into<Span>>(&self, witness: S) -> ObjectIndex<Root> {
|
||||
ObjectIndex::new(self.root_node, witness.into())
|
||||
}
|
||||
|
||||
/// Create a new object on the graph.
|
||||
|
@ -436,19 +196,17 @@ impl Asg {
|
|||
/// _reference_ to the target.
|
||||
///
|
||||
/// For more information on how the ASG's ontology is enforced statically,
|
||||
/// see [`ObjectRelTo`].
|
||||
fn add_edge<OA: ObjectKind, OB: ObjectKind>(
|
||||
/// see [`ObjectRelTo`](object::ObjectRelTo).
|
||||
fn add_edge<OB: ObjectKind + ObjectRelatable>(
|
||||
&mut self,
|
||||
from_oi: ObjectIndex<OA>,
|
||||
from_oi: impl ObjectIndexRelTo<OB>,
|
||||
to_oi: ObjectIndex<OB>,
|
||||
ctx_span: Option<Span>,
|
||||
) where
|
||||
OA: ObjectRelTo<OB>,
|
||||
{
|
||||
) {
|
||||
self.graph.add_edge(
|
||||
from_oi.into(),
|
||||
from_oi.widen().into(),
|
||||
to_oi.into(),
|
||||
(OA::rel_ty(), OB::rel_ty(), ctx_span),
|
||||
(from_oi.src_rel_ty(), OB::rel_ty(), ctx_span),
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -517,7 +275,7 @@ impl Asg {
|
|||
/// what is intended.
|
||||
/// This is sufficient in practice,
|
||||
/// since the graph cannot be constructed without adhering to the edge
|
||||
/// ontology defined by [`ObjectRelTo`],
|
||||
/// ontology defined by [`ObjectRelTo`](object::ObjectRelTo),
|
||||
/// but this API is not helpful for catching problems at
|
||||
/// compile-time.
|
||||
///
|
||||
|
@ -594,78 +352,14 @@ impl Asg {
|
|||
.map(move |edge| ObjectIndex::<OI>::new(edge.source(), oi))
|
||||
}
|
||||
|
||||
/// Retrieve the [`ObjectIndex`] to which the given `ident` is bound,
|
||||
/// if any.
|
||||
///
|
||||
/// The type parameter `O` indicates the _expected_ [`ObjectKind`] to be
|
||||
/// bound to the returned [`ObjectIndex`],
|
||||
/// which will be used for narrowing (downcasting) the object after
|
||||
/// lookup.
|
||||
/// An incorrect kind will not cause any failures until such a lookup
|
||||
/// occurs.
|
||||
///
|
||||
/// This will return [`None`] if the identifier is either opaque or does
|
||||
/// not exist.
|
||||
fn get_ident_oi<O: ObjectKind>(
|
||||
/// Check whether an edge exists from `from` to `to.
|
||||
#[inline]
|
||||
pub fn has_edge<OB: ObjectRelatable>(
|
||||
&self,
|
||||
ident: SPair,
|
||||
) -> Option<ObjectIndex<O>> {
|
||||
self.lookup(ident)
|
||||
.and_then(|identi| {
|
||||
self.graph
|
||||
.neighbors_directed(identi.into(), Direction::Outgoing)
|
||||
.next()
|
||||
})
|
||||
// Note that this use of `O` for `ObjectIndex` here means "I
|
||||
// _expect_ this to `O`";
|
||||
// the type will be verified during narrowing but will panic
|
||||
// if this expectation is not met.
|
||||
.map(|ni| ObjectIndex::<O>::new(ni, ident.span()))
|
||||
}
|
||||
|
||||
/// Retrieve the [`ObjectIndex`] to which the given `ident` is bound,
|
||||
/// panicing if the identifier is either opaque or does not exist.
|
||||
///
|
||||
/// Panics
|
||||
/// ======
|
||||
/// This method will panic if the identifier is opaque
|
||||
/// (has no edge to the object to which it is bound)
|
||||
/// or does not exist on the graph.
|
||||
pub fn expect_ident_oi<O: ObjectKind>(
|
||||
&self,
|
||||
ident: SPair,
|
||||
) -> ObjectIndex<O> {
|
||||
self.get_ident_oi(ident).diagnostic_expect(
|
||||
|| diagnostic_opaque_ident_desc(ident),
|
||||
|| {
|
||||
format!(
|
||||
"opaque identifier: {} has no object binding",
|
||||
TtQuote::wrap(ident),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/// Attempt to retrieve the [`Object`] to which the given `ident` is bound.
|
||||
///
|
||||
/// If the identifier either does not exist on the graph or is opaque
|
||||
/// (is not bound to any expression),
|
||||
/// then [`None`] will be returned.
|
||||
///
|
||||
/// If the system expects that the identifier must exist and would
|
||||
/// otherwise represent a bug in the compiler,
|
||||
/// see [`Self::expect_ident_obj`].
|
||||
///
|
||||
/// Panics
|
||||
/// ======
|
||||
/// This method will panic if certain graph invariants are not met,
|
||||
/// representing an invalid system state that should not be able to
|
||||
/// occur through this API.
|
||||
/// Violations of these invariants represent either a bug in the API
|
||||
/// (that allows for the invariant to be violated)
|
||||
/// or direct manipulation of the underlying graph.
|
||||
pub fn get_ident_obj<O: ObjectKind>(&self, ident: SPair) -> Option<&O> {
|
||||
self.get_ident_oi::<O>(ident).map(|oi| self.expect_obj(oi))
|
||||
from: impl ObjectIndexRelTo<OB>,
|
||||
to: ObjectIndex<OB>,
|
||||
) -> bool {
|
||||
self.graph.contains_edge(from.widen().into(), to.into())
|
||||
}
|
||||
|
||||
pub(super) fn expect_obj<O: ObjectKind>(&self, oi: ObjectIndex<O>) -> &O {
|
||||
|
@ -677,122 +371,6 @@ impl Asg {
|
|||
|
||||
obj_container.get()
|
||||
}
|
||||
|
||||
/// Attempt to retrieve the [`Object`] to which the given `ident` is bound,
|
||||
/// panicing if the identifier is opaque or does not exist.
|
||||
///
|
||||
/// This method represents a compiler invariant;
|
||||
/// it should _only_ be used when the identifier _must_ exist,
|
||||
/// otherwise there is a bug in the compiler.
|
||||
/// If this is _not_ the case,
|
||||
/// use [`Self::get_ident_obj`] to get [`None`] in place of a panic.
|
||||
///
|
||||
/// Panics
|
||||
/// ======
|
||||
/// This method will panic if
|
||||
///
|
||||
/// 1. The identifier does not exist on the graph; or
|
||||
/// 2. The identifier is opaque (has no edge to any object on the
|
||||
/// graph).
|
||||
pub fn expect_ident_obj<O: ObjectKind>(&self, ident: SPair) -> &O {
|
||||
self.get_ident_obj(ident).diagnostic_expect(
|
||||
|| diagnostic_opaque_ident_desc(ident),
|
||||
|| {
|
||||
format!(
|
||||
"opaque identifier: {} has no object binding",
|
||||
TtQuote::wrap(ident),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/// Retrieve an identifier from the graph by [`ObjectIndex`].
|
||||
///
|
||||
/// If the object exists but is not an identifier,
|
||||
/// [`None`] will be returned.
|
||||
#[inline]
|
||||
pub fn get_ident(&self, index: ObjectIndex<Ident>) -> Option<&Ident> {
|
||||
self.get(index)
|
||||
}
|
||||
|
||||
/// Attempt to retrieve an identifier from the graph by name.
|
||||
///
|
||||
/// Since only identifiers carry a name,
|
||||
/// this method cannot be used to retrieve all possible objects on the
|
||||
/// graph---for
|
||||
/// that, see [`Asg::get`].
|
||||
#[inline]
|
||||
pub fn lookup(&self, id: SPair) -> Option<ObjectIndex<Ident>> {
|
||||
let i = id.symbol().as_usize();
|
||||
|
||||
self.index
|
||||
.get(i)
|
||||
.filter(|ni| ni.index() > 0)
|
||||
.map(|ni| ObjectIndex::new(*ni, id.span()))
|
||||
}
|
||||
|
||||
/// Declare that `dep` is a dependency of `ident`.
|
||||
///
|
||||
/// An object must be declared as a dependency if its value must be
|
||||
/// computed before computing the value of `ident`.
|
||||
/// The [linker][crate::ld] will ensure this ordering.
|
||||
///
|
||||
/// See [`add_dep_lookup`][Asg::add_dep_lookup] if identifiers have to
|
||||
/// be looked up by [`SymbolId`] or if they may not yet have been
|
||||
/// declared.
|
||||
pub fn add_dep<O: ObjectKind>(
|
||||
&mut self,
|
||||
identi: ObjectIndex<Ident>,
|
||||
depi: ObjectIndex<O>,
|
||||
) where
|
||||
Ident: ObjectRelTo<O>,
|
||||
{
|
||||
self.graph.update_edge(
|
||||
identi.into(),
|
||||
depi.into(),
|
||||
(Ident::rel_ty(), O::rel_ty(), None),
|
||||
);
|
||||
}
|
||||
|
||||
/// Check whether `dep` is a dependency of `ident`.
|
||||
#[inline]
|
||||
pub fn has_dep(
|
||||
&self,
|
||||
ident: ObjectIndex<Ident>,
|
||||
dep: ObjectIndex<Ident>,
|
||||
) -> bool {
|
||||
self.graph.contains_edge(ident.into(), dep.into())
|
||||
}
|
||||
|
||||
/// Declare that `dep` is a dependency of `ident`,
|
||||
/// regardless of whether they are known.
|
||||
///
|
||||
/// In contrast to [`add_dep`][Asg::add_dep],
|
||||
/// this method will add the dependency even if one or both of `ident`
|
||||
/// or `dep` have not yet been declared.
|
||||
/// In such a case,
|
||||
/// a missing identifier will be added as a placeholder,
|
||||
/// allowing the ASG to be built with partial information as
|
||||
/// identifiers continue to be discovered.
|
||||
/// See [`Ident::declare`] for more information.
|
||||
///
|
||||
/// References to both identifiers are returned in argument order.
|
||||
pub fn add_dep_lookup(
|
||||
&mut self,
|
||||
ident: SPair,
|
||||
dep: SPair,
|
||||
) -> (ObjectIndex<Ident>, ObjectIndex<Ident>) {
|
||||
let identi = self.lookup_or_missing(ident);
|
||||
let depi = self.lookup_or_missing(dep);
|
||||
|
||||
self.graph.update_edge(
|
||||
identi.into(),
|
||||
depi.into(),
|
||||
(Ident::rel_ty(), Ident::rel_ty(), None),
|
||||
);
|
||||
|
||||
(identi, depi)
|
||||
}
|
||||
}
|
||||
|
||||
fn diagnostic_node_missing_desc<O: ObjectKind>(
|
||||
|
@ -808,16 +386,5 @@ fn diagnostic_node_missing_desc<O: ObjectKind>(
|
|||
]
|
||||
}
|
||||
|
||||
fn diagnostic_opaque_ident_desc(ident: SPair) -> Vec<AnnotatedSpan<'static>> {
|
||||
vec![
|
||||
ident.internal_error(
|
||||
"this identifier is not bound to any object on the ASG",
|
||||
),
|
||||
ident.help("the system expects to be able to reach the object that"),
|
||||
ident.help(" this identifies, but this identifier has no"),
|
||||
ident.help(" corresponding object present on the graph."),
|
||||
]
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test;
|
||||
|
|
|
@ -25,24 +25,28 @@
|
|||
//! ==================================
|
||||
//! Unlike the functional lowering pipeline that precedes it,
|
||||
//! the ASG is a mutable, ever-evolving graph of dynamic data.
|
||||
//! The ASG does not benefit from the same type-level guarantees that the
|
||||
//! rest of the system does at compile-time.
|
||||
//! The ASG does not benefit from the same static type-level guarantees that
|
||||
//! the rest of the system does at compile-time.
|
||||
//!
|
||||
//! However,
|
||||
//! we _are_ able to utilize the type system to ensure statically that
|
||||
//! there exists no code path that is able to generated an invalid graph
|
||||
//! we _are_ able to utilize the type system to statically ensure that
|
||||
//! there exists no code path that is able to generate an invalid graph
|
||||
//! (a graph that does not adhere to its ontology as described below).
|
||||
//!
|
||||
//! Any node on the graph can represent any type of [`Object`].
|
||||
//! Every node on the graph can represent any type of [`Object`].
|
||||
//! An [`ObjectIndex`] contains an index into the graph,
|
||||
//! _not_ a reference;
|
||||
//! it is therefore possible (though avoidable) for objects to be
|
||||
//! modified out from underneath references.
|
||||
//! modified out from underneath an [`ObjectIndex`].
|
||||
//! Consequently,
|
||||
//! we cannot trust that an [`ObjectIndex`] is what we expect it to be when
|
||||
//! performing an operation on the graph using that index,
|
||||
//! though the system is designed to uphold an invariant that the _type_
|
||||
//! of [`Object`] cannot be changed.
|
||||
//! we cannot be _absolutely certain_ that the [`Object`] referred to by
|
||||
//! an [`ObjectIndex`] is in fact what we expect it to be when performing
|
||||
//! an operation on the graph using that index;
|
||||
//! though the system is designed to uphold an invariant that the type
|
||||
//! of [`Object`] cannot be changed,
|
||||
//! it is conceivable that the system may contain,
|
||||
//! now or in the future,
|
||||
//! bugs that cause it to fail to uphold that invariant.
|
||||
//!
|
||||
//! To perform an operation on a particular type of object,
|
||||
//! we must first _narrow_ it.
|
||||
|
@ -75,7 +79,7 @@
|
|||
//! edges may reference [`Object`]s of many different types,
|
||||
//! as defined by the graph's ontology.
|
||||
//!
|
||||
//! The set [`ObjectKind`] types that may be related _to_
|
||||
//! The set of [`ObjectKind`] types that may be related _to_
|
||||
//! (via edges)
|
||||
//! from other objects are the variants of [`ObjectRelTy`].
|
||||
//! Each such [`ObjectKind`] must implement [`ObjectRelatable`],
|
||||
|
@ -114,34 +118,51 @@ use crate::{
|
|||
diagnose::{panic::DiagnosticPanic, Annotate, AnnotatedSpan},
|
||||
diagnostic_panic,
|
||||
f::Functor,
|
||||
parse::util::SPair,
|
||||
span::{Span, UNKNOWN_SPAN},
|
||||
};
|
||||
use petgraph::graph::NodeIndex;
|
||||
use std::{
|
||||
convert::Infallible,
|
||||
fmt::{Debug, Display},
|
||||
hash::{Hash, Hasher},
|
||||
marker::PhantomData,
|
||||
};
|
||||
|
||||
#[macro_use]
|
||||
mod rel;
|
||||
|
||||
pub mod doc;
|
||||
pub mod expr;
|
||||
pub mod ident;
|
||||
pub mod meta;
|
||||
pub mod pkg;
|
||||
pub mod root;
|
||||
pub mod tpl;
|
||||
|
||||
pub use doc::Doc;
|
||||
pub use expr::Expr;
|
||||
pub use ident::Ident;
|
||||
pub use meta::Meta;
|
||||
pub use pkg::Pkg;
|
||||
pub use rel::{
|
||||
DynObjectRel, ObjectRel, ObjectRelFrom, ObjectRelTo, ObjectRelTy,
|
||||
ObjectRelatable,
|
||||
DynObjectRel, ObjectIndexRelTo, ObjectIndexTo, ObjectIndexToTree,
|
||||
ObjectIndexTreeRelTo, ObjectRel, ObjectRelFrom, ObjectRelTo, ObjectRelTy,
|
||||
ObjectRelatable, ObjectTreeRelTo,
|
||||
};
|
||||
pub use root::Root;
|
||||
pub use tpl::Tpl;
|
||||
|
||||
/// Often-needed exports for [`ObjectKind`]s.
|
||||
pub mod prelude {
|
||||
pub use super::{
|
||||
super::{super::error::AsgError, Asg},
|
||||
Object, ObjectIndex, ObjectIndexRelTo, ObjectKind, ObjectRel,
|
||||
ObjectRelFrom, ObjectRelTo, ObjectRelTy, ObjectRelatable,
|
||||
ObjectTreeRelTo,
|
||||
};
|
||||
}
|
||||
|
||||
/// Given a list of [`ObjectKind`]s,
|
||||
/// generate [`Object`],
|
||||
/// associated types,
|
||||
|
@ -184,11 +205,44 @@ macro_rules! object_gen {
|
|||
///
|
||||
/// TODO: Encapsulate within `crate::asg` when the graph can be better
|
||||
/// encapsulated.
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
|
||||
#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)]
|
||||
pub enum ObjectTy {
|
||||
$($kind,)+
|
||||
}
|
||||
|
||||
impl ObjectTy {
|
||||
/// Assume that the provided [`ObjectIndex`] is of the type
|
||||
/// associated with `self`,
|
||||
/// and then determine whether that object can be related to
|
||||
/// another object of a given type `OB`.
|
||||
///
|
||||
/// This method should be kept private;
|
||||
/// it is memory safe,
|
||||
/// but incorrect assumptions will violate graph object
|
||||
/// invariants and cause panics when the [`ObjectIndexTo`]
|
||||
/// is later resolved.
|
||||
fn assuming_oi_maybe_rel_to_dyn<OB: ObjectRelatable>(
|
||||
&self,
|
||||
oi: ObjectIndex<Object>,
|
||||
) -> Option<ObjectIndexTo<OB>> {
|
||||
match self {
|
||||
$(
|
||||
Self::$kind => {
|
||||
$kind::oi_rel_to_dyn(oi.must_narrow_into())
|
||||
}
|
||||
)+
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: ObjectInner> From<&Object<T>> for ObjectTy {
|
||||
fn from(obj: &Object<T>) -> Self {
|
||||
match obj {
|
||||
$(Object::$kind(_) => ObjectTy::$kind,)+
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The collection of potential objects of [`Object`].
|
||||
pub trait ObjectInner {
|
||||
$(type $kind;)+
|
||||
|
@ -304,12 +358,16 @@ object_gen! {
|
|||
Ident,
|
||||
|
||||
/// Expression.
|
||||
///
|
||||
/// An expression may optionally be named by one or more [`Ident`]s.
|
||||
Expr,
|
||||
|
||||
/// A template definition.
|
||||
Tpl,
|
||||
|
||||
/// Metasyntactic variable (metavariable).
|
||||
Meta,
|
||||
|
||||
/// Documentation.
|
||||
Doc,
|
||||
}
|
||||
|
||||
impl Object<OnlyObjectInner> {
|
||||
|
@ -320,9 +378,15 @@ impl Object<OnlyObjectInner> {
|
|||
Self::Ident(ident) => ident.span(),
|
||||
Self::Expr(expr) => expr.span(),
|
||||
Self::Tpl(tpl) => tpl.span(),
|
||||
Self::Meta(meta) => meta.span(),
|
||||
Self::Doc(doc) => doc.span(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ty(&self) -> ObjectTy {
|
||||
self.into()
|
||||
}
|
||||
|
||||
/// Retrieve an [`Ident`] reference,
|
||||
/// or [`None`] if the object is not an identifier.
|
||||
pub fn as_ident_ref(&self) -> Option<&Ident> {
|
||||
|
@ -453,8 +517,11 @@ where
|
|||
/// this is intended to be used to provide diagnostic information in the
|
||||
/// event that the object somehow becomes unavailable for later
|
||||
/// operations.
|
||||
/// [`Self::resolve_span`] may be used to replace the inner [`Span`] with
|
||||
/// that of the object that [`Self`] represents,
|
||||
/// which is useful in a different diagnostic context.
|
||||
///
|
||||
/// _The span is not accounted for in [`PartialEq`]_,
|
||||
/// _The span is not accounted for in [`PartialEq`] or [`Hash`]_,
|
||||
/// since it represents the context in which the [`ObjectIndex`] was
|
||||
/// retrieved,
|
||||
/// and the span associated with the underlying [`Object`] may evolve
|
||||
|
@ -506,48 +573,22 @@ impl<O: ObjectKind> ObjectIndex<O> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Add an edge from `self` to `to_oi` on the provided [`Asg`].
|
||||
///
|
||||
/// An edge can only be added if ontologically valid;
|
||||
/// see [`ObjectRelTo`] for more information.
|
||||
///
|
||||
/// Edges may contain _contextual [`Span`]s_ if there is an important
|
||||
/// distinction to be made between a the span of a _reference_ to the
|
||||
/// target and the span of the target itself.
|
||||
/// This is of particular benefit to cross edges
|
||||
/// (see [`ObjectRel::is_cross_edge`]),
|
||||
/// which reference nodes from other trees in different contexts.
|
||||
///
|
||||
/// See also [`Self::add_edge_to`].
|
||||
pub fn add_edge_to<OB: ObjectKind>(
|
||||
self,
|
||||
asg: &mut Asg,
|
||||
to_oi: ObjectIndex<OB>,
|
||||
ctx_span: Option<Span>,
|
||||
) -> Self
|
||||
where
|
||||
O: ObjectRelTo<OB>,
|
||||
{
|
||||
asg.add_edge(self, to_oi, ctx_span);
|
||||
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<OB: ObjectKind>(
|
||||
pub fn add_edge_from<OF: ObjectIndexRelTo<O>>(
|
||||
self,
|
||||
asg: &mut Asg,
|
||||
from_oi: ObjectIndex<OB>,
|
||||
from_oi: OF,
|
||||
ctx_span: Option<Span>,
|
||||
) -> Self
|
||||
where
|
||||
OB: ObjectRelTo<O>,
|
||||
O: ObjectRelatable,
|
||||
{
|
||||
from_oi.add_edge_to(asg, self, ctx_span);
|
||||
asg.add_edge(from_oi, self, ctx_span);
|
||||
self
|
||||
}
|
||||
|
||||
|
@ -612,6 +653,17 @@ impl<O: ObjectKind> ObjectIndex<O> {
|
|||
move |oi| oi.resolve(asg)
|
||||
}
|
||||
|
||||
/// Replace our associated [`Span`] with the span of the resolved
|
||||
/// [`Object`].
|
||||
///
|
||||
/// This is useful to utilize [`ObjectIndex`] in a diagnostic context
|
||||
/// without having to allocate space for additional metadata.
|
||||
pub fn resolve_span(self, asg: &Asg) -> ObjectIndexResolvedSpan<O> {
|
||||
ObjectIndexResolvedSpan(
|
||||
self.overwrite(self.widen().resolve(asg).span()),
|
||||
)
|
||||
}
|
||||
|
||||
/// Resolve the identifier and map over the resulting [`Object`]
|
||||
/// narrowed to [`ObjectKind`] `O`,
|
||||
/// replacing the object on the given [`Asg`].
|
||||
|
@ -676,7 +728,7 @@ impl<O: ObjectKind> ObjectIndex<O> {
|
|||
.filter(|_| O::rel_ty() == OB::rel_ty())
|
||||
}
|
||||
|
||||
/// Root this object in the ASG.
|
||||
/// Root this object in the ASG's [`Root`] object.
|
||||
///
|
||||
/// A rooted object is forced to be reachable.
|
||||
/// This should only be utilized when necessary for toplevel objects;
|
||||
|
@ -692,17 +744,94 @@ impl<O: ObjectKind> ObjectIndex<O> {
|
|||
self
|
||||
}
|
||||
|
||||
/// Whether this object has been rooted in the ASG's [`Root`] object.
|
||||
///
|
||||
/// See [`Self::root`] for more information.
|
||||
pub fn is_rooted(&self, asg: &Asg) -> bool
|
||||
where
|
||||
Root: ObjectRelTo<O>,
|
||||
{
|
||||
asg.root(self.span()).has_edge_to(asg, *self)
|
||||
}
|
||||
|
||||
/// Widen an [`ObjectKind`] `O` into [`Object`],
|
||||
/// generalizing the index type.
|
||||
///
|
||||
/// This generalization is useful in dynamic contexts,
|
||||
/// but it discards type information that must be later re-checked and
|
||||
/// verified.
|
||||
/// See [`Self::widen_dyn_ty`] to retain that type information.
|
||||
pub fn widen(self) -> ObjectIndex<Object> {
|
||||
match self {
|
||||
Self(index, span, _pd) => ObjectIndex::new(index, span),
|
||||
}
|
||||
}
|
||||
|
||||
/// Widen an [`ObjectKind`] `O` into [`Object`] like [`Self::widen`],
|
||||
/// but retain type information for later narrowing at runtime.
|
||||
pub fn widen_dyn_ty(self) -> (ObjectIndex<Object>, ObjectRelTy)
|
||||
where
|
||||
O: ObjectRelatable,
|
||||
{
|
||||
(self.widen(), O::rel_ty())
|
||||
}
|
||||
|
||||
/// Attempt to look up a locally bound [`Ident`] via a linear search of
|
||||
/// `self`'s edges.
|
||||
///
|
||||
/// This should be used only when an index is not available,
|
||||
/// and is currently restricted to tests.
|
||||
///
|
||||
/// See [`ObjectIndexRelTo::lookup_local_linear`].
|
||||
//
|
||||
// TODO: This method exists here only as a fallback when Rust is unable
|
||||
// to infer the proper type for [`ObjectIndexRelTo`].
|
||||
// It can be removed once that is resolved;
|
||||
// delete this method and compile to see.
|
||||
#[cfg(test)]
|
||||
pub fn lookup_local_linear(
|
||||
&self,
|
||||
asg: &Asg,
|
||||
name: SPair,
|
||||
) -> Option<ObjectIndex<Ident>>
|
||||
where
|
||||
O: ObjectRelTo<Ident>,
|
||||
{
|
||||
ObjectIndexRelTo::lookup_local_linear(self, asg, name)
|
||||
}
|
||||
|
||||
/// Retrieve the identifier for this object,
|
||||
/// if any.
|
||||
///
|
||||
/// If there is more than one identifier,
|
||||
/// only one will be returned,
|
||||
/// and the result of the operation is undefined.
|
||||
/// This can be problematic if certain optimizations have been performed
|
||||
/// 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>
|
||||
where
|
||||
O: ObjectRelFrom<Ident>,
|
||||
{
|
||||
self.incoming_edges_filtered(asg)
|
||||
.map(ObjectIndex::cresolve(asg))
|
||||
.next()
|
||||
}
|
||||
|
||||
/// Describe this expression using a short independent clause.
|
||||
///
|
||||
/// This is intended to be a concise description for use either as a
|
||||
/// 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
|
||||
where
|
||||
O: ObjectRelTo<Doc>,
|
||||
{
|
||||
let oi_doc = asg.create(Doc::new_indep_clause(clause));
|
||||
self.add_edge_to(asg, oi_doc, None)
|
||||
}
|
||||
}
|
||||
|
||||
impl ObjectIndex<Object> {
|
||||
|
@ -740,6 +869,14 @@ impl<O: ObjectKind> PartialEq for ObjectIndex<O> {
|
|||
|
||||
impl<O: ObjectKind> Eq for ObjectIndex<O> {}
|
||||
|
||||
impl<O: ObjectKind> Hash for ObjectIndex<O> {
|
||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||
match self {
|
||||
Self(index, _, _) => index.hash(state),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<O: ObjectKind> From<ObjectIndex<O>> for NodeIndex {
|
||||
fn from(objref: ObjectIndex<O>) -> Self {
|
||||
match objref {
|
||||
|
@ -748,6 +885,12 @@ impl<O: ObjectKind> From<ObjectIndex<O>> for NodeIndex {
|
|||
}
|
||||
}
|
||||
|
||||
impl<O: ObjectKind> From<ObjectIndex<O>> for usize {
|
||||
fn from(value: ObjectIndex<O>) -> Self {
|
||||
Into::<NodeIndex>::into(value).index()
|
||||
}
|
||||
}
|
||||
|
||||
impl<O: ObjectKind> From<ObjectIndex<O>> for Span {
|
||||
fn from(value: ObjectIndex<O>) -> Self {
|
||||
match value {
|
||||
|
@ -756,6 +899,47 @@ impl<O: ObjectKind> From<ObjectIndex<O>> for Span {
|
|||
}
|
||||
}
|
||||
|
||||
/// An [`ObjectKind`] whose associated [`Span`] has been resolved.
|
||||
///
|
||||
/// This exists simply to provide proof via the type system that resolution
|
||||
/// occurred.
|
||||
///
|
||||
/// See [`ObjectIndex::resolve_span`].
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub struct ObjectIndexResolvedSpan<O: ObjectKind>(ObjectIndex<O>);
|
||||
|
||||
impl<O: ObjectKind> ObjectIndexResolvedSpan<O> {
|
||||
pub fn oi(&self) -> ObjectIndex<O> {
|
||||
(*self).into()
|
||||
}
|
||||
|
||||
pub fn span(&self) -> Span {
|
||||
self.oi().span()
|
||||
}
|
||||
}
|
||||
|
||||
impl<O: ObjectKind> From<ObjectIndexResolvedSpan<O>> for ObjectIndex<O> {
|
||||
fn from(value: ObjectIndexResolvedSpan<O>) -> Self {
|
||||
match value {
|
||||
ObjectIndexResolvedSpan(oi) => oi,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<O: ObjectKind> From<ObjectIndexResolvedSpan<O>> for Span {
|
||||
fn from(value: ObjectIndexResolvedSpan<O>) -> Self {
|
||||
ObjectIndex::from(value).span()
|
||||
}
|
||||
}
|
||||
|
||||
impl<O: ObjectKind> Clone for ObjectIndexResolvedSpan<O> {
|
||||
fn clone(&self) -> Self {
|
||||
Self(self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl<O: ObjectKind> Copy for ObjectIndexResolvedSpan<O> {}
|
||||
|
||||
/// A container for an [`Object`] allowing for owned borrowing of data.
|
||||
///
|
||||
/// The purpose of allowing this owned borrowing is to permit a functional
|
||||
|
|
|
@ -0,0 +1,93 @@
|
|||
// Documentation represented on the ASG
|
||||
//
|
||||
// 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/>.
|
||||
|
||||
//! Documentation on the ASG.
|
||||
//!
|
||||
//! TODO: Document TAME's stance on documentation and literate programming,
|
||||
//! much of which hasn't been able to be realized over the years.
|
||||
|
||||
use super::prelude::*;
|
||||
use crate::{
|
||||
parse::{util::SPair, Token},
|
||||
span::Span,
|
||||
};
|
||||
use std::fmt::Display;
|
||||
|
||||
/// Documentation string.
|
||||
///
|
||||
/// TODO: This presently serves as a subject line,
|
||||
/// e.g. a description or label,
|
||||
/// but will evolve in the future.
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub enum Doc {
|
||||
/// An (ideally) concise independent clause describing an object.
|
||||
IndepClause(SPair),
|
||||
|
||||
/// Arbitrary text serving as documentation for sibling objects in a
|
||||
/// literate style.
|
||||
///
|
||||
/// TAMER does not presently ascribe any semantic meaning to this text,
|
||||
/// and it may even be entirely whitespace.
|
||||
/// There are plans to improve upon this in the future.
|
||||
///
|
||||
/// The intent is for this text to be mixed with sibling objects,
|
||||
/// in a style similar to that of literate programming.
|
||||
Text(SPair),
|
||||
}
|
||||
|
||||
impl Display for Doc {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
write!(f, "documentation string")
|
||||
}
|
||||
}
|
||||
|
||||
impl Doc {
|
||||
/// Document an object using what is ideally a concise independent
|
||||
/// clause.
|
||||
pub fn new_indep_clause(clause: SPair) -> Self {
|
||||
Self::IndepClause(clause)
|
||||
}
|
||||
|
||||
/// Arbitrary text serving as documentation for sibling objects in a
|
||||
/// literate style.
|
||||
pub fn new_text(text: SPair) -> Self {
|
||||
Self::Text(text)
|
||||
}
|
||||
|
||||
pub fn indep_clause(&self) -> Option<SPair> {
|
||||
match self {
|
||||
Self::IndepClause(spair) => Some(*spair),
|
||||
Self::Text(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn span(&self) -> Span {
|
||||
match self {
|
||||
Self::IndepClause(spair) | Self::Text(spair) => spair.span(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object_rel! {
|
||||
/// Templates may expand into nearly any context,
|
||||
/// and must therefore be able to contain just about anything.
|
||||
Doc -> {
|
||||
// empty
|
||||
}
|
||||
}
|
|
@ -19,19 +19,10 @@
|
|||
|
||||
//! Expressions on the ASG.
|
||||
|
||||
use super::{prelude::*, Doc, Ident, Tpl};
|
||||
use crate::{f::Functor, num::Dim, span::Span};
|
||||
use std::fmt::Display;
|
||||
|
||||
use super::{
|
||||
Asg, Ident, Object, ObjectIndex, ObjectRel, ObjectRelFrom, ObjectRelTy,
|
||||
ObjectRelatable,
|
||||
};
|
||||
use crate::{
|
||||
f::Functor,
|
||||
num::Dim,
|
||||
parse::{util::SPair, Token},
|
||||
span::Span,
|
||||
};
|
||||
|
||||
#[cfg(doc)]
|
||||
use super::ObjectKind;
|
||||
|
||||
|
@ -41,7 +32,7 @@ use super::ObjectKind;
|
|||
/// all child expressions,
|
||||
/// but also any applicable closing span.
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub struct Expr(ExprOp, ExprDim, Span);
|
||||
pub struct Expr(pub ExprOp, ExprDim, Span);
|
||||
|
||||
impl Expr {
|
||||
pub fn new(op: ExprOp, span: Span) -> Self {
|
||||
|
@ -102,12 +93,15 @@ pub enum ExprOp {
|
|||
Conj,
|
||||
/// Logical disjunction (∨)
|
||||
Disj,
|
||||
/// Equality predicate (=)
|
||||
Eq,
|
||||
}
|
||||
|
||||
impl Display for ExprOp {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
use ExprOp::*;
|
||||
|
||||
// "{self} expression"
|
||||
match self {
|
||||
Sum => write!(f, "sum (+)"),
|
||||
Product => write!(f, "product (×)"),
|
||||
|
@ -115,6 +109,7 @@ impl Display for ExprOp {
|
|||
Floor => write!(f, "floor (⌊)"),
|
||||
Conj => write!(f, "conjunctive (∧)"),
|
||||
Disj => write!(f, "disjunctive (∨)"),
|
||||
Eq => write!(f, "equality (=)"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -221,6 +216,10 @@ object_rel! {
|
|||
Expr -> {
|
||||
cross Ident,
|
||||
tree Expr,
|
||||
tree Doc,
|
||||
|
||||
// Template application
|
||||
tree Tpl,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -239,22 +238,9 @@ impl ObjectIndex<Expr> {
|
|||
oi_subexpr.add_edge_from(asg, self, None)
|
||||
}
|
||||
|
||||
/// Reference the value of the expression identified by `ident` as if it
|
||||
/// were a subexpression.
|
||||
///
|
||||
/// If `ident` does not yet exist on the graph,
|
||||
/// a missing identifier will take its place to be later resolved once
|
||||
/// it becomes available.
|
||||
pub fn ref_expr(self, asg: &mut Asg, ident: SPair) -> Self {
|
||||
let identi = asg.lookup_or_missing(ident);
|
||||
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()
|
||||
/// 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 {
|
||||
self.add_edge_to(asg, oi_ident, Some(oi_ident.span()))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,11 +19,7 @@
|
|||
|
||||
//! Identifiers (a type of [object](super)).
|
||||
|
||||
use super::{
|
||||
super::{Asg, AsgError, ObjectIndex, ObjectKind},
|
||||
Expr, Object, ObjectRel, ObjectRelFrom, ObjectRelTo, ObjectRelTy,
|
||||
ObjectRelatable, Pkg, Tpl,
|
||||
};
|
||||
use super::{prelude::*, Expr, Meta, Pkg, Tpl};
|
||||
use crate::{
|
||||
diagnose::{Annotate, Diagnostic},
|
||||
diagnostic_todo,
|
||||
|
@ -978,16 +974,81 @@ object_rel! {
|
|||
/// Opaque identifiers at the time of writing are used by the linker
|
||||
/// which does not reason about cross edges
|
||||
/// (again at the time of writing).
|
||||
/// Consequently,
|
||||
/// this will always return [`false`].
|
||||
///
|
||||
/// Identifiers representing functions are able to produce cycles,
|
||||
/// representing recursion.
|
||||
/// This is a legacy feature expected to be removed in the future;
|
||||
/// see [`ObjectRel::can_recurse`] for more information.
|
||||
Ident -> {
|
||||
tree Ident,
|
||||
tree Expr,
|
||||
tree Tpl,
|
||||
}
|
||||
tree Meta,
|
||||
} can_recurse(ident) if matches!(ident.kind(), Some(IdentKind::Func(..)))
|
||||
}
|
||||
|
||||
impl ObjectIndex<Ident> {
|
||||
/// Declare a concrete identifier.
|
||||
///
|
||||
/// An identifier declaration is similar to a declaration in a header
|
||||
/// file in a language like C,
|
||||
/// describing the structure of the identifier.
|
||||
/// Once declared,
|
||||
/// this information cannot be changed.
|
||||
///
|
||||
/// Identifiers are uniquely identified by a [`SPair`] `name`.
|
||||
/// If an identifier of the same `name` already exists,
|
||||
/// then the provided declaration is compared against the existing
|
||||
/// declaration---should
|
||||
/// they be incompatible,
|
||||
/// then the operation will fail;
|
||||
/// otherwise,
|
||||
/// the existing identifier will be returned.
|
||||
///
|
||||
/// If a concrete identifier has already been declared,
|
||||
/// then extern declarations will be compared and,
|
||||
/// if compatible,
|
||||
/// the identifier will be immediately _resolved_ and the object
|
||||
/// on the graph will not be altered.
|
||||
/// Resolution will otherwise fail in error.
|
||||
///
|
||||
/// For more information on state transitions that can occur when
|
||||
/// redeclaring an identifier that already exists,
|
||||
/// see [`Ident::resolve`].
|
||||
///
|
||||
/// A successful declaration will add an identifier to the graph
|
||||
/// and return an [`ObjectIndex`] reference.
|
||||
pub fn declare(
|
||||
self,
|
||||
asg: &mut Asg,
|
||||
name: SPair,
|
||||
kind: IdentKind,
|
||||
src: Source,
|
||||
) -> Result<ObjectIndex<Ident>, AsgError> {
|
||||
let is_auto_root = kind.is_auto_root();
|
||||
|
||||
self.try_map_obj(asg, |obj| obj.resolve(name.span(), kind, src))
|
||||
.map_err(Into::into)
|
||||
.map(|ident| {
|
||||
is_auto_root.then(|| self.root(asg));
|
||||
ident
|
||||
})
|
||||
}
|
||||
|
||||
/// Declare an abstract identifier.
|
||||
///
|
||||
/// See [`Ident::extern_`] and [`Ident::resolve`] for more information.
|
||||
pub fn declare_extern(
|
||||
self,
|
||||
asg: &mut Asg,
|
||||
name: SPair,
|
||||
kind: IdentKind,
|
||||
src: Source,
|
||||
) -> Result<Self, AsgError> {
|
||||
self.try_map_obj(asg, |obj| obj.extern_(name.span(), kind, src))
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Bind an identifier to a `definition`,
|
||||
/// making it [`Transparent`].
|
||||
///
|
||||
|
@ -1047,6 +1108,31 @@ impl ObjectIndex<Ident> {
|
|||
.map(|ident_oi| ident_oi.add_edge_to(asg, definition, None))
|
||||
}
|
||||
|
||||
/// Set the fragment associated with a concrete identifier.
|
||||
///
|
||||
/// Fragments are intended for use by the [linker][crate::ld].
|
||||
/// For more information,
|
||||
/// see [`Ident::set_fragment`].
|
||||
pub fn set_fragment(
|
||||
self,
|
||||
asg: &mut Asg,
|
||||
text: SymbolId,
|
||||
) -> Result<Self, AsgError> {
|
||||
self.try_map_obj(asg, |obj| obj.set_fragment(text))
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Look up the definition that this identifier binds to,
|
||||
/// if any.
|
||||
///
|
||||
/// See [`Self::bind_definition`].
|
||||
pub fn definition<O: ObjectRelFrom<Ident> + ObjectRelatable>(
|
||||
&self,
|
||||
asg: &Asg,
|
||||
) -> Option<ObjectIndex<O>> {
|
||||
self.edges(asg).find_map(ObjectRel::narrow)
|
||||
}
|
||||
|
||||
/// Whether this identifier is bound to the object represented by `oi`.
|
||||
///
|
||||
/// To bind an identifier,
|
||||
|
@ -1059,11 +1145,28 @@ impl ObjectIndex<Ident> {
|
|||
self.edges(asg).find_map(ObjectRel::narrow) == Some(oi)
|
||||
}
|
||||
|
||||
/// Whether this identifier is bound to an object of kind `O`.
|
||||
///
|
||||
/// To bind an identifier,
|
||||
/// see [`Self::bind_definition`].
|
||||
pub fn is_bound_to_kind<O: ObjectRelFrom<Ident>>(&self, asg: &Asg) -> bool {
|
||||
self.edges_filtered::<O>(asg).next().is_some()
|
||||
}
|
||||
|
||||
/// The source package defining this identifier,
|
||||
/// if known.
|
||||
pub fn src_pkg(&self, asg: &Asg) -> Option<ObjectIndex<Pkg>> {
|
||||
self.incoming_edges_filtered(asg).next()
|
||||
}
|
||||
|
||||
/// Declare that `oi_dep` is an opaque dependency of `self`.
|
||||
pub fn add_opaque_dep(
|
||||
&self,
|
||||
asg: &mut Asg,
|
||||
oi_dep: ObjectIndex<Ident>,
|
||||
) -> Self {
|
||||
self.add_edge_to(asg, oi_dep, None)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
|
@ -0,0 +1,151 @@
|
|||
// Metasyntactic variables represented on the ASG
|
||||
//
|
||||
// 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/>.
|
||||
|
||||
//! Metasyntactic variables on the ASG.
|
||||
//!
|
||||
//! Metasyntactic variables
|
||||
//! (sometimes called "metavariables" herein for short)
|
||||
//! have historically been a feature of the template system.
|
||||
//! The canonical metavariable is the template parameter.
|
||||
|
||||
use super::{prelude::*, Ident};
|
||||
use crate::{
|
||||
diagnose::Annotate,
|
||||
diagnostic_todo,
|
||||
f::Functor,
|
||||
fmt::{DisplayWrapper, TtQuote},
|
||||
parse::util::SPair,
|
||||
span::Span,
|
||||
};
|
||||
use std::fmt::Display;
|
||||
|
||||
/// Metasyntactic variable (metavariable).
|
||||
///
|
||||
/// A metavariable is a lexical construct.
|
||||
/// Its value is a lexeme that represents an [`Ident`],
|
||||
/// whose meaning depends on the context in which the metavariable is
|
||||
/// referenced.
|
||||
/// Its lexeme may be composed of multiple [`Self::Lexeme`]s,
|
||||
/// and may even be constructed dynamically based on the values of other
|
||||
/// [`Meta`]s.
|
||||
///
|
||||
/// Metavariables are identified by being bound by an [`Ident`];
|
||||
/// the symbol representing that identifier then acts as a metavariable.
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub enum Meta {
|
||||
Required(Span),
|
||||
ConcatList(Span),
|
||||
Lexeme(Span, SPair),
|
||||
}
|
||||
|
||||
impl Meta {
|
||||
/// Create a new metavariable without a value.
|
||||
///
|
||||
/// Metavariables with no value cannot be used in an expansion context.
|
||||
/// Intuitively,
|
||||
/// they act as required parameters.
|
||||
pub fn new_required(span: Span) -> Self {
|
||||
Self::Required(span)
|
||||
}
|
||||
|
||||
pub fn span(&self) -> Span {
|
||||
match self {
|
||||
Self::Required(span)
|
||||
| Self::ConcatList(span)
|
||||
| Self::Lexeme(span, _) => *span,
|
||||
}
|
||||
}
|
||||
|
||||
/// Assign a lexeme to a metavariable.
|
||||
///
|
||||
/// In a template definition context,
|
||||
/// this acts as a default value for this metavariable.
|
||||
/// In an application context,
|
||||
/// this has the effect of binding a value to this metavariable.
|
||||
pub fn assign_lexeme(self, lexeme: SPair) -> Self {
|
||||
match self {
|
||||
Self::Required(span) => Self::Lexeme(span, lexeme),
|
||||
|
||||
Self::ConcatList(_) => diagnostic_todo!(
|
||||
vec![lexeme.note("while parsing this lexeme")],
|
||||
"append to ConcatList",
|
||||
),
|
||||
|
||||
Self::Lexeme(_, _) => diagnostic_todo!(
|
||||
vec![lexeme.note("while parsing this lexeme")],
|
||||
"Lexeme => ConcatList",
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&Meta> for Span {
|
||||
fn from(meta: &Meta) -> Self {
|
||||
meta.span()
|
||||
}
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
Self::ConcatList(_) => {
|
||||
write!(f, "metasyntactic concatenation list")
|
||||
}
|
||||
Self::Lexeme(_, spair) => {
|
||||
write!(f, "lexeme {}", TtQuote::wrap(spair))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Functor<Span> for Meta {
|
||||
fn map(self, f: impl FnOnce(Span) -> Span) -> Self::Target {
|
||||
match self {
|
||||
Self::Required(span) => Self::Required(f(span)),
|
||||
Self::ConcatList(span) => Self::ConcatList(f(span)),
|
||||
Self::Lexeme(span, spair) => Self::Lexeme(f(span), spair),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object_rel! {
|
||||
/// Metavariables contain lexical data and references to other
|
||||
/// metavariables.
|
||||
Meta -> {
|
||||
tree Meta, // TODO: do we need tree?
|
||||
cross Ident,
|
||||
}
|
||||
}
|
||||
|
||||
impl ObjectIndex<Meta> {
|
||||
pub fn assign_lexeme(self, asg: &mut Asg, lexeme: SPair) -> Self {
|
||||
self.map_obj(asg, |meta| meta.assign_lexeme(lexeme))
|
||||
}
|
||||
|
||||
pub fn close(self, asg: &mut Asg, close_span: Span) -> Self {
|
||||
self.map_obj(asg, |meta| {
|
||||
meta.map(|open_span| {
|
||||
open_span.merge(close_span).unwrap_or(open_span)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
|
@ -19,41 +19,78 @@
|
|||
|
||||
//! Package object on the ASG.
|
||||
|
||||
use super::{
|
||||
Ident, Object, ObjectIndex, ObjectRel, ObjectRelFrom, ObjectRelTy,
|
||||
ObjectRelatable,
|
||||
use super::{prelude::*, Doc, Ident, Tpl};
|
||||
use crate::{
|
||||
f::Functor,
|
||||
fmt::{DisplayWrapper, TtQuote},
|
||||
parse::{util::SPair, Token},
|
||||
span::Span,
|
||||
};
|
||||
use crate::{asg::Asg, f::Functor, span::Span};
|
||||
use std::fmt::Display;
|
||||
|
||||
#[cfg(doc)]
|
||||
use super::ObjectKind;
|
||||
|
||||
mod name;
|
||||
|
||||
pub use name::{CanonicalName, CanonicalNameError};
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub struct Pkg(Span);
|
||||
pub struct Pkg(Span, CanonicalName);
|
||||
|
||||
impl Pkg {
|
||||
pub fn new<S: Into<Span>>(span: S) -> Self {
|
||||
Self(span.into())
|
||||
/// Create a new package with a canonicalized name.
|
||||
///
|
||||
/// A canonical package name is a path relative to the project root.
|
||||
pub(in crate::asg) fn new_canonical<S: Into<Span>>(
|
||||
start: S,
|
||||
name: SPair,
|
||||
) -> Result<Self, AsgError> {
|
||||
Ok(Self(start.into(), CanonicalName::from_canonical(name)?))
|
||||
}
|
||||
|
||||
/// Import a package with a namespec canonicalized against a reference
|
||||
/// parent package.
|
||||
///
|
||||
/// The parent package should be the package containing the namespec.
|
||||
pub fn new_imported(
|
||||
Pkg(_, parent_name): &Pkg,
|
||||
namespec: SPair,
|
||||
) -> Result<Self, AsgError> {
|
||||
Ok(Self(
|
||||
namespec.span(),
|
||||
CanonicalName::resolve_namespec_rel(parent_name, namespec)?,
|
||||
))
|
||||
}
|
||||
|
||||
pub fn span(&self) -> Span {
|
||||
match self {
|
||||
Self(span) => *span,
|
||||
Self(span, _) => *span,
|
||||
}
|
||||
}
|
||||
|
||||
/// The canonical name for this package assigned by
|
||||
/// [`Self::new_canonical`],
|
||||
/// if any.
|
||||
pub fn canonical_name(&self) -> SPair {
|
||||
match self {
|
||||
Self(_, name) => (*name).into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Pkg {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
write!(f, "package")
|
||||
match self {
|
||||
Self(_, name) => write!(f, "package {}", TtQuote::wrap(name)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Functor<Span> for Pkg {
|
||||
fn map(self, f: impl FnOnce(Span) -> Span) -> Self::Target {
|
||||
match self {
|
||||
Self(span) => Self(f(span)),
|
||||
Self(span, path) => Self(f(span), path),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -64,18 +101,45 @@ object_rel! {
|
|||
///
|
||||
/// Imported [`Ident`]s do not have edges from this package.
|
||||
Pkg -> {
|
||||
// Package import
|
||||
cross Pkg,
|
||||
|
||||
// Identified objects owned by this package.
|
||||
tree Ident,
|
||||
|
||||
// Anonymous templates are used for expansion.
|
||||
tree Tpl,
|
||||
|
||||
// Arbitrary blocks of text serving as documentation.
|
||||
tree Doc,
|
||||
}
|
||||
}
|
||||
|
||||
impl ObjectIndex<Pkg> {
|
||||
/// Indicate that the given identifier `oi` is defined in this package.
|
||||
pub fn defines(self, asg: &mut Asg, oi: ObjectIndex<Ident>) -> Self {
|
||||
self.add_edge_to(asg, oi, None)
|
||||
}
|
||||
|
||||
/// Complete the definition of a package.
|
||||
pub fn close(self, asg: &mut Asg, span: Span) -> Self {
|
||||
self.map_obj(asg, Pkg::fmap(|open| open.merge(span).unwrap_or(open)))
|
||||
}
|
||||
|
||||
/// Indicate that a package should be imported at the provided
|
||||
/// pathspec.
|
||||
///
|
||||
/// This simply adds the import to the graph;
|
||||
/// package loading must be performed by another subsystem.
|
||||
pub fn import(
|
||||
self,
|
||||
asg: &mut Asg,
|
||||
namespec: SPair,
|
||||
) -> Result<Self, AsgError> {
|
||||
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())))
|
||||
}
|
||||
|
||||
/// Arbitrary text serving as documentation in a literate style.
|
||||
pub fn append_doc_text(&self, asg: &mut Asg, text: SPair) -> Self {
|
||||
let oi_doc = asg.create(Doc::new_text(text));
|
||||
self.add_edge_to(asg, oi_doc, None)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,858 @@
|
|||
// Package name
|
||||
//
|
||||
// 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/>.
|
||||
|
||||
//! Package name and canonicalization.
|
||||
//!
|
||||
//! See [`CanonicalName`] for more information.
|
||||
|
||||
use crate::{
|
||||
diagnose::{Annotate, AnnotatedSpan, Diagnostic},
|
||||
fmt::{DisplayWrapper, TtQuote},
|
||||
parse::{util::SPair, Token},
|
||||
span::Span,
|
||||
sym::{
|
||||
st::raw::{FW_SLASH, FW_SLASH_DOT},
|
||||
GlobalSymbolIntern, GlobalSymbolInternBytes, GlobalSymbolResolve,
|
||||
},
|
||||
};
|
||||
use std::{error::Error, fmt::Display};
|
||||
|
||||
/// Canonical package name.
|
||||
///
|
||||
/// A _canonical name_ is a package name that serves as a unique
|
||||
/// representation of a package---that is,
|
||||
/// it should not be possible to identify the same package by more than
|
||||
/// one [`CanonicalName`].
|
||||
/// In practice,
|
||||
/// this means that the name begins with a forward slash;
|
||||
/// contains no `..` or `.` as path-like components;
|
||||
/// does not contain `//`;
|
||||
/// and does not end in a trailing slash.
|
||||
/// A canonical name is represented by [`CanonicalName`].
|
||||
///
|
||||
/// By representing package names in this way,
|
||||
/// we are able to index them uniquely in any context in which they may
|
||||
/// appear,
|
||||
/// ensuring that we are able to uphold the commutative
|
||||
/// definition/reference requirements of TAME.
|
||||
///
|
||||
/// Package names may develop further in the future;
|
||||
/// for now,
|
||||
/// their names are somewhat informal and usually generated from the
|
||||
/// path on the filesystem relative to the project root.
|
||||
///
|
||||
/// Namespecs
|
||||
/// =========
|
||||
/// A _namespec_ is intended for package imports,
|
||||
/// and represents a transformation that can be applied to a parent
|
||||
/// package to produce the name of a package to import.
|
||||
///
|
||||
/// Namespecs (and package names) are designed to _look_ like paths,
|
||||
/// and are indeed derived from paths on the filesystem,
|
||||
/// but TAMER does not produce any hierarchy from that appearance.
|
||||
/// Namespec resolution is a _purely lexical_ operation that is entirely
|
||||
/// distinct from the operating system,
|
||||
/// and the resulting name will be used to resolve package lookups
|
||||
/// _relative to the project root_.[^symlink]
|
||||
///
|
||||
/// That is:
|
||||
/// the purpose of a namespec is that of _convenience and
|
||||
/// maintainability_;
|
||||
/// it serves as an alternative to always providing a canonical name as
|
||||
/// an import.
|
||||
///
|
||||
/// - If a namespec contains a leading slash,
|
||||
/// then it is _absolute_ and will used verbatim as a
|
||||
/// [`CanonicalName`].
|
||||
/// All canonical names are therefore absolute namespecs,
|
||||
/// providing unambiguous context-free package names.
|
||||
///
|
||||
/// - Otherwise, a namespec is _relative_ and must be resolved against a
|
||||
/// parent [`CanonicalName`].
|
||||
/// First,
|
||||
/// the final component of the parent name
|
||||
/// (the last slash and everything that follows)
|
||||
/// is removed,
|
||||
/// which intuitively matches the behavior of resolving against
|
||||
/// sibling packages in the same directory.
|
||||
///
|
||||
/// - If the namespec contains any number of leading parent
|
||||
/// markers `../`,
|
||||
/// they will strip off a portion of [`CanonicalName`] up to and
|
||||
/// including the last `/`,
|
||||
/// recursively.
|
||||
/// So `../quux` applied to `/bar/baz` resolves to `/quux`,
|
||||
/// like a relative path on a Unix-like filesystem.
|
||||
/// (As previously stated,
|
||||
/// `/bar/baz` first becomes `/bar` so that the relative path
|
||||
/// resolves as a sibling.)
|
||||
///
|
||||
/// - Otherwise,
|
||||
/// the relative name is simply concatenated with the remaining
|
||||
/// parent [`CanonicalName`],
|
||||
/// separated by a `/`.
|
||||
///
|
||||
/// A namespec cannot use more parent markers than there are `/`s in the
|
||||
/// parent [`CanonicalName`].
|
||||
/// When viewing a name as a path,
|
||||
/// this means that a namespec cannot traverse outside of the project
|
||||
/// root.
|
||||
///
|
||||
/// A namespec cannot contain `.` in a context that would be interpreted by
|
||||
/// a filesystem to mean the current directory,
|
||||
/// since this would allow for multiple [`CanonicalName`]s for a given
|
||||
/// package,
|
||||
/// which is then not canonical.
|
||||
/// Similarly,
|
||||
/// a [`CanonicalName`] cannot contain `//`.
|
||||
///
|
||||
/// [^symlink]: This distinction is important.
|
||||
/// Since a namespec always produces a [`CanonicalName`] which is treated
|
||||
/// as a path relative to the project root,
|
||||
/// paths will act intuitively despite symlinks,
|
||||
/// which may otherwise exhibit unintuitive behavior when using `../`.
|
||||
///
|
||||
/// [`Span`] Characteristics
|
||||
/// =========================
|
||||
/// At the time of writing,
|
||||
/// TAMER does not yet have an abstraction that indicates whether a given
|
||||
/// [`SPair`] is an original lexeme tied to its original [`Span`].
|
||||
/// It is assumed that [`CanonicalName`] is only ever constructed from
|
||||
/// an [`SPair`] with its original [`Span`],
|
||||
/// either via [`Self::from_canonical`] or
|
||||
/// [`Self::resolve_namespec_rel`].
|
||||
///
|
||||
/// When a [`CanonicalNameError`] is raised,
|
||||
/// its spans will only ever be derived from what it perceives as an
|
||||
/// original [`Span`].
|
||||
///
|
||||
/// A [`CanonicalName`] may be constructed from a relative namespec via
|
||||
/// [`Self::resolve_namespec_rel`].
|
||||
/// In that case,
|
||||
/// the span of the relative namespec is retained in the resulting
|
||||
/// [`CanonicalName`].
|
||||
/// Since a namespec can only contain parent markers (`..`) at the _head_ of
|
||||
/// the namespec,
|
||||
/// it will always be the case that the tail of a [`CanonicalName`] has
|
||||
/// a proper span.
|
||||
/// As long as we only slice up the span as far as we know is valid given
|
||||
/// its construction,
|
||||
/// we can return a [`Span`] in errors that is constructed from the tail
|
||||
/// of the would-be [`CanonicalName`].
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
|
||||
pub struct CanonicalName(SPair);
|
||||
|
||||
impl CanonicalName {
|
||||
/// Verify that the provided `name` is in canonical form,
|
||||
/// otherwise produce an error.
|
||||
///
|
||||
/// For more information on canonical names,
|
||||
/// see [`Self`].
|
||||
pub fn from_canonical(name: SPair) -> Result<Self, CanonicalNameError> {
|
||||
let s = name.symbol().lookup_str();
|
||||
|
||||
// TODO: We shouldn't slice up a string and span separately,
|
||||
// since the two may not be directly associated
|
||||
// (e.g. the symbol may not be a lexeme matching the associated
|
||||
// span if it's generated).
|
||||
// See the section "Span Characteristics" above.
|
||||
if s.ends_with('/') {
|
||||
Err(CanonicalNameError::TrailingSlash(
|
||||
name,
|
||||
name.span().slice_tail(1),
|
||||
))
|
||||
} else if s.starts_with('/') {
|
||||
Ok(Self(validate_components(name, s)?))
|
||||
} else {
|
||||
Err(CanonicalNameError::LeadingSlashExpected(
|
||||
name,
|
||||
// Slash should be inserted _just before_ the beginning of
|
||||
// the span.
|
||||
// This also works with zero-length spans.
|
||||
name.span().slice_head(0),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolve a namespec relative to this canonical name.
|
||||
///
|
||||
/// For more information on relative paths,
|
||||
/// see [`Self`].
|
||||
pub fn resolve_namespec_rel(
|
||||
&self,
|
||||
namespec: SPair,
|
||||
) -> Result<Self, CanonicalNameError> {
|
||||
let Self(parent) = self;
|
||||
|
||||
let s = namespec.symbol().lookup_str();
|
||||
|
||||
if s.starts_with('/') {
|
||||
Self::from_canonical(namespec)
|
||||
} else {
|
||||
let mut parent_parts = parent.symbol().lookup_str().rsplit('/');
|
||||
let mut rel_parts = s.split('/').peekable();
|
||||
|
||||
let mut rm_bytes = 0;
|
||||
|
||||
// The rightmost component of the parent is eliminated,
|
||||
// in much the same way that you'd expect a relative path to
|
||||
// be a sibling of any current file.
|
||||
parent_parts.next();
|
||||
|
||||
while rel_parts.peek() == Some(&"..") {
|
||||
rel_parts.next();
|
||||
|
||||
let _ = parent_parts.next().filter(|s| s != &"").ok_or_else(
|
||||
|| {
|
||||
CanonicalNameError::TooManyParentMarkers(
|
||||
namespec,
|
||||
namespec.span().slice(rm_bytes, 2),
|
||||
*self,
|
||||
)
|
||||
},
|
||||
)?;
|
||||
|
||||
rm_bytes += 3; // "../"
|
||||
// ^ this is a lie if we've ended `rel_parts`
|
||||
}
|
||||
|
||||
let mut new = String::from(parent_parts.remainder().unwrap_or(""));
|
||||
|
||||
if rel_parts.peek().is_some() {
|
||||
new.push('/');
|
||||
rel_parts.intersperse("/").collect_into(&mut new);
|
||||
}
|
||||
|
||||
// If we're empty,
|
||||
// it's more intuitive to indicate that we went too far
|
||||
// relative to the parent rather than throwing an error about
|
||||
// an invalid canonical name.
|
||||
if new.is_empty() {
|
||||
Err(CanonicalNameError::TooManyParentMarkers(
|
||||
namespec,
|
||||
namespec.span().slice(rm_bytes - 3, 2),
|
||||
*self,
|
||||
))
|
||||
} else {
|
||||
Self::from_canonical(SPair(new.intern(), namespec.span()))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Produce an error if the provided name contains a parent marker `..` or
|
||||
/// `.` in any of its components
|
||||
/// (strings between `/`).
|
||||
///
|
||||
/// Conceptually,
|
||||
/// this catches the equivalent of current and parent paths components,
|
||||
/// such as `foo/../bar` and `./foo/bar`.
|
||||
///
|
||||
/// This will iterate through the entire name,
|
||||
/// which is unnecessary work if we came from
|
||||
/// [`CanonicalName::resolve_namespec_rel`],
|
||||
/// but it's not much work and it's a safer implementation with fewer
|
||||
/// conditions.
|
||||
///
|
||||
/// The purpose of this function is to provide guidance and rationale to the
|
||||
/// user,
|
||||
/// so it checks for very specific cases even if something more general
|
||||
/// could have sufficed.
|
||||
/// This produces very specific spans,
|
||||
/// which also makes the scanning more challenging.
|
||||
fn validate_components(
|
||||
name: SPair,
|
||||
s: &str,
|
||||
) -> Result<SPair, CanonicalNameError> {
|
||||
let bytes = s.as_bytes();
|
||||
let len = bytes.len();
|
||||
|
||||
// Was not worth introducing a regex library,
|
||||
// but might be worth replacing this if we've since introduced it.
|
||||
// Equivalent to `/..(/|$)|/.(/$)|//`,
|
||||
// but most of the code is for span slicing and error construction.
|
||||
bytes
|
||||
.windows(2) // A canonical path will always be at least 2 bytes
|
||||
.enumerate()
|
||||
// Again, be mindful of "Span Characteristics" above.
|
||||
.find_map(|(pos, w)| match (w, bytes.get(pos + 2)) {
|
||||
// `..`
|
||||
(b"/.", Some(b'.'))
|
||||
if matches!(bytes.get(pos + 3), Some(b'/') | None,) =>
|
||||
{
|
||||
Some(Err::<(), _>(CanonicalNameError::NonLeadingParentMarker(
|
||||
name,
|
||||
name.span().rslice(len - pos - 1, 2),
|
||||
)))
|
||||
}
|
||||
// `.`
|
||||
(b"/.", la @ (Some(b'/') | None)) => {
|
||||
let roff = la.is_some() as usize;
|
||||
Some(Err(CanonicalNameError::UselessComponent(
|
||||
name,
|
||||
SPair(
|
||||
// ;_;
|
||||
bytes[pos + roff..pos + roff + 2]
|
||||
.intern_utf8()
|
||||
.unwrap_or(FW_SLASH_DOT),
|
||||
name.span().rslice(len - pos - roff, 2),
|
||||
),
|
||||
)))
|
||||
}
|
||||
(b"//", _) => Some(Err(CanonicalNameError::UselessComponent(
|
||||
name,
|
||||
SPair(FW_SLASH, name.span().rslice(len - pos - 1, 1)),
|
||||
))),
|
||||
_ => None,
|
||||
})
|
||||
.transpose()
|
||||
.map(|_| name)
|
||||
}
|
||||
|
||||
impl Display for CanonicalName {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
match self {
|
||||
Self(name) => Display::fmt(name, f),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<CanonicalName> for SPair {
|
||||
fn from(value: CanonicalName) -> Self {
|
||||
match value {
|
||||
CanonicalName(spair) => spair,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<CanonicalName> for Span {
|
||||
fn from(value: CanonicalName) -> Self {
|
||||
match value {
|
||||
CanonicalName(spair) => spair.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum CanonicalNameError {
|
||||
/// Attempted to create a new [`CanonicalName`] using a symbol that was
|
||||
/// expected to already be in canonical form,
|
||||
/// but was missing a leading slash.
|
||||
///
|
||||
/// The latter span is the position at which a leading slash was
|
||||
/// expected.
|
||||
LeadingSlashExpected(SPair, Span),
|
||||
|
||||
/// Namespecs cannot include trailing slashes since it makes the package
|
||||
/// name look like a directory.
|
||||
TrailingSlash(SPair, Span),
|
||||
|
||||
/// A relative namespec attempts to go past the project root because it
|
||||
/// includes too many `..` markers relative to a given canonical
|
||||
/// name.
|
||||
///
|
||||
/// The first [`SPair`] represents the failed relative namespec,
|
||||
/// the [`Span`] represents the marker that caused the failure,
|
||||
/// and the [`CanonicalName`] represents the parent that the namespec
|
||||
/// is being resolved against.
|
||||
TooManyParentMarkers(SPair, Span, CanonicalName),
|
||||
|
||||
/// Relative namespecs may only have parent markers (`..`) in a leading
|
||||
/// position.
|
||||
///
|
||||
/// The provided [`Span`] is the position of the marker that is in
|
||||
/// error.
|
||||
NonLeadingParentMarker(SPair, Span),
|
||||
|
||||
/// A portion of the namespec can be removed and still result in the
|
||||
/// same namespec when interpreted as a path on the filesystem.
|
||||
///
|
||||
/// This restriction is important to ensure that multiple namespecs
|
||||
/// cannot be used to represent the same package,
|
||||
/// which would cause package index lookup failures.
|
||||
UselessComponent(SPair, SPair),
|
||||
}
|
||||
|
||||
impl Display for CanonicalNameError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
use CanonicalNameError::*;
|
||||
|
||||
match self {
|
||||
LeadingSlashExpected(name, _) => write!(
|
||||
f,
|
||||
"non-canonical package name {} where canonical name was expected",
|
||||
TtQuote::wrap(name),
|
||||
),
|
||||
|
||||
TrailingSlash(name, _) => write!(
|
||||
f,
|
||||
"package namespec {} must not include a trailing slash",
|
||||
TtQuote::wrap(name),
|
||||
),
|
||||
|
||||
TooManyParentMarkers(name, _, _) => write!(
|
||||
f,
|
||||
"package namespec {} resolves outside of package root",
|
||||
TtQuote::wrap(name),
|
||||
),
|
||||
|
||||
NonLeadingParentMarker(name, _) => write!(
|
||||
f,
|
||||
"package namespec {} contains parent marker in a \
|
||||
non-head position",
|
||||
TtQuote::wrap(name),
|
||||
),
|
||||
|
||||
UselessComponent(name, part) => write!(
|
||||
f,
|
||||
"package namespec {} contains an invalid {}",
|
||||
TtQuote::wrap(name),
|
||||
TtQuote::wrap(part),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Error for CanonicalNameError {}
|
||||
|
||||
impl Diagnostic for CanonicalNameError {
|
||||
fn describe(&self) -> Vec<AnnotatedSpan> {
|
||||
use CanonicalNameError::*;
|
||||
|
||||
match self {
|
||||
LeadingSlashExpected(_, at) => vec![
|
||||
at.error("expected name beginning with '/'"),
|
||||
at.help(
|
||||
"canonical names begin with '/' and uniquely identify a \
|
||||
package relative to a project root",
|
||||
),
|
||||
],
|
||||
|
||||
TrailingSlash(_, at) => vec![
|
||||
at.error("trailing slash not permitted in package namespec"),
|
||||
at.help(
|
||||
"a trailing slash makes a package name look like a directory, \
|
||||
but a package is always associated with an \
|
||||
explicit file",
|
||||
)
|
||||
],
|
||||
|
||||
TooManyParentMarkers(_, at, parent) => vec![
|
||||
parent.note(format!(
|
||||
"parent package is {}",
|
||||
TtQuote::wrap(Into::<SPair>::into(*parent))
|
||||
)),
|
||||
at.error("this marker exceeds the parent package depth"),
|
||||
at.help(
|
||||
"the marker \"..\" treats the parent package name like a \
|
||||
path and moves into parent directories"
|
||||
),
|
||||
at.help(
|
||||
"for both practical and security reasons, \
|
||||
you cannot resolve past the project root \
|
||||
relative to the parent package"
|
||||
),
|
||||
],
|
||||
|
||||
NonLeadingParentMarker(_, at) => vec![
|
||||
at.error(
|
||||
"parent marker must only appear at the head of a namespec"
|
||||
),
|
||||
at.help(
|
||||
"for example: the namespec `../bar` is valid, \
|
||||
but `../foo/../bar` is not permitted"
|
||||
),
|
||||
at.help(
|
||||
"this restriction helps to avoid unnecessarily confusing \
|
||||
namespecs"
|
||||
)
|
||||
],
|
||||
|
||||
UselessComponent(_, part) => vec![
|
||||
part.error("this component is not permitted here"),
|
||||
part.help(format!(
|
||||
"remove the unnecessary {} from the namespec",
|
||||
TtQuote::wrap(part)
|
||||
)),
|
||||
part.help(
|
||||
"since namespecs are mapped to paths on the filesystem, \
|
||||
certain strings are not permitted that would \
|
||||
otherwise allow the same package to be represented \
|
||||
by multiple different namespecs"
|
||||
)
|
||||
],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use crate::span::dummy::{DUMMY_CONTEXT as DC, *};
|
||||
|
||||
type Sut = CanonicalName;
|
||||
|
||||
#[test]
|
||||
fn slash_prefixed_name_is_canonical() {
|
||||
let name = SPair("/foo/bar".into(), S1);
|
||||
|
||||
assert_eq!(
|
||||
name,
|
||||
Sut::from_canonical(name)
|
||||
.expect("failed to canonicalize name")
|
||||
.into()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_canonical_fails_with_non_prefixed_name() {
|
||||
let a = DC.span(0, 13);
|
||||
let b = DC.span(0, 0); // expected location of '/'
|
||||
|
||||
let name = SPair("not/canonical".into(), a);
|
||||
// [-----------]
|
||||
// 0 12
|
||||
// | A
|
||||
// B (zero-length)
|
||||
|
||||
assert_eq!(
|
||||
Err(CanonicalNameError::LeadingSlashExpected(name, b)),
|
||||
Sut::from_canonical(name),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_canonical_must_not_end_in_trailing_slash() {
|
||||
let a = DC.span(0, 16);
|
||||
let b = DC.span(15, 1);
|
||||
|
||||
let name = SPair("/trailing/slash/".into(), a);
|
||||
// [--------------]
|
||||
// 0 15
|
||||
// A |
|
||||
// B
|
||||
|
||||
assert_eq!(
|
||||
Err(CanonicalNameError::TrailingSlash(name, b)),
|
||||
Sut::from_canonical(name),
|
||||
);
|
||||
}
|
||||
|
||||
// Relative namespec without `..`.
|
||||
#[test]
|
||||
fn resolve_namespec_against_canonical_without_parent() {
|
||||
let parent = canonical(SPair("/parent/pkg".into(), S1));
|
||||
// ^^^ this is stripped
|
||||
let rel = SPair("rel/to/parent".into(), S2);
|
||||
|
||||
assert_eq!(
|
||||
// Note that this assumes the span of the _child_ since it's
|
||||
// more specific and what the user almost certainly wants if
|
||||
// there is a diagnostic message referencing this name.
|
||||
Ok(canonical(SPair("/parent/rel/to/parent".into(), S2))),
|
||||
parent.resolve_namespec_rel(rel),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_absolute_namespec_overrides_parent() {
|
||||
let parent = canonical(SPair("/parent/to/override".into(), S1));
|
||||
let rel = SPair("/abs/name".into(), S2);
|
||||
|
||||
// A canonical name is an absolute namespec.
|
||||
#[rustfmt::skip]
|
||||
assert_eq!(
|
||||
Ok(canonical(rel)),
|
||||
parent.resolve_namespec_rel(rel),
|
||||
);
|
||||
}
|
||||
|
||||
// Resolving parent markers
|
||||
// (e.g. "../foo")
|
||||
// is a _purely lexical_ operation.
|
||||
#[test]
|
||||
fn resolve_namespec_with_leading_parent_markers_and_compatible_parent() {
|
||||
let parent = canonical(SPair("/one/two/three".into(), S1));
|
||||
// note that the rightmost component ^^^^^
|
||||
// will be stripped _before_ processing
|
||||
// the first `..`
|
||||
|
||||
assert_eq!(
|
||||
parent.resolve_namespec_rel(SPair("../a/pkg".into(), S2)),
|
||||
Ok(canonical(SPair("/one/a/pkg".into(), S2))),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
parent.resolve_namespec_rel(SPair("../../b/pkg".into(), S3)),
|
||||
Ok(canonical(SPair("/b/pkg".into(), S3))),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
parent.resolve_namespec_rel(SPair("..".into(), S4)),
|
||||
Ok(canonical(SPair("/one".into(), S4))),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_namespec_with_leading_parent_markers_too_far() {
|
||||
let parent = canonical(SPair("/parent/too/short".into(), S1));
|
||||
|
||||
let a = DC.span(0, 15);
|
||||
let b = DC.span(6, 2);
|
||||
let rel = SPair("../../../foo/bar".into(), a);
|
||||
// [-----++-------]
|
||||
// 0 || 15
|
||||
// A ||
|
||||
// []
|
||||
// 6 `7
|
||||
// B
|
||||
// `this marker goes one too far
|
||||
|
||||
assert_eq!(
|
||||
Err(CanonicalNameError::TooManyParentMarkers(rel, b, parent)),
|
||||
parent.resolve_namespec_rel(rel),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_namespec_with_leading_parent_markers_yielding_empty() {
|
||||
let parent = canonical(SPair("/will/be/empty/pkg".into(), S1));
|
||||
|
||||
let a = DC.span(0, 8);
|
||||
let b = DC.span(6, 2);
|
||||
let rel = SPair("../../..".into(), a);
|
||||
// [-----+]
|
||||
// 0 ||`7
|
||||
// A ||
|
||||
// []
|
||||
// 6 `7
|
||||
// B
|
||||
// `only goes too far because the result would
|
||||
// be `/` which is not a canonical name
|
||||
|
||||
assert_eq!(
|
||||
Err(CanonicalNameError::TooManyParentMarkers(rel, b, parent)),
|
||||
parent.resolve_namespec_rel(rel),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_namespec_with_leading_parent_markers_trailing_slash() {
|
||||
let parent = canonical(SPair("/will/be/empty/pkg".into(), S1));
|
||||
|
||||
let a = DC.span(0, 13);
|
||||
let b = DC.span(12, 1);
|
||||
let rel = SPair("../../../foo/".into(), a);
|
||||
// [-----------]
|
||||
// 0 |`12
|
||||
// A |
|
||||
// B
|
||||
|
||||
assert_eq!(
|
||||
Err(CanonicalNameError::TrailingSlash(
|
||||
SPair("/foo/".into(), a),
|
||||
b,
|
||||
)),
|
||||
parent.resolve_namespec_rel(rel),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_namespec_with_non_head_parent_marker() {
|
||||
let parent = canonical(SPair("/foo/pkg".into(), S1));
|
||||
|
||||
let a = DC.span(0, 10);
|
||||
let b = DC.span(4, 2);
|
||||
let rel = SPair("bar/../baz".into(), a);
|
||||
// [---++---]
|
||||
// 0 || 9
|
||||
// A ||
|
||||
// []
|
||||
// 4 `5
|
||||
// B
|
||||
|
||||
assert_eq!(
|
||||
Err(CanonicalNameError::NonLeadingParentMarker(
|
||||
SPair("/foo/bar/../baz".into(), a),
|
||||
b,
|
||||
)),
|
||||
parent.resolve_namespec_rel(rel),
|
||||
);
|
||||
|
||||
// The same error should happen for a supposedly-canonical name too.
|
||||
let c = DC.span(0, 11);
|
||||
let d = DC.span(5, 2);
|
||||
let name = SPair("/bar/../baz".into(), c);
|
||||
// [----++---]
|
||||
// 0 || 10
|
||||
// C ||
|
||||
// []
|
||||
// 4 `5
|
||||
// D
|
||||
|
||||
assert_eq!(
|
||||
Err(CanonicalNameError::NonLeadingParentMarker(name, d,)),
|
||||
CanonicalName::from_canonical(name),
|
||||
);
|
||||
}
|
||||
|
||||
// The above catches `/../`,
|
||||
// but make sure we catch `/..$`.
|
||||
#[test]
|
||||
fn resolve_namespec_with_non_head_parent_marker_at_tail() {
|
||||
let parent = canonical(SPair("/foo/pkg".into(), S1));
|
||||
|
||||
let a = DC.span(0, 6);
|
||||
let b = DC.span(4, 2);
|
||||
let rel = SPair("bar/..".into(), a);
|
||||
// [---+]
|
||||
// 0 ||`5
|
||||
// A ||
|
||||
// []
|
||||
// 4 `5
|
||||
// B
|
||||
|
||||
assert_eq!(
|
||||
Err(CanonicalNameError::NonLeadingParentMarker(
|
||||
SPair("/foo/bar/..".into(), a),
|
||||
b,
|
||||
)),
|
||||
parent.resolve_namespec_rel(rel),
|
||||
);
|
||||
}
|
||||
|
||||
// `/./` doesn't have a special lexical meaning for `CanonicalName`,
|
||||
// which is the problem---when
|
||||
// we go to resolve the name as a path,
|
||||
// multiple names would resolve to the same package.
|
||||
#[test]
|
||||
fn resolve_namespec_with_single_dot() {
|
||||
let parent = canonical(SPair("/foo/pkg".into(), S1));
|
||||
|
||||
let a = DC.span(0, 9);
|
||||
let b = DC.span(0, 2);
|
||||
let rel = SPair("./bar/baz".into(), a);
|
||||
// [+------]
|
||||
// 0| 8
|
||||
// [] A
|
||||
// 1
|
||||
// B
|
||||
|
||||
// "./" can be removed from the above path and still retain the same
|
||||
// meaning.
|
||||
assert_eq!(
|
||||
Err(CanonicalNameError::UselessComponent(
|
||||
SPair("/foo/./bar/baz".into(), a),
|
||||
SPair("./".into(), b),
|
||||
)),
|
||||
parent.resolve_namespec_rel(rel),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn canonical_name_with_single_dot_middle() {
|
||||
let a = DC.span(0, 10);
|
||||
let b = DC.span(5, 2);
|
||||
let name = SPair("/foo/./bar".into(), a);
|
||||
// [-----++---]
|
||||
// 0 || 9
|
||||
// A []
|
||||
// 5 `6
|
||||
// B
|
||||
|
||||
// "./" can be removed from the above path and still retain the same
|
||||
// meaning.
|
||||
assert_eq!(
|
||||
Err(CanonicalNameError::UselessComponent(
|
||||
name,
|
||||
SPair("./".into(), b),
|
||||
)),
|
||||
CanonicalName::from_canonical(name),
|
||||
);
|
||||
}
|
||||
|
||||
// This error is phrased as the same problem as the above,
|
||||
// but in reality,
|
||||
// `.` in a tail position would act as its own package name if we
|
||||
// append an extension to it
|
||||
// (e.g. `/foo/bar/..xmlo`).
|
||||
// Best to flat out reject that.
|
||||
#[test]
|
||||
fn canonical_name_with_single_dot_end() {
|
||||
let a = DC.span(0, 10);
|
||||
let b = DC.span(8, 2);
|
||||
let name = SPair("/foo/bar/.".into(), a);
|
||||
// [--------+]
|
||||
// 0 |9
|
||||
// A []
|
||||
// 8
|
||||
// B
|
||||
|
||||
// "/." can be removed from the above path and still retain the same
|
||||
// meaning.
|
||||
assert_eq!(
|
||||
Err(CanonicalNameError::UselessComponent(
|
||||
name,
|
||||
SPair("/.".into(), b),
|
||||
)),
|
||||
CanonicalName::from_canonical(name),
|
||||
);
|
||||
}
|
||||
|
||||
// Adjacent slashes are collapsed into a single slash by many Unix-like
|
||||
// filesystems,
|
||||
// and so are equivalent to `./`.
|
||||
#[test]
|
||||
fn canonical_name_double_slash() {
|
||||
let a = DC.span(0, 9);
|
||||
let b = DC.span(5, 1);
|
||||
let name = SPair("/foo//bar".into(), a);
|
||||
// [-----+--]
|
||||
// 0 | 8
|
||||
// A ⌷
|
||||
// 6
|
||||
// B
|
||||
|
||||
// A "/" can be removed from the above path and still retain the
|
||||
// same meaning.
|
||||
assert_eq!(
|
||||
Err(CanonicalNameError::UselessComponent(
|
||||
name,
|
||||
SPair("/".into(), b),
|
||||
)),
|
||||
CanonicalName::from_canonical(name),
|
||||
);
|
||||
}
|
||||
|
||||
// Make sure we're not too naive and restrictive in our search.
|
||||
#[test]
|
||||
fn canonical_name_permits_parent_marker_like_strings() {
|
||||
CanonicalName::from_canonical(SPair("/foo/..bar/baz".into(), S1))
|
||||
.unwrap();
|
||||
CanonicalName::from_canonical(SPair("/foo/bar/baz..".into(), S2))
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
fn canonical(name: SPair) -> CanonicalName {
|
||||
CanonicalName::from_canonical(name).expect(&format!(
|
||||
"broken test case: failed instantiate CanonicalName \
|
||||
with \"{name}\""
|
||||
))
|
||||
}
|
||||
}
|
|
@ -1,4 +1,5 @@
|
|||
// Relationship between objects represented on ASG //
|
||||
// Relationship between objects represented on ASG
|
||||
//
|
||||
// Copyright (C) 2014-2023 Ryan Specialty, LLC.
|
||||
//
|
||||
// This file is part of TAME.
|
||||
|
@ -21,14 +22,15 @@
|
|||
//! See (parent module)[super] for more information.
|
||||
|
||||
use super::{
|
||||
Expr, Ident, Object, ObjectIndex, ObjectKind, OiPairObjectInner, Pkg, Root,
|
||||
Doc, Expr, Ident, Meta, Object, ObjectIndex, ObjectKind, OiPairObjectInner,
|
||||
Pkg, Root,
|
||||
};
|
||||
use crate::{
|
||||
asg::{graph::object::Tpl, Asg},
|
||||
f::Functor,
|
||||
span::Span,
|
||||
};
|
||||
use std::fmt::Display;
|
||||
use std::{fmt::Display, marker::PhantomData};
|
||||
|
||||
pub use super::ObjectTy as ObjectRelTy;
|
||||
|
||||
|
@ -55,6 +57,7 @@ macro_rules! object_rel {
|
|||
$from:ident -> {
|
||||
$($ety:ident $kind:ident,)*
|
||||
}
|
||||
$(can_recurse($rec_obj:ident) if $rec_expr:expr)?
|
||||
) => {paste::paste! {
|
||||
/// Subset of [`ObjectKind`]s that are valid targets for edges from
|
||||
#[doc=concat!("[`", stringify!($from), "`].")]
|
||||
|
@ -70,19 +73,23 @@ macro_rules! object_rel {
|
|||
}
|
||||
|
||||
impl ObjectRel<$from> for [<$from Rel>] {
|
||||
fn narrow<OB: ObjectRelFrom<$from> + ObjectRelatable>(
|
||||
self,
|
||||
fn narrow_ref<OB: ObjectRelFrom<$from> + ObjectRelatable>(
|
||||
&self,
|
||||
) -> Option<ObjectIndex<OB>> {
|
||||
match self {
|
||||
match *self {
|
||||
$(Self::$kind(oi) => oi.filter_rel(),)*
|
||||
}
|
||||
}
|
||||
|
||||
/// The root of the graph by definition has no cross edges.
|
||||
fn is_cross_edge(&self) -> bool {
|
||||
fn is_cross_edge<S, T>(
|
||||
&self,
|
||||
#[allow(unused_variables)] // used only for `dyn` edges
|
||||
rel: &$crate::asg::graph::object::rel::DynObjectRel<S, T>
|
||||
) -> bool {
|
||||
match self {
|
||||
$(
|
||||
Self::$kind(..) => object_rel!(@is_cross_edge $ety),
|
||||
Self::$kind(..) => object_rel!(@is_cross_edge $ety rel),
|
||||
)*
|
||||
|
||||
#[allow(unreachable_patterns)] // for empty Rel types
|
||||
|
@ -91,6 +98,15 @@ macro_rules! object_rel {
|
|||
),
|
||||
}
|
||||
}
|
||||
|
||||
$(
|
||||
fn can_recurse(&self, asg: &Asg) -> bool {
|
||||
self.narrow_ref::<$from>()
|
||||
.map(|oi| oi.resolve(asg))
|
||||
.map(|$rec_obj| $rec_expr)
|
||||
.unwrap_or(false)
|
||||
}
|
||||
)?
|
||||
}
|
||||
|
||||
impl ObjectRelatable for $from {
|
||||
|
@ -114,6 +130,23 @@ macro_rules! object_rel {
|
|||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn oi_rel_to_dyn<OB: ObjectRelatable>(
|
||||
#[allow(unused_variables)] // for empty Rel
|
||||
oi: ObjectIndex<Self>,
|
||||
) -> Option<$crate::asg::graph::object::ObjectIndexTo<OB>> {
|
||||
#[allow(unused_imports)]
|
||||
use $crate::asg::graph::object::ObjectIndexTo;
|
||||
|
||||
match OB::rel_ty() {
|
||||
$(
|
||||
ObjectRelTy::$kind => {
|
||||
ObjectIndexTo::<$kind>::from(oi).reflexivity()
|
||||
},
|
||||
)*
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$(
|
||||
|
@ -122,11 +155,49 @@ macro_rules! object_rel {
|
|||
Self::$kind(value)
|
||||
}
|
||||
}
|
||||
|
||||
object_rel!{ @impl_rel_to $from $ety $kind }
|
||||
)*
|
||||
|
||||
impl From<[<$from Rel>]> for ObjectIndex<Object> {
|
||||
fn from(value: [<$from Rel>]) -> Self {
|
||||
match value {
|
||||
$(
|
||||
[<$from Rel>]::$kind(oi) => oi.widen(),
|
||||
)*
|
||||
}
|
||||
}
|
||||
}
|
||||
}};
|
||||
|
||||
(@is_cross_edge cross) => { true };
|
||||
(@is_cross_edge tree) => { false };
|
||||
// Static edge types.
|
||||
(@is_cross_edge cross $_:ident) => { true };
|
||||
(@is_cross_edge tree $_:ident) => { false };
|
||||
|
||||
// Dynamic edge type.
|
||||
//
|
||||
// We consider an edge to be a cross edge iff it contains a context
|
||||
// span.
|
||||
// If it were _not_ a cross edge,
|
||||
// then the edge would represent ownership,
|
||||
// and the span information on the target would be sufficient context
|
||||
// on its own;
|
||||
// an edge only needs supplemental span information if there is
|
||||
// another context in which that object is referenced.
|
||||
(@is_cross_edge dyn $rel:ident) => { $rel.ctx_span().is_some() };
|
||||
|
||||
// Similar to above but providing _static_ information to the type
|
||||
// system.
|
||||
// The above could be rolled into this at some point.
|
||||
(@impl_rel_to $from:ident cross $kind:ident) => {};
|
||||
(@impl_rel_to $from:ident tree $kind:ident) => {
|
||||
impl ObjectTreeRelTo<$kind> for $from {}
|
||||
};
|
||||
(@impl_rel_to $from:ident dyn $kind:ident) => {
|
||||
// It _could_ be a tree edge;
|
||||
// we can't know statically.
|
||||
impl ObjectTreeRelTo<$kind> for $from {}
|
||||
};
|
||||
}
|
||||
|
||||
/// A dynamic relationship (edge) from one object to another before it has
|
||||
|
@ -205,6 +276,23 @@ impl<S> DynObjectRel<S, ObjectIndex<Object>> {
|
|||
O::new_rel_dyn(self.target_ty(), *self.target())
|
||||
}
|
||||
|
||||
/// Attempt to convert [`Self`] into an [`ObjectIndex`] with an
|
||||
/// [`ObjectKind`] of type `O`.
|
||||
///
|
||||
/// This method allows marrying a dynamically determined type with a
|
||||
/// context requiring a static type.
|
||||
/// If the type `O` does not match the type stored at runtime,
|
||||
/// [`None`] is returned.
|
||||
pub fn filter_into_target<O: ObjectKind + ObjectRelatable>(
|
||||
&self,
|
||||
) -> Option<ObjectIndex<O>> {
|
||||
if self.target_ty() == O::rel_ty() {
|
||||
Some(self.target().must_narrow_into())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Pair the target [`ObjectIndex`] with its resolved [`Object`].
|
||||
///
|
||||
/// This allows the [`ObjectIndex`] to be refined alongside the inner
|
||||
|
@ -219,6 +307,37 @@ impl<S> DynObjectRel<S, ObjectIndex<Object>> {
|
|||
self.map(|(soi, toi)| (soi, toi.resolve(asg).pair_oi(toi)))
|
||||
}
|
||||
|
||||
/// Retrieve the target [`ObjectIndex`] as an [`ObjectIndexTo<OB>`](ObjectIndexTo),
|
||||
/// if the object can be related to objects of type `OB`.
|
||||
///
|
||||
/// This method may be confusing in that it represents another
|
||||
/// _possible_ relation on top of the relation represented by
|
||||
/// [`Self`].
|
||||
/// That is:
|
||||
///
|
||||
/// ```text
|
||||
/// OA -> OB [ -> OC]
|
||||
/// '______'
|
||||
/// Self
|
||||
/// ```
|
||||
///
|
||||
/// If this method returns [`Some`],
|
||||
/// that means that the target of this relation `OB` is an object
|
||||
/// _that is capable of being related to_ an object of type `OC`.
|
||||
pub fn target_oi_rel_to_dyn<OC: ObjectRelatable>(
|
||||
&self,
|
||||
) -> Option<ObjectIndexTo<OC>> {
|
||||
// TODO: A newtype ought to couple these in a way that we don't have
|
||||
// to trust this assumption!
|
||||
// This requires assuming that the target is of the target type,
|
||||
// which _should_ certainly be the case if originating from the graph,
|
||||
// but if it's not,
|
||||
// then later resolving the `ObjectIndex` with a mismatched type
|
||||
// will result in a panic.
|
||||
self.target_ty()
|
||||
.assuming_oi_maybe_rel_to_dyn(*self.target())
|
||||
}
|
||||
|
||||
/// Dynamically determine whether this edge represents a cross edge.
|
||||
///
|
||||
/// This function is intended for _dynamic_ edge types,
|
||||
|
@ -247,7 +366,7 @@ impl<S> DynObjectRel<S, ObjectIndex<Object>> {
|
|||
$(
|
||||
ObjectRelTy::$ty => {
|
||||
self.narrow_target::<$ty>().is_some_and(
|
||||
|rel| rel.is_cross_edge()
|
||||
|rel| rel.is_cross_edge(self)
|
||||
)
|
||||
},
|
||||
)*
|
||||
|
@ -255,7 +374,30 @@ impl<S> DynObjectRel<S, ObjectIndex<Object>> {
|
|||
}
|
||||
}
|
||||
|
||||
ty_cross_edge!(Root, Pkg, Ident, Expr, Tpl)
|
||||
ty_cross_edge!(Root, Pkg, Ident, Expr, Tpl, Meta, Doc)
|
||||
}
|
||||
|
||||
/// Dynamically determine whether this edge represents a permitted
|
||||
/// cycle.
|
||||
///
|
||||
/// A cycle is permitted in certain cases of recursion.
|
||||
/// See [`ObjectRel::can_recurse`] for more information.
|
||||
pub fn can_recurse(&self, asg: &Asg) -> bool {
|
||||
macro_rules! ty_can_recurse {
|
||||
($($ty:ident),*) => {
|
||||
match self.source_ty() {
|
||||
$(
|
||||
ObjectRelTy::$ty => {
|
||||
self.narrow_target::<$ty>().is_some_and(
|
||||
|rel| rel.can_recurse(asg)
|
||||
)
|
||||
},
|
||||
)*
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ty_can_recurse!(Root, Pkg, Ident, Expr, Tpl, Meta, Doc)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -333,6 +475,18 @@ pub trait ObjectRelTo<OB: ObjectKind + ObjectRelatable> =
|
|||
pub trait ObjectRelFrom<OA: ObjectKind + ObjectRelatable> =
|
||||
ObjectRelatable where <OA as ObjectRelatable>::Rel: From<ObjectIndex<Self>>;
|
||||
|
||||
/// Indicate that an [`ObjectKind`] `Self` _could possibly take ownership
|
||||
/// over_ an [`ObjectKind`] `OB`.
|
||||
///
|
||||
/// This is a stronger assertion than [`ObjectRelTo`];
|
||||
/// see that trait for more information.
|
||||
/// This trait is intended to be used in contexts where the distinction
|
||||
/// between reference and ownership is important.
|
||||
pub trait ObjectTreeRelTo<OB: ObjectKind + ObjectRelatable>:
|
||||
ObjectRelTo<OB>
|
||||
{
|
||||
}
|
||||
|
||||
/// Identify [`Self::Rel`] as a sum type consisting of the subset of
|
||||
/// [`Object`] variants representing the valid _target_ edges of
|
||||
/// [`Self`].
|
||||
|
@ -354,7 +508,7 @@ pub trait ObjectRelatable: ObjectKind {
|
|||
/// Represent a relation to another [`ObjectKind`] that cannot be
|
||||
/// statically known and must be handled at runtime.
|
||||
///
|
||||
/// A value of [`None`] means that the provided [`DynObjectRel`] is not
|
||||
/// A value of [`None`] means that the provided [`ObjectRelTy`] is not
|
||||
/// valid for [`Self`].
|
||||
/// If the caller is utilizing edge data that is already present on the graph,
|
||||
/// then this means that the system is not properly upholding edge
|
||||
|
@ -369,6 +523,21 @@ pub trait ObjectRelatable: ObjectKind {
|
|||
ty: ObjectRelTy,
|
||||
oi: ObjectIndex<Object>,
|
||||
) -> Option<Self::Rel>;
|
||||
|
||||
/// Cast the provided [`ObjectIndex`] into an [`ObjectIndexTo`] if it is
|
||||
/// able to be related to the provided `OB`.
|
||||
///
|
||||
/// This is intended to be used in a dynamic context,
|
||||
/// where the caller is not aware statically of the [`ObjectKind`]s
|
||||
/// involved.
|
||||
/// If it is _required_ that an object be relatable,
|
||||
/// use [`ObjectRelTo`] to statically verify that assertion.
|
||||
///
|
||||
/// If the type `OB` is not a valid target of a relation from this type,
|
||||
/// [`None`] will be returned.
|
||||
fn oi_rel_to_dyn<OB: ObjectRelatable>(
|
||||
oi: ObjectIndex<Self>,
|
||||
) -> Option<ObjectIndexTo<OB>>;
|
||||
}
|
||||
|
||||
impl<O: ObjectKind + ObjectRelatable> ObjectIndex<O> {
|
||||
|
@ -410,7 +579,9 @@ impl<O: ObjectKind + ObjectRelatable> ObjectIndex<O> {
|
|||
/// adhere to the prescribed ontology,
|
||||
/// provided that invariants are properly upheld by the
|
||||
/// [`asg`](crate::asg) module.
|
||||
pub trait ObjectRel<OA: ObjectKind + ObjectRelatable>: Sized {
|
||||
pub trait ObjectRel<OA: ObjectKind + ObjectRelatable>:
|
||||
Sized + Into<ObjectIndex<Object>>
|
||||
{
|
||||
/// Attempt to narrow into the [`ObjectKind`] `OB`.
|
||||
///
|
||||
/// Unlike [`Object`] nodes,
|
||||
|
@ -426,6 +597,16 @@ pub trait ObjectRel<OA: ObjectKind + ObjectRelatable>: Sized {
|
|||
/// query for edges of particular kinds.
|
||||
fn narrow<OB: ObjectRelFrom<OA> + ObjectRelatable>(
|
||||
self,
|
||||
) -> Option<ObjectIndex<OB>> {
|
||||
self.narrow_ref()
|
||||
}
|
||||
|
||||
/// Attempt to narrow into the [`ObjectKind`] `OB`.
|
||||
///
|
||||
/// This method is the same as [`Self::narrow`],
|
||||
/// but taking a reference instead of ownership.
|
||||
fn narrow_ref<OB: ObjectRelFrom<OA> + ObjectRelatable>(
|
||||
&self,
|
||||
) -> Option<ObjectIndex<OB>>;
|
||||
|
||||
/// Attempt to narrow into the [`ObjectKind`] `OB`,
|
||||
|
@ -446,6 +627,12 @@ pub trait ObjectRel<OA: ObjectKind + ObjectRelatable>: Sized {
|
|||
self.narrow::<OB>().map(Into::into)
|
||||
}
|
||||
|
||||
/// Widen into an [`ObjectIndex`],
|
||||
/// discarding static type information.
|
||||
fn widen(self) -> ObjectIndex<Object> {
|
||||
self.into()
|
||||
}
|
||||
|
||||
/// Whether this relationship represents an ontological cross edge.
|
||||
///
|
||||
/// A _cross edge_ is an edge between two trees as described by the
|
||||
|
@ -508,5 +695,453 @@ pub trait ObjectRel<OA: ObjectKind + ObjectRelatable>: Sized {
|
|||
/// then do _not_ assume that it is a cross edge without further
|
||||
/// analysis,
|
||||
/// which may require introducing more context to this method.
|
||||
fn is_cross_edge(&self) -> bool;
|
||||
///
|
||||
/// Dynamic Cross Edges
|
||||
/// ==================
|
||||
/// Sometimes a cross edge cannot be determined statically due to
|
||||
/// ontological compromises.
|
||||
/// For example,
|
||||
/// a [`Tpl`]->[`Ident`] edge could be a reference to another tree,
|
||||
/// as in the case of template application,
|
||||
/// or the template could be serving as a container for that
|
||||
/// definition.
|
||||
/// The former case is a cross edge,
|
||||
/// but the ladder is not.
|
||||
///
|
||||
/// We could introduce a layer of indirection on the graph to
|
||||
/// disambiguate,
|
||||
/// but that adds complexity to the graph that many different
|
||||
/// subsystems need to concern themselves with.
|
||||
/// But cross edge determination is fairly isolated,
|
||||
/// needed only by traversals.
|
||||
///
|
||||
/// Therefore,
|
||||
/// this method also receives a [`DynObjectRel`] reference,
|
||||
/// which contains edge information.
|
||||
/// The edge can be augmented with data that helps to determine,
|
||||
/// at runtime,
|
||||
/// whether that particular edge is a cross edge.
|
||||
///
|
||||
/// The use of [`DynObjectRel`] is heavy and effectively makes this
|
||||
/// method useless for callers that do not deal directly with raw
|
||||
/// [`Asg`] data;
|
||||
/// it may be useful to refine this further in the future to correct
|
||||
/// that,
|
||||
/// once all use cases are clear.
|
||||
fn is_cross_edge<S, T>(&self, rel: &DynObjectRel<S, T>) -> bool;
|
||||
|
||||
/// Whether the provided relationship represents a valid recursive
|
||||
/// target.
|
||||
///
|
||||
/// It is expected that this method will be consulted only when the
|
||||
/// provided [`ObjectIndex`] would produce a cycle when added to some
|
||||
/// path.
|
||||
/// This means that the source and target object will be identical.
|
||||
///
|
||||
/// It is expected that a cycle should be able to be "cut" at this point
|
||||
/// while still producing a valid topological ordering of the graph.
|
||||
/// For example,
|
||||
/// consider two mutually recursive functions `A` and `B`,
|
||||
/// as shown here:
|
||||
///
|
||||
/// ```text
|
||||
/// A -> B
|
||||
/// ^----'
|
||||
/// ```
|
||||
///
|
||||
/// There are two cycles that might be encountered:
|
||||
///
|
||||
/// - `A -> B -> A`, which would cut to `A -> B`; and
|
||||
/// - `B -> A -> B`, which would cut to `B -> A`.
|
||||
///
|
||||
/// In both cases,
|
||||
/// since both `A` and `B` are functions,
|
||||
/// a valid ordering is produced.
|
||||
///
|
||||
/// Failure to uphold this invariant when designing the graph's ontology
|
||||
/// will result in an invalid ordering of the graph,
|
||||
/// which will compile a program that does not behave according to
|
||||
/// its specification.
|
||||
/// That is:
|
||||
/// proper ordering is a requirement to uphold soundness.
|
||||
///
|
||||
/// Recursion will continue to be limited as TAMER progresses,
|
||||
/// migrating to a more APL-like alternative to solving
|
||||
/// otherwise-recursive problems and restricting remaining recursion
|
||||
/// to that which can provably terminate.
|
||||
fn can_recurse(&self, _asg: &Asg) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// An [`ObjectIndex`]-like object that is able to relate to
|
||||
/// [`ObjectKind`] `OB`.
|
||||
///
|
||||
/// This serves primarily as an opaque [`ObjectIndex`] that we know can be
|
||||
/// related to some other type of [`ObjectKind`] `OB`.
|
||||
/// This allows for generic graph operations that operate on relationships
|
||||
/// without having to know the type of the source object (`Self`).
|
||||
pub trait ObjectIndexRelTo<OB: ObjectRelatable>: Sized + Clone + Copy {
|
||||
/// The [`ObjectRelTy`] of the inner [`ObjectIndex`] before widening.
|
||||
fn src_rel_ty(&self) -> ObjectRelTy;
|
||||
|
||||
/// Widen this type into a generic [`ObjectIndex`] with no
|
||||
/// [`ObjectKind`] information.
|
||||
///
|
||||
/// See [`ObjectIndex::widen`] for more information.
|
||||
fn widen(&self) -> ObjectIndex<Object>;
|
||||
|
||||
/// 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.
|
||||
/// 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,
|
||||
asg: &mut Asg,
|
||||
to_oi: ObjectIndex<OB>,
|
||||
ctx_span: Option<Span>,
|
||||
) -> Self {
|
||||
asg.add_edge(self, to_oi, ctx_span);
|
||||
self
|
||||
}
|
||||
|
||||
/// 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`.
|
||||
///
|
||||
/// Since this trait only guarantees edges to `OB`,
|
||||
/// only edges targeting that type will be returned;
|
||||
/// other types of edges may or may not exist.
|
||||
/// See also [`ObjectIndex::edges_filtered`].
|
||||
fn edges_rel_to<'a>(
|
||||
&self,
|
||||
asg: &'a Asg,
|
||||
) -> impl Iterator<Item = ObjectIndex<OB>> + 'a {
|
||||
asg.edges_dyn(self.widen())
|
||||
.filter_map(|rel| rel.filter_into_target())
|
||||
}
|
||||
|
||||
/// Attempt to look up a locally bound [`Ident`] via a linear search of
|
||||
/// `self`'s edges.
|
||||
///
|
||||
/// This should be used only when an index is not available,
|
||||
/// and is currently restricted to tests.
|
||||
///
|
||||
/// Performance
|
||||
/// ===========
|
||||
/// _This is a linear (O(1)) search of the edges of the node
|
||||
/// corresponding to `self`!_
|
||||
/// At the time of writing,
|
||||
/// edges are stored using index references in a manner similar to a
|
||||
/// linked list (petgraph).
|
||||
/// And for each such edge,
|
||||
/// the target object must be resolved so that its
|
||||
/// [`SymbolId`](crate::sym::SymbolId) may be retrieved and compared
|
||||
/// against the provided `name`.
|
||||
///
|
||||
/// If the number of edges is small and the objects are fairly localized
|
||||
/// in memory relative to `self`,
|
||||
/// then this may not be a concern.
|
||||
/// However,
|
||||
/// if you've arrived at this method while investigating unfavorable
|
||||
/// circumstances during profiling,
|
||||
/// then you should consider caching like the global environment
|
||||
/// (see [`Asg::lookup`]).
|
||||
#[cfg(test)]
|
||||
fn lookup_local_linear(
|
||||
&self,
|
||||
asg: &Asg,
|
||||
name: crate::parse::util::SPair,
|
||||
) -> Option<ObjectIndex<Ident>>
|
||||
where
|
||||
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())
|
||||
}
|
||||
}
|
||||
|
||||
impl<O: ObjectRelatable, OB: ObjectRelatable> ObjectIndexRelTo<OB>
|
||||
for ObjectIndex<O>
|
||||
where
|
||||
O: ObjectRelTo<OB>,
|
||||
{
|
||||
fn src_rel_ty(&self) -> ObjectRelTy {
|
||||
O::rel_ty()
|
||||
}
|
||||
|
||||
fn widen(&self) -> ObjectIndex<Object> {
|
||||
ObjectIndex::<O>::widen(*self)
|
||||
}
|
||||
}
|
||||
|
||||
impl<OB: ObjectRelatable> ObjectIndexRelTo<OB> for ObjectIndexTo<OB> {
|
||||
fn src_rel_ty(&self) -> ObjectRelTy {
|
||||
match self {
|
||||
Self((_, ty), _) => *ty,
|
||||
}
|
||||
}
|
||||
|
||||
fn widen(&self) -> ObjectIndex<Object> {
|
||||
*self.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
impl<OB: ObjectRelatable> ObjectIndexRelTo<OB> for ObjectIndexToTree<OB> {
|
||||
fn src_rel_ty(&self) -> ObjectRelTy {
|
||||
match self {
|
||||
Self(oito) => oito.src_rel_ty(),
|
||||
}
|
||||
}
|
||||
|
||||
fn widen(&self) -> ObjectIndex<Object> {
|
||||
match self {
|
||||
Self(oito) => oito.widen(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<OB: ObjectRelatable> From<ObjectIndexTo<OB>> for ObjectIndex<Object> {
|
||||
fn from(oi_rel: ObjectIndexTo<OB>) -> Self {
|
||||
oi_rel.widen()
|
||||
}
|
||||
}
|
||||
|
||||
impl<OB: ObjectRelatable> AsRef<ObjectIndex<Object>> for ObjectIndexTo<OB> {
|
||||
fn as_ref(&self) -> &ObjectIndex<Object> {
|
||||
match self {
|
||||
Self((oi, _), _) => oi,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An [`ObjectIndex`]-like object that is able to create a _tree_ edge to
|
||||
/// [`ObjectKind`] `OB`.
|
||||
///
|
||||
/// This allows for generic graph operations that operate on ownership
|
||||
/// relationships without having to know the type of the source
|
||||
/// object (`Self`).
|
||||
///
|
||||
/// This is a specialization of [`ObjectIndexRelTo`].
|
||||
pub trait ObjectIndexTreeRelTo<OB: ObjectRelatable>:
|
||||
ObjectIndexRelTo<OB> + Into<ObjectIndexToTree<OB>>
|
||||
{
|
||||
}
|
||||
|
||||
impl<OB: ObjectRelatable> ObjectIndexTreeRelTo<OB> for ObjectIndexToTree<OB> {}
|
||||
|
||||
impl<O: ObjectRelatable, OB: ObjectRelatable> ObjectIndexTreeRelTo<OB>
|
||||
for ObjectIndex<O>
|
||||
where
|
||||
O: ObjectTreeRelTo<OB>,
|
||||
{
|
||||
}
|
||||
|
||||
pub use private::{ObjectIndexTo, ObjectIndexToTree};
|
||||
|
||||
/// Private inner module to ensure that nothing is able to bypass invariants
|
||||
/// by constructing [`ObjectIndexTo`] manually.
|
||||
mod private {
|
||||
use std::hash::{Hash, Hasher};
|
||||
|
||||
use super::*;
|
||||
|
||||
/// Some [`ObjectIndex`] that is able to [`ObjectRelTo`] `OB`.
|
||||
///
|
||||
/// This type upholds the invariant that any [`ObjectIndex`] contained
|
||||
/// within is [`ObjectRelTo`] `OB`.
|
||||
/// This allows this object to serve in place of a concrete
|
||||
/// [`ObjectIndex`] for graph operations that need only know whether
|
||||
/// an object is relatable to another,
|
||||
/// such as when adding edges.
|
||||
///
|
||||
/// This object is only needed when relations need to be manipulated on
|
||||
/// a known target [`ObjectKind`] `OB`,
|
||||
/// but the source [`ObjectKind`] is dynamic.
|
||||
/// This is necessary because a generic [`Object`] must first be
|
||||
/// narrowed before being able to be used in any graph operation so
|
||||
/// that the ontology can be statically enforced.
|
||||
///
|
||||
/// See [`ObjectIndexRelTo`] for more information.
|
||||
///
|
||||
/// Constructing [`ObjectIndexTo`]
|
||||
/// ==============================
|
||||
/// This object is intended to be constructed using [`From`].
|
||||
/// _Never construct this object in any other way;_
|
||||
/// manually creating the struct will not uphold its invariants,
|
||||
/// which can lead to an invalid graph construction,
|
||||
/// which will in turn lead to internal system failures when trying
|
||||
/// to operate on the graph data down the line.
|
||||
/// There are no memory safety concerns.
|
||||
#[derive(Debug)]
|
||||
pub struct ObjectIndexTo<OB: ObjectRelatable>(
|
||||
(ObjectIndex<Object>, ObjectRelTy),
|
||||
PhantomData<OB>,
|
||||
);
|
||||
|
||||
impl<OB: ObjectRelatable, O: ObjectRelatable> From<ObjectIndex<O>>
|
||||
for ObjectIndexTo<OB>
|
||||
where
|
||||
O: ObjectRelTo<OB>,
|
||||
{
|
||||
fn from(oi: ObjectIndex<O>) -> Self {
|
||||
Self(oi.widen_dyn_ty(), PhantomData::default())
|
||||
}
|
||||
}
|
||||
|
||||
impl<OB: ObjectRelatable> ObjectIndexTo<OB> {
|
||||
pub fn span(&self) -> Span {
|
||||
(*self).into()
|
||||
}
|
||||
|
||||
/// Assert a reflexive relationship between `OB` and `OC`.
|
||||
///
|
||||
/// The types `OB` and `OC` are equivalent (and therefore reflexive)
|
||||
/// iff they have matching `ObjectRelTy`s.
|
||||
///
|
||||
/// The sole purpose of this method is to satisfy Rust's type system
|
||||
/// in dynamic situations where the type system is not able to
|
||||
/// understand what we're doing,
|
||||
/// where the type `OC` is more general than the type `OB`.
|
||||
/// This method is always safe;
|
||||
/// it will return [`None`] if the two types differ in runtime
|
||||
/// value.
|
||||
///
|
||||
/// While the term "reflexive" is a binary relation in mathematics,
|
||||
/// the term "reflexivity" originates from the Coq tactic.
|
||||
///
|
||||
/// For an example of where this is needed,
|
||||
/// see [`ObjectRelatable::oi_rel_to_dyn`].
|
||||
pub fn reflexivity<OC: ObjectRelatable>(
|
||||
self,
|
||||
) -> Option<ObjectIndexTo<OC>> {
|
||||
let Self(parts, _) = self;
|
||||
(OB::rel_ty() == OC::rel_ty())
|
||||
.then_some(ObjectIndexTo(parts, PhantomData::default()))
|
||||
}
|
||||
}
|
||||
|
||||
// Ignore metadata that should always be consistent with the underlying
|
||||
// `ObjectIndex`.
|
||||
impl<OB: ObjectRelatable> PartialEq for ObjectIndexTo<OB> {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
match (self, other) {
|
||||
(Self((oi, _), _), Self((oi_other, _), _)) => oi == oi_other,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<OB: ObjectRelatable> Eq for ObjectIndexTo<OB> {}
|
||||
|
||||
// Ignore metadata that should always be consistent with the underlying
|
||||
// `ObjectIndex`.
|
||||
impl<OB: ObjectRelatable> Hash for ObjectIndexTo<OB> {
|
||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||
match self {
|
||||
Self((oi, _), _) => oi.hash(state),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Deriving `Clone`/`Copy` as of 2023-03 was introducing a
|
||||
// `Clone`/`Copy` bound on `OB`.
|
||||
impl<OB: ObjectRelatable> Clone for ObjectIndexTo<OB> {
|
||||
fn clone(&self) -> Self {
|
||||
Self(self.0, self.1)
|
||||
}
|
||||
}
|
||||
|
||||
impl<OB: ObjectRelatable> Copy for ObjectIndexTo<OB> {}
|
||||
|
||||
impl<OB: ObjectRelatable> From<ObjectIndexTo<OB>> for Span {
|
||||
fn from(oi: ObjectIndexTo<OB>) -> Self {
|
||||
match oi {
|
||||
ObjectIndexTo((oi, _), _) => oi.span(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<OB: ObjectRelatable> From<ObjectIndexToTree<OB>> for ObjectIndexTo<OB> {
|
||||
fn from(value: ObjectIndexToTree<OB>) -> Self {
|
||||
match value {
|
||||
ObjectIndexToTree(oit) => oit,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Some [`ObjectIndex`] that can create a _tree_ edge to `OB`.
|
||||
///
|
||||
/// This is a specialization of
|
||||
/// (and contains)
|
||||
/// [`ObjectIndexTo`];
|
||||
/// see that for more information.
|
||||
///
|
||||
/// See also [`ObjectIndexTreeRelTo`].
|
||||
#[derive(Debug)]
|
||||
pub struct ObjectIndexToTree<OB: ObjectRelatable>(ObjectIndexTo<OB>);
|
||||
|
||||
impl<OB: ObjectRelatable, O: ObjectRelatable> From<ObjectIndex<O>>
|
||||
for ObjectIndexToTree<OB>
|
||||
where
|
||||
O: ObjectTreeRelTo<OB>,
|
||||
{
|
||||
fn from(oi: ObjectIndex<O>) -> Self {
|
||||
Self(oi.into())
|
||||
}
|
||||
}
|
||||
|
||||
// Deriving any of the below were introducing trait bounds on `OB`.
|
||||
|
||||
impl<OB: ObjectRelatable> PartialEq for ObjectIndexToTree<OB> {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
match (self, other) {
|
||||
(Self(oi), Self(oi_other)) => oi == oi_other,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<OB: ObjectRelatable> Eq for ObjectIndexToTree<OB> {}
|
||||
|
||||
impl<OB: ObjectRelatable> Hash for ObjectIndexToTree<OB> {
|
||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||
match self {
|
||||
Self(oi) => oi.hash(state),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<OB: ObjectRelatable> Clone for ObjectIndexToTree<OB> {
|
||||
fn clone(&self) -> Self {
|
||||
Self(self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl<OB: ObjectRelatable> Copy for ObjectIndexToTree<OB> {}
|
||||
|
||||
impl<OB: ObjectRelatable> From<ObjectIndexToTree<OB>> for Span {
|
||||
fn from(oi: ObjectIndexToTree<OB>) -> Self {
|
||||
match oi {
|
||||
ObjectIndexToTree(inner) => inner.span(),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,13 +19,9 @@
|
|||
|
||||
//! Root node of the ASG.
|
||||
|
||||
use super::{prelude::*, Ident, Pkg};
|
||||
use std::fmt::Display;
|
||||
|
||||
use super::{
|
||||
Ident, Object, ObjectIndex, ObjectRel, ObjectRelFrom, ObjectRelTy,
|
||||
ObjectRelatable, Pkg,
|
||||
};
|
||||
|
||||
#[cfg(doc)]
|
||||
use super::ObjectKind;
|
||||
|
||||
|
@ -45,7 +41,32 @@ impl Display for Root {
|
|||
object_rel! {
|
||||
/// The root of the graph by definition has no cross edges.
|
||||
Root -> {
|
||||
// Packages are always rooted since they are the toplevel
|
||||
// collection.
|
||||
tree Pkg,
|
||||
tree Ident,
|
||||
|
||||
// Identifiers may optionally be explicitly rooted in contexts where
|
||||
// the system cares only about particular identifiers and their
|
||||
// dependencies,
|
||||
// with `Pkg`s being only incidental.
|
||||
// For example,
|
||||
// `tameld` only links objects that are reachable from identifiers
|
||||
// in the return map.
|
||||
cross Ident,
|
||||
}
|
||||
}
|
||||
|
||||
impl ObjectIndex<Root> {
|
||||
/// Root an identifier in the graph without a parent [`Pkg`].
|
||||
///
|
||||
/// An identifier ought to be rooted by a package that defines it;
|
||||
/// this is intended as a legacy operation for `tameld` that will be
|
||||
/// removed in the future.
|
||||
pub fn root_ident(
|
||||
&self,
|
||||
asg: &mut Asg,
|
||||
oi: ObjectIndex<Ident>,
|
||||
) -> ObjectIndex<Ident> {
|
||||
oi.add_edge_from(asg, *self, None)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,11 +21,8 @@
|
|||
|
||||
use std::fmt::Display;
|
||||
|
||||
use super::{
|
||||
Expr, Ident, Object, ObjectIndex, ObjectRel, ObjectRelFrom, ObjectRelTy,
|
||||
ObjectRelatable,
|
||||
};
|
||||
use crate::{asg::Asg, f::Functor, span::Span};
|
||||
use super::{prelude::*, Doc, Expr, Ident};
|
||||
use crate::{f::Functor, parse::util::SPair, span::Span};
|
||||
|
||||
/// Template with associated name.
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
|
@ -61,15 +58,27 @@ object_rel! {
|
|||
/// Templates may expand into nearly any context,
|
||||
/// and must therefore be able to contain just about anything.
|
||||
Tpl -> {
|
||||
tree Ident,
|
||||
// Expressions must be able to be anonymous to allow templates in
|
||||
// any `Expr` context.
|
||||
tree Expr,
|
||||
|
||||
// Identifiers are used for both references and identifiers that
|
||||
// will expand into an application site.
|
||||
dyn Ident,
|
||||
|
||||
// Template application.
|
||||
tree Tpl,
|
||||
|
||||
// Short template description and arbitrary documentation to be
|
||||
// expanded into the application site.
|
||||
tree Doc,
|
||||
}
|
||||
}
|
||||
|
||||
impl ObjectIndex<Tpl> {
|
||||
/// Complete a template definition.
|
||||
/// Attempt to complete a template definition.
|
||||
///
|
||||
/// This simply updates the span of the template to encompass the entire
|
||||
/// 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| {
|
||||
|
@ -78,4 +87,43 @@ impl ObjectIndex<Tpl> {
|
|||
})
|
||||
})
|
||||
}
|
||||
|
||||
/// Apply a named template `id` to the context of `self`.
|
||||
///
|
||||
/// During evaluation,
|
||||
/// this application will expand the template in place,
|
||||
/// re-binding metavariables to the context of `self`.
|
||||
pub fn apply_named_tpl(
|
||||
self,
|
||||
asg: &mut Asg,
|
||||
oi_apply: ObjectIndex<Ident>,
|
||||
ref_span: Span,
|
||||
) -> Self {
|
||||
self.add_edge_to(asg, oi_apply, Some(ref_span))
|
||||
}
|
||||
|
||||
/// Directly reference this template from another object
|
||||
/// `oi_target_parent`,
|
||||
/// indicating the intent to expand the template in place.
|
||||
///
|
||||
/// This direct reference allows applying anonymous templates.
|
||||
///
|
||||
/// The term "expansion" is equivalent to the application of a closed
|
||||
/// template.
|
||||
/// If this template is _not_ closed,
|
||||
/// it will result in an error during evaluation.
|
||||
pub fn expand_into<OP: ObjectIndexRelTo<Tpl>>(
|
||||
self,
|
||||
asg: &mut Asg,
|
||||
oi_target_parent: OP,
|
||||
) -> Self {
|
||||
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 {
|
||||
let oi_doc = asg.create(Doc::new_text(text));
|
||||
self.add_edge_to(asg, oi_doc, None)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,10 +17,10 @@
|
|||
// 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::super::error::AsgError;
|
||||
use super::*;
|
||||
use crate::{num::Dim, span::dummy::*, sym::GlobalSymbolIntern};
|
||||
use std::{assert_matches::assert_matches, convert::Infallible};
|
||||
use crate::{parse::util::SPair, span::dummy::*};
|
||||
use object::Ident;
|
||||
use std::convert::Infallible;
|
||||
|
||||
type Sut = Asg;
|
||||
|
||||
|
@ -33,334 +33,6 @@ fn create_with_capacity() {
|
|||
let (nc, ec) = sut.graph.capacity();
|
||||
assert!(nc >= node_capacity);
|
||||
assert!(ec >= edge_capacity);
|
||||
assert!(sut.index.capacity() >= node_capacity);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn declare_new_unique_idents() -> AsgResult<()> {
|
||||
let mut sut = Sut::new();
|
||||
|
||||
// NB: The index ordering is important! We first use a larger
|
||||
// index to create a gap, and then use an index within that gap
|
||||
// to ensure that it's not considered an already-defined
|
||||
// identifier.
|
||||
let syma = "syma".into();
|
||||
let symb = "symab".into();
|
||||
|
||||
let nodea = sut.declare(
|
||||
SPair(syma, S1),
|
||||
IdentKind::Meta,
|
||||
Source {
|
||||
desc: Some("a".into()),
|
||||
..Default::default()
|
||||
},
|
||||
)?;
|
||||
|
||||
let nodeb = sut.declare(
|
||||
SPair(symb, S2),
|
||||
IdentKind::Worksheet,
|
||||
Source {
|
||||
desc: Some("b".into()),
|
||||
..Default::default()
|
||||
},
|
||||
)?;
|
||||
|
||||
assert_ne!(nodea, nodeb);
|
||||
|
||||
let givena = sut.get_ident(nodea).unwrap();
|
||||
assert_eq!(SPair(syma, S1), givena.name());
|
||||
assert_eq!(Some(&IdentKind::Meta), givena.kind());
|
||||
assert_eq!(
|
||||
Some(&Source {
|
||||
desc: Some("a".into()),
|
||||
..Default::default()
|
||||
},),
|
||||
givena.src()
|
||||
);
|
||||
|
||||
let givenb = sut.get_ident(nodeb).unwrap();
|
||||
assert_eq!(SPair(symb, S2), givenb.name());
|
||||
assert_eq!(Some(&IdentKind::Worksheet), givenb.kind());
|
||||
assert_eq!(
|
||||
Some(&Source {
|
||||
desc: Some("b".into()),
|
||||
..Default::default()
|
||||
}),
|
||||
givenb.src()
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn declare_kind_auto_root() -> AsgResult<()> {
|
||||
let mut sut = Sut::new();
|
||||
|
||||
let auto_kind = IdentKind::Worksheet;
|
||||
// Sanity check, in case this changes.
|
||||
assert!(auto_kind.is_auto_root());
|
||||
|
||||
let auto_root_node = sut.declare(
|
||||
SPair("auto_root".into(), S1),
|
||||
auto_kind,
|
||||
Default::default(),
|
||||
)?;
|
||||
|
||||
// Should have been automatically added as a root.
|
||||
assert!(sut
|
||||
.graph
|
||||
.contains_edge(sut.root_node, auto_root_node.into()));
|
||||
|
||||
let no_auto_kind = IdentKind::Tpl;
|
||||
assert!(!no_auto_kind.is_auto_root());
|
||||
|
||||
let no_auto_root_node = sut.declare(
|
||||
SPair("no_auto_root".into(), S2),
|
||||
no_auto_kind,
|
||||
Default::default(),
|
||||
)?;
|
||||
|
||||
// Non-auto-roots should _not_ be added as roots automatically.
|
||||
assert!(!sut
|
||||
.graph
|
||||
.contains_edge(sut.root_node, no_auto_root_node.into()));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lookup_by_symbol() -> AsgResult<()> {
|
||||
let mut sut = Sut::new();
|
||||
|
||||
let id = SPair("lookup".into(), S1);
|
||||
let node = sut.declare(
|
||||
id,
|
||||
IdentKind::Meta,
|
||||
Source {
|
||||
generated: true,
|
||||
..Default::default()
|
||||
},
|
||||
)?;
|
||||
|
||||
assert_eq!(Some(node), sut.lookup(id));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn declare_fails_if_transition_fails() -> AsgResult<()> {
|
||||
let mut sut = Sut::new();
|
||||
|
||||
let sym = "symdup".into();
|
||||
let src = Source {
|
||||
desc: Some("orig".into()),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
// Set up an object to fail redeclaration.
|
||||
let node = sut.declare(SPair(sym, S1), IdentKind::Meta, src.clone())?;
|
||||
let result =
|
||||
sut.declare(SPair(sym, S2), IdentKind::Meta, Source::default());
|
||||
|
||||
assert_matches!(result, Err(AsgError::IdentTransition(..)));
|
||||
|
||||
// The node should have been restored.
|
||||
assert_eq!(Some(&src), sut.get_ident(node).unwrap().src());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn declare_extern_returns_existing() -> AsgResult<()> {
|
||||
let mut sut = Sut::new();
|
||||
|
||||
let sym = "symext".into();
|
||||
let src = Source::default();
|
||||
let kind = IdentKind::Class(Dim::Matrix);
|
||||
let node = sut.declare_extern(SPair(sym, S1), kind.clone(), src.clone())?;
|
||||
|
||||
let resrc = Source {
|
||||
desc: Some("redeclare".into()),
|
||||
..Default::default()
|
||||
};
|
||||
let redeclare =
|
||||
sut.declare_extern(SPair(sym, S2), kind.clone(), resrc.clone())?;
|
||||
|
||||
assert_eq!(node, redeclare);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Builds upon declare_returns_existing.
|
||||
#[test]
|
||||
fn declare_extern_fails_if_transition_fails() -> AsgResult<()> {
|
||||
let mut sut = Sut::new();
|
||||
|
||||
let sym = "symdup".into();
|
||||
let src = Source {
|
||||
desc: Some("orig".into()),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let node = sut.declare(SPair(sym, S1), IdentKind::Meta, src.clone())?;
|
||||
|
||||
// Changes kind, which is invalid.
|
||||
let result = sut.declare_extern(
|
||||
SPair(sym, S2),
|
||||
IdentKind::Worksheet,
|
||||
Source::default(),
|
||||
);
|
||||
|
||||
assert_matches!(result, Err(AsgError::IdentTransition(..)));
|
||||
|
||||
// The node should have been restored.
|
||||
assert_eq!(Some(&src), sut.get_ident(node).unwrap().src());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_fragment_to_ident() -> AsgResult<()> {
|
||||
let mut sut = Sut::new();
|
||||
|
||||
let sym = "tofrag".into();
|
||||
let src = Source {
|
||||
generated: true,
|
||||
..Default::default()
|
||||
};
|
||||
let node = sut.declare(SPair(sym, S1), IdentKind::Meta, src.clone())?;
|
||||
|
||||
let fragment = "a fragment".intern();
|
||||
let node_with_frag = sut.set_fragment(SPair(sym, S2), fragment)?;
|
||||
|
||||
// Attaching a fragment should _replace_ the node, not create a
|
||||
// new one
|
||||
assert_eq!(
|
||||
node, node_with_frag,
|
||||
"fragment node does not match original node"
|
||||
);
|
||||
|
||||
let obj = sut.get_ident(node).unwrap();
|
||||
|
||||
assert_eq!(SPair(sym, S1), obj.name());
|
||||
assert_eq!(Some(&IdentKind::Meta), obj.kind());
|
||||
assert_eq!(Some(&src), obj.src());
|
||||
assert_eq!(Some(fragment), obj.fragment());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_fragment_to_ident_fails_if_transition_fails() -> AsgResult<()> {
|
||||
let mut sut = Sut::new();
|
||||
|
||||
let sym = "failfrag".into();
|
||||
let src = Source {
|
||||
generated: true,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
// The failure will come from terr below, not this.
|
||||
let node = sut.declare(SPair(sym, S1), IdentKind::Meta, src.clone())?;
|
||||
|
||||
// The first set will succeed.
|
||||
sut.set_fragment(SPair(sym, S2), "".into())?;
|
||||
|
||||
// This will fail.
|
||||
let result = sut.set_fragment(SPair(sym, S3), "".into());
|
||||
|
||||
// The node should have been restored.
|
||||
let obj = sut.get_ident(node).unwrap();
|
||||
|
||||
assert_eq!(SPair(sym, S1), obj.name());
|
||||
assert_matches!(result, Err(AsgError::IdentTransition(..)));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_ident_dep_to_ident() -> AsgResult<()> {
|
||||
let mut sut = Sut::new();
|
||||
|
||||
let sym = "sym".into();
|
||||
let dep = "dep".into();
|
||||
|
||||
let symnode =
|
||||
sut.declare(SPair(sym, S1), IdentKind::Meta, Source::default())?;
|
||||
let depnode =
|
||||
sut.declare(SPair(dep, S2), IdentKind::Meta, Source::default())?;
|
||||
|
||||
sut.add_dep(symnode, depnode);
|
||||
assert!(sut.has_dep(symnode, depnode));
|
||||
|
||||
// sanity check if we re-add a dep
|
||||
sut.add_dep(symnode, depnode);
|
||||
assert!(sut.has_dep(symnode, depnode));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// same as above test
|
||||
#[test]
|
||||
fn add_dep_lookup_existing() -> AsgResult<()> {
|
||||
let mut sut = Sut::new();
|
||||
|
||||
let sym = SPair("sym".into(), S1);
|
||||
let dep = SPair("dep".into(), S2);
|
||||
|
||||
let _ = sut.declare(sym, IdentKind::Meta, Source::default())?;
|
||||
let _ = sut.declare(dep, IdentKind::Meta, Source::default())?;
|
||||
|
||||
let (symnode, depnode) = sut.add_dep_lookup(sym, dep);
|
||||
assert!(sut.has_dep(symnode, depnode));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_dep_lookup_missing() -> AsgResult<()> {
|
||||
let mut sut = Sut::new();
|
||||
|
||||
let sym = SPair("sym".into(), S1);
|
||||
let dep = SPair("dep".into(), S2);
|
||||
|
||||
// both of these are missing
|
||||
let (symnode, depnode) = sut.add_dep_lookup(sym, dep);
|
||||
assert!(sut.has_dep(symnode, depnode));
|
||||
|
||||
assert_eq!(sym, sut.get_ident(symnode).unwrap().name());
|
||||
assert_eq!(dep, sut.get_ident(depnode).unwrap().name());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn declare_return_missing_symbol() -> AsgResult<()> {
|
||||
let mut sut = Sut::new();
|
||||
|
||||
let sym = SPair("sym".into(), S1);
|
||||
let dep = SPair("dep".into(), S2);
|
||||
|
||||
// both of these are missing, see add_dep_lookup_missing
|
||||
let (symnode, _) = sut.add_dep_lookup(sym, dep);
|
||||
|
||||
let src = Source {
|
||||
desc: Some("redeclare missing".into()),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
// Check with a declared value
|
||||
let declared = sut.declare(sym, IdentKind::Meta, src.clone())?;
|
||||
|
||||
assert_eq!(symnode, declared);
|
||||
|
||||
let obj = sut.get_ident(declared).unwrap();
|
||||
|
||||
assert_eq!(sym, obj.name());
|
||||
assert_eq!(Some(&IdentKind::Meta), obj.kind());
|
||||
assert_eq!(Some(&src), obj.src());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
@ -20,257 +20,11 @@
|
|||
//! Graph traversals.
|
||||
//!
|
||||
//! The traversal [`tree_reconstruction`] should be used if the intent is to
|
||||
//! reconstruct a source representation of the program from the current
|
||||
//! state of [`Asg`].
|
||||
//! reconstruct a source representation of a compilation unit from the current
|
||||
//! state of the [`Asg`](super::Asg).
|
||||
|
||||
use std::fmt::Display;
|
||||
mod ontree;
|
||||
mod topo;
|
||||
|
||||
use super::{object::DynObjectRel, Asg, Object, ObjectIndex};
|
||||
use crate::{
|
||||
parse::{self, Token},
|
||||
span::{Span, UNKNOWN_SPAN},
|
||||
};
|
||||
|
||||
// Re-export so that users of this API can avoid an awkward import from a
|
||||
// completely different module hierarchy.
|
||||
pub use crate::xir::flat::Depth;
|
||||
|
||||
#[cfg(doc)]
|
||||
use super::object::ObjectRel;
|
||||
|
||||
/// Produce an iterator suitable for reconstructing a source tree based on
|
||||
/// the contents of the [`Asg`].
|
||||
///
|
||||
/// The implementation of this traversal is exceedingly simple because of
|
||||
/// its reliance on important graph invariants,
|
||||
/// but it embodies a number of important and subtle properties.
|
||||
///
|
||||
/// Traversal Properties
|
||||
/// ====================
|
||||
/// 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,
|
||||
/// and may be emitted more than once if
|
||||
/// (a) they are the destination of cross edges or
|
||||
/// (b) they are shared between trees
|
||||
/// (most likely due to compiler optimizations).
|
||||
///
|
||||
/// The tree is defined by the graph ontology,
|
||||
/// not an arbitrary graph traversal.
|
||||
/// This traversal is initialized by pushing each target [`ObjectIndex`] of
|
||||
/// the ASG root
|
||||
/// (see [`Asg::root`])
|
||||
/// onto the stack.
|
||||
/// Each iteration pops a single node off of the stack and visits it,
|
||||
/// until no more nodes remain on the stack,
|
||||
/// after which the traversal completes and the iterator is exhausted.
|
||||
/// If the node was reached via a tree edge,
|
||||
/// its edge targets are pushed onto the stack.
|
||||
/// If a node is a target of a cross edge,
|
||||
/// its edges targets are _not_ added to the stack for later traversal.
|
||||
///
|
||||
/// Targets of a cross edge
|
||||
/// (see [`ObjectRel::is_cross_edge`])
|
||||
/// will be emitted multiple times:
|
||||
///
|
||||
/// 1. The target of a cross edge is emitted each time a cross edge is
|
||||
/// followed; and
|
||||
/// 2. When the node is encountered on a tree edge.
|
||||
///
|
||||
/// The traversal relies on the ontology to enforce a tree-like structure
|
||||
/// and to properly define cross edges via `ObjectRel::is_cross_edge`.
|
||||
/// A _tree edge_ is an edge that is not a cross edge.
|
||||
/// Consequently,
|
||||
/// if a cross edge is replaced by a tree edge,
|
||||
/// then this traversal interprets that edge as part of _multiple_ trees,
|
||||
/// effectively inlining it as if the user had entered the exact same
|
||||
/// code in both locations.
|
||||
/// You should choose carefully where in the lowering pipeline you wish
|
||||
/// for this traversal to take place so that the tree reconstruction has
|
||||
/// the desired properties.
|
||||
///
|
||||
/// Because the graph is expected to be a DAG
|
||||
/// (directed acyclic graph),
|
||||
/// this traversal _does not track visited nodes_;
|
||||
/// this ensures that nodes shared by trees due to optimizations like
|
||||
/// common subexpression elimination will have proper trees
|
||||
/// reconstructed.
|
||||
/// If there are exceptional subgraphs where cycles do appear,
|
||||
/// this traversal's implementation must be modified to take them into
|
||||
/// account,
|
||||
/// otherwise it will iterate indefinitely.
|
||||
///
|
||||
/// Edges are visited in the same order that they were added to the graph,
|
||||
/// so the tree reconstruction should match closely the order of the
|
||||
/// source file.
|
||||
/// However,
|
||||
/// note that compiler passes,
|
||||
/// if present,
|
||||
/// may modify the graph beyond recognition,
|
||||
/// though they should retain ordering where it is important.
|
||||
///
|
||||
/// For more information,
|
||||
/// see [`ObjectRel::is_cross_edge`].
|
||||
///
|
||||
/// [w-depth-first-search]: https://en.wikipedia.org/wiki/Depth-first_search
|
||||
///
|
||||
/// Depth Tracking
|
||||
/// ==============
|
||||
/// Each [`ObjectIndex`] 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,
|
||||
/// the [`Depth`] value will always be ≥1.
|
||||
/// Because nodes are always visited when an edge is followed,
|
||||
/// a lower [`Depth`] will always be emitted prior to switching tree
|
||||
/// branches.
|
||||
///
|
||||
/// Let _S_ be an undirected spanning tree formed from the ontological tree.
|
||||
/// At each iteration,
|
||||
/// one of the following will be true:
|
||||
///
|
||||
/// 1. [`Depth`] will increase by 1,
|
||||
/// representing a tree edge or a cross edge;
|
||||
/// 2. [`Depth`] will remain unchanged from the previous iteration,
|
||||
/// representing a sibling node in the tree; or
|
||||
/// 3. [`Depth`] will decrease by ≥1,
|
||||
/// representing a back edge to an ancestor node on _S_.
|
||||
///
|
||||
/// This depth information is the only means by which to reconstruct the
|
||||
/// structure of the tree from the emitted [`ObjectIndex`]es.
|
||||
/// For example,
|
||||
/// if you are producing output in a nested format like XML,
|
||||
/// an unchanged depth means that the current element should be closed
|
||||
/// and a new one opened,
|
||||
/// and you will close _one or more_ elements on a back edge.
|
||||
///
|
||||
/// Note that,
|
||||
/// because the [`Depth`] represents the current _path_,
|
||||
/// the same [`ObjectIndex`] may be emitted multiple times with different
|
||||
/// [`Depth`]s.
|
||||
pub fn tree_reconstruction(asg: &Asg) -> TreePreOrderDfs {
|
||||
TreePreOrderDfs::new(asg)
|
||||
}
|
||||
|
||||
/// Pre-order depth-first search (DFS) using the ontological tree.
|
||||
///
|
||||
/// This DFS has an interesting property:
|
||||
/// _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
|
||||
/// more than once.
|
||||
///
|
||||
/// See [`tree_reconstruction`] for more information.
|
||||
pub struct TreePreOrderDfs<'a> {
|
||||
/// Reference [`Asg`].
|
||||
///
|
||||
/// Holding a reference to the [`Asg`] allows us to serve conveniently
|
||||
/// as an iterator.
|
||||
asg: &'a Asg,
|
||||
|
||||
/// DFS stack.
|
||||
///
|
||||
/// As objects (nodes/vertices) are visited,
|
||||
/// its relationships (edges) are pushed onto the stack.
|
||||
/// Each iterator pops a relationship off the stack and visits it.
|
||||
///
|
||||
/// The traversal ends once the stack becomes empty.
|
||||
stack: Vec<(DynObjectRel, Depth)>,
|
||||
}
|
||||
|
||||
/// Initial size of the DFS stack for [`TreePreOrderDfs`].
|
||||
///
|
||||
/// TODO: Derive a heuristic from our systems.
|
||||
const TREE_INITIAL_STACK_SIZE: usize = 8;
|
||||
|
||||
impl<'a> TreePreOrderDfs<'a> {
|
||||
fn new(asg: &'a Asg) -> Self {
|
||||
let span = UNKNOWN_SPAN;
|
||||
|
||||
let mut dfs = Self {
|
||||
asg,
|
||||
stack: Vec::with_capacity(TREE_INITIAL_STACK_SIZE),
|
||||
};
|
||||
|
||||
let root = asg.root(span);
|
||||
dfs.push_edges_of(root.widen(), 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);
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Iterator for TreePreOrderDfs<'a> {
|
||||
type Item = TreeWalkRel;
|
||||
|
||||
/// Produce the next [`ObjectIndex`] from the traversal in pre-order.
|
||||
///
|
||||
/// An [`ObjectIndex`] may be emitted more than once;
|
||||
/// see [`tree_reconstruction`] for more information.
|
||||
///
|
||||
/// Each item contains a corresponding [`Depth`],
|
||||
/// which represents the depth of the tree derived from the ASG,
|
||||
/// _not_ the level of nesting of the source language used to
|
||||
/// populate the graph.
|
||||
/// This depth is the only way to derive the tree structure from this
|
||||
/// iterator.
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
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);
|
||||
}
|
||||
|
||||
Some(TreeWalkRel(rel, depth))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub struct TreeWalkRel(pub DynObjectRel, pub Depth);
|
||||
|
||||
impl Display for TreeWalkRel {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
match self {
|
||||
Self(dyn_rel, depth) => {
|
||||
write!(f, "{dyn_rel} at tree depth {depth}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Token for TreeWalkRel {
|
||||
fn ir_name() -> &'static str {
|
||||
"ASG ontological tree pre-order DFS walk"
|
||||
}
|
||||
|
||||
/// Token context span.
|
||||
///
|
||||
/// Note that this is _not_ the same span as other token
|
||||
/// implementations,
|
||||
/// and may default to [`UNKNOWN_SPAN`].
|
||||
/// This is because the token is derived from the relationships on the
|
||||
/// graph,
|
||||
/// while concrete spans are stored on the objects that those
|
||||
/// relationships reference.
|
||||
/// This will return a potentially-useful span only if the inner
|
||||
/// [`DynObjectRel::ctx_span`] does.
|
||||
fn span(&self) -> Span {
|
||||
match self {
|
||||
Self(dyn_rel, _) => dyn_rel.ctx_span().unwrap_or(UNKNOWN_SPAN),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl parse::Object for TreeWalkRel {}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test;
|
||||
pub use ontree::{tree_reconstruction, Depth, TreePreOrderDfs, TreeWalkRel};
|
||||
pub use topo::{topo_sort, Cycle, TopoPostOrderDfs};
|
||||
|
|
|
@ -0,0 +1,287 @@
|
|||
// Ontological tree preorder ASG traversal
|
||||
//
|
||||
// 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/>.
|
||||
|
||||
//! Preorder traversal of ontological tree.
|
||||
//!
|
||||
//! This traversal is instantiated by [`tree_reconstruction`].
|
||||
//!
|
||||
//! This traversal should be used to reconstruct a source representation of
|
||||
//! the compilation unit from the current state of the [`Asg`].
|
||||
//!
|
||||
//! Traversal Properties
|
||||
//! ====================
|
||||
//! 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,
|
||||
//! and may be emitted more than once if
|
||||
//! (a) they are the destination of cross edges or
|
||||
//! (b) they are shared between trees
|
||||
//! (most likely due to compiler optimizations).
|
||||
//!
|
||||
//! The tree is defined by the graph ontology,
|
||||
//! not an arbitrary graph traversal.
|
||||
//! This traversal is initialized by pushing each target [`ObjectIndex`] of
|
||||
//! the ASG root
|
||||
//! (see [`Asg::root`])
|
||||
//! onto the stack.
|
||||
//! Each iteration pops a single node off of the stack and visits it,
|
||||
//! until no more nodes remain on the stack,
|
||||
//! after which the traversal completes and the iterator is exhausted.
|
||||
//! If the node was reached via a tree edge,
|
||||
//! its edge targets are pushed onto the stack.
|
||||
//! If a node is a target of a cross edge,
|
||||
//! its edges targets are _not_ added to the stack for later traversal.
|
||||
//!
|
||||
//! Targets of a cross edge
|
||||
//! (see [`ObjectRel::is_cross_edge`])
|
||||
//! will be emitted multiple times:
|
||||
//!
|
||||
//! 1. The target of a cross edge is emitted each time a cross edge is
|
||||
//! followed; and
|
||||
//! 2. When the node is encountered on a tree edge.
|
||||
//!
|
||||
//! The traversal relies on the ontology to enforce a tree-like structure
|
||||
//! and to properly define cross edges via `ObjectRel::is_cross_edge`.
|
||||
//! A _tree edge_ is an edge that is not a cross edge.
|
||||
//! Consequently,
|
||||
//! if a cross edge is replaced by a tree edge,
|
||||
//! then this traversal interprets that edge as part of _multiple_ trees,
|
||||
//! effectively inlining it as if the user had entered the exact same
|
||||
//! code in both locations.
|
||||
//! You should choose carefully where in the lowering pipeline you wish
|
||||
//! for this traversal to take place so that the tree reconstruction has
|
||||
//! the desired properties.
|
||||
//!
|
||||
//! Because the graph is expected to be a DAG
|
||||
//! (directed acyclic graph),
|
||||
//! this traversal _does not track visited nodes_;
|
||||
//! this ensures that nodes shared by trees due to optimizations like
|
||||
//! common subexpression elimination will have proper trees
|
||||
//! reconstructed.
|
||||
//! If there are exceptional subgraphs where cycles do appear,
|
||||
//! this traversal's implementation must be modified to take them into
|
||||
//! account,
|
||||
//! otherwise it will iterate indefinitely.
|
||||
//!
|
||||
//! Edges are visited in the same order that they were added to the graph,
|
||||
//! so the tree reconstruction should match closely the order of the
|
||||
//! source file.
|
||||
//! However,
|
||||
//! note that compiler passes,
|
||||
//! if present,
|
||||
//! may modify the graph beyond recognition,
|
||||
//! though they should retain ordering where it is important.
|
||||
//!
|
||||
//! _Objects that do not have a path from the root will not be visited by
|
||||
//! this traversal._
|
||||
//! These objects are expected to act as additional metadata,
|
||||
//! and must be queried for explicitly.
|
||||
//! Such querying can be done during the traversal since this visitor holds
|
||||
//! only a shared immutable reference to the [`Asg`].
|
||||
//!
|
||||
//! For more information,
|
||||
//! see [`ObjectRel::is_cross_edge`].
|
||||
//!
|
||||
//! [w-depth-first-search]: https://en.wikipedia.org/wiki/Depth-first_search
|
||||
//!
|
||||
//! Depth Tracking
|
||||
//! ==============
|
||||
//! Each [`ObjectIndex`] 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,
|
||||
//! the [`Depth`] value will always be ≥1.
|
||||
//! Because nodes are always visited when an edge is followed,
|
||||
//! a lower [`Depth`] will always be emitted prior to switching tree
|
||||
//! branches.
|
||||
//!
|
||||
//! Let _S_ be an undirected spanning tree formed from the ontological tree.
|
||||
//! At each iteration,
|
||||
//! one of the following will be true:
|
||||
//!
|
||||
//! 1. [`Depth`] will increase by 1,
|
||||
//! representing a tree edge or a cross edge;
|
||||
//! 2. [`Depth`] will remain unchanged from the previous iteration,
|
||||
//! representing a sibling node in the tree; or
|
||||
//! 3. [`Depth`] will decrease by ≥1,
|
||||
//! representing a back edge to an ancestor node on _S_.
|
||||
//!
|
||||
//! This depth information is the only means by which to reconstruct the
|
||||
//! structure of the tree from the emitted [`ObjectIndex`]es.
|
||||
//! For example,
|
||||
//! if you are producing output in a nested format like XML,
|
||||
//! an unchanged depth means that the current element should be closed
|
||||
//! and a new one opened,
|
||||
//! and you will close _one or more_ elements on a back edge.
|
||||
//!
|
||||
//! Note that,
|
||||
//! because the [`Depth`] represents the current _path_,
|
||||
//! the same [`ObjectIndex`] may be emitted multiple times with different
|
||||
//! [`Depth`]s.
|
||||
|
||||
use std::fmt::Display;
|
||||
|
||||
use super::super::{object::DynObjectRel, Asg, Object, ObjectIndex};
|
||||
use crate::{
|
||||
parse::{self, Token},
|
||||
span::{Span, UNKNOWN_SPAN},
|
||||
};
|
||||
|
||||
// Re-export so that users of this API can avoid an awkward import from a
|
||||
// completely different module hierarchy.
|
||||
pub use crate::xir::flat::Depth;
|
||||
|
||||
#[cfg(doc)]
|
||||
use super::super::object::ObjectRel;
|
||||
|
||||
/// Produce an iterator suitable for reconstructing a source tree based on
|
||||
/// the contents of the [`Asg`].
|
||||
///
|
||||
/// The implementation of this traversal is exceedingly simple because of
|
||||
/// its reliance on important graph invariants,
|
||||
/// but it embodies a number of important and subtle properties.
|
||||
///
|
||||
/// See the [module-level documentation](super) for important information
|
||||
/// about this traversal.
|
||||
pub fn tree_reconstruction(asg: &Asg) -> TreePreOrderDfs {
|
||||
TreePreOrderDfs::new(asg)
|
||||
}
|
||||
|
||||
/// Pre-order depth-first search (DFS) using the ontological tree.
|
||||
///
|
||||
/// This DFS has an interesting property:
|
||||
/// _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
|
||||
/// more than once.
|
||||
///
|
||||
/// See [`tree_reconstruction`] for more information.
|
||||
pub struct TreePreOrderDfs<'a> {
|
||||
/// Reference [`Asg`].
|
||||
///
|
||||
/// Holding a reference to the [`Asg`] allows us to serve conveniently
|
||||
/// as an iterator.
|
||||
asg: &'a Asg,
|
||||
|
||||
/// DFS stack.
|
||||
///
|
||||
/// As objects (nodes/vertices) are visited,
|
||||
/// its relationships (edges) are pushed onto the stack.
|
||||
/// Each iterator pops a relationship off the stack and visits it.
|
||||
///
|
||||
/// The traversal ends once the stack becomes empty.
|
||||
stack: Vec<(DynObjectRel, Depth)>,
|
||||
}
|
||||
|
||||
/// Initial size of the DFS stack for [`TreePreOrderDfs`].
|
||||
///
|
||||
/// TODO: Derive a heuristic from our systems.
|
||||
const TREE_INITIAL_STACK_SIZE: usize = 8;
|
||||
|
||||
impl<'a> TreePreOrderDfs<'a> {
|
||||
fn new(asg: &'a Asg) -> Self {
|
||||
let span = UNKNOWN_SPAN;
|
||||
|
||||
let mut dfs = Self {
|
||||
asg,
|
||||
stack: Vec::with_capacity(TREE_INITIAL_STACK_SIZE),
|
||||
};
|
||||
|
||||
let root = asg.root(span);
|
||||
dfs.push_edges_of(root.widen(), 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);
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Iterator for TreePreOrderDfs<'a> {
|
||||
type Item = TreeWalkRel;
|
||||
|
||||
/// Produce the next [`ObjectIndex`] from the traversal in pre-order.
|
||||
///
|
||||
/// An [`ObjectIndex`] may be emitted more than once;
|
||||
/// see [`tree_reconstruction`] for more information.
|
||||
///
|
||||
/// Each item contains a corresponding [`Depth`],
|
||||
/// which represents the depth of the tree derived from the ASG,
|
||||
/// _not_ the level of nesting of the source language used to
|
||||
/// populate the graph.
|
||||
/// This depth is the only way to derive the tree structure from this
|
||||
/// iterator.
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
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);
|
||||
}
|
||||
|
||||
Some(TreeWalkRel(rel, depth))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub struct TreeWalkRel(pub DynObjectRel, pub Depth);
|
||||
|
||||
impl Display for TreeWalkRel {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
match self {
|
||||
Self(dyn_rel, depth) => {
|
||||
write!(f, "{dyn_rel} at tree depth {depth}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Token for TreeWalkRel {
|
||||
fn ir_name() -> &'static str {
|
||||
"ASG ontological tree pre-order DFS walk"
|
||||
}
|
||||
|
||||
/// Token context span.
|
||||
///
|
||||
/// Note that this is _not_ the same span as other token
|
||||
/// implementations,
|
||||
/// and may default to [`UNKNOWN_SPAN`].
|
||||
/// This is because the token is derived from the relationships on the
|
||||
/// graph,
|
||||
/// while concrete spans are stored on the objects that those
|
||||
/// relationships reference.
|
||||
/// This will return a potentially-useful span only if the inner
|
||||
/// [`DynObjectRel::ctx_span`] does.
|
||||
fn span(&self) -> Span {
|
||||
match self {
|
||||
Self(dyn_rel, _) => dyn_rel.ctx_span().unwrap_or(UNKNOWN_SPAN),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl parse::Object for TreeWalkRel {}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test;
|
|
@ -0,0 +1,330 @@
|
|||
// Test ontological tree preorder ASG traversal
|
||||
//
|
||||
// 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::{
|
||||
asg::{
|
||||
air::{Air, AirAggregate},
|
||||
graph::object::ObjectRelTy,
|
||||
ExprOp,
|
||||
},
|
||||
f::Functor,
|
||||
parse::{util::SPair, ParseState},
|
||||
span::{dummy::*, Span},
|
||||
};
|
||||
use std::fmt::Debug;
|
||||
|
||||
use Air::*;
|
||||
|
||||
// More concise values for tables below.
|
||||
use ObjectRelTy::*;
|
||||
const SU: Span = UNKNOWN_SPAN;
|
||||
|
||||
fn tree_reconstruction_report<I: IntoIterator<Item = Air>>(
|
||||
toks: I,
|
||||
) -> Vec<(DynObjectRel<Span, Span>, Depth)>
|
||||
where
|
||||
I::IntoIter: Debug,
|
||||
{
|
||||
let mut parser = AirAggregate::parse(toks.into_iter());
|
||||
assert!(parser.all(|x| x.is_ok()));
|
||||
|
||||
let asg = &parser.finalize().unwrap().into_context().finish();
|
||||
|
||||
tree_reconstruction(asg)
|
||||
.map(|TreeWalkRel(rel, depth)| {
|
||||
(
|
||||
rel.map(|(soi, toi)| {
|
||||
(soi.resolve(asg).span(), toi.resolve(asg).span())
|
||||
}),
|
||||
depth,
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
// Note that this is an integration test beginning at AIR.
|
||||
// It is therefore fragile,
|
||||
// and will break if any of these complex systems fail in subtle ways.
|
||||
// If you arrived at this test without having modified the visitor,
|
||||
// then it's quite possible that you should be looking elsewhere.
|
||||
//
|
||||
// We will construct a test ASG using the same subsystem as the user;
|
||||
// we want to be sure that the traversal works as we expect it to in
|
||||
// practice,
|
||||
// since the system is fairly complex and failures are more likely
|
||||
// to occur at integration points.
|
||||
#[test]
|
||||
fn traverses_ontological_tree() {
|
||||
let id_a = SPair("expr_a".into(), S3);
|
||||
let id_b = SPair("expr_b".into(), S9);
|
||||
|
||||
#[rustfmt::skip]
|
||||
let toks = vec![
|
||||
PkgStart(S1, SPair("/pkg".into(), S1)),
|
||||
ExprStart(ExprOp::Sum, S2),
|
||||
BindIdent(id_a),
|
||||
|
||||
ExprStart(ExprOp::Sum, S4),
|
||||
ExprEnd(S5),
|
||||
|
||||
RefIdent(SPair(id_b.symbol(), S6)),
|
||||
ExprEnd(S7),
|
||||
|
||||
ExprStart(ExprOp::Sum, S8),
|
||||
BindIdent(id_b),
|
||||
ExprEnd(S10),
|
||||
PkgEnd(S11),
|
||||
];
|
||||
|
||||
// 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();
|
||||
|
||||
// Note that the `Depth` beings at 1 because the actual root of the
|
||||
// graph is not emitted.
|
||||
// Further note that the depth is the depth of the _path_,
|
||||
// and so identifiers contribute to the depth even though the source
|
||||
// language doesn't have such nesting.
|
||||
#[rustfmt::skip]
|
||||
assert_eq!(
|
||||
// A -|-> B | A span -|-> B span | espan | depth
|
||||
vec![//-----|-------|-----------|-----------|--------|-----------------
|
||||
(d(Root, Pkg, SU, m(S1, S11), None ), Depth(1)),
|
||||
(d(Pkg, Ident, m(S1, S11), S3, None ), Depth(2)),
|
||||
(d(Ident, Expr, S3, m(S2, S7), None ), Depth(3)),
|
||||
(d(Expr, Expr, m(S2, S7), m(S4, S5), None ), Depth(4)),
|
||||
(d(Expr, Ident, m(S2, S7), S9, Some(S6)), Depth(4)),
|
||||
(d(Pkg, Ident, m(S1, S11), S9, None ), Depth(2)),
|
||||
(d(Ident, Expr, S9, m(S8, S10), None ), Depth(3)),
|
||||
],
|
||||
tree_reconstruction_report(toks),
|
||||
);
|
||||
}
|
||||
|
||||
// This is a variation of the above test,
|
||||
// focusing on the fact that templates may contain odd constructions that
|
||||
// wouldn't necessarily be valid in other contexts.
|
||||
// This merely establishes a concrete example to re-enforce intuition and
|
||||
// serve as an example of the system's behavior in a laboratory setting,
|
||||
// as opposed to having to scan through real-life traces and all the
|
||||
// complexity and noise therein.
|
||||
//
|
||||
// This also serves as an integration test to ensure that templates produce
|
||||
// the expected result on the graph.
|
||||
// Just as was mentioned above,
|
||||
// that makes this test very fragile,
|
||||
// and you should look at other failing tests before assuming that this
|
||||
// one is broken;
|
||||
// let this help to guide your reasoning of the system rather than
|
||||
// your suspicion.
|
||||
#[test]
|
||||
fn traverses_ontological_tree_tpl_with_sibling_at_increasing_depth() {
|
||||
let id_tpl = SPair("_tpl_".into(), S3);
|
||||
let id_expr = SPair("expr".into(), S7);
|
||||
|
||||
#[rustfmt::skip]
|
||||
let toks = vec![
|
||||
PkgStart(S1, SPair("/pkg".into(), S1)),
|
||||
TplStart(S2),
|
||||
BindIdent(id_tpl),
|
||||
|
||||
// Dangling
|
||||
ExprStart(ExprOp::Sum, S4),
|
||||
ExprEnd(S5),
|
||||
|
||||
// Reachable
|
||||
ExprStart(ExprOp::Sum, S6),
|
||||
BindIdent(id_expr),
|
||||
ExprEnd(S8),
|
||||
TplEnd(S9),
|
||||
PkgEnd(S10),
|
||||
];
|
||||
|
||||
// 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();
|
||||
|
||||
// Writing this example helped to highlight how the system is
|
||||
// functioning and immediately obviated a bug downstream in the
|
||||
// lowering pipeline (xmli derivation) at the time of writing.
|
||||
// The `Tpl->Ident` was ignored along with its `Depth` because it
|
||||
// produced no output,
|
||||
// and therefore the final expression was interpreted as being a
|
||||
// child of its sibling.
|
||||
// This traversal was always correct;
|
||||
// the problem manifested in the integration of these systems and
|
||||
// was caught by system tests.
|
||||
#[rustfmt::skip]
|
||||
assert_eq!(
|
||||
// A -|-> B | A span -|-> B span | espan| depth
|
||||
vec![//-----|-------|-----------|-----------|------|-----------------
|
||||
(d(Root, Pkg, SU, m(S1, S10), None), Depth(1)),
|
||||
(d(Pkg, Ident, m(S1, S10), S3, None), Depth(2)),
|
||||
(d(Ident, Tpl, S3, m(S2, S9), None), Depth(3)),
|
||||
(d(Tpl, Expr, m(S2, S9), m(S4, S5), None), Depth(4)), // --,
|
||||
(d(Tpl, Ident, m(S2, S9), S7, None), Depth(4)), // |
|
||||
(d(Ident, Expr, S7, m(S6, S8), None), Depth(5)), // <'
|
||||
],
|
||||
tree_reconstruction_report(toks),
|
||||
);
|
||||
}
|
||||
|
||||
// The way template applications are handled on the ASG differs from NIR.
|
||||
// This test ensure that the representation on the ASG is precise;
|
||||
// it's far easier to catch those problems here than it is to catch them
|
||||
// in an implementation utilizing these data that's failing in some
|
||||
// unexpected way.
|
||||
#[test]
|
||||
fn traverses_ontological_tree_tpl_apply() {
|
||||
let name_tpl = "_tpl-to-apply_".into();
|
||||
let id_tpl = SPair(name_tpl, S3);
|
||||
let ref_tpl = SPair(name_tpl, S6);
|
||||
let id_param = SPair("@param@".into(), S8);
|
||||
let value_param = SPair("value".into(), S9);
|
||||
|
||||
#[rustfmt::skip]
|
||||
let toks = vec![
|
||||
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
|
||||
PkgEnd(S12),
|
||||
];
|
||||
|
||||
// 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, S12), None ), Depth(1)),
|
||||
(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)),
|
||||
],
|
||||
tree_reconstruction_report(toks),
|
||||
);
|
||||
}
|
||||
|
||||
// A template acts as a container for anything defined therein,
|
||||
// to be expanded into an application site.
|
||||
// This means that identifiers that might otherwise be bound to the package
|
||||
// need to be contained by the template,
|
||||
// and further that identifier _resolution_ must be able to occur within
|
||||
// the template,
|
||||
// e.g. to apply templates defined therein.
|
||||
#[test]
|
||||
fn traverses_ontological_tree_tpl_within_template() {
|
||||
let name_outer = "_tpl-outer_".into();
|
||||
let id_tpl_outer = SPair(name_outer, S3);
|
||||
let name_inner = "_tpl-inner_".into();
|
||||
let id_tpl_inner = SPair(name_inner, S10);
|
||||
let ref_inner_before = SPair(name_inner, S7);
|
||||
let ref_inner_after = SPair(name_inner, S13);
|
||||
|
||||
#[rustfmt::skip]
|
||||
let toks = vec![
|
||||
PkgStart(S1, SPair("/pkg".into(), S1)),
|
||||
TplStart(S2),
|
||||
BindIdent(id_tpl_outer),
|
||||
|
||||
// Anonymous inner template application.
|
||||
TplStart(S4),
|
||||
TplEndRef(S5), // notice the `Ref` at the end
|
||||
|
||||
// Apply above inner template,
|
||||
// _before_ definition,
|
||||
// which will begin as Missing and must be later resolved when
|
||||
// the template is defined.
|
||||
TplStart(S6),
|
||||
RefIdent(ref_inner_before), // --.
|
||||
TplEndRef(S8), // |
|
||||
// |
|
||||
// Named inner template. // |
|
||||
TplStart(S9), // /
|
||||
BindIdent(id_tpl_inner), //<-:
|
||||
TplEnd(S11), // \
|
||||
// |
|
||||
// Apply above inner template, // |
|
||||
// _after_ definition. // |
|
||||
TplStart(S12), // |
|
||||
RefIdent(ref_inner_after), // __/
|
||||
TplEndRef(S14),
|
||||
TplEnd(S15),
|
||||
PkgEnd(S16),
|
||||
];
|
||||
|
||||
// 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, S16), None ), Depth(1)),
|
||||
(d(Pkg, Ident, m(S1, S16), S3, None ), Depth(2)),
|
||||
(d(Ident, Tpl, S3, m(S2, S15), None ), Depth(3)),
|
||||
(d(Tpl, Tpl, m(S2, S15), m(S4, S5), None ), Depth(4)),
|
||||
(d(Tpl, Tpl, m(S2, S15), m(S6, S8), None ), Depth(4)),
|
||||
/*cross*/ (d(Tpl, Ident, m(S6, S8), S10, Some(S7) ), Depth(5)),
|
||||
// ,--------------------------------^^^
|
||||
/* | */ (d(Tpl, Ident, m(S2, S15), S10, None ), Depth(4)),
|
||||
/* | */ (d(Ident, Tpl, S10, m(S9, S11), None ), Depth(5)),
|
||||
/* | */ (d(Tpl, Tpl, m(S2, S15), m(S12,S14), None ), Depth(4)),
|
||||
/*cross*/ (d(Tpl, Ident, m(S12,S14), S10, Some(S13)), Depth(5)),
|
||||
// | // ^^^
|
||||
// | // Note that successfully /
|
||||
// | // resolving this span --`
|
||||
// | // as S10 (Ident::Transparent) instead of S13 (which would mean
|
||||
// | // a new Ident::Missing was created) requires that we resolve
|
||||
// | // a local identifier that is rooted in the _template_ rather
|
||||
// | // than the global scope.
|
||||
// `----> Similarly,
|
||||
// resolving the former as S10 instead of S7 means that a
|
||||
// local identifier that was originally Missing is properly
|
||||
// resolved to Transparent once it was defined;
|
||||
// which asserts consistency in identifier scope regardless
|
||||
// of reference/definition order.
|
||||
// This lexical analysis is something that the XSLT-based TAME
|
||||
// was not capable of doing.
|
||||
],
|
||||
tree_reconstruction_report(toks),
|
||||
);
|
||||
}
|
|
@ -1,195 +0,0 @@
|
|||
// ASG IR
|
||||
//
|
||||
// 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::{
|
||||
asg::{
|
||||
air::{Air, AirAggregate},
|
||||
graph::object::ObjectRelTy,
|
||||
ExprOp,
|
||||
},
|
||||
f::Functor,
|
||||
parse::{util::SPair, ParseState},
|
||||
span::{dummy::*, Span},
|
||||
};
|
||||
use std::fmt::Debug;
|
||||
|
||||
use Air::*;
|
||||
|
||||
// More concise values for tables below.
|
||||
use ObjectRelTy::*;
|
||||
const SU: Span = UNKNOWN_SPAN;
|
||||
|
||||
fn asg_from_toks<I: IntoIterator<Item = Air>>(toks: I) -> Asg
|
||||
where
|
||||
I::IntoIter: Debug,
|
||||
{
|
||||
let mut parser = AirAggregate::parse(toks.into_iter());
|
||||
assert!(parser.all(|x| x.is_ok()));
|
||||
parser.finalize().unwrap().into_context()
|
||||
}
|
||||
|
||||
// Note that this is an integration test beginning at AIR.
|
||||
// It is therefore fragile,
|
||||
// and will break if any of these complex systems fail in subtle ways.
|
||||
// If you arrived at this test without having modified the visitor,
|
||||
// then it's quite possible that you should be looking elsewhere.
|
||||
//
|
||||
// We will construct a test ASG using the same subsystem as the user;
|
||||
// we want to be sure that the traversal works as we expect it to in
|
||||
// practice,
|
||||
// since the system is fairly complex and failures are more likely
|
||||
// to occur at integration points.
|
||||
#[test]
|
||||
fn traverses_ontological_tree() {
|
||||
let id_a = SPair("expr_a".into(), S3);
|
||||
let id_b = SPair("expr_b".into(), S9);
|
||||
|
||||
#[rustfmt::skip]
|
||||
let toks = vec![
|
||||
PkgOpen(S1),
|
||||
ExprOpen(ExprOp::Sum, S2),
|
||||
BindIdent(id_a),
|
||||
|
||||
ExprOpen(ExprOp::Sum, S4),
|
||||
ExprClose(S5),
|
||||
|
||||
RefIdent(SPair(id_b.symbol(), S6)),
|
||||
ExprClose(S7),
|
||||
|
||||
ExprOpen(ExprOp::Sum, S8),
|
||||
BindIdent(id_b),
|
||||
ExprClose(S10),
|
||||
PkgClose(S11),
|
||||
];
|
||||
|
||||
let asg = asg_from_toks(toks);
|
||||
|
||||
// From the above graph,
|
||||
// we're going to traverse in such a way as to reconstruct the source
|
||||
// tree.
|
||||
let sut = tree_reconstruction(&asg);
|
||||
|
||||
// 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();
|
||||
|
||||
// Note that the `Depth` beings at 1 because the actual root of the
|
||||
// graph is not emitted.
|
||||
// Further note that the depth is the depth of the _path_,
|
||||
// and so identifiers contribute to the depth even though the source
|
||||
// language doesn't have such nesting.
|
||||
#[rustfmt::skip]
|
||||
assert_eq!(
|
||||
// A -|-> B | A span -|-> B span | espan | depth
|
||||
vec![//-----|-------|-----------|-----------|--------|-----------------
|
||||
(d(Root, Pkg, SU, m(S1, S11), None ), Depth(1)),
|
||||
(d(Pkg, Ident, m(S1, S11), S3, None ), Depth(2)),
|
||||
(d(Ident, Expr, S3, m(S2, S7), None ), Depth(3)),
|
||||
(d(Expr, Expr, m(S2, S7), m(S4, S5), None ), Depth(4)),
|
||||
(d(Expr, Ident, m(S2, S7), S9, Some(S6)), Depth(4)),
|
||||
(d(Pkg, Ident, m(S1, S11), S9, None ), Depth(2)),
|
||||
(d(Ident, Expr, S9, m(S8, S10), None ), Depth(3)),
|
||||
],
|
||||
sut.map(|TreeWalkRel(rel, depth)| (
|
||||
rel.map(|(soi, toi)| (
|
||||
soi.resolve(&asg).span(),
|
||||
toi.resolve(&asg).span()
|
||||
)),
|
||||
depth
|
||||
)).collect::<Vec<_>>(),
|
||||
);
|
||||
}
|
||||
|
||||
// This is a variation of the above test,
|
||||
// focusing on the fact that templates may contain odd constructions that
|
||||
// wouldn't necessarily be valid in other contexts.
|
||||
// This merely establishes a concrete example to re-enforce intuition and
|
||||
// serve as an example of the system's behavior in a laboratory setting,
|
||||
// as opposed to having to scan through real-life traces and all the
|
||||
// complexity and noise therein.
|
||||
//
|
||||
// This also serves as an integration test to ensure that templates produce
|
||||
// the expected result on the graph.
|
||||
// Just as was mentioned above,
|
||||
// that makes this test very fragile,
|
||||
// and you should look at other failing tests before assuming that this
|
||||
// one is broken;
|
||||
// let this help to guide your reasoning of the system rather than
|
||||
// your suspicion.
|
||||
#[test]
|
||||
fn traverses_ontological_tree_tpl_with_sibling_at_increasing_depth() {
|
||||
let id_tpl = SPair("_tpl_".into(), S3);
|
||||
let id_expr = SPair("expr".into(), S7);
|
||||
|
||||
#[rustfmt::skip]
|
||||
let toks = vec![
|
||||
PkgOpen(S1),
|
||||
TplOpen(S2),
|
||||
BindIdent(id_tpl),
|
||||
|
||||
// Dangling
|
||||
ExprOpen(ExprOp::Sum, S4),
|
||||
ExprClose(S5),
|
||||
|
||||
// Reachable
|
||||
ExprOpen(ExprOp::Sum, S6),
|
||||
BindIdent(id_expr),
|
||||
ExprClose(S8),
|
||||
TplClose(S9),
|
||||
PkgClose(S10),
|
||||
];
|
||||
|
||||
let asg = asg_from_toks(toks);
|
||||
let sut = tree_reconstruction(&asg);
|
||||
|
||||
// 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();
|
||||
|
||||
// Writing this example helped to highlight how the system is
|
||||
// functioning and immediately obviated a bug downstream in the
|
||||
// lowering pipeline (xmli derivation) at the time of writing.
|
||||
// The `Tpl->Ident` was ignored along with its `Depth` because it
|
||||
// produced no output,
|
||||
// and therefore the final expression was interpreted as being a
|
||||
// child of its sibling.
|
||||
// This traversal was always correct;
|
||||
// the problem manifested in the integration of these systems and
|
||||
// was caught by system tests.
|
||||
#[rustfmt::skip]
|
||||
assert_eq!(
|
||||
// A -|-> B | A span -|-> B span | espan| depth
|
||||
vec![//-----|-------|-----------|-----------|------|-----------------
|
||||
(d(Root, Pkg, SU, m(S1, S10), None), Depth(1)),
|
||||
(d(Pkg, Ident, m(S1, S10), S3, None), Depth(2)),
|
||||
(d(Ident, Tpl, S3, m(S2, S9), None), Depth(3)),
|
||||
(d(Tpl, Expr, m(S2, S9), m(S4, S5), None), Depth(4)), // --,
|
||||
(d(Tpl, Ident, m(S2, S9), S7, None), Depth(4)), // |
|
||||
(d(Ident, Expr, S7, m(S6, S8), None), Depth(5)), // <'
|
||||
],
|
||||
sut.map(|TreeWalkRel(rel, depth)| (
|
||||
rel.map(|(soi, toi)| (
|
||||
soi.resolve(&asg).span(),
|
||||
toi.resolve(&asg).span()
|
||||
)),
|
||||
depth
|
||||
)).collect::<Vec<_>>(),
|
||||
);
|
||||
}
|
|
@ -0,0 +1,540 @@
|
|||
// Topological sort ASG traversal
|
||||
//
|
||||
// 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/>.
|
||||
|
||||
//! Topological sort of [`Asg`] with ontological consideration.
|
||||
//!
|
||||
//! This toplogical sort is a depth-first search (DFS) that emits nodes in
|
||||
//! post-order.
|
||||
//! Intuitively,
|
||||
//! it emits objects sorted in such a way that they appear before each of
|
||||
//! their dependencies.
|
||||
//!
|
||||
//! The ordering is deterministic between runs on the same graph,
|
||||
//! but it is only one of potentially many orderings.
|
||||
//!
|
||||
//! The only information provided by this sort is a stream of
|
||||
//! [`ObjectIndex`]es ordered linearly.
|
||||
//! No information about the edge or source object is provided,
|
||||
//! nor is information about the length of the current path,
|
||||
//! since an object may be visited any number of different ways and the
|
||||
//! caller ought not rely on the particular path taken.
|
||||
//! Furthermore,
|
||||
//! an object may be visited any number of times from any number of paths,
|
||||
//! but only the first visit is emitted,
|
||||
//! so any additional information would provide an incomplete picture;
|
||||
//! this sort is _not_ intended to provide information about all paths
|
||||
//! to a particular object and cannot be used in that way.
|
||||
//!
|
||||
//! Cutting Of Cycles
|
||||
//! =================
|
||||
//! A _cycle_ is a path that references another object earlier in the path,
|
||||
//! as if it loops in on itself.
|
||||
//! Cycles are generally not permitted,
|
||||
//! as they would require that a value would have to be computed before it
|
||||
//! could compute itself.
|
||||
//! This almost certainly represents an error in the program's specification.
|
||||
//!
|
||||
//! Cycles are permitted for recursion.
|
||||
//! More information can be found in [`ObjectRel::can_recurse`].
|
||||
//!
|
||||
//! A toplogical ordering is defined only for graphs that do not contain
|
||||
//! cycles.
|
||||
//! To order a graph _with_ cycles,
|
||||
//! the depth-first search performs a _cut_,
|
||||
//! whereby the edge that would have led to the cycle is omitted,
|
||||
//! as if cutting a loop of string at the point that it is tied.
|
||||
//! An example of such a cut can be found in [`ObjectRel::can_recurse`].
|
||||
//!
|
||||
//! This is done in two scenarios:
|
||||
//!
|
||||
//! 1. An unsupported cycle is an error.
|
||||
//! A cut is performed as a means of error recovery so that the process
|
||||
//! may continue and discover more errors before terminating.
|
||||
//!
|
||||
//! 2. A cycle representing allowed recursion performs a cut since the
|
||||
//! path taken thus far already represents a valid ordering.
|
||||
|
||||
use crate::{
|
||||
asg::{
|
||||
graph::object::DynObjectRel, Asg, Object, ObjectIndex,
|
||||
ObjectIndexResolvedSpan, ObjectKind,
|
||||
},
|
||||
diagnose::{Annotate, AnnotatedSpan, Diagnostic},
|
||||
};
|
||||
use fixedbitset::FixedBitSet;
|
||||
use std::{error::Error, fmt::Display, iter::once};
|
||||
|
||||
#[cfg(doc)]
|
||||
use crate::{asg::graph::object::ObjectRel, span::Span};
|
||||
|
||||
/// Topological sort with cutting of ontologically permitted cycles.
|
||||
///
|
||||
/// This is a TAMER-specific topological sort that is aware of the graph's
|
||||
/// ontology and will automatically sort an acyclic subgraph produced by
|
||||
/// cutting permitted cycles.
|
||||
/// See the [module-level documentation](self) for more information.
|
||||
pub fn topo_sort<O: ObjectKind>(
|
||||
asg: &Asg,
|
||||
init: impl Iterator<Item = ObjectIndex<O>>,
|
||||
) -> TopoPostOrderDfs {
|
||||
TopoPostOrderDfs::new(asg, init.map(ObjectIndex::widen))
|
||||
}
|
||||
|
||||
/// Topological sort implemented as a post-order depth-first search (DFS).
|
||||
///
|
||||
/// See the [module-level documentation](self) for important information
|
||||
/// about this traversal.
|
||||
pub struct TopoPostOrderDfs<'a> {
|
||||
/// Reference [`Asg`].
|
||||
///
|
||||
/// Holding a reference to the [`Asg`] allows this object to serve
|
||||
/// conveniently as an iterator.
|
||||
asg: &'a Asg,
|
||||
|
||||
/// DFS stack.
|
||||
///
|
||||
/// As objects (nodes/vertices) are visited,
|
||||
/// its relationships (edge targets) are pushed onto the stack.
|
||||
/// Each iterator pops a relationship off the stack and visits it.
|
||||
///
|
||||
/// The inner [`Result`] serves as a cycle flag set by
|
||||
/// [`Self::flag_or_cut_cycle`].
|
||||
/// Computing the proper [`Cycle`] error before placing it on the stack
|
||||
/// would not only bloat the size of each element of this stack,
|
||||
/// but also use unnecessary memory on the heap.
|
||||
/// The proper [`Cycle`] error will be computed when this element is
|
||||
/// retrieved by [`Self::next_oi`].
|
||||
///
|
||||
/// _This may contain duplicate [`ObjectIndex`]es even if the graph
|
||||
/// contains no cycles;_
|
||||
/// see [`Self::push_neighbors`] for an explanation.
|
||||
///
|
||||
/// The traversal ends once the stack becomes empty.
|
||||
/// It is expected the stack is initialized with at least one initial
|
||||
/// object prior to beginning the traversal.
|
||||
stack: Vec<Result<ObjectIndex<Object>, ObjectIndex<Object>>>,
|
||||
|
||||
/// Objects that have already been added to [`Self::stack`].
|
||||
///
|
||||
/// An object that has already been visited will _not_ be visited
|
||||
/// again.
|
||||
/// A visited object is only present in [`Self::stack`] until it is
|
||||
/// finished,
|
||||
/// after which it appears in [`Self::finished`].
|
||||
visited: FixedBitSet,
|
||||
|
||||
/// Objects that have been emitted and pop'd from [`Self::stack`].
|
||||
///
|
||||
/// This is used for cycle detection.
|
||||
/// Before pushing an object onto [`Self::stack`],
|
||||
/// the system first checks [`Self::visited`].
|
||||
/// If an object has been visited,
|
||||
/// but has not yet been finished,
|
||||
/// then it must still be present on the stack and must therefore
|
||||
/// be part of a cycle.
|
||||
finished: FixedBitSet,
|
||||
}
|
||||
|
||||
pub trait ObjectRelFilter = Fn(DynObjectRel) -> bool;
|
||||
|
||||
/// Initial capacity of the [`TopoPostOrderDfs`] stack.
|
||||
///
|
||||
/// The stack will need to be able to accommodate all nodes and their
|
||||
/// siblings within the longest path taken by the DFS.
|
||||
/// If there are many rooted objects
|
||||
/// (e.g. for `tameld`),
|
||||
/// this may be quite large.
|
||||
///
|
||||
/// The current number is arbitrary and only intended to reduce initial
|
||||
/// small re-allocations;
|
||||
/// it is too small for linking and too large for individual packages.
|
||||
const INIT_STACK_CAP: usize = 32;
|
||||
|
||||
impl<'a> TopoPostOrderDfs<'a> {
|
||||
fn new(
|
||||
asg: &'a Asg,
|
||||
init: impl Iterator<Item = ObjectIndex<Object>>,
|
||||
) -> Self {
|
||||
let set_cap = asg.object_count();
|
||||
|
||||
let mut stack = Vec::with_capacity(INIT_STACK_CAP);
|
||||
init.map(Ok).collect_into(&mut stack);
|
||||
|
||||
Self {
|
||||
asg,
|
||||
stack,
|
||||
visited: FixedBitSet::with_capacity(set_cap),
|
||||
finished: FixedBitSet::with_capacity(set_cap),
|
||||
}
|
||||
}
|
||||
|
||||
/// Push the neighbors of the given [`ObjectIndex`] onto [`Self::stack`]
|
||||
/// for later processing.
|
||||
///
|
||||
/// Placing neighbors on the stack allows us to yield elements from the
|
||||
/// iterator without having to keep track of where we are on the graph
|
||||
/// for each node in the path.
|
||||
///
|
||||
/// When visiting a node for the first time,
|
||||
/// its neighbors
|
||||
/// (objects to which `src_oi` has an edge)
|
||||
/// are pushed onto [`Self::stack`].
|
||||
/// It is expected that `src_oi` is left on the stack,
|
||||
/// ensuring that its neighbors are processed before `src_oi` is,
|
||||
/// leading to a post-order traversal.
|
||||
///
|
||||
/// Objects that have already been emitted will _not_ be pushed onto the
|
||||
/// stack;
|
||||
/// this determination is made by consulting [`Self::finished`].
|
||||
///
|
||||
/// Each object that is pushed onto the stack will be checked by
|
||||
/// [`Self::flag_or_cut_cycle`];
|
||||
/// see that function for more information.
|
||||
/// It is important that each cycle be flagged individually,
|
||||
/// rather than returning an error from this function,
|
||||
/// otherwise only one cycle per object would be found.
|
||||
///
|
||||
/// Duplicate Stack Entries Without Cycles
|
||||
/// ======================================
|
||||
/// [`Self::stack`] may contain duplicate [`ObjectIndex`]es even if
|
||||
/// there is no cycle.
|
||||
///
|
||||
/// The reason for this is that a cycle only occurs when an
|
||||
/// [`ObjectIndex`] is part of the path currently being visited.
|
||||
/// But [`Self::stack`] contains objects that have _not yet been visited_;
|
||||
/// they've been placed onto the stack by this method to be visited at
|
||||
/// a future point.
|
||||
///
|
||||
/// Consider this graph:
|
||||
///
|
||||
/// ```text
|
||||
/// (A) -> (B) -> (D)
|
||||
/// '---> (C) <---'
|
||||
/// ```
|
||||
///
|
||||
/// A traversal might yield this stack if `C` is visited before `B`:
|
||||
///
|
||||
/// ```text
|
||||
/// [A] // root
|
||||
/// [A, C, B] // self.push_neighbors(A)
|
||||
/// [A, C, B, D] // self.push_neighbors(B)
|
||||
/// [A, C, B, D, C] // self.push_neighbors(D)
|
||||
/// ```
|
||||
///
|
||||
/// Since `C` does not contain an edge _to_ any previous object,
|
||||
/// there is no cycle.
|
||||
///
|
||||
/// For this reason,
|
||||
/// it is important for the implementation to check [`Self::finished`]
|
||||
/// when removing objects from the stack to ensure that they have not
|
||||
/// already been emitted.
|
||||
fn push_neighbors(&mut self, src_oi: ObjectIndex<Object>) {
|
||||
self.asg
|
||||
.edges_dyn(src_oi)
|
||||
.filter(|dyn_oi| !self.finished.contains((*dyn_oi.target()).into()))
|
||||
.filter_map(|dyn_oi| {
|
||||
Self::flag_or_cut_cycle(&self.visited, self.asg, dyn_oi)
|
||||
})
|
||||
.collect_into(&mut self.stack);
|
||||
}
|
||||
|
||||
/// Determine if the provided relation would introduce a cycle if
|
||||
/// appended to the current path and flag it if so.
|
||||
///
|
||||
/// This should be called only after having checked [`Self::finished`],
|
||||
/// which means that a node is _not_ in the path because it has
|
||||
/// already been emitted.
|
||||
///
|
||||
/// With [`Self::finished`] having been ruled out,
|
||||
/// this uses [`Self::visited`] to determine if a node must be part of
|
||||
/// the active path of the DFS.
|
||||
/// If so,
|
||||
/// then introducing it again would produce a cycle.
|
||||
///
|
||||
/// Cycles are permitted under limited circumstances,
|
||||
/// where the edge represents a recursive target.
|
||||
/// This determination is made utilizing the graph's ontology via
|
||||
/// [`DynObjectRel::can_recurse`].
|
||||
/// If the cycle ends up being permitted,
|
||||
/// then we perform a cut by filtering out the edge entirely,
|
||||
/// as if it did not exist.
|
||||
/// It is up to the graph's ontology to ensure that all such cuts will
|
||||
/// result in a valid ordering.
|
||||
/// (Cuts also occur during error recovery for unsupported cycles.)
|
||||
///
|
||||
/// We use [`Result`] where `E` is [`ObjectIndex`] to simply flag the
|
||||
/// object as containing a cycle;
|
||||
/// this allows us to defer computation of the cycle and allocation
|
||||
/// of memory for that path until we actually visit the node on
|
||||
/// [`Self::stack`].
|
||||
/// This allows the element size of [`Self::stack`] to remain small.
|
||||
///
|
||||
/// See [`Self::find_cycle_path`] for the actual cycle computation that
|
||||
/// will eventually be performed.
|
||||
fn flag_or_cut_cycle(
|
||||
visited: &FixedBitSet,
|
||||
asg: &Asg,
|
||||
dyn_oi: DynObjectRel,
|
||||
) -> Option<Result<ObjectIndex<Object>, ObjectIndex<Object>>> {
|
||||
let oi = *dyn_oi.target();
|
||||
|
||||
if visited.contains(oi.into()) {
|
||||
if dyn_oi.can_recurse(asg) {
|
||||
None // cut
|
||||
} else {
|
||||
Some(Err(oi))
|
||||
}
|
||||
} else {
|
||||
Some(Ok(oi))
|
||||
}
|
||||
}
|
||||
|
||||
/// Attempt to retrieve the next [`ObjectIndex`] from the stack for
|
||||
/// processing,
|
||||
/// leaving it on the stack.
|
||||
///
|
||||
/// If the object atop of the stack has been flagged as a cycle by
|
||||
/// [`Self::flag_or_cut_cycle`],
|
||||
/// then the actual path associated with the cycle will be computed
|
||||
/// by [`Self::find_cycle_path`] and an a [`Cycle`] returned.
|
||||
///
|
||||
/// See also [`Self::pop_next_oi`].
|
||||
fn next_oi(&self) -> Option<Result<ObjectIndex<Object>, Cycle>> {
|
||||
self.stack
|
||||
.last()
|
||||
.map(|result| result.map_err(|oi| self.find_cycle_path(oi)))
|
||||
}
|
||||
|
||||
/// Remove an item from [`Self::stack`].
|
||||
///
|
||||
/// A better API for the future would take ownership over the stack and
|
||||
/// know for certain that the element being removed is the element
|
||||
/// previously returned.
|
||||
///
|
||||
/// See also [`Self::next_oi`].
|
||||
fn pop_next_oi(&mut self) {
|
||||
self.stack.pop();
|
||||
}
|
||||
|
||||
/// Knowing that the provided [`ObjectIndex`] would produce a cycle if
|
||||
/// added to the current path,
|
||||
/// calculate the path representing the cycle.
|
||||
///
|
||||
/// This is a linear-time (`O(n)`) operation that performs a new heap
|
||||
/// allocation.
|
||||
/// Since cycles are an error case,
|
||||
/// it is expected that they will not often occur and so the DFS
|
||||
/// algorithm is optimized for the most common case;
|
||||
/// it is not worth computing the path during the course of the
|
||||
/// search since that path would almost always be discarded.
|
||||
///
|
||||
/// Deriving a path relies on understanding that:
|
||||
///
|
||||
/// 1. An [`ObjectIndex`] in [`Self::stack`] is either awaiting
|
||||
/// processing or is _currently_ being processed.
|
||||
/// This means that it contains the path,
|
||||
/// but it also contains neighbors of objects in the path.
|
||||
/// We must filter out those neighbors.
|
||||
///
|
||||
/// 2. The [`Result`] in [`Self::stack`] indicates whether the object
|
||||
/// causes a cycle.
|
||||
/// A previous object in the path must therefore be [`Ok`],
|
||||
/// otherwise it would not have been traversed,
|
||||
/// and so we must filter all [`Err`]s.
|
||||
/// In doing so,
|
||||
/// we also filter out `next` at the top of the stack,
|
||||
/// and so _this function works correctly regardless of whether
|
||||
/// `next` has already been `pop`'d from the stack_.
|
||||
///
|
||||
/// 3. [`Self::visited`] is set just before neighbors of an object are
|
||||
/// pushed onto [`Self::stack`].
|
||||
/// Therefore,
|
||||
/// only objects marked as visited are part of the active path,
|
||||
/// and so to discover that path we need only filter out
|
||||
/// non-visited objects.
|
||||
///
|
||||
/// 4. [`Self::stack`] contains a path from a provided root.
|
||||
/// We want to cut off the path at the beginning of the cycle.
|
||||
/// The easiest way to do this is to iterate through the stack in
|
||||
/// reverse,
|
||||
/// stopping as soon as we encounter an [`ObjectIndex`]
|
||||
/// matching `next`.
|
||||
/// This has the effect of producing a cycle path in post-order,
|
||||
/// which is consistent with the ordering of [`Self`]'s
|
||||
/// traversal.
|
||||
///
|
||||
/// 5. The [`ObjectIndex`]es sourced from the [`Asg`] do not contain
|
||||
/// the spans of the target objects.
|
||||
/// Cycles will almost certainly result in diagnostic messages,
|
||||
/// which require accurate spans,
|
||||
/// and so we must resolve the [`ObjectIndex`] to retrieve the
|
||||
/// target [`Span`].
|
||||
///
|
||||
/// The path produced will therefore be reversed,
|
||||
/// with `next` as the last element.
|
||||
/// `next` will _not_ be duplicated as the first element,
|
||||
/// which means that if you were to repeat the returned path
|
||||
/// indefinitely end-to-end
|
||||
/// (e.g. using [`Iterator::cycle`]),
|
||||
/// you would have precisely this cycle.
|
||||
///
|
||||
/// With all of that said,
|
||||
/// the implementation is fairly straightforward and concise.
|
||||
fn find_cycle_path(&self, next: ObjectIndex<Object>) -> Cycle {
|
||||
let mut path = self
|
||||
.stack
|
||||
.iter()
|
||||
.rev()
|
||||
.copied()
|
||||
.filter_map(Result::ok)
|
||||
.take_while(|&oi| oi != next)
|
||||
.filter(|&oi| self.visited.contains(oi.into()))
|
||||
.map(|oi| oi.resolve_span(self.asg))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// We stopped _at_ `next`,
|
||||
// so we need to manually add it to the path.
|
||||
path.push(next.resolve_span(self.asg));
|
||||
|
||||
Cycle { path }
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Iterator for TopoPostOrderDfs<'a> {
|
||||
type Item = Result<ObjectIndex<Object>, Cycle>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
// Rust doesn't have guaranteed TCO as of 2023-04
|
||||
loop {
|
||||
match self.next_oi()? {
|
||||
Ok(next) => {
|
||||
if self.visited.put(next.into()) {
|
||||
self.pop_next_oi();
|
||||
|
||||
// See `Self::push_neighbors` for explanation.
|
||||
if !self.finished.put(next.into()) {
|
||||
break Some(Ok(next));
|
||||
}
|
||||
} else {
|
||||
self.push_neighbors(next);
|
||||
}
|
||||
}
|
||||
|
||||
Err(cycle) => {
|
||||
self.pop_next_oi();
|
||||
return Some(Err(cycle));
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A graph cycle.
|
||||
///
|
||||
/// A cycle means that a path contains a duplicate node,
|
||||
/// as if it looped back on itself.
|
||||
/// In terms of TAME,
|
||||
/// a cycle implies a circular dependency.
|
||||
///
|
||||
/// Identifying Cycle Objects
|
||||
/// =========================
|
||||
/// TODO: Object names need to be derived from the cycle to display
|
||||
/// concisely to the user.
|
||||
/// The cycle very likely contains identifiers that can be used to describe
|
||||
/// the cycle in more concise terms.
|
||||
///
|
||||
/// It used to be the case that cycles contained identifier names,
|
||||
/// but that was before the topological sort was generalized to include
|
||||
/// all graph objects;
|
||||
/// see the commit that introduced this message for more information.
|
||||
///
|
||||
/// TODO: We also ought to represent the spans associated with _references_,
|
||||
/// _in addition to_ just the referenced object.
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub struct Cycle {
|
||||
/// The path representing the cycle in post-order (reversed).
|
||||
///
|
||||
/// It is expected that [`ObjectIndex`]'s associated [`Span`] has been
|
||||
/// resolved to that of the target object
|
||||
/// (e.g. using [`ObjectIndex::resolve_span`]).
|
||||
/// This allows the indexes to be useful in a diagnostic context.
|
||||
///
|
||||
/// See [`Self::path_rev`] for more information.
|
||||
path: Vec<ObjectIndexResolvedSpan<Object>>,
|
||||
}
|
||||
|
||||
impl Cycle {
|
||||
/// The path representing the cycle in post-order (reversed).
|
||||
///
|
||||
/// The path is truncated such that the first node in the path is the
|
||||
/// beginning of the cycle.
|
||||
/// The final node in the cycle is omitted,
|
||||
/// since it is the same as the first;
|
||||
/// if you repeated this path indefinitely
|
||||
/// (e.g. with [`Iterator::cycle`])
|
||||
/// then you would have precisely the cycle.
|
||||
///
|
||||
/// The [`ObjectIndex`]es should have [`Span`]s that are resolved
|
||||
/// against the target so that they are useful in a diagnostic
|
||||
/// context.
|
||||
pub fn path_rev(&self) -> &Vec<ObjectIndexResolvedSpan<Object>> {
|
||||
&self.path
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Cycle {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
// TODO: See note on [`Cycle`] about deriving names.
|
||||
write!(f, "[...]")
|
||||
}
|
||||
}
|
||||
|
||||
impl Error for Cycle {}
|
||||
|
||||
impl Diagnostic for Cycle {
|
||||
fn describe(&self) -> Vec<AnnotatedSpan> {
|
||||
let path = &self.path;
|
||||
let n = path.len();
|
||||
let ident = path.last().unwrap();
|
||||
|
||||
// TODO: See note on [`Cycle`] about deriving names.
|
||||
path.iter()
|
||||
.rev()
|
||||
.enumerate()
|
||||
.map(|(i, oi)| {
|
||||
oi.note(match i {
|
||||
0 => format!(
|
||||
"[0/{n}] the cycle begins here, depending on..."
|
||||
),
|
||||
// TODO: s/object/<TYPE OF OBJECT>/
|
||||
_ => {
|
||||
format!("[{i}/{n}] ...this object, which depends on...")
|
||||
}
|
||||
})
|
||||
})
|
||||
.chain(once(ident.error(format!(
|
||||
"[{n}/{n}] ...the object once again, \
|
||||
creating the cycle"
|
||||
))))
|
||||
.collect::<Vec<_>>()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test;
|
|
@ -0,0 +1,465 @@
|
|||
// Test topological sort ASG traversal
|
||||
//
|
||||
// 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::{
|
||||
asg::{
|
||||
air::{Air, AirAggregate},
|
||||
graph::object::{
|
||||
self, ObjectKind, ObjectRelFrom, ObjectRelatable, ObjectTy, Root,
|
||||
},
|
||||
ExprOp, IdentKind,
|
||||
},
|
||||
num::{Dim, Dtype},
|
||||
parse::{util::SPair, ParseState},
|
||||
span::{dummy::*, Span, UNKNOWN_SPAN},
|
||||
};
|
||||
use std::fmt::Debug;
|
||||
|
||||
use Air::*;
|
||||
|
||||
fn topo_report_only(
|
||||
asg: &Asg,
|
||||
edges: impl Iterator<Item = ObjectIndex<Object>>,
|
||||
) -> Vec<Result<(ObjectTy, Span), Vec<(ObjectTy, Span)>>> {
|
||||
topo_sort(asg, edges)
|
||||
.map(|result| {
|
||||
result
|
||||
.map(|oi| oi.resolve(asg))
|
||||
.map(|obj| (obj.ty(), obj.span()))
|
||||
.map_err(|cycle| {
|
||||
cycle
|
||||
.path_rev()
|
||||
.iter()
|
||||
// Retain the resolved span from Cycle so that our
|
||||
// assertions verify that it is being resolved
|
||||
// correctly.
|
||||
.map(|oirs| (oirs.oi().resolve(asg).ty(), oirs.span()))
|
||||
.collect()
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn topo_report<O: ObjectKind + ObjectRelatable, I: IntoIterator<Item = Air>>(
|
||||
toks: I,
|
||||
) -> Vec<Result<(ObjectTy, Span), Vec<(ObjectTy, Span)>>>
|
||||
where
|
||||
I::IntoIter: Debug,
|
||||
O: ObjectRelFrom<Root>,
|
||||
{
|
||||
let mut parser = AirAggregate::parse(toks.into_iter());
|
||||
assert!(parser.all(|x| x.is_ok()));
|
||||
|
||||
let asg = &parser.finalize().unwrap().into_context().finish();
|
||||
let oi_root = asg.root(UNKNOWN_SPAN);
|
||||
|
||||
topo_report_only(
|
||||
asg,
|
||||
oi_root.edges_filtered::<O>(asg).map(ObjectIndex::widen),
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sorts_objects_given_single_root() {
|
||||
let id_a = SPair("expr_a".into(), S3);
|
||||
let id_b = SPair("expr_b".into(), S9);
|
||||
let id_c = SPair("expr_c".into(), S12);
|
||||
|
||||
#[rustfmt::skip]
|
||||
let toks = vec![
|
||||
// Packages are auto-rooted as part of the graph's ontology.
|
||||
// There is only one for this test.
|
||||
PkgStart(S1, SPair("/pkg".into(), S1)),
|
||||
// Before this can be computed,
|
||||
// its dependencies must be.
|
||||
ExprStart(ExprOp::Sum, S2), // -.
|
||||
BindIdent(id_a), // |
|
||||
// |
|
||||
// This is a dependency, // |
|
||||
// but it is owned by this Expr // |
|
||||
// and so would have been emitted // |
|
||||
// first anyway. // |
|
||||
ExprStart(ExprOp::Sum, S4), // |
|
||||
ExprEnd(S5), // |
|
||||
// v
|
||||
// But this is a reference to another
|
||||
// Expr that appears later.
|
||||
RefIdent(SPair(id_b.symbol(), S6)), // --.
|
||||
ExprEnd(S7), // |
|
||||
// |
|
||||
// This will have to be emitted // |
|
||||
// _before_ the above Expr that // |
|
||||
// depends on its value having been // |
|
||||
// computed. // /
|
||||
ExprStart(ExprOp::Sum, S8), // <`
|
||||
BindIdent(id_b),
|
||||
ExprEnd(S10),
|
||||
|
||||
// A sibling expression with no dependency on
|
||||
// other expressions.
|
||||
ExprStart(ExprOp::Sum, S11),
|
||||
BindIdent(id_c),
|
||||
ExprEnd(S13),
|
||||
PkgEnd(S14),
|
||||
];
|
||||
|
||||
use ObjectTy::*;
|
||||
let m = |a: Span, b: Span| a.merge(b).unwrap();
|
||||
|
||||
#[rustfmt::skip]
|
||||
assert_eq!(
|
||||
Ok(vec![
|
||||
// The first leaf is this anonymous child expression,
|
||||
// which has no dependencies.
|
||||
(Expr, m(S4, S5) ), // child of id_a
|
||||
|
||||
// The sibling of the above expression is a reference to the
|
||||
// value of `id_b`.
|
||||
// `id_a` cannot be computed before it.
|
||||
(Expr, m(S8, S10) ), // id_b
|
||||
(Ident, S9, ), // id_b
|
||||
|
||||
// With `id_b` emitted,
|
||||
// `id_a` has no more dependencies,
|
||||
// and so itself can be emitted.
|
||||
(Expr, m(S2, S7) ), // id_a
|
||||
(Ident, S3, ), // id_a
|
||||
|
||||
// `id_a` has a sibling `id_c`.
|
||||
// Its ordering is undefined relative to `id_a`
|
||||
// (it could also be ordered before it),
|
||||
// but the implementation of the traversal causes it to be
|
||||
// output in the same order as it appeared in the source
|
||||
// token stream.
|
||||
(Expr, m(S11, S13)), // id_c
|
||||
(Ident, S12 ), // id_c
|
||||
|
||||
// We end with the root that was explicitly provided to
|
||||
// `topo_sort` via `topo_report`.
|
||||
(Pkg, m(S1, S14) ),
|
||||
]),
|
||||
topo_report::<object::Pkg, _>(toks).into_iter().collect(),
|
||||
);
|
||||
}
|
||||
|
||||
// Like the above test,
|
||||
// but the path is deeper to emphasize that the topological sort applies
|
||||
// recursively to dependencies.
|
||||
// Multiple expressions depending on the same dependency have an arbitrary
|
||||
// order that is deterministic between runs.
|
||||
#[test]
|
||||
fn sorts_objects_given_single_root_more_complex() {
|
||||
let id_a = SPair("expr_a".into(), S3);
|
||||
let id_b = SPair("expr_b".into(), S7);
|
||||
let id_c = SPair("expr_c".into(), S11);
|
||||
let id_d = SPair("expr_d".into(), S15);
|
||||
|
||||
#[rustfmt::skip]
|
||||
let toks = vec![
|
||||
PkgStart(S1, SPair("/pkg".into(), S1)),
|
||||
ExprStart(ExprOp::Sum, S2),
|
||||
BindIdent(id_a),
|
||||
RefIdent(SPair(id_b.symbol(), S4)), // ---.
|
||||
ExprEnd(S5), // )
|
||||
// /
|
||||
ExprStart(ExprOp::Sum, S6), // /
|
||||
BindIdent(id_b), // <'
|
||||
RefIdent(SPair(id_d.symbol(), S8)), // -------.
|
||||
ExprEnd(S9), // <. |
|
||||
// \ |
|
||||
ExprStart(ExprOp::Sum, S10), // \ |
|
||||
BindIdent(id_c), // ) |
|
||||
RefIdent(SPair(id_b.symbol(), S12)), // ---' /
|
||||
ExprEnd(S13), // /
|
||||
// /
|
||||
ExprStart(ExprOp::Sum, S14), // /
|
||||
BindIdent(id_d), // <--'
|
||||
ExprEnd(S16),
|
||||
PkgEnd(S17),
|
||||
];
|
||||
|
||||
use ObjectTy::*;
|
||||
let m = |a: Span, b: Span| a.merge(b).unwrap();
|
||||
|
||||
#[rustfmt::skip]
|
||||
assert_eq!(
|
||||
Ok(vec![
|
||||
(Expr, m(S14, S16)), // id_d
|
||||
(Ident, S15 ), // id_d
|
||||
|
||||
(Expr, m(S6, S9) ), // id_b
|
||||
(Ident, S7, ), // id_b
|
||||
|
||||
(Expr, m(S2, S5) ), // id_a
|
||||
(Ident, S3, ), // id_a
|
||||
|
||||
(Expr, m(S10, S13)), // id_c
|
||||
(Ident, S11 ), // id_c
|
||||
|
||||
(Pkg, m(S1, S17) ),
|
||||
]),
|
||||
topo_report::<object::Pkg, _>(toks).into_iter().collect(),
|
||||
);
|
||||
}
|
||||
|
||||
// This tests what the linker (tameld) does:
|
||||
// topologically sorts explicitly rooted objects and ignores everything
|
||||
// else.
|
||||
// This also gives us dead code elimination.
|
||||
#[test]
|
||||
fn omits_unreachable() {
|
||||
let id_a = SPair("expr_a".into(), S3);
|
||||
let id_b = SPair("expr_b".into(), S7);
|
||||
let id_c = SPair("expr_c".into(), S11);
|
||||
let id_d = SPair("expr_d".into(), S15);
|
||||
|
||||
// We will only use a portion of this graph.
|
||||
#[rustfmt::skip]
|
||||
let toks = vec![
|
||||
PkgStart(S1, SPair("/pkg".into(), S1)),
|
||||
ExprStart(ExprOp::Sum, S2),
|
||||
BindIdent(id_a),
|
||||
RefIdent(SPair(id_b.symbol(), S4)), // ---.
|
||||
ExprEnd(S5), // )
|
||||
// /
|
||||
ExprStart(ExprOp::Sum, S6), // /
|
||||
BindIdent(id_b), // <'
|
||||
RefIdent(SPair(id_d.symbol(), S8)), // -------.
|
||||
ExprEnd(S9), // <. |
|
||||
// \ |
|
||||
ExprStart(ExprOp::Sum, S10), // \ |
|
||||
BindIdent(id_c), // ) |
|
||||
RefIdent(SPair(id_b.symbol(), S12)), // ---' /
|
||||
ExprEnd(S13), // /
|
||||
// /
|
||||
ExprStart(ExprOp::Sum, S14), // /
|
||||
BindIdent(id_d), // <--'
|
||||
ExprEnd(S16),
|
||||
PkgEnd(S17),
|
||||
];
|
||||
|
||||
use ObjectTy::*;
|
||||
let m = |a: Span, b: Span| a.merge(b).unwrap();
|
||||
|
||||
let mut parser = AirAggregate::parse(toks.into_iter());
|
||||
assert!(parser.all(|x| x.is_ok()));
|
||||
|
||||
let asg = &parser.finalize().unwrap().into_context().finish();
|
||||
|
||||
let oi_pkg = asg
|
||||
.root(UNKNOWN_SPAN)
|
||||
.edges_filtered::<object::Pkg>(&asg)
|
||||
.next()
|
||||
.expect("cannot find Pkg on graph");
|
||||
|
||||
let oi_b = oi_pkg.lookup_local_linear(asg, id_b).expect("missing oi_b");
|
||||
|
||||
// We'll use only `oi_b` as the root,
|
||||
// which will include it and its (only) dependency.
|
||||
// The rest of the graph must be ignored.
|
||||
let report = topo_report_only(&asg, [oi_b.widen()].into_iter());
|
||||
|
||||
#[rustfmt::skip]
|
||||
assert_eq!(
|
||||
Ok(vec![
|
||||
(Expr, m(S14, S16)), // id_d
|
||||
(Ident, S15 ), // id_d
|
||||
|
||||
(Expr, m(S6, S9) ), // id_b
|
||||
(Ident, S7, ), // id_b
|
||||
]),
|
||||
report.into_iter().collect(),
|
||||
);
|
||||
}
|
||||
|
||||
// If multiple roots are given,
|
||||
// and they have entirely independent subgraphs,
|
||||
// then their ordering is deterministic between runs of the same graph,
|
||||
// but undefined.
|
||||
//
|
||||
// This is no different than the ordering of siblings above;
|
||||
// this simply provides an explicit example for the behavior of provided
|
||||
// roots since that is the entry point for this API.
|
||||
#[test]
|
||||
fn sorts_objects_given_multiple_roots() {
|
||||
let pkg_a_name = SPair("/pkg/a".into(), S2);
|
||||
let pkg_b_name = SPair("/pkg/b".into(), S8);
|
||||
|
||||
let id_a = SPair("expr_a".into(), S4);
|
||||
let id_b = SPair("expr_b".into(), S10);
|
||||
|
||||
#[rustfmt::skip]
|
||||
let toks = vec![
|
||||
// First root
|
||||
PkgStart(S1, pkg_a_name),
|
||||
ExprStart(ExprOp::Sum, S3),
|
||||
BindIdent(id_a),
|
||||
ExprEnd(S5),
|
||||
PkgEnd(S6),
|
||||
|
||||
// Second root,
|
||||
// independent of the first.
|
||||
PkgStart(S7, pkg_b_name),
|
||||
ExprStart(ExprOp::Sum, S9),
|
||||
BindIdent(id_b),
|
||||
ExprEnd(S11),
|
||||
PkgEnd(S12),
|
||||
];
|
||||
|
||||
use ObjectTy::*;
|
||||
let m = |a: Span, b: Span| a.merge(b).unwrap();
|
||||
|
||||
#[rustfmt::skip]
|
||||
assert_eq!(
|
||||
Ok(vec![
|
||||
// First root.
|
||||
(Expr, m(S3, S5) ),
|
||||
(Ident, S4),
|
||||
(Pkg, m(S1, S6) ),
|
||||
|
||||
// Second root,
|
||||
// but the fact that it is emitted after the first is not
|
||||
// behavior that should be relied upon.
|
||||
(Expr, m(S9, S11) ),
|
||||
(Ident, S10),
|
||||
(Pkg, m(S7, S12)),
|
||||
]),
|
||||
topo_report::<object::Pkg, _>(toks).into_iter().collect(),
|
||||
);
|
||||
}
|
||||
|
||||
// Most cycles are unsupported by TAME.
|
||||
// Recovery allows compilation/linking to continue so that additional errors
|
||||
// can be discovered and reported.
|
||||
#[test]
|
||||
fn unsupported_cycles_with_recovery() {
|
||||
let id_a = SPair("expr_a".into(), S3);
|
||||
let id_b = SPair("expr_b".into(), S8);
|
||||
|
||||
#[rustfmt::skip]
|
||||
let toks = vec![
|
||||
PkgStart(S1, SPair("/pkg-a".into(), S1)),
|
||||
ExprStart(ExprOp::Sum, S2),
|
||||
BindIdent(id_a), // <----. self-cycle
|
||||
RefIdent(SPair(id_a.symbol(), S4)), // ____/ \
|
||||
RefIdent(SPair(id_b.symbol(), S5)), // ---. \ a->b->a
|
||||
ExprEnd(S6), // ) ) cycle
|
||||
// / /
|
||||
ExprStart(ExprOp::Sum, S7), // / /
|
||||
BindIdent(id_b), // <' /
|
||||
RefIdent(SPair(id_a.symbol(), S9)), // ----'
|
||||
ExprEnd(S10),
|
||||
PkgEnd(S11),
|
||||
];
|
||||
|
||||
use ObjectTy::*;
|
||||
let m = |a: Span, b: Span| a.merge(b).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
#[rustfmt::skip]
|
||||
vec![
|
||||
// Pkg -> Ident (id_a) -> Ref (id_a) gives us a self-cycle.
|
||||
Err(vec![
|
||||
(Expr, m(S2, S6)), // -.
|
||||
(Ident, S3 ), // <' id_a
|
||||
]),
|
||||
|
||||
// RECOVERY: We do not traverse into the cycle and continue as
|
||||
// if the edge causing the cycle was not taken.
|
||||
|
||||
// ...which unfortunately lands us on another cycle caused by
|
||||
// a->b->a before we can emit the parent Expr.
|
||||
// TODO: In the future we ought to represent the reference here
|
||||
// as well.
|
||||
Err(vec![
|
||||
(Expr, m(S7, S10)), // -.
|
||||
(Ident, S8 ), // | id_b
|
||||
(Expr, m(S2, S6)), // |
|
||||
(Ident, S3 ), // <' id_a
|
||||
]),
|
||||
|
||||
// RECOVERY: We ignore the edge leading to the cycle,
|
||||
// which means that id_b Expr has no more dependencies.
|
||||
Ok((Expr, m(S7, S10))),
|
||||
Ok((Ident, S8 )),
|
||||
|
||||
// And id_a is now also complete,
|
||||
// since the cycle was the last dependency.
|
||||
Ok((Expr, m(S2, S6 ))),
|
||||
Ok((Ident, S3 )),
|
||||
|
||||
Ok((Pkg, m(S1, S11))),
|
||||
],
|
||||
topo_report::<object::Pkg, _>(toks)
|
||||
.into_iter()
|
||||
.collect::<Vec<_>>(),
|
||||
);
|
||||
}
|
||||
|
||||
// TAME supports cycles in certain contexts,
|
||||
// as a component of the graph's ontology.
|
||||
// A topological sort of a graph containing permitted cycles should be
|
||||
// viewed as sorting a graph that first "cuts" those cycles,
|
||||
// filtering out the edge that would have caused the cycle to occur.
|
||||
// It is the responsibility of the ontology to ensure that all such cuts
|
||||
// will result in a topological sort.
|
||||
#[test]
|
||||
fn supported_cycles() {
|
||||
let id_a = SPair("func_a".into(), S3);
|
||||
let id_b = SPair("func_b".into(), S8);
|
||||
let kind = IdentKind::Func(Dim::Scalar, Dtype::Integer);
|
||||
|
||||
#[rustfmt::skip]
|
||||
let toks = vec![
|
||||
PkgStart(S1, SPair("/pkg-a".into(), S1)),
|
||||
// Two mutually recursive functions.
|
||||
IdentDecl(id_a, kind.clone(), Default::default()), // <--.
|
||||
IdentDep(id_a, id_b), // -. |
|
||||
// | |
|
||||
IdentDecl(id_b, kind.clone(), Default::default()), // <' |
|
||||
IdentDep(id_b, id_a), // ---'
|
||||
|
||||
// Root so that `topo_report` will find them.
|
||||
IdentRoot(id_a),
|
||||
IdentRoot(id_b),
|
||||
PkgEnd(S11),
|
||||
];
|
||||
|
||||
// TODO: Template recursion was not part of the ontology at the time of
|
||||
// writing.
|
||||
|
||||
use ObjectTy::*;
|
||||
|
||||
assert_eq!(
|
||||
#[rustfmt::skip]
|
||||
Ok(vec![
|
||||
// The order in which the above functions will be visited is
|
||||
// undefined;
|
||||
// this is the ordering that happens to be taken by the
|
||||
// implementation based on the definition and stack
|
||||
// ordering.
|
||||
(Ident, S8),
|
||||
(Ident, S3),
|
||||
]),
|
||||
topo_report::<object::Ident, _>(toks)
|
||||
.into_iter()
|
||||
.collect::<Result<Vec<_>, _>>(),
|
||||
);
|
||||
}
|
|
@ -30,16 +30,17 @@
|
|||
//! or observing template expansions.
|
||||
|
||||
use super::object::{
|
||||
DynObjectRel, Expr, Object, ObjectIndex, ObjectRelTy, OiPairObjectInner,
|
||||
Pkg, Tpl,
|
||||
Doc, DynObjectRel, Expr, Meta, Object, ObjectIndex, ObjectRelTy,
|
||||
OiPairObjectInner, Pkg, Tpl,
|
||||
};
|
||||
use crate::{
|
||||
asg::{
|
||||
visit::{Depth, TreeWalkRel},
|
||||
Asg, ExprOp, Ident,
|
||||
},
|
||||
diagnose::Annotate,
|
||||
diagnostic_panic, diagnostic_unreachable,
|
||||
diagnose::{panic::DiagnosticPanic, Annotate},
|
||||
diagnostic_panic, diagnostic_todo, diagnostic_unreachable,
|
||||
fmt::{DisplayWrapper, TtQuote},
|
||||
parse::{prelude::*, util::SPair, Transitionable},
|
||||
span::{Span, UNKNOWN_SPAN},
|
||||
sym::{
|
||||
|
@ -175,7 +176,14 @@ impl<'a> TreeContext<'a> {
|
|||
let paired_rel = dyn_rel.resolve_oi_pairs(self.asg);
|
||||
|
||||
match paired_rel.target() {
|
||||
Object::Pkg((pkg, _)) => self.emit_package(pkg, depth),
|
||||
Object::Pkg((pkg, oi_pkg)) => match paired_rel.source() {
|
||||
Object::Root(_) => self.emit_package(pkg, depth),
|
||||
Object::Pkg(_) => self.emit_import(pkg, depth),
|
||||
_ => diagnostic_panic!(
|
||||
vec![oi_pkg.error("package was not expected here")],
|
||||
"invalid context for package object during xmli derivation",
|
||||
),
|
||||
},
|
||||
|
||||
// Identifiers will be considered in context;
|
||||
// pass over it for now.
|
||||
|
@ -190,12 +198,20 @@ impl<'a> TreeContext<'a> {
|
|||
depth,
|
||||
)),
|
||||
|
||||
Object::Expr((expr, _)) => {
|
||||
self.emit_expr(expr, paired_rel.source(), depth)
|
||||
Object::Expr((expr, oi_expr)) => {
|
||||
self.emit_expr(expr, *oi_expr, paired_rel.source(), depth)
|
||||
}
|
||||
|
||||
Object::Tpl((tpl, _)) => {
|
||||
self.emit_template(tpl, paired_rel.source(), depth)
|
||||
Object::Tpl((tpl, oi_tpl)) => {
|
||||
self.emit_template(tpl, *oi_tpl, paired_rel.source(), depth)
|
||||
}
|
||||
|
||||
Object::Meta((meta, oi_meta)) => {
|
||||
self.emit_tpl_arg(meta, *oi_meta, depth)
|
||||
}
|
||||
|
||||
Object::Doc((doc, oi_doc)) => {
|
||||
self.emit_doc(doc, *oi_doc, paired_rel.source(), depth)
|
||||
}
|
||||
|
||||
Object::Root(..) => diagnostic_unreachable!(
|
||||
|
@ -218,6 +234,20 @@ impl<'a> TreeContext<'a> {
|
|||
Some(package(pkg, depth))
|
||||
}
|
||||
|
||||
/// Emit a package import statement.
|
||||
///
|
||||
/// The import will have its path canonicalized.
|
||||
fn emit_import(&mut self, pkg: &Pkg, depth: Depth) -> Option<Xirf> {
|
||||
let ps = pkg.canonical_name();
|
||||
self.push(Xirf::attr(QN_PACKAGE, ps.symbol(), (ps.span(), ps.span())));
|
||||
|
||||
Some(Xirf::open(
|
||||
QN_IMPORT,
|
||||
OpenSpan::without_name_span(pkg.span()),
|
||||
depth,
|
||||
))
|
||||
}
|
||||
|
||||
/// Emit an expression as a legacy TAME statement or expression.
|
||||
///
|
||||
/// Identified expressions must be represented using statements in
|
||||
|
@ -238,6 +268,7 @@ impl<'a> TreeContext<'a> {
|
|||
fn emit_expr(
|
||||
&mut self,
|
||||
expr: &Expr,
|
||||
oi_expr: ObjectIndex<Expr>,
|
||||
src: &Object<OiPairObjectInner>,
|
||||
depth: Depth,
|
||||
) -> Option<Xirf> {
|
||||
|
@ -245,7 +276,26 @@ impl<'a> TreeContext<'a> {
|
|||
Object::Ident((ident, _)) => {
|
||||
self.emit_expr_ident(expr, ident, depth)
|
||||
}
|
||||
_ => Some(expr_ele(expr, depth)),
|
||||
Object::Expr((pexpr, _)) => match (pexpr.op(), expr.op()) {
|
||||
(ExprOp::Conj | ExprOp::Disj, ExprOp::Eq) => {
|
||||
Some(self.emit_match(expr, oi_expr, depth))
|
||||
}
|
||||
_ => Some(expr_ele(expr, oi_expr, depth)),
|
||||
},
|
||||
// TODO: See `:tamer/tests/xmli/template` regarding `match` and
|
||||
// `when`/`c:*`;
|
||||
// this is not an ambiguity that can be resolved without
|
||||
// adding more information to the graph,
|
||||
// but is hopefully one that we can avoid.
|
||||
Object::Tpl(..) => match expr.op() {
|
||||
ExprOp::Eq => Some(self.emit_match(expr, oi_expr, depth)),
|
||||
_ => Some(expr_ele(expr, oi_expr, depth)),
|
||||
},
|
||||
// TODO: Perhaps errors for Root, Meta, and Doc?
|
||||
Object::Root(_)
|
||||
| Object::Pkg(_)
|
||||
| Object::Meta(_)
|
||||
| Object::Doc(_) => Some(expr_ele(expr, oi_expr, depth)),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -263,7 +313,11 @@ impl<'a> TreeContext<'a> {
|
|||
ExprOp::Sum => (QN_RATE, QN_YIELDS),
|
||||
ExprOp::Conj => (QN_CLASSIFY, QN_AS),
|
||||
|
||||
ExprOp::Product | ExprOp::Ceil | ExprOp::Floor | ExprOp::Disj => {
|
||||
ExprOp::Product
|
||||
| ExprOp::Ceil
|
||||
| ExprOp::Floor
|
||||
| ExprOp::Disj
|
||||
| ExprOp::Eq => {
|
||||
todo!("stmt: {expr:?}")
|
||||
}
|
||||
};
|
||||
|
@ -278,10 +332,52 @@ impl<'a> TreeContext<'a> {
|
|||
))
|
||||
}
|
||||
|
||||
/// Emit a template definition.
|
||||
/// Emit a match expression.
|
||||
///
|
||||
/// This is intended as a classify/any/all child.
|
||||
fn emit_match(
|
||||
&mut self,
|
||||
expr: &Expr,
|
||||
oi_expr: ObjectIndex<Expr>,
|
||||
depth: Depth,
|
||||
) -> Xirf {
|
||||
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 on = edges
|
||||
.next()
|
||||
.diagnostic_expect(
|
||||
|| vec![oi_expr.note("for this match")],
|
||||
"missing @on ref",
|
||||
)
|
||||
.resolve(self.asg);
|
||||
|
||||
if let Some(unexpected) = edges.next() {
|
||||
diagnostic_panic!(
|
||||
vec![unexpected.error("a third ref was unexpected")],
|
||||
"unexpected third ref during match generation",
|
||||
);
|
||||
}
|
||||
|
||||
self.push(attr_value(value.name()));
|
||||
self.push(attr_on(on.name()));
|
||||
|
||||
Xirf::open(QN_MATCH, OpenSpan::without_name_span(expr.span()), depth)
|
||||
}
|
||||
|
||||
/// Emit a template definition or application.
|
||||
fn emit_template(
|
||||
&mut self,
|
||||
tpl: &Tpl,
|
||||
oi_tpl: ObjectIndex<Tpl>,
|
||||
src: &Object<OiPairObjectInner>,
|
||||
depth: Depth,
|
||||
) -> Option<Xirf> {
|
||||
|
@ -296,7 +392,128 @@ impl<'a> TreeContext<'a> {
|
|||
))
|
||||
}
|
||||
|
||||
_ => todo!("emit_template: {src:?}"),
|
||||
// If we're not behind an Ident,
|
||||
// then this is a direct template reference,
|
||||
// which indicates application of a closed template
|
||||
// (template expansion).
|
||||
// Convert this into a long-hand template expansion so that we
|
||||
// do not have to deal with converting underscore-padded
|
||||
// template names back into short-hand form.
|
||||
Object::Pkg(..) | Object::Tpl(..) | Object::Expr(..) => {
|
||||
// [`Ident`]s are skipped during traversal,
|
||||
// so we'll handle it ourselves here.
|
||||
// This also gives us the opportunity to make sure that
|
||||
// we're deriving something that's actually supported by the
|
||||
// XSLT-based compiler.
|
||||
// TODO: Not checking that it's a Tpl ident because we need
|
||||
// to be able to accommodate Missing identifiers until we
|
||||
// both properly handle scoping rules and support package
|
||||
// imports;
|
||||
// this is making some dangerous assumptions,
|
||||
// though they are tested.
|
||||
let ident = oi_tpl.edges_filtered::<Ident>(self.asg).last();
|
||||
|
||||
let apply_tpl = ident.diagnostic_expect(
|
||||
|| {
|
||||
vec![tpl
|
||||
.span()
|
||||
.internal_error("missing target Tpl Ident")]
|
||||
},
|
||||
"cannot derive name of template for application",
|
||||
);
|
||||
|
||||
self.push(attr_name(apply_tpl.resolve(self.asg).name()));
|
||||
|
||||
Some(Xirf::open(
|
||||
QN_APPLY_TEMPLATE,
|
||||
OpenSpan::without_name_span(tpl.span()),
|
||||
depth,
|
||||
))
|
||||
}
|
||||
|
||||
_ => diagnostic_todo!(
|
||||
vec![
|
||||
oi_tpl.note("interpreting this as a template application"),
|
||||
],
|
||||
"emit_template: {src:?}"
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
/// Emit a long-form template argument.
|
||||
///
|
||||
/// For the parent template application,
|
||||
/// see [`Self::emit_template`].
|
||||
fn emit_tpl_arg(
|
||||
&mut self,
|
||||
meta: &Meta,
|
||||
oi_meta: ObjectIndex<Meta>,
|
||||
depth: Depth,
|
||||
) -> Option<Xirf> {
|
||||
let pname = oi_meta.ident(self.asg).map(Ident::name)
|
||||
.diagnostic_unwrap(|| vec![meta.internal_error(
|
||||
"anonymous metavariables are not supported as template arguments"
|
||||
)]);
|
||||
|
||||
let pval = match meta {
|
||||
Meta::Required(span) => diagnostic_todo!(
|
||||
vec![span.error("value expected for this param")],
|
||||
"value missing for param {}",
|
||||
TtQuote::wrap(pname)
|
||||
),
|
||||
|
||||
Meta::ConcatList(span) => diagnostic_todo!(
|
||||
vec![span.error("concatenation occurs here")],
|
||||
"concatenation not yet supported in xmli for param {}",
|
||||
TtQuote::wrap(pname)
|
||||
),
|
||||
|
||||
Meta::Lexeme(_, value) => *value,
|
||||
};
|
||||
|
||||
self.push(attr_value(pval));
|
||||
self.push(attr_name(pname));
|
||||
|
||||
Some(Xirf::open(
|
||||
QN_WITH_PARAM,
|
||||
OpenSpan::without_name_span(meta.span()),
|
||||
depth,
|
||||
))
|
||||
}
|
||||
|
||||
/// Emit short documentation strings.
|
||||
///
|
||||
/// This derives e.g. `@desc`.
|
||||
fn emit_doc(
|
||||
&mut self,
|
||||
doc: &Doc,
|
||||
oi_doc: ObjectIndex<Doc>,
|
||||
src: &Object<OiPairObjectInner>,
|
||||
_depth: Depth,
|
||||
) -> Option<Xirf> {
|
||||
match (src, doc) {
|
||||
// TODO: Non-stmt exprs should use `@label` instead.
|
||||
(Object::Expr(..) | Object::Tpl(..), Doc::IndepClause(desc)) => {
|
||||
Some(attr_desc(*desc))
|
||||
}
|
||||
|
||||
(_, Doc::Text(_text)) => {
|
||||
// TODO: This isn't utilized by the XSLT parser and
|
||||
// `xmllint` for system tests does not format with mixed
|
||||
// data present,
|
||||
// so let's just omit for now.
|
||||
// Some(Xirf::text(Text(text.symbol(), text.span()), depth))
|
||||
None
|
||||
}
|
||||
|
||||
_ => {
|
||||
diagnostic_todo!(
|
||||
vec![oi_doc.internal_error(
|
||||
"this documentation is not supported in XIRF output"
|
||||
)],
|
||||
"unsupported documentation",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -359,7 +576,19 @@ fn attr_name(name: SPair) -> Xirf {
|
|||
Xirf::attr(QN_NAME, name, (name.span(), name.span()))
|
||||
}
|
||||
|
||||
fn expr_ele(expr: &Expr, depth: Depth) -> Xirf {
|
||||
fn attr_on(on: SPair) -> Xirf {
|
||||
Xirf::attr(QN_ON, on, (on.span(), on.span()))
|
||||
}
|
||||
|
||||
fn attr_value(val: SPair) -> Xirf {
|
||||
Xirf::attr(QN_VALUE, val, (val.span(), val.span()))
|
||||
}
|
||||
|
||||
fn attr_desc(desc: SPair) -> Xirf {
|
||||
Xirf::attr(QN_DESC, desc, (desc.span(), desc.span()))
|
||||
}
|
||||
|
||||
fn expr_ele(expr: &Expr, oi_expr: ObjectIndex<Expr>, depth: Depth) -> Xirf {
|
||||
use ExprOp::*;
|
||||
|
||||
let qname = match expr.op() {
|
||||
|
@ -369,6 +598,11 @@ fn expr_ele(expr: &Expr, depth: Depth) -> Xirf {
|
|||
Floor => QN_C_FLOOR,
|
||||
Conj => QN_ALL,
|
||||
Disj => QN_ANY,
|
||||
|
||||
Eq => diagnostic_panic!(
|
||||
vec![oi_expr.error("unsupported expression type in this context")],
|
||||
"cannot derive expression of this type in this context",
|
||||
),
|
||||
};
|
||||
|
||||
Xirf::open(qname, OpenSpan::without_name_span(expr.span()), depth)
|
||||
|
|
|
@ -56,16 +56,6 @@
|
|||
//! [asg]: https://en.wikipedia.org/wiki/Abstract_semantic_graph
|
||||
//! [graph]: https://en.wikipedia.org/wiki/Graph_(discrete_mathematics)
|
||||
//! [scc]: https://en.wikipedia.org/wiki/Strongly_connected_component
|
||||
//!
|
||||
//! Missing Identifiers
|
||||
//! -------------------
|
||||
//! Since identifiers in TAME can be defined in any order relative to their
|
||||
//! dependencies within a source file,
|
||||
//! it is often the case that a dependency will have to be added to the
|
||||
//! graph before it is resolved.
|
||||
//! For example,
|
||||
//! [`Asg::add_dep_lookup`] will add an [`Ident::Missing`] to the graph
|
||||
//! if either identifier has not yet been declared.
|
||||
|
||||
mod error;
|
||||
mod graph;
|
||||
|
@ -80,7 +70,8 @@ pub use graph::{
|
|||
FragmentText, Ident, IdentKind, Source, TransitionError,
|
||||
TransitionResult, UnresolvedError,
|
||||
},
|
||||
Object, ObjectIndex, ObjectKind,
|
||||
Object, ObjectIndex, ObjectIndexRelTo, ObjectIndexResolvedSpan,
|
||||
ObjectKind,
|
||||
},
|
||||
visit,
|
||||
xmli::AsgTreeToXirf,
|
||||
|
|
|
@ -39,26 +39,18 @@ use std::{
|
|||
path::Path,
|
||||
};
|
||||
use tamer::{
|
||||
asg::{
|
||||
air::{Air, AirAggregate},
|
||||
AsgError, DefaultAsg,
|
||||
},
|
||||
asg::{air::Air, AsgError, DefaultAsg},
|
||||
diagnose::{
|
||||
AnnotatedSpan, Diagnostic, FsSpanResolver, Reporter, VisualReporter,
|
||||
},
|
||||
nir::{
|
||||
InterpError, InterpolateNir, Nir, NirToAir, NirToAirError, XirfToNir,
|
||||
XirfToNirError,
|
||||
},
|
||||
parse::{
|
||||
lowerable, FinalizeError, Lower, ParseError, ParsedObject, Token,
|
||||
UnknownToken,
|
||||
},
|
||||
nir::{InterpError, Nir, NirToAirError, XirfToNirError},
|
||||
parse::{lowerable, FinalizeError, ParseError, Token, UnknownToken},
|
||||
pipeline::parse_package_xml,
|
||||
xir::{
|
||||
self,
|
||||
flat::{RefinedText, XirToXirf, XirToXirfError, XirfToken},
|
||||
flat::{RefinedText, XirToXirfError, XirfToken},
|
||||
reader::XmlXirReader,
|
||||
DefaultEscaper, Error as XirError, Token as XirToken,
|
||||
DefaultEscaper, Token as XirToken,
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -96,7 +88,7 @@ fn src_reader<'a>(
|
|||
fn copy_xml_to<'e, W: io::Write + 'e>(
|
||||
mut fout: W,
|
||||
escaper: &'e DefaultEscaper,
|
||||
) -> impl FnMut(&Result<XirToken, XirError>) + 'e {
|
||||
) -> impl FnMut(&Result<XirToken, tamer::xir::Error>) + 'e {
|
||||
use tamer::xir::writer::XmlWriter;
|
||||
|
||||
let mut xmlwriter = Default::default();
|
||||
|
@ -127,18 +119,16 @@ fn compile<R: Reporter>(
|
|||
|
||||
let mut ebuf = String::new();
|
||||
|
||||
fn report_err<R: Reporter>(
|
||||
e: &RecoverableError,
|
||||
reporter: &mut R,
|
||||
ebuf: &mut String,
|
||||
) -> Result<(), UnrecoverableError> {
|
||||
// See below note about buffering.
|
||||
ebuf.clear();
|
||||
writeln!(ebuf, "{}", reporter.render(e))?;
|
||||
println!("{ebuf}");
|
||||
let report_err = |result: Result<(), RecoverableError>| {
|
||||
result.or_else(|e| {
|
||||
// See below note about buffering.
|
||||
ebuf.clear();
|
||||
writeln!(ebuf, "{}", reporter.render(&e))?;
|
||||
println!("{ebuf}");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Ok::<_, UnrecoverableError>(())
|
||||
})
|
||||
};
|
||||
|
||||
// TODO: We're just echoing back out XIR,
|
||||
// which will be the same sans some formatting.
|
||||
|
@ -151,48 +141,26 @@ fn compile<R: Reporter>(
|
|||
{
|
||||
|_| ()
|
||||
}
|
||||
}))
|
||||
.map(|result| result.map_err(RecoverableError::from));
|
||||
}));
|
||||
|
||||
// TODO: Determine a good default capacity once we have this populated
|
||||
// and can come up with some heuristics.
|
||||
let asg = DefaultAsg::with_capacity(1024, 2048);
|
||||
|
||||
let (_, asg) = Lower::<
|
||||
ParsedObject<UnknownToken, XirToken, XirError>,
|
||||
XirToXirf<64, RefinedText>,
|
||||
_,
|
||||
>::lower::<_, UnrecoverableError>(src, |toks| {
|
||||
Lower::<XirToXirf<64, RefinedText>, XirfToNir, _>::lower(toks, |nir| {
|
||||
Lower::<XirfToNir, InterpolateNir, _>::lower(nir, |nir| {
|
||||
Lower::<InterpolateNir, NirToAir, _>::lower(nir, |air| {
|
||||
Lower::<NirToAir, AirAggregate, _>::lower_with_context(
|
||||
air,
|
||||
asg,
|
||||
|end| {
|
||||
end.fold(Ok(()), |x, result| match result {
|
||||
Ok(_) => x,
|
||||
Err(e) => {
|
||||
report_err(&e, reporter, &mut ebuf)?;
|
||||
x
|
||||
}
|
||||
})
|
||||
},
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
})?;
|
||||
let (air_ctx,) = parse_package_xml(
|
||||
src,
|
||||
DefaultAsg::with_capacity(1024, 2048),
|
||||
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 _ = asg; // unused_variables
|
||||
let _ = air_ctx; // unused_variables
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
@ -220,59 +188,26 @@ fn derive_xmli(
|
|||
escaper: &DefaultEscaper,
|
||||
) -> Result<(), UnrecoverableError> {
|
||||
use tamer::{
|
||||
asg::{
|
||||
visit::{tree_reconstruction, TreeWalkRel},
|
||||
AsgTreeToXirf,
|
||||
},
|
||||
iter::TrippableIterator,
|
||||
parse::terminal,
|
||||
xir::{
|
||||
autoclose::XirfAutoClose,
|
||||
flat::{Text, XirfToXir},
|
||||
writer::XmlWriter,
|
||||
},
|
||||
asg::visit::tree_reconstruction,
|
||||
pipeline,
|
||||
xir::writer::{WriterState, XmlWriter},
|
||||
};
|
||||
|
||||
let mut head =
|
||||
lowerable::<UnknownToken, _, _>(tree_reconstruction(&asg).map(Ok))
|
||||
.map(|result| result.map_err(UnrecoverableError::from));
|
||||
let src = lowerable(tree_reconstruction(&asg).map(Ok));
|
||||
|
||||
// THIS IS A PROOF-OF-CONCEPT LOWERING PIPELINE.
|
||||
Lower::<
|
||||
ParsedObject<UnknownToken, TreeWalkRel, Infallible>,
|
||||
AsgTreeToXirf,
|
||||
_,
|
||||
>::lower_with_context::<_, UnrecoverableError>(
|
||||
&mut head,
|
||||
&asg,
|
||||
|xirf_unclosed| {
|
||||
Lower::<AsgTreeToXirf, XirfAutoClose, _>::lower(
|
||||
xirf_unclosed,
|
||||
|xirf| {
|
||||
Lower::<XirfAutoClose, XirfToXir<Text>, _>::lower(
|
||||
xirf,
|
||||
|xir| {
|
||||
terminal::<XirfToXir<Text>, _>(xir).while_ok(
|
||||
|toks| {
|
||||
// Write failures should immediately bail out;
|
||||
// we can't skip writing portions of the file and
|
||||
// just keep going!
|
||||
toks.write(
|
||||
&mut fout,
|
||||
Default::default(),
|
||||
escaper,
|
||||
)?;
|
||||
Ok::<_, UnrecoverableError>(())
|
||||
},
|
||||
// TODO: Remove bad file?
|
||||
// Let make do it?
|
||||
)
|
||||
},
|
||||
)
|
||||
},
|
||||
)
|
||||
},
|
||||
)?;
|
||||
// TODO: Remove bad file?
|
||||
// Let make do it?
|
||||
let mut st = WriterState::default();
|
||||
let (_asg,) = pipeline::lower_xmli(src, &asg, |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)
|
||||
})
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
@ -461,6 +396,14 @@ 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)
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! Diagnostic system for error reporting.
|
||||
//! Diagnostic system for error reporting and logging.
|
||||
//!
|
||||
//! This system is heavily motivated by Rust's.
|
||||
//! While the data structures and organization may differ,
|
||||
|
@ -81,12 +81,15 @@ pub mod panic;
|
|||
mod report;
|
||||
mod resolve;
|
||||
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
convert::Infallible,
|
||||
fmt::{self, Debug, Display},
|
||||
};
|
||||
|
||||
pub use report::{Reporter, VisualReporter};
|
||||
pub use resolve::FsSpanResolver;
|
||||
|
||||
use core::fmt;
|
||||
use std::{borrow::Cow, convert::Infallible, error::Error, fmt::Display};
|
||||
|
||||
use crate::span::Span;
|
||||
|
||||
/// No annotated description is applicable for the diagnostic message.
|
||||
|
@ -97,12 +100,17 @@ use crate::span::Span;
|
|||
/// depends on the error context.
|
||||
pub const NO_DESC: Vec<AnnotatedSpan> = vec![];
|
||||
|
||||
/// Diagnostic report.
|
||||
/// An event able to describe itself for diagnostic reporting.
|
||||
///
|
||||
/// This describes an error condition or other special event using a series
|
||||
/// of [`Span`]s to describe the source, cause, and circumstances around
|
||||
/// an event.
|
||||
pub trait Diagnostic: Error + Sized {
|
||||
/// This describes an event using a series of [`Span`]s to describe the
|
||||
/// source, cause, and circumstances around an event.
|
||||
///
|
||||
/// A diagnostic event is not necessarily an error condition;
|
||||
/// for example,
|
||||
/// a user may request logging of compilation events to inspect the
|
||||
/// state of the system or help them to debug why the system is
|
||||
/// interpreting their program in a certain way.
|
||||
pub trait Diagnostic: Display + Debug + Sized {
|
||||
/// Produce a series of [`AnnotatedSpan`]s describing the source and
|
||||
/// circumstances of the diagnostic event.
|
||||
fn describe(&self) -> Vec<AnnotatedSpan>;
|
||||
|
@ -283,7 +291,7 @@ pub trait Annotate: Sized {
|
|||
self.annotate(Level::Error, Some(label.into()))
|
||||
}
|
||||
|
||||
/// Like [`Annotate::error`],
|
||||
/// Like [`Annotate::error`]r
|
||||
/// but only styles the span as a [`Level::Error`] without attaching a
|
||||
/// label.
|
||||
///
|
||||
|
|
|
@ -27,8 +27,8 @@ use super::xmle::{
|
|||
};
|
||||
use crate::{
|
||||
asg::{
|
||||
air::{Air, AirAggregate},
|
||||
Asg, AsgError, DefaultAsg,
|
||||
air::{Air, AirAggregateCtx},
|
||||
AsgError, DefaultAsg,
|
||||
},
|
||||
diagnose::{AnnotatedSpan, Diagnostic},
|
||||
fs::{
|
||||
|
@ -36,17 +36,12 @@ use crate::{
|
|||
VisitOnceFilesystem,
|
||||
},
|
||||
ld::xmle::Sections,
|
||||
obj::xmlo::{
|
||||
XmloAirContext, XmloAirError, XmloError, XmloReader, XmloToAir,
|
||||
XmloToken,
|
||||
},
|
||||
parse::{
|
||||
lowerable, FinalizeError, Lower, ParseError, Parsed, ParsedObject,
|
||||
UnknownToken,
|
||||
},
|
||||
obj::xmlo::{XmloAirContext, XmloAirError, XmloError, XmloToken},
|
||||
parse::{lowerable, FinalizeError, ParseError, UnknownToken},
|
||||
pipeline,
|
||||
sym::{GlobalSymbolResolve, SymbolId},
|
||||
xir::{
|
||||
flat::{PartialXirToXirf, Text, XirToXirfError, XirfToken},
|
||||
flat::{Text, XirToXirfError, XirfToken},
|
||||
reader::XmlXirReader,
|
||||
writer::{Error as XirWriterError, XmlWriter},
|
||||
DefaultEscaper, Error as XirError, Escaper, Token as XirToken,
|
||||
|
@ -54,6 +49,7 @@ use crate::{
|
|||
};
|
||||
use fxhash::FxBuildHasher;
|
||||
use std::{
|
||||
convert::identity,
|
||||
error::Error,
|
||||
fmt::{self, Display},
|
||||
fs,
|
||||
|
@ -67,10 +63,10 @@ pub fn xmle(package_path: &str, output: &str) -> Result<(), TameldError> {
|
|||
let mut fs = VisitOnceFilesystem::new();
|
||||
let escaper = DefaultEscaper::default();
|
||||
|
||||
let (depgraph, state) = load_xmlo(
|
||||
let (air_ctx, state) = load_xmlo(
|
||||
package_path,
|
||||
&mut fs,
|
||||
LinkerAsg::with_capacity(65536, 65536),
|
||||
LinkerAsg::with_capacity(65536, 65536).into(),
|
||||
&escaper,
|
||||
XmloAirContext::default(),
|
||||
)?;
|
||||
|
@ -81,7 +77,8 @@ pub fn xmle(package_path: &str, output: &str) -> Result<(), TameldError> {
|
|||
..
|
||||
} = state;
|
||||
|
||||
let sorted = sort(&depgraph, Sections::new())?;
|
||||
let asg = air_ctx.finish();
|
||||
let sorted = sort(&asg, Sections::new())?;
|
||||
|
||||
output_xmle(
|
||||
sorted,
|
||||
|
@ -97,57 +94,21 @@ pub fn xmle(package_path: &str, output: &str) -> Result<(), TameldError> {
|
|||
fn load_xmlo<P: AsRef<Path>, S: Escaper>(
|
||||
path_str: P,
|
||||
fs: &mut VisitOnceFilesystem<FsCanonicalizer, FxBuildHasher>,
|
||||
asg: Asg,
|
||||
air_ctx: AirAggregateCtx,
|
||||
escaper: &S,
|
||||
state: XmloAirContext,
|
||||
) -> Result<(Asg, XmloAirContext), TameldError> {
|
||||
) -> Result<(AirAggregateCtx, XmloAirContext), TameldError> {
|
||||
let PathFile(path, file, ctx): PathFile<BufReader<fs::File>> =
|
||||
match fs.open(path_str)? {
|
||||
VisitOnceFile::FirstVisit(file) => file,
|
||||
VisitOnceFile::Visited => return Ok((asg, state)),
|
||||
VisitOnceFile::Visited => return Ok((air_ctx, state)),
|
||||
};
|
||||
|
||||
let src = &mut lowerable(XmlXirReader::new(file, escaper, ctx))
|
||||
.map(|result| result.map_err(TameldError::from));
|
||||
let src = &mut lowerable(XmlXirReader::new(file, escaper, ctx));
|
||||
|
||||
// TODO: This entire block is a WIP and will be incrementally
|
||||
// abstracted away.
|
||||
let (mut asg, mut state) = Lower::<
|
||||
ParsedObject<UnknownToken, XirToken, XirError>,
|
||||
PartialXirToXirf<4, Text>,
|
||||
_,
|
||||
>::lower(src, |toks| {
|
||||
Lower::<PartialXirToXirf<4, Text>, XmloReader, _>::lower(toks, |xmlo| {
|
||||
let mut iter = xmlo.scan(false, |st, rtok| match st {
|
||||
true => None,
|
||||
false => {
|
||||
*st =
|
||||
matches!(rtok, Ok(Parsed::Object(XmloToken::Eoh(..))));
|
||||
Some(rtok)
|
||||
}
|
||||
});
|
||||
|
||||
Lower::<XmloReader, XmloToAir, _>::lower_with_context(
|
||||
&mut iter,
|
||||
state,
|
||||
|air| {
|
||||
let (_, asg) =
|
||||
Lower::<XmloToAir, AirAggregate, _>::lower_with_context(
|
||||
air,
|
||||
asg,
|
||||
|end| {
|
||||
end.fold(
|
||||
Result::<(), TameldError>::Ok(()),
|
||||
|x, _| x,
|
||||
)
|
||||
},
|
||||
)?;
|
||||
|
||||
Ok::<_, TameldError>(asg)
|
||||
},
|
||||
)
|
||||
})
|
||||
})?;
|
||||
let (mut state, mut air_ctx) = pipeline::load_xmlo::<_, TameldError, _>(
|
||||
src, state, air_ctx, identity,
|
||||
)?;
|
||||
|
||||
let mut dir = path;
|
||||
dir.pop();
|
||||
|
@ -159,10 +120,10 @@ fn load_xmlo<P: AsRef<Path>, S: Escaper>(
|
|||
path_buf.push(relpath.lookup_str());
|
||||
path_buf.set_extension("xmlo");
|
||||
|
||||
(asg, state) = load_xmlo(path_buf, fs, asg, escaper, state)?;
|
||||
(air_ctx, state) = load_xmlo(path_buf, fs, air_ctx, escaper, state)?;
|
||||
}
|
||||
|
||||
Ok((asg, state))
|
||||
Ok((air_ctx, state))
|
||||
}
|
||||
|
||||
fn output_xmle<'a, X: XmleSections<'a>, S: Escaper>(
|
||||
|
@ -194,9 +155,9 @@ fn output_xmle<'a, X: XmleSections<'a>, S: Escaper>(
|
|||
/// This cannot include panics,
|
||||
/// but efforts have been made to reduce panics to situations that
|
||||
/// represent the equivalent of assertions.
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum TameldError {
|
||||
Io(io::Error),
|
||||
Io(NeqIoError),
|
||||
SortError(SortError),
|
||||
XirParseError(ParseError<UnknownToken, XirError>),
|
||||
XirfParseError(ParseError<XirToken, XirToXirfError>),
|
||||
|
@ -208,9 +169,34 @@ pub enum TameldError {
|
|||
Fmt(fmt::Error),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct NeqIoError(io::Error);
|
||||
|
||||
impl PartialEq for NeqIoError {
|
||||
fn eq(&self, _other: &Self) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for NeqIoError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self(e) => e.fmt(f),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Error for NeqIoError {}
|
||||
|
||||
impl From<io::Error> for NeqIoError {
|
||||
fn from(e: io::Error) -> Self {
|
||||
Self(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<io::Error> for TameldError {
|
||||
fn from(e: io::Error) -> Self {
|
||||
Self::Io(e)
|
||||
Self::Io(e.into())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,216 @@
|
|||
// Test ASG lowering into xmle sections
|
||||
//
|
||||
// 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::{
|
||||
asg::{
|
||||
air::{test::asg_from_toks, Air},
|
||||
AsgError, FragmentText, Ident, IdentKind, Source,
|
||||
},
|
||||
ld::xmle::{section::PushResult, Sections},
|
||||
parse::util::SPair,
|
||||
span::dummy::*,
|
||||
sym::SymbolId,
|
||||
};
|
||||
|
||||
use Air::*;
|
||||
|
||||
#[test]
|
||||
fn graph_sort() -> SortResult<()> {
|
||||
// We care only about the order of pushes, not the sections they end
|
||||
// up in.
|
||||
struct StubSections<'a> {
|
||||
pushed: Vec<&'a Ident>,
|
||||
}
|
||||
|
||||
impl<'a> XmleSections<'a> for StubSections<'a> {
|
||||
fn push(&mut self, ident: &'a Ident) -> PushResult {
|
||||
self.pushed.push(ident);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn take_deps(&mut self) -> Vec<&'a Ident> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn take_static(&mut self) -> Vec<SymbolId> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn take_map(&mut self) -> Vec<SymbolId> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn take_map_froms(&mut self) -> fxhash::FxHashSet<SymbolId> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn take_retmap(&mut self) -> Vec<SymbolId> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn take_exec(&mut self) -> Vec<SymbolId> {
|
||||
unimplemented!()
|
||||
}
|
||||
}
|
||||
|
||||
let name_a_dep = SPair("adep".into(), S4);
|
||||
let name_a = SPair("a".into(), S3);
|
||||
let name_a_dep_dep = SPair("adepdep".into(), S2);
|
||||
|
||||
#[rustfmt::skip]
|
||||
let toks = [
|
||||
PkgStart(S1, SPair("/pkg".into(), S1)),
|
||||
// Intentionally out-of-order.
|
||||
IdentDecl(
|
||||
name_a_dep,
|
||||
IdentKind::Meta,
|
||||
Default::default(),
|
||||
),
|
||||
|
||||
IdentDecl(
|
||||
name_a,
|
||||
IdentKind::Meta,
|
||||
Default::default(),
|
||||
),
|
||||
|
||||
IdentDecl(
|
||||
name_a_dep_dep,
|
||||
IdentKind::Meta,
|
||||
Default::default(),
|
||||
),
|
||||
|
||||
IdentDep(name_a, name_a_dep),
|
||||
IdentDep(name_a_dep, name_a_dep_dep),
|
||||
IdentRoot(name_a),
|
||||
PkgEnd(S5),
|
||||
];
|
||||
|
||||
let asg = asg_from_toks(toks);
|
||||
let sections = sort(&asg, StubSections { pushed: Vec::new() })?;
|
||||
|
||||
let expected = vec![
|
||||
// Post-order
|
||||
name_a_dep_dep,
|
||||
name_a_dep,
|
||||
name_a,
|
||||
]
|
||||
.into_iter()
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
assert_eq!(
|
||||
expected,
|
||||
sections
|
||||
.pushed
|
||||
.iter()
|
||||
.map(|ident| ident.name())
|
||||
.collect::<Vec<_>>(),
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn graph_sort_missing_node() -> SortResult<()> {
|
||||
let sym = SPair("sym".into(), S1);
|
||||
let dep = SPair("dep".into(), S2);
|
||||
|
||||
#[rustfmt::skip]
|
||||
let toks = [
|
||||
PkgStart(S1, SPair("/pkg".into(), S1)),
|
||||
IdentDecl(
|
||||
sym,
|
||||
IdentKind::Tpl,
|
||||
Source {
|
||||
virtual_: true,
|
||||
..Default::default()
|
||||
},
|
||||
),
|
||||
IdentFragment(sym, FragmentText::from("foo")),
|
||||
|
||||
// dep will be added as Missing
|
||||
IdentDep(sym, dep),
|
||||
|
||||
IdentRoot(sym),
|
||||
PkgEnd(S5),
|
||||
];
|
||||
|
||||
let asg = asg_from_toks(toks);
|
||||
|
||||
match sort(&asg, Sections::new()) {
|
||||
Ok(_) => panic!("Unexpected success - dependency is not in graph"),
|
||||
Err(SortError::SectionsError(SectionsError::UnresolvedObject(_))) => (),
|
||||
bad => {
|
||||
panic!(
|
||||
"Incorrect error result when dependency is not in graph: {:?}",
|
||||
bad
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn graph_sort_simple_cycle() -> SortResult<()> {
|
||||
let sym = SPair("sym".into(), S1);
|
||||
let dep = SPair("dep".into(), S2);
|
||||
|
||||
#[rustfmt::skip]
|
||||
let toks = [
|
||||
PkgStart(S1, SPair("/pkg".into(), S1)),
|
||||
IdentDecl(
|
||||
sym,
|
||||
IdentKind::Tpl,
|
||||
Source {
|
||||
virtual_: true,
|
||||
..Default::default()
|
||||
},
|
||||
),
|
||||
IdentFragment(sym, FragmentText::from("foo")),
|
||||
|
||||
IdentDecl(
|
||||
dep,
|
||||
IdentKind::Tpl,
|
||||
Source {
|
||||
virtual_: true,
|
||||
..Default::default()
|
||||
},
|
||||
),
|
||||
IdentFragment(dep, FragmentText::from("bar")),
|
||||
|
||||
// Produce a cycle (which will be an error)
|
||||
IdentDep(sym, dep),
|
||||
IdentDep(dep, sym),
|
||||
|
||||
IdentRoot(sym),
|
||||
PkgEnd(S5),
|
||||
];
|
||||
|
||||
let asg = asg_from_toks(toks);
|
||||
let result = sort(&asg, Sections::new());
|
||||
|
||||
match result {
|
||||
Ok(_) => panic!("sort did not detect cycle"),
|
||||
Err(SortError::AsgError(AsgError::UnsupportedCycle(_))) => (),
|
||||
Err(e) => panic!("unexpected error: {}", e),
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
|
@ -60,15 +60,18 @@
|
|||
// in which case this feature's only substitute is a type parameter.
|
||||
#![feature(associated_type_defaults)]
|
||||
// Convenience features that are easily replaced if not stabalized.
|
||||
#![feature(nonzero_min_max)]
|
||||
#![feature(nonzero_ops)]
|
||||
// Enabled for qualified paths in `matches!`.
|
||||
#![feature(more_qualified_paths)]
|
||||
// Collecting interators into existing objects.
|
||||
// Can be done manually in a more verbose way.
|
||||
#![feature(iter_collect_into)]
|
||||
// Convenience; can be done more verbosely.
|
||||
#![feature(is_some_and)]
|
||||
// Concise and descriptive.
|
||||
// Can be done manually in a more verbose way.
|
||||
#![feature(str_split_remainder)]
|
||||
// Concise and descriptive.
|
||||
// Can be done manually in a more verbose way.
|
||||
#![feature(iter_intersperse)]
|
||||
// Used for const params like `&'static str` in `crate::fmt`.
|
||||
// If this is not stabalized,
|
||||
// then we can do without by changing the abstraction;
|
||||
|
@ -84,6 +87,12 @@
|
|||
// Added for use with `rustfmt::skip`,
|
||||
// so that we can ignore formatting more precisely.
|
||||
#![feature(stmt_expr_attributes)]
|
||||
// Allows using `impl Trait` for associated type bounds instead of having to
|
||||
// extract it into a more verbose `where` clause.
|
||||
// This is not necessary,
|
||||
// and may not even be desirable,
|
||||
// but it's a nice option to have if `impl` would otherwise be used.
|
||||
#![feature(associated_type_bounds)]
|
||||
// We build docs for private items.
|
||||
#![allow(rustdoc::private_intra_doc_links)]
|
||||
// For sym::prefill recursive macro `static_symbols!`.
|
||||
|
@ -185,6 +194,7 @@ pub mod nir;
|
|||
pub mod num;
|
||||
pub mod obj;
|
||||
pub mod parse;
|
||||
pub mod pipeline;
|
||||
pub mod span;
|
||||
pub mod sym;
|
||||
|
||||
|
|
157
tamer/src/nir.rs
157
tamer/src/nir.rs
|
@ -52,17 +52,17 @@
|
|||
mod air;
|
||||
mod interp;
|
||||
mod parse;
|
||||
mod tplshort;
|
||||
|
||||
use crate::{
|
||||
diagnose::{Annotate, Diagnostic},
|
||||
f::Functor,
|
||||
fmt::{DisplayWrapper, TtQuote},
|
||||
parse::{util::SPair, Object, Token},
|
||||
span::{Span, UNKNOWN_SPAN},
|
||||
span::Span,
|
||||
sym::SymbolId,
|
||||
xir::{
|
||||
attr::{Attr, AttrSpan},
|
||||
fmt::TtXmlAttr,
|
||||
QName,
|
||||
},
|
||||
};
|
||||
|
@ -77,6 +77,7 @@ pub use interp::{InterpError, InterpState as InterpolateNir};
|
|||
pub use parse::{
|
||||
NirParseState as XirfToNir, NirParseStateError_ as XirfToNirError,
|
||||
};
|
||||
pub use tplshort::TplShortDesugar;
|
||||
|
||||
/// IR that is "near" the source code.
|
||||
///
|
||||
|
@ -85,7 +86,7 @@ pub use parse::{
|
|||
/// (e.g. XML).
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub enum Nir {
|
||||
Todo,
|
||||
Todo(Span),
|
||||
TodoAttr(SPair),
|
||||
|
||||
/// Begin the definition of some [`NirEntity`] and place it atop of the
|
||||
|
@ -97,12 +98,40 @@ pub enum Nir {
|
|||
|
||||
/// Bind the given name as an identifier for the entity atop of the
|
||||
/// stack.
|
||||
///
|
||||
/// [`Self::Ref`] references identifiers created using this token.
|
||||
BindIdent(SPair),
|
||||
|
||||
/// Reference the value of the given identifier as the subject of the
|
||||
/// current expression.
|
||||
///
|
||||
/// The source language of TAME is historically XML,
|
||||
/// which permits attributes in any order.
|
||||
/// If an expression contains multiple references that the user expects
|
||||
/// to mean a particular thing depending on attribute name,
|
||||
/// then this can be used to disambiguate.
|
||||
/// Other references may use [`Self::Ref`].
|
||||
///
|
||||
/// In XML notation,
|
||||
/// examples include `match/@on` and `c:value-of/@name`.
|
||||
///
|
||||
/// NIR may at its discretion use this token to impose ordering,
|
||||
/// which would have the effect of imposing XML attribute order.
|
||||
RefSubject(SPair),
|
||||
|
||||
/// Reference the value of the given identifier.
|
||||
///
|
||||
/// Permissible identifiers and values depend on the context in which
|
||||
/// this appears.
|
||||
/// Identifiers are defined using [`Self::BindIdent`].
|
||||
///
|
||||
/// This reference has no particular significance other than not being
|
||||
/// the [`Self::RefSubject`] of the expression;
|
||||
/// all [`Self::Ref`]s within a given expression are semantically
|
||||
/// equivalent,
|
||||
/// and so should not be distinguished to the user by different
|
||||
/// attribute names unless those names are too semantically
|
||||
/// equivalent.
|
||||
Ref(SPair),
|
||||
|
||||
/// Describe the [`NirEntity`] atop of the stack.
|
||||
|
@ -115,6 +144,22 @@ pub enum Nir {
|
|||
/// it may represent literate documentation or a literal in a
|
||||
/// metavariable definition.
|
||||
Text(SPair),
|
||||
|
||||
/// Import a package identified by the provided namespec.
|
||||
Import(SPair),
|
||||
|
||||
/// "No-op" (no operation) that does nothing.
|
||||
///
|
||||
/// Since this is taking user input and effectively discarding it,
|
||||
/// this contains a [`Span`],
|
||||
/// so that we can clearly see the source code associated with what we
|
||||
/// chose to discard.
|
||||
///
|
||||
/// Ideally this can be eliminated in the future by causing an
|
||||
/// incomplete parse,
|
||||
/// which is all this does in the end.
|
||||
/// See its uses for more information.
|
||||
Noop(Span),
|
||||
}
|
||||
|
||||
impl Nir {
|
||||
|
@ -130,14 +175,15 @@ impl Nir {
|
|||
use Nir::*;
|
||||
|
||||
match self {
|
||||
Todo => None,
|
||||
Todo(_) => None,
|
||||
TodoAttr(spair) => Some(spair.symbol()),
|
||||
|
||||
Open(_, _) | Close(_, _) => None,
|
||||
|
||||
BindIdent(spair) | Ref(spair) | Desc(spair) | Text(spair) => {
|
||||
Some(spair.symbol())
|
||||
}
|
||||
BindIdent(spair) | RefSubject(spair) | Ref(spair) | Desc(spair)
|
||||
| Text(spair) | Import(spair) => Some(spair.symbol()),
|
||||
|
||||
Noop(_) => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -161,15 +207,19 @@ impl Functor<SymbolId> for Nir {
|
|||
use Nir::*;
|
||||
|
||||
match self {
|
||||
Todo => self,
|
||||
Todo(_) => self,
|
||||
TodoAttr(spair) => TodoAttr(spair.map(f)),
|
||||
|
||||
Open(_, _) | Close(_, _) => self,
|
||||
|
||||
BindIdent(spair) => BindIdent(spair.map(f)),
|
||||
RefSubject(spair) => RefSubject(spair.map(f)),
|
||||
Ref(spair) => Ref(spair.map(f)),
|
||||
Desc(spair) => Desc(spair.map(f)),
|
||||
Text(spair) => Text(spair.map(f)),
|
||||
Import(spair) => Import(spair.map(f)),
|
||||
|
||||
Noop(_) => self,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -198,11 +248,27 @@ pub enum NirEntity {
|
|||
All,
|
||||
/// Disjunctive (∨) expression.
|
||||
Any,
|
||||
/// Logical predicate.
|
||||
Match,
|
||||
|
||||
/// Template.
|
||||
Tpl,
|
||||
/// Template parameter (metavariable).
|
||||
|
||||
/// Template application (long form).
|
||||
TplApply,
|
||||
/// Template parameter (long form).
|
||||
TplParam,
|
||||
|
||||
/// Template application (shorthand).
|
||||
TplApplyShort(QName),
|
||||
/// Template parameter (shorthand).
|
||||
///
|
||||
/// The non-`@`-padded name is the first of the pair and the value as
|
||||
/// the second.
|
||||
///
|
||||
/// A shorthand param is implicitly closed and should not have a
|
||||
/// matching [`Nir::Close`] token.
|
||||
TplParamShort(SPair, SPair),
|
||||
}
|
||||
|
||||
impl NirEntity {
|
||||
|
@ -231,16 +297,31 @@ impl Display for NirEntity {
|
|||
Classify => write!(f, "classification"),
|
||||
All => write!(f, "conjunctive (∧) expression"),
|
||||
Any => write!(f, "disjunctive (∨) expression"),
|
||||
Match => write!(f, "logical predicate (match)"),
|
||||
|
||||
Tpl => write!(f, "template"),
|
||||
TplParam => write!(f, "template param (metavariable)"),
|
||||
TplParamShort(name, val) => write!(
|
||||
f,
|
||||
"shorthand template param key {} with value {}",
|
||||
TtQuote::wrap(name),
|
||||
TtQuote::wrap(val),
|
||||
),
|
||||
TplApply => {
|
||||
write!(f, "full template application and expansion")
|
||||
}
|
||||
TplApplyShort(qname) => write!(
|
||||
f,
|
||||
"full shorthand template application and expansion of {}",
|
||||
TtQuote::wrap(qname.local_name())
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Token for Nir {
|
||||
fn ir_name() -> &'static str {
|
||||
"Plain NIR"
|
||||
"NIR"
|
||||
}
|
||||
|
||||
/// Identifying span of a token.
|
||||
|
@ -252,15 +333,19 @@ impl Token for Nir {
|
|||
use Nir::*;
|
||||
|
||||
match self {
|
||||
Todo => UNKNOWN_SPAN,
|
||||
Todo(span) => *span,
|
||||
TodoAttr(spair) => spair.span(),
|
||||
|
||||
Open(_, span) => *span,
|
||||
Close(_, span) => *span,
|
||||
|
||||
BindIdent(spair) | Ref(spair) | Desc(spair) | Text(spair) => {
|
||||
spair.span()
|
||||
}
|
||||
BindIdent(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
|
||||
// explicitly inquire about and report on it.
|
||||
Noop(span) => *span,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -272,7 +357,7 @@ impl Display for Nir {
|
|||
use Nir::*;
|
||||
|
||||
match self {
|
||||
Todo => write!(f, "TODO"),
|
||||
Todo(_) => write!(f, "TODO"),
|
||||
TodoAttr(spair) => write!(f, "TODO Attr {spair}"),
|
||||
|
||||
Open(entity, _) => write!(f, "open {entity} entity"),
|
||||
|
@ -280,6 +365,9 @@ impl Display for Nir {
|
|||
BindIdent(spair) => {
|
||||
write!(f, "bind identifier {}", TtQuote::wrap(spair))
|
||||
}
|
||||
RefSubject(spair) => {
|
||||
write!(f, "subject ref {}", TtQuote::wrap(spair))
|
||||
}
|
||||
Ref(spair) => write!(f, "ref {}", TtQuote::wrap(spair)),
|
||||
|
||||
// TODO: TtQuote doesn't yet escape quotes at the time of writing!
|
||||
|
@ -289,6 +377,14 @@ impl Display for Nir {
|
|||
// need to determine how to handle newlines and other types of
|
||||
// output.
|
||||
Text(_) => write!(f, "text"),
|
||||
|
||||
Import(namespec) => write!(
|
||||
f,
|
||||
"import package with namespec {}",
|
||||
TtQuote::wrap(namespec)
|
||||
),
|
||||
|
||||
Noop(_) => write!(f, "no-op"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -448,21 +544,14 @@ pub enum PkgType {
|
|||
Mod,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub struct Literal<const S: SymbolId>;
|
||||
|
||||
impl<const S: SymbolId> TryFrom<Attr> for Literal<S> {
|
||||
type Error = NirAttrParseError;
|
||||
|
||||
fn try_from(attr: Attr) -> Result<Self, Self::Error> {
|
||||
match attr {
|
||||
Attr(_, val, _) if val == S => Ok(Literal),
|
||||
Attr(name, _, aspan) => Err(NirAttrParseError::LiteralMismatch(
|
||||
name,
|
||||
aspan.value_span(),
|
||||
S,
|
||||
)),
|
||||
}
|
||||
/// Assert that a literal value `S` was provided,
|
||||
/// yielding a [`Nir::Noop`] if successful.
|
||||
pub fn literal<const S: SymbolId>(
|
||||
value: SPair,
|
||||
) -> Result<Nir, NirAttrParseError> {
|
||||
match value {
|
||||
SPair(val, span) if val == S => Ok(Nir::Noop(span)),
|
||||
_ => Err(NirAttrParseError::LiteralMismatch(value.span(), S)),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -476,7 +565,7 @@ type ExpectedSymbolId = SymbolId;
|
|||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub enum NirAttrParseError {
|
||||
LiteralMismatch(QName, Span, ExpectedSymbolId),
|
||||
LiteralMismatch(Span, ExpectedSymbolId),
|
||||
}
|
||||
|
||||
impl Error for NirAttrParseError {
|
||||
|
@ -488,8 +577,8 @@ impl Error for NirAttrParseError {
|
|||
impl Display for NirAttrParseError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::LiteralMismatch(name, _, _) => {
|
||||
write!(f, "unexpected value for {}", TtXmlAttr::wrap(name),)
|
||||
Self::LiteralMismatch(_, expected) => {
|
||||
write!(f, "expected literal {}", TtQuote::wrap(expected),)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -498,7 +587,7 @@ impl Display for NirAttrParseError {
|
|||
impl Diagnostic for NirAttrParseError {
|
||||
fn describe(&self) -> Vec<crate::diagnose::AnnotatedSpan> {
|
||||
match self {
|
||||
Self::LiteralMismatch(_, span, expected) => span
|
||||
Self::LiteralMismatch(span, expected) => span
|
||||
.error(format!("expecting {}", TtQuote::wrap(expected)))
|
||||
.into(),
|
||||
}
|
||||
|
|
|
@ -19,22 +19,39 @@
|
|||
|
||||
//! Lower [NIR](super) into [AIR](crate::asg::air).
|
||||
|
||||
use super::Nir;
|
||||
use crate::{
|
||||
asg::air::Air,
|
||||
diagnose::{Annotate, Diagnostic},
|
||||
fmt::{DisplayWrapper, TtQuote},
|
||||
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::{air::Air, ExprOp},
|
||||
diagnose::Diagnostic,
|
||||
nir::NirEntity,
|
||||
parse::prelude::*,
|
||||
span::UNKNOWN_SPAN,
|
||||
asg::ExprOp,
|
||||
nir::{Nir::*, NirEntity::*},
|
||||
sym::{st::raw::U_TRUE, SymbolId},
|
||||
};
|
||||
|
||||
use super::Nir;
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Default)]
|
||||
pub enum NirToAir {
|
||||
#[default]
|
||||
Ready,
|
||||
|
||||
/// Predicate opened but its subject is not yet known.
|
||||
PredOpen(Span),
|
||||
|
||||
/// A predicate has been partially applied to its subject,
|
||||
/// but we do not yet know its function or comparison value.
|
||||
PredPartial(Span, SPair),
|
||||
|
||||
/// Processing a metavariable.
|
||||
Meta(Span),
|
||||
}
|
||||
|
||||
impl Display for NirToAir {
|
||||
|
@ -43,105 +60,253 @@ impl Display for NirToAir {
|
|||
|
||||
match self {
|
||||
Ready => write!(f, "ready to lower NIR to AIR"),
|
||||
PredOpen(_) => {
|
||||
write!(f, "awaiting information about open predicate")
|
||||
}
|
||||
PredPartial(_, name) => write!(
|
||||
f,
|
||||
"waiting to determine type of predicate for identifier {}",
|
||||
TtQuote::wrap(name),
|
||||
),
|
||||
Meta(_) => write!(f, "parsing metavariable definition"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Stack of [`Air`] objects to yield on subsequent iterations.
|
||||
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;
|
||||
|
||||
#[cfg(not(feature = "wip-asg-derived-xmli"))]
|
||||
fn parse_token(
|
||||
self,
|
||||
tok: Self::Token,
|
||||
_: NoContext,
|
||||
_queue: &mut Self::Context,
|
||||
) -> TransitionResult<Self::Super> {
|
||||
use NirToAir::*;
|
||||
|
||||
#[cfg(not(feature = "wip-nir-to-air"))]
|
||||
{
|
||||
let _ = tok; // prevent `unused_variables` warning
|
||||
return Transition(Ready).ok(Air::Todo(UNKNOWN_SPAN));
|
||||
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,
|
||||
) -> TransitionResult<Self::Super> {
|
||||
use NirToAir::*;
|
||||
use NirToAirError::*;
|
||||
|
||||
use crate::diagnostic_panic;
|
||||
|
||||
if let Some(obj) = stack.pop() {
|
||||
return Transition(Ready).ok(obj).with_lookahead(tok);
|
||||
}
|
||||
|
||||
#[allow(unreachable_code)] // due to wip-nir-to-air
|
||||
// TODO: The intent is to refactor this monstrosity once we're far
|
||||
// enough along that a clear pattern emerges.
|
||||
// Part of this process has been deriving appropriate
|
||||
// responsibilities betwen XIR->NIR, NIR->AIR, and AIR->ASG.
|
||||
match (self, tok) {
|
||||
(Ready, Nir::Open(NirEntity::Package, span)) => {
|
||||
Transition(Ready).ok(Air::PkgOpen(span))
|
||||
(Ready, Open(Package, span)) => {
|
||||
// TODO: Package name needs to be generated and provided to us;
|
||||
// this is transitionary.
|
||||
Transition(Ready)
|
||||
.ok(Air::PkgStart(span, SPair("/TODO".into(), span)))
|
||||
}
|
||||
|
||||
(Ready, Nir::Close(NirEntity::Package, span)) => {
|
||||
Transition(Ready).ok(Air::PkgClose(span))
|
||||
(Ready, Close(Package, span)) => {
|
||||
Transition(Ready).ok(Air::PkgEnd(span))
|
||||
}
|
||||
|
||||
(Ready, Nir::Open(NirEntity::Rate | NirEntity::Sum, span)) => {
|
||||
Transition(Ready).ok(Air::ExprOpen(ExprOp::Sum, span))
|
||||
(Ready, Open(Rate | Sum, span)) => {
|
||||
Transition(Ready).ok(Air::ExprStart(ExprOp::Sum, span))
|
||||
}
|
||||
(Ready, Nir::Open(NirEntity::Product, span)) => {
|
||||
Transition(Ready).ok(Air::ExprOpen(ExprOp::Product, span))
|
||||
(Ready, Open(Product, span)) => {
|
||||
Transition(Ready).ok(Air::ExprStart(ExprOp::Product, span))
|
||||
}
|
||||
(Ready, Nir::Open(NirEntity::Ceil, span)) => {
|
||||
Transition(Ready).ok(Air::ExprOpen(ExprOp::Ceil, span))
|
||||
(Ready, Open(Ceil, span)) => {
|
||||
Transition(Ready).ok(Air::ExprStart(ExprOp::Ceil, span))
|
||||
}
|
||||
(Ready, Nir::Open(NirEntity::Floor, span)) => {
|
||||
Transition(Ready).ok(Air::ExprOpen(ExprOp::Floor, span))
|
||||
(Ready, Open(Floor, span)) => {
|
||||
Transition(Ready).ok(Air::ExprStart(ExprOp::Floor, span))
|
||||
}
|
||||
(Ready, Nir::Open(NirEntity::Classify | NirEntity::All, span)) => {
|
||||
Transition(Ready).ok(Air::ExprOpen(ExprOp::Conj, span))
|
||||
(Ready, Open(Classify | All, span)) => {
|
||||
Transition(Ready).ok(Air::ExprStart(ExprOp::Conj, span))
|
||||
}
|
||||
(Ready, Nir::Open(NirEntity::Any, span)) => {
|
||||
Transition(Ready).ok(Air::ExprOpen(ExprOp::Disj, span))
|
||||
(Ready, Open(Any, span)) => {
|
||||
Transition(Ready).ok(Air::ExprStart(ExprOp::Disj, span))
|
||||
}
|
||||
|
||||
(Ready, Nir::Open(NirEntity::Tpl, span)) => {
|
||||
Transition(Ready).ok(Air::TplOpen(span))
|
||||
// Match
|
||||
(Ready, Open(Match, span)) => {
|
||||
Transition(PredOpen(span)).incomplete()
|
||||
}
|
||||
(PredOpen(ospan), RefSubject(on)) => {
|
||||
Transition(PredPartial(ospan, on)).incomplete()
|
||||
}
|
||||
(PredPartial(ospan, on), Ref(value)) => {
|
||||
stack.push(Air::RefIdent(value));
|
||||
stack.push(Air::RefIdent(on));
|
||||
Transition(Ready).ok(Air::ExprStart(ExprOp::Eq, ospan))
|
||||
}
|
||||
(PredPartial(ospan, on), Close(Match, cspan)) => {
|
||||
stack.push(Air::RefIdent(SPair(SYM_TRUE, ospan)));
|
||||
stack.push(Air::RefIdent(on));
|
||||
Transition(Ready)
|
||||
.ok(Air::ExprStart(ExprOp::Eq, ospan))
|
||||
.with_lookahead(Close(Match, cspan))
|
||||
}
|
||||
// Special case of the general error below,
|
||||
// since recovery here involves discarding the nonsense match.
|
||||
(PredOpen(ospan), Close(Match, span)) => Transition(Ready)
|
||||
.err(MatchSubjectExpected(ospan, Close(Match, span))),
|
||||
(PredOpen(ospan), tok) => Transition(PredOpen(ospan))
|
||||
.err(MatchSubjectExpected(ospan, tok)),
|
||||
(Ready, Close(Match, cspan)) => {
|
||||
Transition(Ready).ok(Air::ExprEnd(cspan))
|
||||
}
|
||||
(PredPartial(ospan, on), tok) => {
|
||||
// TODO: Until match body is supported,
|
||||
// error and discard tokens.
|
||||
Transition(PredPartial(ospan, on))
|
||||
.err(TodoMatchBody(ospan, tok.span()))
|
||||
}
|
||||
|
||||
(Ready, Nir::Close(NirEntity::Tpl, span)) => {
|
||||
Transition(Ready).ok(Air::TplClose(span))
|
||||
(Ready, Open(Tpl, span)) => {
|
||||
Transition(Ready).ok(Air::TplStart(span))
|
||||
}
|
||||
(Ready, Close(Tpl, span)) => {
|
||||
Transition(Ready).ok(Air::TplEnd(span))
|
||||
}
|
||||
|
||||
(Ready, Open(TplApply, span)) => {
|
||||
Transition(Ready).ok(Air::TplStart(span))
|
||||
}
|
||||
|
||||
// Short-hand template application must be handled through
|
||||
// desugaring as part of the lowering pipeline,
|
||||
// so that it is converted to long form before getting here.
|
||||
(
|
||||
Ready,
|
||||
Open(TplApplyShort(..) | TplParamShort(..), span)
|
||||
| Close(TplApplyShort(..) | TplParamShort(..), span),
|
||||
) => {
|
||||
// TODO: In the future maybe TAMER will have evolved its
|
||||
// abstractions enough that there's an ROI for prohibiting
|
||||
// this at the type level.
|
||||
diagnostic_panic!(
|
||||
vec![
|
||||
span.internal_error(
|
||||
"attempted shorthand template application"
|
||||
),
|
||||
span.help(
|
||||
"TAMER must be compiled with support for \
|
||||
shorthand template application by utilizing the \
|
||||
nir::tplshort module in the lowering pipeline."
|
||||
)
|
||||
],
|
||||
"shortand template application is unsupported in this \
|
||||
build of TAMER"
|
||||
)
|
||||
}
|
||||
(Ready, Close(TplApply, span)) => {
|
||||
Transition(Ready).ok(Air::TplEndRef(span))
|
||||
}
|
||||
|
||||
(Ready, Open(TplParam, span)) => {
|
||||
Transition(Meta(span)).ok(Air::MetaStart(span))
|
||||
}
|
||||
(Meta(mspan), Text(lexeme)) => {
|
||||
Transition(Meta(mspan)).ok(Air::MetaLexeme(lexeme))
|
||||
}
|
||||
(Ready | Meta(_), Close(TplParam, span)) => {
|
||||
Transition(Ready).ok(Air::MetaEnd(span))
|
||||
}
|
||||
// Some of these will be permitted in the future.
|
||||
(
|
||||
Meta(mspan),
|
||||
tok @ (Open(..) | Close(..) | Ref(..) | RefSubject(..)
|
||||
| Desc(..)),
|
||||
) => Transition(Meta(mspan))
|
||||
.err(NirToAirError::UnexpectedMetaToken(mspan, tok)),
|
||||
|
||||
(Ready, Text(text)) => Transition(Ready).ok(Air::DocText(text)),
|
||||
|
||||
(
|
||||
Ready,
|
||||
Nir::Close(
|
||||
NirEntity::Rate
|
||||
| NirEntity::Sum
|
||||
| NirEntity::Product
|
||||
| NirEntity::Ceil
|
||||
| NirEntity::Floor
|
||||
| NirEntity::Classify
|
||||
| NirEntity::All
|
||||
| NirEntity::Any,
|
||||
Close(
|
||||
Rate | Sum | Product | Ceil | Floor | Classify | All | Any,
|
||||
span,
|
||||
),
|
||||
) => Transition(Ready).ok(Air::ExprClose(span)),
|
||||
) => Transition(Ready).ok(Air::ExprEnd(span)),
|
||||
|
||||
(Ready, Nir::BindIdent(spair)) => {
|
||||
Transition(Ready).ok(Air::BindIdent(spair))
|
||||
(st @ (Ready | Meta(_)), BindIdent(spair)) => {
|
||||
Transition(st).ok(Air::BindIdent(spair))
|
||||
}
|
||||
(Ready, Ref(spair) | RefSubject(spair)) => {
|
||||
Transition(Ready).ok(Air::RefIdent(spair))
|
||||
}
|
||||
|
||||
(
|
||||
Ready,
|
||||
Nir::Todo
|
||||
| Nir::TodoAttr(..)
|
||||
| Nir::Ref(..)
|
||||
| Nir::Desc(..)
|
||||
| Nir::Text(_)
|
||||
| Nir::Open(NirEntity::TplParam, _)
|
||||
| Nir::Close(NirEntity::TplParam, _),
|
||||
) => Transition(Ready).ok(Air::Todo(UNKNOWN_SPAN)),
|
||||
(Ready, Desc(clause)) => {
|
||||
Transition(Ready).ok(Air::DocIndepClause(clause))
|
||||
}
|
||||
|
||||
(Ready, Import(namespec)) => {
|
||||
Transition(Ready).ok(Air::PkgImport(namespec))
|
||||
}
|
||||
|
||||
// Shouldn't happen in practice because nir::parse will not
|
||||
// produce this.
|
||||
// This assumption is only valid so long as that's the only
|
||||
// thing producing NIR.
|
||||
(st @ Meta(..), tok @ Import(_)) => 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}",
|
||||
)
|
||||
}
|
||||
|
||||
(st, Noop(_)) => Transition(st).incomplete(),
|
||||
}
|
||||
}
|
||||
|
||||
fn is_accepting(&self, _: &Self::Context) -> bool {
|
||||
true
|
||||
fn is_accepting(&self, stack: &Self::Context) -> bool {
|
||||
matches!(self, Self::Ready) && stack.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub enum NirToAirError {
|
||||
Todo,
|
||||
/// Expected a match subject,
|
||||
/// but encountered some other token.
|
||||
///
|
||||
/// TODO: "match subject" is not familiar terminology to the user;
|
||||
/// we'll want to introduce a layer permitting XML-specific augmenting
|
||||
/// with `@on` when derived from an XML source.
|
||||
MatchSubjectExpected(Span, Nir),
|
||||
|
||||
/// Match body is not yet supported.
|
||||
TodoMatchBody(Span, Span),
|
||||
|
||||
/// The provided [`Nir`] token of input was unexpected for the body of a
|
||||
/// metavariable that was opened at the provided [`Span`].
|
||||
UnexpectedMetaToken(Span, Nir),
|
||||
}
|
||||
|
||||
impl Display for NirToAirError {
|
||||
|
@ -149,19 +314,57 @@ impl Display for NirToAirError {
|
|||
use NirToAirError::*;
|
||||
|
||||
match self {
|
||||
Todo => write!(f, "TODO"),
|
||||
MatchSubjectExpected(_, nir) => {
|
||||
write!(f, "expected match subject, found {nir}")
|
||||
}
|
||||
|
||||
TodoMatchBody(_, _) => {
|
||||
write!(f, "match body is not yet supported by TAMER")
|
||||
}
|
||||
|
||||
UnexpectedMetaToken(_, tok) => {
|
||||
write!(
|
||||
f,
|
||||
"expected lexical token for metavariable, found {tok}"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Error for NirToAirError {}
|
||||
|
||||
// TODO: We need to be able to augment with useful context,
|
||||
// e.g. XML suggestions.
|
||||
impl Diagnostic for NirToAirError {
|
||||
fn describe(&self) -> Vec<crate::diagnose::AnnotatedSpan> {
|
||||
// TODO
|
||||
vec![]
|
||||
use NirToAirError::*;
|
||||
|
||||
match self {
|
||||
MatchSubjectExpected(ospan, given) => vec![
|
||||
ospan.note("for this match"),
|
||||
given
|
||||
.span()
|
||||
.error("comparison value provided before subject"),
|
||||
],
|
||||
|
||||
TodoMatchBody(ospan, given) => vec![
|
||||
ospan.note("for this match"),
|
||||
given.error(
|
||||
"tokens in match body are not yet supported by TAMER",
|
||||
),
|
||||
],
|
||||
|
||||
// The user should have been preempted by the parent parser
|
||||
// (e.g. XML->Nir),
|
||||
// and so shouldn't see this.
|
||||
UnexpectedMetaToken(mspan, given) => vec![
|
||||
mspan.note("while parsing the body of this metavariable"),
|
||||
given.span().error("expected a lexical token here"),
|
||||
],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(test, feature = "wip-nir-to-air"))]
|
||||
#[cfg(all(test, feature = "wip-asg-derived-xmli"))]
|
||||
mod test;
|
||||
|
|
|
@ -22,17 +22,17 @@ use crate::{parse::util::SPair, span::dummy::*};
|
|||
|
||||
type Sut = NirToAir;
|
||||
|
||||
use Parsed::Object as O;
|
||||
use Parsed::{Incomplete, Object as O};
|
||||
|
||||
#[test]
|
||||
fn package_to_pkg() {
|
||||
let toks = vec![
|
||||
Nir::Open(NirEntity::Package, S1),
|
||||
Nir::Close(NirEntity::Package, S2),
|
||||
];
|
||||
let toks = vec![Open(Package, S1), Close(Package, S2)];
|
||||
|
||||
assert_eq!(
|
||||
Ok(vec![O(Air::PkgOpen(S1)), O(Air::PkgClose(S2)),]),
|
||||
Ok(vec![
|
||||
O(Air::PkgStart(S1, SPair("/TODO".into(), S1))),
|
||||
O(Air::PkgEnd(S2)),
|
||||
]),
|
||||
Sut::parse(toks.into_iter()).collect(),
|
||||
);
|
||||
}
|
||||
|
@ -41,17 +41,19 @@ fn package_to_pkg() {
|
|||
fn rate_to_sum_expr() {
|
||||
let id = SPair("foo".into(), S2);
|
||||
|
||||
#[rustfmt::skip]
|
||||
let toks = vec![
|
||||
Nir::Open(NirEntity::Rate, S1),
|
||||
Nir::BindIdent(id),
|
||||
Nir::Close(NirEntity::Rate, S3),
|
||||
Open(Rate, S1),
|
||||
BindIdent(id),
|
||||
Close(Rate, S3),
|
||||
];
|
||||
|
||||
assert_eq!(
|
||||
#[rustfmt::skip]
|
||||
Ok(vec![
|
||||
O(Air::ExprOpen(ExprOp::Sum, S1)),
|
||||
O(Air::BindIdent(id)),
|
||||
O(Air::ExprClose(S3)),
|
||||
O(Air::ExprStart(ExprOp::Sum, S1)),
|
||||
O(Air::BindIdent(id)),
|
||||
O(Air::ExprEnd(S3)),
|
||||
]),
|
||||
Sut::parse(toks.into_iter()).collect(),
|
||||
);
|
||||
|
@ -59,19 +61,21 @@ fn rate_to_sum_expr() {
|
|||
|
||||
#[test]
|
||||
fn calc_exprs() {
|
||||
#[rustfmt::skip]
|
||||
let toks = vec![
|
||||
Nir::Open(NirEntity::Sum, S1),
|
||||
Nir::Open(NirEntity::Product, S2),
|
||||
Nir::Close(NirEntity::Product, S3),
|
||||
Nir::Close(NirEntity::Sum, S4),
|
||||
Open(Sum, S1),
|
||||
Open(Product, S2),
|
||||
Close(Product, S3),
|
||||
Close(Sum, S4),
|
||||
];
|
||||
|
||||
assert_eq!(
|
||||
#[rustfmt::skip]
|
||||
Ok(vec![
|
||||
O(Air::ExprOpen(ExprOp::Sum, S1)),
|
||||
O(Air::ExprOpen(ExprOp::Product, S2)),
|
||||
O(Air::ExprClose(S3)),
|
||||
O(Air::ExprClose(S4)),
|
||||
O(Air::ExprStart(ExprOp::Sum, S1)),
|
||||
O(Air::ExprStart(ExprOp::Product, S2)),
|
||||
O(Air::ExprEnd(S3)),
|
||||
O(Air::ExprEnd(S4)),
|
||||
]),
|
||||
Sut::parse(toks.into_iter()).collect(),
|
||||
);
|
||||
|
@ -81,17 +85,19 @@ fn calc_exprs() {
|
|||
fn classify_to_conj_expr() {
|
||||
let id = SPair("always".into(), S2);
|
||||
|
||||
#[rustfmt::skip]
|
||||
let toks = vec![
|
||||
Nir::Open(NirEntity::Classify, S1),
|
||||
Nir::BindIdent(id),
|
||||
Nir::Close(NirEntity::Classify, S3),
|
||||
Open(Classify, S1),
|
||||
BindIdent(id),
|
||||
Close(Classify, S3),
|
||||
];
|
||||
|
||||
assert_eq!(
|
||||
#[rustfmt::skip]
|
||||
Ok(vec![
|
||||
O(Air::ExprOpen(ExprOp::Conj, S1)),
|
||||
O(Air::BindIdent(id)),
|
||||
O(Air::ExprClose(S3)),
|
||||
O(Air::ExprStart(ExprOp::Conj, S1)),
|
||||
O(Air::BindIdent(id)),
|
||||
O(Air::ExprEnd(S3)),
|
||||
]),
|
||||
Sut::parse(toks.into_iter()).collect(),
|
||||
);
|
||||
|
@ -99,19 +105,48 @@ fn classify_to_conj_expr() {
|
|||
|
||||
#[test]
|
||||
fn logic_exprs() {
|
||||
#[rustfmt::skip]
|
||||
let toks = vec![
|
||||
Nir::Open(NirEntity::All, S1),
|
||||
Nir::Open(NirEntity::Any, S2),
|
||||
Nir::Close(NirEntity::Any, S3),
|
||||
Nir::Close(NirEntity::All, S4),
|
||||
Open(All, S1),
|
||||
Open(Any, S2),
|
||||
Close(Any, S3),
|
||||
Close(All, S4),
|
||||
];
|
||||
|
||||
assert_eq!(
|
||||
#[rustfmt::skip]
|
||||
Ok(vec![
|
||||
O(Air::ExprOpen(ExprOp::Conj, S1)),
|
||||
O(Air::ExprOpen(ExprOp::Disj, S2)),
|
||||
O(Air::ExprClose(S3)),
|
||||
O(Air::ExprClose(S4)),
|
||||
O(Air::ExprStart(ExprOp::Conj, S1)),
|
||||
O(Air::ExprStart(ExprOp::Disj, S2)),
|
||||
O(Air::ExprEnd(S3)),
|
||||
O(Air::ExprEnd(S4)),
|
||||
]),
|
||||
Sut::parse(toks.into_iter()).collect(),
|
||||
);
|
||||
}
|
||||
|
||||
// @desc becomes an independent clause,
|
||||
// intended for short summary documentation.
|
||||
#[test]
|
||||
fn desc_as_indep_clause() {
|
||||
let id = SPair("class".into(), S2);
|
||||
let desc = SPair("class desc".into(), S3);
|
||||
|
||||
#[rustfmt::skip]
|
||||
let toks = vec![
|
||||
Open(Classify, S1),
|
||||
BindIdent(id),
|
||||
Desc(desc),
|
||||
Close(Classify, S4),
|
||||
];
|
||||
|
||||
assert_eq!(
|
||||
#[rustfmt::skip]
|
||||
Ok(vec![
|
||||
O(Air::ExprStart(ExprOp::Conj, S1)),
|
||||
O(Air::BindIdent(id)),
|
||||
O(Air::DocIndepClause(desc)),
|
||||
O(Air::ExprEnd(S4)),
|
||||
]),
|
||||
Sut::parse(toks.into_iter()).collect(),
|
||||
);
|
||||
|
@ -121,18 +156,244 @@ fn logic_exprs() {
|
|||
fn tpl_with_name() {
|
||||
let name = SPair("_tpl_name_".into(), S2);
|
||||
|
||||
#[rustfmt::skip]
|
||||
let toks = vec![
|
||||
Nir::Open(NirEntity::Tpl, S1),
|
||||
Nir::BindIdent(name),
|
||||
Nir::Close(NirEntity::Tpl, S3),
|
||||
Open(Tpl, S1),
|
||||
BindIdent(name),
|
||||
Close(Tpl, S3),
|
||||
];
|
||||
|
||||
#[rustfmt::skip]
|
||||
assert_eq!(
|
||||
#[rustfmt::skip]
|
||||
Ok(vec![
|
||||
O(Air::TplOpen(S1)),
|
||||
O(Air::BindIdent(name)),
|
||||
O(Air::TplClose(S3)),
|
||||
O(Air::TplStart(S1)),
|
||||
O(Air::BindIdent(name)),
|
||||
O(Air::TplEnd(S3)),
|
||||
]),
|
||||
Sut::parse(toks.into_iter()).collect(),
|
||||
);
|
||||
}
|
||||
|
||||
// Long form takes the actual underscore-padded template name without any
|
||||
// additional processing.
|
||||
#[test]
|
||||
fn apply_template_long_form_nullary() {
|
||||
let name = SPair("_tpl-name_".into(), S2);
|
||||
|
||||
#[rustfmt::skip]
|
||||
let toks = vec![
|
||||
Open(TplApply, S1),
|
||||
RefSubject(name),
|
||||
Close(TplApply, S3),
|
||||
];
|
||||
|
||||
assert_eq!(
|
||||
#[rustfmt::skip]
|
||||
Ok(vec![
|
||||
O(Air::TplStart(S1)),
|
||||
O(Air::RefIdent(name)),
|
||||
O(Air::TplEndRef(S3)),
|
||||
]),
|
||||
Sut::parse(toks.into_iter()).collect(),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn apply_template_long_form_args() {
|
||||
let name = SPair("_tpl-name_".into(), S2);
|
||||
let p1 = SPair("@p1@".into(), S4);
|
||||
let v1 = SPair("value1".into(), S5);
|
||||
let p2 = SPair("@p2@".into(), S8);
|
||||
let v2 = SPair("value2".into(), S9);
|
||||
|
||||
#[rustfmt::skip]
|
||||
let toks = vec![
|
||||
Open(TplApply, S1),
|
||||
RefSubject(name),
|
||||
|
||||
Open(TplParam, S3),
|
||||
BindIdent(p1),
|
||||
Text(v1),
|
||||
Close(TplParam, S6),
|
||||
|
||||
Open(TplParam, S7),
|
||||
BindIdent(p2),
|
||||
Text(v2),
|
||||
Close(TplParam, S10),
|
||||
Close(TplApply, S11),
|
||||
];
|
||||
|
||||
assert_eq!(
|
||||
#[rustfmt::skip]
|
||||
Ok(vec![
|
||||
O(Air::TplStart(S1)),
|
||||
O(Air::RefIdent(name)),
|
||||
|
||||
O(Air::MetaStart(S3)),
|
||||
O(Air::BindIdent(p1)),
|
||||
O(Air::MetaLexeme(v1)),
|
||||
O(Air::MetaEnd(S6)),
|
||||
|
||||
O(Air::MetaStart(S7)),
|
||||
O(Air::BindIdent(p2)),
|
||||
O(Air::MetaLexeme(v2)),
|
||||
O(Air::MetaEnd(S10)),
|
||||
O(Air::TplEndRef(S11)),
|
||||
]),
|
||||
Sut::parse(toks.into_iter()).collect(),
|
||||
);
|
||||
}
|
||||
|
||||
// Short-hand matches with no value desugar to an equality check
|
||||
// against `TRUE`.
|
||||
#[test]
|
||||
fn match_short_no_value() {
|
||||
let name = SPair("matchOn".into(), S2);
|
||||
|
||||
#[rustfmt::skip]
|
||||
let toks = vec![
|
||||
Open(Match, S1),
|
||||
RefSubject(name), // @on
|
||||
Close(Match, S3),
|
||||
];
|
||||
|
||||
assert_eq!(
|
||||
#[rustfmt::skip]
|
||||
Ok(vec![
|
||||
// When first encountering a `Match`,
|
||||
// we do not know what predicate needs to be emitted for AIR.
|
||||
Incomplete,
|
||||
// Nor do we know when encountering an identifier,
|
||||
// which serves as the first argument to the yet-to-be-known
|
||||
// predicate.
|
||||
Incomplete,
|
||||
// Once closing,
|
||||
// we default to an equality check against `TRUE`.
|
||||
O(Air::ExprStart(ExprOp::Eq, S1)),
|
||||
O(Air::RefIdent(name)),
|
||||
O(Air::RefIdent(SPair(SYM_TRUE, S1))),
|
||||
O(Air::ExprEnd(S3)),
|
||||
]),
|
||||
Sut::parse(toks.into_iter()).collect(),
|
||||
);
|
||||
}
|
||||
|
||||
// Same as above but _not_ defaulted to `TRUE`.
|
||||
#[test]
|
||||
fn match_short_with_value() {
|
||||
let name = SPair("matchOn".into(), S2);
|
||||
let value = SPair("value".into(), S3);
|
||||
|
||||
#[rustfmt::skip]
|
||||
let toks = vec![
|
||||
Open(Match, S1),
|
||||
RefSubject(name), // @on
|
||||
Ref(value), // @value
|
||||
Close(Match, S4),
|
||||
];
|
||||
|
||||
// See notes from `match_short_no_value`,
|
||||
// which are not duplicated here.
|
||||
assert_eq!(
|
||||
#[rustfmt::skip]
|
||||
Ok(vec![
|
||||
Incomplete,
|
||||
Incomplete,
|
||||
O(Air::ExprStart(ExprOp::Eq, S1)),
|
||||
O(Air::RefIdent(name)),
|
||||
// Rather than defaulting to `SYM_TRUE` as above,
|
||||
// we use the _user-provided_ value.
|
||||
O(Air::RefIdent(value)),
|
||||
O(Air::ExprEnd(S4)),
|
||||
]),
|
||||
Sut::parse(toks.into_iter()).collect(),
|
||||
);
|
||||
}
|
||||
|
||||
// Equivalently stated in XML: `match/@value` before `match/@on`;
|
||||
// NIR imposes ordering,
|
||||
// such that the `@on` must come first.
|
||||
#[test]
|
||||
fn match_short_value_before_subject_err() {
|
||||
let name = SPair("matchOn".into(), S2);
|
||||
|
||||
#[rustfmt::skip]
|
||||
let toks = vec![
|
||||
Open(Match, S1),
|
||||
Ref(name), // @value, not @on
|
||||
|
||||
// RECOVERY: Provide the subject
|
||||
RefSubject(name),
|
||||
Close(Match, S3),
|
||||
];
|
||||
|
||||
assert_eq!(
|
||||
#[rustfmt::skip]
|
||||
vec![
|
||||
Ok(Incomplete), // Open
|
||||
// We were expecting RefSubject (@on) but got Ref (@value)
|
||||
Err(ParseError::StateError(
|
||||
NirToAirError::MatchSubjectExpected(S1, Ref(name))
|
||||
)),
|
||||
// RECOVERY: Subject is provided.
|
||||
Ok(Incomplete),
|
||||
// And we then close as an eq match on TRUE,
|
||||
// because no value has been provided
|
||||
// (rather the value was consumed in the error).
|
||||
Ok(O(Air::ExprStart(ExprOp::Eq, S1))),
|
||||
Ok(O(Air::RefIdent(name))),
|
||||
Ok(O(Air::RefIdent(SPair(SYM_TRUE, S1)))),
|
||||
Ok(O(Air::ExprEnd(S3))),
|
||||
],
|
||||
Sut::parse(toks.into_iter()).collect::<Vec<Result<_, _>>>(),
|
||||
);
|
||||
}
|
||||
|
||||
// Closing a match before providing any arguments.
|
||||
#[test]
|
||||
fn match_no_args_err() {
|
||||
#[rustfmt::skip]
|
||||
let toks = vec![
|
||||
Open(Match, S1),
|
||||
// No refs.
|
||||
Close(Match, S2),
|
||||
// RECOVERY: We have no choice but to discard the above match.
|
||||
];
|
||||
|
||||
assert_eq!(
|
||||
#[rustfmt::skip]
|
||||
vec![
|
||||
Ok(Incomplete), // Open
|
||||
// We were expecting RefSubject (@on) but closed instead.
|
||||
Err(ParseError::StateError(
|
||||
NirToAirError::MatchSubjectExpected(S1, Close(Match, S2))
|
||||
)),
|
||||
// RECOVERY: Useless match above discarded.
|
||||
],
|
||||
Sut::parse(toks.into_iter()).collect::<Vec<Result<_, _>>>(),
|
||||
);
|
||||
}
|
||||
|
||||
// Sibling text (e.g. mixed content in XML) becomes arbitrary
|
||||
// documentation,
|
||||
// intended to be written in a literate style.
|
||||
#[test]
|
||||
fn text_as_arbitrary_doc() {
|
||||
let text = SPair("foo bar baz".into(), S2);
|
||||
|
||||
#[rustfmt::skip]
|
||||
let toks = vec![
|
||||
Open(Package, S1),
|
||||
Text(text),
|
||||
Close(Package, S3),
|
||||
];
|
||||
|
||||
assert_eq!(
|
||||
#[rustfmt::skip]
|
||||
Ok(vec![
|
||||
O(Air::PkgStart(S1, SPair("/TODO".into(), S1))),
|
||||
O(Air::DocText(text)),
|
||||
O(Air::PkgEnd(S3)),
|
||||
]),
|
||||
Sut::parse(toks.into_iter()).collect(),
|
||||
);
|
||||
|
|
|
@ -250,6 +250,13 @@ impl ParseState for InterpState {
|
|||
let span = tok.span();
|
||||
|
||||
match self {
|
||||
// TODO: For now we must maintain BC with existing documentation
|
||||
// that was written as LaTeX,
|
||||
// which naturally contains many curly braces.
|
||||
// This can go away once we introduce a proper documentation
|
||||
// language.
|
||||
Ready if matches!(tok, Nir::Text(..)) => Transition(Ready).ok(tok),
|
||||
|
||||
// When receiving a new symbol,
|
||||
// we must make a quick determination as to whether it
|
||||
// requires desugaring.
|
||||
|
|
|
@ -63,6 +63,22 @@ fn does_not_desugar_tokens_without_symbols() {
|
|||
);
|
||||
}
|
||||
|
||||
// We ought to desugar text at some point,
|
||||
// but we need to maintain BC with existing systems.
|
||||
// The literate documentation was intended to be LaTeX back in the day,
|
||||
// rather than a proper abstraction atop of it,
|
||||
// and so there are many curly braces in existing code.
|
||||
#[test]
|
||||
fn does_not_desugar_text() {
|
||||
let spair = SPair("\\footnote{foo}".into(), S1);
|
||||
let toks = vec![Nir::Text(spair)];
|
||||
|
||||
assert_eq!(
|
||||
Ok(vec![Object(Nir::Text(spair))]),
|
||||
Sut::parse(toks.into_iter()).collect(),
|
||||
);
|
||||
}
|
||||
|
||||
fn expect_expanded_header(
|
||||
sut: &mut Parser<InterpState, std::vec::IntoIter<Nir>>,
|
||||
given_val: &str,
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,287 @@
|
|||
// Shorthand template application desugaring 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/>.
|
||||
|
||||
//! Shorthand template application desugaring for NIR.
|
||||
//!
|
||||
//! Desugaring is performed by [`TplShortDesugar`].
|
||||
//!
|
||||
//! A shorthand template application looks something like this,
|
||||
//! in XML form:
|
||||
//!
|
||||
//! ```xml
|
||||
//! <t:foo bar="baz">
|
||||
//! <c:sum />
|
||||
//! </t:foo>
|
||||
//!
|
||||
//! <!-- the above desugars into the below -->
|
||||
//!
|
||||
//! <apply-template name="_foo_">
|
||||
//! <with-param name="@bar@" value="baz" />
|
||||
//! <with-param name="@values@" value="___dsgr-01___" />
|
||||
//! </apply-template>
|
||||
//!
|
||||
//! <template name="___dsgr-01___">
|
||||
//! <c:sum />
|
||||
//! </template>
|
||||
//! ```
|
||||
//!
|
||||
//! The shorthand syntax makes templates look like another language
|
||||
//! primitive,
|
||||
//! with the exception of the namespace prefix.
|
||||
//!
|
||||
//! Note how desugaring also wraps template names in `'_'` and param names
|
||||
//! in `'@'`.
|
||||
//! These naming requirements were intended to eliminate conflicts with
|
||||
//! other types of identifiers and to make it obvious when templates and
|
||||
//! metavariables were being used,
|
||||
//! but it works against the goal of making template applications look
|
||||
//! like language primitives.
|
||||
//! Shorthand form was added well after the long `apply-template` form.
|
||||
//!
|
||||
//! The body of a shorthand template becomes the body of a new template,
|
||||
//! and its id referenced as the lexical value of the param `@values@`.
|
||||
//! (This poor name is a historical artifact.)
|
||||
//! Since the template is closed
|
||||
//! (has no free metavariables),
|
||||
//! it will be expanded on reference,
|
||||
//! inlining its body into the reference site.
|
||||
//! This is a different and generalized approach to the `param-copy`
|
||||
//! behavior of the XLST-based TAME.
|
||||
//!
|
||||
//! This shorthand version does not permit metavariables for template or
|
||||
//! param names,
|
||||
//! so the long form is still a useful language feature for more
|
||||
//! sophisticated cases.
|
||||
//!
|
||||
//! This was originally handled in the XSLT compiler in
|
||||
//! `:src/current/include/preproc/template.xsl`.
|
||||
//! You may need to consult the Git history if this file is no longer
|
||||
//! available or if the XSLT template was since removed.
|
||||
//! The XSLT-based compiler did not produce a separate template for
|
||||
//! `@values@`.
|
||||
|
||||
use arrayvec::ArrayVec;
|
||||
|
||||
use super::{Nir, NirEntity};
|
||||
use crate::{
|
||||
fmt::{DisplayWrapper, TtQuote},
|
||||
parse::prelude::*,
|
||||
span::Span,
|
||||
sym::{
|
||||
st::raw::L_TPLP_VALUES, GlobalSymbolIntern, GlobalSymbolResolve,
|
||||
SymbolId,
|
||||
},
|
||||
};
|
||||
use std::convert::Infallible;
|
||||
|
||||
use Nir::*;
|
||||
use NirEntity::*;
|
||||
|
||||
/// Desugar shorthand template applications into long-form template
|
||||
/// applications.
|
||||
///
|
||||
/// This parser is able to handle nested applications without having to
|
||||
/// track nesting by taking advantage of the parsing that NIR has already
|
||||
/// performed.
|
||||
///
|
||||
/// See the [module-level documentation](super) for more information.
|
||||
#[derive(Debug, PartialEq, Eq, Default)]
|
||||
pub enum TplShortDesugar {
|
||||
/// Waiting for shorthand template application,
|
||||
/// passing tokens along in the meantime.
|
||||
///
|
||||
/// This state is also used when parsing the body of a shorthand
|
||||
/// template application.
|
||||
#[default]
|
||||
Scanning,
|
||||
|
||||
/// A shorthand template application associated with the provided
|
||||
/// [`SPair`] was encountered and shorthand params are being
|
||||
/// desugared.
|
||||
DesugaringParams(SPair),
|
||||
}
|
||||
|
||||
impl Display for TplShortDesugar {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Scanning => {
|
||||
write!(f, "awaiting shorthand template application")
|
||||
}
|
||||
Self::DesugaringParams(_) => {
|
||||
write!(f, "desugaring shorthand template application params")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ParseState for TplShortDesugar {
|
||||
type Token = Nir;
|
||||
type Object = Nir;
|
||||
type Error = Infallible;
|
||||
type Context = Stack;
|
||||
|
||||
fn parse_token(
|
||||
self,
|
||||
tok: Self::Token,
|
||||
stack: &mut Self::Context,
|
||||
) -> TransitionResult<Self::Super> {
|
||||
use TplShortDesugar::*;
|
||||
|
||||
if let Some(obj) = stack.pop() {
|
||||
return Transition(self).ok(obj).with_lookahead(tok);
|
||||
}
|
||||
|
||||
match (self, tok) {
|
||||
// Shorthand template applications are identified by a `Some`
|
||||
// QName in the `TplApply` entity.
|
||||
//
|
||||
// The name of the template _without_ the underscore padding is
|
||||
// the local part of the QName.
|
||||
(Scanning, Open(TplApplyShort(qname), span)) => {
|
||||
// TODO: Determine whether caching these has any notable
|
||||
// benefit over repeated heap allocations,
|
||||
// comparing packages with very few applications and
|
||||
// packages with thousands
|
||||
// (we'd still have to hit the heap for the cache).
|
||||
let tpl_name =
|
||||
format!("_{}_", qname.local_name().lookup_str()).intern();
|
||||
|
||||
let name = SPair(tpl_name, span);
|
||||
stack.push(Ref(name));
|
||||
|
||||
Transition(DesugaringParams(name)).ok(Open(TplApply, span))
|
||||
}
|
||||
|
||||
// Shorthand template params' names do not contain the
|
||||
// surrounding `@`s.
|
||||
(DesugaringParams(ospan), Open(TplParamShort(name, val), span)) => {
|
||||
let pname = format!("@{name}@").intern();
|
||||
|
||||
// note: reversed (stack)
|
||||
stack.push(Close(TplParam, span));
|
||||
stack.push(Text(val));
|
||||
stack.push(BindIdent(SPair(pname, name.span())));
|
||||
Transition(DesugaringParams(ospan)).ok(Open(TplParam, span))
|
||||
}
|
||||
|
||||
// A child element while we're desugaring template params
|
||||
// means that we have reached the body,
|
||||
// which is to desugar into `@values@`.
|
||||
// We generate a name for a new template,
|
||||
// set `@values@` to the name of the template,
|
||||
// close our active template application,
|
||||
// and then place the body into that template.
|
||||
//
|
||||
// TODO: This does not handle nested template applications.
|
||||
(DesugaringParams(name), tok @ Open(..)) => {
|
||||
let ospan = name.span();
|
||||
let gen_name = gen_tpl_name_at_offset(ospan);
|
||||
let gen_desc = values_tpl_desc(name);
|
||||
|
||||
// The spans are awkward here because we are streaming,
|
||||
// and so don't have much choice but to use the opening
|
||||
// span for everything.
|
||||
// If this ends up being unhelpful for diagnostics,
|
||||
// we can have AIR do some adjustment through some
|
||||
// yet-to-be-defined means.
|
||||
//
|
||||
// note: reversed (stack)
|
||||
stack.push(Desc(gen_desc));
|
||||
stack.push(BindIdent(SPair(gen_name, ospan)));
|
||||
stack.push(Open(Tpl, ospan));
|
||||
|
||||
// Application ends here,
|
||||
// and the new template (above) will absorb both this
|
||||
// token `tok` and all tokens that come after.
|
||||
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)));
|
||||
|
||||
// Note that we must have `tok` as lookahead instead of
|
||||
// pushing directly on the stack in case it's a
|
||||
// `TplApplyShort`.
|
||||
Transition(Scanning)
|
||||
.ok(Open(TplParam, ospan))
|
||||
.with_lookahead(tok)
|
||||
}
|
||||
|
||||
// If we're scanning and find a closing shorthand application,
|
||||
// then we must have just finished with a shorthand body.
|
||||
(Scanning, Close(TplApplyShort(_), span)) => {
|
||||
Transition(Scanning).ok(Close(Tpl, span))
|
||||
}
|
||||
|
||||
// If we complete the shorthand template during param parsing,
|
||||
// then we have no body and must close the application.
|
||||
(DesugaringParams(_), Close(TplApplyShort(_), span)) => {
|
||||
Transition(Scanning).ok(Close(TplApply, span))
|
||||
}
|
||||
|
||||
// Any tokens that we don't recognize will be passed on unchanged.
|
||||
(st, tok) => Transition(st).ok(tok),
|
||||
}
|
||||
}
|
||||
|
||||
fn is_accepting(&self, stack: &Self::Context) -> bool {
|
||||
matches!(self, Self::Scanning) && stack.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
type Stack = ArrayVec<Nir, 7>;
|
||||
|
||||
/// Generate a deterministic template identifier name that is unique
|
||||
/// relative to the offset in the source context (file) of the given
|
||||
/// [`Span`].
|
||||
///
|
||||
/// Hygiene is not a concern since identifiers cannot be redeclared,
|
||||
/// so conflicts with manually-created identifiers will result in a
|
||||
/// compilation error
|
||||
/// (albeit a cryptic one);
|
||||
/// the hope is that the informally-compiler-reserved `___` convention
|
||||
/// mitigates that unlikely occurrence.
|
||||
/// Consequently,
|
||||
/// we _must_ intern to ensure that error can occur
|
||||
/// (we cannot use [`GlobalSymbolIntern::clone_uninterned`]).
|
||||
#[inline]
|
||||
fn gen_tpl_name_at_offset(span: Span) -> SymbolId {
|
||||
format!("___dsgr-{:x}___", span.offset()).intern()
|
||||
}
|
||||
|
||||
/// Generate a description for the template generated from the body of a
|
||||
/// shorthand template application.
|
||||
///
|
||||
/// Descriptions are required on templates,
|
||||
/// but we should also provide something that is useful in debugging.
|
||||
/// The description will contain the name of the template being applied and
|
||||
/// will share the same span as the provided [`SPair`] `applying`'s,
|
||||
/// having been derived from it.
|
||||
fn values_tpl_desc(applying: SPair) -> SPair {
|
||||
SPair(
|
||||
format!(
|
||||
"Desugared body of shorthand template application of {}",
|
||||
TtQuote::wrap(applying.symbol())
|
||||
)
|
||||
.intern(),
|
||||
applying.span(),
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test;
|
|
@ -0,0 +1,254 @@
|
|||
// Tests shorthand template application desugaring 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::{convert::ExpectInto, span::dummy::*};
|
||||
|
||||
use Parsed::Object as O;
|
||||
|
||||
type Sut = TplShortDesugar;
|
||||
|
||||
#[test]
|
||||
fn desugars_nullary() {
|
||||
// Shorthand converts `t:tpl-name` into `_tpl-name_`.
|
||||
let qname = ("t", "tpl-name").unwrap_into();
|
||||
let tpl = "_tpl-name_".into();
|
||||
|
||||
let toks = [
|
||||
Open(TplApplyShort(qname), S1),
|
||||
Close(TplApplyShort(qname), S2),
|
||||
];
|
||||
|
||||
#[rustfmt::skip]
|
||||
assert_eq!(
|
||||
Ok(vec![
|
||||
O(Open(TplApply, S1)),
|
||||
// The span associated with the name of the template to be
|
||||
// applied is the span of the entire QName from NIR.
|
||||
// The reason for this is that `t:foo` is transformed into
|
||||
// `_foo_`,
|
||||
// and so the `t:` is a necessary part of the
|
||||
// representation of the name of the template;
|
||||
// `foo` is not in itself a valid template name at the
|
||||
// time of writing.
|
||||
O(Ref(SPair(tpl, S1))),
|
||||
O(Close(TplApply, S2)),
|
||||
]),
|
||||
Sut::parse(toks.into_iter()).collect(),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn desugars_unary() {
|
||||
// Shorthand converts `t:tpl-name` into `_tpl-name_`.
|
||||
let qname = ("t", "tpl-name").unwrap_into();
|
||||
let name = SPair("_tpl-name_".into(), S1);
|
||||
|
||||
let aname = SPair("foo".into(), S3);
|
||||
let pval = SPair("foo value".into(), S4);
|
||||
|
||||
// The attribute name gets padded with '@',
|
||||
// much like the template does with underscores.
|
||||
let pname = SPair("@foo@".into(), S3);
|
||||
|
||||
#[rustfmt::skip]
|
||||
let toks = vec![
|
||||
// <t:qname
|
||||
Open(TplApplyShort(qname), S1),
|
||||
// foo="foo value"
|
||||
Open(TplParamShort(aname, pval), S2),
|
||||
// Implicit close.
|
||||
// />
|
||||
Close(TplApplyShort(qname), S6),
|
||||
];
|
||||
|
||||
#[rustfmt::skip]
|
||||
assert_eq!(
|
||||
Ok(vec![
|
||||
O(Open(TplApply, S1)),
|
||||
O(Ref(name)),
|
||||
|
||||
O(Open(TplParam, S2)),
|
||||
// Derived from `aname` (by padding)
|
||||
O(BindIdent(pname)),
|
||||
// The value is left untouched.
|
||||
O(Text(pval)),
|
||||
// Close is derived from open.
|
||||
O(Close(TplParam, S2)),
|
||||
O(Close(TplApply, S6)),
|
||||
]),
|
||||
Sut::parse(toks.into_iter()).collect(),
|
||||
);
|
||||
}
|
||||
|
||||
// Body of shorthand is desugared into `@values@` param.
|
||||
#[test]
|
||||
fn desugars_body_into_tpl_with_ref_in_values_param() {
|
||||
// Shorthand converts `t:tpl-name` into `_tpl-name_`.
|
||||
let qname = ("t", "short").unwrap_into();
|
||||
let name = SPair("_short_".into(), S1);
|
||||
|
||||
#[rustfmt::skip]
|
||||
let toks = vec![
|
||||
// <t:qname>
|
||||
Open(TplApplyShort(qname), S1),
|
||||
// Body to desugar into own template (@values@).
|
||||
Open(Sum, S2),
|
||||
Open(Product, S3),
|
||||
Close(Product, S4),
|
||||
Close(Sum, S5),
|
||||
|
||||
// Body can contain siblings.
|
||||
Open(Product, S6),
|
||||
Close(Product, S7),
|
||||
// </t:qname>
|
||||
Close(TplApplyShort(qname), S8),
|
||||
];
|
||||
|
||||
// The name of the generated template.
|
||||
// This test is a bit too friendly with implementation details,
|
||||
// but it does allow us to be perfectly precise in the output
|
||||
// assertion.
|
||||
let gen_name = gen_tpl_name_at_offset(S1);
|
||||
let gen_desc = values_tpl_desc(name);
|
||||
|
||||
#[rustfmt::skip]
|
||||
assert_eq!(
|
||||
Ok(vec![
|
||||
O(Open(TplApply, S1)),
|
||||
O(Ref(name)),
|
||||
|
||||
// @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(Text(SPair(gen_name, S1))), //:-.
|
||||
O(Close(TplParam, S1)), // |
|
||||
O(Close(TplApply, S1)), // |
|
||||
// |
|
||||
// Generate a template to hold the // |
|
||||
// body of `@values@`. // |
|
||||
// It is closed and so expandable. // |
|
||||
O(Open(Tpl, S1)), // /
|
||||
O(BindIdent(SPair(gen_name, S1))), //<`
|
||||
O(Desc(gen_desc)),
|
||||
|
||||
// And here we have the body of the above
|
||||
// shorthand application.
|
||||
O(Open(Sum, S2)),
|
||||
O(Open(Product, S3)),
|
||||
O(Close(Product, S4)),
|
||||
O(Close(Sum, S5)),
|
||||
|
||||
O(Open(Product, S6)),
|
||||
O(Close(Product, S7)),
|
||||
O(Close(Tpl, S8)),
|
||||
]),
|
||||
Sut::parse(toks.into_iter()).collect(),
|
||||
);
|
||||
}
|
||||
|
||||
// Shorthand within a shorthand.
|
||||
#[test]
|
||||
fn desugar_nested_apply() {
|
||||
let qname_outer = ("t", "outer").unwrap_into();
|
||||
let name_outer = SPair("_outer_".into(), S1);
|
||||
|
||||
let qname_inner = ("t", "inner").unwrap_into();
|
||||
let name_inner = SPair("_inner_".into(), S2);
|
||||
|
||||
#[rustfmt::skip]
|
||||
let toks = vec![
|
||||
Open(TplApplyShort(qname_outer), S1),
|
||||
// Body is a second shorthand template application.
|
||||
Open(TplApplyShort(qname_inner), S2),
|
||||
Close(TplApplyShort(qname_inner), S3),
|
||||
Close(TplApplyShort(qname_outer), S4),
|
||||
];
|
||||
|
||||
let gen_name_outer = gen_tpl_name_at_offset(S1);
|
||||
let gen_desc = values_tpl_desc(name_outer);
|
||||
|
||||
#[rustfmt::skip]
|
||||
assert_eq!(
|
||||
Ok(vec![
|
||||
O(Open(TplApply, S1)),
|
||||
O(Ref(name_outer)),
|
||||
|
||||
// @values@
|
||||
O(Open(TplParam, S1)),
|
||||
O(BindIdent(SPair(L_TPLP_VALUES, S1))),
|
||||
O(Text(SPair(gen_name_outer, S1))), //:-.
|
||||
O(Close(TplParam, S1)), // |
|
||||
O(Close(TplApply, S1)), // |
|
||||
// |
|
||||
O(Open(Tpl, S1)), // /
|
||||
O(BindIdent(SPair(gen_name_outer, S1))), //<`
|
||||
O(Desc(gen_desc)),
|
||||
|
||||
// And within this template,
|
||||
// we generate another application.
|
||||
// This one has no body and so no `@values@`.
|
||||
O(Open(TplApply, S2)),
|
||||
O(Ref(name_inner)),
|
||||
O(Close(TplApply, S3)),
|
||||
O(Close(Tpl, S4)),
|
||||
]),
|
||||
Sut::parse(toks.into_iter()).collect(),
|
||||
);
|
||||
}
|
||||
|
||||
// Don't parse what we desugar into!
|
||||
#[test]
|
||||
fn does_not_desugar_long_form() {
|
||||
let name = SPair("_tpl-name_".into(), S2);
|
||||
let pname = SPair("@param@".into(), S4);
|
||||
let pval = SPair("value".into(), S5);
|
||||
|
||||
#[rustfmt::skip]
|
||||
let toks = [
|
||||
Open(TplApply, S1),
|
||||
BindIdent(name),
|
||||
|
||||
Open(TplParam, S3),
|
||||
BindIdent(pname),
|
||||
Text(pval),
|
||||
Close(TplParam, S6),
|
||||
Close(TplApply, S7),
|
||||
];
|
||||
|
||||
#[rustfmt::skip]
|
||||
assert_eq!(
|
||||
// We avoid #[derive(Clone)] on Nir so that we have confidence that
|
||||
// we're not doing anything suspicious with tokens.
|
||||
// So this is a duplicate of the above,
|
||||
// mapped over `O`.
|
||||
Ok(vec![
|
||||
O(Open(TplApply, S1)),
|
||||
O(BindIdent(name)),
|
||||
|
||||
O(Open(TplParam, S3)),
|
||||
O(BindIdent(pname)),
|
||||
O(Text(pval)),
|
||||
O(Close(TplParam, S6)),
|
||||
O(Close(TplApply, S7)),
|
||||
]),
|
||||
Sut::parse(toks.into_iter()).collect(),
|
||||
);
|
||||
}
|
|
@ -35,7 +35,7 @@ use crate::{
|
|||
sym::SymbolId,
|
||||
};
|
||||
|
||||
use super::XmloToken;
|
||||
use super::XmloToken::{self, *};
|
||||
|
||||
/// Persistent `xmlo` lowering context to be shared among all `xmlo` files.
|
||||
///
|
||||
|
@ -92,6 +92,7 @@ type PackageSPair = SPair;
|
|||
pub enum XmloToAir {
|
||||
#[default]
|
||||
PackageExpected,
|
||||
PackageFound(Span),
|
||||
Package(PackageSPair),
|
||||
SymDep(PackageSPair, SPair),
|
||||
/// End of header (EOH) reached.
|
||||
|
@ -113,15 +114,21 @@ impl ParseState for XmloToAir {
|
|||
use XmloToAir::*;
|
||||
|
||||
match (self, tok) {
|
||||
(PackageExpected, XmloToken::PkgName(name)) => {
|
||||
(PackageExpected, PkgStart(span)) => {
|
||||
Transition(PackageFound(span)).incomplete()
|
||||
}
|
||||
|
||||
(PackageExpected, tok) => Transition(PackageExpected).dead(tok),
|
||||
|
||||
(PackageFound(span), PkgName(name)) => {
|
||||
if ctx.is_first() {
|
||||
ctx.prog_name = Some(name.symbol());
|
||||
}
|
||||
|
||||
Transition(Package(name)).incomplete()
|
||||
Transition(Package(name)).ok(Air::PkgStart(span, name))
|
||||
}
|
||||
|
||||
(st @ Package(..), XmloToken::PkgRootPath(relroot)) => {
|
||||
(st @ Package(..), PkgRootPath(relroot)) => {
|
||||
if ctx.is_first() {
|
||||
ctx.relroot = Some(relroot.symbol());
|
||||
}
|
||||
|
@ -135,7 +142,7 @@ impl ParseState for XmloToAir {
|
|||
// definition is encountered later within the same file.
|
||||
// TODO: Let's remove the need for this special root handling
|
||||
// here.
|
||||
(Package(name), XmloToken::PkgEligClassYields(pkg_elig)) => {
|
||||
(Package(name), PkgEligClassYields(pkg_elig)) => {
|
||||
// The span for this is a bit awkward,
|
||||
// given that rooting is automatic,
|
||||
// but it it should never have to be utilized in
|
||||
|
@ -143,27 +150,23 @@ impl ParseState for XmloToAir {
|
|||
Transition(Package(name)).ok(Air::IdentRoot(pkg_elig))
|
||||
}
|
||||
|
||||
(
|
||||
st @ (PackageExpected | Package(..)),
|
||||
XmloToken::PkgProgramFlag(_),
|
||||
) => {
|
||||
(st @ (PackageFound(..) | Package(..)), PkgProgramFlag(_)) => {
|
||||
// TODO: Unused
|
||||
Transition(st).incomplete()
|
||||
}
|
||||
|
||||
(
|
||||
Package(pkg_name) | SymDep(pkg_name, ..),
|
||||
XmloToken::SymDepStart(sym),
|
||||
) => Transition(SymDep(pkg_name, sym)).incomplete(),
|
||||
(Package(pkg_name) | SymDep(pkg_name, ..), SymDepStart(sym)) => {
|
||||
Transition(SymDep(pkg_name, sym)).incomplete()
|
||||
}
|
||||
|
||||
(SymDep(pkg_name, sym), XmloToken::Symbol(dep_sym)) => {
|
||||
(SymDep(pkg_name, sym), Symbol(dep_sym)) => {
|
||||
Transition(SymDep(pkg_name, sym))
|
||||
.ok(Air::IdentDep(sym, dep_sym))
|
||||
}
|
||||
|
||||
(
|
||||
Package(pkg_name),
|
||||
XmloToken::SymDecl(
|
||||
SymDecl(
|
||||
_name,
|
||||
SymAttrs {
|
||||
src: Some(sym_src), ..
|
||||
|
@ -174,7 +177,7 @@ impl ParseState for XmloToAir {
|
|||
Transition(Package(pkg_name)).incomplete()
|
||||
}
|
||||
|
||||
(Package(pkg_name), XmloToken::SymDecl(name, attrs)) => {
|
||||
(Package(pkg_name), SymDecl(name, attrs)) => {
|
||||
let extern_ = attrs.extern_;
|
||||
|
||||
// TODO: This attr/source separation is a mess,
|
||||
|
@ -209,16 +212,13 @@ impl ParseState for XmloToAir {
|
|||
.transition(Package(pkg_name))
|
||||
}
|
||||
|
||||
(
|
||||
Package(pkg_name) | SymDep(pkg_name, _),
|
||||
XmloToken::Fragment(name, text),
|
||||
) => {
|
||||
(Package(pkg_name) | SymDep(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(..), XmloToken::Eoh(span)) => {
|
||||
(Package(..) | SymDep(..), Eoh(span)) => {
|
||||
// It's important to set this _after_ we're done processing,
|
||||
// otherwise our `first` checks above will be inaccurate.
|
||||
ctx.first = false;
|
||||
|
@ -226,10 +226,17 @@ impl ParseState for XmloToAir {
|
|||
// Note that this uses `incomplete` because we have nothing
|
||||
// to yield,
|
||||
// but we are in fact done.
|
||||
Transition(Done(span)).incomplete()
|
||||
Transition(Done(span)).ok(Air::PkgEnd(span))
|
||||
}
|
||||
|
||||
(st, tok) => Transition(st).dead(tok),
|
||||
(
|
||||
st @ Package(..),
|
||||
tok @ (PkgStart(..) | PkgName(..) | Symbol(..)),
|
||||
) => Transition(st).dead(tok),
|
||||
|
||||
(st @ (PackageFound(..) | SymDep(..) | Done(..)), tok) => {
|
||||
Transition(st).dead(tok)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -244,6 +251,7 @@ impl Display for XmloToAir {
|
|||
|
||||
match self {
|
||||
PackageExpected => write!(f, "expecting package definition"),
|
||||
PackageFound(_) => write!(f, "package found, awaiting definition"),
|
||||
Package(name) => {
|
||||
write!(f, "expecting package `/{name}` declarations")
|
||||
}
|
||||
|
@ -391,526 +399,4 @@ impl Error for XmloAirError {
|
|||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use crate::{
|
||||
asg::{FragmentText, IdentKind},
|
||||
num::{Dim, Dtype},
|
||||
obj::xmlo::{SymAttrs, SymType},
|
||||
parse::Parsed,
|
||||
span::dummy::*,
|
||||
sym::GlobalSymbolIntern,
|
||||
};
|
||||
|
||||
type Sut = XmloToAir;
|
||||
|
||||
#[test]
|
||||
fn data_from_package_event() {
|
||||
let name = "name".into();
|
||||
let relroot = "some/path".into();
|
||||
|
||||
let toks = vec![
|
||||
XmloToken::PkgName(SPair(name, S1)),
|
||||
XmloToken::PkgRootPath(SPair(relroot, S2)),
|
||||
XmloToken::Eoh(S3),
|
||||
]
|
||||
.into_iter();
|
||||
|
||||
let mut sut = Sut::parse(toks);
|
||||
|
||||
assert_eq!(Some(Ok(Parsed::Incomplete)), sut.next()); // PkgName
|
||||
assert_eq!(Some(Ok(Parsed::Incomplete)), sut.next()); // PkgRootPath
|
||||
assert_eq!(Some(Ok(Parsed::Incomplete)), sut.next()); // Eoh
|
||||
|
||||
let ctx = sut.finalize().unwrap().into_context();
|
||||
|
||||
assert_eq!(Some(name), ctx.prog_name);
|
||||
assert_eq!(Some(relroot), ctx.relroot);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn adds_elig_as_root() {
|
||||
let name = "name-root".into();
|
||||
let elig_sym = "elig".into();
|
||||
|
||||
let toks = vec![
|
||||
XmloToken::PkgName(SPair(name, S1)),
|
||||
XmloToken::PkgEligClassYields(SPair(elig_sym, S2)),
|
||||
XmloToken::Eoh(S3),
|
||||
];
|
||||
|
||||
assert_eq!(
|
||||
Ok(vec![
|
||||
Parsed::Incomplete, // PkgName
|
||||
Parsed::Object(Air::IdentRoot(SPair(elig_sym, S2))),
|
||||
Parsed::Incomplete, // Eoh
|
||||
]),
|
||||
Sut::parse(toks.into_iter()).collect(),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn adds_sym_deps() {
|
||||
let sym_from = "from".into();
|
||||
let sym_to1 = "to1".into();
|
||||
let sym_to2 = "to2".into();
|
||||
|
||||
let toks = vec![
|
||||
XmloToken::PkgName(SPair("name".into(), S1)),
|
||||
XmloToken::SymDepStart(SPair(sym_from, S2)),
|
||||
XmloToken::Symbol(SPair(sym_to1, S3)),
|
||||
XmloToken::Symbol(SPair(sym_to2, S4)),
|
||||
XmloToken::Eoh(S1),
|
||||
];
|
||||
|
||||
assert_eq!(
|
||||
Ok(vec![
|
||||
Parsed::Incomplete, // PkgName
|
||||
Parsed::Incomplete, // SymDepStart
|
||||
Parsed::Object(Air::IdentDep(
|
||||
SPair(sym_from, S2),
|
||||
SPair(sym_to1, S3)
|
||||
)),
|
||||
Parsed::Object(Air::IdentDep(
|
||||
SPair(sym_from, S2),
|
||||
SPair(sym_to2, S4)
|
||||
)),
|
||||
Parsed::Incomplete, // Eoh
|
||||
]),
|
||||
Sut::parse(toks.into_iter()).collect(),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sym_decl_with_src_not_added_and_populates_found() {
|
||||
let sym = "sym".into();
|
||||
let src_a = "src_a".into();
|
||||
let src_b = "src_b".into();
|
||||
|
||||
let toks = vec![
|
||||
XmloToken::PkgName(SPair("name".into(), S1)),
|
||||
XmloToken::SymDecl(
|
||||
SPair(sym, S2),
|
||||
SymAttrs {
|
||||
src: Some(src_a),
|
||||
..Default::default()
|
||||
},
|
||||
),
|
||||
XmloToken::SymDecl(
|
||||
SPair(sym, S3),
|
||||
SymAttrs {
|
||||
src: Some(src_b),
|
||||
..Default::default()
|
||||
},
|
||||
),
|
||||
XmloToken::Eoh(S1),
|
||||
];
|
||||
|
||||
let mut sut = Sut::parse(toks.into_iter());
|
||||
|
||||
assert_eq!(Some(Ok(Parsed::Incomplete)), sut.next()); // PkgName
|
||||
assert_eq!(Some(Ok(Parsed::Incomplete)), sut.next()); // SymDecl (@src)
|
||||
assert_eq!(Some(Ok(Parsed::Incomplete)), sut.next()); // SymDecl (@src)
|
||||
assert_eq!(Some(Ok(Parsed::Incomplete)), sut.next()); // Eoh
|
||||
|
||||
let ctx = sut.finalize().unwrap().into_context();
|
||||
let mut founds = ctx.found.unwrap().into_iter().collect::<Vec<_>>();
|
||||
|
||||
// Just to remove nondeterminism in case the iteration order happens
|
||||
// to change.
|
||||
founds.sort();
|
||||
|
||||
assert_eq!(vec![src_a, src_b], founds);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sym_decl_added_to_graph() {
|
||||
let sym_extern = "sym_extern".into();
|
||||
let sym_non_extern = "sym_non_extern".into();
|
||||
let sym_map = "sym_map".into();
|
||||
let sym_retmap = "sym_retmap".into();
|
||||
let pkg_name = "pkg name".into();
|
||||
|
||||
let toks = vec![
|
||||
XmloToken::PkgName(SPair("name".into(), S1)),
|
||||
XmloToken::SymDecl(
|
||||
SPair(sym_extern, S1),
|
||||
SymAttrs {
|
||||
pkg_name: Some(pkg_name),
|
||||
extern_: true,
|
||||
ty: Some(SymType::Meta),
|
||||
..Default::default()
|
||||
},
|
||||
),
|
||||
XmloToken::SymDecl(
|
||||
SPair(sym_non_extern, S2),
|
||||
SymAttrs {
|
||||
pkg_name: Some(pkg_name),
|
||||
ty: Some(SymType::Meta),
|
||||
..Default::default()
|
||||
},
|
||||
),
|
||||
XmloToken::SymDecl(
|
||||
SPair(sym_map, S3),
|
||||
SymAttrs {
|
||||
pkg_name: Some(pkg_name),
|
||||
ty: Some(SymType::Map),
|
||||
..Default::default()
|
||||
},
|
||||
),
|
||||
XmloToken::SymDecl(
|
||||
SPair(sym_retmap, S4),
|
||||
SymAttrs {
|
||||
pkg_name: Some(pkg_name),
|
||||
ty: Some(SymType::RetMap),
|
||||
..Default::default()
|
||||
},
|
||||
),
|
||||
XmloToken::Eoh(S1),
|
||||
];
|
||||
|
||||
let mut sut = Sut::parse(toks.into_iter());
|
||||
|
||||
// Note that each of these will have their package names cleared
|
||||
// since this is considered to be the first package encountered.
|
||||
assert_eq!(Some(Ok(Parsed::Incomplete)), sut.next()); // PkgName
|
||||
assert_eq!(
|
||||
Some(Ok(Parsed::Object(Air::IdentExternDecl(
|
||||
SPair(sym_extern, S1),
|
||||
IdentKind::Meta,
|
||||
Source {
|
||||
pkg_name: None,
|
||||
..Default::default()
|
||||
}
|
||||
)))),
|
||||
sut.next(),
|
||||
);
|
||||
assert_eq!(
|
||||
Some(Ok(Parsed::Object(Air::IdentDecl(
|
||||
SPair(sym_non_extern, S2),
|
||||
IdentKind::Meta,
|
||||
Source {
|
||||
pkg_name: None,
|
||||
..Default::default()
|
||||
}
|
||||
)))),
|
||||
sut.next(),
|
||||
);
|
||||
assert_eq!(
|
||||
Some(Ok(Parsed::Object(Air::IdentDecl(
|
||||
SPair(sym_map, S3),
|
||||
IdentKind::Map,
|
||||
Source {
|
||||
pkg_name: None,
|
||||
..Default::default()
|
||||
}
|
||||
)))),
|
||||
sut.next(),
|
||||
);
|
||||
assert_eq!(
|
||||
Some(Ok(Parsed::Object(Air::IdentDecl(
|
||||
SPair(sym_retmap, S4),
|
||||
IdentKind::RetMap,
|
||||
Source {
|
||||
pkg_name: None,
|
||||
..Default::default()
|
||||
}
|
||||
)))),
|
||||
sut.next(),
|
||||
);
|
||||
assert_eq!(Some(Ok(Parsed::Incomplete)), sut.next()); // Eoh
|
||||
|
||||
let ctx = sut.finalize().unwrap().into_context();
|
||||
|
||||
// Both above symbols were local (no `src`),
|
||||
// but note that we don't care if it's None or initialized with a
|
||||
// length of 0.
|
||||
assert!(ctx.found.unwrap_or_default().len() == 0);
|
||||
}
|
||||
|
||||
// See above test, where pkg_name was cleared.
|
||||
#[test]
|
||||
fn sym_decl_pkg_name_retained_if_not_first() {
|
||||
let sym = "sym".into();
|
||||
let pkg_name = "pkg name".into();
|
||||
|
||||
// This is all that's needed to not consider this to be the first
|
||||
// package,
|
||||
// so that pkg_name is retained below.
|
||||
let ctx = XmloAirContext {
|
||||
first: false,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let toks = vec![
|
||||
XmloToken::PkgName(SPair(pkg_name, S1)),
|
||||
XmloToken::SymDecl(
|
||||
SPair(sym, S2),
|
||||
SymAttrs {
|
||||
pkg_name: Some(pkg_name),
|
||||
ty: Some(SymType::Meta),
|
||||
..Default::default()
|
||||
},
|
||||
),
|
||||
XmloToken::Eoh(S1),
|
||||
];
|
||||
|
||||
assert_eq!(
|
||||
Ok(vec![
|
||||
Parsed::Incomplete, // PkgName
|
||||
Parsed::Object(Air::IdentDecl(
|
||||
SPair(sym, S2),
|
||||
IdentKind::Meta,
|
||||
Source {
|
||||
pkg_name: Some(pkg_name),
|
||||
..Default::default()
|
||||
}
|
||||
)),
|
||||
Parsed::Incomplete, // Eoh
|
||||
]),
|
||||
Sut::parse_with_context(toks.into_iter(), ctx).collect(),
|
||||
);
|
||||
}
|
||||
|
||||
// This used to be set in SymAttrs by XmloReader,
|
||||
// but that's no longer true with the new reader.
|
||||
#[test]
|
||||
fn sym_decl_pkg_name_set_if_empty_and_not_first() {
|
||||
let sym = "sym".into();
|
||||
let pkg_name = "pkg name".into();
|
||||
|
||||
let ctx = XmloAirContext {
|
||||
first: false,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let toks = vec![
|
||||
XmloToken::PkgName(SPair(pkg_name, S1)),
|
||||
XmloToken::SymDecl(
|
||||
SPair(sym, S2),
|
||||
SymAttrs {
|
||||
// No name
|
||||
ty: Some(SymType::Meta),
|
||||
..Default::default()
|
||||
},
|
||||
),
|
||||
XmloToken::Eoh(S1),
|
||||
];
|
||||
|
||||
assert_eq!(
|
||||
Ok(vec![
|
||||
Parsed::Incomplete, // PkgName
|
||||
Parsed::Object(Air::IdentDecl(
|
||||
SPair(sym, S2),
|
||||
IdentKind::Meta,
|
||||
Source {
|
||||
pkg_name: Some(pkg_name), // Name inherited
|
||||
..Default::default()
|
||||
},
|
||||
)),
|
||||
Parsed::Incomplete, // Eoh
|
||||
]),
|
||||
Sut::parse_with_context(toks.into_iter(), ctx).collect(),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ident_kind_conversion_error_propagates() {
|
||||
let sym = "sym".into();
|
||||
let bad_attrs = SymAttrs::default();
|
||||
|
||||
let toks = vec![
|
||||
XmloToken::PkgName(SPair("name".into(), S1)),
|
||||
XmloToken::SymDecl(SPair(sym, S2), bad_attrs),
|
||||
XmloToken::Eoh(S1),
|
||||
];
|
||||
|
||||
Sut::parse(toks.into_iter())
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.expect_err("expected IdentKind conversion error");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sets_fragment() {
|
||||
let sym = "sym".into();
|
||||
let frag = FragmentText::from("foo");
|
||||
|
||||
let toks = vec![
|
||||
XmloToken::PkgName(SPair("name".into(), S1)),
|
||||
XmloToken::Fragment(SPair(sym, S2), frag.clone()),
|
||||
XmloToken::Eoh(S1),
|
||||
];
|
||||
|
||||
assert_eq!(
|
||||
Ok(vec![
|
||||
Parsed::Incomplete, // PkgName
|
||||
Parsed::Object(Air::IdentFragment(SPair(sym, S2), frag)),
|
||||
Parsed::Incomplete, // Eoh
|
||||
]),
|
||||
Sut::parse(toks.into_iter()).collect(),
|
||||
);
|
||||
}
|
||||
|
||||
macro_rules! test_kind {
|
||||
($name:ident, $src:expr => $dest:expr) => {
|
||||
#[test]
|
||||
fn $name() {
|
||||
assert_eq!(
|
||||
Ok($dest),
|
||||
SymAttrs {
|
||||
ty: Some($src),
|
||||
..Default::default()
|
||||
}
|
||||
.try_into()
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
($name:ident, $src:expr => $dest:expr, dim) => {
|
||||
#[test]
|
||||
fn $name() {
|
||||
let dim = Dim::Vector;
|
||||
|
||||
assert_eq!(
|
||||
Ok($dest(Dim::Vector)),
|
||||
SymAttrs {
|
||||
ty: Some($src),
|
||||
dim: Some(dim),
|
||||
..Default::default()
|
||||
}
|
||||
.try_into()
|
||||
);
|
||||
|
||||
// no dim
|
||||
let result = IdentKind::try_from(SymAttrs {
|
||||
ty: Some($src),
|
||||
..Default::default()
|
||||
})
|
||||
.expect_err("must fail when missing dim");
|
||||
|
||||
assert_eq!(XmloAirError::MissingDim, result);
|
||||
}
|
||||
};
|
||||
|
||||
($name:ident, $src:expr => $dest:expr, dtype) => {
|
||||
#[test]
|
||||
fn $name() {
|
||||
let dtype = Dtype::Float;
|
||||
|
||||
assert_eq!(
|
||||
Ok($dest(dtype)),
|
||||
SymAttrs {
|
||||
ty: Some($src),
|
||||
dtype: Some(dtype),
|
||||
..Default::default()
|
||||
}
|
||||
.try_into()
|
||||
);
|
||||
|
||||
// no dtype
|
||||
let result = IdentKind::try_from(SymAttrs {
|
||||
ty: Some($src),
|
||||
..Default::default()
|
||||
})
|
||||
.expect_err("must fail when missing dtype");
|
||||
|
||||
assert_eq!(XmloAirError::MissingDtype, result);
|
||||
}
|
||||
};
|
||||
|
||||
($name:ident, $src:expr => $dest:expr, dim, dtype) => {
|
||||
#[test]
|
||||
fn $name() {
|
||||
let dim = Dim::Vector;
|
||||
let dtype = Dtype::Float;
|
||||
|
||||
assert_eq!(
|
||||
Ok($dest(Dim::Vector, dtype)),
|
||||
SymAttrs {
|
||||
ty: Some($src),
|
||||
dim: Some(dim),
|
||||
dtype: Some(dtype),
|
||||
..Default::default()
|
||||
}
|
||||
.try_into()
|
||||
);
|
||||
|
||||
// no dim
|
||||
let dim_result = IdentKind::try_from(SymAttrs {
|
||||
ty: Some($src),
|
||||
dtype: Some(dtype),
|
||||
..Default::default()
|
||||
})
|
||||
.expect_err("must fail when missing dim");
|
||||
|
||||
assert_eq!(XmloAirError::MissingDim, dim_result);
|
||||
|
||||
// no dtype
|
||||
let dtype_result = IdentKind::try_from(SymAttrs {
|
||||
ty: Some($src),
|
||||
dim: Some(dim),
|
||||
..Default::default()
|
||||
})
|
||||
.expect_err("must fail when missing dtype");
|
||||
|
||||
assert_eq!(XmloAirError::MissingDtype, dtype_result);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
test_kind!(cgen, SymType::Cgen => IdentKind::Cgen, dim);
|
||||
test_kind!(class, SymType::Class => IdentKind::Class, dim);
|
||||
test_kind!(r#const, SymType::Const => IdentKind::Const, dim, dtype);
|
||||
test_kind!(func, SymType::Func => IdentKind::Func, dim, dtype);
|
||||
test_kind!(gen, SymType::Gen => IdentKind::Gen, dim, dtype);
|
||||
test_kind!(lparam, SymType::Lparam => IdentKind::Lparam, dim, dtype);
|
||||
test_kind!(param, SymType::Param => IdentKind::Param, dim, dtype);
|
||||
test_kind!(rate, SymType::Rate => IdentKind::Rate, dtype);
|
||||
test_kind!(tpl, SymType::Tpl => IdentKind::Tpl);
|
||||
test_kind!(r#type, SymType::Type => IdentKind::Type, dtype);
|
||||
test_kind!(maphead, SymType::MapHead => IdentKind::MapHead);
|
||||
test_kind!(map, SymType::Map => IdentKind::Map);
|
||||
test_kind!(maptail, SymType::MapTail => IdentKind::MapTail);
|
||||
test_kind!(retmaphead, SymType::RetMapHead => IdentKind::RetMapHead);
|
||||
test_kind!(retmap, SymType::RetMap => IdentKind::RetMap);
|
||||
test_kind!(retmaptail, SymType::RetMapTail => IdentKind::RetMapTail);
|
||||
test_kind!(meta, SymType::Meta => IdentKind::Meta);
|
||||
test_kind!(worksheet, SymType::Worksheet => IdentKind::Worksheet);
|
||||
|
||||
#[test]
|
||||
fn source_from_sym_attrs() {
|
||||
let nsym: SymbolId = "name".intern();
|
||||
let ssym: SymbolId = "src".intern();
|
||||
let psym: SymbolId = "parent".intern();
|
||||
let ysym: SymbolId = "yields".intern();
|
||||
let fsym: SymbolId = "from".intern();
|
||||
|
||||
let attrs = SymAttrs {
|
||||
pkg_name: Some(nsym),
|
||||
src: Some(ssym),
|
||||
generated: true,
|
||||
parent: Some(psym),
|
||||
yields: Some(ysym),
|
||||
desc: Some("sym desc".into()),
|
||||
from: Some(fsym),
|
||||
virtual_: true,
|
||||
override_: true,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
Source {
|
||||
pkg_name: Some(nsym),
|
||||
src: Some(ssym),
|
||||
generated: attrs.generated,
|
||||
parent: attrs.parent,
|
||||
yields: attrs.yields,
|
||||
desc: Some("sym desc".into()),
|
||||
from: Some(fsym),
|
||||
virtual_: true,
|
||||
override_: true,
|
||||
},
|
||||
attrs.into(),
|
||||
);
|
||||
}
|
||||
}
|
||||
mod test;
|
||||
|
|
|
@ -0,0 +1,589 @@
|
|||
// Tests lowering `xmlo` object file into AIR
|
||||
//
|
||||
// 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::{
|
||||
asg::{FragmentText, IdentKind},
|
||||
num::{Dim, Dtype},
|
||||
obj::xmlo::{SymAttrs, SymType},
|
||||
parse::Parsed,
|
||||
span::dummy::*,
|
||||
sym::GlobalSymbolIntern,
|
||||
};
|
||||
|
||||
type Sut = XmloToAir;
|
||||
|
||||
use Parsed::{Incomplete, Object as O};
|
||||
|
||||
#[test]
|
||||
fn data_from_package_event() {
|
||||
let name = "name".into();
|
||||
let relroot = "some/path".into();
|
||||
|
||||
#[rustfmt::skip]
|
||||
let toks = vec![
|
||||
PkgStart(S1),
|
||||
PkgName(SPair(name, S2)),
|
||||
PkgRootPath(SPair(relroot, S4)),
|
||||
Eoh(S4),
|
||||
]
|
||||
.into_iter();
|
||||
|
||||
let mut sut = Sut::parse(toks);
|
||||
|
||||
assert_eq!(
|
||||
#[rustfmt::skip]
|
||||
Ok(vec![
|
||||
Incomplete, // PkgStart
|
||||
O(Air::PkgStart(S1, SPair(name, S2))),
|
||||
Incomplete, // PkgRootPath
|
||||
O(Air::PkgEnd(S4)),
|
||||
]),
|
||||
sut.by_ref().collect(),
|
||||
);
|
||||
|
||||
let ctx = sut.finalize().unwrap().into_context();
|
||||
|
||||
assert_eq!(Some(name), ctx.prog_name);
|
||||
assert_eq!(Some(relroot), ctx.relroot);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn adds_elig_as_root() {
|
||||
let name = "name-root".into();
|
||||
let elig_sym = "elig".into();
|
||||
|
||||
#[rustfmt::skip]
|
||||
let toks = vec![
|
||||
PkgStart(S1),
|
||||
PkgName(SPair(name, S2)),
|
||||
PkgEligClassYields(SPair(elig_sym, S3)),
|
||||
Eoh(S4),
|
||||
];
|
||||
|
||||
assert_eq!(
|
||||
#[rustfmt::skip]
|
||||
Ok(vec![
|
||||
Incomplete, // PkgStart
|
||||
O(Air::PkgStart(S1, SPair(name, S2))),
|
||||
O(Air::IdentRoot(SPair(elig_sym, S3))),
|
||||
O(Air::PkgEnd(S4)), // Eoh
|
||||
]),
|
||||
Sut::parse(toks.into_iter()).collect(),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn adds_sym_deps() {
|
||||
let name = "name".into();
|
||||
let sym_from = "from".into();
|
||||
let sym_to1 = "to1".into();
|
||||
let sym_to2 = "to2".into();
|
||||
|
||||
#[rustfmt::skip]
|
||||
let toks = vec![
|
||||
PkgStart(S1),
|
||||
PkgName(SPair(name, S2)),
|
||||
|
||||
SymDepStart(SPair(sym_from, S3)),
|
||||
Symbol(SPair(sym_to1, S4)),
|
||||
Symbol(SPair(sym_to2, S5)),
|
||||
Eoh(S6),
|
||||
];
|
||||
|
||||
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))),
|
||||
O(Air::IdentDep(SPair(sym_from, S3), SPair(sym_to2, S5))),
|
||||
O(Air::PkgEnd(S6)),
|
||||
]),
|
||||
Sut::parse(toks.into_iter()).collect(),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sym_decl_with_src_not_added_and_populates_found() {
|
||||
let name = "name".into();
|
||||
let sym = "sym".into();
|
||||
let src_a = "src_a".into();
|
||||
let src_b = "src_b".into();
|
||||
|
||||
#[rustfmt::skip]
|
||||
let toks = vec![
|
||||
PkgStart(S1),
|
||||
PkgName(SPair(name, S2)),
|
||||
|
||||
SymDecl(
|
||||
SPair(sym, S3),
|
||||
SymAttrs {
|
||||
src: Some(src_a),
|
||||
..Default::default()
|
||||
},
|
||||
),
|
||||
SymDecl(
|
||||
SPair(sym, S4),
|
||||
SymAttrs {
|
||||
src: Some(src_b),
|
||||
..Default::default()
|
||||
},
|
||||
),
|
||||
Eoh(S5),
|
||||
];
|
||||
|
||||
let mut sut = Sut::parse(toks.into_iter());
|
||||
|
||||
assert_eq!(
|
||||
#[rustfmt::skip]
|
||||
Ok(vec![
|
||||
Incomplete, // PkgStart
|
||||
O(Air::PkgStart(S1, SPair(name, S2))),
|
||||
Incomplete, // SymDecl (@src)
|
||||
Incomplete, // SymDecl (@src)
|
||||
O(Air::PkgEnd(S5)),
|
||||
]),
|
||||
sut.by_ref().collect(),
|
||||
);
|
||||
|
||||
let ctx = sut.finalize().unwrap().into_context();
|
||||
let mut founds = ctx.found.unwrap().into_iter().collect::<Vec<_>>();
|
||||
|
||||
// Just to remove nondeterminism in case the iteration order happens
|
||||
// to change.
|
||||
founds.sort();
|
||||
|
||||
assert_eq!(vec![src_a, src_b], founds);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sym_decl_added_to_graph() {
|
||||
let name = "name".into();
|
||||
let sym_extern = "sym_extern".into();
|
||||
let sym_non_extern = "sym_non_extern".into();
|
||||
let sym_map = "sym_map".into();
|
||||
let sym_retmap = "sym_retmap".into();
|
||||
let pkg_name = "pkg name".into();
|
||||
|
||||
#[rustfmt::skip]
|
||||
let toks = vec![
|
||||
PkgStart(S1),
|
||||
PkgName(SPair(name, S2)),
|
||||
|
||||
SymDecl(
|
||||
SPair(sym_extern, S3),
|
||||
SymAttrs {
|
||||
pkg_name: Some(pkg_name),
|
||||
extern_: true,
|
||||
ty: Some(SymType::Meta),
|
||||
..Default::default()
|
||||
},
|
||||
),
|
||||
SymDecl(
|
||||
SPair(sym_non_extern, S4),
|
||||
SymAttrs {
|
||||
pkg_name: Some(pkg_name),
|
||||
ty: Some(SymType::Meta),
|
||||
..Default::default()
|
||||
},
|
||||
),
|
||||
SymDecl(
|
||||
SPair(sym_map, S5),
|
||||
SymAttrs {
|
||||
pkg_name: Some(pkg_name),
|
||||
ty: Some(SymType::Map),
|
||||
..Default::default()
|
||||
},
|
||||
),
|
||||
SymDecl(
|
||||
SPair(sym_retmap, S6),
|
||||
SymAttrs {
|
||||
pkg_name: Some(pkg_name),
|
||||
ty: Some(SymType::RetMap),
|
||||
..Default::default()
|
||||
},
|
||||
),
|
||||
Eoh(S7),
|
||||
];
|
||||
|
||||
let mut sut = Sut::parse(toks.into_iter());
|
||||
|
||||
// Note that each of these will have their package names cleared
|
||||
// since this is considered to be the first package encountered.
|
||||
assert_eq!(Some(Ok(Incomplete)), sut.next()); // PkgStart
|
||||
assert_eq!(Some(Ok(O(Air::PkgStart(S1, SPair(name, S2))))), sut.next()); // PkgName
|
||||
assert_eq!(
|
||||
Some(Ok(O(Air::IdentExternDecl(
|
||||
SPair(sym_extern, S3),
|
||||
IdentKind::Meta,
|
||||
Source {
|
||||
pkg_name: None,
|
||||
..Default::default()
|
||||
}
|
||||
)))),
|
||||
sut.next(),
|
||||
);
|
||||
assert_eq!(
|
||||
Some(Ok(O(Air::IdentDecl(
|
||||
SPair(sym_non_extern, S4),
|
||||
IdentKind::Meta,
|
||||
Source {
|
||||
pkg_name: None,
|
||||
..Default::default()
|
||||
}
|
||||
)))),
|
||||
sut.next(),
|
||||
);
|
||||
assert_eq!(
|
||||
Some(Ok(O(Air::IdentDecl(
|
||||
SPair(sym_map, S5),
|
||||
IdentKind::Map,
|
||||
Source {
|
||||
pkg_name: None,
|
||||
..Default::default()
|
||||
}
|
||||
)))),
|
||||
sut.next(),
|
||||
);
|
||||
assert_eq!(
|
||||
Some(Ok(O(Air::IdentDecl(
|
||||
SPair(sym_retmap, S6),
|
||||
IdentKind::RetMap,
|
||||
Source {
|
||||
pkg_name: None,
|
||||
..Default::default()
|
||||
}
|
||||
)))),
|
||||
sut.next(),
|
||||
);
|
||||
|
||||
assert_eq!(Some(Ok(O(Air::PkgEnd(S7)))), sut.next());
|
||||
|
||||
let ctx = sut.finalize().unwrap().into_context();
|
||||
|
||||
// Both above symbols were local (no `src`),
|
||||
// but note that we don't care if it's None or initialized with a
|
||||
// length of 0.
|
||||
assert!(ctx.found.unwrap_or_default().len() == 0);
|
||||
}
|
||||
|
||||
// See above test, where pkg_name was cleared.
|
||||
#[test]
|
||||
fn sym_decl_pkg_name_retained_if_not_first() {
|
||||
let sym = "sym".into();
|
||||
let pkg_name = "pkg name".into();
|
||||
|
||||
// This is all that's needed to not consider this to be the first
|
||||
// package,
|
||||
// so that pkg_name is retained below.
|
||||
let ctx = XmloAirContext {
|
||||
first: false,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
#[rustfmt::skip]
|
||||
let toks = vec![
|
||||
PkgStart(S1),
|
||||
PkgName(SPair(pkg_name, S2)),
|
||||
|
||||
SymDecl(
|
||||
SPair(sym, S3),
|
||||
SymAttrs {
|
||||
pkg_name: Some(pkg_name),
|
||||
ty: Some(SymType::Meta),
|
||||
..Default::default()
|
||||
},
|
||||
),
|
||||
Eoh(S4),
|
||||
];
|
||||
|
||||
assert_eq!(
|
||||
#[rustfmt::skip]
|
||||
Ok(vec![
|
||||
Incomplete, // PkgStart
|
||||
O(Air::PkgStart(S1, SPair(pkg_name, S2))),
|
||||
O(Air::IdentDecl(
|
||||
SPair(sym, S3),
|
||||
IdentKind::Meta,
|
||||
Source {
|
||||
pkg_name: Some(pkg_name),
|
||||
..Default::default()
|
||||
}
|
||||
)),
|
||||
O(Air::PkgEnd(S4)),
|
||||
]),
|
||||
Sut::parse_with_context(toks.into_iter(), ctx).collect(),
|
||||
);
|
||||
}
|
||||
|
||||
// This used to be set in SymAttrs by XmloReader,
|
||||
// but that's no longer true with the new reader.
|
||||
#[test]
|
||||
fn sym_decl_pkg_name_set_if_empty_and_not_first() {
|
||||
let sym = "sym".into();
|
||||
let pkg_name = "pkg name".into();
|
||||
|
||||
let ctx = XmloAirContext {
|
||||
first: false,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
#[rustfmt::skip]
|
||||
let toks = vec![
|
||||
PkgStart(S1),
|
||||
PkgName(SPair(pkg_name, S2)),
|
||||
|
||||
SymDecl(
|
||||
SPair(sym, S3),
|
||||
SymAttrs {
|
||||
// No name
|
||||
ty: Some(SymType::Meta),
|
||||
..Default::default()
|
||||
},
|
||||
),
|
||||
Eoh(S4),
|
||||
];
|
||||
|
||||
assert_eq!(
|
||||
#[rustfmt::skip]
|
||||
Ok(vec![
|
||||
Incomplete, // PkgStart
|
||||
O(Air::PkgStart(S1, SPair(pkg_name, S2))),
|
||||
O(Air::IdentDecl(
|
||||
SPair(sym, S3),
|
||||
IdentKind::Meta,
|
||||
Source {
|
||||
pkg_name: Some(pkg_name), // Name inherited
|
||||
..Default::default()
|
||||
},
|
||||
)),
|
||||
O(Air::PkgEnd(S4)),
|
||||
]),
|
||||
Sut::parse_with_context(toks.into_iter(), ctx).collect(),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ident_kind_conversion_error_propagates() {
|
||||
let sym = "sym".into();
|
||||
let bad_attrs = SymAttrs::default();
|
||||
|
||||
#[rustfmt::skip]
|
||||
let toks = vec![
|
||||
PkgStart(S1),
|
||||
PkgName(SPair("name".into(), S2)),
|
||||
SymDecl(SPair(sym, S3), bad_attrs),
|
||||
Eoh(S1),
|
||||
];
|
||||
|
||||
Sut::parse(toks.into_iter())
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.expect_err("expected IdentKind conversion error");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sets_fragment() {
|
||||
let name = "name".into();
|
||||
let sym = "sym".into();
|
||||
let frag = FragmentText::from("foo");
|
||||
|
||||
#[rustfmt::skip]
|
||||
let toks = vec![
|
||||
PkgStart(S1),
|
||||
PkgName(SPair(name, S2)),
|
||||
Fragment(SPair(sym, S3), frag.clone()),
|
||||
Eoh(S4),
|
||||
];
|
||||
|
||||
assert_eq!(
|
||||
#[rustfmt::skip]
|
||||
Ok(vec![
|
||||
Incomplete, // PkgStart
|
||||
O(Air::PkgStart(S1, SPair(name, S2))),
|
||||
O(Air::IdentFragment(SPair(sym, S3), frag)),
|
||||
O(Air::PkgEnd(S4)),
|
||||
]),
|
||||
Sut::parse(toks.into_iter()).collect(),
|
||||
);
|
||||
}
|
||||
|
||||
macro_rules! test_kind {
|
||||
($name:ident, $src:expr => $dest:expr) => {
|
||||
#[test]
|
||||
fn $name() {
|
||||
assert_eq!(
|
||||
Ok($dest),
|
||||
SymAttrs {
|
||||
ty: Some($src),
|
||||
..Default::default()
|
||||
}
|
||||
.try_into()
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
($name:ident, $src:expr => $dest:expr, dim) => {
|
||||
#[test]
|
||||
fn $name() {
|
||||
let dim = Dim::Vector;
|
||||
|
||||
assert_eq!(
|
||||
Ok($dest(Dim::Vector)),
|
||||
SymAttrs {
|
||||
ty: Some($src),
|
||||
dim: Some(dim),
|
||||
..Default::default()
|
||||
}
|
||||
.try_into()
|
||||
);
|
||||
|
||||
// no dim
|
||||
let result = IdentKind::try_from(SymAttrs {
|
||||
ty: Some($src),
|
||||
..Default::default()
|
||||
})
|
||||
.expect_err("must fail when missing dim");
|
||||
|
||||
assert_eq!(XmloAirError::MissingDim, result);
|
||||
}
|
||||
};
|
||||
|
||||
($name:ident, $src:expr => $dest:expr, dtype) => {
|
||||
#[test]
|
||||
fn $name() {
|
||||
let dtype = Dtype::Float;
|
||||
|
||||
assert_eq!(
|
||||
Ok($dest(dtype)),
|
||||
SymAttrs {
|
||||
ty: Some($src),
|
||||
dtype: Some(dtype),
|
||||
..Default::default()
|
||||
}
|
||||
.try_into()
|
||||
);
|
||||
|
||||
// no dtype
|
||||
let result = IdentKind::try_from(SymAttrs {
|
||||
ty: Some($src),
|
||||
..Default::default()
|
||||
})
|
||||
.expect_err("must fail when missing dtype");
|
||||
|
||||
assert_eq!(XmloAirError::MissingDtype, result);
|
||||
}
|
||||
};
|
||||
|
||||
($name:ident, $src:expr => $dest:expr, dim, dtype) => {
|
||||
#[test]
|
||||
fn $name() {
|
||||
let dim = Dim::Vector;
|
||||
let dtype = Dtype::Float;
|
||||
|
||||
assert_eq!(
|
||||
Ok($dest(Dim::Vector, dtype)),
|
||||
SymAttrs {
|
||||
ty: Some($src),
|
||||
dim: Some(dim),
|
||||
dtype: Some(dtype),
|
||||
..Default::default()
|
||||
}
|
||||
.try_into()
|
||||
);
|
||||
|
||||
// no dim
|
||||
let dim_result = IdentKind::try_from(SymAttrs {
|
||||
ty: Some($src),
|
||||
dtype: Some(dtype),
|
||||
..Default::default()
|
||||
})
|
||||
.expect_err("must fail when missing dim");
|
||||
|
||||
assert_eq!(XmloAirError::MissingDim, dim_result);
|
||||
|
||||
// no dtype
|
||||
let dtype_result = IdentKind::try_from(SymAttrs {
|
||||
ty: Some($src),
|
||||
dim: Some(dim),
|
||||
..Default::default()
|
||||
})
|
||||
.expect_err("must fail when missing dtype");
|
||||
|
||||
assert_eq!(XmloAirError::MissingDtype, dtype_result);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
test_kind!(cgen, SymType::Cgen => IdentKind::Cgen, dim);
|
||||
test_kind!(class, SymType::Class => IdentKind::Class, dim);
|
||||
test_kind!(r#const, SymType::Const => IdentKind::Const, dim, dtype);
|
||||
test_kind!(func, SymType::Func => IdentKind::Func, dim, dtype);
|
||||
test_kind!(gen, SymType::Gen => IdentKind::Gen, dim, dtype);
|
||||
test_kind!(lparam, SymType::Lparam => IdentKind::Lparam, dim, dtype);
|
||||
test_kind!(param, SymType::Param => IdentKind::Param, dim, dtype);
|
||||
test_kind!(rate, SymType::Rate => IdentKind::Rate, dtype);
|
||||
test_kind!(tpl, SymType::Tpl => IdentKind::Tpl);
|
||||
test_kind!(r#type, SymType::Type => IdentKind::Type, dtype);
|
||||
test_kind!(maphead, SymType::MapHead => IdentKind::MapHead);
|
||||
test_kind!(map, SymType::Map => IdentKind::Map);
|
||||
test_kind!(maptail, SymType::MapTail => IdentKind::MapTail);
|
||||
test_kind!(retmaphead, SymType::RetMapHead => IdentKind::RetMapHead);
|
||||
test_kind!(retmap, SymType::RetMap => IdentKind::RetMap);
|
||||
test_kind!(retmaptail, SymType::RetMapTail => IdentKind::RetMapTail);
|
||||
test_kind!(meta, SymType::Meta => IdentKind::Meta);
|
||||
test_kind!(worksheet, SymType::Worksheet => IdentKind::Worksheet);
|
||||
|
||||
#[test]
|
||||
fn source_from_sym_attrs() {
|
||||
let nsym: SymbolId = "name".intern();
|
||||
let ssym: SymbolId = "src".intern();
|
||||
let psym: SymbolId = "parent".intern();
|
||||
let ysym: SymbolId = "yields".intern();
|
||||
let fsym: SymbolId = "from".intern();
|
||||
|
||||
let attrs = SymAttrs {
|
||||
pkg_name: Some(nsym),
|
||||
src: Some(ssym),
|
||||
generated: true,
|
||||
parent: Some(psym),
|
||||
yields: Some(ysym),
|
||||
desc: Some("sym desc".into()),
|
||||
from: Some(fsym),
|
||||
virtual_: true,
|
||||
override_: true,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
Source {
|
||||
pkg_name: Some(nsym),
|
||||
src: Some(ssym),
|
||||
generated: attrs.generated,
|
||||
parent: attrs.parent,
|
||||
yields: attrs.yields,
|
||||
desc: Some("sym desc".into()),
|
||||
from: Some(fsym),
|
||||
virtual_: true,
|
||||
override_: true,
|
||||
},
|
||||
attrs.into(),
|
||||
);
|
||||
}
|
|
@ -29,7 +29,7 @@ use crate::{
|
|||
ParseState, Token, Transition, TransitionResult, Transitionable,
|
||||
},
|
||||
span::Span,
|
||||
sym::{st::raw, SymbolId},
|
||||
sym::{st::raw, GlobalSymbolIntern, GlobalSymbolResolve, SymbolId},
|
||||
xir::{
|
||||
attr::{Attr, AttrSpan},
|
||||
flat::{Text, XirfToken as Xirf},
|
||||
|
@ -49,6 +49,9 @@ use crate::{
|
|||
/// be useful and can't be easily skipped without parsing.
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub enum XmloToken {
|
||||
/// A new package has been found.
|
||||
PkgStart(Span),
|
||||
|
||||
/// Canonical package name.
|
||||
PkgName(SPair),
|
||||
/// Relative path from package to project root.
|
||||
|
@ -109,7 +112,8 @@ impl Token for XmloToken {
|
|||
// important since these initial tokens seed
|
||||
// `Parser::last_span`,
|
||||
// which is used for early error messages.
|
||||
PkgName(SPair(_, span))
|
||||
PkgStart(span)
|
||||
| PkgName(SPair(_, span))
|
||||
| PkgRootPath(SPair(_, span))
|
||||
| PkgProgramFlag(span)
|
||||
| PkgEligClassYields(SPair(_, span))
|
||||
|
@ -127,6 +131,7 @@ impl Display for XmloToken {
|
|||
use XmloToken::*;
|
||||
|
||||
match self {
|
||||
PkgStart(_) => write!(f, "package start"),
|
||||
PkgName(sym) => write!(f, "package of name {}", TtQuote::wrap(sym)),
|
||||
PkgRootPath(sym) => {
|
||||
write!(f, "package root path {}", TtQuote::wrap(sym))
|
||||
|
@ -208,8 +213,9 @@ impl<SS: XmloState, SD: XmloState, SF: XmloState> ParseState
|
|||
use XmloReader::*;
|
||||
|
||||
match (self, tok) {
|
||||
(Ready, Xirf::Open(QN_LV_PACKAGE | QN_PACKAGE, span, ..)) => {
|
||||
Transition(Package(span.tag_span())).incomplete()
|
||||
(Ready, Xirf::Open(QN_LV_PACKAGE | QN_PACKAGE, ospan, ..)) => {
|
||||
let span = ospan.tag_span();
|
||||
Transition(Package(span)).ok(XmloToken::PkgStart(span))
|
||||
}
|
||||
|
||||
(Ready, tok) => {
|
||||
|
@ -221,9 +227,10 @@ impl<SS: XmloState, SD: XmloState, SF: XmloState> ParseState
|
|||
// which can result in confusing output depending on the context;
|
||||
// we ought to retain _both_ token- and value-spans.
|
||||
Transition(Package(span)).ok(match name {
|
||||
QN_NAME => {
|
||||
XmloToken::PkgName(SPair(value, aspan.value_span()))
|
||||
}
|
||||
QN_NAME => XmloToken::PkgName(SPair(
|
||||
canonical_slash(value),
|
||||
aspan.value_span(),
|
||||
)),
|
||||
QN_UUROOTPATH => {
|
||||
XmloToken::PkgRootPath(SPair(value, aspan.value_span()))
|
||||
}
|
||||
|
@ -315,6 +322,27 @@ impl<SS: XmloState, SD: XmloState, SF: XmloState> ParseState
|
|||
}
|
||||
}
|
||||
|
||||
/// Introduce a leading `/` to `name` if missing.
|
||||
///
|
||||
/// A new [`SymbolId`] will be allocated if the leading slash is missing
|
||||
/// from `name.
|
||||
///
|
||||
/// The XSLT-based compiler at the time of writing produced canonical names
|
||||
/// _without_ a leading slash.
|
||||
/// This convention was not changed until TAMER,
|
||||
/// so that canonical paths could be used as namespecs for import in an
|
||||
/// unambiguous way.
|
||||
/// We want to support both,
|
||||
/// so that TAMER-compiled object files will also work.
|
||||
fn canonical_slash(name: SymbolId) -> SymbolId {
|
||||
let s = name.lookup_str();
|
||||
|
||||
match s.starts_with('/') {
|
||||
true => name,
|
||||
false => format!("/{s}").intern(),
|
||||
}
|
||||
}
|
||||
|
||||
impl<SS: XmloState, SD: XmloState, SF: XmloState> Display
|
||||
for XmloReader<SS, SD, SF>
|
||||
{
|
||||
|
|
|
@ -28,9 +28,8 @@ use crate::{
|
|||
span::{dummy::*, Span},
|
||||
sym::GlobalSymbolIntern,
|
||||
xir::{
|
||||
attr::Attr,
|
||||
flat::{
|
||||
test::{close, close_empty, open},
|
||||
test::{attr, close, close_empty, open},
|
||||
Depth, XirfToken as Xirf,
|
||||
},
|
||||
QName,
|
||||
|
@ -39,6 +38,9 @@ use crate::{
|
|||
|
||||
type Sut = XmloReader;
|
||||
|
||||
use Parsed::{Incomplete, Object as O};
|
||||
use XmloToken::*;
|
||||
|
||||
#[test]
|
||||
fn fails_on_invalid_root() {
|
||||
let tok = open("not-a-valid-package-node", S1, Depth(0));
|
||||
|
@ -52,24 +54,17 @@ fn fails_on_invalid_root() {
|
|||
}
|
||||
|
||||
fn common_parses_package_attrs(package: QName) {
|
||||
let name = "pkgroot".into();
|
||||
let name = "/pkgroot".into();
|
||||
let relroot = "../../".into();
|
||||
let elig = "elig-class-yields".into();
|
||||
|
||||
#[rustfmt::skip]
|
||||
let toks = [
|
||||
open(package, S1, Depth(0)),
|
||||
Xirf::Attr(Attr::new("name".unwrap_into(), name, (S2, S3))),
|
||||
Xirf::Attr(Attr::new("__rootpath".unwrap_into(), relroot, (S2, S3))),
|
||||
Xirf::Attr(Attr::new(
|
||||
"program".unwrap_into(),
|
||||
crate::sym::st::raw::L_TRUE,
|
||||
(S3, S4),
|
||||
)),
|
||||
Xirf::Attr(Attr::new(
|
||||
("preproc", "elig-class-yields").unwrap_into(),
|
||||
elig,
|
||||
(S3, S4),
|
||||
)),
|
||||
attr("name", name, (S2, S3)),
|
||||
attr("__rootpath", relroot, (S2, S3)),
|
||||
attr("program", crate::sym::st::raw::L_TRUE, (S3, S4)),
|
||||
attr(("preproc", "elig-class-yields"), elig, (S3, S4)),
|
||||
close(Some(package), S2, Depth(0)),
|
||||
]
|
||||
.into_iter();
|
||||
|
@ -77,17 +72,18 @@ fn common_parses_package_attrs(package: QName) {
|
|||
let sut = Sut::parse(toks);
|
||||
|
||||
assert_eq!(
|
||||
#[rustfmt::skip]
|
||||
Ok(vec![
|
||||
Parsed::Incomplete,
|
||||
Parsed::Object(XmloToken::PkgName(SPair(name, S3))),
|
||||
Parsed::Object(XmloToken::PkgRootPath(SPair(relroot, S3))),
|
||||
// Span for the program flag is the attr name,
|
||||
// rather than the value,
|
||||
// since the value is just a boolean and does not provide as
|
||||
// useful of context.
|
||||
Parsed::Object(XmloToken::PkgProgramFlag(S3)),
|
||||
Parsed::Object(XmloToken::PkgEligClassYields(SPair(elig, S4))),
|
||||
Parsed::Incomplete,
|
||||
O(PkgStart(S1)),
|
||||
O(PkgName(SPair(name, S3))),
|
||||
O(PkgRootPath(SPair(relroot, S3))),
|
||||
// Span for the program flag is the attr name,
|
||||
// rather than the value,
|
||||
// since the value is just a boolean and does not provide as
|
||||
// useful of context.
|
||||
O(PkgProgramFlag(S3)),
|
||||
O(PkgEligClassYields(SPair(elig, S4))),
|
||||
Incomplete,
|
||||
]),
|
||||
sut.collect(),
|
||||
);
|
||||
|
@ -105,17 +101,15 @@ fn parses_package_attrs_with_ns_prefix() {
|
|||
common_parses_package_attrs(("lv", "package").unwrap_into());
|
||||
}
|
||||
|
||||
// Maintains BC with existing system,
|
||||
// but this ought to reject in the future.
|
||||
// For compatibility with XSLT-based compiler.
|
||||
#[test]
|
||||
fn ignores_unknown_package_attr() {
|
||||
let name = "pkgroot".into();
|
||||
fn adds_missing_leading_slash_to_canonical_name() {
|
||||
let name = "needs/leading".into();
|
||||
|
||||
#[rustfmt::skip]
|
||||
let toks = [
|
||||
open(QN_PACKAGE, S1, Depth(0)),
|
||||
Xirf::Attr(Attr::new("name".unwrap_into(), name, (S2, S3))),
|
||||
// This is ignored.
|
||||
Xirf::Attr(Attr::new("unknown".unwrap_into(), name, (S2, S3))),
|
||||
attr("name", name, (S2, S3)),
|
||||
close(Some(QN_PACKAGE), S2, Depth(0)),
|
||||
]
|
||||
.into_iter();
|
||||
|
@ -123,11 +117,41 @@ fn ignores_unknown_package_attr() {
|
|||
let sut = Sut::parse(toks);
|
||||
|
||||
assert_eq!(
|
||||
#[rustfmt::skip]
|
||||
Ok(vec![
|
||||
Parsed::Incomplete,
|
||||
Parsed::Object(XmloToken::PkgName(SPair(name, S3))),
|
||||
Parsed::Incomplete, // The unknown attribute
|
||||
Parsed::Incomplete,
|
||||
O(PkgStart(S1)),
|
||||
O(PkgName(SPair("/needs/leading".into(), S3))),
|
||||
Incomplete,
|
||||
]),
|
||||
sut.collect(),
|
||||
);
|
||||
}
|
||||
|
||||
// Maintains BC with existing system,
|
||||
// but this ought to reject in the future.
|
||||
#[test]
|
||||
fn ignores_unknown_package_attr() {
|
||||
let name = "/pkgroot".into();
|
||||
|
||||
#[rustfmt::skip]
|
||||
let toks = [
|
||||
open(QN_PACKAGE, S1, Depth(0)),
|
||||
attr("name", name, (S2, S3)),
|
||||
// This is ignored.
|
||||
attr("unknown", name, (S2, S3)),
|
||||
close(Some(QN_PACKAGE), S2, Depth(0)),
|
||||
]
|
||||
.into_iter();
|
||||
|
||||
let sut = Sut::parse(toks);
|
||||
|
||||
assert_eq!(
|
||||
#[rustfmt::skip]
|
||||
Ok(vec![
|
||||
O(PkgStart(S1)),
|
||||
O(PkgName(SPair(name, S3))),
|
||||
Incomplete, // The unknown attribute
|
||||
Incomplete,
|
||||
]),
|
||||
sut.collect(),
|
||||
);
|
||||
|
@ -144,7 +168,7 @@ fn symtable_err_missing_sym_name() {
|
|||
|
||||
let mut sut = SymtableState::parse(toks);
|
||||
|
||||
assert_eq!(sut.next(), Some(Ok(Parsed::Incomplete)),);
|
||||
assert_eq!(sut.next(), Some(Ok(Incomplete)),);
|
||||
|
||||
assert_eq!(
|
||||
sut.next(),
|
||||
|
@ -166,13 +190,13 @@ macro_rules! symtable_tests {
|
|||
|
||||
let toks = [
|
||||
open(QN_P_SYM, SSYM, Depth(0)),
|
||||
Xirf::Attr(Attr(QN_NAME, name, AttrSpan(S2, S3))),
|
||||
attr(QN_NAME, name, (S2, S3)),
|
||||
$(
|
||||
Xirf::Attr(Attr(
|
||||
stringify!($key).unwrap_into(),
|
||||
attr(
|
||||
stringify!($key),
|
||||
$val.unwrap_into(),
|
||||
AttrSpan(S3, SATTRVAL)
|
||||
)),
|
||||
(S3, SATTRVAL)
|
||||
),
|
||||
)*
|
||||
close(Some(QN_P_SYM), S2, Depth(0)),
|
||||
]
|
||||
|
@ -182,16 +206,16 @@ macro_rules! symtable_tests {
|
|||
match $expect {
|
||||
Ok(expected) =>
|
||||
Ok(vec![
|
||||
Parsed::Incomplete, // Opening tag
|
||||
Parsed::Incomplete, // @name
|
||||
Incomplete, // Opening tag
|
||||
Incomplete, // @name
|
||||
$(
|
||||
// For each attribute ($key here is necessary
|
||||
// for macro iteration).
|
||||
#[allow(unused)]
|
||||
#[doc=stringify!($key)]
|
||||
Parsed::Incomplete,
|
||||
Incomplete,
|
||||
)*
|
||||
Parsed::Object(XmloToken::SymDecl(
|
||||
O(SymDecl(
|
||||
SPair(name, S3),
|
||||
expected
|
||||
)),
|
||||
|
@ -326,14 +350,15 @@ symtable_tests! {
|
|||
fn symtable_sym_generated_true() {
|
||||
let name = "generated_true".into();
|
||||
|
||||
#[rustfmt::skip]
|
||||
let toks = [
|
||||
open(QN_P_SYM, SSYM, Depth(0)),
|
||||
Xirf::Attr(Attr(QN_NAME, name, AttrSpan(S2, S3))),
|
||||
Xirf::Attr(Attr(
|
||||
("preproc", "generated").unwrap_into(),
|
||||
raw::L_TRUE,
|
||||
AttrSpan(S3, S4),
|
||||
)),
|
||||
attr(QN_NAME, name, (S2, S3)),
|
||||
attr(
|
||||
("preproc", "generated"),
|
||||
raw::L_TRUE,
|
||||
(S3, S4),
|
||||
),
|
||||
close(Some(QN_P_SYM), S2, Depth(0)),
|
||||
]
|
||||
.into_iter();
|
||||
|
@ -344,11 +369,12 @@ fn symtable_sym_generated_true() {
|
|||
};
|
||||
|
||||
assert_eq!(
|
||||
#[rustfmt::skip]
|
||||
Ok(vec![
|
||||
Parsed::Incomplete, // Opening tag
|
||||
Parsed::Incomplete, // @name
|
||||
Parsed::Incomplete, // @preproc:generated
|
||||
Parsed::Object(XmloToken::SymDecl(SPair(name, S3), expected)),
|
||||
Incomplete, // Opening tag
|
||||
Incomplete, // @name
|
||||
Incomplete, // @preproc:generated
|
||||
O(SymDecl(SPair(name, S3), expected)),
|
||||
]),
|
||||
SymtableState::parse(toks).collect(),
|
||||
);
|
||||
|
@ -361,15 +387,17 @@ fn symtable_map_from() {
|
|||
let name = "sym-map-from".into();
|
||||
let map_from = "from-a".into();
|
||||
|
||||
#[rustfmt::skip]
|
||||
let toks = [
|
||||
open(QN_P_SYM, SSYM, Depth(0)),
|
||||
Xirf::Attr(Attr(QN_NAME, name, AttrSpan(S2, S3))),
|
||||
Xirf::Attr(Attr(QN_TYPE, raw::L_MAP, AttrSpan(S3, S4))),
|
||||
// <preproc:from>
|
||||
open(QN_P_FROM, S2, Depth(1)),
|
||||
Xirf::Attr(Attr(QN_NAME, map_from, AttrSpan(S2, S3))),
|
||||
close_empty(S4, Depth(1)),
|
||||
// />
|
||||
attr(QN_NAME, name, (S2, S3)),
|
||||
attr(QN_TYPE, raw::L_MAP, (S3, S4)),
|
||||
|
||||
// <preproc:from>
|
||||
open(QN_P_FROM, S2, Depth(1)),
|
||||
attr(QN_NAME, map_from, (S2, S3)),
|
||||
close_empty(S4, Depth(1)),
|
||||
// />
|
||||
close(Some(QN_P_SYM), S2, Depth(0)),
|
||||
]
|
||||
.into_iter();
|
||||
|
@ -382,13 +410,13 @@ fn symtable_map_from() {
|
|||
|
||||
assert_eq!(
|
||||
Ok(vec![
|
||||
Parsed::Incomplete, // Opening tag
|
||||
Parsed::Incomplete, // @name
|
||||
Parsed::Incomplete, // @type
|
||||
Parsed::Incomplete, // <preproc:from
|
||||
Parsed::Incomplete, // @name
|
||||
Parsed::Incomplete, // />
|
||||
Parsed::Object(XmloToken::SymDecl(SPair(name, S3), expected)),
|
||||
Incomplete, // Opening tag
|
||||
Incomplete, // @name
|
||||
Incomplete, // @type
|
||||
Incomplete, // <preproc:from
|
||||
Incomplete, // @name
|
||||
Incomplete, // />
|
||||
O(SymDecl(SPair(name, S3), expected)),
|
||||
]),
|
||||
SymtableState::parse(toks).collect(),
|
||||
);
|
||||
|
@ -398,15 +426,17 @@ fn symtable_map_from() {
|
|||
fn symtable_map_from_missing_name() {
|
||||
let name = "sym-map-from-missing".into();
|
||||
|
||||
#[rustfmt::skip]
|
||||
let toks = [
|
||||
open(QN_P_SYM, SSYM, Depth(0)),
|
||||
Xirf::Attr(Attr(QN_NAME, name, AttrSpan(S2, S3))),
|
||||
Xirf::Attr(Attr(QN_TYPE, raw::L_MAP, AttrSpan(S3, S4))),
|
||||
// <preproc:from>
|
||||
open(QN_P_FROM, S2, Depth(1)),
|
||||
// @name missing
|
||||
close_empty(S4, Depth(1)),
|
||||
// />
|
||||
attr(QN_NAME, name, (S2, S3)),
|
||||
attr(QN_TYPE, raw::L_MAP, (S3, S4)),
|
||||
|
||||
// <preproc:from>
|
||||
open(QN_P_FROM, S2, Depth(1)),
|
||||
// @name missing
|
||||
close_empty(S4, Depth(1)),
|
||||
// />
|
||||
close(Some(QN_P_SYM), S2, Depth(0)),
|
||||
]
|
||||
.into_iter();
|
||||
|
@ -423,20 +453,23 @@ fn symtable_map_from_missing_name() {
|
|||
fn symtable_map_from_multiple() {
|
||||
let name = "sym-map-from-missing".into();
|
||||
|
||||
#[rustfmt::skip]
|
||||
let toks = [
|
||||
open(QN_P_SYM, SSYM, Depth(0)),
|
||||
Xirf::Attr(Attr(QN_NAME, name, AttrSpan(S2, S3))),
|
||||
Xirf::Attr(Attr(QN_TYPE, raw::L_MAP, AttrSpan(S3, S4))),
|
||||
// <preproc:from>
|
||||
open(QN_P_FROM, S2, Depth(1)),
|
||||
Xirf::Attr(Attr(QN_NAME, "ok".into(), AttrSpan(S2, S3))),
|
||||
close_empty(S4, Depth(1)),
|
||||
// />
|
||||
// <preproc:from> again (err)
|
||||
open(QN_P_FROM, S3, Depth(1)),
|
||||
Xirf::Attr(Attr(QN_NAME, "bad".into(), AttrSpan(S2, S3))),
|
||||
close_empty(S4, Depth(1)),
|
||||
// />
|
||||
attr(QN_NAME, name, (S2, S3)),
|
||||
attr(QN_TYPE, raw::L_MAP, (S3, S4)),
|
||||
|
||||
// <preproc:from>
|
||||
open(QN_P_FROM, S2, Depth(1)),
|
||||
attr(QN_NAME, "ok".into(), (S2, S3)),
|
||||
close_empty(S4, Depth(1)),
|
||||
// />
|
||||
|
||||
// <preproc:from> again (err)
|
||||
open(QN_P_FROM, S3, Depth(1)),
|
||||
attr(QN_NAME, "bad".into(), (S2, S3)),
|
||||
close_empty(S4, Depth(1)),
|
||||
// />
|
||||
close(Some(QN_P_SYM), S2, Depth(0)),
|
||||
]
|
||||
.into_iter();
|
||||
|
@ -454,34 +487,37 @@ fn sym_dep_event() {
|
|||
let dep1 = "dep1".into();
|
||||
let dep2 = "dep2".into();
|
||||
|
||||
#[rustfmt::skip]
|
||||
let toks = [
|
||||
open(QN_P_SYM_DEP, S1, Depth(0)),
|
||||
Xirf::Attr(Attr(QN_NAME, name, AttrSpan(S2, S3))),
|
||||
// <preproc:sym-ref
|
||||
open(QN_P_SYM_REF, S2, Depth(1)),
|
||||
Xirf::Attr(Attr(QN_NAME, dep1, AttrSpan(S3, S4))),
|
||||
close_empty(S4, Depth(1)),
|
||||
// />
|
||||
// <preproc:sym-ref
|
||||
open(QN_P_SYM_REF, S3, Depth(1)),
|
||||
Xirf::Attr(Attr(QN_NAME, dep2, AttrSpan(S4, S5))),
|
||||
close_empty(S4, Depth(1)),
|
||||
// />
|
||||
attr(QN_NAME, name, (S2, S3)),
|
||||
|
||||
// <preproc:sym-ref
|
||||
open(QN_P_SYM_REF, S2, Depth(1)),
|
||||
attr(QN_NAME, dep1, (S3, S4)),
|
||||
close_empty(S4, Depth(1)),
|
||||
// />
|
||||
|
||||
// <preproc:sym-ref
|
||||
open(QN_P_SYM_REF, S3, Depth(1)),
|
||||
attr(QN_NAME, dep2, (S4, S5)),
|
||||
close_empty(S4, Depth(1)),
|
||||
// />
|
||||
close(Some(QN_P_SYM_DEP), S5, Depth(0)),
|
||||
]
|
||||
.into_iter();
|
||||
|
||||
assert_eq!(
|
||||
Ok(vec![
|
||||
Parsed::Incomplete, // <preproc:sym-ref
|
||||
Parsed::Object(XmloToken::SymDepStart(SPair(name, S1))), // @name
|
||||
Parsed::Incomplete, // <preproc:sym-ref
|
||||
Parsed::Object(XmloToken::Symbol(SPair(dep1, S4))), // @name
|
||||
Parsed::Incomplete, // />
|
||||
Parsed::Incomplete, // <preproc:sym-ref
|
||||
Parsed::Object(XmloToken::Symbol(SPair(dep2, S5))), // @name
|
||||
Parsed::Incomplete, // />
|
||||
Parsed::Incomplete, // </preproc:sym-dep>
|
||||
Incomplete, // <preproc:sym-ref
|
||||
O(SymDepStart(SPair(name, S1))), // @name
|
||||
Incomplete, // <preproc:sym-ref
|
||||
O(Symbol(SPair(dep1, S4))), // @name
|
||||
Incomplete, // />
|
||||
Incomplete, // <preproc:sym-ref
|
||||
O(Symbol(SPair(dep2, S5))), // @name
|
||||
Incomplete, // />
|
||||
Incomplete, // </preproc:sym-dep>
|
||||
]),
|
||||
SymDepsState::parse(toks).collect()
|
||||
);
|
||||
|
@ -507,12 +543,14 @@ fn sym_dep_missing_name() {
|
|||
fn sym_ref_missing_name() {
|
||||
let name = "depsym".into();
|
||||
|
||||
#[rustfmt::skip]
|
||||
let toks = [
|
||||
open(QN_P_SYM_DEP, S1, Depth(0)),
|
||||
Xirf::Attr(Attr(QN_NAME, name, AttrSpan(S2, S3))),
|
||||
open(QN_P_SYM_REF, S2, Depth(1)),
|
||||
// missing @name, causes error
|
||||
close_empty(S3, Depth(1)),
|
||||
attr(QN_NAME, name, (S2, S3)),
|
||||
|
||||
open(QN_P_SYM_REF, S2, Depth(1)),
|
||||
// missing @name, causes error
|
||||
close_empty(S3, Depth(1)),
|
||||
]
|
||||
.into_iter();
|
||||
|
||||
|
@ -530,30 +568,32 @@ fn sym_fragment_event() {
|
|||
let frag1 = "fragment text 1".into();
|
||||
let frag2 = "fragment text 2".into();
|
||||
|
||||
#[rustfmt::skip]
|
||||
let toks = [
|
||||
// first
|
||||
open(QN_P_FRAGMENT, S1, Depth(0)),
|
||||
Xirf::Attr(Attr(QN_ID, id1, AttrSpan(S2, S3))),
|
||||
Xirf::Text(Text(frag1, S4), Depth(1)),
|
||||
attr(QN_ID, id1, (S2, S3)),
|
||||
Xirf::Text(Text(frag1, S4), Depth(1)),
|
||||
close(Some(QN_P_FRAGMENT), S5, Depth(0)),
|
||||
|
||||
// second
|
||||
open(QN_P_FRAGMENT, S2, Depth(0)),
|
||||
Xirf::Attr(Attr(QN_ID, id2, AttrSpan(S3, S4))),
|
||||
Xirf::Text(Text(frag2, S5), Depth(1)),
|
||||
attr(QN_ID, id2, (S3, S4)),
|
||||
Xirf::Text(Text(frag2, S5), Depth(1)),
|
||||
close(Some(QN_P_FRAGMENT), S5, Depth(0)),
|
||||
]
|
||||
.into_iter();
|
||||
|
||||
assert_eq!(
|
||||
Ok(vec![
|
||||
Parsed::Incomplete, // <preproc:fragment
|
||||
Parsed::Incomplete, // @id
|
||||
Parsed::Object(XmloToken::Fragment(SPair(id1, S1), frag1)), // text
|
||||
Parsed::Incomplete, // </preproc:fragment>
|
||||
Parsed::Incomplete, // <preproc:fragment
|
||||
Parsed::Incomplete, // @id
|
||||
Parsed::Object(XmloToken::Fragment(SPair(id2, S2), frag2)), // text
|
||||
Parsed::Incomplete, // </preproc:fragment>
|
||||
Incomplete, // <preproc:fragment
|
||||
Incomplete, // @id
|
||||
O(Fragment(SPair(id1, S1), frag1)), // text
|
||||
Incomplete, // </preproc:fragment>
|
||||
Incomplete, // <preproc:fragment
|
||||
Incomplete, // @id
|
||||
O(Fragment(SPair(id2, S2), frag2)), // text
|
||||
Incomplete, // </preproc:fragment>
|
||||
]),
|
||||
FragmentsState::parse(toks).collect()
|
||||
);
|
||||
|
@ -581,7 +621,7 @@ fn sym_fragment_empty_id() {
|
|||
let toks = [
|
||||
open(QN_P_FRAGMENT, S1, Depth(0)),
|
||||
// empty @id
|
||||
Xirf::Attr(Attr(QN_ID, "".into(), AttrSpan(S3, S4))),
|
||||
attr(QN_ID, "".into(), (S3, S4)),
|
||||
Xirf::Text(Text("text".into(), S4), Depth(1)),
|
||||
]
|
||||
.into_iter();
|
||||
|
@ -601,7 +641,7 @@ fn _sym_fragment_missing_text() {
|
|||
|
||||
let toks = [
|
||||
open(QN_P_FRAGMENT, S1, Depth(0)),
|
||||
Xirf::Attr(Attr(QN_ID, id, AttrSpan(S3, S4))),
|
||||
attr(QN_ID, id, (S3, S4)),
|
||||
// missing text
|
||||
close(Some(QN_P_FRAGMENT), S5, Depth(0)),
|
||||
]
|
||||
|
@ -627,55 +667,45 @@ fn xmlo_composite_parsers_header() {
|
|||
let symfrag_id = "symfrag".into();
|
||||
let frag = "fragment text".into();
|
||||
|
||||
#[rustfmt::skip]
|
||||
let toks_header = [
|
||||
open(QN_PACKAGE, S1, Depth(0)),
|
||||
// <preproc:symtable>
|
||||
open(QN_P_SYMTABLE, S2, Depth(1)),
|
||||
// <preproc:sym
|
||||
open(QN_P_SYM, S3, Depth(2)),
|
||||
Xirf::Attr(Attr(QN_NAME, sym_name, AttrSpan(S2, S3))),
|
||||
close_empty(S4, Depth(2)),
|
||||
// />
|
||||
close(Some(QN_P_SYMTABLE), S4, Depth(1)),
|
||||
// </preproc:symtable>
|
||||
// <preproc:sym-deps>
|
||||
open(QN_P_SYM_DEPS, S2, Depth(1)),
|
||||
// <preproc:sym-dep
|
||||
open(QN_P_SYM_DEP, S3, Depth(3)),
|
||||
Xirf::Attr(Attr(QN_NAME, symdep_name, AttrSpan(S2, S3))),
|
||||
close(Some(QN_P_SYM_DEP), S4, Depth(3)),
|
||||
// </preproc:sym-dep>
|
||||
close(Some(QN_P_SYM_DEPS), S3, Depth(1)),
|
||||
// </preproc:sym-deps>
|
||||
// <preproc:fragments>
|
||||
open(QN_P_FRAGMENTS, S2, Depth(1)),
|
||||
// <preproc:fragment
|
||||
open(QN_P_FRAGMENT, S4, Depth(2)),
|
||||
Xirf::Attr(Attr(QN_ID, symfrag_id, AttrSpan(S2, S3))),
|
||||
Xirf::Text(Text(frag, S5), Depth(3)),
|
||||
close(Some(QN_P_FRAGMENT), S4, Depth(2)),
|
||||
// </preproc:fragment>
|
||||
close(Some(QN_P_FRAGMENTS), S3, Depth(1)),
|
||||
// </preproc:fragments>
|
||||
// No closing root node:
|
||||
// ensure that we can just end at the header without parsing further).
|
||||
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)),
|
||||
|
||||
open(QN_P_FRAGMENTS, S2, Depth(1)),
|
||||
open(QN_P_FRAGMENT, S4, Depth(2)),
|
||||
attr(QN_ID, symfrag_id, (S2, S3)),
|
||||
Xirf::Text(Text(frag, S5), Depth(3)),
|
||||
close(Some(QN_P_FRAGMENT), S4, Depth(2)),
|
||||
close(Some(QN_P_FRAGMENTS), S3, Depth(1)),
|
||||
// No closing root node:
|
||||
// ensure that we can just end at the header without parsing further).
|
||||
]
|
||||
.into_iter();
|
||||
|
||||
let sut = Sut::parse(toks_header);
|
||||
|
||||
#[rustfmt::skip]
|
||||
assert_eq!(
|
||||
Ok(vec![
|
||||
Parsed::Object(XmloToken::SymDecl(
|
||||
SPair(sym_name, S3),
|
||||
Default::default(),
|
||||
)),
|
||||
Parsed::Object(XmloToken::SymDepStart(SPair(symdep_name, S3))),
|
||||
Parsed::Object(XmloToken::Fragment(SPair(symfrag_id, S4), frag)),
|
||||
Parsed::Object(XmloToken::Eoh(S3)),
|
||||
O(PkgStart(S1)),
|
||||
O(SymDecl(SPair(sym_name, S3), Default::default(),)),
|
||||
O(SymDepStart(SPair(symdep_name, S3))),
|
||||
O(Fragment(SPair(symfrag_id, S4), frag)),
|
||||
O(Eoh(S3)),
|
||||
]),
|
||||
sut.filter(|parsed| match parsed {
|
||||
Ok(Parsed::Incomplete) => false,
|
||||
Ok(Incomplete) => false,
|
||||
_ => true,
|
||||
})
|
||||
.collect(),
|
||||
|
|
|
@ -30,12 +30,15 @@ mod trace;
|
|||
pub mod util;
|
||||
|
||||
pub use error::{FinalizeError, ParseError};
|
||||
pub use lower::{lowerable, terminal, Lower, LowerIter, ParsedObject};
|
||||
pub use lower::{
|
||||
lowerable, terminal, FromParseError, Lower, LowerIter, LowerSource,
|
||||
ParsedObject,
|
||||
};
|
||||
pub use parser::{FinalizedParser, Parsed, ParsedResult, Parser};
|
||||
pub use state::{
|
||||
context::{Context, Empty as EmptyContext, NoContext},
|
||||
ClosedParseState, ParseResult, ParseState, ParseStatus, Transition,
|
||||
TransitionResult, Transitionable,
|
||||
ClosedParseState, ParseResult, ParseState, ParseStatus, StateStack,
|
||||
Transition, TransitionResult, Transitionable,
|
||||
};
|
||||
|
||||
use crate::span::{Span, UNKNOWN_SPAN};
|
||||
|
|
|
@ -107,7 +107,9 @@ impl<T: Token, E: Diagnostic + PartialEq> Display for ParseError<T, E> {
|
|||
}
|
||||
}
|
||||
|
||||
impl<T: Token, E: Diagnostic + PartialEq + 'static> Error for ParseError<T, E> {
|
||||
impl<T: Token, E: Diagnostic + Error + PartialEq + 'static> Error
|
||||
for ParseError<T, E>
|
||||
{
|
||||
fn source(&self) -> Option<&(dyn Error + 'static)> {
|
||||
match self {
|
||||
Self::UnexpectedToken(_, _) => None,
|
||||
|
|
|
@ -138,9 +138,9 @@ where
|
|||
#[inline]
|
||||
fn lower_with_context<U, E>(
|
||||
&mut self,
|
||||
ctx: impl Into<LS::Context>,
|
||||
ctx: impl Into<LS::PubContext>,
|
||||
f: impl FnOnce(&mut LowerIter<S, Self, LS, EW>) -> Result<U, E>,
|
||||
) -> Result<(U, LS::Context), E>
|
||||
) -> Result<(U, LS::PubContext), E>
|
||||
where
|
||||
Self: Iterator<Item = WidenedParsedResult<S, EW>> + Sized,
|
||||
E: Diagnostic + From<FinalizeError>,
|
||||
|
@ -239,10 +239,24 @@ 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>>;
|
||||
|
||||
/// Convenience trait for converting [`From`] a [`ParseError`] for the
|
||||
/// provided [`ParseState`] `S`.
|
||||
///
|
||||
/// This allows specifying this type in terms of only the [`ParseState`]
|
||||
/// 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>>;
|
||||
|
||||
/// A [`ParsedResult`](super::ParsedResult) with a [`WidenedError`].
|
||||
pub type WidenedParsedResult<S, E> =
|
||||
Result<Parsed<<S as ParseState>::Object>, E>;
|
||||
|
||||
/// A source of a lowering operation.
|
||||
pub trait LowerSource<T: Token, O: Object, E: Diagnostic + PartialEq> =
|
||||
Iterator<Item = ParsedResult<ParsedObject<T, O, E>>>;
|
||||
|
||||
/// Make the provided [`Iterator`] `iter` usable in a `Lower` pipeline.
|
||||
///
|
||||
/// This will produce an iterator that shares the same output as a
|
||||
|
@ -255,7 +269,7 @@ pub type WidenedParsedResult<S, E> =
|
|||
/// This is the dual of [`terminal`].
|
||||
pub fn lowerable<T: Token, O: Object, E: Diagnostic + PartialEq>(
|
||||
iter: impl Iterator<Item = Result<O, E>>,
|
||||
) -> impl Iterator<Item = ParsedResult<ParsedObject<T, O, E>>> {
|
||||
) -> impl LowerSource<T, O, E> {
|
||||
iter.map(|result| {
|
||||
result.map(Parsed::Object).map_err(ParseError::StateError)
|
||||
})
|
||||
|
|
|
@ -496,10 +496,27 @@ where
|
|||
pub struct FinalizedParser<S: ParseState>(S::Context);
|
||||
|
||||
impl<S: ParseState> FinalizedParser<S> {
|
||||
/// Take ownership over the inner [`ParseState::Context`].
|
||||
pub fn into_context(self) -> S::Context {
|
||||
/// Take ownership over the inner [`ParseState::Context`],
|
||||
/// converting it into its public representation.
|
||||
///
|
||||
/// This represents,
|
||||
/// in essence,
|
||||
/// a final aggregate return value of the parser.
|
||||
pub fn into_context(self) -> S::PubContext {
|
||||
match self {
|
||||
Self(ctx) => ctx,
|
||||
Self(ctx) => ctx.into(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Take ownership over the inner [`ParseState::Context`].
|
||||
///
|
||||
/// It is expected that `S::Context` is not suitable for consumption
|
||||
/// outside of the module that defined `S`.
|
||||
/// [`Self::into_context`] should be used as a public API.
|
||||
#[cfg(test)]
|
||||
pub fn into_private_context(self) -> S::Context {
|
||||
match self {
|
||||
Self(ctx) => ctx.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,7 +22,7 @@
|
|||
mod transition;
|
||||
|
||||
use super::{Object, ParseError, Parser, Token, TokenStream};
|
||||
use crate::diagnose::Diagnostic;
|
||||
use crate::{diagnose::Diagnostic, diagnostic_panic};
|
||||
use std::fmt::{Debug, Display};
|
||||
pub use transition::*;
|
||||
|
||||
|
@ -129,7 +129,7 @@ where
|
|||
Self::Error: Into<<Self::Super as ParseState>::Error>,
|
||||
{
|
||||
/// Input tokens to the parser.
|
||||
type Token: Token;
|
||||
type Token: Token + Into<<Self::Super as ParseState>::Token>;
|
||||
|
||||
/// Objects produced by a parser utilizing these states.
|
||||
type Object: Object;
|
||||
|
@ -156,11 +156,8 @@ where
|
|||
/// This is the same concept as [`StitchableParseState`],
|
||||
/// but operating in reverse
|
||||
/// (delegation via trampoline instead of direct function call).
|
||||
type Super: ClosedParseState<
|
||||
Token = Self::Token,
|
||||
Object = Self::Object,
|
||||
Context = Self::Context,
|
||||
> = Self;
|
||||
type Super: ClosedParseState<Object = Self::Object, Context = Self::Context> =
|
||||
Self;
|
||||
|
||||
/// Object provided to parser alongside each token.
|
||||
///
|
||||
|
@ -169,6 +166,16 @@ where
|
|||
/// otherwise-immutable [`ParseState`].
|
||||
type Context: Debug = context::Empty;
|
||||
|
||||
/// Public representation of [`Self::Context`].
|
||||
///
|
||||
/// If [`Self::Context`] holds internal state that isn't intended to be
|
||||
/// exposed via a public API,
|
||||
/// this represents the type that should be used in its place.
|
||||
/// Since public APIs include both input and output,
|
||||
/// this type must be convertable to _and_ from [`Self::Context`].
|
||||
type PubContext: Debug + From<Self::Context> + Into<Self::Context> =
|
||||
Self::Context;
|
||||
|
||||
/// Construct a parser with a [`Default`] state.
|
||||
///
|
||||
/// Whether this method is helpful or provides any clarity depends on
|
||||
|
@ -198,12 +205,12 @@ where
|
|||
/// see [`Parser::finalize`].
|
||||
fn parse_with_context<I: TokenStream<Self::Token>>(
|
||||
toks: I,
|
||||
ctx: Self::Context,
|
||||
ctx: Self::PubContext,
|
||||
) -> Parser<Self, I>
|
||||
where
|
||||
Self: ClosedParseState + Default,
|
||||
{
|
||||
Parser::from((toks, ctx))
|
||||
Parser::from((toks, ctx.into()))
|
||||
}
|
||||
|
||||
/// Parse a single [`Token`] and optionally perform a state transition.
|
||||
|
@ -349,7 +356,7 @@ where
|
|||
mut context: C,
|
||||
dead: impl FnOnce(
|
||||
Self::Super,
|
||||
Self::Token,
|
||||
<Self::Super as ParseState>::Token,
|
||||
C,
|
||||
) -> TransitionResult<Self::Super>,
|
||||
) -> TransitionResult<Self::Super>
|
||||
|
@ -450,7 +457,7 @@ where
|
|||
into(newst, Some(obj), env),
|
||||
TransitionData::Result(
|
||||
Ok(Incomplete),
|
||||
lookahead.map(Lookahead::inner_into),
|
||||
lookahead.map(Lookahead::into_super::<SP>),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
@ -462,7 +469,7 @@ where
|
|||
Ok(_) => Ok(Incomplete),
|
||||
Err(e) => Err(e.into()),
|
||||
},
|
||||
lookahead.map(Lookahead::inner_into),
|
||||
lookahead.map(Lookahead::into_super::<SP>),
|
||||
),
|
||||
),
|
||||
}
|
||||
|
@ -501,7 +508,8 @@ pub trait StitchableParseState<SP: ParseState> =
|
|||
|
||||
pub trait PartiallyStitchableParseState<SP: ParseState> = ClosedParseState
|
||||
where
|
||||
<SP as ParseState>::Token: From<<Self as ParseState>::Token>,
|
||||
<<SP as ParseState>::Super as ParseState>::Token:
|
||||
From<<<Self as ParseState>::Super as ParseState>::Token>,
|
||||
<Self as ParseState>::Error: Into<<SP as ParseState>::Error>;
|
||||
|
||||
pub mod context {
|
||||
|
@ -616,3 +624,151 @@ pub mod context {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Parser stack for trampoline.
|
||||
///
|
||||
/// This can be used as a call stack for parsers while avoiding creating
|
||||
/// otherwise-recursive data structures with composition-based delegation.
|
||||
/// However,
|
||||
/// it is more similar to CPS,
|
||||
/// in that the parser popped off the stack need not be the parser that
|
||||
/// initiated the request and merely represents the next step in
|
||||
/// a delayed computation.
|
||||
/// If such a return context is unneeded,
|
||||
/// a [`ParseState`] may implement tail calls by simply not pushing itself
|
||||
/// onto the stack before requesting transfer to another [`ParseState`].
|
||||
#[derive(Debug, Default)]
|
||||
pub struct StateStack<S: ClosedParseState, const MAX_DEPTH: usize>(Vec<S>);
|
||||
|
||||
// Note that public visibility is needed because `ele_parse` expands outside
|
||||
// of this module.
|
||||
impl<S: ClosedParseState, const MAX_DEPTH: usize> StateStack<S, MAX_DEPTH> {
|
||||
/// Request a transfer to another [`ParseState`],
|
||||
/// expecting that control be returned to `ret` after it has
|
||||
/// completed.
|
||||
///
|
||||
/// This can be reasoned about like calling a thunk:
|
||||
/// the return [`ParseState`] is put onto the stack,
|
||||
/// the target [`ParseState`] is used for the state transition to
|
||||
/// cause [`Parser`] to perform the call to it,
|
||||
/// and when it is done
|
||||
/// (e.g. a dead state),
|
||||
/// `ret` will be pop'd from the stack and we'll transition back to
|
||||
/// it.
|
||||
/// Note that this method is not responsible for returning;
|
||||
/// see [`Self::ret_or_dead`] to perform a return.
|
||||
///
|
||||
/// However,
|
||||
/// the calling [`ParseState`] is not responsible for its return,
|
||||
/// unlike a typical function call.
|
||||
/// Instead,
|
||||
/// this _actually_ more closely resembles CPS
|
||||
/// (continuation passing style),
|
||||
/// and so the caller must be careful to ensure that stack
|
||||
/// operations are properly paired.
|
||||
/// On the upside,
|
||||
/// if something is erroneously `ret`'d,
|
||||
/// the parser is guaranteed to be in a consistent state since the
|
||||
/// entire state has been reified
|
||||
/// (but the input would then be parsed incorrectly).
|
||||
///
|
||||
/// Note that tail calls can be implemented by transferring control
|
||||
/// without pushing an entry on the stack to return to,
|
||||
/// but that hasn't been formalized \[yet\] and requires extra care.
|
||||
pub fn transfer_with_ret<SA, ST>(
|
||||
&mut self,
|
||||
Transition(ret): Transition<SA>,
|
||||
target: TransitionResult<ST>,
|
||||
) -> TransitionResult<ST>
|
||||
where
|
||||
SA: ParseState<Super = S::Super>,
|
||||
ST: ParseState,
|
||||
{
|
||||
let Self(stack) = self;
|
||||
|
||||
if stack.len() == MAX_DEPTH {
|
||||
// TODO: We need some spans here and ideally convert the
|
||||
// parenthetical error message into a diagnostic footnote.
|
||||
// TODO: Or should we have a special error type that tells the
|
||||
// parent `Parser` to panic with context?
|
||||
diagnostic_panic!(
|
||||
vec![],
|
||||
"maximum parsing depth of {} exceeded while attempting \
|
||||
to push return state: {ret}",
|
||||
MAX_DEPTH,
|
||||
);
|
||||
}
|
||||
|
||||
stack.push(ret.into());
|
||||
target
|
||||
}
|
||||
|
||||
/// Attempt to return to a previous [`ParseState`] that transferred
|
||||
/// control away from itself,
|
||||
/// otherwise yield a dead state transition to `deadst`.
|
||||
///
|
||||
/// Conceptually,
|
||||
/// this is like returning from a function call,
|
||||
/// where the function was invoked using [`Self::transfer_with_ret`].
|
||||
/// However,
|
||||
/// this system is more akin to CPS
|
||||
/// (continuation passing style);
|
||||
/// see [`Self::transfer_with_ret`] for important information.
|
||||
///
|
||||
/// If there is no state to return to on the stack,
|
||||
/// then it is assumed that we have received more input than expected
|
||||
/// after having completed a full parse.
|
||||
///
|
||||
/// If a missing state is _not_ an error condition,
|
||||
/// see [`Self::continue_or_init`] instead.
|
||||
pub fn ret_or_dead(
|
||||
&mut self,
|
||||
deadst: S,
|
||||
lookahead: impl Token + Into<S::Token>,
|
||||
) -> TransitionResult<S> {
|
||||
let Self(stack) = self;
|
||||
|
||||
// This should certainly never happen unless there is a bug in the
|
||||
// parser,
|
||||
// since it means that we're trying to return to a caller that
|
||||
// does not exist.
|
||||
match stack.pop() {
|
||||
Some(st) => Transition(st).incomplete().with_lookahead(lookahead),
|
||||
None => Transition(deadst).dead(lookahead),
|
||||
}
|
||||
}
|
||||
|
||||
/// Attempt to resume a computation atop of the stack,
|
||||
/// or initialize with a new [`ParseState`] if the stack is empty.
|
||||
///
|
||||
/// This can be thought of like invoking a stored continuation,
|
||||
/// as if with `call-with-current-continuation` in Scheme.
|
||||
/// It is fully the responsibility of the caller to ensure that all
|
||||
/// necessary state is captured or is otherwise able to be restored in
|
||||
/// such a way that the computation can be resumed.
|
||||
///
|
||||
/// If a missing state is an error condition,
|
||||
/// see [`Self::ret_or_dead`] instead.
|
||||
pub fn continue_or_init(
|
||||
&mut self,
|
||||
init: impl FnOnce() -> S,
|
||||
lookahead: impl Token + Into<S::Token>,
|
||||
) -> TransitionResult<S> {
|
||||
let Self(stack) = self;
|
||||
|
||||
let st = stack.pop().unwrap_or_else(init);
|
||||
Transition(st).incomplete().with_lookahead(lookahead)
|
||||
}
|
||||
|
||||
/// Iterate through each [`ClosedParseState`] held on the stack.
|
||||
pub fn iter(&self) -> std::slice::Iter<'_, S> {
|
||||
let Self(stack) = self;
|
||||
stack[..].iter()
|
||||
}
|
||||
|
||||
/// Number of [`ClosedParseState`]s held on the stack.
|
||||
pub fn len(&self) -> usize {
|
||||
let Self(stack) = self;
|
||||
stack.len()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -206,13 +206,13 @@ impl<S: ParseState> TransitionResult<S> {
|
|||
S: PartiallyStitchableParseState<SB>,
|
||||
{
|
||||
self.branch_dead_la(
|
||||
|st, Lookahead(la), bctx| {
|
||||
|st, la, bctx| {
|
||||
fdead(st, bctx)
|
||||
.with_lookahead(<SB as ParseState>::Token::from(la))
|
||||
.maybe_with_lookahead(Some(la.into_super::<SB>()))
|
||||
},
|
||||
|st, result, la, bctx| {
|
||||
falive(st, result, bctx)
|
||||
.maybe_with_lookahead(la.map(Lookahead::inner_into))
|
||||
.maybe_with_lookahead(la.map(Lookahead::into_super::<SB>))
|
||||
},
|
||||
bctx,
|
||||
)
|
||||
|
@ -257,42 +257,6 @@ impl<S: ParseState> TransitionResult<S> {
|
|||
Result(result, la) => falive(st, result, la, bctx),
|
||||
}
|
||||
}
|
||||
|
||||
/// Conditionally map to a [`TransitionResult`] based on whether the
|
||||
/// inner [`TransitionData`] represents an object.
|
||||
pub(in super::super) fn branch_obj_la<SB: ParseState>(
|
||||
self,
|
||||
fobj: impl FnOnce(
|
||||
Transition<S>,
|
||||
<S as ParseState>::Object,
|
||||
Option<Lookahead<<S as ParseState>::Token>>,
|
||||
) -> TransitionResult<<SB as ParseState>::Super>,
|
||||
fother: impl FnOnce(Transition<S>) -> Transition<SB>,
|
||||
) -> TransitionResult<<SB as ParseState>::Super>
|
||||
where
|
||||
S: PartiallyStitchableParseState<SB>,
|
||||
{
|
||||
use ParseStatus::{Incomplete, Object};
|
||||
use TransitionData::{Dead, Result};
|
||||
|
||||
let Self(st, data) = self;
|
||||
|
||||
match data {
|
||||
Result(Ok(Object(obj)), la) => fobj(st, obj, la).into_super(),
|
||||
|
||||
// Can't use `TransitionData::inner_into` since we only have a
|
||||
// `PartiallyStitchableParseState`,
|
||||
// and `into_inner` requires being able to convert the inner
|
||||
// object that we handled above.
|
||||
Result(Ok(Incomplete), la) => fother(st)
|
||||
.incomplete()
|
||||
.maybe_with_lookahead(la.map(Lookahead::inner_into)),
|
||||
Result(Err(e), la) => fother(st)
|
||||
.err(e)
|
||||
.maybe_with_lookahead(la.map(Lookahead::inner_into)),
|
||||
Dead(Lookahead(la)) => fother(st).dead(la.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Token to use as a lookahead token in place of the next token from the
|
||||
|
@ -330,6 +294,22 @@ impl<T: Token> Lookahead<T> {
|
|||
Self(tok) => Lookahead(tok.into()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert the inner [`Token`] of lookahead into the token expected by
|
||||
/// the superstate [`S::Super`](ParseState::Super).
|
||||
///
|
||||
/// This simply sets strict trait bounds to serve as a checkpoint where
|
||||
/// we know for certain what types are involved;
|
||||
/// there's a whole lot of types involved in the parsing framework
|
||||
/// and it gets very difficult to understand when errors occur.
|
||||
pub fn into_super<S: ParseState>(
|
||||
self,
|
||||
) -> Lookahead<<S::Super as ParseState>::Token>
|
||||
where
|
||||
T: Into<<S::Super as ParseState>::Token>,
|
||||
{
|
||||
self.inner_into::<<S::Super as ParseState>::Token>()
|
||||
}
|
||||
}
|
||||
|
||||
/// Information about the state transition.
|
||||
|
@ -376,9 +356,9 @@ impl<S: ParseState> TransitionData<S> {
|
|||
match self {
|
||||
Self::Result(st_result, ola) => TransitionData::Result(
|
||||
st_result.map(ParseStatus::into_super).map_err(|e| e.into()),
|
||||
ola,
|
||||
ola.map(Lookahead::inner_into),
|
||||
),
|
||||
Self::Dead(la) => TransitionData::Dead(la),
|
||||
Self::Dead(la) => TransitionData::Dead(la.inner_into()),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -474,7 +454,7 @@ impl<S: ParseState> TransitionData<S> {
|
|||
use TransitionData::*;
|
||||
|
||||
match self {
|
||||
Dead(la) => Dead(la.inner_into()),
|
||||
Dead(la) => Dead(la.into_super::<SB>()),
|
||||
Result(result, la) => Result(
|
||||
match result {
|
||||
Ok(status) => Ok(status.inner_into()),
|
||||
|
@ -483,7 +463,7 @@ impl<S: ParseState> TransitionData<S> {
|
|||
// (which will be the same type if SB is closed).
|
||||
Err(e) => Err(e.into().into()),
|
||||
},
|
||||
la.map(Lookahead::inner_into),
|
||||
la.map(Lookahead::into_super::<SB>),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
@ -603,10 +583,13 @@ impl<S: ParseState> Transition<S> {
|
|||
/// object first,
|
||||
/// use [`Transition::result`] or other methods along with a token
|
||||
/// of [`Lookahead`].
|
||||
pub fn dead(self, tok: S::Token) -> TransitionResult<S::Super> {
|
||||
pub fn dead<T: Token + Into<<S::Super as ParseState>::Token>>(
|
||||
self,
|
||||
tok: T,
|
||||
) -> TransitionResult<S::Super> {
|
||||
TransitionResult(
|
||||
self.into_super(),
|
||||
TransitionData::Dead(Lookahead(tok)),
|
||||
TransitionData::Dead(Lookahead(tok).into_super::<S>()),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -25,8 +25,6 @@
|
|||
//! they provide wrappers around core functionality that make it easier
|
||||
//! to use outside of the domain of the parsing system itself.
|
||||
|
||||
pub mod expand;
|
||||
|
||||
use super::{prelude::*, state::TransitionData};
|
||||
use crate::{f::Functor, span::Span, sym::SymbolId};
|
||||
use std::fmt::Display;
|
||||
|
|
|
@ -1,145 +0,0 @@
|
|||
// TAMER parsing framework utilities for token expansion
|
||||
//
|
||||
// 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/>.
|
||||
|
||||
//! Token expansion utilities.
|
||||
//!
|
||||
//! _Expansion_ refers to the production of many [`Object`]s that are
|
||||
//! derived from a single [`Token`].
|
||||
//! An [`ExpandableParseState`] is a [`ClosedParseState`] that,
|
||||
//! provided a [`Token`],
|
||||
//! produces an [`Expansion`] of zero or more [`Expansion::Expanded`]
|
||||
//! [`Object`]s before terminating with a [`Expansion::DoneExpanding`]
|
||||
//! [`Token`] intended to replace the originally provided [`Token`].
|
||||
//!
|
||||
//! An [`ExpandableParseState`] can be stitched with a parent parser using
|
||||
//! [`StitchExpansion`],
|
||||
//! giving the perception of expanding into that parent's token stream.
|
||||
|
||||
use super::super::{prelude::*, state::Lookahead};
|
||||
use crate::{
|
||||
diagnose::{panic::DiagnosticOptionPanic, Annotate},
|
||||
parse::state::PartiallyStitchableParseState,
|
||||
};
|
||||
|
||||
/// Represents an expansion operation on some source token of type `T`.
|
||||
///
|
||||
/// See variants and [`ExpandableParseState`] for more information.
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub enum Expansion<T, O: Object> {
|
||||
/// A token of type `O` has been derived from the source token and
|
||||
/// should be merged into the target token stream.
|
||||
Expanded(O),
|
||||
|
||||
/// Expansion is complete and the source token should be replaced with
|
||||
/// the inner `T`.
|
||||
///
|
||||
/// Since the expectation is that the parser has completed parsing and
|
||||
/// no longer requires the token provided to it,
|
||||
/// the parser yielding this variant _must not_ yield a token of
|
||||
/// lookahead,
|
||||
/// otherwise the system assume that the parser has an
|
||||
/// implementation defect (bug) and will be forced to panic rather
|
||||
/// than discard it.
|
||||
DoneExpanding(T),
|
||||
}
|
||||
|
||||
impl<T: Token, O: Object> Object for Expansion<T, O> {}
|
||||
|
||||
/// A [`ClosedParseState`] that is able to serve as an expansion parser.
|
||||
///
|
||||
/// An expansion parser is a parser yielding [`Expansion`],
|
||||
/// intended to be integrated into another token stream.
|
||||
pub trait ExpandableParseState<O: Object> = ClosedParseState
|
||||
where
|
||||
O: Token + Eq,
|
||||
Self: ParseState<Object = Expansion<<Self as ParseState>::Token, O>>;
|
||||
|
||||
/// An [`ExpandableParseState`] capable of expanding into the token stream
|
||||
/// of a parent [`ParseState`] `SP`.
|
||||
pub trait ExpandableInto<SP: ParseState> =
|
||||
ExpandableParseState<<SP as ParseState>::Object>
|
||||
where
|
||||
Self: ExpandableParseState<<SP as ParseState>::Object>
|
||||
+ PartiallyStitchableParseState<SP>;
|
||||
|
||||
/// [`ExpandableParseState`] state stitching.
|
||||
///
|
||||
/// See [`Self::stitch_expansion`] for more information.
|
||||
pub trait StitchExpansion: ClosedParseState {
|
||||
/// Stitch a [`ExpandableParseState`] that is
|
||||
/// [`ExpandableInto<SP>`](ExpandableInto).
|
||||
///
|
||||
/// This combines the state machine of an [`ExpandableParseState`],
|
||||
/// allowing that parser to expand into the token stream of [`Self`].
|
||||
///
|
||||
/// Panics
|
||||
/// ======
|
||||
/// This will panic with diagnostic information if a token of lookahead
|
||||
/// is provided with a [`Expansion::DoneExpanding`] variant.
|
||||
/// See that variant for more information.
|
||||
fn stitch_expansion<SP: ParseState, C>(
|
||||
self,
|
||||
tok: <Self as ParseState>::Token,
|
||||
mut ctx: C,
|
||||
into: impl Fn(Transition<Self>) -> Transition<SP>,
|
||||
done: impl FnOnce(
|
||||
Transition<Self>,
|
||||
<SP as ParseState>::Token,
|
||||
) -> TransitionResult<SP>,
|
||||
) -> TransitionResult<<SP as ParseState>::Super>
|
||||
where
|
||||
Self: ExpandableInto<SP>,
|
||||
C: AsMut<<Self as ParseState>::Context>,
|
||||
{
|
||||
use Expansion::{DoneExpanding, Expanded};
|
||||
|
||||
self.parse_token(tok, ctx.as_mut()).branch_obj_la(
|
||||
|st, obj, la| match (obj, la) {
|
||||
(Expanded(obj), la) => into(st)
|
||||
.ok(obj)
|
||||
.maybe_with_lookahead(la.map(Lookahead::inner_into)),
|
||||
|
||||
(DoneExpanding(tok), la) => {
|
||||
// Uphold parser lookahead invariant.
|
||||
la.diagnostic_expect_none(
|
||||
|Lookahead(la_tok)| {
|
||||
vec![
|
||||
la_tok.span().note(
|
||||
"this token of lookahead would be lost",
|
||||
),
|
||||
tok.span().internal_error(
|
||||
"unexpected token of lookahead while \
|
||||
completing expansion of this token",
|
||||
),
|
||||
]
|
||||
},
|
||||
"cannot provide lookahead token with \
|
||||
Expansion::DoneExpanding",
|
||||
);
|
||||
|
||||
done(st, tok.into()).into_super()
|
||||
}
|
||||
},
|
||||
&into,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod test;
|
|
@ -1,258 +0,0 @@
|
|||
// Tests for TAMER parsing framework utilities
|
||||
//
|
||||
// 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::super::SPair;
|
||||
use super::*;
|
||||
use crate::{
|
||||
span::{dummy::*, Span},
|
||||
sym::{st::raw, SymbolId},
|
||||
};
|
||||
use std::{
|
||||
assert_matches::assert_matches, convert::Infallible, fmt::Display,
|
||||
marker::PhantomData,
|
||||
};
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub struct StitchableExpansionState<S: ClosedParseState, O: Object> {
|
||||
st: S,
|
||||
_phantom: PhantomData<O>,
|
||||
}
|
||||
|
||||
impl<S: ClosedParseState, O: Object> Default for StitchableExpansionState<S, O>
|
||||
where
|
||||
S: Default,
|
||||
{
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
st: Default::default(),
|
||||
_phantom: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<S: ClosedParseState, O: Object> ParseState
|
||||
for StitchableExpansionState<S, O>
|
||||
where
|
||||
S: ExpandableParseState<O> + StitchExpansion,
|
||||
<S as ParseState>::Context: AsMut<<S as ParseState>::Context>,
|
||||
{
|
||||
type Token = S::Token;
|
||||
type Object = O;
|
||||
type Error = S::Error;
|
||||
type Context = S::Context;
|
||||
|
||||
#[inline]
|
||||
fn parse_token(
|
||||
self,
|
||||
tok: Self::Token,
|
||||
ctx: &mut Self::Context,
|
||||
) -> TransitionResult<Self::Super> {
|
||||
let Self { st, _phantom } = self;
|
||||
|
||||
st.stitch_expansion(
|
||||
tok,
|
||||
ctx,
|
||||
Transition::fmap(|st| Self { st, _phantom }),
|
||||
|Transition(st), tok| Transition(Self { st, _phantom }).dead(tok),
|
||||
)
|
||||
}
|
||||
|
||||
fn is_accepting(&self, ctx: &Self::Context) -> bool {
|
||||
self.st.is_accepting(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
impl<S: ClosedParseState, O: Object> Display
|
||||
for StitchableExpansionState<S, O>
|
||||
{
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
match self {
|
||||
Self {
|
||||
st: parser,
|
||||
_phantom,
|
||||
} => {
|
||||
write!(f, "{parser}, with Expansion stripped")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
struct TestObject(SPair);
|
||||
|
||||
impl Token for TestObject {
|
||||
fn ir_name() -> &'static str {
|
||||
"TestObject"
|
||||
}
|
||||
|
||||
fn span(&self) -> Span {
|
||||
match self {
|
||||
Self(SPair(_, span)) => *span,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for TestObject {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
match self {
|
||||
Self(spair) => Display::fmt(spair, f),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Object for TestObject {}
|
||||
|
||||
/// Just some parser to wrap for our tests.
|
||||
///
|
||||
/// Eventually we'll be able to more easily create these on-demand without so
|
||||
/// so much boilerplate,
|
||||
/// but that hasn't evolved yet.
|
||||
#[derive(Debug, PartialEq, Eq, Default)]
|
||||
struct TestParseState;
|
||||
|
||||
impl ParseState for TestParseState {
|
||||
type Token = SPair;
|
||||
type Object = Expansion<Self::Token, TestObject>;
|
||||
type Error = Infallible;
|
||||
|
||||
fn parse_token(
|
||||
self,
|
||||
tok: Self::Token,
|
||||
_ctx: &mut Self::Context,
|
||||
) -> TransitionResult<Self::Super> {
|
||||
match tok {
|
||||
tok @ SPair(sym @ (STOP | DEAD_SYM), span) => {
|
||||
let st = Transition(self).ok(Expansion::DoneExpanding(tok));
|
||||
|
||||
st.maybe_with_lookahead(if sym == DEAD_SYM {
|
||||
// It doesn't matter what this token is for our tests.
|
||||
Some(Lookahead(SPair(sym, span)))
|
||||
} else {
|
||||
None
|
||||
})
|
||||
}
|
||||
_ => Transition(self).ok(Expansion::Expanded(TestObject(tok))),
|
||||
}
|
||||
}
|
||||
|
||||
fn is_accepting(&self, _ctx: &Self::Context) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for TestParseState {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
write!(f, "doing its thing") // well, it is
|
||||
}
|
||||
}
|
||||
|
||||
impl StitchExpansion for TestParseState {}
|
||||
|
||||
const STOP: SymbolId = raw::L_YIELD;
|
||||
const DEAD_SYM: SymbolId = raw::L_WARNING;
|
||||
|
||||
type ExpansionSut = StitchableExpansionState<TestParseState, TestObject>;
|
||||
|
||||
#[test]
|
||||
fn expansion_can_be_stripped_for_stitching() {
|
||||
let syma = "foo".into();
|
||||
let symb = "bar".into();
|
||||
|
||||
let toks = vec![SPair(syma, S1), SPair(symb, S2), SPair(STOP, S3)];
|
||||
|
||||
// The wraps the above TestParseState to strip Expansion.
|
||||
let mut sut = ExpansionSut::parse(toks.into_iter());
|
||||
|
||||
// Our test parser echoes back the tokens wrapped in an "expanded"
|
||||
// `TestObject` until we reach `STOP`.
|
||||
// The first two are expanded,
|
||||
// and our SUT strips the expansion.
|
||||
assert_eq!(
|
||||
sut.next(),
|
||||
Some(Ok(Parsed::Object(TestObject(SPair(syma, S1))))),
|
||||
);
|
||||
assert_eq!(
|
||||
sut.next(),
|
||||
Some(Ok(Parsed::Object(TestObject(SPair(symb, S2))))),
|
||||
);
|
||||
|
||||
// The final `Expansion::DoneExpanding` is converted into a dead state
|
||||
// transition.
|
||||
// That manifests here as an `UnexpectedToken` error because nothing
|
||||
// handled it within our parser,
|
||||
// but this is expected to stitched via delegation,
|
||||
// which _would_ handle this case.
|
||||
assert_matches!(
|
||||
sut.next(),
|
||||
Some(Err(ParseError::UnexpectedToken(dead_tok, _)))
|
||||
if dead_tok == SPair(STOP, S3)
|
||||
);
|
||||
}
|
||||
|
||||
// We must not lose lookahead tokens;
|
||||
// see SUT for more information.
|
||||
#[should_panic]
|
||||
#[test]
|
||||
fn expansion_stripping_panics_if_lookahead() {
|
||||
let toks = vec![SPair(DEAD_SYM, S1)];
|
||||
|
||||
// The above token will trigger the panic on the first call.
|
||||
let _ = ExpansionSut::parse(toks.into_iter()).next();
|
||||
}
|
||||
|
||||
// This test would fail at compile-time.
|
||||
#[test]
|
||||
fn expandable_into_is_stitchable_with_target() {
|
||||
// This is utilized only for its types in the below assertions.
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
struct TargetParseState;
|
||||
|
||||
impl ParseState for TargetParseState {
|
||||
type Token = SPair;
|
||||
type Object = TestObject;
|
||||
type Error = Infallible;
|
||||
|
||||
fn parse_token(
|
||||
self,
|
||||
_tok: Self::Token,
|
||||
_ctx: &mut Self::Context,
|
||||
) -> TransitionResult<Self::Super> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn is_accepting(&self, _ctx: &Self::Context) -> bool {
|
||||
unimplemented!()
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for TargetParseState {
|
||||
fn fmt(&self, _f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
unimplemented!()
|
||||
}
|
||||
}
|
||||
|
||||
// The `ExpandableInto` trait alias is responsible for asserting that a
|
||||
// given parser is an expansion parser that is able to be converted
|
||||
// into a parser stitchable with the target.
|
||||
//
|
||||
// If this fails but the above assertion succeeds,
|
||||
// then the compatibility is working but something is wrong with the
|
||||
// definition of `ExpandableInto`.
|
||||
assert_impl_all!(TestParseState: ExpandableInto<TargetParseState>);
|
||||
}
|
|
@ -0,0 +1,99 @@
|
|||
// Lowering pipelines
|
||||
//
|
||||
// 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/>.
|
||||
|
||||
//! Lowering pipelines.
|
||||
//!
|
||||
//! TAMER is composed of a series of IR lowering operations connected in
|
||||
//! series as a pipeline of [`Parser`](crate::parse::Parser)s,
|
||||
//! called the _lowering pipeline_.
|
||||
//! Each parser in the pipeline produces a stream of tokens that is read and
|
||||
//! acted upon by the next.
|
||||
//! The system can therefore be reasoned about as a series of mappings
|
||||
//! between IRs,
|
||||
//! where each parser in the pipeline produces a lower-level IR.
|
||||
//!
|
||||
//! Portions of the pipeline require operating on data in aggregate.
|
||||
//! Most notably,
|
||||
//! the [ASG](crate::asg) aggregates data into a graph;
|
||||
//! the graph acts as a _sink_ for the pipeline.
|
||||
//! At the other end,
|
||||
//! the ASG serves as a source for another lowering pipeline that emits
|
||||
//! the target object.
|
||||
//!
|
||||
//! The module is responsible for pipeline composition.
|
||||
//! For information on the lowering pipeline as an abstraction,
|
||||
//! see [`Lower`].
|
||||
|
||||
use crate::{
|
||||
asg::{air::AirAggregate, AsgTreeToXirf},
|
||||
diagnose::Diagnostic,
|
||||
nir::{InterpolateNir, NirToAir, TplShortDesugar, XirfToNir},
|
||||
obj::xmlo::{XmloReader, XmloToAir, XmloToken},
|
||||
parse::{
|
||||
terminal, FinalizeError, Lower, LowerSource, ParseError, ParseState,
|
||||
Parsed, ParsedObject, UnknownToken,
|
||||
},
|
||||
xir::{
|
||||
autoclose::XirfAutoClose,
|
||||
flat::{PartialXirToXirf, RefinedText, Text, XirToXirf, XirfToXir},
|
||||
},
|
||||
};
|
||||
|
||||
// The `lower_pipeline` macro is tucked away here to allow the reader to
|
||||
// focus on the pipelines themselves rather than how they wired together.
|
||||
#[macro_use]
|
||||
mod r#macro;
|
||||
|
||||
lower_pipeline! {
|
||||
/// Load an `xmlo` file represented by `src` into the graph held
|
||||
/// by `air_ctx`.
|
||||
///
|
||||
/// Loading an object file will result in opaque objects being added to the
|
||||
/// graph.
|
||||
///
|
||||
/// 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
|
||||
|> 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>
|
||||
|> AsgTreeToXirf<'a>[asg]
|
||||
|> XirfAutoClose
|
||||
|> XirfToXir<Text>;
|
||||
}
|
|
@ -0,0 +1,288 @@
|
|||
// Lowering pipeline macro
|
||||
//
|
||||
// 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/>.
|
||||
|
||||
//! Declarative lowering pipelines.
|
||||
//!
|
||||
//! This module defines the `lower_pipeline` macro,
|
||||
//! which allows for declarative lowering pipeline definitions.
|
||||
//! For more information of what a lowering pipeline is,
|
||||
//! and to see TAMER's pipelines,
|
||||
//! see the [parent module](super).
|
||||
|
||||
/// Declaratively define a lowering pipeline.
|
||||
///
|
||||
/// A lowering pipeline stitches together parsers such that the objects of
|
||||
/// one become the tokens of the next;
|
||||
/// see the [module-level documentation](self) for more information.
|
||||
///
|
||||
/// Composing those types results in a significant amount of boilerplate.
|
||||
/// This macro is responsible for generating a function that,
|
||||
/// given a source and optional [`ParseState`](crate::parse::ParseState) contexts,
|
||||
/// will carry out the lowering operation and invoke the provided sink on
|
||||
/// each object that comes out the end.
|
||||
///
|
||||
/// TODO: This is not yet finalized;
|
||||
/// more documentation is needed once the approach is solid.
|
||||
macro_rules! lower_pipeline {
|
||||
($(
|
||||
$(#[$meta:meta])*
|
||||
$vis:vis $fn:ident$(<$l:lifetime>)?
|
||||
$(|> $lower:ty $([$ctx:ident])? $(, until ($until:pat))?)*;
|
||||
)*) => {$(
|
||||
$(#[$meta])*
|
||||
///
|
||||
/// Pipeline Definition
|
||||
/// ===================
|
||||
/// This pipeline consists of the following parsers,
|
||||
/// where the first parser accepts the `src` token stream and the
|
||||
/// final parser outputs to the provided `sink`.
|
||||
///
|
||||
$(
|
||||
#[doc=concat!(" - [`", stringify!($lower), "`]")]
|
||||
$(
|
||||
#[doc=concat!(" with context `", stringify!($ctx), "`")]
|
||||
)?
|
||||
$(
|
||||
#[doc=concat!(
|
||||
"up to and including the first token matching `",
|
||||
stringify!($until),
|
||||
"`",
|
||||
)]
|
||||
)?
|
||||
#[doc="\n"]
|
||||
)*
|
||||
///
|
||||
/// The type signature of this function is complex due to the wiring
|
||||
/// 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
|
||||
/// 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
|
||||
/// 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
|
||||
/// 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
|
||||
>,
|
||||
$(
|
||||
// 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<
|
||||
(
|
||||
$(
|
||||
// Any context that is passed in is also returned so
|
||||
// that individual pipelines can continue to build
|
||||
// upon state from previous pipelines.
|
||||
$( lower_pipeline!(@ret_ctx_ty $lower, $ctx), )?
|
||||
)*
|
||||
),
|
||||
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,
|
||||
// so the only type conversion that we are concerned about
|
||||
// ourselves is producing it from a finalization error,
|
||||
// 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,)?
|
||||
)*))
|
||||
}
|
||||
)*};
|
||||
|
||||
(@ret_ctx_ty $lower:ty, $_ctx:ident) => {
|
||||
<$lower as ParseState>::PubContext
|
||||
};
|
||||
|
||||
// We are able to determine the necessary type of the source token
|
||||
// stream by observing what token is expected by the first parser in the
|
||||
// pipeline.
|
||||
(@first_tok_ty $lower:ty, $($rest:ty),+) => {
|
||||
<$lower as ParseState>::Token
|
||||
};
|
||||
|
||||
// The last object type enters the sink.
|
||||
(@last_obj_ty $lower:ty, $($rest:ty),+) => {
|
||||
lower_pipeline!(@last_obj_ty $($rest),+)
|
||||
};
|
||||
(@last_obj_ty $last:ty) => {
|
||||
<$last as ParseState>::Object
|
||||
};
|
||||
|
||||
// Because of how the lowering pipeline composes,
|
||||
// contexts are nested with a terminal unit at the left,
|
||||
// as in `(((), B), A)` with `A` being a parser that appears earlier
|
||||
// in the pipeline than `B`.
|
||||
(@ret_pat $ctx:ident $($rest:tt)*) => {
|
||||
(lower_pipeline!(@ret_pat $($rest)*), $ctx)
|
||||
};
|
||||
(@ret_pat) => {
|
||||
()
|
||||
};
|
||||
|
||||
// First lowering operation from the source.
|
||||
//
|
||||
// This doesn't support context or `until`;
|
||||
// it can be added if ever it is needed.
|
||||
(
|
||||
@body_head($src:ident, $sink:ident)
|
||||
(|> $head:ty) $($rest:tt)*
|
||||
) => {
|
||||
Lower::<
|
||||
ParsedObject<UnknownToken, _, ES>,
|
||||
$head,
|
||||
ER,
|
||||
>::lower::<_, EU>(&mut $src.map(|result| result.map_err(ER::from)), |next| {
|
||||
lower_pipeline!(
|
||||
@body_inner(next, $head, $sink)
|
||||
$($rest)*
|
||||
)
|
||||
})
|
||||
};
|
||||
|
||||
// TODO: Roll this into the above
|
||||
(
|
||||
@body_head($src:ident, $sink:ident)
|
||||
(|> $head:ty [$ctx:ident]) $($rest:tt)*
|
||||
) => {
|
||||
Lower::<
|
||||
ParsedObject<UnknownToken, _, ES>,
|
||||
$head,
|
||||
ER,
|
||||
>::lower_with_context::<_, EU>(
|
||||
&mut $src.map(|result| result.map_err(ER::from)),
|
||||
$ctx,
|
||||
|next| {
|
||||
lower_pipeline!(
|
||||
@body_inner(next, $head, $sink)
|
||||
$($rest)*
|
||||
)
|
||||
}
|
||||
)
|
||||
};
|
||||
|
||||
// Lower without context
|
||||
// (with the default context for the parser).
|
||||
//
|
||||
// This doesn't support `until`;
|
||||
// it can be added if needed.
|
||||
(
|
||||
@body_inner($next:ident, $lower_prev:ty, $sink:ident)
|
||||
(|> $lower:ty) $($rest:tt)*
|
||||
) => {
|
||||
Lower::<$lower_prev, $lower, _>::lower($next, |next| {
|
||||
lower_pipeline!(
|
||||
@body_inner(next, $lower, $sink)
|
||||
$($rest)*
|
||||
)
|
||||
})
|
||||
};
|
||||
|
||||
// Lower with a context provided by the caller,
|
||||
// optionally with an `until` clause that stops at (and includes) a
|
||||
// matching object from the previous parser.
|
||||
//
|
||||
// TODO: Roll this into the above
|
||||
(
|
||||
@body_inner($next:ident, $lower_prev:ty, $sink:ident)
|
||||
(|> $lower:ty [$ctx:ident] $(, until ($until:pat))?) $($rest:tt)*
|
||||
) => {{
|
||||
// Shadow $next with a new iterator if `until` clause was provided.
|
||||
// `take_while` unfortunately doesn't include the first object that
|
||||
// does not match,
|
||||
// thus the more verbose `scan` which includes the first
|
||||
// non-match and then trips the iterator.
|
||||
$(
|
||||
let $next = &mut $next.scan(false, |st, rtok| match st {
|
||||
true => None,
|
||||
false => {
|
||||
*st =
|
||||
matches!(rtok, Ok(Parsed::Object($until)));
|
||||
Some(rtok)
|
||||
}
|
||||
});
|
||||
)?
|
||||
|
||||
Lower::<$lower_prev, $lower, _>::lower_with_context($next, $ctx, |next| {
|
||||
lower_pipeline!(
|
||||
@body_inner(next, $lower, $sink)
|
||||
$($rest)*
|
||||
)
|
||||
})
|
||||
}};
|
||||
|
||||
// Terminal sink is applied after there are no more lowering operations
|
||||
// remaining in the pipeline definition.
|
||||
(@body_inner($next:ident, $lower_prev:ty, $sink:ident)) => {
|
||||
terminal::<$lower_prev, _>($next).try_for_each($sink)
|
||||
};
|
||||
}
|
|
@ -439,6 +439,30 @@ impl Span {
|
|||
}
|
||||
}
|
||||
|
||||
/// Slice a span with an offset from the end.
|
||||
///
|
||||
/// This can be read as "reverse slice" or "right-hand slice".
|
||||
/// It is like [`Self::slice`],
|
||||
/// but the provided offset is relative to the _end_ of the span
|
||||
/// rather than the beginning.
|
||||
pub fn rslice(self, rel_offset: usize, len: usize) -> Self {
|
||||
self.slice(self.len() as usize - rel_offset, len)
|
||||
}
|
||||
|
||||
/// Slice a span from its beginning.
|
||||
///
|
||||
/// This is equivalent to [`Self::slice`] with an offset of `0`.
|
||||
pub fn slice_head(self, len: usize) -> Self {
|
||||
self.slice(0, len)
|
||||
}
|
||||
|
||||
/// Slice a span from its end.
|
||||
///
|
||||
/// This is equivalent to [`Self::rslice`] with an offset of `len`.
|
||||
pub fn slice_tail(self, len: usize) -> Self {
|
||||
self.rslice(len, len)
|
||||
}
|
||||
|
||||
/// Adjust span such that its offset is relative to the provided span.
|
||||
///
|
||||
/// If the provide `rel_span` does not precede this span,
|
||||
|
@ -883,6 +907,13 @@ mod test {
|
|||
let span = ctx.span(10, 10);
|
||||
|
||||
assert_eq!(ctx.span(15, 5), span.slice(5, 5));
|
||||
// and from the opposite direction
|
||||
assert_eq!(ctx.span(17, 2), span.rslice(3, 2));
|
||||
|
||||
// While we're at it,
|
||||
// these also use the above:
|
||||
assert_eq!(ctx.span(10, 5), span.slice_head(5));
|
||||
assert_eq!(ctx.span(15, 5), span.slice_tail(5));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
@ -710,17 +710,20 @@ pub mod st {
|
|||
L_VIRTUAL: cid "virtual",
|
||||
L_WARNING: cid "warning",
|
||||
L_WHEN: cid "when",
|
||||
L_WITH_PARAM: tid "with-param",
|
||||
L_WORKSHEET: cid "worksheet",
|
||||
L_XMLNS: cid "xmlns",
|
||||
L_YIELD: cid "yield",
|
||||
L_YIELDS: cid "yields",
|
||||
|
||||
L_TPLP_VALUES: str "@values@",
|
||||
|
||||
FW_SLASH: str "/",
|
||||
FW_SLASH_DOT: str "/.",
|
||||
|
||||
CC_ANY_OF: cid "anyOf",
|
||||
|
||||
L_MAP_UUUHEAD: str ":map:___head",
|
||||
L_MAP_UUUTAIL: str ":map:___tail",
|
||||
L_RETMAP_UUUHEAD: str ":retmap:___head",
|
||||
L_RETMAP_UUUTAIL: str ":retmap:___tail",
|
||||
U_TRUE: cid "TRUE",
|
||||
|
||||
URI_LV_CALC: uri "http://www.lovullo.com/calc",
|
||||
URI_LV_LINKER: uri "http://www.lovullo.com/rater/linker",
|
||||
|
|
|
@ -80,6 +80,20 @@ where
|
|||
XirfToken::Close(qname.map(ExpectInto::unwrap_into), span.into(), depth)
|
||||
}
|
||||
|
||||
/// Hastily and lazily produce a [`XirfToken::Attr`].
|
||||
///
|
||||
/// This function is intended for testing only.
|
||||
pub fn attr<Q: TryInto<QName>, T: TextType>(
|
||||
qname: Q,
|
||||
value: SymbolId,
|
||||
spans: (Span, Span),
|
||||
) -> XirfToken<T>
|
||||
where
|
||||
<Q as TryInto<QName>>::Error: Debug,
|
||||
{
|
||||
XirfToken::Attr(Attr::new(qname.unwrap_into(), value, spans))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_element_self_close() {
|
||||
let name = ("ns", "elem");
|
||||
|
|
|
@ -28,7 +28,7 @@ mod error;
|
|||
|
||||
pub use attr::{parse_attrs, AttrParseState};
|
||||
pub use ele::{
|
||||
EleParseState, NodeMatcher, Nt, NtState, StateStack, SumNt, SumNtState,
|
||||
SuperState, SuperStateContext,
|
||||
EleParseState, NodeMatcher, Nt, NtState, SumNt, SumNtState, SuperState,
|
||||
SuperStateContext,
|
||||
};
|
||||
pub use error::{AttrParseError, NtError, SumNtError};
|
||||
|
|
|
@ -24,15 +24,11 @@
|
|||
|
||||
use super::AttrParseState;
|
||||
use crate::{
|
||||
diagnostic_panic,
|
||||
fmt::{DisplayWrapper, TtQuote},
|
||||
parse::{
|
||||
ClosedParseState, Context, ParseState, Transition, TransitionResult,
|
||||
},
|
||||
parse::{ClosedParseState, Context, ParseState, StateStack},
|
||||
span::Span,
|
||||
xir::{flat::Depth, CloseSpan, OpenSpan, Prefix, QName},
|
||||
};
|
||||
use arrayvec::ArrayVec;
|
||||
use std::{
|
||||
fmt::{Debug, Display, Formatter},
|
||||
marker::PhantomData,
|
||||
|
@ -44,143 +40,22 @@ use crate::{ele_parse, parse::Parser};
|
|||
/// A parser accepting a single element.
|
||||
pub trait EleParseState: ParseState {}
|
||||
|
||||
/// Maximum level of parser nesting.
|
||||
/// Maximum level of parser stack nesting.
|
||||
///
|
||||
/// The intent of this value is to ensure that TAMER will fail if something
|
||||
/// goes terribly wrong,
|
||||
/// e.g. if the system has a bug where lookahead causes the system to
|
||||
/// recurse infinitely.
|
||||
///
|
||||
/// Unfortunately,
|
||||
/// this limit _does not_ correspond to the level of XML nesting;
|
||||
/// parsers composed of Sum NTs,
|
||||
/// in particular,
|
||||
/// push multiple parsers onto the stack for a single element.
|
||||
///
|
||||
/// Note that this is assuming that this parser is used only for TAME
|
||||
/// sources.
|
||||
/// If that's not the case,
|
||||
/// this can be made to be configurable like XIRF.
|
||||
pub const MAX_DEPTH: usize = 64;
|
||||
|
||||
/// Parser stack for trampoline.
|
||||
///
|
||||
/// This can be used as a call stack for parsers while avoiding creating
|
||||
/// otherwise-recursive data structures with composition-based delegation.
|
||||
/// However,
|
||||
/// it is more similar to CPS,
|
||||
/// in that the parser popped off the stack need not be the parser that
|
||||
/// initiated the request and merely represents the next step in
|
||||
/// a delayed computation.
|
||||
/// If such a return context is unneeded,
|
||||
/// a [`ParseState`] may implement tail calls by simply not pushing itself
|
||||
/// onto the stack before requesting transfer to another [`ParseState`].
|
||||
#[derive(Debug, Default)]
|
||||
pub struct StateStack<S: SuperState>(ArrayVec<S, MAX_DEPTH>);
|
||||
pub const XIR_MAX_DEPTH: usize = 1024;
|
||||
|
||||
/// [`SuperState`] [`Context`] that gets propagated to each child parser.
|
||||
pub type SuperStateContext<S> = Context<StateStack<S>>;
|
||||
|
||||
// Note that public visibility is needed because `ele_parse` expands outside
|
||||
// of this module.
|
||||
impl<S: SuperState> StateStack<S> {
|
||||
/// Request a transfer to another [`ParseState`],
|
||||
/// expecting that control be returned to `ret` after it has
|
||||
/// completed.
|
||||
///
|
||||
/// This can be reasoned about like calling a thunk:
|
||||
/// the return [`ParseState`] is put onto the stack,
|
||||
/// the target [`ParseState`] is used for the state transition to
|
||||
/// cause [`Parser`] to perform the call to it,
|
||||
/// and when it is done
|
||||
/// (e.g. a dead state),
|
||||
/// `ret` will be pop'd from the stack and we'll transition back to
|
||||
/// it.
|
||||
/// Note that this method is not responsible for returning;
|
||||
/// see [`Self::ret_or_dead`] to perform a return.
|
||||
///
|
||||
/// However,
|
||||
/// the calling [`ParseState`] is not responsible for its return,
|
||||
/// unlike a typical function call.
|
||||
/// Instead,
|
||||
/// this _actually_ more closely resembles CPS
|
||||
/// (continuation passing style),
|
||||
/// and so [`ele_parse!`] must be careful to ensure that stack
|
||||
/// operations are properly paired.
|
||||
/// On the upside,
|
||||
/// if something is erroneously `ret`'d,
|
||||
/// the parser is guaranteed to be in a consistent state since the
|
||||
/// entire state has been reified
|
||||
/// (but the input would then be parsed incorrectly).
|
||||
///
|
||||
/// Note that tail calls can be implemented by transferring control
|
||||
/// without pushing an entry on the stack to return to,
|
||||
/// but that hasn't been formalized \[yet\] and requires extra care.
|
||||
pub fn transfer_with_ret<SA, ST>(
|
||||
&mut self,
|
||||
Transition(ret): Transition<SA>,
|
||||
target: TransitionResult<ST>,
|
||||
) -> TransitionResult<ST>
|
||||
where
|
||||
SA: ParseState<Super = S::Super>,
|
||||
ST: ParseState,
|
||||
{
|
||||
let Self(stack) = self;
|
||||
|
||||
// TODO: Global configuration to (hopefully) ensure that XIRF will
|
||||
// actually catch this.
|
||||
if stack.is_full() {
|
||||
// TODO: We need some spans here and ideally convert the
|
||||
// parenthetical error message into a diagnostic footnote.
|
||||
// TODO: Or should we have a special error type that tells the
|
||||
// parent `Parser` to panic with context?
|
||||
diagnostic_panic!(
|
||||
vec![],
|
||||
"maximum parsing depth of {} exceeded while attempting \
|
||||
to push return state {} \
|
||||
(try reducing XML nesting as a workaround)",
|
||||
MAX_DEPTH,
|
||||
TtQuote::wrap(ret),
|
||||
);
|
||||
}
|
||||
|
||||
stack.push(ret.into());
|
||||
target
|
||||
}
|
||||
|
||||
/// Attempt to return to a previous [`ParseState`] that transferred
|
||||
/// control away from itself,
|
||||
/// otherwise yield a dead state transition to `deadst`.
|
||||
///
|
||||
/// Conceptually,
|
||||
/// this is like returning from a function call,
|
||||
/// where the function was invoked using [`Self::transfer_with_ret`].
|
||||
/// However,
|
||||
/// this system is more akin to CPS
|
||||
/// (continuation passing style);
|
||||
/// see [`Self::transfer_with_ret`] for important information.
|
||||
///
|
||||
/// If there is no state to return to on the stack,
|
||||
/// then it is assumed that we have received more input than expected
|
||||
/// after having completed a full parse.
|
||||
pub fn ret_or_dead(
|
||||
&mut self,
|
||||
lookahead: S::Token,
|
||||
deadst: S,
|
||||
) -> TransitionResult<S> {
|
||||
let Self(stack) = self;
|
||||
|
||||
// This should certainly never happen unless there is a bug in the
|
||||
// `ele_parse!` parser-generator,
|
||||
// since it means that we're trying to return to a caller that
|
||||
// does not exist.
|
||||
match stack.pop() {
|
||||
Some(st) => Transition(st).incomplete().with_lookahead(lookahead),
|
||||
None => Transition(deadst).dead(lookahead),
|
||||
}
|
||||
}
|
||||
|
||||
/// Test every [`ParseState`] on the stack against the predicate `f`.
|
||||
pub fn all(&self, f: impl Fn(&S) -> bool) -> bool {
|
||||
let Self(stack) = self;
|
||||
stack[..].iter().all(f)
|
||||
}
|
||||
}
|
||||
pub type SuperStateContext<S> = Context<StateStack<S, XIR_MAX_DEPTH>>;
|
||||
|
||||
/// Match some type of node.
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
|
@ -320,7 +195,7 @@ macro_rules! ele_parse {
|
|||
|
||||
// Special forms (`[sp](args) => expr`).
|
||||
$(
|
||||
[$special:ident]$(($($special_arg:ident),*))?
|
||||
[$special:ident]$(($($special_arg:tt)*))?
|
||||
=> $special_map:expr,
|
||||
)?
|
||||
|
||||
|
@ -338,7 +213,7 @@ macro_rules! ele_parse {
|
|||
@ { $($attrbody)* } => $attrmap,
|
||||
/$($($close_span)?)? => ele_parse!(@!ele_close $($closemap)?),
|
||||
|
||||
$([$special]$(($($special_arg),*))? => $special_map,)?
|
||||
$([$special]$(($($special_arg)*))? => $special_map,)?
|
||||
|
||||
<> {
|
||||
$(
|
||||
|
@ -422,7 +297,7 @@ macro_rules! ele_parse {
|
|||
/$($close_span:ident)? => $closemap:expr,
|
||||
|
||||
// Streaming (as opposed to aggregate) attribute parsing.
|
||||
$([attr]($attr_stream_binding:ident) => $attr_stream_map:expr,)?
|
||||
$([attr]($attr_stream_binding:pat) => $attr_stream_map:expr,)?
|
||||
|
||||
// Nonterminal references.
|
||||
<> {
|
||||
|
@ -632,10 +507,12 @@ macro_rules! ele_parse {
|
|||
let $open_span = span;
|
||||
)?
|
||||
|
||||
Transition(Self(Attrs(
|
||||
(qname, span, depth),
|
||||
parse_attrs(qname, span)
|
||||
))).ok(<$objty>::from($attrmap))
|
||||
<$objty>::try_from($attrmap)
|
||||
.map($crate::parse::ParseStatus::Object)
|
||||
.transition(Self(Attrs(
|
||||
(qname, span, depth),
|
||||
parse_attrs(qname, span)
|
||||
)))
|
||||
},
|
||||
|
||||
// We only attempt recovery when encountering an
|
||||
|
@ -813,9 +690,8 @@ macro_rules! ele_parse {
|
|||
| CloseRecoverIgnore((qname, _, depth), _),
|
||||
XirfToken::Close(_, span, tok_depth)
|
||||
) if tok_depth == depth => {
|
||||
$(
|
||||
let $close_span = span;
|
||||
)?
|
||||
$(#[allow(unused_variables)] let $qname_matched = qname;)?
|
||||
$(let $close_span = span;)?
|
||||
$closemap.transition(Self(Closed(Some(qname), span.tag_span())))
|
||||
},
|
||||
|
||||
|
@ -1329,7 +1205,7 @@ macro_rules! ele_parse {
|
|||
tok,
|
||||
stack,
|
||||
|deadst, tok, stack| {
|
||||
stack.ret_or_dead(tok, deadst)
|
||||
stack.ret_or_dead(deadst, tok)
|
||||
},
|
||||
),
|
||||
)*
|
||||
|
@ -1353,7 +1229,7 @@ macro_rules! ele_parse {
|
|||
//
|
||||
// After having considered the stack,
|
||||
// we can then consider the active `ParseState`.
|
||||
stack.all(|st| st.is_inner_accepting(stack))
|
||||
stack.iter().all(|st| st.is_inner_accepting(stack))
|
||||
&& self.is_inner_accepting(stack)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,6 +21,7 @@
|
|||
|
||||
use core::fmt::Debug;
|
||||
use std::{
|
||||
convert::Infallible,
|
||||
error::Error,
|
||||
fmt::{Display, Formatter},
|
||||
marker::PhantomData,
|
||||
|
@ -71,6 +72,12 @@ impl<NT: Nt> Error for NtError<NT> {
|
|||
}
|
||||
}
|
||||
|
||||
impl<NT: Nt> From<Infallible> for NtError<NT> {
|
||||
fn from(_value: Infallible) -> Self {
|
||||
unreachable!("From<Infallible>")
|
||||
}
|
||||
}
|
||||
|
||||
impl<NT: Nt> Display for NtError<NT> {
|
||||
fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
|
||||
use crate::xir::fmt::{TtCloseXmlEle, TtOpenXmlEle};
|
||||
|
|
|
@ -225,6 +225,7 @@ pub mod qname {
|
|||
QN_VALUES: :L_VALUES,
|
||||
QN_VIRTUAL: :L_VIRTUAL,
|
||||
QN_WARNING: :L_WARNING,
|
||||
QN_WITH_PARAM: :L_WITH_PARAM,
|
||||
QN_WORKSHEET: :L_WORKSHEET,
|
||||
QN_YIELD: :L_YIELD,
|
||||
QN_YIELDS: :L_YIELDS,
|
||||
|
|
|
@ -60,6 +60,21 @@ impl std::fmt::Display for Error {
|
|||
}
|
||||
}
|
||||
|
||||
impl PartialEq for Error {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
match (self, other) {
|
||||
(Error::Io(_), _) => false,
|
||||
(Error::Xir(a), Error::Xir(b)) => a == b,
|
||||
(
|
||||
Error::UnexpectedToken(aa, ab),
|
||||
Error::UnexpectedToken(ba, bb),
|
||||
) => aa == ba && ab == bb,
|
||||
(Error::Todo(aa, ab), Error::Todo(ba, bb)) => aa == ba && ab == bb,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for Error {
|
||||
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
||||
match self {
|
||||
|
|
|
@ -3,9 +3,9 @@
|
|||
xmlns:c="http://www.lovullo.com/calc"
|
||||
xmlns:t="http://www.lovullo.com/rater/apply-template">
|
||||
|
||||
<classify as="always" />
|
||||
<classify as="always" desc="Always" />
|
||||
|
||||
<classify as="sometimes">
|
||||
<classify as="sometimes" desc="Sometimes">
|
||||
<any>
|
||||
<all />
|
||||
<any />
|
||||
|
@ -13,5 +13,17 @@
|
|||
</any>
|
||||
<any />
|
||||
</classify>
|
||||
|
||||
<classify as="short-match-implicit-eq-implicit-true"
|
||||
desc="Short match with implicit eq and
|
||||
an implicit value">
|
||||
<match on="foo" value="TRUE" />
|
||||
</classify>
|
||||
|
||||
<classify as="short-match-implicit-eq-explicit-value"
|
||||
desc="Short match with an implicit eq and
|
||||
an explicit value">
|
||||
<match on="foo" value="bar" />
|
||||
</classify>
|
||||
</package>
|
||||
|
||||
|
|
|
@ -3,9 +3,9 @@
|
|||
xmlns:c="http://www.lovullo.com/calc"
|
||||
xmlns:t="http://www.lovullo.com/rater/apply-template">
|
||||
|
||||
<classify as="always" />
|
||||
<classify as="always" desc="Always" />
|
||||
|
||||
<classify as="sometimes">
|
||||
<classify as="sometimes" desc="Sometimes">
|
||||
<any>
|
||||
<all />
|
||||
<any />
|
||||
|
@ -13,5 +13,17 @@
|
|||
</any>
|
||||
<any />
|
||||
</classify>
|
||||
|
||||
<classify as="short-match-implicit-eq-implicit-true"
|
||||
desc="Short match with implicit eq and
|
||||
an implicit value">
|
||||
<match on="foo" />
|
||||
</classify>
|
||||
|
||||
<classify as="short-match-implicit-eq-explicit-value"
|
||||
desc="Short match with an implicit eq and
|
||||
an explicit value">
|
||||
<match on="foo" value="bar" />
|
||||
</classify>
|
||||
</package>
|
||||
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
|
||||
<package xmlns="http://www.lovullo.com/rater"
|
||||
xmlns:c="http://www.lovullo.com/calc"
|
||||
xmlns:t="http://www.lovullo.com/rater/apply-template">
|
||||
|
||||
|
||||
|
||||
<import package="/first/pkg" />
|
||||
<import package="/second" />
|
||||
<import package="/third" />
|
||||
</package>
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
<?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">
|
||||
|
||||
Packages aren't actually imported yet,
|
||||
but they do need to be represented on the graph for `xmli` derivation.
|
||||
Import paths will be canonicalized.
|
||||
|
||||
<import package="first/pkg" />
|
||||
<import package="second" />
|
||||
<import package="third" />
|
||||
</package>
|
||||
|
|
@ -3,12 +3,10 @@
|
|||
xmlns:c="http://www.lovullo.com/calc"
|
||||
xmlns:t="http://www.lovullo.com/rater/apply-template">
|
||||
|
||||
<template name="_empty_" />
|
||||
|
||||
<template name="_with-static-reachable_">
|
||||
|
||||
|
||||
<template name="_empty_" desc="Empty" />
|
||||
|
||||
<template name="_with-static-identified_"
|
||||
desc="Template with identified expressions">
|
||||
<rate yields="tplStaticA">
|
||||
<c:sum />
|
||||
</rate>
|
||||
|
@ -17,10 +15,8 @@
|
|||
</classify>
|
||||
</template>
|
||||
|
||||
<template name="_with-static-unreachable_">
|
||||
|
||||
|
||||
|
||||
<template name="_with-static-unidentified_"
|
||||
desc="Unidentified expressions in body">
|
||||
<c:sum>
|
||||
<c:product />
|
||||
</c:sum>
|
||||
|
@ -30,11 +26,9 @@
|
|||
</c:product>
|
||||
</template>
|
||||
|
||||
<template name="_with-static-mix-reachability_">
|
||||
|
||||
|
||||
|
||||
|
||||
<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>
|
||||
|
@ -50,11 +44,151 @@
|
|||
|
||||
|
||||
|
||||
|
||||
|
||||
<rate yields="tplStaticMix" />
|
||||
|
||||
<c:sum>
|
||||
<c:product />
|
||||
</c:sum>
|
||||
</template>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<template name="_short-hand-nullary_" desc="No params" />
|
||||
<apply-template name="_short-hand-nullary_" />
|
||||
|
||||
<template name="_short-hand-unary_" desc="One param" />
|
||||
<apply-template name="_short-hand-unary_">
|
||||
<with-param name="@foo@" value="bar" />
|
||||
</apply-template>
|
||||
|
||||
<template name="_short-hand-nary_" desc="N params" />
|
||||
<apply-template name="_short-hand-nary_">
|
||||
<with-param name="@foo@" value="bar" />
|
||||
<with-param name="@bar@" value="baz" />
|
||||
<with-param name="@baz@" value="quux" />
|
||||
</apply-template>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<template name="_short-hand-nullary-body_" desc="Nullary with body" />
|
||||
<apply-template name="_short-hand-nullary-body_">
|
||||
<with-param name="@values@" value="___dsgr-bfe___" />
|
||||
</apply-template>
|
||||
<template name="___dsgr-bfe___"
|
||||
desc="Desugared body of shorthand template application of `_short-hand-nullary-body_`">
|
||||
<c:product>
|
||||
<c:sum />
|
||||
</c:product>
|
||||
</template>
|
||||
|
||||
<template name="_short-hand-nary-body_" desc="N-ary with body" />
|
||||
<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___" />
|
||||
</apply-template>
|
||||
<template name="___dsgr-cb5___"
|
||||
desc="Desugared body of shorthand template application of `_short-hand-nary-body_`">
|
||||
<c:sum>
|
||||
<c:product />
|
||||
</c:sum>
|
||||
</template>
|
||||
|
||||
<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___" />
|
||||
</apply-template>
|
||||
<template name="___dsgr-d99___"
|
||||
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" />
|
||||
<apply-template name="_short-hand-nullary-inner-dfn-inner_" />
|
||||
</template>
|
||||
|
||||
<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___" />
|
||||
</apply-template>
|
||||
<template name="___dsgr-eed___"
|
||||
desc="Desugared body of shorthand template application of `_short-hand-nullary-outer_`">
|
||||
<apply-template name="_short-hand-nullary-inner-dfn-outer_" />
|
||||
</template>
|
||||
|
||||
<template name="_short-hand-unary-with-body_"
|
||||
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___" />
|
||||
</apply-template>
|
||||
<template name="___dsgr-fb4___"
|
||||
desc="Desugared body of shorthand template application of `_short-hand-unary-with-body_`">
|
||||
<template name="_short-hand-unary-with-body-inner_"
|
||||
desc="Inner template" />
|
||||
<apply-template name="_short-hand-unary-with-body-inner_" />
|
||||
</template>
|
||||
|
||||
<template name="_short-hand-in-expr_"
|
||||
desc="Template to be applied within an expression" />
|
||||
<rate yields="shortHandTplInExpr">
|
||||
<apply-template name="_short-hand-in-expr_">
|
||||
<with-param name="@in@" value="rate" />
|
||||
</apply-template>
|
||||
</rate>
|
||||
|
||||
<template name="_tpl-with-short-hand-inner_"
|
||||
desc="Template with a shorthand application in its body">
|
||||
<template name="_tpl-with-short-hand-inner-inner_" />
|
||||
<apply-template name="_tpl-with-short-hand-inner-inner_" />
|
||||
|
||||
<c:sum>
|
||||
<apply-template name="_tpl-with-short-hand-inner-inner_">
|
||||
<with-param name="@in@" value="sum" />
|
||||
</apply-template>
|
||||
</c:sum>
|
||||
</template>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<template name="_match-child_" desc="Template with a match child">
|
||||
<match on="foo" value="TRUE" />
|
||||
</template>
|
||||
</package>
|
||||
|
||||
|
|
|
@ -3,12 +3,10 @@
|
|||
xmlns:c="http://www.lovullo.com/calc"
|
||||
xmlns:t="http://www.lovullo.com/rater/apply-template">
|
||||
|
||||
<template name="_empty_" />
|
||||
|
||||
<template name="_with-static-reachable_">
|
||||
All expressions here are reachable
|
||||
(having been derived from named statements).
|
||||
<template name="_empty_" desc="Empty" />
|
||||
|
||||
<template name="_with-static-identified_"
|
||||
desc="Template with identified expressions">
|
||||
<rate yields="tplStaticA">
|
||||
<c:sum />
|
||||
</rate>
|
||||
|
@ -17,10 +15,8 @@
|
|||
</classify>
|
||||
</template>
|
||||
|
||||
<template name="_with-static-unreachable_">
|
||||
This expression is on its own unreachable,
|
||||
intended to be expanded into another expression.
|
||||
|
||||
<template name="_with-static-unidentified_"
|
||||
desc="Unidentified expressions in body">
|
||||
<c:sum>
|
||||
<c:product />
|
||||
</c:sum>
|
||||
|
@ -30,11 +26,9 @@
|
|||
</c:product>
|
||||
</template>
|
||||
|
||||
<template name="_with-static-mix-reachability_">
|
||||
Both reachable and unreachable,
|
||||
with the intent of expanding into an expression but also providing
|
||||
itself with supporting expressions.
|
||||
|
||||
<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>
|
||||
|
@ -58,5 +52,142 @@
|
|||
<c:product />
|
||||
</c:sum>
|
||||
</template>
|
||||
|
||||
Short-hand template application.
|
||||
These get expanding into the long form so that we don't have to translate
|
||||
back and forth between the underscore-padded strings.
|
||||
The fixpoint test will further verify that TAMER also recognizes the long
|
||||
`apply-template` form,
|
||||
asserting their equivalency.
|
||||
|
||||
<template name="_short-hand-nullary_" desc="No params" />
|
||||
<t:short-hand-nullary />
|
||||
|
||||
<template name="_short-hand-unary_" desc="One param" />
|
||||
<t:short-hand-unary foo="bar" />
|
||||
|
||||
|
||||
|
||||
<template name="_short-hand-nary_" desc="N params" />
|
||||
<t:short-hand-nary foo="bar" bar="baz" baz="quux" />
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Shorthand template bodies desugar into the param `@values@`.
|
||||
Unlike in the XSLT-based TAMER,
|
||||
metavaraibles (template parameters) are purely lexical,
|
||||
and do not contain trees,
|
||||
simplifying its implementation.
|
||||
Desugaring instead takes advantage of existing features by generating a
|
||||
_new_ closed template with the body from the shorthand application.
|
||||
Since closed templates can be applied by referencing them as a value,
|
||||
which expands them in place,
|
||||
this ends up having the same effect as a `param-copy`.
|
||||
|
||||
For now,
|
||||
the expected output asserts on this behavior,
|
||||
but if this has a significantly negative impact on performance of the
|
||||
XSLT-based compiler,
|
||||
then it'll have to inline during desugaring.
|
||||
|
||||
This asserts verbatim on the output,
|
||||
which uses a generated id based on the span.
|
||||
This is fragile,
|
||||
and it may break often;
|
||||
just take the hex span from the test failure in that case.
|
||||
|
||||
<template name="_short-hand-nullary-body_" desc="Nullary with body" />
|
||||
<t:short-hand-nullary-body>
|
||||
<c:product>
|
||||
<c:sum />
|
||||
</c:product>
|
||||
</t:short-hand-nullary-body>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<template name="_short-hand-nary-body_" desc="N-ary with body" />
|
||||
<t:short-hand-nary-body bar="baz" baz="quux">
|
||||
<c:sum>
|
||||
<c:product />
|
||||
</c:sum>
|
||||
</t:short-hand-nary-body>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<template name="_short-hand-nullary-outer_"
|
||||
desc="Outer template holding an inner" />
|
||||
<t:short-hand-nullary-outer>
|
||||
<template name="_short-hand-nullary-inner-dfn-inner_"
|
||||
desc="Inner template applied inner" />
|
||||
<t:short-hand-nullary-inner-dfn-inner />
|
||||
</t:short-hand-nullary-outer>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<template name="_short-hand-nullary-inner-dfn-outer_"
|
||||
desc="Define template outer but apply inner" />
|
||||
<t:short-hand-nullary-outer>
|
||||
<t:short-hand-nullary-inner-dfn-outer />
|
||||
</t:short-hand-nullary-outer>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<template name="_short-hand-unary-with-body_"
|
||||
desc="Unary with body" />
|
||||
<t:short-hand-unary-with-body foo="bar">
|
||||
<template name="_short-hand-unary-with-body-inner_"
|
||||
desc="Inner template" />
|
||||
<t:short-hand-unary-with-body-inner />
|
||||
</t:short-hand-unary-with-body>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<template name="_short-hand-in-expr_"
|
||||
desc="Template to be applied within an expression" />
|
||||
<rate yields="shortHandTplInExpr">
|
||||
<t:short-hand-in-expr in="rate" />
|
||||
</rate>
|
||||
|
||||
|
||||
|
||||
<template name="_tpl-with-short-hand-inner_"
|
||||
desc="Template with a shorthand application in its body">
|
||||
<template name="_tpl-with-short-hand-inner-inner_" />
|
||||
<t:tpl-with-short-hand-inner-inner />
|
||||
|
||||
<c:sum>
|
||||
<t:tpl-with-short-hand-inner-inner in="sum" />
|
||||
</c:sum>
|
||||
</template>
|
||||
|
||||
This next one is a bit awkward,
|
||||
because it creates an ambiguity when regenerating XML.
|
||||
Each of `match`, `when`, and `c:*` are represented the same on the graph,
|
||||
so it will not be clear until expansion how the body of the below
|
||||
|
||||
The ambiguity will go away once template application is performed by
|
||||
TAMER in the near future;
|
||||
until that time,
|
||||
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>
|
||||
</package>
|
||||
|
||||
|
|
Loading…
Reference in New Issue