Prepare to have targeted error diagnostics (#604)

This commit starts to add infrastructure for targeted diagnostics in the
`#[wasm_bindgen]` attribute, intended eventually at providing much better errors
as they'll be pointing to exactly the code in question rather than always to a
`#[wasm_bindgen]` attribute.

The general changes are are:

* A new `Diagnostic` error type is added to the backend. A `Diagnostic` is
  created with a textual error or with a span, and it can also be created from a
  list of diagnostics. A `Diagnostic` implements `ToTokens` which emits a bunch
  of invocations of `compile_error!` that will cause rustc to later generate
  errors.

* Fallible implementations of `ToTokens` have switched to using a new trait,
  `TryToTokens`, which returns a `Result` to use `?` with.

* The `MacroParse` trait has changed to returning a `Result` to propagate errors
  upwards.

* A new `ui-tests` crate was added which uses `compiletest_rs` to add UI tests.
  These UI tests will verify that our output improves over time and does not
  regress. This test suite is added to CI as a new builder as well.

* No `Diagnostic` instances are created just yet, everything continues to panic
  and return `Ok`, with the one exception of the top-level invocations of
  `syn::parse` which now create a `Diagnostic` and pass it along.

This commit does not immediately improve diagnostics but the intention is that
it is laying the groundwork for improving diagnostics over time. It should
ideally be much easier to contribute improved diagnostics after this commit!

cc #601
This commit is contained in:
Alex Crichton 2018-08-01 17:15:27 -05:00 committed by GitHub
parent fce687cf7b
commit c4dcaee1b9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 370 additions and 54 deletions

View File

@ -117,6 +117,12 @@ matrix:
- cargo test -p webidl-tests --target wasm32-unknown-unknown
if: branch = master
# UI tests for the macro work just fine
- rust: nightly
env: JOB=macro-ui
script: cargo test -p ui-tests
if: branch = master
# Dist linux binary
- rust: nightly
env: JOB=dist-linux TARGET=x86_64-unknown-linux-musl

View File

@ -44,6 +44,7 @@ members = [
"crates/test",
"crates/test/sample",
"crates/typescript",
"crates/macro/ui-tests",
"crates/web-sys",
"crates/webidl",
"crates/webidl-tests",

View File

@ -2,6 +2,8 @@ use proc_macro2::{Ident, Span};
use shared;
use syn;
use Diagnostic;
/// An abstract syntax tree representing a rust program. Contains
/// extra information for joining up this rust code with javascript.
#[cfg_attr(feature = "extra-traits", derive(Debug, PartialEq))]
@ -223,15 +225,17 @@ pub enum ConstValue {
}
impl Program {
pub(crate) fn shared(&self) -> shared::Program {
shared::Program {
pub(crate) fn shared(&self) -> Result<shared::Program, Diagnostic> {
Ok(shared::Program {
exports: self.exports.iter().map(|a| a.shared()).collect(),
structs: self.structs.iter().map(|a| a.shared()).collect(),
enums: self.enums.iter().map(|a| a.shared()).collect(),
imports: self.imports.iter().map(|a| a.shared()).collect(),
imports: self.imports.iter()
.map(|a| a.shared())
.collect::<Result<_, Diagnostic>>()?,
version: shared::version(),
schema_version: shared::SCHEMA_VERSION.to_string(),
}
})
}
}
@ -305,7 +309,7 @@ impl Variant {
}
impl Import {
fn shared(&self) -> shared::Import {
fn shared(&self) -> Result<shared::Import, Diagnostic> {
match (&self.module, &self.version) {
(&Some(ref m), None) if m.starts_with("./") => {}
(&Some(ref m), &Some(_)) if m.starts_with("./") => {
@ -330,12 +334,12 @@ impl Import {
}
(&None, &None) => {}
}
shared::Import {
Ok(shared::Import {
module: self.module.clone(),
version: self.version.clone(),
js_namespace: self.js_namespace.as_ref().map(|s| s.to_string()),
kind: self.kind.shared(),
}
})
}
}

View File

@ -2,19 +2,31 @@ use std::collections::HashSet;
use std::sync::Mutex;
use std::sync::atomic::{AtomicUsize, Ordering, ATOMIC_USIZE_INIT};
use ast;
use proc_macro2::{Ident, Literal, Span, TokenStream};
use quote::ToTokens;
use serde_json;
use shared;
use syn;
use ast;
use Diagnostic;
use util::ShortHash;
impl ToTokens for ast::Program {
pub trait TryToTokens {
fn try_to_tokens(&self, tokens: &mut TokenStream) -> Result<(), Diagnostic>;
fn try_to_token_stream(&self) -> Result<TokenStream, Diagnostic> {
let mut tokens = TokenStream::new();
self.try_to_tokens(&mut tokens)?;
Ok(tokens)
}
}
impl TryToTokens for ast::Program {
// Generate wrappers for all the items that we've found
fn to_tokens(&self, tokens: &mut TokenStream) {
fn try_to_tokens(&self, tokens: &mut TokenStream) -> Result<(), Diagnostic> {
for export in self.exports.iter() {
export.to_tokens(tokens);
export.try_to_tokens(tokens)?;
}
for s in self.structs.iter() {
s.to_tokens(tokens);
@ -30,13 +42,13 @@ impl ToTokens for ast::Program {
if let Some(ns) = &i.js_namespace {
if types.contains(ns) && i.kind.fits_on_impl() {
let kind = &i.kind;
let kind = i.kind.try_to_token_stream()?;
(quote! { impl #ns { #kind } }).to_tokens(tokens);
continue;
}
}
i.kind.to_tokens(tokens);
i.kind.try_to_tokens(tokens)?;
}
for e in self.enums.iter() {
e.to_tokens(tokens);
@ -60,7 +72,7 @@ impl ToTokens for ast::Program {
);
let generated_static_name = Ident::new(&generated_static_name, Span::call_site());
let description = serde_json::to_string(&self.shared()).unwrap();
let description = serde_json::to_string(&self.shared()?).unwrap();
// Each JSON blob is prepended with the length of the JSON blob so when
// all these sections are concatenated in the final wasm file we know
@ -83,6 +95,8 @@ impl ToTokens for ast::Program {
pub static #generated_static_name: [u8; #generated_static_length] =
*#generated_static_value;
}).to_tokens(tokens);
Ok(())
}
}
@ -276,8 +290,10 @@ impl ToTokens for ast::StructField {
}
}
impl ToTokens for ast::Export {
fn to_tokens(self: &ast::Export, into: &mut TokenStream) {
impl TryToTokens for ast::Export {
fn try_to_tokens(self: &ast::Export, into: &mut TokenStream)
-> Result<(), Diagnostic>
{
let generated_name = self.rust_symbol();
let export_name = self.export_name();
let mut args = vec![];
@ -461,17 +477,21 @@ impl ToTokens for ast::Export {
#(<#argtys as WasmDescribe>::describe();)*
#describe_ret
}).to_tokens(into);
Ok(())
}
}
impl ToTokens for ast::ImportKind {
fn to_tokens(&self, tokens: &mut TokenStream) {
impl TryToTokens for ast::ImportKind {
fn try_to_tokens(&self, tokens: &mut TokenStream) -> Result<(), Diagnostic> {
match *self {
ast::ImportKind::Function(ref f) => f.to_tokens(tokens),
ast::ImportKind::Function(ref f) => f.try_to_tokens(tokens)?,
ast::ImportKind::Static(ref s) => s.to_tokens(tokens),
ast::ImportKind::Type(ref t) => t.to_tokens(tokens),
ast::ImportKind::Enum(ref e) => e.to_tokens(tokens),
}
Ok(())
}
}
@ -663,8 +683,8 @@ impl ToTokens for ast::ImportEnum {
}
}
impl ToTokens for ast::ImportFunction {
fn to_tokens(&self, tokens: &mut TokenStream) {
impl TryToTokens for ast::ImportFunction {
fn try_to_tokens(&self, tokens: &mut TokenStream) -> Result<(), Diagnostic> {
let mut class_ty = None;
let mut is_method = false;
match self.kind {
@ -827,6 +847,8 @@ impl ToTokens for ast::ImportFunction {
} else {
invocation.to_tokens(tokens);
}
Ok(())
}
}

View File

@ -0,0 +1,83 @@
use proc_macro2::*;
use quote::ToTokens;
pub struct Diagnostic {
inner: Repr,
}
enum Repr {
Single {
text: String,
span: Option<(Span, Span)>,
},
Multi {
diagnostics: Vec<Diagnostic>,
}
}
impl Diagnostic {
pub fn error<T: Into<String>>(text: T) -> Diagnostic {
Diagnostic {
inner: Repr::Single {
text: text.into(),
span: None,
}
}
}
pub fn span_error<T: Into<String>>(node: &ToTokens, text: T) -> Diagnostic {
Diagnostic {
inner: Repr::Single {
text: text.into(),
span: extract_spans(node),
}
}
}
pub fn from_vec(diagnostics: Vec<Diagnostic>) -> Result<(), Diagnostic> {
if diagnostics.len() == 0 {
Ok(())
} else {
Err(Diagnostic { inner: Repr::Multi { diagnostics } })
}
}
#[allow(unconditional_recursion)]
pub fn panic(&self) -> ! {
match &self.inner {
Repr::Single { text, .. } => panic!("{}", text),
Repr::Multi { diagnostics } => diagnostics[0].panic(),
}
}
}
fn extract_spans(node: &ToTokens) -> Option<(Span, Span)> {
let mut t = TokenStream::new();
node.to_tokens(&mut t);
let mut tokens = t.into_iter();
let start = tokens.next().map(|t| t.span());
let end = tokens.last().map(|t| t.span());
start.map(|start| (start, end.unwrap_or(start)))
}
impl ToTokens for Diagnostic {
fn to_tokens(&self, dst: &mut TokenStream) {
match &self.inner {
Repr::Single { text, span } => {
let cs2 = (Span::call_site(), Span::call_site());
let (start, end) = span.unwrap_or(cs2);
dst.extend(Some(Ident::new("compile_error", start).into()));
dst.extend(Some(Punct::new('!', Spacing::Alone).into()));
let mut message = TokenStream::new();
message.extend(Some(Literal::string(text).into()));
let mut group = Group::new(Delimiter::Brace, message);
group.set_span(end);
dst.extend(Some(group.into()));
}
Repr::Multi { diagnostics } => {
for diagnostic in diagnostics {
diagnostic.to_tokens(dst);
}
}
}
}
}

View File

@ -14,7 +14,11 @@ extern crate syn;
extern crate wasm_bindgen_shared as shared;
pub use codegen::TryToTokens;
pub use error::Diagnostic;
pub mod ast;
mod codegen;
pub mod defined;
mod error;
pub mod util;

View File

@ -12,15 +12,26 @@ extern crate wasm_bindgen_shared as shared;
pub use parser::BindgenAttrs;
use parser::MacroParse;
use quote::ToTokens;
use backend::{Diagnostic, TryToTokens};
use proc_macro2::TokenStream;
mod parser;
/// Takes the parsed input from a `#[wasm_bindgen]` macro and returns the generated bindings
pub fn expand(item: syn::Item, opts: parser::BindgenAttrs) -> proc_macro2::TokenStream {
pub fn expand(attr: TokenStream, input: TokenStream) -> Result<TokenStream, Diagnostic> {
let item = syn_parse::<syn::Item>(input, "rust item")?;
let opts = syn_parse(attr, "#[wasm_bindgen] attribute options")?;
let mut tokens = proc_macro2::TokenStream::new();
let mut program = backend::ast::Program::default();
item.macro_parse(&mut program, (Some(opts), &mut tokens));
program.to_tokens(&mut tokens);
tokens
item.macro_parse(&mut program, (Some(opts), &mut tokens))?;
program.try_to_tokens(&mut tokens)?;
Ok(tokens)
}
fn syn_parse<T: syn::synom::Synom>(tokens: TokenStream, name: &str) -> Result<T, Diagnostic> {
syn::parse2(tokens.clone())
.map_err(|err| {
Diagnostic::span_error(&tokens, format!("error parsing {}: {}", name, err))
})
}

View File

@ -1,4 +1,5 @@
use backend::ast;
use backend::Diagnostic;
use backend::util::{ident_ty, ShortHash};
use proc_macro2::{Ident, Span, TokenStream, TokenTree};
use quote::ToTokens;
@ -590,7 +591,8 @@ pub(crate) trait MacroParse<Ctx> {
///
/// The context is used to have access to the attributes on `#[wasm_bindgen]`, and to allow
/// writing to the output `TokenStream`.
fn macro_parse(self, program: &mut ast::Program, context: Ctx);
fn macro_parse(self, program: &mut ast::Program, context: Ctx)
-> Result<(), Diagnostic>;
}
impl<'a> MacroParse<(Option<BindgenAttrs>, &'a mut TokenStream)> for syn::Item {
@ -598,7 +600,7 @@ impl<'a> MacroParse<(Option<BindgenAttrs>, &'a mut TokenStream)> for syn::Item {
self,
program: &mut ast::Program,
(opts, tokens): (Option<BindgenAttrs>, &'a mut TokenStream),
) {
) -> Result<(), Diagnostic> {
match self {
syn::Item::Fn(mut f) => {
let no_mangle = f
@ -629,27 +631,31 @@ impl<'a> MacroParse<(Option<BindgenAttrs>, &'a mut TokenStream)> for syn::Item {
s.to_tokens(tokens);
}
syn::Item::Impl(mut i) => {
(&mut i).macro_parse(program, ());
(&mut i).macro_parse(program, ())?;
i.to_tokens(tokens);
}
syn::Item::ForeignMod(mut f) => {
let opts = opts.unwrap_or_else(|| BindgenAttrs::find(&mut f.attrs));
f.macro_parse(program, opts);
f.macro_parse(program, opts)?;
}
syn::Item::Enum(e) => {
e.to_tokens(tokens);
e.macro_parse(program, ());
e.macro_parse(program, ())?;
}
_ => panic!(
"#[wasm_bindgen] can only be applied to a function, \
struct, enum, impl, or extern block"
),
}
Ok(())
}
}
impl<'a> MacroParse<()> for &'a mut syn::ItemImpl {
fn macro_parse(self, program: &mut ast::Program, (): ()) {
fn macro_parse(self, program: &mut ast::Program, (): ())
-> Result<(), Diagnostic>
{
if self.defaultness.is_some() {
panic!("default impls are not supported");
}
@ -672,14 +678,20 @@ impl<'a> MacroParse<()> for &'a mut syn::ItemImpl {
},
_ => panic!("unsupported self type in impl"),
};
let mut errors = Vec::new();
for item in self.items.iter_mut() {
(&name, item).macro_parse(program, ())
if let Err(e) = (&name, item).macro_parse(program, ()) {
errors.push(e);
}
}
Diagnostic::from_vec(errors)
}
}
impl<'a, 'b> MacroParse<()> for (&'a Ident, &'b mut syn::ImplItem) {
fn macro_parse(self, program: &mut ast::Program, (): ()) {
fn macro_parse(self, program: &mut ast::Program, (): ())
-> Result<(), Diagnostic>
{
let (class, item) = self;
replace_self(class, item);
let method = match item {
@ -691,7 +703,7 @@ impl<'a, 'b> MacroParse<()> for (&'a Ident, &'b mut syn::ImplItem) {
};
match method.vis {
syn::Visibility::Public(_) => {}
_ => return,
_ => return Ok(()),
}
if method.defaultness.is_some() {
panic!("default methods are not supported");
@ -728,11 +740,14 @@ impl<'a, 'b> MacroParse<()> for (&'a Ident, &'b mut syn::ImplItem) {
comments,
rust_name: method.sig.ident.clone(),
});
Ok(())
}
}
impl MacroParse<()> for syn::ItemEnum {
fn macro_parse(self, program: &mut ast::Program, (): ()) {
fn macro_parse(self, program: &mut ast::Program, (): ())
-> Result<(), Diagnostic>
{
match self.vis {
syn::Visibility::Public(_) => {}
_ => panic!("only public enums are allowed"),
@ -776,11 +791,14 @@ impl MacroParse<()> for syn::ItemEnum {
variants,
comments,
});
Ok(())
}
}
impl MacroParse<BindgenAttrs> for syn::ItemForeignMod {
fn macro_parse(self, program: &mut ast::Program, opts: BindgenAttrs) {
fn macro_parse(self, program: &mut ast::Program, opts: BindgenAttrs)
-> Result<(), Diagnostic>
{
match self.abi.name {
Some(ref l) if l.value() == "C" => {}
None => {}
@ -816,6 +834,7 @@ impl MacroParse<BindgenAttrs> for syn::ItemForeignMod {
kind,
});
}
Ok(())
}
}

View File

@ -14,10 +14,9 @@ Definition of the `#[wasm_bindgen]` attribute, an internal dependency
proc-macro = true
[features]
spans = ["proc-macro2/nightly", "wasm-bindgen-macro-support/spans"]
spans = ["wasm-bindgen-macro-support/spans"]
xxx_debug_only_print_generated_code = []
[dependencies]
syn = { version = '0.14', features = ['full'] }
proc-macro2 = "0.4.9"
wasm-bindgen-macro-support = { path = "../macro-support", version = "=0.2.15" }
quote = "0.6"

28
crates/macro/README.md Normal file
View File

@ -0,0 +1,28 @@
# `wasm-bindgen-macro`
Implementation of the `#[wasm_bindgen]` attribute. See the `wasm-bindgen`
documentation for more information about what this macro does.
## Testing
Testing of this macro is done through "ui tests" in the `ui-tests` subdirectory
of this crate. Each Rust file in this folder is compiled with the `wasm_bindgen`
crate, and the `*.stderr` file sitting next to it is the asserted output of the
compiler. If the output matches, the test passes, and if the output doesn't
match the test fails. Note that it is also considered a failure if a test
actually compiles successfully.
To add a test:
* Create `ui-tests/my-awesome-test.rs`
* Write an invalid `#[wasm_bindgen]` invocation, testing the error you're
generating
* Execute `cargo test -p ui-tests`, the test will fail
* From within the `ui-tests` folder, execute `./update-all-references.sh`. This
should create a `my-awesome-test.stderr` file.
* Inspect `my-awesome-test.stderr` to make sure it looks ok
* Rerun `cargo test -p ui-tests` and your tests should pass!
Testing here is a work in progress, see
[#601](https://github.com/rustwasm/wasm-bindgen/issues/601) for more
information.

View File

@ -1,23 +1,21 @@
#![doc(html_root_url = "https://docs.rs/wasm-bindgen-macro/0.2")]
extern crate proc_macro;
extern crate proc_macro2;
extern crate syn;
#[macro_use]
extern crate quote;
extern crate wasm_bindgen_macro_support as macro_support;
use macro_support::BindgenAttrs;
use proc_macro::TokenStream;
#[proc_macro_attribute]
pub fn wasm_bindgen(attr: TokenStream, input: TokenStream) -> TokenStream {
let item = syn::parse::<syn::Item>(input.clone()).expect("expected a valid Rust item");
let opts = syn::parse::<BindgenAttrs>(attr).expect("invalid arguments to #[wasm_bindgen]");
let tokens = macro_support::expand(item, opts);
match macro_support::expand(attr.into(), input.into()) {
Ok(tokens) => {
if cfg!(feature = "xxx_debug_only_print_generated_code") {
println!("{}", tokens);
}
tokens.into()
}
Err(diagnostic) => (quote! { #diagnostic }).into(),
}
}

View File

@ -0,0 +1,15 @@
[package]
name = "ui-tests"
version = "0.1.0"
authors = ["The wasm-bindgen Authors"]
[lib]
path = "test.rs"
doctest = false
harness = false
[dependencies]
wasm-bindgen = { path = "../../.." }
[dev-dependencies]
compiletest_rs = "0.3"

View File

@ -0,0 +1,8 @@
#![feature(use_extern_macros)]
extern crate wasm_bindgen;
use wasm_bindgen::prelude::*;
#[wasm_bindgen(nonsense)]
pub fn foo() {}

View File

@ -0,0 +1,8 @@
error: error parsing #[wasm_bindgen] attribute options: failed to parse anything
--> $DIR/attribute-fails-to-parse.rs:7:16
|
7 | #[wasm_bindgen(nonsense)]
| ^^^^^^^^
error: aborting due to previous error

View File

@ -0,0 +1,8 @@
#![feature(use_extern_macros)]
extern crate wasm_bindgen;
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
fn foo() {}

View File

@ -0,0 +1,10 @@
error: custom attribute panicked
--> $DIR/non-public-function.rs:7:1
|
7 | #[wasm_bindgen]
| ^^^^^^^^^^^^^^^
|
= help: message: can only bindgen public functions
error: aborting due to previous error

View File

@ -0,0 +1,25 @@
// ignore-test - not a test
#![cfg(test)]
extern crate compiletest_rs as compiletest;
use std::env;
use std::fs;
use std::path::PathBuf;
fn main() {
let mut config = compiletest::Config::default();
config.mode = "ui".parse().expect("invalid mode");
let mut me = env::current_exe().unwrap();
me.pop();
config.target_rustcflags = Some(format!("-L {}", me.display()));
let src = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
config.src_base = src;
me.pop();
me.pop();
config.build_base = me.join("tests/ui");
drop(fs::remove_dir_all(&config.build_base));
compiletest::run_tests(&config);
}

View File

@ -0,0 +1,23 @@
#!/bin/bash
#
# Copyright 2015 The Rust Project Developers. See the COPYRIGHT
# file at the top-level directory of this distribution and at
# http://rust-lang.org/COPYRIGHT.
#
# Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
# http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
# <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
# option. This file may not be copied, modified, or distributed
# except according to those terms.
# A script to update the references for all tests. The idea is that
# you do a run, which will generate files in the build directory
# containing the (normalized) actual output of the compiler. You then
# run this script, which will copy those files over. If you find
# yourself manually editing a foo.stderr file, you're doing it wrong.
#
# See all `update-references.sh`, if you just want to update a single test.
MY_DIR=$(dirname $0)
cd $MY_DIR
find . -name '*.rs' | xargs ./update-references.sh

View File

@ -0,0 +1,42 @@
#!/bin/bash
#
# Copyright 2015 The Rust Project Developers. See the COPYRIGHT
# file at the top-level directory of this distribution and at
# http://rust-lang.org/COPYRIGHT.
#
# Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
# http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
# <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
# option. This file may not be copied, modified, or distributed
# except according to those terms.
# A script to update the references for particular tests. The idea is
# that you do a run, which will generate files in the build directory
# containing the (normalized) actual output of the compiler. This
# script will then copy that output and replace the "expected output"
# files. You can then commit the changes.
#
# If you find yourself manually editing a foo.stderr file, you're
# doing it wrong.
MYDIR=$(dirname $0)
BUILD_DIR="../../../target/tests/ui"
while [[ "$1" != "" ]]; do
STDERR_NAME="${1/%.rs/.stderr}"
STDOUT_NAME="${1/%.rs/.stdout}"
shift
if [ -f $BUILD_DIR/$STDOUT_NAME ] && \
! (diff $BUILD_DIR/$STDOUT_NAME $MYDIR/$STDOUT_NAME >& /dev/null); then
echo updating $MYDIR/$STDOUT_NAME
cp $BUILD_DIR/$STDOUT_NAME $MYDIR/$STDOUT_NAME
fi
if [ -f $BUILD_DIR/$STDERR_NAME ] && \
! (diff $BUILD_DIR/$STDERR_NAME $MYDIR/$STDERR_NAME >& /dev/null); then
echo updating $MYDIR/$STDERR_NAME
cp $BUILD_DIR/$STDERR_NAME $MYDIR/$STDERR_NAME
fi
done

View File

@ -33,11 +33,11 @@ use std::io::{self, Read};
use std::iter::FromIterator;
use std::path::Path;
use backend::TryToTokens;
use backend::defined::{ImportedTypeDefinitions, RemoveUndefinedImports};
use backend::util::{ident_ty, rust_ident, wrap_import_function};
use failure::{ResultExt, Fail};
use heck::{ShoutySnakeCase};
use quote::ToTokens;
use first_pass::{FirstPass, FirstPassRecord};
use util::{public, webidl_const_ty_to_syn_ty, webidl_const_v_to_backend_const_v, TypePosition, camel_case_ident, mdn_doc};
@ -111,7 +111,9 @@ fn compile_ast(mut ast: backend::ast::Program) -> String {
ast.remove_undefined_imports(&|id| defined.contains(id));
let mut tokens = proc_macro2::TokenStream::new();
ast.to_tokens(&mut tokens);
if let Err(e) = ast.try_to_tokens(&mut tokens) {
e.panic();
}
tokens.to_string()
}