Import 'diplomat_core' crate

Request Document: go/android-rust-importing-crates
For CL Reviewers: go/android3p#cl-review
Bug: 391960687
Test: m libdiplomat_core

Change-Id: I7798f0b48bca10e2c0eadbdb28e643a36d1a4f9d
diff --git a/crates/diplomat_core/.android-checksum.json b/crates/diplomat_core/.android-checksum.json
new file mode 100644
index 0000000..824c778
--- /dev/null
+++ b/crates/diplomat_core/.android-checksum.json
@@ -0,0 +1 @@
+{"package":null,"files":{".cargo-checksum.json":"dfcb117cf89415f5fe4b58ae0032a0da4af01dfa95853a0809eb7a9e6283f83d","Android.bp":"4dc55c04acc59075c2aef2bac12effaa4d0d6081946be54a048bb2a0fd90f877","Cargo.toml":"c38dc98680fc373efbb68d73dad5a32770fbf44afd4d8a253169f3592c05f345","LICENSE":"c96314154285c10cace9fbdd749bb99df065bc09c718ca11eb3c9bcc3fedc823","LICENSE-APACHE":"c96314154285c10cace9fbdd749bb99df065bc09c718ca11eb3c9bcc3fedc823","LICENSE-MIT":"82963c45175dab0ab1d5a10c32b27b6ece4089ff17deca7f922923c53a04935a","METADATA":"ca8df89910cabf708bdc4f3b0284b46cfbccf466cb6c305f10b30167e0ec14ab","MODULE_LICENSE_APACHE2":"0d6f8afa3940b7f06bebee651376d43bc8b0d5b437337be2696d30377451e93a","cargo_embargo.json":"958f9a85753bf1fa1d9dbd47b92221018d717240f25a8bf606d88b24bcd30416","rules.mk":"6ee7ab0e0d5dc019a7e45323d5799800e3f772fead77acc1a53018c13aa44b3d","src/ast/attrs.rs":"5ac3980b23f06ac06b16496c9901a607b885b4885c3054d3b09606946ea50681","src/ast/docs.rs":"88e00220085a483cb654b941e0690a1dd873255f59ffda273e665a2b989874de","src/ast/enums.rs":"8dd933a376b4eff09e1e220668017ebe16f7cf4e09efcfdd5159d5ddcdf00fd6","src/ast/idents.rs":"d2cb3fa5d145d1636118dd382c1471343d7e7c689b0dda87c03468625f0543b5","src/ast/lifetimes.rs":"3bd41dc9b39705e7bf69daf95e5d22da9e0eb7e77d07ad325a61832da60b49d7","src/ast/methods.rs":"4028caeb4766ad20c8ecc7c09adb813bd5fbb221db7e210140987adbfbafc381","src/ast/mod.rs":"fa4f6e4bfb293ce236e2707aa23536a254c0e1842c06447e6cef32d3ef5f75ae","src/ast/modules.rs":"5f7c7233273c20731b5142ef515559f83cebade05f4536e4886ff843c854aea6","src/ast/paths.rs":"a033da3954916f6b4a24c4e382576b32be93cee1cd1dfa372d9c4f20156a9864","src/ast/snapshots/diplomat_core__ast__attrs__tests__attr.snap":"d560f6ccba0c26c2fa6f372f28384464194634c06827f82c38209b935d0bd699","src/ast/snapshots/diplomat_core__ast__attrs__tests__cfgs-2.snap":"07a7758d9124039fac763b0ec832ada7ff68b7a54dadb4445293a9369160821e","src/ast/snapshots/diplomat_core__ast__attrs__tests__cfgs-3.snap":"6ae9d8505edd71fd026ad9e6e3382838647dde47e1ed86fafd52a7d19449bd33","src/ast/snapshots/diplomat_core__ast__attrs__tests__cfgs-4.snap":"6ae9d8505edd71fd026ad9e6e3382838647dde47e1ed86fafd52a7d19449bd33","src/ast/snapshots/diplomat_core__ast__attrs__tests__cfgs-5.snap":"f8348506c857981e5cd1cff690215b46fbfc097b12e2a7a9e5adaf4542b57dc4","src/ast/snapshots/diplomat_core__ast__attrs__tests__cfgs.snap":"be8afaaf7346e69e7653db91c5b222af1a4c54ce0ca0341c1581dc4fca184be4","src/ast/snapshots/diplomat_core__ast__attrs__tests__rename-2.snap":"38d636a1f5cc48fc4655b30b47753919a65dbe64bb343c51f0db0389a30f2c84","src/ast/snapshots/diplomat_core__ast__attrs__tests__rename.snap":"38d636a1f5cc48fc4655b30b47753919a65dbe64bb343c51f0db0389a30f2c84","src/ast/snapshots/diplomat_core__ast__enums__tests__enum_with_discr.snap":"49822af14e8898641e5af907dc046e3d6abff1784ae2e5f0948a628f462f23a1","src/ast/snapshots/diplomat_core__ast__enums__tests__simple_enum.snap":"11d78b8b06a5b021dfde2420e718c2c86ab11a8781ac8a7d07ebd9b14d5b9602","src/ast/snapshots/diplomat_core__ast__methods__tests__cfged_method.snap":"4b583a1626f65d77b51ba30b18b786db5ef4499ff269eac38542e9defe1d72f1","src/ast/snapshots/diplomat_core__ast__methods__tests__nonstatic_methods-2.snap":"35843a16e1bb2c9943ca14e0f6366a6d548f325f9e76796523dea6eec8b8e572","src/ast/snapshots/diplomat_core__ast__methods__tests__nonstatic_methods.snap":"9f3b1e88def2e947b39c78c98d9f0bf14c8ca7f050fb07c9a6dbff0ce9cfcf9f","src/ast/snapshots/diplomat_core__ast__methods__tests__static_methods-2.snap":"38c82dd5a11ceedbe48fedd42ed1946029c246e9e7efa04326e8e3feacd8cda4","src/ast/snapshots/diplomat_core__ast__methods__tests__static_methods.snap":"9778bcfd3921df391c4ac06c6c9c2209276655ec14713635457056c19fbd5be4","src/ast/snapshots/diplomat_core__ast__modules__tests__import_in_non_diplomat_not_analyzed.snap":"2b3c0a5bebbf397e9c25638a4ed05c6eab16a101b52a0aa486d83fba9adc0720","src/ast/snapshots/diplomat_core__ast__modules__tests__method_visibility.snap":"98ff27c487b3fc1972f30bcbbac339139f9af94a6575102c8bee34e187500e01","src/ast/snapshots/diplomat_core__ast__modules__tests__simple_mod.snap":"90ddddbabbe40622fe91ef8da91a11dc260c82f766ddf4df2d6f032cce711cf7","src/ast/snapshots/diplomat_core__ast__structs__tests__simple_struct.snap":"96768de34fcd8ea6f97f46e4640a0352bf11b916f7943fe14f7c9e0021006a8c","src/ast/snapshots/diplomat_core__ast__types__tests__lifetimes-2.snap":"b9129f8e4a73e534e2a4fcad1f6bfd4173383273a9e5e837a075bb4017f41e0c","src/ast/snapshots/diplomat_core__ast__types__tests__lifetimes-3.snap":"01d174928534003996e696954703aa93b1d2f56ea3959b37b9954a25456fe638","src/ast/snapshots/diplomat_core__ast__types__tests__lifetimes-4.snap":"21876d9a6ee712c393bfff33d88411cfe58e70cd67e1f6b543c7831948e75386","src/ast/snapshots/diplomat_core__ast__types__tests__lifetimes-5.snap":"e5fadef9bfcd741f74fc02fe20c5ae34637accdc9eba2eb3c0d9f8109b1ac408","src/ast/snapshots/diplomat_core__ast__types__tests__lifetimes-6.snap":"97bbeeb1cd9efc7ed419ee945a878c0be3841d4b477360ead875328fdb29c739","src/ast/snapshots/diplomat_core__ast__types__tests__lifetimes-7.snap":"60f8e15979600a1fa9b0c7efec3068a57a2d9e5d9231088b2922fdc92811d26d","src/ast/snapshots/diplomat_core__ast__types__tests__lifetimes.snap":"f11b2e4daa8a7229b4fbc4dbac6816834b5367f4081b3d4c516a3bf99f5db76b","src/ast/snapshots/diplomat_core__ast__types__tests__typename_boxes-2.snap":"b2caf0ebb1085e65cb74cb2ee21cba8f5450e39309033da714d1147f55e581f0","src/ast/snapshots/diplomat_core__ast__types__tests__typename_boxes.snap":"ea241a0ed44e38134e24885382d060834536fcd8390e8924f3689cd855c81695","src/ast/snapshots/diplomat_core__ast__types__tests__typename_named.snap":"0f6e75b41366ba31a42a4c86e74b8ec48b8e7ab105a1360027a6798f6847df97","src/ast/snapshots/diplomat_core__ast__types__tests__typename_option-2.snap":"afb1c4876eb82939a122d64a507a0b5f3ca4388120ea867ffefeca8aa0419caf","src/ast/snapshots/diplomat_core__ast__types__tests__typename_option.snap":"cf8a34f9a60e7e4ac8c2eeb0b7c62847482309d14ed44c040bb8f138f529f526","src/ast/snapshots/diplomat_core__ast__types__tests__typename_primitives-2.snap":"0b9230ce53d4db086ac4976867ebd093a080e209ef301d30ec08935b48bb6896","src/ast/snapshots/diplomat_core__ast__types__tests__typename_primitives-3.snap":"6f0ab0393da6b01f42bad1e58e3702ce195346770cae3d8210b29fec8828c283","src/ast/snapshots/diplomat_core__ast__types__tests__typename_primitives.snap":"ee01e60f3b3d732ee4cba03be0c79bb8d2a5a4b12ba4127cddd2fc2727a27a15","src/ast/snapshots/diplomat_core__ast__types__tests__typename_references-2.snap":"151fc890399b2f1d22a14903105117483364999d4306ca90a3c4f9120ad1a49d","src/ast/snapshots/diplomat_core__ast__types__tests__typename_references.snap":"be5f3ae253861195227e0bd7fc6221449d7a6240cc71ca5e8e9130aaac5e4cca","src/ast/snapshots/diplomat_core__ast__types__tests__typename_result-2.snap":"5fec5ee1501c4550eaa7c1ce235295f371cbe17333fb029a1f678f877f911c1e","src/ast/snapshots/diplomat_core__ast__types__tests__typename_result-3.snap":"52f970e28ba7839db1693726443a299c7f932eedefad89a885a191645f06b830","src/ast/snapshots/diplomat_core__ast__types__tests__typename_result-4.snap":"320f11fda8d120def05728fd6dd2e84042bfa1a8195caa94142640c6b9be2fa4","src/ast/snapshots/diplomat_core__ast__types__tests__typename_result.snap":"c560eb23c0b5e67344e7588b507c1aa548381ca78a989ba41a5d6d37b3a44d91","src/ast/snapshots/diplomat_core__ast__validity__tests__lifetime_in_return.snap":"c89b6c0cca5bd544214d7b8ff473d969213289e11ca7f7b6d08b6f51503e4468","src/ast/structs.rs":"8cc878f1d90bc806ae18f3655cad69a803b4b334efd8c1ffe4cf0656ae927085","src/ast/traits.rs":"84a8e393a858d8b73664842d9a3c2b595e0f157fc83333de02e688e0502931c7","src/ast/types.rs":"ce8454682d3d6836d1c5d4bcab6d7897912b5ca83d71d528b68d9b485f397796","src/environment.rs":"eb3027ab865457d0253e742ca52f674acee99f72d45a983b33c6485f42633589","src/hir/attrs.rs":"eb8368b34a25c26f2bbd396a73e1a2335d66c49ad975956ec6dc031c1fdcee8f","src/hir/defs.rs":"04ca2d631ac5917e3fbf618d9d154db88fe2cc73be9dd487289b77367b77a9e8","src/hir/elision.rs":"b2496e8ad480562ac9cdbc9d339b98fc8ec2e2fdff0051b4ff9c9124e9067d3d","src/hir/lifetimes.rs":"a8552091663aac9af38b4e1c24e0fe23e55af0ef3db0c72da60681cd79b33e1f","src/hir/lowering.rs":"1e3f0ed267469bed78218efad7b6f0361647f42a39f81e806e1e3c5fbd5dbad3","src/hir/methods.rs":"cb5dc3ebd7252efe3ab96b5e88db0a586dc5bcee43905c2c4a16bc40311c9272","src/hir/methods/borrowing_field.rs":"8d4b8eab3cb161ddb771d327cf98b66396d5de40db7e20cb8beead58aed7344c","src/hir/methods/borrowing_param.rs":"41c2b0111d655226a55daa093363842076eae70413f1599dbd92d78822a40cb6","src/hir/mod.rs":"216ea1d3ce3cb2a7127ecc50f43aef6c3a44a2dd089882b7d995c2f9cea8ddf6","src/hir/paths.rs":"0fcbb459e92cf0acd9444669c46cd17490b60c83268c400e24b8703b7e851623","src/hir/primitives.rs":"93d52f6ef496051c1561f9d00d58b462bfb752085dd3dfd84d310ff1ed3180ac","src/hir/snapshots/diplomat_core__hir__attrs__tests__auto.snap":"2cf071682c620e836da60aa88f8f7d3ab795f243b14992001a0f37ef4c35fc11","src/hir/snapshots/diplomat_core__hir__attrs__tests__comparator.snap":"c8708974280e72957d05601f69c008590eca30f5c8344a7f9e68bd4f5ea1bd9f","src/hir/snapshots/diplomat_core__hir__attrs__tests__iterator.snap":"cb0a8e763a9799d9814626109105e308f25ba421e72824f7217a0f5cd0518e34","src/hir/snapshots/diplomat_core__hir__attrs__tests__unsupported_features.snap":"a1167949471515c81fe369ebf69442ec79dadf7fd580dad828e313a5e38b5d85","src/hir/snapshots/diplomat_core__hir__elision__tests__borrowing_fields.snap":"e320338939daed60f8c5af2017a3ff4757224437aefebb9dbafc2bc2e014fa3f","src/hir/snapshots/diplomat_core__hir__elision__tests__elision_in_struct.snap":"a5086b6d646823431937618a7027c707b4ae8183835e5a9be85e91e5d10f4d33","src/hir/snapshots/diplomat_core__hir__elision__tests__simple_mod.snap":"fbdc0c10bacfdd69a5ca51d7dd9c132f159ecf6be15c258bb7556a9fe4950654","src/hir/snapshots/diplomat_core__hir__type_context__tests__basic_lowering.snap":"2c301b9cf25f8e66566c8ea19121199a8b6dfb57e03c3d556a1447d2d08de8c9","src/hir/snapshots/diplomat_core__hir__type_context__tests__lifetime_in_return.snap":"7b10b9def24b0435d6b3889156dd7bda9706afbc0d0e26b821e8f2f057ce7faf","src/hir/snapshots/diplomat_core__hir__type_context__tests__non_opaque_move.snap":"86b475c55ab1130901b52a08438eea69f066057b79c35e34c64d3ca26182d45e","src/hir/snapshots/diplomat_core__hir__type_context__tests__opaque_checks_with_error.snap":"a501cdd3f1869fe6977166c9a25c78ba113d169dbe396c7ef28d2e9cda514795","src/hir/snapshots/diplomat_core__hir__type_context__tests__opaque_checks_with_safe_use.snap":"f0798b0c35a9737e22fd12982aa8103a16cf41770c3bfec0ee8275202bb5fbe4","src/hir/snapshots/diplomat_core__hir__type_context__tests__opaque_ffi.snap":"dbaa6e4bafb2ad2e371b4cd2f3a174dfa54879e956eacc8adc832fc1ecadd21d","src/hir/snapshots/diplomat_core__hir__type_context__tests__option.snap":"35f6f13ccc2a9422df499cd7dcc88d27afbfa8f60e3cd3a781eb1cd251e9564c","src/hir/snapshots/diplomat_core__hir__type_context__tests__option_invalid.snap":"e58a6c2fa6121e6b865d5bed39a8014651e152c5bd86fc53b5f3dc318bffd8e5","src/hir/snapshots/diplomat_core__hir__type_context__tests__option_valid.snap":"de35f4688b545c2069d6e21f2a7de61ac91fb9c9833fd2175b5dd9d9927afbd0","src/hir/snapshots/diplomat_core__hir__type_context__tests__required_implied_bounds.snap":"78b3d1af6a11e9c7716352ce609b8d8595cf9b197ec75d6d8adb7fb02723f91d","src/hir/snapshots/diplomat_core__hir__type_context__tests__struct_forbidden.snap":"705c90764215473972a8dcb1a204659ab58d71872c73470da8b50d3e0c54d188","src/hir/snapshots/diplomat_core__hir__type_context__tests__zst_non_opaque.snap":"a0b3d437ff0eb6ca44a1147fe1df5418e019366b0af15e8338e273c4400539f5","src/hir/ty_position.rs":"c21f94a375301c8485d35ae47a5a0bc35c32e8c07c025ccc5bee079bf03278c6","src/hir/type_context.rs":"b6f3dd0672062906be2bfeea4d1d211e8fafdadcdf115b1d795ffd3c0e83cd35","src/hir/types.rs":"bd20ac65fad73819ed6db1b74b6b704c384fc62f7e67cfb62775589c26f3d178","src/lib.rs":"7223d5d1973615128a2e6eb39deb6eeb7c5ff9c206371fd6abca69f4b30417fb"}}
\ No newline at end of file
diff --git a/crates/diplomat_core/.cargo-checksum.json b/crates/diplomat_core/.cargo-checksum.json
new file mode 100644
index 0000000..30bdd09
--- /dev/null
+++ b/crates/diplomat_core/.cargo-checksum.json
@@ -0,0 +1 @@
+{"files":{"Cargo.toml":"99c49d34e32f5be3057736911087705b3a049c7018d2ac5c71e776e619a85ff9","LICENSE-APACHE":"639c20c7f14fb122750d5ad1a6cfb116d9bf8d103e709ee40949e5a12a731666","LICENSE-MIT":"3337fe6e4a3830ad87c23cb9d6d750f9a1e5c45efc08de9c76c1a207fc6966c4","src/ast/attrs.rs":"e666895ad0adb5fd38f622583a4d1c27e64e08fdbaf9119e88e106a1179abd9e","src/ast/docs.rs":"40a8d0b6efa46a20c92cbabd3df786b286b6279a532855ad83d9b2097d420f88","src/ast/enums.rs":"ca2d018a3cf3cb6d0b075d2c8f19015e870a050cd5d1f0ae730738e9b0e4b138","src/ast/idents.rs":"05c7245ce6cb0d46b070f92a4996247545e1699da66d546bfd039a1e3a1a717d","src/ast/lifetimes.rs":"472ceed18ae7d74c0cef200274d677b8dd57b2cda938de111ce6fd619aadbf7b","src/ast/methods.rs":"4b889c34c8029909b1eda2bc6b764d5ac5feb720b45ba34baad38538570a2643","src/ast/mod.rs":"a398ae20d74cd36c18805c8d7b48ab668746f7a3aadeba6a3dcf15b5322687cb","src/ast/modules.rs":"5638e1ef521a96daf52ae2cf1d0aee8d9d15554ee2ffc5abd1708d30ed29b6b8","src/ast/paths.rs":"480bef69cb395eb931a7e086ccde7b49f508ace10e61367002c4b072b9dd4b44","src/ast/snapshots/diplomat_core__ast__attrs__tests__attr.snap":"a67d1d88995e5140fae1b85175885c79e833a6c09d30df164bb800cf70bb9725","src/ast/snapshots/diplomat_core__ast__attrs__tests__cfgs-2.snap":"1fbb268bb55cad81803555e0552948bfa9ee1edc161f76b0e26bb5c16ac8389b","src/ast/snapshots/diplomat_core__ast__attrs__tests__cfgs-3.snap":"ed4ba8423cb7605fd7a5e3faf35baafa58ce6758b50da2c98ccc0cd60792bfff","src/ast/snapshots/diplomat_core__ast__attrs__tests__cfgs-4.snap":"ed4ba8423cb7605fd7a5e3faf35baafa58ce6758b50da2c98ccc0cd60792bfff","src/ast/snapshots/diplomat_core__ast__attrs__tests__cfgs-5.snap":"2cbfa3d75adb944b19a4e0abdfa5cf7f9a4f82cdcd4d4b0efdbf1c857dc12aaf","src/ast/snapshots/diplomat_core__ast__attrs__tests__cfgs.snap":"0f42d7d22c82d1950aea7a59ead36701d6d572efe0d0969608afa58d8780e052","src/ast/snapshots/diplomat_core__ast__attrs__tests__rename-2.snap":"e790b3af120d71bb30b89b3a9b803cb42c58cf2f77101517cc3aa718497aa65c","src/ast/snapshots/diplomat_core__ast__attrs__tests__rename.snap":"e790b3af120d71bb30b89b3a9b803cb42c58cf2f77101517cc3aa718497aa65c","src/ast/snapshots/diplomat_core__ast__enums__tests__enum_with_discr.snap":"b89a5ec4a27f4b37a9579e177bede43fa6d453dd613ba4bc36e8e8e85ec92e7e","src/ast/snapshots/diplomat_core__ast__enums__tests__simple_enum.snap":"ff5ff54221eb87d980ed7bc1d1377b335b918dcea53caa3f4b9345b2e4b105f4","src/ast/snapshots/diplomat_core__ast__methods__tests__cfged_method.snap":"c40532110650d39deec501fdff4fe69d9513adb40a787eff60391926918f2b04","src/ast/snapshots/diplomat_core__ast__methods__tests__nonstatic_methods-2.snap":"af48687633e88c1de24495b7dc8d6cbdf2972e9316e0d3f83fbe1f6686595634","src/ast/snapshots/diplomat_core__ast__methods__tests__nonstatic_methods.snap":"1ad9b01794280d738c76fc4dc2bd561f7177eb93855ff37d6ddc40d3e587c48e","src/ast/snapshots/diplomat_core__ast__methods__tests__static_methods-2.snap":"8af6ba37c419c4d9dc4e61a33f87585d25b0ef51716c216e0372f8c1952cf053","src/ast/snapshots/diplomat_core__ast__methods__tests__static_methods.snap":"c9ea1881533ce39fa7d9dc9ccc0949db2040f1828d260a1bacac870dc3b84863","src/ast/snapshots/diplomat_core__ast__modules__tests__import_in_non_diplomat_not_analyzed.snap":"d3abb336a5ef69b2e2264a29c629fb21e38923274c4e1925bcdb45036d568178","src/ast/snapshots/diplomat_core__ast__modules__tests__method_visibility.snap":"d9f6d2a0e2fad3234286a9059864c10a430e4fb52d7f02e30cf28e8fd7adc89b","src/ast/snapshots/diplomat_core__ast__modules__tests__simple_mod.snap":"036eb71424c981ce651daf118f378977ef1d9a12f044f14257954e6c6dabea15","src/ast/snapshots/diplomat_core__ast__structs__tests__simple_struct.snap":"f0b5e2a7da2fe0e50d1b123b2b746891b35cbd79d6de1394217007d90cc2fa0d","src/ast/snapshots/diplomat_core__ast__types__tests__lifetimes-2.snap":"75c377161d51dc0c4d7d91b59b04c1de62b5dffaa1a2c9cac7841ce359f82d9d","src/ast/snapshots/diplomat_core__ast__types__tests__lifetimes-3.snap":"418ff306adad93373b277f1a2fa02d1c9e66c54534d2f675288e05dcdec9696f","src/ast/snapshots/diplomat_core__ast__types__tests__lifetimes-4.snap":"f7bad0025bf2c64a18e8dd7d4a5c43f2b5ccbd4ff88e9bd2f78c226fb3f22540","src/ast/snapshots/diplomat_core__ast__types__tests__lifetimes-5.snap":"9a7b7dde9399f300bfcd739506ded5113021880629d75291b962984ec1e8fe8c","src/ast/snapshots/diplomat_core__ast__types__tests__lifetimes-6.snap":"ff1e3b2867786c467ef1148d2837da441a23e4b32d7a284a438ff5c317bafc81","src/ast/snapshots/diplomat_core__ast__types__tests__lifetimes-7.snap":"291a10613d0128d3b4cf9ef4eeea0ca43d5266c539f9a8ff130addef140bacac","src/ast/snapshots/diplomat_core__ast__types__tests__lifetimes.snap":"6306fae6bc6fbaf5eb679bf3313877259f9ccc09232da568fd98fe8e20eab284","src/ast/snapshots/diplomat_core__ast__types__tests__typename_boxes-2.snap":"97946b1e3118aec7ac7490f0bf38ba45013bb63c5d6bac12e3dd20fc7103348f","src/ast/snapshots/diplomat_core__ast__types__tests__typename_boxes.snap":"e3a1a07e2bfa0869918c973983b658a8e4123203f86a54a743dc6aff6a984f65","src/ast/snapshots/diplomat_core__ast__types__tests__typename_named.snap":"30b58ef9e59641eea9dfba9b965256d7a5d8667e241b7a209eb7d09208d75f7e","src/ast/snapshots/diplomat_core__ast__types__tests__typename_option-2.snap":"c90606884d4100b2d1c13822452743df2d771eec673c181eaf52808b0ba438e6","src/ast/snapshots/diplomat_core__ast__types__tests__typename_option.snap":"b60052fe5d5d553e8c9ed9995ffefbb8d0a414c567c8a58675c3cc5394392d73","src/ast/snapshots/diplomat_core__ast__types__tests__typename_primitives-2.snap":"506dcd6ea86644d4043229804bf615c8bc34cd20feef2204cd755674bf3cd00f","src/ast/snapshots/diplomat_core__ast__types__tests__typename_primitives-3.snap":"b92cd7f7cb2877c83ed8fac16d27b670e0e9d501cd6b3f70db84b322b686405f","src/ast/snapshots/diplomat_core__ast__types__tests__typename_primitives.snap":"c086765d65faac19f529a03f50ca0566cfe4e9a10b32f608ae4b9c3ad27ffdad","src/ast/snapshots/diplomat_core__ast__types__tests__typename_references-2.snap":"3ced7f6c3e2078d9534dff84e5e4a200efa84bf94435fde478c9e02b4a2b2053","src/ast/snapshots/diplomat_core__ast__types__tests__typename_references.snap":"77d5bcd560adff2216e349e0c87cff988501aef22848fbf1906b6b622ffe47cf","src/ast/snapshots/diplomat_core__ast__types__tests__typename_result-2.snap":"aafb55a23e6be26ea03ece0595e0fa0c451bdba51f606ecaa2cddea7b21cd642","src/ast/snapshots/diplomat_core__ast__types__tests__typename_result-3.snap":"5cc176444520a4ebf76c184d4c33643d83a87f18a4856b2565d41de79798e831","src/ast/snapshots/diplomat_core__ast__types__tests__typename_result-4.snap":"64addbbcc9efce4ff6b98856154ce695271dc9dd74e9d921b35c2b0cf55e4bbe","src/ast/snapshots/diplomat_core__ast__types__tests__typename_result.snap":"d67d6adbd0d814503f4bcde00d65ee3e7dd353a4a6cf96b4206f145e46fbcddb","src/ast/snapshots/diplomat_core__ast__validity__tests__lifetime_in_return.snap":"aa05830ac368b2d10bc9403aaa2c8afc2345a2e21ff86e2089a0e664fde16254","src/ast/structs.rs":"6f3137e7dfd82621f9cc03c48c30a078939b274e64a528f46d4f29e82bb67146","src/ast/traits.rs":"edc878c09b52bc28dc59ed799eab4be11775a6c6625f5c075c521b4b4969f124","src/ast/types.rs":"acec39566d9e7ba9ac852d97e711e7608b156aaead2c1ded4bcad75eedc2a398","src/environment.rs":"85005a4afabd0b1c22ca17d7ba12ef71783e9c001359037c714e13788f3e39a4","src/hir/attrs.rs":"76cbf095e79b096d66bc3c2dcf491e455af4b7ec2efe9a89bedc5e3ffd7a6793","src/hir/defs.rs":"e69c37ecba56d8aa3dd73659132fd568cd37786f671bd3519aa4395cb490772c","src/hir/elision.rs":"0500b3ce8f064ccb255078c9232b8195d399d9a17a5988be8fb823a2e07b5d08","src/hir/lifetimes.rs":"f816dcc83401f027c602eab3111b1eac02b1f7b80efdc2a1ad20aa58bd460c9c","src/hir/lowering.rs":"d383584ad424ae5459671486ece82fe4d2a227eb029efe5b30aff9ee0d4f3b97","src/hir/methods.rs":"3e3e47bb6c751d62afd4158c6cafcd162b93a54ee4c874d0fcb572c052d39cd8","src/hir/methods/borrowing_field.rs":"f2b478e116ce5417875745271465650156296658b8b85de4b2f191268cf62f1d","src/hir/methods/borrowing_param.rs":"7a98fe6bc2918a22e3530172629fe8bc1097e3ae37fa7f066f3fc1a014070807","src/hir/mod.rs":"04cbd8a20366e6954384dc897b1493e9666d3e9d5f65040c3dfedafd3ffb3f83","src/hir/paths.rs":"917e63a413c97c0a8a3763fa38ef29ac2b75464171d4432bac7dc748fdcbe276","src/hir/primitives.rs":"863200474471f7bbedd52756949625d929f7bee11bd5baf95bc4bc57dce41fe1","src/hir/snapshots/diplomat_core__hir__attrs__tests__auto.snap":"6b564da738c33c30d5fa2d980e963ff5ceaee3f6e225bbaed46fdd57ffc09b52","src/hir/snapshots/diplomat_core__hir__attrs__tests__comparator.snap":"e3e722be30320849899344aa96b9635c6918ac0579b48e43952908a6d5a81cd9","src/hir/snapshots/diplomat_core__hir__attrs__tests__iterator.snap":"a38579d2737691b3ce6185fd6fc3398210e8bf115add9ba186a80d77684dd940","src/hir/snapshots/diplomat_core__hir__attrs__tests__unsupported_features.snap":"35dc32635b7b55cb877d4c3158e56352ef54de6ccb95ebe03d915c2c8bc71df4","src/hir/snapshots/diplomat_core__hir__elision__tests__borrowing_fields.snap":"6c2a9c1135f6259df281ed47760469849f21858be7c02caa4d47bcd77fc51f9a","src/hir/snapshots/diplomat_core__hir__elision__tests__elision_in_struct.snap":"0aa039c732a81c74e41e2ff8d0c32976a2cc89c1f8559a12aea6de743a97dc71","src/hir/snapshots/diplomat_core__hir__elision__tests__simple_mod.snap":"731ae15583212d5a558bf0b3bb62442fbf48803c10f47e067e0db14d6ec0aad9","src/hir/snapshots/diplomat_core__hir__type_context__tests__basic_lowering.snap":"c3f453168b41a7268e6a3b022cb1c33822962df9fa683395ce759a8db4dd9714","src/hir/snapshots/diplomat_core__hir__type_context__tests__lifetime_in_return.snap":"6729419ac3c2fc2132dc8a7c12ed6b1383657ce33538a4d12b89fe73d65b036b","src/hir/snapshots/diplomat_core__hir__type_context__tests__non_opaque_move.snap":"dfc9bcce400229c476381b135958ffbc5b4850bdf36c3aa059a96bc28d627680","src/hir/snapshots/diplomat_core__hir__type_context__tests__opaque_checks_with_error.snap":"1c9a5bffce9ccf66732caf6b379a6523eff53e533ebf4adbecfb169b2a384d60","src/hir/snapshots/diplomat_core__hir__type_context__tests__opaque_checks_with_safe_use.snap":"c3dee2afc274a8b70bf686ad9970826d5b59b798eecabb70a4b7bdb272816bab","src/hir/snapshots/diplomat_core__hir__type_context__tests__opaque_ffi.snap":"37d1f25e0a479e960194f2ab351ee7977e35def2354501713ad6fbf3a98670c0","src/hir/snapshots/diplomat_core__hir__type_context__tests__option.snap":"1bf15a0b08a68742cb9f8a16e0aa1157596faf7c5e24e01e529633864a30efa9","src/hir/snapshots/diplomat_core__hir__type_context__tests__option_invalid.snap":"608a711234cd1c19d0faa750065d4eb4f3357ad1f49e06f24260ad91b7721a95","src/hir/snapshots/diplomat_core__hir__type_context__tests__option_valid.snap":"d29eabf19445e75454532259015011b06ec37d9cc9209476360d75bd75c61bfb","src/hir/snapshots/diplomat_core__hir__type_context__tests__required_implied_bounds.snap":"67bb19010f4db7eeb8261c7a6d31f59107340507d525f2a993a0485aff06519b","src/hir/snapshots/diplomat_core__hir__type_context__tests__struct_forbidden.snap":"36852df51a118656dbd474858d6466ab803c0ac2a17b80e77c52a319b791298c","src/hir/snapshots/diplomat_core__hir__type_context__tests__zst_non_opaque.snap":"e78ca15abab15a89da23c6f8f08bd6534331e7ca37e599061150615b1c674fbf","src/hir/ty_position.rs":"1a8031ba91796ece482570df20130604ac69cad4b499e0ae2244d6e6bf26962f","src/hir/type_context.rs":"ed8947e6591541501a1eb4063e0fe52e8e125377c822f8a2b9458cd6fd4f9591","src/hir/types.rs":"663c327f1b93733280f9cf4947f8839581e5fc14c9242b262f15fcb018c0a941","src/lib.rs":"6bca84ef449b737270c0da7e8ae9dc82cfbd57e83c078b20c9894050274ed7e7"},"package":"58e5ba87fee6b8b9dcc575cfbc84ae97b8b9f891fa27f670996a4684e20bd178"}
\ No newline at end of file
diff --git a/crates/diplomat_core/Android.bp b/crates/diplomat_core/Android.bp
new file mode 100644
index 0000000..711ff1c
--- /dev/null
+++ b/crates/diplomat_core/Android.bp
@@ -0,0 +1,39 @@
+// This file is generated by cargo_embargo.
+// Do not modify this file because the changes will be overridden on upgrade.
+
+package {
+    default_applicable_licenses: ["external_rust_crates_diplomat_core_license"],
+    default_team: "trendy_team_android_rust",
+}
+
+license {
+    name: "external_rust_crates_diplomat_core_license",
+    visibility: [":__subpackages__"],
+    license_kinds: ["SPDX-license-identifier-Apache-2.0"],
+    license_text: ["LICENSE"],
+}
+
+rust_library_host {
+    name: "libdiplomat_core",
+    host_cross_supported: false,
+    crate_name: "diplomat_core",
+    cargo_env_compat: true,
+    cargo_pkg_version: "0.9.0",
+    crate_root: "src/lib.rs",
+    edition: "2021",
+    rustlibs: [
+        "libproc_macro2",
+        "libquote",
+        "libserde",
+        "libsmallvec",
+        "libstrck",
+        "libsyn",
+    ],
+    compile_multilib: "first",
+}
+
+dirgroup {
+    name: "trusty_dirgroup_external_rust_crates_diplomat_core",
+    visibility: ["//trusty/vendor/google/aosp/scripts"],
+    dirs: ["."],
+}
diff --git a/crates/diplomat_core/Cargo.toml b/crates/diplomat_core/Cargo.toml
new file mode 100644
index 0000000..df807f7
--- /dev/null
+++ b/crates/diplomat_core/Cargo.toml
@@ -0,0 +1,89 @@
+# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO
+#
+# When uploading crates to the registry Cargo will automatically
+# "normalize" Cargo.toml files for maximal compatibility
+# with all versions of Cargo and also rewrite `path` dependencies
+# to registry (e.g., crates.io) dependencies.
+#
+# If you are reading this file be aware that the original Cargo.toml
+# will likely look very different (and much more reasonable).
+# See Cargo.toml.orig for the original contents.
+
+[package]
+edition = "2021"
+rust-version = "1.71"
+name = "diplomat_core"
+version = "0.9.0"
+authors = [
+    "Shadaj Laddad <shadaj@users.noreply.github.com>",
+    "Manish Goregaokar <manishsmail@gmail.com>",
+    "Quinn Okabayashi <QnnOkabayashi@users.noreply.github.com>",
+]
+build = false
+autobins = false
+autoexamples = false
+autotests = false
+autobenches = false
+description = "Shared utilities between Diplomat macros and code generation"
+readme = false
+keywords = [
+    "ffi",
+    "codegen",
+]
+categories = [
+    "development-tools",
+    "compilers",
+]
+license = "MIT OR Apache-2.0"
+repository = "https://github.com/rust-diplomat/diplomat"
+
+[package.metadata.docs.rs]
+all-features = true
+
+[lib]
+name = "diplomat_core"
+path = "src/lib.rs"
+
+[dependencies.displaydoc]
+version = "0.2"
+optional = true
+
+[dependencies.either]
+version = "1.9.0"
+optional = true
+default-features = false
+
+[dependencies.proc-macro2]
+version = "1.0.27"
+
+[dependencies.quote]
+version = "1.0"
+
+[dependencies.serde]
+version = "1.0"
+features = [
+    "derive",
+    "alloc",
+]
+default-features = false
+
+[dependencies.smallvec]
+version = "1.9.0"
+
+[dependencies.strck]
+version = "1.0"
+features = ["ident"]
+
+[dependencies.syn]
+version = "2"
+features = [
+    "full",
+    "extra-traits",
+]
+
+[dev-dependencies.insta]
+version = "1.7.1"
+features = ["yaml"]
+
+[features]
+hir = ["either"]
diff --git a/crates/diplomat_core/LICENSE b/crates/diplomat_core/LICENSE
new file mode 120000
index 0000000..6b579aa
--- /dev/null
+++ b/crates/diplomat_core/LICENSE
@@ -0,0 +1 @@
+LICENSE-APACHE
\ No newline at end of file
diff --git a/crates/diplomat_core/LICENSE-APACHE b/crates/diplomat_core/LICENSE-APACHE
new file mode 100644
index 0000000..05fcffa
--- /dev/null
+++ b/crates/diplomat_core/LICENSE-APACHE
@@ -0,0 +1,201 @@
+                              Apache License
+                        Version 2.0, January 2004
+                     http://www.apache.org/licenses/
+
+TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+1. Definitions.
+
+   "License" shall mean the terms and conditions for use, reproduction,
+   and distribution as defined by Sections 1 through 9 of this document.
+
+   "Licensor" shall mean the copyright owner or entity authorized by
+   the copyright owner that is granting the License.
+
+   "Legal Entity" shall mean the union of the acting entity and all
+   other entities that control, are controlled by, or are under common
+   control with that entity. For the purposes of this definition,
+   "control" means (i) the power, direct or indirect, to cause the
+   direction or management of such entity, whether by contract or
+   otherwise, or (ii) ownership of fifty percent (50%) or more of the
+   outstanding shares, or (iii) beneficial ownership of such entity.
+
+   "You" (or "Your") shall mean an individual or Legal Entity
+   exercising permissions granted by this License.
+
+   "Source" form shall mean the preferred form for making modifications,
+   including but not limited to software source code, documentation
+   source, and configuration files.
+
+   "Object" form shall mean any form resulting from mechanical
+   transformation or translation of a Source form, including but
+   not limited to compiled object code, generated documentation,
+   and conversions to other media types.
+
+   "Work" shall mean the work of authorship, whether in Source or
+   Object form, made available under the License, as indicated by a
+   copyright notice that is included in or attached to the work
+   (an example is provided in the Appendix below).
+
+   "Derivative Works" shall mean any work, whether in Source or Object
+   form, that is based on (or derived from) the Work and for which the
+   editorial revisions, annotations, elaborations, or other modifications
+   represent, as a whole, an original work of authorship. For the purposes
+   of this License, Derivative Works shall not include works that remain
+   separable from, or merely link (or bind by name) to the interfaces of,
+   the Work and Derivative Works thereof.
+
+   "Contribution" shall mean any work of authorship, including
+   the original version of the Work and any modifications or additions
+   to that Work or Derivative Works thereof, that is intentionally
+   submitted to Licensor for inclusion in the Work by the copyright owner
+   or by an individual or Legal Entity authorized to submit on behalf of
+   the copyright owner. For the purposes of this definition, "submitted"
+   means any form of electronic, verbal, or written communication sent
+   to the Licensor or its representatives, including but not limited to
+   communication on electronic mailing lists, source code control systems,
+   and issue tracking systems that are managed by, or on behalf of, the
+   Licensor for the purpose of discussing and improving the Work, but
+   excluding communication that is conspicuously marked or otherwise
+   designated in writing by the copyright owner as "Not a Contribution."
+
+   "Contributor" shall mean Licensor and any individual or Legal Entity
+   on behalf of whom a Contribution has been received by Licensor and
+   subsequently incorporated within the Work.
+
+2. Grant of Copyright License. Subject to the terms and conditions of
+   this License, each Contributor hereby grants to You a perpetual,
+   worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+   copyright license to reproduce, prepare Derivative Works of,
+   publicly display, publicly perform, sublicense, and distribute the
+   Work and such Derivative Works in Source or Object form.
+
+3. Grant of Patent License. Subject to the terms and conditions of
+   this License, each Contributor hereby grants to You a perpetual,
+   worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+   (except as stated in this section) patent license to make, have made,
+   use, offer to sell, sell, import, and otherwise transfer the Work,
+   where such license applies only to those patent claims licensable
+   by such Contributor that are necessarily infringed by their
+   Contribution(s) alone or by combination of their Contribution(s)
+   with the Work to which such Contribution(s) was submitted. If You
+   institute patent litigation against any entity (including a
+   cross-claim or counterclaim in a lawsuit) alleging that the Work
+   or a Contribution incorporated within the Work constitutes direct
+   or contributory patent infringement, then any patent licenses
+   granted to You under this License for that Work shall terminate
+   as of the date such litigation is filed.
+
+4. Redistribution. You may reproduce and distribute copies of the
+   Work or Derivative Works thereof in any medium, with or without
+   modifications, and in Source or Object form, provided that You
+   meet the following conditions:
+
+   (a) You must give any other recipients of the Work or
+       Derivative Works a copy of this License; and
+
+   (b) You must cause any modified files to carry prominent notices
+       stating that You changed the files; and
+
+   (c) You must retain, in the Source form of any Derivative Works
+       that You distribute, all copyright, patent, trademark, and
+       attribution notices from the Source form of the Work,
+       excluding those notices that do not pertain to any part of
+       the Derivative Works; and
+
+   (d) If the Work includes a "NOTICE" text file as part of its
+       distribution, then any Derivative Works that You distribute must
+       include a readable copy of the attribution notices contained
+       within such NOTICE file, excluding those notices that do not
+       pertain to any part of the Derivative Works, in at least one
+       of the following places: within a NOTICE text file distributed
+       as part of the Derivative Works; within the Source form or
+       documentation, if provided along with the Derivative Works; or,
+       within a display generated by the Derivative Works, if and
+       wherever such third-party notices normally appear. The contents
+       of the NOTICE file are for informational purposes only and
+       do not modify the License. You may add Your own attribution
+       notices within Derivative Works that You distribute, alongside
+       or as an addendum to the NOTICE text from the Work, provided
+       that such additional attribution notices cannot be construed
+       as modifying the License.
+
+   You may add Your own copyright statement to Your modifications and
+   may provide additional or different license terms and conditions
+   for use, reproduction, or distribution of Your modifications, or
+   for any such Derivative Works as a whole, provided Your use,
+   reproduction, and distribution of the Work otherwise complies with
+   the conditions stated in this License.
+
+5. Submission of Contributions. Unless You explicitly state otherwise,
+   any Contribution intentionally submitted for inclusion in the Work
+   by You to the Licensor shall be under the terms and conditions of
+   this License, without any additional terms or conditions.
+   Notwithstanding the above, nothing herein shall supersede or modify
+   the terms of any separate license agreement you may have executed
+   with Licensor regarding such Contributions.
+
+6. Trademarks. This License does not grant permission to use the trade
+   names, trademarks, service marks, or product names of the Licensor,
+   except as required for reasonable and customary use in describing the
+   origin of the Work and reproducing the content of the NOTICE file.
+
+7. Disclaimer of Warranty. Unless required by applicable law or
+   agreed to in writing, Licensor provides the Work (and each
+   Contributor provides its Contributions) on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+   implied, including, without limitation, any warranties or conditions
+   of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+   PARTICULAR PURPOSE. You are solely responsible for determining the
+   appropriateness of using or redistributing the Work and assume any
+   risks associated with Your exercise of permissions under this License.
+
+8. Limitation of Liability. In no event and under no legal theory,
+   whether in tort (including negligence), contract, or otherwise,
+   unless required by applicable law (such as deliberate and grossly
+   negligent acts) or agreed to in writing, shall any Contributor be
+   liable to You for damages, including any direct, indirect, special,
+   incidental, or consequential damages of any character arising as a
+   result of this License or out of the use or inability to use the
+   Work (including but not limited to damages for loss of goodwill,
+   work stoppage, computer failure or malfunction, or any and all
+   other commercial damages or losses), even if such Contributor
+   has been advised of the possibility of such damages.
+
+9. Accepting Warranty or Additional Liability. While redistributing
+   the Work or Derivative Works thereof, You may choose to offer,
+   and charge a fee for, acceptance of support, warranty, indemnity,
+   or other liability obligations and/or rights consistent with this
+   License. However, in accepting such obligations, You may act only
+   on Your own behalf and on Your sole responsibility, not on behalf
+   of any other Contributor, and only if You agree to indemnify,
+   defend, and hold each Contributor harmless for any liability
+   incurred by, or claims asserted against, such Contributor by reason
+   of your accepting any such warranty or additional liability.
+
+END OF TERMS AND CONDITIONS
+
+APPENDIX: How to apply the Apache License to your work.
+
+   To apply the Apache License to your work, attach the following
+   boilerplate notice, with the fields enclosed by brackets "[]"
+   replaced with your own identifying information. (Don't include
+   the brackets!)  The text should be enclosed in the appropriate
+   comment syntax for the file format. We also recommend that a
+   file or class name and description of purpose be included on the
+   same "printed page" as the copyright notice for easier
+   identification within third-party archives.
+
+Copyright 2022 The Diplomat Developers
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+	http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
diff --git a/crates/diplomat_core/LICENSE-MIT b/crates/diplomat_core/LICENSE-MIT
new file mode 100644
index 0000000..cf62c31
--- /dev/null
+++ b/crates/diplomat_core/LICENSE-MIT
@@ -0,0 +1,27 @@
+MIT License
+
+Copyright (c) 2022 The Diplomat Developers
+
+Permission is hereby granted, free of charge, to any
+person obtaining a copy of this software and associated
+documentation files (the "Software"), to deal in the
+Software without restriction, including without
+limitation the rights to use, copy, modify, merge,
+publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software
+is furnished to do so, subject to the following
+conditions:
+
+The above copyright notice and this permission notice
+shall be included in all copies or substantial portions
+of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
+ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
+TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
+PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
+SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
+IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+DEALINGS IN THE SOFTWARE.
diff --git a/crates/diplomat_core/METADATA b/crates/diplomat_core/METADATA
new file mode 100644
index 0000000..6732ebd
--- /dev/null
+++ b/crates/diplomat_core/METADATA
@@ -0,0 +1,17 @@
+name: "diplomat_core"
+description: "Shared utilities between Diplomat macros and code generation"
+third_party {
+  version: "0.9.0"
+  license_type: NOTICE
+  last_upgrade_date {
+    year: 2025
+    month: 1
+    day: 24
+  }
+  homepage: "https://crates.io/crates/diplomat_core"
+  identifier {
+    type: "Archive"
+    value: "https://static.crates.io/crates/diplomat_core/diplomat_core-0.9.0.crate"
+    version: "0.9.0"
+  }
+}
diff --git a/crates/diplomat_core/MODULE_LICENSE_APACHE2 b/crates/diplomat_core/MODULE_LICENSE_APACHE2
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/crates/diplomat_core/MODULE_LICENSE_APACHE2
diff --git a/crates/diplomat_core/cargo_embargo.json b/crates/diplomat_core/cargo_embargo.json
new file mode 100644
index 0000000..fdb7cb5
--- /dev/null
+++ b/crates/diplomat_core/cargo_embargo.json
@@ -0,0 +1,10 @@
+{
+  "package": {
+    "diplomat_core": {
+      "device_supported": false,
+      "host_first_multilib": true
+    }
+  },
+  "run_cargo": false,
+  "generate_rulesmk": true
+}
\ No newline at end of file
diff --git a/crates/diplomat_core/src/ast/attrs.rs b/crates/diplomat_core/src/ast/attrs.rs
new file mode 100644
index 0000000..fda31b9
--- /dev/null
+++ b/crates/diplomat_core/src/ast/attrs.rs
@@ -0,0 +1,493 @@
+//! This module contains utilities for dealing with Rust attributes
+
+use serde::ser::{SerializeStruct, Serializer};
+use serde::Serialize;
+use std::borrow::Cow;
+use std::convert::Infallible;
+use std::str::FromStr;
+use syn::parse::{Error as ParseError, Parse, ParseStream};
+use syn::{Attribute, Expr, Ident, Lit, LitStr, Meta, MetaList, Token};
+
+/// The list of attributes on a type. All attributes except `attrs` (HIR attrs) are
+/// potentially read by the diplomat macro and the AST backends, anything that is not should
+/// be added as an HIR attribute ([`crate::hir::Attrs`]).
+///
+/// # Inheritance
+///
+/// Attributes are typically "inherited": the attributes on a module
+/// apply to all types and methods with it, the attributes on an impl apply to all
+/// methods in it, and the attributes on an enum apply to all variants within it.
+/// This allows the user to specify a single attribute to affect multiple fields.
+///
+/// However, the details of inheritance are not always the same for each attribute. For example, rename attributes
+/// on a module only apply to the types within it (others methods would get doubly renamed).
+///
+/// Each attribute here documents its inheritance behavior. Note that the HIR attributes do not get inherited
+/// during AST construction, since at that time it's unclear which of those attributes are actually available.
+#[derive(Clone, PartialEq, Eq, Hash, Debug, Default)]
+#[non_exhaustive]
+pub struct Attrs {
+    /// The regular #[cfg()] attributes. Inherited, though the inheritance onto methods is the
+    /// only relevant one here.
+    pub cfg: Vec<Attribute>,
+    /// HIR backend attributes.
+    ///
+    /// Inherited, but only during lowering. See [`crate::hir::Attrs`] for details on which HIR attributes are inherited.
+    ///
+    /// During AST attribute inheritance, HIR backend attributes are copied over from impls to their methods since the HIR does
+    /// not see the impl blocks.
+    pub attrs: Vec<DiplomatBackendAttr>,
+
+    /// Renames to apply to the underlying C symbol. Can be found on methods, impls, and bridge modules, and is inherited.
+    ///
+    /// Affects method names when inherited onto methods.
+    ///
+    /// Affects destructor names when inherited onto types.
+    ///
+    /// Inherited.
+    pub abi_rename: RenameAttr,
+
+    /// For use by [`crate::hir::Attrs::demo_attrs`]
+    pub demo_attrs: Vec<DemoBackendAttr>,
+}
+
+impl Attrs {
+    fn add_attr(&mut self, attr: Attr) {
+        match attr {
+            Attr::Cfg(attr) => self.cfg.push(attr),
+            Attr::DiplomatBackend(attr) => self.attrs.push(attr),
+            Attr::CRename(rename) => self.abi_rename.extend(&rename),
+            Attr::DemoBackend(attr) => self.demo_attrs.push(attr),
+        }
+    }
+
+    /// Get a copy of these attributes for use in inheritance, masking out things
+    /// that should not be inherited
+    pub(crate) fn attrs_for_inheritance(&self, context: AttrInheritContext) -> Self {
+        // These attributes are inherited during lowering (since that's when they're parsed)
+        //
+        // Except for impls: lowering has no concept of impls so these get inherited early. This
+        // is fine since impls have no inherent behavior and all attributes on impls are necessarily
+        // only there for inheritance
+        let attrs = if context == AttrInheritContext::MethodFromImpl {
+            self.attrs.clone()
+        } else {
+            Vec::new()
+        };
+
+        let demo_attrs = if context == AttrInheritContext::MethodFromImpl {
+            self.demo_attrs.clone()
+        } else {
+            Vec::new()
+        };
+
+        let abi_rename = self.abi_rename.attrs_for_inheritance(context, true);
+        Self {
+            cfg: self.cfg.clone(),
+
+            attrs,
+            abi_rename,
+            demo_attrs,
+        }
+    }
+
+    pub(crate) fn add_attrs(&mut self, attrs: &[Attribute]) {
+        for attr in syn_attr_to_ast_attr(attrs) {
+            self.add_attr(attr)
+        }
+    }
+    pub(crate) fn from_attrs(attrs: &[Attribute]) -> Self {
+        let mut this = Self::default();
+        this.add_attrs(attrs);
+        this
+    }
+}
+
+impl From<&[Attribute]> for Attrs {
+    fn from(other: &[Attribute]) -> Self {
+        Self::from_attrs(other)
+    }
+}
+
+enum Attr {
+    Cfg(Attribute),
+    DiplomatBackend(DiplomatBackendAttr),
+    CRename(RenameAttr),
+    DemoBackend(DemoBackendAttr),
+    // More goes here
+}
+
+fn syn_attr_to_ast_attr(attrs: &[Attribute]) -> impl Iterator<Item = Attr> + '_ {
+    let cfg_path: syn::Path = syn::parse_str("cfg").unwrap();
+    let dattr_path: syn::Path = syn::parse_str("diplomat::attr").unwrap();
+    let crename_attr: syn::Path = syn::parse_str("diplomat::abi_rename").unwrap();
+    let demo_path: syn::Path = syn::parse_str("diplomat::demo").unwrap();
+    attrs.iter().filter_map(move |a| {
+        if a.path() == &cfg_path {
+            Some(Attr::Cfg(a.clone()))
+        } else if a.path() == &dattr_path {
+            Some(Attr::DiplomatBackend(
+                a.parse_args()
+                    .expect("Failed to parse malformed diplomat::attr"),
+            ))
+        } else if a.path() == &crename_attr {
+            Some(Attr::CRename(RenameAttr::from_meta(&a.meta).unwrap()))
+        } else if a.path() == &demo_path {
+            Some(Attr::DemoBackend(
+                a.parse_args()
+                    .expect("Failed to parse malformed diplomat::demo"),
+            ))
+        } else {
+            None
+        }
+    })
+}
+
+impl Serialize for Attrs {
+    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+    where
+        S: Serializer,
+    {
+        // 1 is the number of fields in the struct.
+        let mut state = serializer.serialize_struct("Attrs", 1)?;
+        if !self.cfg.is_empty() {
+            let cfg: Vec<_> = self
+                .cfg
+                .iter()
+                .map(|a| quote::quote!(#a).to_string())
+                .collect();
+            state.serialize_field("cfg", &cfg)?;
+        }
+        if !self.attrs.is_empty() {
+            state.serialize_field("attrs", &self.attrs)?;
+        }
+        if !self.abi_rename.is_empty() {
+            state.serialize_field("abi_rename", &self.abi_rename)?;
+        }
+        state.end()
+    }
+}
+
+/// A `#[diplomat::attr(...)]` attribute
+///
+/// Its contents must start with single element that is a CFG-expression
+/// (so it may contain `foo = bar`, `foo = "bar"`, `ident`, `*` atoms,
+/// and `all()`, `not()`, and `any()` combiners), and then be followed by one
+/// or more backend-specific attributes, which can be any valid meta-item
+#[derive(Clone, PartialEq, Eq, Hash, Debug, Serialize)]
+#[non_exhaustive]
+pub struct DiplomatBackendAttr {
+    pub cfg: DiplomatBackendAttrCfg,
+    #[serde(serialize_with = "serialize_meta")]
+    pub meta: Meta,
+}
+
+fn serialize_meta<S>(m: &Meta, s: S) -> Result<S::Ok, S::Error>
+where
+    S: Serializer,
+{
+    quote::quote!(#m).to_string().serialize(s)
+}
+
+#[derive(Clone, PartialEq, Eq, Hash, Debug, Serialize)]
+#[non_exhaustive]
+pub enum DiplomatBackendAttrCfg {
+    Not(Box<DiplomatBackendAttrCfg>),
+    Any(Vec<DiplomatBackendAttrCfg>),
+    All(Vec<DiplomatBackendAttrCfg>),
+    Star,
+    // "auto", smartly figure out based on the attribute used
+    Auto,
+    BackendName(String),
+    NameValue(String, String),
+}
+
+impl Parse for DiplomatBackendAttrCfg {
+    fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
+        let lookahead = input.lookahead1();
+        if lookahead.peek(Ident) {
+            let name: Ident = input.parse()?;
+            if name == "auto" {
+                Ok(DiplomatBackendAttrCfg::Auto)
+            } else if name == "not" {
+                let content;
+                let _paren = syn::parenthesized!(content in input);
+                Ok(DiplomatBackendAttrCfg::Not(Box::new(content.parse()?)))
+            } else if name == "any" || name == "all" {
+                let content;
+                let _paren = syn::parenthesized!(content in input);
+                let list = content.parse_terminated(Self::parse, Token![,])?;
+                let vec = list.into_iter().collect();
+                if name == "any" {
+                    Ok(DiplomatBackendAttrCfg::Any(vec))
+                } else {
+                    Ok(DiplomatBackendAttrCfg::All(vec))
+                }
+            } else if input.peek(Token![=]) {
+                let _t: Token![=] = input.parse()?;
+                if input.peek(Ident) {
+                    let value: Ident = input.parse()?;
+                    Ok(DiplomatBackendAttrCfg::NameValue(
+                        name.to_string(),
+                        value.to_string(),
+                    ))
+                } else {
+                    let value: LitStr = input.parse()?;
+                    Ok(DiplomatBackendAttrCfg::NameValue(
+                        name.to_string(),
+                        value.value(),
+                    ))
+                }
+            } else {
+                Ok(DiplomatBackendAttrCfg::BackendName(name.to_string()))
+            }
+        } else if lookahead.peek(Token![*]) {
+            let _t: Token![*] = input.parse()?;
+            Ok(DiplomatBackendAttrCfg::Star)
+        } else {
+            Err(ParseError::new(
+                input.span(),
+                "CFG portion of #[diplomat::attr] fails to parse",
+            ))
+        }
+    }
+}
+
+/// Meant to be used with Attribute::parse_args()
+impl Parse for DiplomatBackendAttr {
+    fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
+        let cfg = input.parse()?;
+        let _comma: Token![,] = input.parse()?;
+        let meta = input.parse()?;
+        Ok(Self { cfg, meta })
+    }
+}
+
+// #region demo_gen specific attributes
+/// A `#[diplomat::demo(...)]` attribute
+#[derive(Clone, PartialEq, Eq, Hash, Debug, Serialize)]
+#[non_exhaustive]
+pub struct DemoBackendAttr {
+    #[serde(serialize_with = "serialize_meta")]
+    pub meta: Meta,
+}
+
+/// Meant to be used with Attribute::parse_args()
+impl Parse for DemoBackendAttr {
+    fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
+        let meta = input.parse()?;
+        Ok(Self { meta })
+    }
+}
+
+// #endregion
+
+#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd)]
+pub(crate) enum AttrInheritContext {
+    Variant,
+    Type,
+    /// When a method or an impl is inheriting from a module
+    MethodOrImplFromModule,
+    /// When a method is inheriting from an impl
+    ///
+    /// This distinction is made because HIR attributes are pre-inherited from the impl to the
+    /// method, so the boundary of "method inheriting from module" is different
+    MethodFromImpl,
+    // Currently there's no way to feed an attribute to a Module, but such inheritance will
+    // likely apply during lowering for config defaults.
+    #[allow(unused)]
+    Module,
+}
+
+/// A pattern for use in rename attributes, like `#[diplomat::abi_rename]`
+///
+/// This can be parsed from a string, typically something like `icu4x_{0}`.
+/// It can have up to one {0} for replacement.
+///
+/// In the future this may support transformations like to_camel_case, etc,
+/// probably specified as a list like `#[diplomat::abi_rename("foo{0}", to_camel_case)]`
+#[derive(Default, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Serialize)]
+pub struct RenameAttr {
+    pattern: Option<RenamePattern>,
+}
+
+impl RenameAttr {
+    /// Apply all renames to a given string
+    pub fn apply<'a>(&'a self, name: Cow<'a, str>) -> Cow<'a, str> {
+        if let Some(ref pattern) = self.pattern {
+            let replacement = &pattern.replacement;
+            if let Some(index) = pattern.insertion_index {
+                format!("{}{name}{}", &replacement[..index], &replacement[index..]).into()
+            } else {
+                replacement.into()
+            }
+        } else {
+            name
+        }
+    }
+
+    /// Whether this rename is empty and will perform no changes
+    pub(crate) fn is_empty(&self) -> bool {
+        self.pattern.is_none()
+    }
+
+    pub(crate) fn extend(&mut self, other: &Self) {
+        if other.pattern.is_some() {
+            self.pattern.clone_from(&other.pattern);
+        }
+
+        // In the future if we support things like to_lower_case they may inherit separately
+        // from patterns.
+    }
+
+    /// Get a copy of these attributes for use in inheritance, masking out things
+    /// that should not be inherited
+    pub(crate) fn attrs_for_inheritance(
+        &self,
+        context: AttrInheritContext,
+        is_abi_rename: bool,
+    ) -> Self {
+        let pattern = match context {
+            // No inheritance from modules to method-likes for the rename attribute
+            AttrInheritContext::MethodOrImplFromModule if !is_abi_rename => Default::default(),
+            // No effect on variants
+            AttrInheritContext::Variant => Default::default(),
+            _ => self.pattern.clone(),
+        };
+        // In the future if we support things like to_lower_case they may inherit separately
+        // from patterns.
+        Self { pattern }
+    }
+
+    /// From a replacement pattern, like "icu4x_{0}". Can have up to one {0} in it for substitution.
+    fn from_pattern(s: &str) -> Self {
+        Self {
+            pattern: Some(s.parse().unwrap()),
+        }
+    }
+
+    pub(crate) fn from_meta(meta: &Meta) -> Result<Self, &'static str> {
+        let attr = StandardAttribute::from_meta(meta)
+            .map_err(|_| "#[diplomat::abi_rename] must be given a string value")?;
+
+        match attr {
+            StandardAttribute::String(s) => Ok(RenameAttr::from_pattern(&s)),
+            StandardAttribute::List(_) => {
+                Err("Failed to parse malformed #[diplomat::abi_rename(...)]: found list")
+            }
+            StandardAttribute::Empty => {
+                Err("Failed to parse malformed #[diplomat::abi_rename(...)]: found no parameters")
+            }
+        }
+    }
+}
+
+impl FromStr for RenamePattern {
+    type Err = Infallible;
+    fn from_str(s: &str) -> Result<Self, Infallible> {
+        if let Some(index) = s.find("{0}") {
+            let replacement = format!("{}{}", &s[..index], &s[index + 3..]);
+            Ok(Self {
+                replacement,
+                insertion_index: Some(index),
+            })
+        } else {
+            Ok(Self {
+                replacement: s.into(),
+                insertion_index: None,
+            })
+        }
+    }
+}
+
+#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Serialize)]
+struct RenamePattern {
+    /// The string to replace with
+    replacement: String,
+    /// The index in `replacement` in which to insert the original string. If None,
+    /// this is a pure rename
+    insertion_index: Option<usize>,
+}
+
+/// Helper type for parsing standard attributes. A standard attribute typically will accept the forms:
+///
+/// - `#[attr = "foo"]` and `#[attr("foo")]` for a simple string
+/// - `#[attr(....)]` for a more complicated context
+/// - `#[attr]` for a "defaulting" context
+///
+/// This allows attributes to parse simple string values without caring too much about the NameValue vs List representation
+/// and then attributes can choose to handle more complicated lists if they so desire.
+pub(crate) enum StandardAttribute<'a> {
+    String(String),
+    List(#[allow(dead_code)] &'a MetaList),
+    Empty,
+}
+
+impl<'a> StandardAttribute<'a> {
+    /// Parse from a Meta. Returns an error when no string value is specified in the path/namevalue forms.
+    pub(crate) fn from_meta(meta: &'a Meta) -> Result<Self, ()> {
+        match meta {
+            Meta::Path(..) => Ok(Self::Empty),
+            Meta::NameValue(ref nv) => {
+                // Support a shortcut `abi_rename = "..."`
+                let Expr::Lit(ref lit) = nv.value else {
+                    return Err(());
+                };
+                let Lit::Str(ref lit) = lit.lit else {
+                    return Err(());
+                };
+                Ok(Self::String(lit.value()))
+            }
+            // The full syntax to which we'll add more things in the future, `abi_rename("")`
+            Meta::List(list) => {
+                if let Ok(lit) = list.parse_args::<LitStr>() {
+                    Ok(Self::String(lit.value()))
+                } else {
+                    Ok(Self::List(list))
+                }
+            }
+        }
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use insta;
+
+    use syn;
+
+    use super::{DiplomatBackendAttr, DiplomatBackendAttrCfg, RenameAttr};
+
+    #[test]
+    fn test_cfgs() {
+        let attr_cfg: DiplomatBackendAttrCfg = syn::parse_quote!(*);
+        insta::assert_yaml_snapshot!(attr_cfg);
+        let attr_cfg: DiplomatBackendAttrCfg = syn::parse_quote!(cpp);
+        insta::assert_yaml_snapshot!(attr_cfg);
+        let attr_cfg: DiplomatBackendAttrCfg = syn::parse_quote!(has = overloading);
+        insta::assert_yaml_snapshot!(attr_cfg);
+        let attr_cfg: DiplomatBackendAttrCfg = syn::parse_quote!(has = "overloading");
+        insta::assert_yaml_snapshot!(attr_cfg);
+        let attr_cfg: DiplomatBackendAttrCfg =
+            syn::parse_quote!(any(all(*, cpp, has="overloading"), not(js)));
+        insta::assert_yaml_snapshot!(attr_cfg);
+    }
+
+    #[test]
+    fn test_attr() {
+        let attr: syn::Attribute =
+            syn::parse_quote!(#[diplomat::attr(any(cpp, has = "overloading"), namespacing)]);
+        let attr: DiplomatBackendAttr = attr.parse_args().unwrap();
+        insta::assert_yaml_snapshot!(attr);
+    }
+
+    #[test]
+    fn test_rename() {
+        let attr: syn::Attribute = syn::parse_quote!(#[diplomat::abi_rename = "foobar_{0}"]);
+        let attr = RenameAttr::from_meta(&attr.meta).unwrap();
+        insta::assert_yaml_snapshot!(attr);
+        let attr: syn::Attribute = syn::parse_quote!(#[diplomat::abi_rename("foobar_{0}")]);
+        let attr = RenameAttr::from_meta(&attr.meta).unwrap();
+        insta::assert_yaml_snapshot!(attr);
+    }
+}
diff --git a/crates/diplomat_core/src/ast/docs.rs b/crates/diplomat_core/src/ast/docs.rs
new file mode 100644
index 0000000..c77fe5c
--- /dev/null
+++ b/crates/diplomat_core/src/ast/docs.rs
@@ -0,0 +1,423 @@
+use super::Path;
+use core::fmt;
+use quote::ToTokens;
+use serde::{Deserialize, Serialize};
+use std::collections::HashMap;
+use syn::parse::{self, Parse, ParseStream};
+use syn::{Attribute, Ident, Meta, Token};
+
+#[derive(Clone, PartialEq, Eq, Hash, Serialize, Deserialize, Debug, Default)]
+pub struct Docs(String, Vec<RustLink>);
+
+impl Docs {
+    pub fn from_attrs(attrs: &[Attribute]) -> Self {
+        Self(Self::get_doc_lines(attrs), Self::get_rust_link(attrs))
+    }
+
+    fn get_doc_lines(attrs: &[Attribute]) -> String {
+        let mut lines: String = String::new();
+
+        attrs.iter().for_each(|attr| {
+            if let Meta::NameValue(ref nv) = attr.meta {
+                if nv.path.is_ident("doc") {
+                    let node: syn::LitStr = syn::parse2(nv.value.to_token_stream()).unwrap();
+                    let line = node.value().trim().to_string();
+
+                    if !lines.is_empty() {
+                        lines.push('\n');
+                    }
+
+                    lines.push_str(&line);
+                }
+            }
+        });
+
+        lines
+    }
+
+    fn get_rust_link(attrs: &[Attribute]) -> Vec<RustLink> {
+        attrs
+            .iter()
+            .filter(|i| i.path().to_token_stream().to_string() == "diplomat :: rust_link")
+            .map(|i| i.parse_args().expect("Malformed attribute"))
+            .collect()
+    }
+
+    pub fn is_empty(&self) -> bool {
+        self.0.is_empty() && self.1.is_empty()
+    }
+
+    /// Convert to markdown
+    pub fn to_markdown(&self, docs_url_gen: &DocsUrlGenerator) -> String {
+        use std::fmt::Write;
+        let mut lines = self.0.clone();
+        let mut has_compact = false;
+        for rust_link in &self.1 {
+            if rust_link.display == RustLinkDisplay::Compact {
+                has_compact = true;
+            } else if rust_link.display == RustLinkDisplay::Normal {
+                if !lines.is_empty() {
+                    write!(lines, "\n\n").unwrap();
+                }
+                write!(
+                    lines,
+                    "See the [Rust documentation for `{name}`]({link}) for more information.",
+                    name = rust_link.path.elements.last().unwrap(),
+                    link = docs_url_gen.gen_for_rust_link(rust_link)
+                )
+                .unwrap();
+            }
+        }
+        if has_compact {
+            if !lines.is_empty() {
+                write!(lines, "\n\n").unwrap();
+            }
+            write!(lines, "Additional information: ").unwrap();
+            for (i, rust_link) in self
+                .1
+                .iter()
+                .filter(|r| r.display == RustLinkDisplay::Compact)
+                .enumerate()
+            {
+                if i != 0 {
+                    write!(lines, ", ").unwrap();
+                }
+                write!(
+                    lines,
+                    "[{}]({})",
+                    i + 1,
+                    docs_url_gen.gen_for_rust_link(rust_link)
+                )
+                .unwrap();
+            }
+        }
+        lines
+    }
+
+    pub fn rust_links(&self) -> &[RustLink] {
+        &self.1
+    }
+}
+
+#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
+#[non_exhaustive]
+pub enum RustLinkDisplay {
+    /// A nice expanded representation that includes the type name
+    ///
+    /// e.g. "See the \[link to Rust documentation\] for more details"
+    Normal,
+    /// A compact representation that will fit multiple rust_link entries in one line
+    ///
+    /// E.g. "For further information, see: 1, 2, 3, 4" (all links)
+    Compact,
+    /// Hidden. Useful for programmatically annotating an API as related without showing a link to the user
+    Hidden,
+}
+
+#[derive(Clone, PartialEq, Eq, Hash, Serialize, Deserialize, Debug, PartialOrd, Ord)]
+#[non_exhaustive]
+pub struct RustLink {
+    pub path: Path,
+    pub typ: DocType,
+    pub display: RustLinkDisplay,
+}
+
+impl Parse for RustLink {
+    fn parse(input: ParseStream<'_>) -> parse::Result<Self> {
+        let path = input.parse()?;
+        let path = Path::from_syn(&path);
+        let _comma: Token![,] = input.parse()?;
+        let ty_ident: Ident = input.parse()?;
+        let typ = match &*ty_ident.to_string() {
+            "Struct" => DocType::Struct,
+            "StructField" => DocType::StructField,
+            "Enum" => DocType::Enum,
+            "EnumVariant" => DocType::EnumVariant,
+            "EnumVariantField" => DocType::EnumVariantField,
+            "Trait" => DocType::Trait,
+            "FnInStruct" => DocType::FnInStruct,
+            "FnInEnum" => DocType::FnInEnum,
+            "FnInTrait" => DocType::FnInTrait,
+            "DefaultFnInTrait" => DocType::DefaultFnInTrait,
+            "Fn" => DocType::Fn,
+            "Mod" => DocType::Mod,
+            "Constant" => DocType::Constant,
+            "AssociatedConstantInEnum" => DocType::AssociatedConstantInEnum,
+            "AssociatedConstantInTrait" => DocType::AssociatedConstantInTrait,
+            "AssociatedConstantInStruct" => DocType::AssociatedConstantInStruct,
+            "Macro" => DocType::Macro,
+            "AssociatedTypeInEnum" => DocType::AssociatedTypeInEnum,
+            "AssociatedTypeInTrait" => DocType::AssociatedTypeInTrait,
+            "AssociatedTypeInStruct" => DocType::AssociatedTypeInStruct,
+            "Typedef" => DocType::Typedef,
+            _ => {
+                return Err(parse::Error::new(
+                    ty_ident.span(),
+                    "Unknown rust_link doc type",
+                ))
+            }
+        };
+        let lookahead = input.lookahead1();
+        let display = if lookahead.peek(Token![,]) {
+            let _comma: Token![,] = input.parse()?;
+            let display_ident: Ident = input.parse()?;
+            match &*display_ident.to_string() {
+                "normal" => RustLinkDisplay::Normal,
+                "compact" => RustLinkDisplay::Compact,
+                "hidden" => RustLinkDisplay::Hidden,
+                _ => return Err(parse::Error::new(display_ident.span(), "Unknown rust_link display style: Must be must be `normal`, `compact`, or `hidden`.")),
+            }
+        } else {
+            RustLinkDisplay::Normal
+        };
+        Ok(RustLink { path, typ, display })
+    }
+}
+impl fmt::Display for RustLink {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        write!(f, "{}#{:?}", self.path, self.typ)
+    }
+}
+
+#[derive(Clone, PartialEq, Eq, Hash, Serialize, Deserialize, Debug, PartialOrd, Ord)]
+#[non_exhaustive]
+pub enum DocType {
+    Struct,
+    StructField,
+    Enum,
+    EnumVariant,
+    EnumVariantField,
+    Trait,
+    FnInStruct,
+    FnInEnum,
+    FnInTrait,
+    DefaultFnInTrait,
+    Fn,
+    Mod,
+    Constant,
+    AssociatedConstantInEnum,
+    AssociatedConstantInTrait,
+    AssociatedConstantInStruct,
+    Macro,
+    AssociatedTypeInEnum,
+    AssociatedTypeInTrait,
+    AssociatedTypeInStruct,
+    Typedef,
+}
+
+#[derive(Default)]
+pub struct DocsUrlGenerator {
+    default_url: Option<String>,
+    base_urls: HashMap<String, String>,
+}
+
+impl DocsUrlGenerator {
+    pub fn with_base_urls(default_url: Option<String>, base_urls: HashMap<String, String>) -> Self {
+        Self {
+            default_url,
+            base_urls,
+        }
+    }
+
+    fn gen_for_rust_link(&self, rust_link: &RustLink) -> String {
+        use DocType::*;
+
+        let mut r = String::new();
+
+        let base = self
+            .base_urls
+            .get(rust_link.path.elements[0].as_str())
+            .map(String::as_str)
+            .or(self.default_url.as_deref())
+            .unwrap_or("https://docs.rs/");
+
+        r.push_str(base);
+        if !base.ends_with('/') {
+            r.push('/');
+        }
+        if r == "https://docs.rs/" {
+            r.push_str(rust_link.path.elements[0].as_str());
+            r.push_str("/latest/");
+        }
+
+        let mut elements = rust_link.path.elements.iter().peekable();
+
+        let module_depth = rust_link.path.elements.len()
+            - match rust_link.typ {
+                Mod => 0,
+                Struct | Enum | Trait | Fn | Macro | Constant | Typedef => 1,
+                FnInEnum
+                | FnInStruct
+                | FnInTrait
+                | DefaultFnInTrait
+                | EnumVariant
+                | StructField
+                | AssociatedTypeInEnum
+                | AssociatedTypeInStruct
+                | AssociatedTypeInTrait
+                | AssociatedConstantInEnum
+                | AssociatedConstantInStruct
+                | AssociatedConstantInTrait => 2,
+                EnumVariantField => 3,
+            };
+
+        for _ in 0..module_depth {
+            r.push_str(elements.next().unwrap().as_str());
+            r.push('/');
+        }
+
+        if elements.peek().is_none() {
+            r.push_str("index.html");
+            return r;
+        }
+
+        r.push_str(match rust_link.typ {
+            Typedef => "type.",
+            Struct
+            | StructField
+            | FnInStruct
+            | AssociatedTypeInStruct
+            | AssociatedConstantInStruct => "struct.",
+            Enum
+            | EnumVariant
+            | EnumVariantField
+            | FnInEnum
+            | AssociatedTypeInEnum
+            | AssociatedConstantInEnum => "enum.",
+            Trait
+            | FnInTrait
+            | DefaultFnInTrait
+            | AssociatedTypeInTrait
+            | AssociatedConstantInTrait => "trait.",
+            Fn => "fn.",
+            Constant => "constant.",
+            Macro => "macro.",
+            Mod => unreachable!(),
+        });
+
+        r.push_str(elements.next().unwrap().as_str());
+
+        r.push_str(".html");
+
+        match rust_link.typ {
+            FnInStruct | FnInEnum | DefaultFnInTrait => {
+                r.push_str("#method.");
+                r.push_str(elements.next().unwrap().as_str());
+            }
+            AssociatedTypeInStruct | AssociatedTypeInEnum | AssociatedTypeInTrait => {
+                r.push_str("#associatedtype.");
+                r.push_str(elements.next().unwrap().as_str());
+            }
+            AssociatedConstantInStruct | AssociatedConstantInEnum | AssociatedConstantInTrait => {
+                r.push_str("#associatedconstant.");
+                r.push_str(elements.next().unwrap().as_str());
+            }
+            FnInTrait => {
+                r.push_str("#tymethod.");
+                r.push_str(elements.next().unwrap().as_str());
+            }
+            EnumVariant => {
+                r.push_str("#variant.");
+                r.push_str(elements.next().unwrap().as_str());
+            }
+            StructField => {
+                r.push_str("#structfield.");
+                r.push_str(elements.next().unwrap().as_str());
+            }
+            EnumVariantField => {
+                r.push_str("#variant.");
+                r.push_str(elements.next().unwrap().as_str());
+                r.push_str(".field.");
+                r.push_str(elements.next().unwrap().as_str());
+            }
+            _ => {}
+        }
+        r
+    }
+}
+
+#[test]
+fn test_docs_url_generator() {
+    let test_cases = [
+        (
+            syn::parse_quote! { #[diplomat::rust_link(std::foo::bar::batz, Struct)] },
+            "https://docs.rs/std/latest/std/foo/bar/struct.batz.html",
+        ),
+        (
+            syn::parse_quote! { #[diplomat::rust_link(std::foo::bar::batz, StructField)] },
+            "https://docs.rs/std/latest/std/foo/struct.bar.html#structfield.batz",
+        ),
+        (
+            syn::parse_quote! { #[diplomat::rust_link(std::foo::bar::batz, Enum)] },
+            "https://docs.rs/std/latest/std/foo/bar/enum.batz.html",
+        ),
+        (
+            syn::parse_quote! { #[diplomat::rust_link(std::foo::bar::batz, EnumVariant)] },
+            "https://docs.rs/std/latest/std/foo/enum.bar.html#variant.batz",
+        ),
+        (
+            syn::parse_quote! { #[diplomat::rust_link(std::foo::bar::batz, EnumVariantField)] },
+            "https://docs.rs/std/latest/std/enum.foo.html#variant.bar.field.batz",
+        ),
+        (
+            syn::parse_quote! { #[diplomat::rust_link(std::foo::bar::batz, Trait)] },
+            "https://docs.rs/std/latest/std/foo/bar/trait.batz.html",
+        ),
+        (
+            syn::parse_quote! { #[diplomat::rust_link(std::foo::bar::batz, FnInStruct)] },
+            "https://docs.rs/std/latest/std/foo/struct.bar.html#method.batz",
+        ),
+        (
+            syn::parse_quote! { #[diplomat::rust_link(std::foo::bar::batz, FnInEnum)] },
+            "https://docs.rs/std/latest/std/foo/enum.bar.html#method.batz",
+        ),
+        (
+            syn::parse_quote! { #[diplomat::rust_link(std::foo::bar::batz, FnInTrait)] },
+            "https://docs.rs/std/latest/std/foo/trait.bar.html#tymethod.batz",
+        ),
+        (
+            syn::parse_quote! { #[diplomat::rust_link(std::foo::bar::batz, DefaultFnInTrait)] },
+            "https://docs.rs/std/latest/std/foo/trait.bar.html#method.batz",
+        ),
+        (
+            syn::parse_quote! { #[diplomat::rust_link(std::foo::bar::batz, Fn)] },
+            "https://docs.rs/std/latest/std/foo/bar/fn.batz.html",
+        ),
+        (
+            syn::parse_quote! { #[diplomat::rust_link(std::foo::bar::batz, Mod)] },
+            "https://docs.rs/std/latest/std/foo/bar/batz/index.html",
+        ),
+        (
+            syn::parse_quote! { #[diplomat::rust_link(std::foo::bar::batz, Constant)] },
+            "https://docs.rs/std/latest/std/foo/bar/constant.batz.html",
+        ),
+        (
+            syn::parse_quote! { #[diplomat::rust_link(std::foo::bar::batz, Macro)] },
+            "https://docs.rs/std/latest/std/foo/bar/macro.batz.html",
+        ),
+    ];
+
+    for (attr, expected) in test_cases.clone() {
+        assert_eq!(
+            DocsUrlGenerator::default().gen_for_rust_link(&Docs::from_attrs(&[attr]).1[0]),
+            expected
+        );
+    }
+
+    assert_eq!(
+        DocsUrlGenerator::with_base_urls(
+            None,
+            [("std".to_string(), "http://std-docs.biz/".to_string())]
+                .into_iter()
+                .collect()
+        )
+        .gen_for_rust_link(&Docs::from_attrs(&[test_cases[0].0.clone()]).1[0]),
+        "http://std-docs.biz/std/foo/bar/struct.batz.html"
+    );
+
+    assert_eq!(
+        DocsUrlGenerator::with_base_urls(Some("http://std-docs.biz/".to_string()), HashMap::new())
+            .gen_for_rust_link(&Docs::from_attrs(&[test_cases[0].0.clone()]).1[0]),
+        "http://std-docs.biz/std/foo/bar/struct.batz.html"
+    );
+}
diff --git a/crates/diplomat_core/src/ast/enums.rs b/crates/diplomat_core/src/ast/enums.rs
new file mode 100644
index 0000000..3169b63
--- /dev/null
+++ b/crates/diplomat_core/src/ast/enums.rs
@@ -0,0 +1,127 @@
+use serde::Serialize;
+
+use super::docs::Docs;
+use super::{AttrInheritContext, Attrs, Ident, Method};
+use quote::ToTokens;
+
+/// A fieldless enum declaration in an FFI module.
+#[derive(Clone, Serialize, Debug, Hash, PartialEq, Eq)]
+#[non_exhaustive]
+pub struct Enum {
+    pub name: Ident,
+    pub docs: Docs,
+    /// A list of variants of the enum. (name, discriminant, docs, attrs)
+    pub variants: Vec<(Ident, isize, Docs, Attrs)>,
+    pub methods: Vec<Method>,
+    pub attrs: Attrs,
+}
+
+impl Enum {
+    /// Extract an [`Enum`] metadata value from an AST node.
+    pub fn new(enm: &syn::ItemEnum, parent_attrs: &Attrs) -> Enum {
+        let mut last_discriminant = -1;
+        if !enm.generics.params.is_empty() {
+            // Generic types are not allowed.
+            // Assuming all enums cannot have lifetimes? We don't even have a
+            // `lifetimes` field. If we change our minds we can adjust this later
+            // and update the `CustomType::lifetimes` API accordingly.
+            panic!("Enums cannot have generic parameters");
+        }
+
+        let mut attrs = parent_attrs.clone();
+        attrs.add_attrs(&enm.attrs);
+        let variant_parent_attrs = attrs.attrs_for_inheritance(AttrInheritContext::Variant);
+
+        Enum {
+            name: (&enm.ident).into(),
+            docs: Docs::from_attrs(&enm.attrs),
+            variants: enm
+                .variants
+                .iter()
+                .map(|v| {
+                    if !matches!(v.fields, syn::Fields::Unit) {
+                        panic!("Enums cannot have fields, we only support C-like enums");
+                    }
+                    let new_discriminant = v
+                        .discriminant
+                        .as_ref()
+                        .map(|d| {
+                            // Reparsing, signed literals are represented
+                            // as a negation expression
+                            let lit: Result<syn::Lit, _> = syn::parse2(d.1.to_token_stream());
+                            if let Ok(syn::Lit::Int(ref lit_int)) = lit {
+                                lit_int.base10_parse::<isize>().unwrap()
+                            } else {
+                                panic!("Expected a discriminant to be a constant integer");
+                            }
+                        })
+                        .unwrap_or_else(|| last_discriminant + 1);
+
+                    last_discriminant = new_discriminant;
+                    let mut v_attrs = variant_parent_attrs.clone();
+                    v_attrs.add_attrs(&v.attrs);
+                    (
+                        (&v.ident).into(),
+                        new_discriminant,
+                        Docs::from_attrs(&v.attrs),
+                        v_attrs,
+                    )
+                })
+                .collect(),
+            methods: vec![],
+            attrs,
+        }
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use insta::{self, Settings};
+
+    use syn;
+
+    use super::Enum;
+
+    #[test]
+    fn simple_enum() {
+        let mut settings = Settings::new();
+        settings.set_sort_maps(true);
+
+        settings.bind(|| {
+            insta::assert_yaml_snapshot!(Enum::new(
+                &syn::parse_quote! {
+                    /// Some docs.
+                    #[diplomat::rust_link(foo::Bar, Enum)]
+                    enum MyLocalEnum {
+                        Abc,
+                        /// Some more docs.
+                        Def
+                    }
+                },
+                &Default::default()
+            ));
+        });
+    }
+
+    #[test]
+    fn enum_with_discr() {
+        let mut settings = Settings::new();
+        settings.set_sort_maps(true);
+
+        settings.bind(|| {
+            insta::assert_yaml_snapshot!(Enum::new(
+                &syn::parse_quote! {
+                    /// Some docs.
+                    #[diplomat::rust_link(foo::Bar, Enum)]
+                    enum DiscriminantedEnum {
+                        Abc = -1,
+                        Def = 0,
+                        Ghi = 1,
+                        Jkl = 2,
+                    }
+                },
+                &Default::default()
+            ));
+        });
+    }
+}
diff --git a/crates/diplomat_core/src/ast/idents.rs b/crates/diplomat_core/src/ast/idents.rs
new file mode 100644
index 0000000..2177923
--- /dev/null
+++ b/crates/diplomat_core/src/ast/idents.rs
@@ -0,0 +1,86 @@
+use proc_macro2::Span;
+use quote::{ToTokens, TokenStreamExt};
+use serde::{Deserialize, Serialize};
+use std::borrow::{Borrow, Cow};
+use std::fmt;
+
+/// An identifier, analogous to `syn::Ident` and `proc_macro2::Ident`.
+#[derive(Hash, Eq, PartialEq, Serialize, Clone, Debug, Ord, PartialOrd)]
+#[serde(transparent)]
+pub struct Ident(Cow<'static, str>);
+
+impl Ident {
+    /// Validate a string
+    fn validate(string: &str) -> syn::Result<()> {
+        syn::parse_str::<syn::Ident>(string).map(|_| {})
+    }
+
+    /// Attempt to create a new `Ident`.
+    ///
+    /// This function fails if the input isn't valid according to
+    /// `proc_macro2::Ident`'s invariants.
+    pub fn try_new(string: String) -> syn::Result<Self> {
+        Self::validate(&string).map(|_| Self(Cow::from(string)))
+    }
+
+    pub fn to_syn(&self) -> syn::Ident {
+        syn::Ident::new(self.as_str(), Span::call_site())
+    }
+
+    /// Get the `&str` representation.
+    pub fn as_str(&self) -> &str {
+        &self.0
+    }
+
+    /// An [`Ident`] containing "this".
+    pub const THIS: Self = Ident(Cow::Borrowed("this"));
+}
+
+impl From<&'static str> for Ident {
+    fn from(string: &'static str) -> Self {
+        Self::validate(string).unwrap();
+        Self(Cow::from(string))
+    }
+}
+
+impl From<String> for Ident {
+    fn from(string: String) -> Self {
+        Self::validate(&string).unwrap();
+        Self(Cow::from(string))
+    }
+}
+
+impl<'de> Deserialize<'de> for Ident {
+    /// The derived `Deserialize` allows for creating `Ident`s that do not uphold
+    /// the proper invariants. This custom impl ensures that this cannot happen.
+    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+    where
+        D: serde::Deserializer<'de>,
+    {
+        Ok(Ident::from(String::deserialize(deserializer)?))
+    }
+}
+
+impl Borrow<str> for Ident {
+    fn borrow(&self) -> &str {
+        self.as_str()
+    }
+}
+
+impl fmt::Display for Ident {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        self.as_str().fmt(f)
+    }
+}
+
+impl From<&syn::Ident> for Ident {
+    fn from(ident: &syn::Ident) -> Self {
+        Self(Cow::from(ident.to_string()))
+    }
+}
+
+impl ToTokens for Ident {
+    fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
+        tokens.append(self.to_syn());
+    }
+}
diff --git a/crates/diplomat_core/src/ast/lifetimes.rs b/crates/diplomat_core/src/ast/lifetimes.rs
new file mode 100644
index 0000000..6e41392
--- /dev/null
+++ b/crates/diplomat_core/src/ast/lifetimes.rs
@@ -0,0 +1,609 @@
+use proc_macro2::Span;
+use quote::{quote, ToTokens};
+use serde::{Deserialize, Serialize};
+use std::fmt;
+
+use super::{Attrs, Docs, Ident, Param, SelfParam, TraitSelfParam, TypeName};
+
+/// A named lifetime, e.g. `'a`.
+///
+/// # Invariants
+///
+/// Cannot be `'static` or `'_`, use [`Lifetime`] to represent those instead.
+#[derive(Clone, Debug, Hash, Eq, PartialEq, Serialize, PartialOrd, Ord)]
+#[serde(transparent)]
+pub struct NamedLifetime(Ident);
+
+impl NamedLifetime {
+    pub fn name(&self) -> &Ident {
+        &self.0
+    }
+}
+
+impl<'de> Deserialize<'de> for NamedLifetime {
+    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+    where
+        D: serde::Deserializer<'de>,
+    {
+        // Special `Deserialize` impl to ensure invariants.
+        let named = Ident::deserialize(deserializer)?;
+        if named.as_str() == "static" {
+            panic!("cannot be static");
+        }
+        Ok(NamedLifetime(named))
+    }
+}
+
+impl From<&syn::Lifetime> for NamedLifetime {
+    fn from(lt: &syn::Lifetime) -> Self {
+        Lifetime::from(lt).to_named().expect("cannot be static")
+    }
+}
+
+impl From<&NamedLifetime> for NamedLifetime {
+    fn from(this: &NamedLifetime) -> Self {
+        this.clone()
+    }
+}
+
+impl PartialEq<syn::Lifetime> for NamedLifetime {
+    fn eq(&self, other: &syn::Lifetime) -> bool {
+        other.ident == self.0.as_str()
+    }
+}
+
+impl fmt::Display for NamedLifetime {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        write!(f, "'{}", self.0)
+    }
+}
+
+impl ToTokens for NamedLifetime {
+    fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
+        use proc_macro2::{Punct, Spacing};
+        Punct::new('\'', Spacing::Joint).to_tokens(tokens);
+        self.0.to_tokens(tokens);
+    }
+}
+
+/// A lifetime dependency graph used for tracking which lifetimes outlive,
+/// and are outlived by, other lifetimes.
+///
+/// It is similar to [`syn::LifetimeDef`], except it can also track lifetime
+/// bounds defined in the `where` clause.
+#[derive(Clone, PartialEq, Eq, Hash, Debug)]
+pub struct LifetimeEnv {
+    pub(crate) nodes: Vec<LifetimeNode>,
+}
+
+impl LifetimeEnv {
+    /// Construct an empty [`LifetimeEnv`].
+    ///
+    /// To create one outside of this module, use `LifetimeEnv::from_method_item`
+    /// or `LifetimeEnv::from` on `&syn::Generics`.
+    fn new() -> Self {
+        Self { nodes: vec![] }
+    }
+
+    /// Iterate through the names of the lifetimes in scope.
+    pub fn names(&self) -> impl Iterator<Item = &NamedLifetime> + Clone {
+        self.nodes.iter().map(|node| &node.lifetime)
+    }
+
+    /// Returns a [`LifetimeEnv`] for a method, accounting for lifetimes and bounds
+    /// defined in both the impl block and the method, as well as implicit lifetime
+    /// bounds in the optional `self` param, other param, and optional return type.
+    /// For example, the type `&'a Foo<'b>` implies `'b: 'a`.
+    pub fn from_method_item(
+        method: &syn::ImplItemFn,
+        impl_generics: Option<&syn::Generics>,
+        self_param: Option<&SelfParam>,
+        params: &[Param],
+        return_type: Option<&TypeName>,
+    ) -> Self {
+        let mut this = LifetimeEnv::new();
+        // The impl generics _must_ be loaded into the env first, since the method
+        // generics might use lifetimes defined in the impl, and `extend_generics`
+        // panics if `'a: 'b` where `'b` isn't declared by the time it finishes.
+        if let Some(generics) = impl_generics {
+            this.extend_generics(generics);
+        }
+        this.extend_generics(&method.sig.generics);
+
+        if let Some(self_param) = self_param {
+            this.extend_implicit_lifetime_bounds(&self_param.to_typename(), None);
+        }
+        for param in params {
+            this.extend_implicit_lifetime_bounds(&param.ty, None);
+        }
+        if let Some(return_type) = return_type {
+            this.extend_implicit_lifetime_bounds(return_type, None);
+        }
+
+        this
+    }
+
+    pub fn from_trait_item(
+        trait_fct_item: &syn::TraitItem,
+        self_param: Option<&TraitSelfParam>,
+        params: &[Param],
+        return_type: Option<&TypeName>,
+    ) -> Self {
+        let mut this = LifetimeEnv::new();
+        if let syn::TraitItem::Fn(_) = trait_fct_item {
+            if let Some(self_param) = self_param {
+                this.extend_implicit_lifetime_bounds(&self_param.to_typename(), None);
+            }
+            for param in params {
+                this.extend_implicit_lifetime_bounds(&param.ty, None);
+            }
+            if let Some(return_type) = return_type {
+                this.extend_implicit_lifetime_bounds(return_type, None);
+            }
+        } else {
+            panic!(
+                "Diplomat traits can only have associated methods and no other associated items."
+            )
+        }
+        this
+    }
+
+    pub fn from_trait(trt: &syn::ItemTrait) -> Self {
+        if trt.generics.lifetimes().next().is_some() {
+            panic!("Diplomat traits are not allowed to have any lifetime parameters")
+        }
+        LifetimeEnv::new()
+    }
+
+    /// Returns a [`LifetimeEnv`] for a struct, accounding for lifetimes and bounds
+    /// defined in the struct generics, as well as implicit lifetime bounds in
+    /// the struct's fields. For example, the field `&'a Foo<'b>` implies `'b: 'a`.
+    pub fn from_struct_item(
+        strct: &syn::ItemStruct,
+        fields: &[(Ident, TypeName, Docs, Attrs)],
+    ) -> Self {
+        let mut this = LifetimeEnv::new();
+        this.extend_generics(&strct.generics);
+        for (_, typ, _, _) in fields {
+            this.extend_implicit_lifetime_bounds(typ, None);
+        }
+        this
+    }
+
+    /// Traverse a type, adding any implicit lifetime bounds that arise from
+    /// having a reference to an opaque containing a lifetime.
+    /// For example, the type `&'a Foo<'b>` implies `'b: 'a`.
+    fn extend_implicit_lifetime_bounds(
+        &mut self,
+        typ: &TypeName,
+        behind_ref: Option<&NamedLifetime>,
+    ) {
+        match typ {
+            TypeName::Named(path_type) => {
+                if let Some(borrow_lifetime) = behind_ref {
+                    let explicit_longer_than_borrow =
+                        LifetimeTransitivity::longer_than(self, borrow_lifetime);
+                    let mut implicit_longer_than_borrow = vec![];
+
+                    for path_lifetime in path_type.lifetimes.iter() {
+                        if let Lifetime::Named(path_lifetime) = path_lifetime {
+                            if !explicit_longer_than_borrow.contains(&path_lifetime) {
+                                implicit_longer_than_borrow.push(path_lifetime);
+                            }
+                        }
+                    }
+
+                    self.extend_bounds(
+                        implicit_longer_than_borrow
+                            .into_iter()
+                            .map(|path_lifetime| (path_lifetime, Some(borrow_lifetime))),
+                    );
+                }
+            }
+            TypeName::Reference(lifetime, _, typ) => {
+                let behind_ref = if let Lifetime::Named(named) = lifetime {
+                    Some(named)
+                } else {
+                    None
+                };
+                self.extend_implicit_lifetime_bounds(typ, behind_ref);
+            }
+            TypeName::Option(typ, _) => self.extend_implicit_lifetime_bounds(typ, None),
+            TypeName::Result(ok, err, _) => {
+                self.extend_implicit_lifetime_bounds(ok, None);
+                self.extend_implicit_lifetime_bounds(err, None);
+            }
+            _ => {}
+        }
+    }
+
+    /// Add the lifetimes from generic parameters and where bounds.
+    fn extend_generics(&mut self, generics: &syn::Generics) {
+        let generic_bounds = generics.params.iter().map(|generic| match generic {
+            syn::GenericParam::Type(_) => panic!("generic types are unsupported"),
+            syn::GenericParam::Lifetime(def) => (&def.lifetime, &def.bounds),
+            syn::GenericParam::Const(_) => panic!("const generics are unsupported"),
+        });
+
+        let generic_defs = generic_bounds.clone().map(|(lifetime, _)| lifetime);
+
+        self.extend_lifetimes(generic_defs);
+        self.extend_bounds(generic_bounds);
+
+        if let Some(ref where_clause) = generics.where_clause {
+            self.extend_bounds(where_clause.predicates.iter().map(|pred| match pred {
+                syn::WherePredicate::Type(_) => panic!("trait bounds are unsupported"),
+                syn::WherePredicate::Lifetime(pred) => (&pred.lifetime, &pred.bounds),
+                _ => panic!("Found unknown kind of where predicate"),
+            }));
+        }
+    }
+
+    /// Returns the number of lifetimes in the graph.
+    pub fn len(&self) -> usize {
+        self.nodes.len()
+    }
+
+    /// Returns `true` if the graph contains no lifetimes.
+    pub fn is_empty(&self) -> bool {
+        self.nodes.is_empty()
+    }
+
+    /// `<'a, 'b, 'c>`
+    ///
+    /// Write the existing lifetimes, excluding bounds, as generic parameters.
+    ///
+    /// To include lifetime bounds, use [`LifetimeEnv::lifetime_defs_to_tokens`].
+    pub fn lifetimes_to_tokens(&self) -> proc_macro2::TokenStream {
+        if self.is_empty() {
+            return quote! {};
+        }
+
+        let lifetimes = self.nodes.iter().map(|node| &node.lifetime);
+        quote! { <#(#lifetimes),*> }
+    }
+
+    /// Returns the index of a lifetime in the graph, or `None` if the lifetime
+    /// isn't in the graph.
+    pub(crate) fn id<L>(&self, lifetime: &L) -> Option<usize>
+    where
+        NamedLifetime: PartialEq<L>,
+    {
+        self.nodes
+            .iter()
+            .position(|node| &node.lifetime == lifetime)
+    }
+
+    /// Add isolated lifetimes to the graph.
+    fn extend_lifetimes<'a, L, I>(&mut self, iter: I)
+    where
+        NamedLifetime: PartialEq<L> + From<&'a L>,
+        L: 'a,
+        I: IntoIterator<Item = &'a L>,
+    {
+        for lifetime in iter {
+            if self.id(lifetime).is_some() {
+                panic!(
+                    "lifetime name `{}` declared twice in the same scope",
+                    NamedLifetime::from(lifetime)
+                );
+            }
+
+            self.nodes.push(LifetimeNode {
+                lifetime: lifetime.into(),
+                shorter: vec![],
+                longer: vec![],
+            });
+        }
+    }
+
+    /// Add edges to the lifetime graph.
+    ///
+    /// This method is decoupled from [`LifetimeEnv::extend_lifetimes`] because
+    /// generics can define new lifetimes, while `where` clauses cannot.
+    ///
+    /// # Panics
+    ///
+    /// This method panics if any of the lifetime bounds aren't already defined
+    /// in the graph. This isn't allowed by rustc in the first place, so it should
+    /// only ever occur when deserializing an invalid [`LifetimeEnv`].
+    fn extend_bounds<'a, L, B, I>(&mut self, iter: I)
+    where
+        NamedLifetime: PartialEq<L> + From<&'a L>,
+        L: 'a,
+        B: IntoIterator<Item = &'a L>,
+        I: IntoIterator<Item = (&'a L, B)>,
+    {
+        for (lifetime, bounds) in iter {
+            let long = self.id(lifetime).expect("use of undeclared lifetime, this is a bug: try calling `LifetimeEnv::extend_lifetimes` first");
+            for bound in bounds {
+                let short = self
+                    .id(bound)
+                    .expect("cannot use undeclared lifetime as a bound");
+                self.nodes[short].longer.push(long);
+                self.nodes[long].shorter.push(short);
+            }
+        }
+    }
+}
+
+impl fmt::Display for LifetimeEnv {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        self.to_token_stream().fmt(f)
+    }
+}
+
+impl ToTokens for LifetimeEnv {
+    fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
+        for node in self.nodes.iter() {
+            let lifetime = &node.lifetime;
+            if node.shorter.is_empty() {
+                tokens.extend(quote! { #lifetime, });
+            } else {
+                let bounds = node.shorter.iter().map(|&id| &self.nodes[id].lifetime);
+                tokens.extend(quote! { #lifetime: #(#bounds)+*, });
+            }
+        }
+    }
+}
+
+/// Serialize a [`LifetimeEnv`] as a map from lifetimes to their bounds.
+impl Serialize for LifetimeEnv {
+    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+    where
+        S: serde::Serializer,
+    {
+        use serde::ser::SerializeMap;
+        let mut seq = serializer.serialize_map(Some(self.len()))?;
+
+        for node in self.nodes.iter() {
+            /// Helper type for serializing bounds.
+            struct Bounds<'a> {
+                ids: &'a [usize],
+                nodes: &'a [LifetimeNode],
+            }
+
+            impl<'a> Serialize for Bounds<'a> {
+                fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+                where
+                    S: serde::Serializer,
+                {
+                    use serde::ser::SerializeSeq;
+                    let mut seq = serializer.serialize_seq(Some(self.ids.len()))?;
+                    for &id in self.ids {
+                        seq.serialize_element(&self.nodes[id].lifetime)?;
+                    }
+                    seq.end()
+                }
+            }
+
+            seq.serialize_entry(
+                &node.lifetime,
+                &Bounds {
+                    ids: &node.shorter[..],
+                    nodes: &self.nodes,
+                },
+            )?;
+        }
+        seq.end()
+    }
+}
+
+impl<'de> Deserialize<'de> for LifetimeEnv {
+    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+    where
+        D: serde::Deserializer<'de>,
+    {
+        use std::collections::BTreeMap;
+
+        let m: BTreeMap<NamedLifetime, Vec<NamedLifetime>> =
+            Deserialize::deserialize(deserializer)?;
+
+        let mut this = LifetimeEnv::new();
+        this.extend_lifetimes(m.keys());
+        this.extend_bounds(m.iter());
+        Ok(this)
+    }
+}
+
+/// A lifetime, along with ptrs to all lifetimes that are explicitly
+/// shorter/longer than it.
+///
+/// This type is internal to [`LifetimeGraph`]- the ptrs are stored as `usize`s,
+/// meaning that they may be invalid if a `LifetimeEdges` is created in one
+/// `LifetimeGraph` and then used in another.
+#[derive(Clone, PartialEq, Eq, Hash, Debug)]
+pub(crate) struct LifetimeNode {
+    /// The name of the lifetime.
+    pub(crate) lifetime: NamedLifetime,
+
+    /// Pointers to all lifetimes that this lives _at least_ as long as.
+    ///
+    /// Note: This doesn't account for transitivity.
+    pub(crate) shorter: Vec<usize>,
+
+    /// Pointers to all lifetimes that live _at least_ as long as this.
+    ///
+    /// Note: This doesn't account for transitivity.
+    pub(crate) longer: Vec<usize>,
+}
+
+/// A lifetime, analogous to [`syn::Lifetime`].
+#[derive(Clone, Debug, Hash, Eq, PartialEq, Serialize, Deserialize)]
+#[non_exhaustive]
+pub enum Lifetime {
+    /// The `'static` lifetime.
+    Static,
+
+    /// A named lifetime, like `'a`.
+    Named(NamedLifetime),
+
+    /// An elided lifetime.
+    Anonymous,
+}
+
+impl Lifetime {
+    /// Returns the inner `NamedLifetime` if the lifetime is the `Named` variant,
+    /// otherwise `None`.
+    pub fn to_named(self) -> Option<NamedLifetime> {
+        if let Lifetime::Named(named) = self {
+            return Some(named);
+        }
+        None
+    }
+
+    /// Returns a reference to the inner `NamedLifetime` if the lifetime is the
+    /// `Named` variant, otherwise `None`.
+    pub fn as_named(&self) -> Option<&NamedLifetime> {
+        if let Lifetime::Named(named) = self {
+            return Some(named);
+        }
+        None
+    }
+}
+
+impl fmt::Display for Lifetime {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        match self {
+            Lifetime::Static => "'static".fmt(f),
+            Lifetime::Named(ref named) => named.fmt(f),
+            Lifetime::Anonymous => "'_".fmt(f),
+        }
+    }
+}
+
+impl ToTokens for Lifetime {
+    fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
+        match self {
+            Lifetime::Static => syn::Lifetime::new("'static", Span::call_site()).to_tokens(tokens),
+            Lifetime::Named(ref s) => s.to_tokens(tokens),
+            Lifetime::Anonymous => syn::Lifetime::new("'_", Span::call_site()).to_tokens(tokens),
+        };
+    }
+}
+
+impl From<&syn::Lifetime> for Lifetime {
+    fn from(lt: &syn::Lifetime) -> Self {
+        if lt.ident == "static" {
+            Self::Static
+        } else {
+            Self::Named(NamedLifetime((&lt.ident).into()))
+        }
+    }
+}
+
+impl From<&Option<syn::Lifetime>> for Lifetime {
+    fn from(lt: &Option<syn::Lifetime>) -> Self {
+        lt.as_ref().map(Into::into).unwrap_or(Self::Anonymous)
+    }
+}
+
+impl Lifetime {
+    /// Converts the [`Lifetime`] back into an AST node that can be spliced into a program.
+    pub fn to_syn(&self) -> Option<syn::Lifetime> {
+        match *self {
+            Self::Static => Some(syn::Lifetime::new("'static", Span::call_site())),
+            Self::Anonymous => None,
+            Self::Named(ref s) => Some(syn::Lifetime::new(&s.to_string(), Span::call_site())),
+        }
+    }
+}
+
+/// Collect all lifetimes that are either longer_or_shorter
+pub struct LifetimeTransitivity<'env> {
+    env: &'env LifetimeEnv,
+    visited: Vec<bool>,
+    out: Vec<&'env NamedLifetime>,
+    longer_or_shorter: LongerOrShorter,
+}
+
+impl<'env> LifetimeTransitivity<'env> {
+    /// Returns a new [`LifetimeTransitivity`] that finds all longer lifetimes.
+    pub fn longer(env: &'env LifetimeEnv) -> Self {
+        Self::new(env, LongerOrShorter::Longer)
+    }
+
+    /// Returns a new [`LifetimeTransitivity`] that finds all shorter lifetimes.
+    pub fn shorter(env: &'env LifetimeEnv) -> Self {
+        Self::new(env, LongerOrShorter::Shorter)
+    }
+
+    /// Returns all the lifetimes longer than a provided `NamedLifetime`.
+    pub fn longer_than(env: &'env LifetimeEnv, named: &NamedLifetime) -> Vec<&'env NamedLifetime> {
+        let mut this = Self::new(env, LongerOrShorter::Longer);
+        this.visit(named);
+        this.finish()
+    }
+
+    /// Returns all the lifetimes shorter than the provided `NamedLifetime`.
+    pub fn shorter_than(env: &'env LifetimeEnv, named: &NamedLifetime) -> Vec<&'env NamedLifetime> {
+        let mut this = Self::new(env, LongerOrShorter::Shorter);
+        this.visit(named);
+        this.finish()
+    }
+
+    /// Returns a new [`LifetimeTransitivity`].
+    fn new(env: &'env LifetimeEnv, longer_or_shorter: LongerOrShorter) -> Self {
+        LifetimeTransitivity {
+            env,
+            visited: vec![false; env.len()],
+            out: vec![],
+            longer_or_shorter,
+        }
+    }
+
+    /// Visits a lifetime, as well as all the nodes it's transitively longer or
+    /// shorter than, depending on how the `LifetimeTransitivity` was constructed.
+    pub fn visit(&mut self, named: &NamedLifetime) {
+        if let Some(id) = self
+            .env
+            .nodes
+            .iter()
+            .position(|node| node.lifetime == *named)
+        {
+            self.dfs(id);
+        }
+    }
+
+    /// Performs depth-first search through the `LifetimeEnv` created at construction
+    /// for all nodes longer or shorter than the node at the provided index,
+    /// depending on how the `LifetimeTransitivity` was constructed.
+    fn dfs(&mut self, index: usize) {
+        // Note: all of these indexings SHOULD be valid because
+        // `visited.len() == nodes.len()`, and the ids come from
+        // calling `Iterator::position` on `nodes`, which never shrinks.
+        // So we should be able to change these to `get_unchecked`...
+        if !self.visited[index] {
+            self.visited[index] = true;
+
+            let node = &self.env.nodes[index];
+            self.out.push(&node.lifetime);
+            for &edge_index in self.longer_or_shorter.edges(node).iter() {
+                self.dfs(edge_index);
+            }
+        }
+    }
+
+    /// Returns the transitively reachable lifetimes.
+    pub fn finish(self) -> Vec<&'env NamedLifetime> {
+        self.out
+    }
+}
+
+/// A helper type for [`LifetimeTransitivity`] determining whether to find the
+/// transitively longer or transitively shorter lifetimes.
+enum LongerOrShorter {
+    Longer,
+    Shorter,
+}
+
+impl LongerOrShorter {
+    /// Returns either the indices of the longer or shorter lifetimes, depending
+    /// on `self`.
+    fn edges<'node>(&self, node: &'node LifetimeNode) -> &'node [usize] {
+        match self {
+            LongerOrShorter::Longer => &node.longer[..],
+            LongerOrShorter::Shorter => &node.shorter[..],
+        }
+    }
+}
diff --git a/crates/diplomat_core/src/ast/methods.rs b/crates/diplomat_core/src/ast/methods.rs
new file mode 100644
index 0000000..dbf33a3
--- /dev/null
+++ b/crates/diplomat_core/src/ast/methods.rs
@@ -0,0 +1,615 @@
+use serde::{Deserialize, Serialize};
+use std::ops::ControlFlow;
+
+use super::docs::Docs;
+use super::{Attrs, Ident, Lifetime, LifetimeEnv, Mutability, PathType, TypeName};
+
+/// A method declared in the `impl` associated with an FFI struct.
+/// Includes both static and non-static methods, which can be distinguished
+/// by inspecting [`Method::self_param`].
+#[derive(Clone, PartialEq, Eq, Hash, Serialize, Debug)]
+#[non_exhaustive]
+pub struct Method {
+    /// The name of the method as initially declared.
+    pub name: Ident,
+
+    /// Lines of documentation for the method.
+    pub docs: Docs,
+
+    /// The name of the generated `extern "C"` function
+    pub abi_name: Ident,
+
+    /// The `self` param of the method, if any.
+    pub self_param: Option<SelfParam>,
+
+    /// All non-`self` params taken by the method.
+    pub params: Vec<Param>,
+
+    /// The return type of the method, if any.
+    pub return_type: Option<TypeName>,
+
+    /// The lifetimes introduced in this method and surrounding impl block.
+    pub lifetime_env: LifetimeEnv,
+
+    /// The list of `cfg` attributes (if any).
+    ///
+    /// These are strings instead of `syn::Attribute` or `proc_macro2::TokenStream`
+    /// because those types are not `PartialEq`, `Hash`, `Serialize`, etc.
+    pub attrs: Attrs,
+}
+
+impl Method {
+    /// Extracts a [`Method`] from an AST node inside an `impl`.
+    pub fn from_syn(
+        m: &syn::ImplItemFn,
+        self_path_type: PathType,
+        impl_generics: Option<&syn::Generics>,
+        impl_attrs: &Attrs,
+    ) -> Method {
+        let mut attrs = impl_attrs.clone();
+        attrs.add_attrs(&m.attrs);
+
+        let self_ident = self_path_type.path.elements.last().unwrap();
+        let method_ident = &m.sig.ident;
+        let concat_method_ident = format!("{self_ident}_{method_ident}");
+        let extern_ident = syn::Ident::new(
+            &attrs.abi_rename.apply(concat_method_ident.into()),
+            m.sig.ident.span(),
+        );
+
+        let all_params = m
+            .sig
+            .inputs
+            .iter()
+            .filter_map(|a| match a {
+                syn::FnArg::Receiver(_) => None,
+                syn::FnArg::Typed(ref t) => Some(Param::from_syn(t, self_path_type.clone())),
+            })
+            .collect::<Vec<_>>();
+
+        let self_param = m
+            .sig
+            .receiver()
+            .map(|rec| SelfParam::from_syn(rec, self_path_type.clone()));
+
+        let return_ty = match &m.sig.output {
+            syn::ReturnType::Type(_, return_typ) => {
+                // When we allow lifetime elision, this is where we would want to
+                // support it so we can insert the expanded explicit lifetimes.
+                Some(TypeName::from_syn(
+                    return_typ.as_ref(),
+                    Some(self_path_type),
+                ))
+            }
+            syn::ReturnType::Default => None,
+        };
+
+        let lifetime_env = LifetimeEnv::from_method_item(
+            m,
+            impl_generics,
+            self_param.as_ref(),
+            &all_params[..],
+            return_ty.as_ref(),
+        );
+
+        Method {
+            name: Ident::from(method_ident),
+            docs: Docs::from_attrs(&m.attrs),
+            abi_name: Ident::from(&extern_ident),
+            self_param,
+            params: all_params,
+            return_type: return_ty,
+            lifetime_env,
+            attrs,
+        }
+    }
+
+    /// Returns the parameters that the output is lifetime-bound to.
+    ///
+    /// # Examples
+    ///
+    /// Given the following method:
+    /// ```ignore
+    /// fn foo<'a, 'b: 'a, 'c>(&'a self, bar: Bar<'b>, baz: Baz<'c>) -> FooBar<'a> { ... }
+    /// ```
+    /// Then this method would return the `&'a self` and `bar: Bar<'b>` params
+    /// because `'a` is in the return type, and `'b` must live at least as long
+    /// as `'a`. It wouldn't include `baz: Baz<'c>` though, because the return
+    /// type isn't bound by `'c` in any way.
+    ///
+    /// # Panics
+    ///
+    /// This method may panic if `TypeName::check_result_type_validity` (called by
+    /// `Method::check_validity`) doesn't pass first, since the result type may
+    /// contain elided lifetimes that we depend on for this method. The validity
+    /// checks ensure that the return type doesn't elide any lifetimes, ensuring
+    /// that this method will produce correct results.
+    pub fn borrowed_params(&self) -> BorrowedParams {
+        // To determine which params the return type is bound to, we just have to
+        // find the params that contain a lifetime that's also in the return type.
+        if let Some(ref return_type) = self.return_type {
+            // The lifetimes that must outlive the return type
+            let lifetimes = return_type.longer_lifetimes(&self.lifetime_env);
+
+            let held_self_param = self.self_param.as_ref().filter(|self_param| {
+                // Check if `self` is a reference with a lifetime in the return type.
+                if let Some((Lifetime::Named(ref name), _)) = self_param.reference {
+                    if lifetimes.contains(&name) {
+                        return true;
+                    }
+                }
+                self_param.path_type.lifetimes.iter().any(|lt| {
+                    if let Lifetime::Named(name) = lt {
+                        lifetimes.contains(&name)
+                    } else {
+                        false
+                    }
+                })
+            });
+
+            // Collect all the params that contain a named lifetime that's also
+            // in the return type.
+            let held_params = self
+                .params
+                .iter()
+                .filter_map(|param| {
+                    let mut lt_kind = LifetimeKind::ReturnValue;
+                    param
+                        .ty
+                        .visit_lifetimes(&mut |lt, _| {
+                            // Thanks to `TypeName::visit_lifetimes`, we can
+                            // traverse the lifetimes without allocations and
+                            // short-circuit if we find a match.
+                            match lt {
+                                Lifetime::Named(name) if lifetimes.contains(&name) => {
+                                    return ControlFlow::Break(());
+                                }
+                                Lifetime::Static => {
+                                    lt_kind = LifetimeKind::Static;
+                                    return ControlFlow::Break(());
+                                }
+                                _ => {}
+                            };
+                            ControlFlow::Continue(())
+                        })
+                        .is_break()
+                        .then(|| (param, lt_kind))
+                })
+                .collect();
+
+            BorrowedParams(held_self_param, held_params)
+        } else {
+            BorrowedParams(None, vec![])
+        }
+    }
+
+    /// Checks whether the method qualifies for special write handling.
+    /// To qualify, a method must:
+    ///  - not return any value
+    ///  - have the last argument be an `&mut diplomat_runtime::DiplomatWrite`
+    ///
+    /// Typically, methods of this form will be transformed in the bindings to a
+    /// method that doesn't take the write as an argument but instead creates
+    /// one locally and just returns the final string.
+    pub fn is_write_out(&self) -> bool {
+        let return_compatible = self
+            .return_type
+            .as_ref()
+            .map(|return_type| match return_type {
+                TypeName::Unit => true,
+                TypeName::Result(ok, _, _) | TypeName::Option(ok, _) => {
+                    matches!(ok.as_ref(), TypeName::Unit)
+                }
+
+                _ => false,
+            })
+            .unwrap_or(true);
+
+        return_compatible && self.params.last().map(Param::is_write).unwrap_or(false)
+    }
+
+    /// Checks if any parameters are write (regardless of other compatibilities for write output)
+    pub fn has_write_param(&self) -> bool {
+        self.params.iter().any(|p| p.is_write())
+    }
+
+    /// Returns the documentation block
+    pub fn docs(&self) -> &Docs {
+        &self.docs
+    }
+}
+
+/// The `self` parameter taken by a [`Method`].
+#[derive(Clone, PartialEq, Eq, Hash, Serialize, Debug)]
+#[non_exhaustive]
+pub struct SelfParam {
+    /// The lifetime and mutability of the `self` param, if it's a reference.
+    pub reference: Option<(Lifetime, Mutability)>,
+
+    /// The type of the parameter, which will be a named reference to
+    /// the associated struct,
+    pub path_type: PathType,
+
+    /// Associated attributes with this self parameter. Used in Demo Generation, mostly.
+    pub attrs: Attrs,
+}
+
+impl SelfParam {
+    pub fn to_typename(&self) -> TypeName {
+        let typ = TypeName::Named(self.path_type.clone());
+        if let Some((ref lifetime, ref mutability)) = self.reference {
+            return TypeName::Reference(lifetime.clone(), *mutability, Box::new(typ));
+        }
+        typ
+    }
+
+    pub fn from_syn(rec: &syn::Receiver, path_type: PathType) -> Self {
+        SelfParam {
+            reference: rec
+                .reference
+                .as_ref()
+                .map(|(_, lt)| (lt.into(), Mutability::from_syn(&rec.mutability))),
+            path_type,
+            attrs: Attrs::from_attrs(&rec.attrs),
+        }
+    }
+}
+
+/// The `self` parameter taken by a [`TraitMethod`].
+#[derive(Clone, PartialEq, Eq, Hash, Serialize, Debug, Deserialize)]
+#[non_exhaustive]
+pub struct TraitSelfParam {
+    /// The lifetime and mutability of the `self` param, if it's a reference.
+    pub reference: Option<(Lifetime, Mutability)>,
+
+    /// The trait of the parameter, which will be a named reference to
+    /// the associated trait,
+    pub path_trait: PathType,
+}
+
+impl TraitSelfParam {
+    pub fn to_typename(&self) -> TypeName {
+        let typ = TypeName::ImplTrait(self.path_trait.clone());
+        if let Some((ref lifetime, ref mutability)) = self.reference {
+            return TypeName::Reference(lifetime.clone(), *mutability, Box::new(typ));
+        }
+        typ
+    }
+
+    pub fn from_syn(rec: &syn::Receiver, path_trait: PathType) -> Self {
+        TraitSelfParam {
+            reference: rec
+                .reference
+                .as_ref()
+                .map(|(_, lt)| (lt.into(), Mutability::from_syn(&rec.mutability))),
+            path_trait,
+        }
+    }
+}
+
+/// A parameter taken by a [`Method`], not including `self`.
+#[derive(Clone, PartialEq, Eq, Hash, Serialize, Debug)]
+#[non_exhaustive]
+pub struct Param {
+    /// The name of the parameter in the original method declaration.
+    pub name: Ident,
+
+    /// The type of the parameter.
+    pub ty: TypeName,
+
+    /// Parameter attributes (like #[diplomat::demo(label = "Out")])
+    pub attrs: Attrs,
+}
+
+impl Param {
+    /// Check if this parameter is a Write
+    pub fn is_write(&self) -> bool {
+        match self.ty {
+            TypeName::Reference(_, Mutability::Mutable, ref w) => **w == TypeName::Write,
+            _ => false,
+        }
+    }
+
+    pub fn from_syn(t: &syn::PatType, self_path_type: PathType) -> Self {
+        let ident = match t.pat.as_ref() {
+            syn::Pat::Ident(ident) => ident,
+            _ => panic!("Unexpected param type"),
+        };
+
+        let attrs = Attrs::from_attrs(&t.attrs);
+
+        Param {
+            name: (&ident.ident).into(),
+            ty: TypeName::from_syn(&t.ty, Some(self_path_type)),
+            attrs,
+        }
+    }
+}
+
+/// The type of lifetime.
+#[derive(Debug, Copy, Clone, PartialEq, Eq)]
+pub enum LifetimeKind {
+    /// Param must live at least as long as the returned object.
+    ReturnValue,
+    /// Param must live for the duration of the program.
+    Static,
+}
+
+#[derive(Default, Debug)]
+/// Parameters in a method that might be borrowed in the return type.
+#[non_exhaustive]
+pub struct BorrowedParams<'a>(
+    pub Option<&'a SelfParam>,
+    pub Vec<(&'a Param, LifetimeKind)>,
+);
+
+impl BorrowedParams<'_> {
+    /// Returns an [`Iterator`] through the names of the parameters that are borrowed
+    /// for the lifetime of the return value, accepting an `Ident` that the `self`
+    /// param will be called if present.
+    pub fn return_names<'a>(&'a self, self_name: &'a Ident) -> impl Iterator<Item = &'a Ident> {
+        self.0.iter().map(move |_| self_name).chain(
+            self.1
+                .iter()
+                .filter(|(_, ltk)| (*ltk == LifetimeKind::ReturnValue))
+                .map(|(param, _)| &param.name),
+        )
+    }
+
+    /// Returns an [`Iterator`] through the names of the parameters that are borrowed for a
+    /// static lifetime.
+    pub fn static_names(&self) -> impl Iterator<Item = &'_ Ident> {
+        self.1
+            .iter()
+            .filter(|(_, ltk)| (*ltk == LifetimeKind::Static))
+            .map(|(param, _)| &param.name)
+    }
+
+    /// Returns `true` if a provided param name is included in the borrowed params,
+    /// otherwise `false`.
+    ///
+    /// This method doesn't check the `self` parameter. Use
+    /// [`BorrowedParams::borrows_self`] instead.
+    pub fn contains(&self, param_name: &Ident) -> bool {
+        self.1.iter().any(|(param, _)| &param.name == param_name)
+    }
+
+    /// Returns `true` if there are no borrowed parameters, otherwise `false`.
+    pub fn is_empty(&self) -> bool {
+        self.0.is_none() && self.1.is_empty()
+    }
+
+    /// Returns `true` if the `self` param is borrowed, otherwise `false`.
+    pub fn borrows_self(&self) -> bool {
+        self.0.is_some()
+    }
+
+    /// Returns `true` if there are any borrowed params, otherwise `false`.
+    pub fn borrows_params(&self) -> bool {
+        !self.1.is_empty()
+    }
+
+    /// Returns the number of borrowed params.
+    pub fn len(&self) -> usize {
+        self.1.len() + usize::from(self.0.is_some())
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use insta;
+
+    use syn;
+
+    use crate::ast::{Attrs, Ident, Method, Path, PathType};
+
+    #[test]
+    fn static_methods() {
+        insta::assert_yaml_snapshot!(Method::from_syn(
+            &syn::parse_quote! {
+                /// Some docs.
+                #[diplomat::rust_link(foo::Bar::batz, FnInStruct)]
+                fn foo(x: u64, y: MyCustomStruct) {
+
+                }
+            },
+            PathType::new(Path::empty().sub_path(Ident::from("MyStructContainingMethod"))),
+            None,
+            &Attrs::default()
+        ));
+
+        insta::assert_yaml_snapshot!(Method::from_syn(
+            &syn::parse_quote! {
+                /// Some docs.
+                /// Some more docs.
+                ///
+                /// Even more docs.
+                #[diplomat::rust_link(foo::Bar::batz, FnInEnum)]
+                fn foo(x: u64, y: MyCustomStruct) -> u64 {
+                    x
+                }
+            },
+            PathType::new(Path::empty().sub_path(Ident::from("MyStructContainingMethod"))),
+            None,
+            &Attrs::default()
+        ));
+    }
+
+    #[test]
+    fn cfged_method() {
+        insta::assert_yaml_snapshot!(Method::from_syn(
+            &syn::parse_quote! {
+                /// Some docs.
+                #[diplomat::rust_link(foo::Bar::batz, FnInStruct)]
+                #[cfg(any(feature = "foo", not(feature = "bar")))]
+                fn foo(x: u64, y: MyCustomStruct) {
+
+                }
+            },
+            PathType::new(Path::empty().sub_path(Ident::from("MyStructContainingMethod"))),
+            None,
+            &Attrs::default()
+        ));
+    }
+
+    #[test]
+    fn nonstatic_methods() {
+        insta::assert_yaml_snapshot!(Method::from_syn(
+            &syn::parse_quote! {
+                fn foo(&self, x: u64, y: MyCustomStruct) {
+
+                }
+            },
+            PathType::new(Path::empty().sub_path(Ident::from("MyStructContainingMethod"))),
+            None,
+            &Attrs::default()
+        ));
+
+        insta::assert_yaml_snapshot!(Method::from_syn(
+            &syn::parse_quote! {
+                #[diplomat::rust_link(foo::Bar::batz, FnInStruct)]
+                fn foo(&mut self, x: u64, y: MyCustomStruct) -> u64 {
+                    x
+                }
+            },
+            PathType::new(Path::empty().sub_path(Ident::from("MyStructContainingMethod"))),
+            None,
+            &Attrs::default()
+        ));
+    }
+
+    macro_rules! assert_borrowed_params {
+        ([$($return_param:ident),*] $(, [$($static_param:ident),*])? => $($tokens:tt)* ) => {{
+            let method = Method::from_syn(
+                &syn::parse_quote! { $($tokens)* },
+                PathType::new(Path::empty().sub_path(Ident::from("MyStructContainingMethod"))),
+                None,
+                &Attrs::default()
+            );
+
+            let borrowed_params = method.borrowed_params();
+            // The ident parser in syn doesn't allow `self`, so we use "this" as a placeholder
+            // and then change it.
+            let mut actual_return: Vec<&str> = borrowed_params.return_names(&Ident::THIS).map(|ident| ident.as_str()).collect();
+            if borrowed_params.0.is_some() {
+                actual_return[0] = "self";
+            }
+            let expected_return: &[&str] = &[$(stringify!($return_param)),*];
+            assert_eq!(actual_return, expected_return);
+            let actual_static: Vec<&str> = borrowed_params.static_names().map(|ident| ident.as_str()).collect();
+            let expected_static: &[&str] = &[$($(stringify!($static_param)),*)?];
+            assert_eq!(actual_static, expected_static);
+        }};
+    }
+
+    #[test]
+    fn static_params_held_by_return_type() {
+        assert_borrowed_params! { [first, second] =>
+            #[diplomat::rust_link(foo::Bar::batz, FnInStruct)]
+            fn foo<'a, 'b>(first: &'a First, second: &'b Second, third: &Third) -> Foo<'a, 'b> {
+                unimplemented!()
+            }
+        }
+
+        assert_borrowed_params! { [hold] =>
+            #[diplomat::rust_link(Foo, FnInStruct)]
+            fn transitivity<'a, 'b: 'a, 'c: 'b, 'd: 'c, 'e: 'd, 'x>(hold: &'x One<'e>, nohold: &One<'x>) -> Box<Foo<'a>> {
+                unimplemented!()
+            }
+        }
+
+        assert_borrowed_params! { [hold] =>
+            #[diplomat::rust_link(Foo, FnInStruct)]
+            fn a_le_b_and_b_le_a<'a: 'b, 'b: 'a>(hold: &'b Bar, nohold: &'c Bar) -> Box<Foo<'a>> {
+                unimplemented!()
+            }
+        }
+
+        assert_borrowed_params! { [a, b, c, d] =>
+            #[diplomat::rust_link(Foo, FnInStruct)]
+            fn many_dependents<'a, 'b: 'a, 'c: 'a, 'd: 'b, 'x, 'y>(a: &'x One<'a>, b: &'b One<'a>, c: &Two<'x, 'c>, d: &'x Two<'d, 'y>, nohold: &'x Two<'x, 'y>) -> Box<Foo<'a>> {
+                unimplemented!()
+            }
+        }
+
+        assert_borrowed_params! { [hold] =>
+            #[diplomat::rust_link(Foo, FnInStruct)]
+            fn return_outlives_param<'short, 'long: 'short>(hold: &Two<'long, 'short>, nohold: &'short One<'short>) -> Box<Foo<'long>> {
+                unimplemented!()
+            }
+        }
+
+        assert_borrowed_params! { [hold] =>
+            #[diplomat::rust_link(Foo, FnInStruct)]
+            fn transitivity_deep_types<'a, 'b: 'a, 'c: 'b, 'd: 'c>(hold: Option<Box<Bar<'d>>>, nohold: &'a Box<Option<Baz<'a>>>) -> Result<Box<Foo<'b>>, Error> {
+                unimplemented!()
+            }
+        }
+
+        assert_borrowed_params! { [top, left, right, bottom] =>
+            #[diplomat::rust_link(Foo, FnInStruct)]
+            fn diamond_top<'top, 'left: 'top, 'right: 'top, 'bottom: 'left + 'right>(top: One<'top>, left: One<'left>, right: One<'right>, bottom: One<'bottom>) -> Box<Foo<'top>> {
+                unimplemented!()
+            }
+        }
+
+        assert_borrowed_params! { [left, bottom] =>
+            #[diplomat::rust_link(Foo, FnInStruct)]
+            fn diamond_left<'top, 'left: 'top, 'right: 'top, 'bottom: 'left + 'right>(top: One<'top>, left: One<'left>, right: One<'right>, bottom: One<'bottom>) -> Box<Foo<'left>> {
+                unimplemented!()
+            }
+        }
+
+        assert_borrowed_params! { [right, bottom] =>
+            #[diplomat::rust_link(Foo, FnInStruct)]
+            fn diamond_right<'top, 'left: 'top, 'right: 'top, 'bottom: 'left + 'right>(top: One<'top>, left: One<'left>, right: One<'right>, bottom: One<'bottom>) -> Box<Foo<'right>> {
+                unimplemented!()
+            }
+        }
+
+        assert_borrowed_params! { [bottom] =>
+            #[diplomat::rust_link(Foo, FnInStruct)]
+            fn diamond_bottom<'top, 'left: 'top, 'right: 'top, 'bottom: 'left + 'right>(top: One<'top>, left: One<'left>, right: One<'right>, bottom: One<'bottom>) -> Box<Foo<'bottom>> {
+                unimplemented!()
+            }
+        }
+
+        assert_borrowed_params! { [a, b, c, d] =>
+            #[diplomat::rust_link(Foo, FnInStruct)]
+            fn diamond_and_nested_types<'a, 'b: 'a, 'c: 'b, 'd: 'b + 'c, 'x, 'y>(a: &'x One<'a>, b: &'y One<'b>, c: &One<'c>, d: &One<'d>, nohold: &One<'x>) -> Box<Foo<'a>> {
+                unimplemented!()
+            }
+        }
+    }
+
+    #[test]
+    fn nonstatic_params_held_by_return_type() {
+        assert_borrowed_params! { [self] =>
+            #[diplomat::rust_link(foo::Bar::batz, FnInStruct)]
+            fn foo<'a>(&'a self) -> Foo<'a> {
+                unimplemented!()
+            }
+        }
+
+        assert_borrowed_params! { [self, foo, bar] =>
+            #[diplomat::rust_link(foo::Bar::batz, FnInStruct)]
+            fn foo<'x, 'y>(&'x self, foo: &'x Foo, bar: &Bar<'y>, baz: &Baz) -> Foo<'x, 'y> {
+                unimplemented!()
+            }
+        }
+
+        assert_borrowed_params! { [self, bar] =>
+            #[diplomat::rust_link(foo::Bar::batz, FnInStruct)]
+            fn foo<'a, 'b>(&'a self, bar: Bar<'b>) -> Foo<'a, 'b> {
+                unimplemented!()
+            }
+        }
+
+        assert_borrowed_params! { [self, bar], [baz] =>
+            #[diplomat::rust_link(foo::Bar::batz, FnInStruct)]
+            fn foo<'a, 'b>(&'a self, bar: Bar<'b>, baz: &'static str) -> Foo<'a, 'b, 'static> {
+                unimplemented!()
+            }
+        }
+    }
+}
diff --git a/crates/diplomat_core/src/ast/mod.rs b/crates/diplomat_core/src/ast/mod.rs
new file mode 100644
index 0000000..bde46af
--- /dev/null
+++ b/crates/diplomat_core/src/ast/mod.rs
@@ -0,0 +1,40 @@
+/// As part of the macro expansion and code generation process, Diplomat
+/// generates a simplified version of the Rust AST that captures special
+/// types such as opaque structs, [`Box`], and [`Result`] with utilities
+/// for handling such types.
+pub mod attrs;
+pub(crate) use attrs::AttrInheritContext;
+pub use attrs::Attrs;
+
+mod methods;
+pub use methods::{BorrowedParams, Method, Param, SelfParam, TraitSelfParam};
+
+mod modules;
+pub use modules::{File, Module};
+
+mod structs;
+pub use structs::{OpaqueStruct, Struct};
+
+mod traits;
+pub use traits::{Trait, TraitMethod};
+
+mod enums;
+pub use enums::Enum;
+
+mod types;
+pub use types::{
+    CustomType, LifetimeOrigin, ModSymbol, Mutability, PathType, PrimitiveType, StdlibOrDiplomat,
+    StringEncoding, TypeName,
+};
+
+pub(crate) mod lifetimes;
+pub use lifetimes::{Lifetime, LifetimeEnv, LifetimeTransitivity, NamedLifetime};
+
+mod paths;
+pub use paths::Path;
+
+mod idents;
+pub use idents::Ident;
+
+mod docs;
+pub use docs::{DocType, Docs, DocsUrlGenerator, RustLink, RustLinkDisplay};
diff --git a/crates/diplomat_core/src/ast/modules.rs b/crates/diplomat_core/src/ast/modules.rs
new file mode 100644
index 0000000..0879929
--- /dev/null
+++ b/crates/diplomat_core/src/ast/modules.rs
@@ -0,0 +1,416 @@
+use std::collections::{BTreeMap, HashSet};
+use std::fmt::Write as _;
+
+use quote::ToTokens;
+use serde::Serialize;
+use syn::{ImplItem, Item, ItemMod, UseTree, Visibility};
+
+use super::{
+    AttrInheritContext, Attrs, CustomType, Enum, Ident, Method, ModSymbol, Mutability,
+    OpaqueStruct, Path, PathType, RustLink, Struct, Trait,
+};
+use crate::environment::*;
+
+/// Custom Diplomat attribute that can be placed on a struct definition.
+#[derive(Debug)]
+enum DiplomatStructAttribute {
+    /// The `#[diplomat::out]` attribute, used for non-opaque structs that
+    /// contain an owned opaque in the form of a `Box`.
+    Out,
+    /// The `#[diplomat::opaque]` attribute, used for marking a struct as opaque.
+    /// Note that opaque structs can be borrowed in return types, but cannot
+    /// be passed into a function behind a mutable reference.
+    Opaque,
+    /// The `#[diplomat::opaque_mut]` attribute, used for marking a struct as
+    /// opaque and mutable.
+    /// Note that mutable opaque structs can never be borrowed in return types
+    /// (even immutably!), but can be passed into a function behind a mutable
+    /// reference.
+    OpaqueMut,
+}
+
+impl DiplomatStructAttribute {
+    /// Parses a [`DiplomatStructAttribute`] from an array of [`syn::Attribute`]s.
+    /// If more than one kind is found, an error is returned containing all the
+    /// ones encountered, since all the current attributes are disjoint.
+    fn parse(attrs: &[syn::Attribute]) -> Result<Option<Self>, Vec<Self>> {
+        let mut buf = String::with_capacity(32);
+        let mut res = Ok(None);
+        for attr in attrs {
+            buf.clear();
+            write!(&mut buf, "{}", attr.path().to_token_stream()).unwrap();
+            let parsed = match buf.as_str() {
+                "diplomat :: out" => Some(Self::Out),
+                "diplomat :: opaque" => Some(Self::Opaque),
+                "diplomat :: opaque_mut" => Some(Self::OpaqueMut),
+                _ => None,
+            };
+
+            if let Some(parsed) = parsed {
+                match res {
+                    Ok(None) => res = Ok(Some(parsed)),
+                    Ok(Some(first)) => res = Err(vec![first, parsed]),
+                    Err(ref mut errors) => errors.push(parsed),
+                }
+            }
+        }
+
+        res
+    }
+}
+
+#[derive(Clone, Serialize, Debug)]
+#[non_exhaustive]
+pub struct Module {
+    pub name: Ident,
+    pub imports: Vec<(Path, Ident)>,
+    pub declared_types: BTreeMap<Ident, CustomType>,
+    pub declared_traits: BTreeMap<Ident, Trait>,
+    pub sub_modules: Vec<Module>,
+    pub attrs: Attrs,
+}
+
+impl Module {
+    pub fn all_rust_links(&self) -> HashSet<&RustLink> {
+        let mut rust_links = self
+            .declared_types
+            .values()
+            .flat_map(|t| t.all_rust_links())
+            .collect::<HashSet<_>>();
+
+        self.sub_modules.iter().for_each(|m| {
+            rust_links.extend(m.all_rust_links().iter());
+        });
+        rust_links
+    }
+
+    pub fn insert_all_types(&self, in_path: Path, out: &mut Env) {
+        let mut mod_symbols = ModuleEnv::new(self.attrs.clone());
+
+        self.imports.iter().for_each(|(path, name)| {
+            mod_symbols.insert(name.clone(), ModSymbol::Alias(path.clone()));
+        });
+
+        self.declared_types.iter().for_each(|(k, v)| {
+            if mod_symbols
+                .insert(k.clone(), ModSymbol::CustomType(v.clone()))
+                .is_some()
+            {
+                panic!("Two types were declared with the same name, this needs to be implemented");
+            }
+        });
+
+        self.declared_traits.iter().for_each(|(k, v)| {
+            if mod_symbols
+                .insert(k.clone(), ModSymbol::Trait(v.clone()))
+                .is_some()
+            {
+                panic!("Two traits were declared with the same name, this needs to be implemented");
+            }
+        });
+
+        let path_to_self = in_path.sub_path(self.name.clone());
+        self.sub_modules.iter().for_each(|m| {
+            m.insert_all_types(path_to_self.clone(), out);
+            mod_symbols.insert(m.name.clone(), ModSymbol::SubModule(m.name.clone()));
+        });
+
+        out.insert(path_to_self, mod_symbols);
+    }
+
+    pub fn from_syn(input: &ItemMod, force_analyze: bool) -> Module {
+        let mut custom_types_by_name = BTreeMap::new();
+        let mut custom_traits_by_name = BTreeMap::new();
+        let mut sub_modules = Vec::new();
+        let mut imports = Vec::new();
+
+        let analyze_types = force_analyze
+            || input
+                .attrs
+                .iter()
+                .any(|a| a.path().to_token_stream().to_string() == "diplomat :: bridge");
+
+        let mod_attrs: Attrs = (&*input.attrs).into();
+
+        let impl_parent_attrs: Attrs =
+            mod_attrs.attrs_for_inheritance(AttrInheritContext::MethodOrImplFromModule);
+        let type_parent_attrs: Attrs = mod_attrs.attrs_for_inheritance(AttrInheritContext::Type);
+
+        input
+            .content
+            .as_ref()
+            .map(|t| &t.1[..])
+            .unwrap_or_default()
+            .iter()
+            .for_each(|a| match a {
+                Item::Use(u) => {
+                    if analyze_types {
+                        extract_imports(&Path::empty(), &u.tree, &mut imports);
+                    }
+                }
+                Item::Struct(strct) => {
+                    if analyze_types {
+                        let custom_type = match DiplomatStructAttribute::parse(&strct.attrs[..]) {
+                            Ok(None) => CustomType::Struct(Struct::new(strct, false, &type_parent_attrs)),
+                            Ok(Some(DiplomatStructAttribute::Out)) => {
+                                CustomType::Struct(Struct::new(strct, true, &type_parent_attrs))
+                            }
+                            Ok(Some(DiplomatStructAttribute::Opaque)) => {
+                                CustomType::Opaque(OpaqueStruct::new(strct, Mutability::Immutable, &type_parent_attrs))
+                            }
+                            Ok(Some(DiplomatStructAttribute::OpaqueMut)) => {
+                                CustomType::Opaque(OpaqueStruct::new(strct, Mutability::Mutable, &type_parent_attrs))
+                            }
+                            Err(errors) => {
+                                panic!("Multiple conflicting Diplomat struct attributes, there can be at most one: {errors:?}");
+                            }
+                        };
+
+                        custom_types_by_name.insert(Ident::from(&strct.ident), custom_type);
+                    }
+                }
+
+                Item::Enum(enm) => {
+                    if analyze_types {
+                        let ident = (&enm.ident).into();
+                        let enm = Enum::new(enm, &type_parent_attrs);
+                        custom_types_by_name
+                            .insert(ident, CustomType::Enum(enm));
+                    }
+                }
+
+                Item::Impl(imp) => {
+                    if analyze_types && imp.trait_.is_none() {
+                        let self_path = match imp.self_ty.as_ref() {
+                            syn::Type::Path(s) => PathType::from(s),
+                            _ => panic!("Self type not found"),
+                        };
+                        let mut impl_attrs = impl_parent_attrs.clone();
+                        impl_attrs.add_attrs(&imp.attrs);
+                        let method_parent_attrs = impl_attrs.attrs_for_inheritance(AttrInheritContext::MethodFromImpl);
+                        let mut new_methods = imp
+                            .items
+                            .iter()
+                            .filter_map(|i| match i {
+                                ImplItem::Fn(m) => Some(m),
+                                _ => None,
+                            })
+                            .filter(|m| matches!(m.vis, Visibility::Public(_)))
+                            .map(|m| Method::from_syn(m, self_path.clone(), Some(&imp.generics), &method_parent_attrs))
+                            .collect();
+
+                        let self_ident = self_path.path.elements.last().unwrap();
+
+                        match custom_types_by_name.get_mut(self_ident).unwrap() {
+                            CustomType::Struct(strct) => {
+                                strct.methods.append(&mut new_methods);
+                            }
+                            CustomType::Opaque(strct) => {
+                                strct.methods.append(&mut new_methods);
+                            }
+                            CustomType::Enum(enm) => {
+                                enm.methods.append(&mut new_methods);
+                            }
+                        }
+                    }
+                }
+                Item::Mod(item_mod) => {
+                    sub_modules.push(Module::from_syn(item_mod, false));
+                }
+                Item::Trait(trt) => {
+                    if analyze_types {
+                        let ident = (&trt.ident).into();
+                        let trt = Trait::new(trt, &type_parent_attrs);
+                        custom_traits_by_name
+                            .insert(ident, trt);
+                    }
+                }
+                _ => {}
+            });
+
+        Module {
+            name: (&input.ident).into(),
+            imports,
+            declared_types: custom_types_by_name,
+            declared_traits: custom_traits_by_name,
+            sub_modules,
+            attrs: mod_attrs,
+        }
+    }
+}
+
+fn extract_imports(base_path: &Path, use_tree: &UseTree, out: &mut Vec<(Path, Ident)>) {
+    match use_tree {
+        UseTree::Name(name) => out.push((
+            base_path.sub_path((&name.ident).into()),
+            (&name.ident).into(),
+        )),
+        UseTree::Path(path) => {
+            extract_imports(&base_path.sub_path((&path.ident).into()), &path.tree, out)
+        }
+        UseTree::Glob(_) => todo!("Glob imports are not yet supported"),
+        UseTree::Group(group) => {
+            group
+                .items
+                .iter()
+                .for_each(|i| extract_imports(base_path, i, out));
+        }
+        UseTree::Rename(rename) => out.push((
+            base_path.sub_path((&rename.ident).into()),
+            (&rename.rename).into(),
+        )),
+    }
+}
+
+#[derive(Serialize, Clone, Debug)]
+#[non_exhaustive]
+pub struct File {
+    pub modules: BTreeMap<String, Module>,
+}
+
+impl File {
+    /// Fuses all declared types into a single environment `HashMap`.
+    pub fn all_types(&self) -> Env {
+        let mut out = Env::default();
+        let mut top_symbols = ModuleEnv::new(Default::default());
+
+        self.modules.values().for_each(|m| {
+            m.insert_all_types(Path::empty(), &mut out);
+            top_symbols.insert(m.name.clone(), ModSymbol::SubModule(m.name.clone()));
+        });
+
+        out.insert(Path::empty(), top_symbols);
+
+        out
+    }
+
+    pub fn all_rust_links(&self) -> HashSet<&RustLink> {
+        self.modules
+            .values()
+            .flat_map(|m| m.all_rust_links().into_iter())
+            .collect()
+    }
+}
+
+impl From<&syn::File> for File {
+    /// Get all custom types across all modules defined in a given file.
+    fn from(file: &syn::File) -> File {
+        let mut out = BTreeMap::new();
+        file.items.iter().for_each(|i| {
+            if let Item::Mod(item_mod) = i {
+                out.insert(
+                    item_mod.ident.to_string(),
+                    Module::from_syn(item_mod, false),
+                );
+            }
+        });
+
+        File { modules: out }
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use insta::{self, Settings};
+
+    use syn;
+
+    use crate::ast::{File, Module};
+
+    #[test]
+    fn simple_mod() {
+        let mut settings = Settings::new();
+        settings.set_sort_maps(true);
+
+        settings.bind(|| {
+            insta::assert_yaml_snapshot!(Module::from_syn(
+                &syn::parse_quote! {
+                    mod ffi {
+                        struct NonOpaqueStruct {
+                            a: i32,
+                            b: Box<NonOpaqueStruct>
+                        }
+
+                        impl NonOpaqueStruct {
+                            pub fn new(x: i32) -> NonOpaqueStruct {
+                                unimplemented!();
+                            }
+
+                            pub fn set_a(&mut self, new_a: i32) {
+                                self.a = new_a;
+                            }
+                        }
+
+                        #[diplomat::opaque]
+                        struct OpaqueStruct {
+                            a: SomeExternalType
+                        }
+
+                        impl OpaqueStruct {
+                            pub fn new() -> Box<OpaqueStruct> {
+                                unimplemented!();
+                            }
+
+                            pub fn get_string(&self) -> String {
+                                unimplemented!()
+                            }
+                        }
+                    }
+                },
+                true
+            ));
+        });
+    }
+
+    #[test]
+    fn method_visibility() {
+        let mut settings = Settings::new();
+        settings.set_sort_maps(true);
+
+        settings.bind(|| {
+            insta::assert_yaml_snapshot!(Module::from_syn(
+                &syn::parse_quote! {
+                    #[diplomat::bridge]
+                    mod ffi {
+                        struct Foo {}
+
+                        impl Foo {
+                            pub fn pub_fn() {
+                                unimplemented!()
+                            }
+                            pub(crate) fn pub_crate_fn() {
+                                unimplemented!()
+                            }
+                            pub(super) fn pub_super_fn() {
+                                unimplemented!()
+                            }
+                            fn priv_fn() {
+                                unimplemented!()
+                            }
+                        }
+                    }
+                },
+                true
+            ));
+        });
+    }
+
+    #[test]
+    fn import_in_non_diplomat_not_analyzed() {
+        let mut settings = Settings::new();
+        settings.set_sort_maps(true);
+
+        settings.bind(|| {
+            insta::assert_yaml_snapshot!(File::from(&syn::parse_quote! {
+                #[diplomat::bridge]
+                mod ffi {
+                    struct Foo {}
+                }
+
+                mod other {
+                    use something::*;
+                }
+            }));
+        });
+    }
+}
diff --git a/crates/diplomat_core/src/ast/paths.rs b/crates/diplomat_core/src/ast/paths.rs
new file mode 100644
index 0000000..8bd9129
--- /dev/null
+++ b/crates/diplomat_core/src/ast/paths.rs
@@ -0,0 +1,77 @@
+use serde::{Deserialize, Serialize};
+use std::fmt;
+
+use super::Ident;
+
+#[derive(Hash, Eq, PartialEq, Deserialize, Serialize, Clone, Debug, Ord, PartialOrd)]
+#[non_exhaustive]
+pub struct Path {
+    pub elements: Vec<Ident>,
+}
+
+impl Path {
+    pub fn get_super(&self) -> Path {
+        let mut new_elements = self.elements.clone();
+        new_elements.remove(new_elements.len() - 1);
+        Path {
+            elements: new_elements,
+        }
+    }
+
+    pub fn sub_path(&self, ident: Ident) -> Path {
+        let mut new_elements = self.elements.clone();
+        new_elements.push(ident);
+        Path {
+            elements: new_elements,
+        }
+    }
+
+    pub fn to_syn(&self) -> syn::Path {
+        syn::Path {
+            leading_colon: None,
+            segments: self
+                .elements
+                .iter()
+                .map(|s| syn::PathSegment {
+                    ident: s.to_syn(),
+                    arguments: syn::PathArguments::None,
+                })
+                .collect(),
+        }
+    }
+
+    pub fn from_syn(path: &syn::Path) -> Path {
+        Path {
+            elements: path
+                .segments
+                .iter()
+                .map(|seg| (&seg.ident).into())
+                .collect(),
+        }
+    }
+
+    pub fn empty() -> Path {
+        Path { elements: vec![] }
+    }
+}
+
+impl fmt::Display for Path {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        if let Some((head, tail)) = self.elements.split_first() {
+            head.fmt(f)?;
+            for seg in tail {
+                "::".fmt(f)?;
+                seg.fmt(f)?;
+            }
+        }
+        Ok(())
+    }
+}
+
+impl FromIterator<Ident> for Path {
+    fn from_iter<T: IntoIterator<Item = Ident>>(iter: T) -> Self {
+        Path {
+            elements: iter.into_iter().collect(),
+        }
+    }
+}
diff --git a/crates/diplomat_core/src/ast/snapshots/diplomat_core__ast__attrs__tests__attr.snap b/crates/diplomat_core/src/ast/snapshots/diplomat_core__ast__attrs__tests__attr.snap
new file mode 100644
index 0000000..d105a73
--- /dev/null
+++ b/crates/diplomat_core/src/ast/snapshots/diplomat_core__ast__attrs__tests__attr.snap
@@ -0,0 +1,12 @@
+---
+source: core/src/ast/attrs.rs
+expression: attr
+---
+cfg:
+  Any:
+    - BackendName: cpp
+    - NameValue:
+        - has
+        - overloading
+meta: namespacing
+
diff --git a/crates/diplomat_core/src/ast/snapshots/diplomat_core__ast__attrs__tests__cfgs-2.snap b/crates/diplomat_core/src/ast/snapshots/diplomat_core__ast__attrs__tests__cfgs-2.snap
new file mode 100644
index 0000000..cc58dea
--- /dev/null
+++ b/crates/diplomat_core/src/ast/snapshots/diplomat_core__ast__attrs__tests__cfgs-2.snap
@@ -0,0 +1,6 @@
+---
+source: core/src/ast/attrs.rs
+expression: attr_cfg
+---
+BackendName: cpp
+
diff --git a/crates/diplomat_core/src/ast/snapshots/diplomat_core__ast__attrs__tests__cfgs-3.snap b/crates/diplomat_core/src/ast/snapshots/diplomat_core__ast__attrs__tests__cfgs-3.snap
new file mode 100644
index 0000000..635fcc9
--- /dev/null
+++ b/crates/diplomat_core/src/ast/snapshots/diplomat_core__ast__attrs__tests__cfgs-3.snap
@@ -0,0 +1,8 @@
+---
+source: core/src/ast/attrs.rs
+expression: attr_cfg
+---
+NameValue:
+  - has
+  - overloading
+
diff --git a/crates/diplomat_core/src/ast/snapshots/diplomat_core__ast__attrs__tests__cfgs-4.snap b/crates/diplomat_core/src/ast/snapshots/diplomat_core__ast__attrs__tests__cfgs-4.snap
new file mode 100644
index 0000000..635fcc9
--- /dev/null
+++ b/crates/diplomat_core/src/ast/snapshots/diplomat_core__ast__attrs__tests__cfgs-4.snap
@@ -0,0 +1,8 @@
+---
+source: core/src/ast/attrs.rs
+expression: attr_cfg
+---
+NameValue:
+  - has
+  - overloading
+
diff --git a/crates/diplomat_core/src/ast/snapshots/diplomat_core__ast__attrs__tests__cfgs-5.snap b/crates/diplomat_core/src/ast/snapshots/diplomat_core__ast__attrs__tests__cfgs-5.snap
new file mode 100644
index 0000000..5f7daa1
--- /dev/null
+++ b/crates/diplomat_core/src/ast/snapshots/diplomat_core__ast__attrs__tests__cfgs-5.snap
@@ -0,0 +1,14 @@
+---
+source: core/src/ast/attrs.rs
+expression: attr_cfg
+---
+Any:
+  - All:
+      - Star
+      - BackendName: cpp
+      - NameValue:
+          - has
+          - overloading
+  - Not:
+      BackendName: js
+
diff --git a/crates/diplomat_core/src/ast/snapshots/diplomat_core__ast__attrs__tests__cfgs.snap b/crates/diplomat_core/src/ast/snapshots/diplomat_core__ast__attrs__tests__cfgs.snap
new file mode 100644
index 0000000..658b25d
--- /dev/null
+++ b/crates/diplomat_core/src/ast/snapshots/diplomat_core__ast__attrs__tests__cfgs.snap
@@ -0,0 +1,6 @@
+---
+source: core/src/ast/attrs.rs
+expression: attr_cfg
+---
+Star
+
diff --git a/crates/diplomat_core/src/ast/snapshots/diplomat_core__ast__attrs__tests__rename-2.snap b/crates/diplomat_core/src/ast/snapshots/diplomat_core__ast__attrs__tests__rename-2.snap
new file mode 100644
index 0000000..2bb6693
--- /dev/null
+++ b/crates/diplomat_core/src/ast/snapshots/diplomat_core__ast__attrs__tests__rename-2.snap
@@ -0,0 +1,8 @@
+---
+source: core/src/ast/attrs.rs
+expression: attr
+---
+pattern:
+  replacement: foobar_
+  insertion_index: 7
+
diff --git a/crates/diplomat_core/src/ast/snapshots/diplomat_core__ast__attrs__tests__rename.snap b/crates/diplomat_core/src/ast/snapshots/diplomat_core__ast__attrs__tests__rename.snap
new file mode 100644
index 0000000..2bb6693
--- /dev/null
+++ b/crates/diplomat_core/src/ast/snapshots/diplomat_core__ast__attrs__tests__rename.snap
@@ -0,0 +1,8 @@
+---
+source: core/src/ast/attrs.rs
+expression: attr
+---
+pattern:
+  replacement: foobar_
+  insertion_index: 7
+
diff --git a/crates/diplomat_core/src/ast/snapshots/diplomat_core__ast__enums__tests__enum_with_discr.snap b/crates/diplomat_core/src/ast/snapshots/diplomat_core__ast__enums__tests__enum_with_discr.snap
new file mode 100644
index 0000000..56d6eaf
--- /dev/null
+++ b/crates/diplomat_core/src/ast/snapshots/diplomat_core__ast__enums__tests__enum_with_discr.snap
@@ -0,0 +1,37 @@
+---
+source: core/src/ast/enums.rs
+expression: "Enum::new(&syn::parse_quote! {\n                /// Some docs.\n                #[diplomat :: rust_link(foo :: Bar, Enum)] enum\n                DiscriminantedEnum { Abc = - 1, Def = 0, Ghi = 1, Jkl = 2, }\n            }, &Default::default())"
+---
+name: DiscriminantedEnum
+docs:
+  - Some docs.
+  - - path:
+        elements:
+          - foo
+          - Bar
+      typ: Enum
+      display: Normal
+variants:
+  - - Abc
+    - -1
+    - - ""
+      - []
+    - {}
+  - - Def
+    - 0
+    - - ""
+      - []
+    - {}
+  - - Ghi
+    - 1
+    - - ""
+      - []
+    - {}
+  - - Jkl
+    - 2
+    - - ""
+      - []
+    - {}
+methods: []
+attrs: {}
+
diff --git a/crates/diplomat_core/src/ast/snapshots/diplomat_core__ast__enums__tests__simple_enum.snap b/crates/diplomat_core/src/ast/snapshots/diplomat_core__ast__enums__tests__simple_enum.snap
new file mode 100644
index 0000000..3c509d5
--- /dev/null
+++ b/crates/diplomat_core/src/ast/snapshots/diplomat_core__ast__enums__tests__simple_enum.snap
@@ -0,0 +1,27 @@
+---
+source: core/src/ast/enums.rs
+expression: "Enum::new(&syn::parse_quote! {\n                /// Some docs.\n                #[diplomat :: rust_link(foo :: Bar, Enum)] enum MyLocalEnum\n                {\n                    Abc, /// Some more docs.\n                    Def\n                }\n            }, &Default::default())"
+---
+name: MyLocalEnum
+docs:
+  - Some docs.
+  - - path:
+        elements:
+          - foo
+          - Bar
+      typ: Enum
+      display: Normal
+variants:
+  - - Abc
+    - 0
+    - - ""
+      - []
+    - {}
+  - - Def
+    - 1
+    - - Some more docs.
+      - []
+    - {}
+methods: []
+attrs: {}
+
diff --git a/crates/diplomat_core/src/ast/snapshots/diplomat_core__ast__methods__tests__cfged_method.snap b/crates/diplomat_core/src/ast/snapshots/diplomat_core__ast__methods__tests__cfged_method.snap
new file mode 100644
index 0000000..245fe61
--- /dev/null
+++ b/crates/diplomat_core/src/ast/snapshots/diplomat_core__ast__methods__tests__cfged_method.snap
@@ -0,0 +1,34 @@
+---
+source: core/src/ast/methods.rs
+expression: "Method::from_syn(&syn::parse_quote! {\n                #[doc = r\" Some docs.\"]\n                #[diplomat::rust_link(foo::Bar::batz, FnInStruct)]\n                #[cfg(any(feature = \"foo\", not(feature = \"bar\")))] fn\n                foo(x: u64, y: MyCustomStruct) {}\n            },\n    PathType::new(Path::empty().sub_path(Ident::from(\"MyStructContainingMethod\"))),\n    None, &Attrs::default())"
+---
+name: foo
+docs:
+  - Some docs.
+  - - path:
+        elements:
+          - foo
+          - Bar
+          - batz
+      typ: FnInStruct
+      display: Normal
+abi_name: MyStructContainingMethod_foo
+self_param: ~
+params:
+  - name: x
+    ty:
+      Primitive: u64
+    attrs: {}
+  - name: y
+    ty:
+      Named:
+        path:
+          elements:
+            - MyCustomStruct
+        lifetimes: []
+    attrs: {}
+return_type: ~
+lifetime_env: {}
+attrs:
+  cfg:
+    - "# [cfg (any (feature = \"foo\" , not (feature = \"bar\")))]"
diff --git a/crates/diplomat_core/src/ast/snapshots/diplomat_core__ast__methods__tests__nonstatic_methods-2.snap b/crates/diplomat_core/src/ast/snapshots/diplomat_core__ast__methods__tests__nonstatic_methods-2.snap
new file mode 100644
index 0000000..5dfc34c
--- /dev/null
+++ b/crates/diplomat_core/src/ast/snapshots/diplomat_core__ast__methods__tests__nonstatic_methods-2.snap
@@ -0,0 +1,42 @@
+---
+source: core/src/ast/methods.rs
+expression: "Method::from_syn(&syn::parse_quote! {\n                #[diplomat::rust_link(foo::Bar::batz, FnInStruct)] fn\n                foo(&mut self, x: u64, y: MyCustomStruct) -> u64 { x }\n            },\n    PathType::new(Path::empty().sub_path(Ident::from(\"MyStructContainingMethod\"))),\n    None, &Attrs::default())"
+---
+name: foo
+docs:
+  - ""
+  - - path:
+        elements:
+          - foo
+          - Bar
+          - batz
+      typ: FnInStruct
+      display: Normal
+abi_name: MyStructContainingMethod_foo
+self_param:
+  reference:
+    - Anonymous
+    - Mutable
+  path_type:
+    path:
+      elements:
+        - MyStructContainingMethod
+    lifetimes: []
+  attrs: {}
+params:
+  - name: x
+    ty:
+      Primitive: u64
+    attrs: {}
+  - name: y
+    ty:
+      Named:
+        path:
+          elements:
+            - MyCustomStruct
+        lifetimes: []
+    attrs: {}
+return_type:
+  Primitive: u64
+lifetime_env: {}
+attrs: {}
diff --git a/crates/diplomat_core/src/ast/snapshots/diplomat_core__ast__methods__tests__nonstatic_methods.snap b/crates/diplomat_core/src/ast/snapshots/diplomat_core__ast__methods__tests__nonstatic_methods.snap
new file mode 100644
index 0000000..4147b13
--- /dev/null
+++ b/crates/diplomat_core/src/ast/snapshots/diplomat_core__ast__methods__tests__nonstatic_methods.snap
@@ -0,0 +1,35 @@
+---
+source: core/src/ast/methods.rs
+expression: "Method::from_syn(&syn::parse_quote! {\n                fn foo(&self, x: u64, y: MyCustomStruct) {}\n            },\n    PathType::new(Path::empty().sub_path(Ident::from(\"MyStructContainingMethod\"))),\n    None, &Attrs::default())"
+---
+name: foo
+docs:
+  - ""
+  - []
+abi_name: MyStructContainingMethod_foo
+self_param:
+  reference:
+    - Anonymous
+    - Immutable
+  path_type:
+    path:
+      elements:
+        - MyStructContainingMethod
+    lifetimes: []
+  attrs: {}
+params:
+  - name: x
+    ty:
+      Primitive: u64
+    attrs: {}
+  - name: y
+    ty:
+      Named:
+        path:
+          elements:
+            - MyCustomStruct
+        lifetimes: []
+    attrs: {}
+return_type: ~
+lifetime_env: {}
+attrs: {}
diff --git a/crates/diplomat_core/src/ast/snapshots/diplomat_core__ast__methods__tests__static_methods-2.snap b/crates/diplomat_core/src/ast/snapshots/diplomat_core__ast__methods__tests__static_methods-2.snap
new file mode 100644
index 0000000..516f511
--- /dev/null
+++ b/crates/diplomat_core/src/ast/snapshots/diplomat_core__ast__methods__tests__static_methods-2.snap
@@ -0,0 +1,33 @@
+---
+source: core/src/ast/methods.rs
+expression: "Method::from_syn(&syn::parse_quote! {\n                #[doc = r\" Some docs.\"] #[doc = r\" Some more docs.\"]\n                #[doc = r\"\"] #[doc = r\" Even more docs.\"]\n                #[diplomat::rust_link(foo::Bar::batz, FnInEnum)] fn\n                foo(x: u64, y: MyCustomStruct) -> u64 { x }\n            },\n    PathType::new(Path::empty().sub_path(Ident::from(\"MyStructContainingMethod\"))),\n    None, &Attrs::default())"
+---
+name: foo
+docs:
+  - "Some docs.\nSome more docs.\n\nEven more docs."
+  - - path:
+        elements:
+          - foo
+          - Bar
+          - batz
+      typ: FnInEnum
+      display: Normal
+abi_name: MyStructContainingMethod_foo
+self_param: ~
+params:
+  - name: x
+    ty:
+      Primitive: u64
+    attrs: {}
+  - name: y
+    ty:
+      Named:
+        path:
+          elements:
+            - MyCustomStruct
+        lifetimes: []
+    attrs: {}
+return_type:
+  Primitive: u64
+lifetime_env: {}
+attrs: {}
diff --git a/crates/diplomat_core/src/ast/snapshots/diplomat_core__ast__methods__tests__static_methods.snap b/crates/diplomat_core/src/ast/snapshots/diplomat_core__ast__methods__tests__static_methods.snap
new file mode 100644
index 0000000..58bc55c
--- /dev/null
+++ b/crates/diplomat_core/src/ast/snapshots/diplomat_core__ast__methods__tests__static_methods.snap
@@ -0,0 +1,32 @@
+---
+source: core/src/ast/methods.rs
+expression: "Method::from_syn(&syn::parse_quote! {\n                #[doc = r\" Some docs.\"]\n                #[diplomat::rust_link(foo::Bar::batz, FnInStruct)] fn\n                foo(x: u64, y: MyCustomStruct) {}\n            },\n    PathType::new(Path::empty().sub_path(Ident::from(\"MyStructContainingMethod\"))),\n    None, &Attrs::default())"
+---
+name: foo
+docs:
+  - Some docs.
+  - - path:
+        elements:
+          - foo
+          - Bar
+          - batz
+      typ: FnInStruct
+      display: Normal
+abi_name: MyStructContainingMethod_foo
+self_param: ~
+params:
+  - name: x
+    ty:
+      Primitive: u64
+    attrs: {}
+  - name: y
+    ty:
+      Named:
+        path:
+          elements:
+            - MyCustomStruct
+        lifetimes: []
+    attrs: {}
+return_type: ~
+lifetime_env: {}
+attrs: {}
diff --git a/crates/diplomat_core/src/ast/snapshots/diplomat_core__ast__modules__tests__import_in_non_diplomat_not_analyzed.snap b/crates/diplomat_core/src/ast/snapshots/diplomat_core__ast__modules__tests__import_in_non_diplomat_not_analyzed.snap
new file mode 100644
index 0000000..f7e4241
--- /dev/null
+++ b/crates/diplomat_core/src/ast/snapshots/diplomat_core__ast__modules__tests__import_in_non_diplomat_not_analyzed.snap
@@ -0,0 +1,31 @@
+---
+source: core/src/ast/modules.rs
+expression: "File::from(&syn::parse_quote! {\n                #[diplomat :: bridge] mod ffi { struct Foo {} } mod other\n                { use something :: * ; }\n            })"
+---
+modules:
+  ffi:
+    name: ffi
+    imports: []
+    declared_types:
+      Foo:
+        Struct:
+          name: Foo
+          docs:
+            - ""
+            - []
+          lifetimes: {}
+          fields: []
+          methods: []
+          output_only: false
+          attrs: {}
+    declared_traits: {}
+    sub_modules: []
+    attrs: {}
+  other:
+    name: other
+    imports: []
+    declared_types: {}
+    declared_traits: {}
+    sub_modules: []
+    attrs: {}
+
diff --git a/crates/diplomat_core/src/ast/snapshots/diplomat_core__ast__modules__tests__method_visibility.snap b/crates/diplomat_core/src/ast/snapshots/diplomat_core__ast__modules__tests__method_visibility.snap
new file mode 100644
index 0000000..f28031d
--- /dev/null
+++ b/crates/diplomat_core/src/ast/snapshots/diplomat_core__ast__modules__tests__method_visibility.snap
@@ -0,0 +1,32 @@
+---
+source: core/src/ast/modules.rs
+assertion_line: 371
+expression: "Module::from_syn(&syn::parse_quote! {\n                #[diplomat::bridge] mod ffi\n                {\n                    struct Foo {} impl Foo\n                    {\n                        pub fn pub_fn() { unimplemented!() } pub(crate) fn\n                        pub_crate_fn() { unimplemented!() } pub(super) fn\n                        pub_super_fn() { unimplemented!() } fn priv_fn()\n                        { unimplemented!() }\n                    }\n                }\n            }, true)"
+---
+name: ffi
+imports: []
+declared_types:
+  Foo:
+    Struct:
+      name: Foo
+      docs:
+        - ""
+        - []
+      lifetimes: {}
+      fields: []
+      methods:
+        - name: pub_fn
+          docs:
+            - ""
+            - []
+          abi_name: Foo_pub_fn
+          self_param: ~
+          params: []
+          return_type: ~
+          lifetime_env: {}
+          attrs: {}
+      output_only: false
+      attrs: {}
+declared_traits: {}
+sub_modules: []
+attrs: {}
diff --git a/crates/diplomat_core/src/ast/snapshots/diplomat_core__ast__modules__tests__simple_mod.snap b/crates/diplomat_core/src/ast/snapshots/diplomat_core__ast__modules__tests__simple_mod.snap
new file mode 100644
index 0000000..409136a
--- /dev/null
+++ b/crates/diplomat_core/src/ast/snapshots/diplomat_core__ast__modules__tests__simple_mod.snap
@@ -0,0 +1,130 @@
+---
+source: core/src/ast/modules.rs
+assertion_line: 326
+expression: "Module::from_syn(&syn::parse_quote! {\n                mod ffi\n                {\n                    struct NonOpaqueStruct { a: i32, b: Box<NonOpaqueStruct> }\n                    impl NonOpaqueStruct\n                    {\n                        pub fn new(x: i32) -> NonOpaqueStruct { unimplemented!(); }\n                        pub fn set_a(&mut self, new_a: i32) { self.a = new_a; }\n                    } #[diplomat::opaque] struct OpaqueStruct\n                    { a: SomeExternalType } impl OpaqueStruct\n                    {\n                        pub fn new() -> Box<OpaqueStruct> { unimplemented!(); } pub\n                        fn get_string(&self) -> String { unimplemented!() }\n                    }\n                }\n            }, true)"
+---
+name: ffi
+imports: []
+declared_types:
+  NonOpaqueStruct:
+    Struct:
+      name: NonOpaqueStruct
+      docs:
+        - ""
+        - []
+      lifetimes: {}
+      fields:
+        - - a
+          - Primitive: i32
+          - - ""
+            - []
+          - {}
+        - - b
+          - Box:
+              Named:
+                path:
+                  elements:
+                    - NonOpaqueStruct
+                lifetimes: []
+          - - ""
+            - []
+          - {}
+      methods:
+        - name: new
+          docs:
+            - ""
+            - []
+          abi_name: NonOpaqueStruct_new
+          self_param: ~
+          params:
+            - name: x
+              ty:
+                Primitive: i32
+              attrs: {}
+          return_type:
+            Named:
+              path:
+                elements:
+                  - NonOpaqueStruct
+              lifetimes: []
+          lifetime_env: {}
+          attrs: {}
+        - name: set_a
+          docs:
+            - ""
+            - []
+          abi_name: NonOpaqueStruct_set_a
+          self_param:
+            reference:
+              - Anonymous
+              - Mutable
+            path_type:
+              path:
+                elements:
+                  - NonOpaqueStruct
+              lifetimes: []
+            attrs: {}
+          params:
+            - name: new_a
+              ty:
+                Primitive: i32
+              attrs: {}
+          return_type: ~
+          lifetime_env: {}
+          attrs: {}
+      output_only: false
+      attrs: {}
+  OpaqueStruct:
+    Opaque:
+      name: OpaqueStruct
+      docs:
+        - ""
+        - []
+      lifetimes: {}
+      methods:
+        - name: new
+          docs:
+            - ""
+            - []
+          abi_name: OpaqueStruct_new
+          self_param: ~
+          params: []
+          return_type:
+            Box:
+              Named:
+                path:
+                  elements:
+                    - OpaqueStruct
+                lifetimes: []
+          lifetime_env: {}
+          attrs: {}
+        - name: get_string
+          docs:
+            - ""
+            - []
+          abi_name: OpaqueStruct_get_string
+          self_param:
+            reference:
+              - Anonymous
+              - Immutable
+            path_type:
+              path:
+                elements:
+                  - OpaqueStruct
+              lifetimes: []
+            attrs: {}
+          params: []
+          return_type:
+            Named:
+              path:
+                elements:
+                  - String
+              lifetimes: []
+          lifetime_env: {}
+          attrs: {}
+      mutability: Immutable
+      attrs: {}
+      dtor_abi_name: OpaqueStruct_destroy
+declared_traits: {}
+sub_modules: []
+attrs: {}
diff --git a/crates/diplomat_core/src/ast/snapshots/diplomat_core__ast__structs__tests__simple_struct.snap b/crates/diplomat_core/src/ast/snapshots/diplomat_core__ast__structs__tests__simple_struct.snap
new file mode 100644
index 0000000..e3f44b0
--- /dev/null
+++ b/crates/diplomat_core/src/ast/snapshots/diplomat_core__ast__structs__tests__simple_struct.snap
@@ -0,0 +1,33 @@
+---
+source: core/src/ast/structs.rs
+expression: "Struct::new(&syn::parse_quote! {\n                #[doc = r\" Some docs.\"]\n                #[diplomat::rust_link(foo::Bar, Struct)] struct MyLocalStruct\n                { a: i32, b: Box<MyLocalStruct> }\n            }, true, &Default::default())"
+---
+name: MyLocalStruct
+docs:
+  - Some docs.
+  - - path:
+        elements:
+          - foo
+          - Bar
+      typ: Struct
+      display: Normal
+lifetimes: {}
+fields:
+  - - a
+    - Primitive: i32
+    - - ""
+      - []
+    - {}
+  - - b
+    - Box:
+        Named:
+          path:
+            elements:
+              - MyLocalStruct
+          lifetimes: []
+    - - ""
+      - []
+    - {}
+methods: []
+output_only: true
+attrs: {}
diff --git a/crates/diplomat_core/src/ast/snapshots/diplomat_core__ast__types__tests__lifetimes-2.snap b/crates/diplomat_core/src/ast/snapshots/diplomat_core__ast__types__tests__lifetimes-2.snap
new file mode 100644
index 0000000..8a60d74
--- /dev/null
+++ b/crates/diplomat_core/src/ast/snapshots/diplomat_core__ast__types__tests__lifetimes-2.snap
@@ -0,0 +1,12 @@
+---
+source: core/src/ast/types.rs
+expression: "TypeName::from_syn(&syn::parse_quote! { :: core :: my_type :: Foo }, None)"
+---
+Named:
+  path:
+    elements:
+      - core
+      - my_type
+      - Foo
+  lifetimes: []
+
diff --git a/crates/diplomat_core/src/ast/snapshots/diplomat_core__ast__types__tests__lifetimes-3.snap b/crates/diplomat_core/src/ast/snapshots/diplomat_core__ast__types__tests__lifetimes-3.snap
new file mode 100644
index 0000000..8aabe3d
--- /dev/null
+++ b/crates/diplomat_core/src/ast/snapshots/diplomat_core__ast__types__tests__lifetimes-3.snap
@@ -0,0 +1,13 @@
+---
+source: core/src/ast/types.rs
+expression: "TypeName::from_syn(&syn::parse_quote! { :: core :: my_type :: Foo < 'test > },\n    None)"
+---
+Named:
+  path:
+    elements:
+      - core
+      - my_type
+      - Foo
+  lifetimes:
+    - Named: test
+
diff --git a/crates/diplomat_core/src/ast/snapshots/diplomat_core__ast__types__tests__lifetimes-4.snap b/crates/diplomat_core/src/ast/snapshots/diplomat_core__ast__types__tests__lifetimes-4.snap
new file mode 100644
index 0000000..1d7f0c5
--- /dev/null
+++ b/crates/diplomat_core/src/ast/snapshots/diplomat_core__ast__types__tests__lifetimes-4.snap
@@ -0,0 +1,12 @@
+---
+source: core/src/ast/types.rs
+expression: "TypeName::from_syn(&syn::parse_quote! { Option<Ref<'object>> }, None)"
+---
+Option:
+  - Named:
+      path:
+        elements:
+          - Ref
+      lifetimes:
+        - Named: object
+  - Stdlib
diff --git a/crates/diplomat_core/src/ast/snapshots/diplomat_core__ast__types__tests__lifetimes-5.snap b/crates/diplomat_core/src/ast/snapshots/diplomat_core__ast__types__tests__lifetimes-5.snap
new file mode 100644
index 0000000..8c8ed1c
--- /dev/null
+++ b/crates/diplomat_core/src/ast/snapshots/diplomat_core__ast__types__tests__lifetimes-5.snap
@@ -0,0 +1,14 @@
+---
+source: core/src/ast/types.rs
+expression: "TypeName::from_syn(&syn::parse_quote! { Foo < 'a, 'b, 'c, 'd > }, None)"
+---
+Named:
+  path:
+    elements:
+      - Foo
+  lifetimes:
+    - Named: a
+    - Named: b
+    - Named: c
+    - Named: d
+
diff --git a/crates/diplomat_core/src/ast/snapshots/diplomat_core__ast__types__tests__lifetimes-6.snap b/crates/diplomat_core/src/ast/snapshots/diplomat_core__ast__types__tests__lifetimes-6.snap
new file mode 100644
index 0000000..fbbae27
--- /dev/null
+++ b/crates/diplomat_core/src/ast/snapshots/diplomat_core__ast__types__tests__lifetimes-6.snap
@@ -0,0 +1,18 @@
+---
+source: core/src/ast/types.rs
+expression: "TypeName::from_syn(&syn::parse_quote! {\n                very :: long :: path :: to :: my :: Type < 'x, 'y, 'z >\n            }, None)"
+---
+Named:
+  path:
+    elements:
+      - very
+      - long
+      - path
+      - to
+      - my
+      - Type
+  lifetimes:
+    - Named: x
+    - Named: y
+    - Named: z
+
diff --git a/crates/diplomat_core/src/ast/snapshots/diplomat_core__ast__types__tests__lifetimes-7.snap b/crates/diplomat_core/src/ast/snapshots/diplomat_core__ast__types__tests__lifetimes-7.snap
new file mode 100644
index 0000000..6bb2a47
--- /dev/null
+++ b/crates/diplomat_core/src/ast/snapshots/diplomat_core__ast__types__tests__lifetimes-7.snap
@@ -0,0 +1,19 @@
+---
+source: core/src/ast/types.rs
+expression: "TypeName::from_syn(&syn::parse_quote! { Result<OkRef<'a, 'b>, ErrRef<'c>> },\n    None)"
+---
+Result:
+  - Named:
+      path:
+        elements:
+          - OkRef
+      lifetimes:
+        - Named: a
+        - Named: b
+  - Named:
+      path:
+        elements:
+          - ErrRef
+      lifetimes:
+        - Named: c
+  - Stdlib
diff --git a/crates/diplomat_core/src/ast/snapshots/diplomat_core__ast__types__tests__lifetimes.snap b/crates/diplomat_core/src/ast/snapshots/diplomat_core__ast__types__tests__lifetimes.snap
new file mode 100644
index 0000000..41047ac
--- /dev/null
+++ b/crates/diplomat_core/src/ast/snapshots/diplomat_core__ast__types__tests__lifetimes.snap
@@ -0,0 +1,12 @@
+---
+source: core/src/ast/types.rs
+expression: "TypeName::from_syn(&syn::parse_quote! { Foo < 'a, 'b > }, None)"
+---
+Named:
+  path:
+    elements:
+      - Foo
+  lifetimes:
+    - Named: a
+    - Named: b
+
diff --git a/crates/diplomat_core/src/ast/snapshots/diplomat_core__ast__types__tests__typename_boxes-2.snap b/crates/diplomat_core/src/ast/snapshots/diplomat_core__ast__types__tests__typename_boxes-2.snap
new file mode 100644
index 0000000..18a72a2
--- /dev/null
+++ b/crates/diplomat_core/src/ast/snapshots/diplomat_core__ast__types__tests__typename_boxes-2.snap
@@ -0,0 +1,11 @@
+---
+source: core/src/ast/types.rs
+expression: "TypeName::from_syn(&syn::parse_quote! { Box < MyLocalStruct > }, None)"
+---
+Box:
+  Named:
+    path:
+      elements:
+        - MyLocalStruct
+    lifetimes: []
+
diff --git a/crates/diplomat_core/src/ast/snapshots/diplomat_core__ast__types__tests__typename_boxes.snap b/crates/diplomat_core/src/ast/snapshots/diplomat_core__ast__types__tests__typename_boxes.snap
new file mode 100644
index 0000000..30d9ae3
--- /dev/null
+++ b/crates/diplomat_core/src/ast/snapshots/diplomat_core__ast__types__tests__typename_boxes.snap
@@ -0,0 +1,7 @@
+---
+source: core/src/ast/types.rs
+expression: "TypeName::from_syn(&syn::parse_quote! { Box < i32 > }, None)"
+---
+Box:
+  Primitive: i32
+
diff --git a/crates/diplomat_core/src/ast/snapshots/diplomat_core__ast__types__tests__typename_named.snap b/crates/diplomat_core/src/ast/snapshots/diplomat_core__ast__types__tests__typename_named.snap
new file mode 100644
index 0000000..f7763c5
--- /dev/null
+++ b/crates/diplomat_core/src/ast/snapshots/diplomat_core__ast__types__tests__typename_named.snap
@@ -0,0 +1,10 @@
+---
+source: core/src/ast/types.rs
+expression: "TypeName::from_syn(&syn::parse_quote! { MyLocalStruct }, None)"
+---
+Named:
+  path:
+    elements:
+      - MyLocalStruct
+  lifetimes: []
+
diff --git a/crates/diplomat_core/src/ast/snapshots/diplomat_core__ast__types__tests__typename_option-2.snap b/crates/diplomat_core/src/ast/snapshots/diplomat_core__ast__types__tests__typename_option-2.snap
new file mode 100644
index 0000000..b1136d0
--- /dev/null
+++ b/crates/diplomat_core/src/ast/snapshots/diplomat_core__ast__types__tests__typename_option-2.snap
@@ -0,0 +1,11 @@
+---
+source: core/src/ast/types.rs
+expression: "TypeName::from_syn(&syn::parse_quote! { Option<MyLocalStruct> }, None)"
+---
+Option:
+  - Named:
+      path:
+        elements:
+          - MyLocalStruct
+      lifetimes: []
+  - Stdlib
diff --git a/crates/diplomat_core/src/ast/snapshots/diplomat_core__ast__types__tests__typename_option.snap b/crates/diplomat_core/src/ast/snapshots/diplomat_core__ast__types__tests__typename_option.snap
new file mode 100644
index 0000000..0cfd954
--- /dev/null
+++ b/crates/diplomat_core/src/ast/snapshots/diplomat_core__ast__types__tests__typename_option.snap
@@ -0,0 +1,7 @@
+---
+source: core/src/ast/types.rs
+expression: "TypeName::from_syn(&syn::parse_quote! { Option<i32> }, None)"
+---
+Option:
+  - Primitive: i32
+  - Stdlib
diff --git a/crates/diplomat_core/src/ast/snapshots/diplomat_core__ast__types__tests__typename_primitives-2.snap b/crates/diplomat_core/src/ast/snapshots/diplomat_core__ast__types__tests__typename_primitives-2.snap
new file mode 100644
index 0000000..3887727
--- /dev/null
+++ b/crates/diplomat_core/src/ast/snapshots/diplomat_core__ast__types__tests__typename_primitives-2.snap
@@ -0,0 +1,6 @@
+---
+source: core/src/ast/types.rs
+expression: "TypeName::from_syn(&syn::parse_quote! { usize }, None)"
+---
+Primitive: usize
+
diff --git a/crates/diplomat_core/src/ast/snapshots/diplomat_core__ast__types__tests__typename_primitives-3.snap b/crates/diplomat_core/src/ast/snapshots/diplomat_core__ast__types__tests__typename_primitives-3.snap
new file mode 100644
index 0000000..2fac7f4
--- /dev/null
+++ b/crates/diplomat_core/src/ast/snapshots/diplomat_core__ast__types__tests__typename_primitives-3.snap
@@ -0,0 +1,6 @@
+---
+source: core/src/ast/types.rs
+expression: "TypeName::from_syn(&syn::parse_quote! { bool }, None)"
+---
+Primitive: bool
+
diff --git a/crates/diplomat_core/src/ast/snapshots/diplomat_core__ast__types__tests__typename_primitives.snap b/crates/diplomat_core/src/ast/snapshots/diplomat_core__ast__types__tests__typename_primitives.snap
new file mode 100644
index 0000000..5ef0eaf
--- /dev/null
+++ b/crates/diplomat_core/src/ast/snapshots/diplomat_core__ast__types__tests__typename_primitives.snap
@@ -0,0 +1,6 @@
+---
+source: core/src/ast/types.rs
+expression: "TypeName::from_syn(&syn::parse_quote! { i32 }, None)"
+---
+Primitive: i32
+
diff --git a/crates/diplomat_core/src/ast/snapshots/diplomat_core__ast__types__tests__typename_references-2.snap b/crates/diplomat_core/src/ast/snapshots/diplomat_core__ast__types__tests__typename_references-2.snap
new file mode 100644
index 0000000..396affe
--- /dev/null
+++ b/crates/diplomat_core/src/ast/snapshots/diplomat_core__ast__types__tests__typename_references-2.snap
@@ -0,0 +1,13 @@
+---
+source: core/src/ast/types.rs
+expression: "TypeName::from_syn(&syn::parse_quote! { & mut MyLocalStruct }, None)"
+---
+Reference:
+  - Anonymous
+  - Mutable
+  - Named:
+      path:
+        elements:
+          - MyLocalStruct
+      lifetimes: []
+
diff --git a/crates/diplomat_core/src/ast/snapshots/diplomat_core__ast__types__tests__typename_references.snap b/crates/diplomat_core/src/ast/snapshots/diplomat_core__ast__types__tests__typename_references.snap
new file mode 100644
index 0000000..38c72a4
--- /dev/null
+++ b/crates/diplomat_core/src/ast/snapshots/diplomat_core__ast__types__tests__typename_references.snap
@@ -0,0 +1,9 @@
+---
+source: core/src/ast/types.rs
+expression: "TypeName::from_syn(&syn::parse_quote! { & i32 }, None)"
+---
+Reference:
+  - Anonymous
+  - Immutable
+  - Primitive: i32
+
diff --git a/crates/diplomat_core/src/ast/snapshots/diplomat_core__ast__types__tests__typename_result-2.snap b/crates/diplomat_core/src/ast/snapshots/diplomat_core__ast__types__tests__typename_result-2.snap
new file mode 100644
index 0000000..a3c187a
--- /dev/null
+++ b/crates/diplomat_core/src/ast/snapshots/diplomat_core__ast__types__tests__typename_result-2.snap
@@ -0,0 +1,12 @@
+---
+source: core/src/ast/types.rs
+expression: "TypeName::from_syn(&syn::parse_quote! { DiplomatResult<(), MyLocalStruct> },\n    None)"
+---
+Result:
+  - Unit
+  - Named:
+      path:
+        elements:
+          - MyLocalStruct
+      lifetimes: []
+  - Diplomat
diff --git a/crates/diplomat_core/src/ast/snapshots/diplomat_core__ast__types__tests__typename_result-3.snap b/crates/diplomat_core/src/ast/snapshots/diplomat_core__ast__types__tests__typename_result-3.snap
new file mode 100644
index 0000000..e83fdda
--- /dev/null
+++ b/crates/diplomat_core/src/ast/snapshots/diplomat_core__ast__types__tests__typename_result-3.snap
@@ -0,0 +1,12 @@
+---
+source: core/src/ast/types.rs
+expression: "TypeName::from_syn(&syn::parse_quote! { Result<MyLocalStruct, i32> }, None)"
+---
+Result:
+  - Named:
+      path:
+        elements:
+          - MyLocalStruct
+      lifetimes: []
+  - Primitive: i32
+  - Stdlib
diff --git a/crates/diplomat_core/src/ast/snapshots/diplomat_core__ast__types__tests__typename_result-4.snap b/crates/diplomat_core/src/ast/snapshots/diplomat_core__ast__types__tests__typename_result-4.snap
new file mode 100644
index 0000000..54ddc98
--- /dev/null
+++ b/crates/diplomat_core/src/ast/snapshots/diplomat_core__ast__types__tests__typename_result-4.snap
@@ -0,0 +1,12 @@
+---
+source: core/src/ast/types.rs
+expression: "TypeName::from_syn(&syn::parse_quote! { Result<(), MyLocalStruct> }, None)"
+---
+Result:
+  - Unit
+  - Named:
+      path:
+        elements:
+          - MyLocalStruct
+      lifetimes: []
+  - Stdlib
diff --git a/crates/diplomat_core/src/ast/snapshots/diplomat_core__ast__types__tests__typename_result.snap b/crates/diplomat_core/src/ast/snapshots/diplomat_core__ast__types__tests__typename_result.snap
new file mode 100644
index 0000000..dc28b42
--- /dev/null
+++ b/crates/diplomat_core/src/ast/snapshots/diplomat_core__ast__types__tests__typename_result.snap
@@ -0,0 +1,12 @@
+---
+source: core/src/ast/types.rs
+expression: "TypeName::from_syn(&syn::parse_quote! { DiplomatResult<MyLocalStruct, i32> },\n    None)"
+---
+Result:
+  - Named:
+      path:
+        elements:
+          - MyLocalStruct
+      lifetimes: []
+  - Primitive: i32
+  - Diplomat
diff --git a/crates/diplomat_core/src/ast/snapshots/diplomat_core__ast__validity__tests__lifetime_in_return.snap b/crates/diplomat_core/src/ast/snapshots/diplomat_core__ast__validity__tests__lifetime_in_return.snap
new file mode 100644
index 0000000..8a00617
--- /dev/null
+++ b/crates/diplomat_core/src/ast/snapshots/diplomat_core__ast__validity__tests__lifetime_in_return.snap
@@ -0,0 +1,7 @@
+---
+source: core/src/ast/validity.rs
+expression: output
+---
+A return type contains elided lifetimes, which aren't yet supported: &Opaque in &Opaque
+A return type contains elided lifetimes, which aren't yet supported: Foo in Foo
+
diff --git a/crates/diplomat_core/src/ast/structs.rs b/crates/diplomat_core/src/ast/structs.rs
new file mode 100644
index 0000000..ab0681a
--- /dev/null
+++ b/crates/diplomat_core/src/ast/structs.rs
@@ -0,0 +1,120 @@
+use serde::Serialize;
+
+use super::docs::Docs;
+use super::{Attrs, Ident, LifetimeEnv, Method, Mutability, PathType, TypeName};
+
+/// A struct declaration in an FFI module that is not opaque.
+#[derive(Clone, PartialEq, Eq, Hash, Serialize, Debug)]
+#[non_exhaustive]
+pub struct Struct {
+    pub name: Ident,
+    pub docs: Docs,
+    pub lifetimes: LifetimeEnv,
+    pub fields: Vec<(Ident, TypeName, Docs, Attrs)>,
+    pub methods: Vec<Method>,
+    pub output_only: bool,
+    pub attrs: Attrs,
+}
+
+impl Struct {
+    /// Extract a [`Struct`] metadata value from an AST node.
+    pub fn new(strct: &syn::ItemStruct, output_only: bool, parent_attrs: &Attrs) -> Self {
+        let self_path_type = PathType::extract_self_type(strct);
+        let fields: Vec<_> = strct
+            .fields
+            .iter()
+            .map(|field| {
+                // Non-opaque tuple structs will never be allowed
+                let name = field
+                    .ident
+                    .as_ref()
+                    .map(Into::into)
+                    .expect("non-opaque tuples structs are disallowed");
+                let type_name = TypeName::from_syn(&field.ty, Some(self_path_type.clone()));
+                let docs = Docs::from_attrs(&field.attrs);
+
+                (name, type_name, docs, Attrs::from_attrs(&field.attrs))
+            })
+            .collect();
+
+        let lifetimes = LifetimeEnv::from_struct_item(strct, &fields[..]);
+        let mut attrs = parent_attrs.clone();
+        attrs.add_attrs(&strct.attrs);
+        Struct {
+            name: (&strct.ident).into(),
+            docs: Docs::from_attrs(&strct.attrs),
+            lifetimes,
+            fields,
+            methods: vec![],
+            output_only,
+            attrs,
+        }
+    }
+}
+
+/// A struct annotated with [`diplomat::opaque`] whose fields are not visible.
+/// Opaque structs cannot be passed by-value across the FFI boundary, so they
+/// must be boxed or passed as references.
+#[derive(Clone, Serialize, Debug, Hash, PartialEq, Eq)]
+#[non_exhaustive]
+pub struct OpaqueStruct {
+    pub name: Ident,
+    pub docs: Docs,
+    pub lifetimes: LifetimeEnv,
+    pub methods: Vec<Method>,
+    pub mutability: Mutability,
+    pub attrs: Attrs,
+    /// The ABI name of the generated destructor
+    pub dtor_abi_name: Ident,
+}
+
+impl OpaqueStruct {
+    /// Extract a [`OpaqueStruct`] metadata value from an AST node.
+    pub fn new(strct: &syn::ItemStruct, mutability: Mutability, parent_attrs: &Attrs) -> Self {
+        let mut attrs = parent_attrs.clone();
+        attrs.add_attrs(&strct.attrs);
+        let name = Ident::from(&strct.ident);
+        let dtor_abi_name = format!("{}_destroy", name);
+        let dtor_abi_name = String::from(attrs.abi_rename.apply(dtor_abi_name.into()));
+        let dtor_abi_name = Ident::from(dtor_abi_name);
+        OpaqueStruct {
+            name,
+            docs: Docs::from_attrs(&strct.attrs),
+            lifetimes: LifetimeEnv::from_struct_item(strct, &[]),
+            methods: vec![],
+            mutability,
+            attrs,
+            dtor_abi_name,
+        }
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use insta::{self, Settings};
+
+    use syn;
+
+    use super::Struct;
+
+    #[test]
+    fn simple_struct() {
+        let mut settings = Settings::new();
+        settings.set_sort_maps(true);
+
+        settings.bind(|| {
+            insta::assert_yaml_snapshot!(Struct::new(
+                &syn::parse_quote! {
+                    /// Some docs.
+                    #[diplomat::rust_link(foo::Bar, Struct)]
+                    struct MyLocalStruct {
+                        a: i32,
+                        b: Box<MyLocalStruct>
+                    }
+                },
+                true,
+                &Default::default()
+            ));
+        });
+    }
+}
diff --git a/crates/diplomat_core/src/ast/traits.rs b/crates/diplomat_core/src/ast/traits.rs
new file mode 100644
index 0000000..4b7ef09
--- /dev/null
+++ b/crates/diplomat_core/src/ast/traits.rs
@@ -0,0 +1,117 @@
+use serde::Serialize;
+
+use super::docs::Docs;
+use super::{Attrs, Ident, LifetimeEnv, Param, PathType, TraitSelfParam, TypeName};
+
+/// A trait declaration in an FFI module.
+#[derive(Clone, PartialEq, Eq, Hash, Serialize, Debug)]
+#[non_exhaustive]
+pub struct Trait {
+    pub name: Ident,
+    pub lifetimes: LifetimeEnv,
+    pub methods: Vec<TraitMethod>,
+    pub docs: Docs,
+    pub attrs: Attrs,
+}
+
+#[derive(Clone, PartialEq, Eq, Hash, Serialize, Debug)]
+#[non_exhaustive]
+pub struct TraitMethod {
+    pub name: Ident,
+    pub abi_name: Ident,
+    pub self_param: Option<TraitSelfParam>,
+    // corresponds to the types in Function(Vec<Box<TypeName>>, Box<TypeName>)
+    // the callback type; except here the params aren't anonymous
+    pub params: Vec<Param>,
+    pub output_type: Option<TypeName>,
+    pub lifetimes: LifetimeEnv,
+    pub attrs: Attrs,
+    pub docs: Docs,
+}
+
+impl Trait {
+    /// Extract a [`Trait`] metadata value from an AST node.
+    pub fn new(trt: &syn::ItemTrait, parent_attrs: &Attrs) -> Self {
+        let mut attrs = parent_attrs.clone();
+        attrs.add_attrs(&trt.attrs);
+
+        let mut trait_fcts = Vec::new();
+
+        let self_ident = &trt.ident;
+        // TODO check this
+        let self_path_trait = PathType::from(&syn::TraitBound {
+            paren_token: None,
+            modifier: syn::TraitBoundModifier::None,
+            lifetimes: None, // todo this is an assumption
+            path: syn::PathSegment {
+                ident: self_ident.clone(),
+                arguments: syn::PathArguments::None,
+            }
+            .into(),
+        });
+        for trait_item in trt.items.iter() {
+            if let syn::TraitItem::Fn(fct) = trait_item {
+                let mut fct_attrs = attrs.clone();
+                fct_attrs.add_attrs(&fct.attrs);
+                // copied from the method parsing
+                let fct_ident = &fct.sig.ident;
+                let concat_fct_ident = format!("{self_ident}_{fct_ident}");
+                let extern_ident = syn::Ident::new(
+                    &attrs.abi_rename.apply(concat_fct_ident.into()),
+                    fct.sig.ident.span(),
+                );
+
+                let all_params = fct
+                    .sig
+                    .inputs
+                    .iter()
+                    .filter_map(|a| match a {
+                        syn::FnArg::Receiver(_) => None,
+                        syn::FnArg::Typed(ref t) => {
+                            Some(Param::from_syn(t, self_path_trait.clone()))
+                        }
+                    })
+                    .collect::<Vec<_>>();
+
+                let self_param = fct
+                    .sig
+                    .receiver()
+                    .map(|rec| TraitSelfParam::from_syn(rec, self_path_trait.clone()));
+
+                let output_type = match &fct.sig.output {
+                    syn::ReturnType::Type(_, return_typ) => Some(TypeName::from_syn(
+                        return_typ.as_ref(),
+                        Some(self_path_trait.clone()),
+                    )),
+                    syn::ReturnType::Default => None,
+                };
+
+                let lifetimes = LifetimeEnv::from_trait_item(
+                    trait_item,
+                    self_param.as_ref(),
+                    &all_params[..],
+                    output_type.as_ref(),
+                );
+
+                trait_fcts.push(TraitMethod {
+                    name: fct_ident.into(),
+                    abi_name: (&extern_ident).into(),
+                    self_param,
+                    params: all_params,
+                    output_type,
+                    lifetimes,
+                    attrs: fct_attrs,
+                    docs: Docs::from_attrs(&fct.attrs),
+                });
+            }
+        }
+
+        Self {
+            name: (&trt.ident).into(),
+            methods: trait_fcts,
+            docs: Docs::from_attrs(&trt.attrs),
+            lifetimes: LifetimeEnv::from_trait(trt), // TODO
+            attrs,
+        }
+    }
+}
diff --git a/crates/diplomat_core/src/ast/types.rs b/crates/diplomat_core/src/ast/types.rs
new file mode 100644
index 0000000..72594ac
--- /dev/null
+++ b/crates/diplomat_core/src/ast/types.rs
@@ -0,0 +1,1658 @@
+use proc_macro2::Span;
+use quote::{ToTokens, TokenStreamExt};
+use serde::{Deserialize, Serialize};
+use syn::Token;
+
+use std::fmt;
+use std::ops::ControlFlow;
+use std::str::FromStr;
+
+use super::{
+    Attrs, Docs, Enum, Ident, Lifetime, LifetimeEnv, LifetimeTransitivity, Method, NamedLifetime,
+    OpaqueStruct, Path, RustLink, Struct, Trait,
+};
+use crate::Env;
+
+/// A type declared inside a Diplomat-annotated module.
+#[derive(Clone, Serialize, Debug, Hash, PartialEq, Eq)]
+#[non_exhaustive]
+pub enum CustomType {
+    /// A non-opaque struct whose fields will be visible across the FFI boundary.
+    Struct(Struct),
+    /// A struct annotated with [`diplomat::opaque`] whose fields are not visible.
+    Opaque(OpaqueStruct),
+    /// A fieldless enum.
+    Enum(Enum),
+}
+
+impl CustomType {
+    /// Get the name of the custom type, which is unique within a module.
+    pub fn name(&self) -> &Ident {
+        match self {
+            CustomType::Struct(strct) => &strct.name,
+            CustomType::Opaque(strct) => &strct.name,
+            CustomType::Enum(enm) => &enm.name,
+        }
+    }
+
+    /// Get the methods declared in impls of the custom type.
+    pub fn methods(&self) -> &Vec<Method> {
+        match self {
+            CustomType::Struct(strct) => &strct.methods,
+            CustomType::Opaque(strct) => &strct.methods,
+            CustomType::Enum(enm) => &enm.methods,
+        }
+    }
+
+    pub fn attrs(&self) -> &Attrs {
+        match self {
+            CustomType::Struct(strct) => &strct.attrs,
+            CustomType::Opaque(strct) => &strct.attrs,
+            CustomType::Enum(enm) => &enm.attrs,
+        }
+    }
+
+    /// Get the doc lines of the custom type.
+    pub fn docs(&self) -> &Docs {
+        match self {
+            CustomType::Struct(strct) => &strct.docs,
+            CustomType::Opaque(strct) => &strct.docs,
+            CustomType::Enum(enm) => &enm.docs,
+        }
+    }
+
+    /// Get all rust links on this type and its methods
+    pub fn all_rust_links(&self) -> impl Iterator<Item = &RustLink> + '_ {
+        [self.docs()]
+            .into_iter()
+            .chain(self.methods().iter().map(|m| m.docs()))
+            .flat_map(|d| d.rust_links().iter())
+    }
+
+    pub fn self_path(&self, in_path: &Path) -> Path {
+        in_path.sub_path(self.name().clone())
+    }
+
+    /// Get the lifetimes of the custom type.
+    pub fn lifetimes(&self) -> Option<&LifetimeEnv> {
+        match self {
+            CustomType::Struct(strct) => Some(&strct.lifetimes),
+            CustomType::Opaque(strct) => Some(&strct.lifetimes),
+            CustomType::Enum(_) => None,
+        }
+    }
+}
+
+/// A symbol declared in a module, which can either be a pointer to another path,
+/// or a custom type defined directly inside that module
+#[derive(Clone, Serialize, Debug)]
+#[non_exhaustive]
+pub enum ModSymbol {
+    /// A symbol that is a pointer to another path.
+    Alias(Path),
+    /// A symbol that is a submodule.
+    SubModule(Ident),
+    /// A symbol that is a custom type.
+    CustomType(CustomType),
+    /// A trait
+    Trait(Trait),
+}
+
+/// A named type that is just a path, e.g. `std::borrow::Cow<'a, T>`.
+#[derive(Clone, PartialEq, Eq, Hash, Serialize, Deserialize, Debug)]
+#[non_exhaustive]
+pub struct PathType {
+    pub path: Path,
+    pub lifetimes: Vec<Lifetime>,
+}
+
+impl PathType {
+    pub fn to_syn(&self) -> syn::TypePath {
+        let mut path = self.path.to_syn();
+
+        if !self.lifetimes.is_empty() {
+            if let Some(seg) = path.segments.last_mut() {
+                let lifetimes = &self.lifetimes;
+                seg.arguments =
+                    syn::PathArguments::AngleBracketed(syn::parse_quote! { <#(#lifetimes),*> });
+            }
+        }
+
+        syn::TypePath { qself: None, path }
+    }
+
+    pub fn new(path: Path) -> Self {
+        Self {
+            path,
+            lifetimes: vec![],
+        }
+    }
+
+    /// Get the `Self` type from a struct declaration.
+    ///
+    /// Consider the following struct declaration:
+    /// ```
+    /// struct RefList<'a> {
+    ///     data: &'a i32,
+    ///     next: Option<Box<Self>>,
+    /// }
+    /// ```
+    /// When determining what type `Self` is in the `next` field, we would have to call
+    /// this method on the `syn::ItemStruct` that represents this struct declaration.
+    /// This method would then return a `PathType` representing `RefList<'a>`, so we
+    /// know that's what `Self` should refer to.
+    ///
+    /// The reason this function exists though is so when we convert the fields' types
+    /// to `PathType`s, we don't panic. We don't actually need to write the struct's
+    /// field types expanded in the macro, so this function is more for correctness,
+    pub fn extract_self_type(strct: &syn::ItemStruct) -> Self {
+        let self_name = (&strct.ident).into();
+
+        PathType {
+            path: Path {
+                elements: vec![self_name],
+            },
+            lifetimes: strct
+                .generics
+                .lifetimes()
+                .map(|lt_def| (&lt_def.lifetime).into())
+                .collect(),
+        }
+    }
+
+    /// If this is a [`TypeName::Named`], grab the [`CustomType`] it points to from
+    /// the `env`, which contains all [`CustomType`]s across all FFI modules.
+    ///
+    /// Also returns the path the CustomType is in (useful for resolving fields)
+    pub fn resolve_with_path<'a>(&self, in_path: &Path, env: &'a Env) -> (Path, &'a CustomType) {
+        let local_path = &self.path;
+        let mut cur_path = in_path.clone();
+        for (i, elem) in local_path.elements.iter().enumerate() {
+            match elem.as_str() {
+                "crate" => {
+                    // TODO(#34): get the name of enclosing crate from env when we support multiple crates
+                    cur_path = Path::empty()
+                }
+
+                "super" => cur_path = cur_path.get_super(),
+
+                o => match env.get(&cur_path, o) {
+                    Some(ModSymbol::Alias(p)) => {
+                        let mut remaining_elements: Vec<Ident> =
+                            local_path.elements.iter().skip(i + 1).cloned().collect();
+                        let mut new_path = p.elements.clone();
+                        new_path.append(&mut remaining_elements);
+                        return PathType::new(Path { elements: new_path })
+                            .resolve_with_path(&cur_path.clone(), env);
+                    }
+                    Some(ModSymbol::SubModule(name)) => {
+                        cur_path.elements.push(name.clone());
+                    }
+                    Some(ModSymbol::CustomType(t)) => {
+                        if i == local_path.elements.len() - 1 {
+                            return (cur_path, t);
+                        } else {
+                            panic!(
+                                "Unexpected custom type when resolving symbol {} in {}",
+                                o,
+                                cur_path.elements.join("::")
+                            )
+                        }
+                    }
+                    Some(ModSymbol::Trait(trt)) => {
+                        panic!("Found trait {} but expected a type", trt.name);
+                    }
+                    None => panic!(
+                        "Could not resolve symbol {} in {}",
+                        o,
+                        cur_path.elements.join("::")
+                    ),
+                },
+            }
+        }
+
+        panic!(
+            "Path {} does not point to a custom type",
+            in_path.elements.join("::")
+        )
+    }
+
+    /// If this is a [`TypeName::Named`], grab the [`CustomType`] it points to from
+    /// the `env`, which contains all [`CustomType`]s across all FFI modules.
+    ///
+    /// If you need to resolve struct fields later, call [`Self::resolve_with_path()`] instead
+    /// to get the path to resolve the fields in.
+    pub fn resolve<'a>(&self, in_path: &Path, env: &'a Env) -> &'a CustomType {
+        self.resolve_with_path(in_path, env).1
+    }
+
+    pub fn trait_to_syn(&self) -> syn::TraitBound {
+        let mut path = self.path.to_syn();
+
+        if !self.lifetimes.is_empty() {
+            if let Some(seg) = path.segments.last_mut() {
+                let lifetimes = &self.lifetimes;
+                seg.arguments =
+                    syn::PathArguments::AngleBracketed(syn::parse_quote! { <#(#lifetimes),*> });
+            }
+        }
+        syn::TraitBound {
+            paren_token: None,
+            modifier: syn::TraitBoundModifier::None,
+            lifetimes: None, // todo this is an assumption
+            path,
+        }
+    }
+
+    pub fn resolve_trait_with_path<'a>(&self, in_path: &Path, env: &'a Env) -> (Path, Trait) {
+        let local_path = &self.path;
+        let cur_path = in_path.clone();
+        for (i, elem) in local_path.elements.iter().enumerate() {
+            if let Some(ModSymbol::Trait(trt)) = env.get(&cur_path, elem.as_str()) {
+                if i == local_path.elements.len() - 1 {
+                    return (cur_path, trt.clone());
+                } else {
+                    panic!(
+                        "Unexpected custom trait when resolving symbol {} in {}",
+                        trt.name,
+                        cur_path.elements.join("::")
+                    )
+                }
+            }
+        }
+
+        panic!(
+            "Path {} does not point to a custom trait",
+            in_path.elements.join("::")
+        )
+    }
+
+    /// If this is a [`TypeName::Named`], grab the [`CustomType`] it points to from
+    /// the `env`, which contains all [`CustomType`]s across all FFI modules.
+    ///
+    /// If you need to resolve struct fields later, call [`Self::resolve_with_path()`] instead
+    /// to get the path to resolve the fields in.
+    pub fn resolve_trait<'a>(&self, in_path: &Path, env: &'a Env) -> Trait {
+        self.resolve_trait_with_path(in_path, env).1
+    }
+}
+
+impl From<&syn::TypePath> for PathType {
+    fn from(other: &syn::TypePath) -> Self {
+        let lifetimes = other
+            .path
+            .segments
+            .last()
+            .and_then(|last| {
+                if let syn::PathArguments::AngleBracketed(angle_generics) = &last.arguments {
+                    Some(
+                        angle_generics
+                            .args
+                            .iter()
+                            .map(|generic_arg| match generic_arg {
+                                syn::GenericArgument::Lifetime(lifetime) => lifetime.into(),
+                                _ => panic!("generic type arguments are unsupported {other:?}"),
+                            })
+                            .collect(),
+                    )
+                } else {
+                    None
+                }
+            })
+            .unwrap_or_default();
+
+        Self {
+            path: Path::from_syn(&other.path),
+            lifetimes,
+        }
+    }
+}
+
+impl From<&syn::TraitBound> for PathType {
+    fn from(other: &syn::TraitBound) -> Self {
+        let lifetimes = other
+            .path
+            .segments
+            .last()
+            .and_then(|last| {
+                if let syn::PathArguments::AngleBracketed(angle_generics) = &last.arguments {
+                    Some(
+                        angle_generics
+                            .args
+                            .iter()
+                            .map(|generic_arg| match generic_arg {
+                                syn::GenericArgument::Lifetime(lifetime) => lifetime.into(),
+                                _ => panic!("generic type arguments are unsupported {other:?}"),
+                            })
+                            .collect(),
+                    )
+                } else {
+                    None
+                }
+            })
+            .unwrap_or_default();
+
+        Self {
+            path: Path::from_syn(&other.path),
+            lifetimes,
+        }
+    }
+}
+
+impl From<Path> for PathType {
+    fn from(other: Path) -> Self {
+        PathType::new(other)
+    }
+}
+
+#[derive(Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, Debug)]
+#[allow(clippy::exhaustive_enums)] // there are only two kinds of mutability we care about
+pub enum Mutability {
+    Mutable,
+    Immutable,
+}
+
+impl Mutability {
+    pub fn to_syn(&self) -> Option<Token![mut]> {
+        match self {
+            Mutability::Mutable => Some(syn::token::Mut(Span::call_site())),
+            Mutability::Immutable => None,
+        }
+    }
+
+    pub fn from_syn(t: &Option<Token![mut]>) -> Self {
+        match t {
+            Some(_) => Mutability::Mutable,
+            None => Mutability::Immutable,
+        }
+    }
+
+    /// Returns `true` if `&self` is the mutable variant, otherwise `false`.
+    pub fn is_mutable(&self) -> bool {
+        matches!(self, Mutability::Mutable)
+    }
+
+    /// Returns `true` if `&self` is the immutable variant, otherwise `false`.
+    pub fn is_immutable(&self) -> bool {
+        matches!(self, Mutability::Immutable)
+    }
+
+    /// Shorthand ternary operator for choosing a value based on whether
+    /// a `Mutability` is mutable or immutable.
+    ///
+    /// The following pattern (with very slight variations) shows up often in code gen:
+    /// ```ignore
+    /// if mutability.is_mutable() {
+    ///     ""
+    /// } else {
+    ///     "const "
+    /// }
+    /// ```
+    /// This is particularly annoying in `write!(...)` statements, where `cargo fmt`
+    /// expands it to take up 5 lines.
+    ///
+    /// This method offers a 1-line alternative:
+    /// ```ignore
+    /// mutability.if_mut_else("", "const ")
+    /// ```
+    /// For cases where lazy evaluation is desired, consider using a conditional
+    /// or a `match` statement.
+    pub fn if_mut_else<T>(&self, if_mut: T, if_immut: T) -> T {
+        match self {
+            Mutability::Mutable => if_mut,
+            Mutability::Immutable => if_immut,
+        }
+    }
+}
+
+/// For types like `Result`/`DiplomatResult`, `&[T]`/`DiplomatSlice<T>` which can be
+/// specified using (non-ffi-safe) Rust stdlib types, or FFI-safe `repr(C)` types from
+/// `diplomat_runtime`, this tracks which of the two were used.
+#[derive(Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, Debug)]
+#[allow(clippy::exhaustive_enums)] // This can only have two values
+pub enum StdlibOrDiplomat {
+    Stdlib,
+    Diplomat,
+}
+
+/// A local type reference, such as the type of a field, parameter, or return value.
+/// Unlike [`CustomType`], which represents a type declaration, [`TypeName`]s can compose
+/// types through references and boxing, and can also capture unresolved paths.
+#[derive(Clone, PartialEq, Eq, Hash, Serialize, Deserialize, Debug)]
+#[non_exhaustive]
+pub enum TypeName {
+    /// A built-in Rust scalar primitive.
+    Primitive(PrimitiveType),
+    /// An unresolved path to a custom type, which can be resolved after all types
+    /// are collected with [`TypeName::resolve()`].
+    Named(PathType),
+    /// An optionally mutable reference to another type.
+    Reference(Lifetime, Mutability, Box<TypeName>),
+    /// A `Box<T>` type.
+    Box(Box<TypeName>),
+    /// An `Option<T>` or DiplomatOption type.
+    Option(Box<TypeName>, StdlibOrDiplomat),
+    /// A `Result<T, E>` or `diplomat_runtime::DiplomatResult` type.
+    Result(Box<TypeName>, Box<TypeName>, StdlibOrDiplomat),
+    Write,
+    /// A `&DiplomatStr` or `Box<DiplomatStr>` type.
+    /// Owned strings don't have a lifetime.
+    ///
+    /// If StdlibOrDiplomat::Stdlib, it's specified using Rust pointer types (&T, Box<T>),
+    /// if StdlibOrDiplomat::Diplomat, it's specified using DiplomatStrSlice, etc
+    StrReference(Option<Lifetime>, StringEncoding, StdlibOrDiplomat),
+    /// A `&[T]` or `Box<[T]>` type, where `T` is a primitive.
+    /// Owned slices don't have a lifetime or mutability.
+    ///
+    /// If StdlibOrDiplomat::Stdlib, it's specified using Rust pointer types (&T, Box<T>),
+    /// if StdlibOrDiplomat::Diplomat, it's specified using DiplomatSlice/DiplomatOwnedSlice/DiplomatSliceMut
+    PrimitiveSlice(
+        Option<(Lifetime, Mutability)>,
+        PrimitiveType,
+        StdlibOrDiplomat,
+    ),
+    /// `&[&DiplomatStr]`, etc. Equivalent to `&[&str]`
+    ///
+    /// If StdlibOrDiplomat::Stdlib, it's specified as `&[&DiplomatFoo]`, if StdlibOrDiplomat::Diplomat it's specified
+    /// as `DiplomatSlice<&DiplomatFoo>`
+    StrSlice(StringEncoding, StdlibOrDiplomat),
+    /// The `()` type.
+    Unit,
+    /// The `Self` type.
+    SelfType(PathType),
+    /// std::cmp::Ordering or core::cmp::Ordering
+    ///
+    /// The path must be present! Ordering will be parsed as an AST type!
+    Ordering,
+    Function(Vec<Box<TypeName>>, Box<TypeName>),
+    ImplTrait(PathType),
+}
+
+#[derive(Clone, PartialEq, Eq, Hash, Serialize, Deserialize, Debug, Copy)]
+#[non_exhaustive]
+pub enum StringEncoding {
+    UnvalidatedUtf8,
+    UnvalidatedUtf16,
+    /// The caller guarantees that they're passing valid UTF-8, under penalty of UB
+    Utf8,
+}
+
+impl StringEncoding {
+    /// Get the diplomat slice type when specified using diplomat_runtime types
+    pub fn get_diplomat_slice_type(self, lt: &Option<Lifetime>) -> syn::Type {
+        if let Some(ref lt) = *lt {
+            let lt = LifetimeGenericsListDisplay(lt);
+
+            match self {
+                Self::UnvalidatedUtf8 => {
+                    syn::parse_quote_spanned!(Span::call_site() => diplomat_runtime::DiplomatStrSlice #lt)
+                }
+                Self::UnvalidatedUtf16 => {
+                    syn::parse_quote_spanned!(Span::call_site() => diplomat_runtime::DiplomatStr16Slice #lt)
+                }
+                Self::Utf8 => {
+                    syn::parse_quote_spanned!(Span::call_site() => diplomat_runtime::DiplomatUtf8StrSlice #lt)
+                }
+            }
+        } else {
+            match self {
+                Self::UnvalidatedUtf8 => {
+                    syn::parse_quote_spanned!(Span::call_site() => diplomat_runtime::DiplomatOwnedStrSlice)
+                }
+                Self::UnvalidatedUtf16 => {
+                    syn::parse_quote_spanned!(Span::call_site() => diplomat_runtime::DiplomatOwnedStr16Slice)
+                }
+                Self::Utf8 => {
+                    syn::parse_quote_spanned!(Span::call_site() => diplomat_runtime::DiplomatOwnedUTF8StrSlice)
+                }
+            }
+        }
+    }
+
+    fn get_diplomat_slice_type_str(self) -> &'static str {
+        match self {
+            StringEncoding::Utf8 => "str",
+            StringEncoding::UnvalidatedUtf8 => "DiplomatStr",
+            StringEncoding::UnvalidatedUtf16 => "DiplomatStr16",
+        }
+    }
+    /// Get slice type when specified using rust stdlib types
+    pub fn get_stdlib_slice_type(self, lt: &Option<Lifetime>) -> syn::Type {
+        let inner = match self {
+            Self::UnvalidatedUtf8 => quote::quote!(DiplomatStr),
+            Self::UnvalidatedUtf16 => quote::quote!(DiplomatStr16),
+            Self::Utf8 => quote::quote!(str),
+        };
+        if let Some(ref lt) = *lt {
+            let lt = ReferenceDisplay(lt, &Mutability::Immutable);
+
+            syn::parse_quote_spanned!(Span::call_site() => #lt #inner)
+        } else {
+            syn::parse_quote_spanned!(Span::call_site() => Box<#inner>)
+        }
+    }
+    pub fn get_stdlib_slice_type_str(self) -> &'static str {
+        match self {
+            StringEncoding::Utf8 => "DiplomatUtf8Str",
+            StringEncoding::UnvalidatedUtf8 => "DiplomatStrSlice",
+            StringEncoding::UnvalidatedUtf16 => "DiplomatStr16Slice",
+        }
+    }
+}
+
+fn get_lifetime_from_syn_path(p: &syn::TypePath) -> Lifetime {
+    if let syn::PathArguments::AngleBracketed(ref generics) =
+        p.path.segments[p.path.segments.len() - 1].arguments
+    {
+        if let Some(syn::GenericArgument::Lifetime(lt)) = generics.args.first() {
+            return Lifetime::from(lt);
+        }
+    }
+    Lifetime::Anonymous
+}
+
+fn get_ty_from_syn_path(p: &syn::TypePath) -> Option<&syn::Type> {
+    if let syn::PathArguments::AngleBracketed(ref generics) =
+        p.path.segments[p.path.segments.len() - 1].arguments
+    {
+        for gen in generics.args.iter() {
+            if let syn::GenericArgument::Type(ref ty) = gen {
+                return Some(ty);
+            }
+        }
+    }
+    None
+}
+
+impl TypeName {
+    /// Is this type safe to be passed across the FFI boundary?
+    ///
+    /// This also marks DiplomatOption<&T> as FFI-unsafe: these are technically safe from an ABI standpoint
+    /// however Diplomat always expects these to be equivalent to a nullable pointer, so Option<&T> is required.
+    pub fn is_ffi_safe(&self) -> bool {
+        match self {
+            TypeName::Primitive(..) | TypeName::Named(_) | TypeName::SelfType(_) | TypeName::Reference(..) |
+            TypeName::Box(..) |
+            // can only be passed across the FFI boundary; callbacks and traits are input-only
+            TypeName::Function(..) | TypeName::ImplTrait(..) |
+            // These are specified using FFI-safe diplomat_runtime types
+            TypeName::StrReference(.., StdlibOrDiplomat::Diplomat) | TypeName::StrSlice(.., StdlibOrDiplomat::Diplomat) |TypeName::PrimitiveSlice(.., StdlibOrDiplomat::Diplomat) => true,
+            // These are special anyway and shouldn't show up in structs
+            TypeName::Unit | TypeName::Write | TypeName::Result(..) |
+            // This is basically only useful in return types
+            TypeName::Ordering |
+            // These are specified using Rust stdlib types and not safe across FFI
+            TypeName::StrReference(.., StdlibOrDiplomat::Stdlib) | TypeName::StrSlice(.., StdlibOrDiplomat::Stdlib) | TypeName::PrimitiveSlice(.., StdlibOrDiplomat::Stdlib)  => false,
+            TypeName::Option(inner, stdlib) => match **inner {
+                // Option<&T>/Option<Box<T>> are the ffi-safe way to specify options
+                TypeName::Reference(..) | TypeName::Box(..) => *stdlib == StdlibOrDiplomat::Stdlib,
+                // For other types (primitives, structs, enums) we need DiplomatOption
+                _ => *stdlib == StdlibOrDiplomat::Diplomat,
+             }
+        }
+    }
+
+    /// What's the FFI safe version of this type?
+    ///
+    /// This also marks DiplomatOption<&T> as FFI-unsafe: these are technically safe from an ABI standpoint
+    /// however Diplomat always expects these to be equivalent to a nullable pointer, so Option<&T> is required.
+    pub fn ffi_safe_version(&self) -> TypeName {
+        match self {
+            TypeName::StrReference(lt, encoding, StdlibOrDiplomat::Stdlib) => {
+                TypeName::StrReference(lt.clone(), *encoding, StdlibOrDiplomat::Diplomat)
+            }
+            TypeName::StrSlice(encoding, StdlibOrDiplomat::Stdlib) => {
+                TypeName::StrSlice(*encoding, StdlibOrDiplomat::Diplomat)
+            }
+            TypeName::PrimitiveSlice(ltmt, prim, StdlibOrDiplomat::Stdlib) => {
+                TypeName::PrimitiveSlice(ltmt.clone(), *prim, StdlibOrDiplomat::Diplomat)
+            }
+            TypeName::Ordering => TypeName::Primitive(PrimitiveType::i8),
+            TypeName::Option(inner, _stdlib) => match **inner {
+                // Option<&T>/Option<Box<T>> are the ffi-safe way to specify options
+                TypeName::Reference(..) | TypeName::Box(..) => {
+                    TypeName::Option(inner.clone(), StdlibOrDiplomat::Stdlib)
+                }
+                // For other types (primitives, structs, enums) we need DiplomatOption
+                _ => TypeName::Option(inner.clone(), StdlibOrDiplomat::Diplomat),
+            },
+            _ => self.clone(),
+        }
+    }
+    /// Converts the [`TypeName`] back into an AST node that can be spliced into a program.
+    pub fn to_syn(&self) -> syn::Type {
+        match self {
+            TypeName::Primitive(primitive) => {
+                let primitive = primitive.to_ident();
+                syn::parse_quote_spanned!(Span::call_site() => #primitive)
+            }
+            TypeName::Ordering => syn::parse_quote_spanned!(Span::call_site() => i8),
+            TypeName::Named(name) | TypeName::SelfType(name) => {
+                // Self also gets expanded instead of turning into `Self` because
+                // this code is used to generate the `extern "C"` functions, which
+                // aren't in an impl block.
+                let name = name.to_syn();
+                syn::parse_quote_spanned!(Span::call_site() => #name)
+            }
+            TypeName::Reference(lifetime, mutability, underlying) => {
+                let reference = ReferenceDisplay(lifetime, mutability);
+                let underlying = underlying.to_syn();
+
+                syn::parse_quote_spanned!(Span::call_site() => #reference #underlying)
+            }
+            TypeName::Box(underlying) => {
+                let underlying = underlying.to_syn();
+                syn::parse_quote_spanned!(Span::call_site() => Box<#underlying>)
+            }
+            TypeName::Option(underlying, StdlibOrDiplomat::Stdlib) => {
+                let underlying = underlying.to_syn();
+                syn::parse_quote_spanned!(Span::call_site() => Option<#underlying>)
+            }
+            TypeName::Option(underlying, StdlibOrDiplomat::Diplomat) => {
+                let underlying = underlying.to_syn();
+                syn::parse_quote_spanned!(Span::call_site() => diplomat_runtime::DiplomatOption<#underlying>)
+            }
+            TypeName::Result(ok, err, StdlibOrDiplomat::Stdlib) => {
+                let ok = ok.to_syn();
+                let err = err.to_syn();
+                syn::parse_quote_spanned!(Span::call_site() => Result<#ok, #err>)
+            }
+            TypeName::Result(ok, err, StdlibOrDiplomat::Diplomat) => {
+                let ok = ok.to_syn();
+                let err = err.to_syn();
+                syn::parse_quote_spanned!(Span::call_site() => diplomat_runtime::DiplomatResult<#ok, #err>)
+            }
+            TypeName::Write => {
+                syn::parse_quote_spanned!(Span::call_site() => diplomat_runtime::DiplomatWrite)
+            }
+            TypeName::StrReference(lt, encoding, is_stdlib_type) => {
+                if *is_stdlib_type == StdlibOrDiplomat::Stdlib {
+                    encoding.get_stdlib_slice_type(lt)
+                } else {
+                    encoding.get_diplomat_slice_type(lt)
+                }
+            }
+            TypeName::StrSlice(encoding, is_stdlib_type) => {
+                if *is_stdlib_type == StdlibOrDiplomat::Stdlib {
+                    let inner = encoding.get_stdlib_slice_type(&Some(Lifetime::Anonymous));
+                    syn::parse_quote_spanned!(Span::call_site() => &[#inner])
+                } else {
+                    let inner = encoding.get_diplomat_slice_type(&Some(Lifetime::Anonymous));
+                    syn::parse_quote_spanned!(Span::call_site() => diplomat_runtime::DiplomatSlice<#inner>)
+                }
+            }
+            TypeName::PrimitiveSlice(ltmt, primitive, is_stdlib_type) => {
+                if *is_stdlib_type == StdlibOrDiplomat::Stdlib {
+                    primitive.get_stdlib_slice_type(ltmt)
+                } else {
+                    primitive.get_diplomat_slice_type(ltmt)
+                }
+            }
+
+            TypeName::Unit => syn::parse_quote_spanned!(Span::call_site() => ()),
+            TypeName::Function(_input_types, output_type) => {
+                let output_type = output_type.to_syn();
+                // should be DiplomatCallback<function_output_type>
+                syn::parse_quote_spanned!(Span::call_site() => DiplomatCallback<#output_type>)
+            }
+            TypeName::ImplTrait(trt_path) => {
+                let trait_name =
+                    Ident::from(format!("DiplomatTraitStruct_{}", trt_path.path.elements[0]));
+                // should be DiplomatTraitStruct_trait_name
+                syn::parse_quote_spanned!(Span::call_site() => #trait_name)
+            }
+        }
+    }
+
+    /// Extract a [`TypeName`] from a [`syn::Type`] AST node.
+    /// The following rules are used to infer [`TypeName`] variants:
+    /// - If the type is a path with a single element that is the name of a Rust primitive, returns a [`TypeName::Primitive`]
+    /// - If the type is a path with a single element [`Box`], returns a [`TypeName::Box`] with the type parameter recursively converted
+    /// - If the type is a path with a single element [`Option`], returns a [`TypeName::Option`] with the type parameter recursively converted
+    /// - If the type is a path with a single element `Self` and `self_path_type` is provided, returns a [`TypeName::Named`]
+    /// - If the type is a path with a single element [`Result`], returns a [`TypeName::Result`] with the type parameters recursively converted
+    /// - If the type is a path equal to [`diplomat_runtime::DiplomatResult`], returns a [`TypeName::DiplomatResult`] with the type parameters recursively converted
+    /// - If the type is a path equal to [`diplomat_runtime::DiplomatWrite`], returns a [`TypeName::Write`]
+    /// - If the type is a owned or borrowed string type, returns a [`TypeName::StrReference`]
+    /// - If the type is a owned or borrowed slice of a Rust primitive, returns a [`TypeName::PrimitiveSlice`]
+    /// - If the type is a reference (`&` or `&mut`), returns a [`TypeName::Reference`] with the referenced type recursively converted
+    /// - Otherwise, assume that the reference is to a [`CustomType`] in either the current module or another one, returns a [`TypeName::Named`]
+    pub fn from_syn(ty: &syn::Type, self_path_type: Option<PathType>) -> TypeName {
+        match ty {
+            syn::Type::Reference(r) => {
+                let lifetime = Lifetime::from(&r.lifetime);
+                let mutability = Mutability::from_syn(&r.mutability);
+
+                let name = r.elem.to_token_stream().to_string();
+                if name.starts_with("DiplomatStr") || name == "str" {
+                    if mutability.is_mutable() {
+                        panic!("mutable string references are disallowed");
+                    }
+                    if name == "DiplomatStr" {
+                        return TypeName::StrReference(
+                            Some(lifetime),
+                            StringEncoding::UnvalidatedUtf8,
+                            StdlibOrDiplomat::Stdlib,
+                        );
+                    } else if name == "DiplomatStr16" {
+                        return TypeName::StrReference(
+                            Some(lifetime),
+                            StringEncoding::UnvalidatedUtf16,
+                            StdlibOrDiplomat::Stdlib,
+                        );
+                    } else if name == "str" {
+                        return TypeName::StrReference(
+                            Some(lifetime),
+                            StringEncoding::Utf8,
+                            StdlibOrDiplomat::Stdlib,
+                        );
+                    }
+                }
+                if let syn::Type::Slice(slice) = &*r.elem {
+                    if let syn::Type::Path(p) = &*slice.elem {
+                        if let Some(primitive) = p
+                            .path
+                            .get_ident()
+                            .and_then(|i| PrimitiveType::from_str(i.to_string().as_str()).ok())
+                        {
+                            return TypeName::PrimitiveSlice(
+                                Some((lifetime, mutability)),
+                                primitive,
+                                StdlibOrDiplomat::Stdlib,
+                            );
+                        }
+                    }
+                    if let TypeName::StrReference(
+                        Some(Lifetime::Anonymous),
+                        encoding,
+                        is_stdlib_type,
+                    ) = TypeName::from_syn(&slice.elem, self_path_type.clone())
+                    {
+                        if is_stdlib_type == StdlibOrDiplomat::Stdlib {
+                            panic!("Slice-of-slice is only supported with DiplomatRuntime slice types (DiplomatStrSlice, DiplomatStr16Slice, DiplomatUtf8StrSlice)");
+                        }
+                        return TypeName::StrSlice(encoding, StdlibOrDiplomat::Stdlib);
+                    }
+                }
+                TypeName::Reference(
+                    lifetime,
+                    mutability,
+                    Box::new(TypeName::from_syn(r.elem.as_ref(), self_path_type)),
+                )
+            }
+            syn::Type::Path(p) => {
+                let p_len = p.path.segments.len();
+                if let Some(primitive) = p
+                    .path
+                    .get_ident()
+                    .and_then(|i| PrimitiveType::from_str(i.to_string().as_str()).ok())
+                {
+                    TypeName::Primitive(primitive)
+                } else if p_len >= 2
+                    && p.path.segments[p_len - 2].ident == "cmp"
+                    && p.path.segments[p_len - 1].ident == "Ordering"
+                {
+                    TypeName::Ordering
+                } else if p_len == 1 && p.path.segments[0].ident == "Box" {
+                    if let syn::PathArguments::AngleBracketed(type_args) =
+                        &p.path.segments[0].arguments
+                    {
+                        if let syn::GenericArgument::Type(syn::Type::Slice(slice)) =
+                            &type_args.args[0]
+                        {
+                            if let TypeName::Primitive(p) =
+                                TypeName::from_syn(&slice.elem, self_path_type)
+                            {
+                                TypeName::PrimitiveSlice(None, p, StdlibOrDiplomat::Stdlib)
+                            } else {
+                                panic!("Owned slices only support primitives.")
+                            }
+                        } else if let syn::GenericArgument::Type(tpe) = &type_args.args[0] {
+                            if tpe.to_token_stream().to_string() == "DiplomatStr" {
+                                TypeName::StrReference(
+                                    None,
+                                    StringEncoding::UnvalidatedUtf8,
+                                    StdlibOrDiplomat::Stdlib,
+                                )
+                            } else if tpe.to_token_stream().to_string() == "DiplomatStr16" {
+                                TypeName::StrReference(
+                                    None,
+                                    StringEncoding::UnvalidatedUtf16,
+                                    StdlibOrDiplomat::Stdlib,
+                                )
+                            } else if tpe.to_token_stream().to_string() == "str" {
+                                TypeName::StrReference(
+                                    None,
+                                    StringEncoding::Utf8,
+                                    StdlibOrDiplomat::Stdlib,
+                                )
+                            } else {
+                                TypeName::Box(Box::new(TypeName::from_syn(tpe, self_path_type)))
+                            }
+                        } else {
+                            panic!("Expected first type argument for Box to be a type")
+                        }
+                    } else {
+                        panic!("Expected angle brackets for Box type")
+                    }
+                } else if p_len == 1 && p.path.segments[0].ident == "Option"
+                    || is_runtime_type(p, "DiplomatOption")
+                {
+                    if let syn::PathArguments::AngleBracketed(type_args) =
+                        &p.path.segments[0].arguments
+                    {
+                        if let syn::GenericArgument::Type(tpe) = &type_args.args[0] {
+                            let stdlib = if p.path.segments[0].ident == "Option" {
+                                StdlibOrDiplomat::Stdlib
+                            } else {
+                                StdlibOrDiplomat::Diplomat
+                            };
+                            TypeName::Option(
+                                Box::new(TypeName::from_syn(tpe, self_path_type)),
+                                stdlib,
+                            )
+                        } else {
+                            panic!("Expected first type argument for Option to be a type")
+                        }
+                    } else {
+                        panic!("Expected angle brackets for Option type")
+                    }
+                } else if p_len == 1 && p.path.segments[0].ident == "Self" {
+                    if let Some(self_path_type) = self_path_type {
+                        TypeName::SelfType(self_path_type)
+                    } else {
+                        panic!("Cannot have `Self` type outside of a method");
+                    }
+                } else if is_runtime_type(p, "DiplomatOwnedStrSlice")
+                    || is_runtime_type(p, "DiplomatOwnedStr16Slice")
+                    || is_runtime_type(p, "DiplomatOwnedUTF8StrSlice")
+                {
+                    let encoding = if is_runtime_type(p, "DiplomatOwnedStrSlice") {
+                        StringEncoding::UnvalidatedUtf8
+                    } else if is_runtime_type(p, "DiplomatOwnedStr16Slice") {
+                        StringEncoding::UnvalidatedUtf16
+                    } else {
+                        StringEncoding::Utf8
+                    };
+
+                    TypeName::StrReference(None, encoding, StdlibOrDiplomat::Diplomat)
+                } else if is_runtime_type(p, "DiplomatStrSlice")
+                    || is_runtime_type(p, "DiplomatStr16Slice")
+                    || is_runtime_type(p, "DiplomatUtf8StrSlice")
+                {
+                    let lt = get_lifetime_from_syn_path(p);
+
+                    let encoding = if is_runtime_type(p, "DiplomatStrSlice") {
+                        StringEncoding::UnvalidatedUtf8
+                    } else if is_runtime_type(p, "DiplomatStr16Slice") {
+                        StringEncoding::UnvalidatedUtf16
+                    } else {
+                        StringEncoding::Utf8
+                    };
+
+                    TypeName::StrReference(Some(lt), encoding, StdlibOrDiplomat::Diplomat)
+                } else if is_runtime_type(p, "DiplomatSlice")
+                    || is_runtime_type(p, "DiplomatSliceMut")
+                    || is_runtime_type(p, "DiplomatOwnedSlice")
+                {
+                    let ltmut = if is_runtime_type(p, "DiplomatOwnedSlice") {
+                        let mutability = if is_runtime_type(p, "DiplomatSlice") {
+                            Mutability::Immutable
+                        } else {
+                            Mutability::Mutable
+                        };
+                        let lt = get_lifetime_from_syn_path(p);
+                        Some((lt, mutability))
+                    } else {
+                        None
+                    };
+
+                    let ty = get_ty_from_syn_path(p).expect("Expected type argument to DiplomatSlice/DiplomatSliceMut/DiplomatOwnedSlice");
+
+                    if let syn::Type::Path(p) = &ty {
+                        if let Some(ident) = p.path.get_ident() {
+                            let ident = ident.to_string();
+                            let i = ident.as_str();
+                            match i {
+                                "DiplomatStrSlice" => {
+                                    return TypeName::StrSlice(
+                                        StringEncoding::UnvalidatedUtf8,
+                                        StdlibOrDiplomat::Diplomat,
+                                    )
+                                }
+                                "DiplomatStr16Slice" => {
+                                    return TypeName::StrSlice(
+                                        StringEncoding::UnvalidatedUtf16,
+                                        StdlibOrDiplomat::Diplomat,
+                                    )
+                                }
+                                "DiplomatUtf8StrSlice" => {
+                                    return TypeName::StrSlice(
+                                        StringEncoding::Utf8,
+                                        StdlibOrDiplomat::Diplomat,
+                                    )
+                                }
+                                _ => {
+                                    if let Ok(prim) = PrimitiveType::from_str(i) {
+                                        return TypeName::PrimitiveSlice(
+                                            ltmut,
+                                            prim,
+                                            StdlibOrDiplomat::Diplomat,
+                                        );
+                                    }
+                                }
+                            }
+                        }
+                    }
+                    panic!("Found DiplomatSlice/DiplomatSliceMut/DiplomatOwnedSlice without primitive or DiplomatStrSlice-like generic");
+                } else if p_len == 1 && p.path.segments[0].ident == "Result"
+                    || is_runtime_type(p, "DiplomatResult")
+                {
+                    if let syn::PathArguments::AngleBracketed(type_args) =
+                        &p.path.segments.last().unwrap().arguments
+                    {
+                        if let (syn::GenericArgument::Type(ok), syn::GenericArgument::Type(err)) =
+                            (&type_args.args[0], &type_args.args[1])
+                        {
+                            let ok = TypeName::from_syn(ok, self_path_type.clone());
+                            let err = TypeName::from_syn(err, self_path_type);
+                            TypeName::Result(
+                                Box::new(ok),
+                                Box::new(err),
+                                if is_runtime_type(p, "DiplomatResult") {
+                                    StdlibOrDiplomat::Diplomat
+                                } else {
+                                    StdlibOrDiplomat::Stdlib
+                                },
+                            )
+                        } else {
+                            panic!("Expected both type arguments for Result to be a type")
+                        }
+                    } else {
+                        panic!("Expected angle brackets for Result type")
+                    }
+                } else if is_runtime_type(p, "DiplomatWrite") {
+                    TypeName::Write
+                } else {
+                    TypeName::Named(PathType::from(p))
+                }
+            }
+            syn::Type::Tuple(tup) => {
+                if tup.elems.is_empty() {
+                    TypeName::Unit
+                } else {
+                    todo!("Tuples are not currently supported")
+                }
+            }
+            syn::Type::ImplTrait(tr) => {
+                let trait_bound = tr.bounds.first();
+                if tr.bounds.len() > 1 {
+                    todo!("Currently don't support implementing multiple traits");
+                }
+                if let Some(syn::TypeParamBound::Trait(syn::TraitBound { path: p, .. })) =
+                    trait_bound
+                {
+                    let rel_segs = &p.segments;
+                    let path_seg = &rel_segs[0];
+                    if path_seg.ident.eq("Fn") {
+                        // we're in a function type
+                        // get input and output args
+                        if let syn::PathArguments::Parenthesized(
+                            syn::ParenthesizedGenericArguments {
+                                inputs: input_types,
+                                output: output_type,
+                                ..
+                            },
+                        ) = &path_seg.arguments
+                        {
+                            let in_types = input_types
+                                .iter()
+                                .map(|in_ty| {
+                                    Box::new(TypeName::from_syn(in_ty, self_path_type.clone()))
+                                })
+                                .collect::<Vec<Box<TypeName>>>();
+                            let out_type = match output_type {
+                                syn::ReturnType::Type(_, output_type) => {
+                                    TypeName::from_syn(output_type, self_path_type.clone())
+                                }
+                                syn::ReturnType::Default => TypeName::Unit,
+                            };
+                            let ret = TypeName::Function(in_types, Box::new(out_type));
+                            return ret;
+                        }
+                        panic!("Unsupported function type: {:?}", &path_seg.arguments);
+                    } else {
+                        let ret = TypeName::ImplTrait(PathType::from(&syn::TraitBound {
+                            paren_token: None,
+                            modifier: syn::TraitBoundModifier::None,
+                            lifetimes: None, // todo this is an assumption
+                            path: p.clone(),
+                        }));
+                        return ret;
+                    }
+                }
+                panic!("Unsupported trait type: {:?}", tr);
+            }
+            other => panic!("Unsupported type: {}", other.to_token_stream()),
+        }
+    }
+
+    /// Returns `true` if `self` is the `TypeName::SelfType` variant, otherwise
+    /// `false`.
+    pub fn is_self(&self) -> bool {
+        matches!(self, TypeName::SelfType(_))
+    }
+
+    /// Recurse down the type tree, visiting all lifetimes.
+    ///
+    /// Using this function, you can collect all the lifetimes into a collection,
+    /// or examine each one without having to make any additional allocations.
+    pub fn visit_lifetimes<'a, F, B>(&'a self, visit: &mut F) -> ControlFlow<B>
+    where
+        F: FnMut(&'a Lifetime, LifetimeOrigin) -> ControlFlow<B>,
+    {
+        match self {
+            TypeName::Named(path_type) | TypeName::SelfType(path_type) => path_type
+                .lifetimes
+                .iter()
+                .try_for_each(|lt| visit(lt, LifetimeOrigin::Named)),
+            TypeName::Reference(lt, _, ty) => {
+                ty.visit_lifetimes(visit)?;
+                visit(lt, LifetimeOrigin::Reference)
+            }
+            TypeName::Box(ty) | TypeName::Option(ty, _) => ty.visit_lifetimes(visit),
+            TypeName::Result(ok, err, _) => {
+                ok.visit_lifetimes(visit)?;
+                err.visit_lifetimes(visit)
+            }
+            TypeName::StrReference(Some(lt), ..) => visit(lt, LifetimeOrigin::StrReference),
+            TypeName::PrimitiveSlice(Some((lt, _)), ..) => {
+                visit(lt, LifetimeOrigin::PrimitiveSlice)
+            }
+            _ => ControlFlow::Continue(()),
+        }
+    }
+
+    /// Returns `true` if any lifetime satisfies a predicate, otherwise `false`.
+    ///
+    /// This method is short-circuiting, meaning that if the predicate ever succeeds,
+    /// it will return immediately.
+    pub fn any_lifetime<'a, F>(&'a self, mut f: F) -> bool
+    where
+        F: FnMut(&'a Lifetime, LifetimeOrigin) -> bool,
+    {
+        self.visit_lifetimes(&mut |lifetime, origin| {
+            if f(lifetime, origin) {
+                ControlFlow::Break(())
+            } else {
+                ControlFlow::Continue(())
+            }
+        })
+        .is_break()
+    }
+
+    /// Returns `true` if all lifetimes satisfy a predicate, otherwise `false`.
+    ///
+    /// This method is short-circuiting, meaning that if the predicate ever fails,
+    /// it will return immediately.
+    pub fn all_lifetimes<'a, F>(&'a self, mut f: F) -> bool
+    where
+        F: FnMut(&'a Lifetime, LifetimeOrigin) -> bool,
+    {
+        self.visit_lifetimes(&mut |lifetime, origin| {
+            if f(lifetime, origin) {
+                ControlFlow::Continue(())
+            } else {
+                ControlFlow::Break(())
+            }
+        })
+        .is_continue()
+    }
+
+    /// Returns all lifetimes in a [`LifetimeEnv`] that must live at least as
+    /// long as the type.
+    pub fn longer_lifetimes<'env>(
+        &self,
+        lifetime_env: &'env LifetimeEnv,
+    ) -> Vec<&'env NamedLifetime> {
+        self.transitive_lifetime_bounds(LifetimeTransitivity::longer(lifetime_env))
+    }
+
+    /// Returns all lifetimes in a [`LifetimeEnv`] that are outlived by the type.
+    pub fn shorter_lifetimes<'env>(
+        &self,
+        lifetime_env: &'env LifetimeEnv,
+    ) -> Vec<&'env NamedLifetime> {
+        self.transitive_lifetime_bounds(LifetimeTransitivity::shorter(lifetime_env))
+    }
+
+    /// Visits the provided [`LifetimeTransitivity`] value with all `NamedLifetime`s
+    /// in the type tree, and returns the transitively reachable lifetimes.
+    fn transitive_lifetime_bounds<'env>(
+        &self,
+        mut transitivity: LifetimeTransitivity<'env>,
+    ) -> Vec<&'env NamedLifetime> {
+        self.visit_lifetimes(&mut |lifetime, _| -> ControlFlow<()> {
+            if let Lifetime::Named(named) = lifetime {
+                transitivity.visit(named);
+            }
+            ControlFlow::Continue(())
+        });
+        transitivity.finish()
+    }
+
+    pub fn is_zst(&self) -> bool {
+        // check_zst() prevents non-unit types from being ZSTs
+        matches!(*self, TypeName::Unit)
+    }
+
+    pub fn is_pointer(&self) -> bool {
+        matches!(*self, TypeName::Reference(..) | TypeName::Box(_))
+    }
+}
+
+#[non_exhaustive]
+pub enum LifetimeOrigin {
+    Named,
+    Reference,
+    StrReference,
+    PrimitiveSlice,
+}
+
+fn is_runtime_type(p: &syn::TypePath, name: &str) -> bool {
+    (p.path.segments.len() == 1 && p.path.segments[0].ident == name)
+        || (p.path.segments.len() == 2
+            && p.path.segments[0].ident == "diplomat_runtime"
+            && p.path.segments[1].ident == name)
+}
+
+impl fmt::Display for TypeName {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        match self {
+            TypeName::Primitive(p) => p.fmt(f),
+            TypeName::Ordering => write!(f, "Ordering"),
+            TypeName::Named(p) | TypeName::SelfType(p) => p.fmt(f),
+            TypeName::Reference(lifetime, mutability, typ) => {
+                write!(f, "{}{typ}", ReferenceDisplay(lifetime, mutability))
+            }
+            TypeName::Box(typ) => write!(f, "Box<{typ}>"),
+            TypeName::Option(typ, StdlibOrDiplomat::Stdlib) => write!(f, "Option<{typ}>"),
+            TypeName::Option(typ, StdlibOrDiplomat::Diplomat) => write!(f, "DiplomatOption<{typ}>"),
+            TypeName::Result(ok, err, _) => {
+                write!(f, "Result<{ok}, {err}>")
+            }
+            TypeName::Write => "DiplomatWrite".fmt(f),
+            TypeName::StrReference(lt, encoding, is_stdlib_type) => {
+                if let Some(lt) = lt {
+                    if *is_stdlib_type == StdlibOrDiplomat::Stdlib {
+                        let lt = ReferenceDisplay(lt, &Mutability::Immutable);
+                        let ty = encoding.get_diplomat_slice_type_str();
+                        write!(f, "{lt}{ty}")
+                    } else {
+                        let ty = encoding.get_stdlib_slice_type_str();
+                        let lt = LifetimeGenericsListDisplay(lt);
+                        write!(f, "{ty}{lt}")
+                    }
+                } else {
+                    match (encoding, is_stdlib_type) {
+                        (_, StdlibOrDiplomat::Stdlib) => {
+                            write!(f, "Box<{}>", encoding.get_diplomat_slice_type_str())
+                        }
+                        (StringEncoding::Utf8, StdlibOrDiplomat::Diplomat) => {
+                            "DiplomatOwnedUtf8Str".fmt(f)
+                        }
+                        (StringEncoding::UnvalidatedUtf8, StdlibOrDiplomat::Diplomat) => {
+                            "DiplomatOwnedStrSlice".fmt(f)
+                        }
+                        (StringEncoding::UnvalidatedUtf16, StdlibOrDiplomat::Diplomat) => {
+                            "DiplomatOwnedStr16Slice".fmt(f)
+                        }
+                    }
+                }
+            }
+
+            TypeName::StrSlice(encoding, StdlibOrDiplomat::Stdlib) => {
+                let inner = encoding.get_stdlib_slice_type_str();
+
+                write!(f, "&[&{inner}]")
+            }
+            TypeName::StrSlice(encoding, StdlibOrDiplomat::Diplomat) => {
+                let inner = encoding.get_diplomat_slice_type_str();
+                write!(f, "DiplomatSlice<{inner}>")
+            }
+
+            TypeName::PrimitiveSlice(
+                Some((lifetime, mutability)),
+                typ,
+                StdlibOrDiplomat::Stdlib,
+            ) => {
+                write!(f, "{}[{typ}]", ReferenceDisplay(lifetime, mutability))
+            }
+            TypeName::PrimitiveSlice(
+                Some((lifetime, mutability)),
+                typ,
+                StdlibOrDiplomat::Diplomat,
+            ) => {
+                let maybemut = if *mutability == Mutability::Immutable {
+                    ""
+                } else {
+                    "Mut"
+                };
+                let lt = LifetimeGenericsListPartialDisplay(lifetime);
+                write!(f, "DiplomatSlice{maybemut}<{lt}{typ}>")
+            }
+            TypeName::PrimitiveSlice(None, typ, _) => write!(f, "Box<[{typ}]>"),
+            TypeName::Unit => "()".fmt(f),
+            TypeName::Function(input_types, out_type) => {
+                write!(f, "fn (")?;
+                for in_typ in input_types.iter() {
+                    write!(f, "{in_typ}")?;
+                }
+                write!(f, ")->{out_type}")
+            }
+            TypeName::ImplTrait(trt) => {
+                write!(f, "impl ")?;
+                trt.fmt(f)
+            }
+        }
+    }
+}
+
+/// An [`fmt::Display`] type for formatting Rust references.
+///
+/// # Examples
+///
+/// ```ignore
+/// let lifetime = Lifetime::from(&syn::parse_str::<syn::Lifetime>("'a"));
+/// let mutability = Mutability::Mutable;
+/// // ...
+/// let fmt = format!("{}[u8]", ReferenceDisplay(&lifetime, &mutability));
+///
+/// assert_eq!(fmt, "&'a mut [u8]");
+/// ```
+struct ReferenceDisplay<'a>(&'a Lifetime, &'a Mutability);
+
+impl<'a> fmt::Display for ReferenceDisplay<'a> {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        match self.0 {
+            Lifetime::Static => "&'static ".fmt(f)?,
+            Lifetime::Named(lifetime) => write!(f, "&{lifetime} ")?,
+            Lifetime::Anonymous => '&'.fmt(f)?,
+        }
+
+        if self.1.is_mutable() {
+            "mut ".fmt(f)?;
+        }
+
+        Ok(())
+    }
+}
+
+impl<'a> quote::ToTokens for ReferenceDisplay<'a> {
+    fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
+        let lifetime = self.0.to_syn();
+        let mutability = self.1.to_syn();
+
+        tokens.append_all(quote::quote!(& #lifetime #mutability))
+    }
+}
+
+/// An [`fmt::Display`] type for formatting Rust lifetimes as they show up in generics list, when
+/// the generics list has no other elements
+struct LifetimeGenericsListDisplay<'a>(&'a Lifetime);
+
+impl<'a> fmt::Display for LifetimeGenericsListDisplay<'a> {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        match self.0 {
+            Lifetime::Static => "<'static>".fmt(f),
+            Lifetime::Named(lifetime) => write!(f, "<{lifetime}>"),
+            Lifetime::Anonymous => Ok(()),
+        }
+    }
+}
+
+impl<'a> quote::ToTokens for LifetimeGenericsListDisplay<'a> {
+    fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
+        if let Lifetime::Anonymous = self.0 {
+        } else {
+            let lifetime = self.0.to_syn();
+            tokens.append_all(quote::quote!(<#lifetime>))
+        }
+    }
+}
+
+/// An [`fmt::Display`] type for formatting Rust lifetimes as they show up in generics list, when
+/// the generics list has another element
+struct LifetimeGenericsListPartialDisplay<'a>(&'a Lifetime);
+
+impl<'a> fmt::Display for LifetimeGenericsListPartialDisplay<'a> {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        match self.0 {
+            Lifetime::Static => "'static,".fmt(f),
+            Lifetime::Named(lifetime) => write!(f, "{lifetime},"),
+            Lifetime::Anonymous => Ok(()),
+        }
+    }
+}
+
+impl<'a> quote::ToTokens for LifetimeGenericsListPartialDisplay<'a> {
+    fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
+        if let Lifetime::Anonymous = self.0 {
+        } else {
+            let lifetime = self.0.to_syn();
+            tokens.append_all(quote::quote!(#lifetime,))
+        }
+    }
+}
+
+impl fmt::Display for PathType {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        self.path.fmt(f)?;
+
+        if let Some((first, rest)) = self.lifetimes.split_first() {
+            write!(f, "<{first}")?;
+            for lifetime in rest {
+                write!(f, ", {lifetime}")?;
+            }
+            '>'.fmt(f)?;
+        }
+        Ok(())
+    }
+}
+
+/// A built-in Rust primitive scalar type.
+#[derive(Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Debug)]
+#[allow(non_camel_case_types)]
+#[allow(clippy::exhaustive_enums)] // there are only these (scalar types)
+pub enum PrimitiveType {
+    i8,
+    u8,
+    i16,
+    u16,
+    i32,
+    u32,
+    i64,
+    u64,
+    i128,
+    u128,
+    isize,
+    usize,
+    f32,
+    f64,
+    bool,
+    char,
+    /// a primitive byte that is not meant to be interpreted numerically
+    /// in languages that don't have fine-grained integer types
+    byte,
+}
+
+impl PrimitiveType {
+    fn as_code_str(self) -> &'static str {
+        match self {
+            PrimitiveType::i8 => "i8",
+            PrimitiveType::u8 => "u8",
+            PrimitiveType::i16 => "i16",
+            PrimitiveType::u16 => "u16",
+            PrimitiveType::i32 => "i32",
+            PrimitiveType::u32 => "u32",
+            PrimitiveType::i64 => "i64",
+            PrimitiveType::u64 => "u64",
+            PrimitiveType::i128 => "i128",
+            PrimitiveType::u128 => "u128",
+            PrimitiveType::isize => "isize",
+            PrimitiveType::usize => "usize",
+            PrimitiveType::f32 => "f32",
+            PrimitiveType::f64 => "f64",
+            PrimitiveType::bool => "bool",
+            PrimitiveType::char => "DiplomatChar",
+            PrimitiveType::byte => "DiplomatByte",
+        }
+    }
+
+    fn to_ident(self) -> proc_macro2::Ident {
+        proc_macro2::Ident::new(self.as_code_str(), Span::call_site())
+    }
+
+    /// Get the type for a slice of this, as specified using Rust stdlib types
+    pub fn get_stdlib_slice_type(self, lt: &Option<(Lifetime, Mutability)>) -> syn::Type {
+        let primitive = self.to_ident();
+
+        if let Some((ref lt, ref mtbl)) = lt {
+            let reference = ReferenceDisplay(lt, mtbl);
+            syn::parse_quote_spanned!(Span::call_site() => #reference [#primitive])
+        } else {
+            syn::parse_quote_spanned!(Span::call_site() => Box<[#primitive]>)
+        }
+    }
+
+    /// Get the type for a slice of this, as specified using Diplomat runtime types
+    pub fn get_diplomat_slice_type(self, lt: &Option<(Lifetime, Mutability)>) -> syn::Type {
+        let primitive = self.to_ident();
+
+        if let Some((lt, mtbl)) = lt {
+            let lifetime = LifetimeGenericsListPartialDisplay(lt);
+
+            if *mtbl == Mutability::Immutable {
+                syn::parse_quote_spanned!(Span::call_site() => diplomat_runtime::DiplomatSlice<#lifetime #primitive>)
+            } else {
+                syn::parse_quote_spanned!(Span::call_site() => diplomat_runtime::DiplomatSliceMut<#lifetime #primitive>)
+            }
+        } else {
+            syn::parse_quote_spanned!(Span::call_site() => diplomat_runtime::DiplomatOwnedSlice<#primitive>)
+        }
+    }
+}
+
+impl fmt::Display for PrimitiveType {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        match self {
+            PrimitiveType::byte => "u8",
+            PrimitiveType::char => "char",
+            _ => self.as_code_str(),
+        }
+        .fmt(f)
+    }
+}
+
+impl FromStr for PrimitiveType {
+    type Err = ();
+    fn from_str(s: &str) -> Result<Self, ()> {
+        Ok(match s {
+            "i8" => PrimitiveType::i8,
+            "u8" => PrimitiveType::u8,
+            "i16" => PrimitiveType::i16,
+            "u16" => PrimitiveType::u16,
+            "i32" => PrimitiveType::i32,
+            "u32" => PrimitiveType::u32,
+            "i64" => PrimitiveType::i64,
+            "u64" => PrimitiveType::u64,
+            "i128" => PrimitiveType::i128,
+            "u128" => PrimitiveType::u128,
+            "isize" => PrimitiveType::isize,
+            "usize" => PrimitiveType::usize,
+            "f32" => PrimitiveType::f32,
+            "f64" => PrimitiveType::f64,
+            "bool" => PrimitiveType::bool,
+            "DiplomatChar" => PrimitiveType::char,
+            "DiplomatByte" => PrimitiveType::byte,
+            _ => return Err(()),
+        })
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use insta;
+
+    use syn;
+
+    use super::TypeName;
+
+    #[test]
+    fn typename_primitives() {
+        insta::assert_yaml_snapshot!(TypeName::from_syn(
+            &syn::parse_quote! {
+                i32
+            },
+            None
+        ));
+
+        insta::assert_yaml_snapshot!(TypeName::from_syn(
+            &syn::parse_quote! {
+                usize
+            },
+            None
+        ));
+
+        insta::assert_yaml_snapshot!(TypeName::from_syn(
+            &syn::parse_quote! {
+                bool
+            },
+            None
+        ));
+    }
+
+    #[test]
+    fn typename_named() {
+        insta::assert_yaml_snapshot!(TypeName::from_syn(
+            &syn::parse_quote! {
+                MyLocalStruct
+            },
+            None
+        ));
+    }
+
+    #[test]
+    fn typename_references() {
+        insta::assert_yaml_snapshot!(TypeName::from_syn(
+            &syn::parse_quote! {
+                &i32
+            },
+            None
+        ));
+
+        insta::assert_yaml_snapshot!(TypeName::from_syn(
+            &syn::parse_quote! {
+                &mut MyLocalStruct
+            },
+            None
+        ));
+    }
+
+    #[test]
+    fn typename_boxes() {
+        insta::assert_yaml_snapshot!(TypeName::from_syn(
+            &syn::parse_quote! {
+                Box<i32>
+            },
+            None
+        ));
+
+        insta::assert_yaml_snapshot!(TypeName::from_syn(
+            &syn::parse_quote! {
+                Box<MyLocalStruct>
+            },
+            None
+        ));
+    }
+
+    #[test]
+    fn typename_option() {
+        insta::assert_yaml_snapshot!(TypeName::from_syn(
+            &syn::parse_quote! {
+                Option<i32>
+            },
+            None
+        ));
+
+        insta::assert_yaml_snapshot!(TypeName::from_syn(
+            &syn::parse_quote! {
+                Option<MyLocalStruct>
+            },
+            None
+        ));
+    }
+
+    #[test]
+    fn typename_result() {
+        insta::assert_yaml_snapshot!(TypeName::from_syn(
+            &syn::parse_quote! {
+                DiplomatResult<MyLocalStruct, i32>
+            },
+            None
+        ));
+
+        insta::assert_yaml_snapshot!(TypeName::from_syn(
+            &syn::parse_quote! {
+                DiplomatResult<(), MyLocalStruct>
+            },
+            None
+        ));
+
+        insta::assert_yaml_snapshot!(TypeName::from_syn(
+            &syn::parse_quote! {
+                Result<MyLocalStruct, i32>
+            },
+            None
+        ));
+
+        insta::assert_yaml_snapshot!(TypeName::from_syn(
+            &syn::parse_quote! {
+                Result<(), MyLocalStruct>
+            },
+            None
+        ));
+    }
+
+    #[test]
+    fn lifetimes() {
+        insta::assert_yaml_snapshot!(TypeName::from_syn(
+            &syn::parse_quote! {
+                Foo<'a, 'b>
+            },
+            None
+        ));
+
+        insta::assert_yaml_snapshot!(TypeName::from_syn(
+            &syn::parse_quote! {
+                ::core::my_type::Foo
+            },
+            None
+        ));
+
+        insta::assert_yaml_snapshot!(TypeName::from_syn(
+            &syn::parse_quote! {
+                ::core::my_type::Foo<'test>
+            },
+            None
+        ));
+
+        insta::assert_yaml_snapshot!(TypeName::from_syn(
+            &syn::parse_quote! {
+                Option<Ref<'object>>
+            },
+            None
+        ));
+
+        insta::assert_yaml_snapshot!(TypeName::from_syn(
+            &syn::parse_quote! {
+                Foo<'a, 'b, 'c, 'd>
+            },
+            None
+        ));
+
+        insta::assert_yaml_snapshot!(TypeName::from_syn(
+            &syn::parse_quote! {
+                very::long::path::to::my::Type<'x, 'y, 'z>
+            },
+            None
+        ));
+
+        insta::assert_yaml_snapshot!(TypeName::from_syn(
+            &syn::parse_quote! {
+                Result<OkRef<'a, 'b>, ErrRef<'c>>
+            },
+            None
+        ));
+    }
+}
diff --git a/crates/diplomat_core/src/environment.rs b/crates/diplomat_core/src/environment.rs
new file mode 100644
index 0000000..67b0f96
--- /dev/null
+++ b/crates/diplomat_core/src/environment.rs
@@ -0,0 +1,96 @@
+use crate::ast::*;
+use std::collections::BTreeMap;
+use std::ops::Index;
+
+/// The type resolution environment
+///
+/// Also contains the entire module structure
+#[derive(Default, Clone)]
+pub struct Env {
+    pub(crate) env: BTreeMap<Path, ModuleEnv>,
+}
+
+/// The type resolution environment within a specific module
+#[derive(Clone)]
+pub struct ModuleEnv {
+    pub(crate) module: BTreeMap<Ident, ModSymbol>,
+    #[cfg_attr(not(feature = "hir"), allow(unused))]
+    pub(crate) attrs: Attrs,
+}
+
+impl Env {
+    pub(crate) fn insert(&mut self, path: Path, module: ModuleEnv) {
+        self.env.insert(path, module);
+    }
+
+    /// Given a path to a module and a name, get the item, if any
+    pub fn get(&self, path: &Path, name: &str) -> Option<&ModSymbol> {
+        self.env.get(path).and_then(|m| m.module.get(name))
+    }
+
+    /// Iterate over all items in the environment
+    ///
+    /// This will occur in a stable lexically sorted order by path and then name
+    pub fn iter_items(&self) -> impl Iterator<Item = (&Path, &Ident, &ModSymbol)> + '_ {
+        self.env
+            .iter()
+            .flat_map(|(k, v)| v.module.iter().map(move |v2| (k, v2.0, v2.1)))
+    }
+
+    /// Iterate over all modules
+    ///
+    /// This will occur in a stable lexically sorted order by path
+    pub fn iter_modules(&self) -> impl Iterator<Item = (&Path, &ModuleEnv)> + '_ {
+        self.env.iter()
+    }
+}
+
+impl ModuleEnv {
+    pub(crate) fn new(attrs: Attrs) -> Self {
+        Self {
+            module: Default::default(),
+            attrs,
+        }
+    }
+    pub(crate) fn insert(&mut self, name: Ident, symbol: ModSymbol) -> Option<ModSymbol> {
+        self.module.insert(name, symbol)
+    }
+
+    /// Given an item name, fetch it
+    pub fn get(&self, name: &str) -> Option<&ModSymbol> {
+        self.module.get(name)
+    }
+
+    /// Iterate over all name-item pairs in this module
+    pub fn iter(&self) -> impl Iterator<Item = (&Ident, &ModSymbol)> + '_ {
+        self.module.iter()
+    }
+
+    /// Iterate over all names in this module
+    ///
+    /// This will occur in a stable lexically sorted order by name
+    pub fn names(&self) -> impl Iterator<Item = &Ident> + '_ {
+        self.module.keys()
+    }
+
+    /// Iterate over all items in this module
+    ///
+    /// This will occur in a stable lexically sorted order by name
+    pub fn items(&self) -> impl Iterator<Item = &ModSymbol> + '_ {
+        self.module.values()
+    }
+}
+
+impl Index<&Path> for Env {
+    type Output = ModuleEnv;
+    fn index(&self, i: &Path) -> &ModuleEnv {
+        &self.env[i]
+    }
+}
+
+impl Index<&str> for ModuleEnv {
+    type Output = ModSymbol;
+    fn index(&self, i: &str) -> &ModSymbol {
+        &self.module[i]
+    }
+}
diff --git a/crates/diplomat_core/src/hir/attrs.rs b/crates/diplomat_core/src/hir/attrs.rs
new file mode 100644
index 0000000..c7c2261
--- /dev/null
+++ b/crates/diplomat_core/src/hir/attrs.rs
@@ -0,0 +1,1270 @@
+//! #[diplomat::attr] and other attributes
+
+use crate::ast;
+use crate::ast::attrs::{AttrInheritContext, DiplomatBackendAttrCfg, StandardAttribute};
+use crate::hir::lowering::ErrorStore;
+use crate::hir::{
+    EnumVariant, LoweringError, Method, Mutability, OpaqueId, ReturnType, SelfType, SuccessType,
+    TraitDef, Type, TypeDef, TypeId,
+};
+use syn::Meta;
+
+pub use crate::ast::attrs::RenameAttr;
+
+/// Diplomat attribute that can be specified on items, methods, and enum variants. These
+/// can be used to control the codegen in a particular backend.
+///
+/// Most of these are specified via `#[diplomat::attr(some cfg here, attrname)]`, where `some cfg here`
+/// can be used to pick which backends something applies to.
+#[non_exhaustive]
+#[derive(Clone, Default, Debug)]
+pub struct Attrs {
+    /// "disable" this item: do not generate code for it in the backend
+    ///
+    /// This attribute is always inherited except to variants
+    pub disable: bool,
+    /// An optional namespace. None is equivalent to the root namespace.
+    ///
+    /// This attribute is inherited to types (and is not allowed elsewhere)
+    pub namespace: Option<String>,
+    /// Rename this item/method/variant
+    ///
+    /// This attribute is inherited except through methods and variants (and is not allowed on variants)
+    pub rename: RenameAttr,
+    /// Rename this item in the C ABI. This *must* be respected by backends.
+    ///
+    /// This attribute is inherited except through variants
+    pub abi_rename: RenameAttr,
+    /// This method is "special": it should generate something other than a regular method on the other side.
+    /// This can be something like a constructor, an accessor, a stringifier etc.
+    ///
+    /// This attribute does not participate in inheritance and must always
+    /// be specified on individual methods
+    pub special_method: Option<SpecialMethod>,
+
+    /// From #[diplomat::demo()]. Created from [`crate::ast::attrs::Attrs::demo_attrs`].
+    /// List of attributes specific to automatic demo generation.
+    /// Currently just for demo_gen in diplomat-tool (which generates sample webpages), but could be used for broader purposes (i.e., demo Android apps)
+    pub demo_attrs: DemoInfo,
+}
+
+// #region: Demo specific attributes.
+
+/// For `#[diplomat::demo(input(...))]`, stored in [DemoInfo::input_cfg].
+#[non_exhaustive]
+#[derive(Clone, Default, Debug)]
+pub struct DemoInputCFG {
+    /// `#[diplomat::demo(input(label = "..."))]`
+    /// Label that this input parameter should have. Let demo_gen pick a valid name if this is empty.
+    ///
+    /// For instance <label for="v">Number Here</label><input name="v"/>
+    pub label: String,
+
+    /// `#[diplomat::demo(input(default_value = "..."))]`
+    /// Sets the default value for a parameter.
+    ///
+    /// Should ALWAYS be a string. The HTML renderer is expected to do validation for us.
+    pub default_value: String,
+}
+
+#[non_exhaustive]
+#[derive(Clone, Default, Debug)]
+pub struct DemoInfo {
+    /// `#[diplomat::demo(generate)]`. If automatic generation is disabled by default (see [`diplomat_tool::demo_gen::DemoConfig`]), then the below render terminus will be allowed to generate.
+    pub generate: bool,
+
+    /// `#[diplomat::demo(default_constructor)]`
+    /// We search for any methods specially tagged with `Constructor`, but if there's are no default Constructors and there's NamedConstructor that you want to be default instead, use this.
+    /// TODO: Should probably ignore other `Constructors` if a default has been set.
+    pub default_constructor: bool,
+
+    /// `#[diplomat::demo(external)]` represents an item that we will not evaluate, and should be passed to the rendering engine to provide.
+    pub external: bool,
+
+    /// `#[diplomat::demo(custom_func = "/file/name/here.mjs")]` can be used above any `struct` definition in the bridge. The linked `.mjs` should contain a JS definition of functions that should be bundled with demo_gen's output.
+    ///
+    /// We call these functions "custom functions", as they are JS functions that are not automagically generated by demo_gen, but rather included as part of its JS output in the `RenderInfo` object.
+    ///
+    /// For more information on custom functions (and their use), see the relevant chapter in [the book](https://rust-diplomat.github.io/book/demo_gen/custom_functions.html).
+    ///
+    /// Files are located relative to lib.rs.
+    ///
+    pub custom_func: Option<String>,
+
+    /// `#[diplomat::demo(input(...))]` represents configuration options for anywhere we might expect user input.
+    pub input_cfg: DemoInputCFG,
+}
+
+// #endregion
+
+/// Attributes that mark methods as "special"
+#[non_exhaustive]
+#[derive(Clone, Debug)]
+pub enum SpecialMethod {
+    /// A constructor.
+    ///
+    /// Must return Self (or Result<Self> for backends with `fallible_constructors` enabled )
+    Constructor,
+    /// A named constructor, with optional name. If the name isn't specified, it will be derived
+    /// from the method name
+    ///
+    /// Must return Self (or Result<Self> for backends with `fallible_constructors` enabled )
+    NamedConstructor(Option<String>),
+
+    /// A getter, with optional name. If the name isn't specified, it will be derived
+    /// from the method name
+    ///
+    /// Must have no parameters and must return something.
+    Getter(Option<String>),
+    /// A setter, with optional name. If the name isn't specified, it will be derived
+    /// from the method name
+    ///
+    /// Must have no return type (aside from potentially a `Result<(), _>`) and must have one parameter
+    Setter(Option<String>),
+    /// A stringifier. Must have no parameters and return a string (DiplomatWrite)
+    Stringifier,
+    /// A comparison operator. Currently unsupported
+    Comparison,
+    /// An iterator (a type that is mutated to produce new values)
+    Iterator,
+    /// An iterable (a type that can produce an iterator)
+    Iterable,
+    /// Indexes into the type using an integer
+    Indexer,
+}
+
+/// For special methods that affect type semantics, whether this type has this method.
+///
+/// This will likely only contain a subset of special methods, but feel free to add more as needed.
+#[derive(Debug, Default)]
+#[non_exhaustive]
+pub struct SpecialMethodPresence {
+    pub comparator: bool,
+    /// If it is an iterator, the type it iterates over
+    pub iterator: Option<SuccessType>,
+    /// If it is an iterable, the iterator type it returns (*not* the type it iterates over,
+    /// perform lookup on that type to access)
+    pub iterable: Option<OpaqueId>,
+}
+
+/// Where the attribute was found. Some attributes are only allowed in some contexts
+/// (e.g. namespaces cannot be specified on methods)
+#[non_exhaustive] // might add module attrs in the future
+#[derive(Debug)]
+pub enum AttributeContext<'a, 'b> {
+    Type(TypeDef<'a>),
+    Trait(&'a TraitDef),
+    EnumVariant(&'a EnumVariant),
+    Method(&'a Method, TypeId, &'b mut SpecialMethodPresence),
+    Module,
+    Param,
+    SelfParam,
+    Field,
+}
+
+fn maybe_error_unsupported(
+    auto_found: bool,
+    attribute: &str,
+    backend: &str,
+    errors: &mut ErrorStore,
+) {
+    if !auto_found {
+        errors.push(LoweringError::Other(format!(
+            "`{attribute}` not supported in backend {backend}"
+        )));
+    }
+}
+impl Attrs {
+    pub fn from_ast(
+        ast: &ast::Attrs,
+        validator: &(impl AttributeValidator + ?Sized),
+        parent_attrs: &Attrs,
+        errors: &mut ErrorStore,
+    ) -> Self {
+        let mut this = parent_attrs.clone();
+        // Backends must support this since it applies to the macro/C code.
+        // No special inheritance, was already appropriately inherited in AST
+        this.abi_rename = ast.abi_rename.clone();
+
+        let support = validator.attrs_supported();
+        let backend = validator.primary_name();
+        for attr in &ast.attrs {
+            let mut auto_found = false;
+            let mut auto_used = false;
+            let satisfies = match validator.satisfies_cfg(&attr.cfg, Some(&mut auto_found)) {
+                Ok(satisfies) => satisfies,
+                Err(e) => {
+                    errors.push(e);
+                    continue;
+                }
+            };
+            if satisfies {
+                let path = attr.meta.path();
+                if let Some(path) = path.get_ident() {
+                    if path == "disable" {
+                        if let Meta::Path(_) = attr.meta {
+                            if this.disable {
+                                errors.push(LoweringError::Other(
+                                    "Duplicate `disable` attribute".into(),
+                                ));
+                            } else {
+                                this.disable = true;
+                            }
+                        } else {
+                            errors.push(LoweringError::Other(
+                                "`disable` must be a simple path".into(),
+                            ))
+                        }
+                    } else if path == "rename" {
+                        match RenameAttr::from_meta(&attr.meta) {
+                            Ok(rename) => {
+                                // We use the override extend mode: a single ast::Attrs
+                                // will have had these attributes inherited into the list by appending
+                                // to the end; so a later attribute in the list is more pertinent.
+                                this.rename.extend(&rename);
+                            }
+                            Err(e) => errors.push(LoweringError::Other(format!(
+                                "`rename` attr failed to parse: {e:?}"
+                            ))),
+                        }
+                    } else if path == "namespace" {
+                        if !support.namespacing {
+                            maybe_error_unsupported(auto_found, "constructor", backend, errors);
+                            continue;
+                        }
+                        auto_used = true;
+                        match StandardAttribute::from_meta(&attr.meta) {
+                            Ok(StandardAttribute::String(s)) if s.is_empty() => {
+                                this.namespace = None
+                            }
+                            Ok(StandardAttribute::String(s)) => this.namespace = Some(s),
+                            Ok(_) | Err(_) => {
+                                errors.push(LoweringError::Other(
+                                    "`namespace` must have a single string parameter".to_string(),
+                                ));
+                                continue;
+                            }
+                        }
+                    } else if path == "constructor"
+                        || path == "stringifier"
+                        || path == "comparison"
+                        || path == "iterable"
+                        || path == "iterator"
+                        || path == "indexer"
+                    {
+                        if let Some(ref existing) = this.special_method {
+                            errors.push(LoweringError::Other(format!(
+                            "Multiple special method markers found on the same method, found {path} and {existing:?}"
+                        )));
+                            continue;
+                        }
+                        let kind = if path == "constructor" {
+                            if !support.constructors {
+                                maybe_error_unsupported(auto_found, "constructor", backend, errors);
+                                continue;
+                            }
+                            auto_used = true;
+                            SpecialMethod::Constructor
+                        } else if path == "stringifier" {
+                            if !support.stringifiers {
+                                maybe_error_unsupported(auto_found, "stringifier", backend, errors);
+                                continue;
+                            }
+                            auto_used = true;
+                            SpecialMethod::Stringifier
+                        } else if path == "iterable" {
+                            if !support.iterables {
+                                maybe_error_unsupported(auto_found, "iterable", backend, errors);
+                                continue;
+                            }
+                            auto_used = true;
+                            SpecialMethod::Iterable
+                        } else if path == "iterator" {
+                            if !support.iterators {
+                                maybe_error_unsupported(auto_found, "iterator", backend, errors);
+                                continue;
+                            }
+                            auto_used = true;
+                            SpecialMethod::Iterator
+                        } else if path == "indexer" {
+                            if !support.indexing {
+                                maybe_error_unsupported(auto_found, "indexer", backend, errors);
+                                continue;
+                            }
+                            auto_used = true;
+                            SpecialMethod::Indexer
+                        } else {
+                            if !support.comparators {
+                                maybe_error_unsupported(auto_found, "comparator", backend, errors);
+                                continue;
+                            }
+                            auto_used = true;
+                            SpecialMethod::Comparison
+                        };
+
+                        this.special_method = Some(kind);
+                    } else if path == "named_constructor" || path == "getter" || path == "setter" {
+                        if let Some(ref existing) = this.special_method {
+                            errors.push(LoweringError::Other(format!(
+                            "Multiple special method markers found on the same method, found {path} and {existing:?}"
+                        )));
+                            continue;
+                        }
+                        let kind = if path == "named_constructor" {
+                            if !support.named_constructors {
+                                maybe_error_unsupported(
+                                    auto_found,
+                                    "named_constructors",
+                                    backend,
+                                    errors,
+                                );
+                                continue;
+                            }
+                            auto_used = true;
+                            SpecialMethod::NamedConstructor
+                        } else if path == "getter" {
+                            if !support.accessors {
+                                maybe_error_unsupported(auto_found, "accessors", backend, errors);
+                                continue;
+                            }
+                            auto_used = true;
+                            SpecialMethod::Getter
+                        } else {
+                            if !support.accessors {
+                                maybe_error_unsupported(auto_found, "accessors", backend, errors);
+                                continue;
+                            }
+                            auto_used = true;
+                            SpecialMethod::Setter
+                        };
+                        match StandardAttribute::from_meta(&attr.meta) {
+                            Ok(StandardAttribute::String(s)) => {
+                                this.special_method = Some(kind(Some(s)))
+                            }
+                            Ok(StandardAttribute::Empty) => this.special_method = Some(kind(None)),
+                            Ok(_) | Err(_) => {
+                                errors.push(LoweringError::Other(format!(
+                                    "`{path}` must have a single string parameter or no parameter",
+                                )));
+                                continue;
+                            }
+                        }
+                    } else {
+                        errors.push(LoweringError::Other(format!(
+                            "Unknown diplomat attribute {path}: expected one of: `disable, rename, namespace, constructor, stringifier, comparison, named_constructor, getter, setter, indexer`"
+                        )));
+                    }
+                    if auto_found && !auto_used {
+                        errors.push(LoweringError::Other(format!(
+                            "Diplomat attribute {path} gated on 'auto' but is not one that works with 'auto'"
+                        )));
+                    }
+                } else {
+                    errors.push(LoweringError::Other(format!(
+                        "Unknown diplomat attribute {path:?}: expected one of: `disable, rename, namespace, constructor, stringifier, comparison, named_constructor, getter, setter, indexer`"
+                    )));
+                }
+            }
+        }
+
+        for attr in &ast.demo_attrs {
+            let path = attr.meta.path();
+            if let Some(path_ident) = path.get_ident() {
+                if path_ident == "external" {
+                    this.demo_attrs.external = true;
+                } else if path_ident == "default_constructor" {
+                    this.demo_attrs.default_constructor = true;
+                } else if path_ident == "generate" {
+                    this.demo_attrs.generate = true;
+                } else if path_ident == "input" {
+                    let meta_list = attr
+                        .meta
+                        .require_list()
+                        .expect("Could not get MetaList, expected #[diplomat::demo(input(...))]");
+
+                    meta_list
+                        .parse_nested_meta(|meta| {
+                            if meta.path.is_ident("label") {
+                                let value = meta.value()?;
+                                let s: syn::LitStr = value.parse()?;
+                                this.demo_attrs.input_cfg.label = s.value();
+                                Ok(())
+                            } else if meta.path.is_ident("default_value") {
+                                let value = meta.value()?;
+
+                                let str_val: String;
+
+                                let ahead = value.lookahead1();
+                                if ahead.peek(syn::LitFloat) {
+                                    let s: syn::LitFloat = value.parse()?;
+                                    str_val = s.base10_parse::<f64>()?.to_string();
+                                } else if ahead.peek(syn::LitInt) {
+                                    let s: syn::LitInt = value.parse()?;
+                                    str_val = s.base10_parse::<i64>()?.to_string();
+                                } else {
+                                    let s: syn::LitStr = value.parse()?;
+                                    str_val = s.value();
+                                }
+                                this.demo_attrs.input_cfg.default_value = str_val;
+                                Ok(())
+                            } else {
+                                Err(meta.error(format!(
+                                    "Unsupported ident {:?}",
+                                    meta.path.get_ident()
+                                )))
+                            }
+                        })
+                        .expect("Could not read input(...)");
+                } else if path_ident == "custom_func" {
+                    let v = &attr.meta.require_name_value().unwrap().value;
+
+                    if let syn::Expr::Lit(s) = v {
+                        if let syn::Lit::Str(string) = &s.lit {
+                            this.demo_attrs.custom_func = Some(string.value());
+                        } else {
+                            errors.push(LoweringError::Other(format!(
+                                "#[diplomat::demo(custom_func={s:?}) must be a literal string."
+                            )));
+                        }
+                    } else {
+                        errors.push(LoweringError::Other(format!(
+                            "#[diplomat::demo(custom_func={v:?}) must be a literal string."
+                        )));
+                    }
+                } else {
+                    errors.push(LoweringError::Other(format!(
+                        "Unknown demo_attr: {path_ident:?}"
+                    )));
+                }
+            } else {
+                errors.push(LoweringError::Other(format!("Unknown demo_attr: {path:?}")));
+            }
+        }
+
+        this
+    }
+
+    /// Validate that this attribute is allowed in this context
+    pub(crate) fn validate(
+        &self,
+        validator: &(impl AttributeValidator + ?Sized),
+        mut context: AttributeContext,
+        errors: &mut ErrorStore,
+    ) {
+        // use an exhaustive destructure so new attributes are handled
+        let Attrs {
+            disable,
+            namespace,
+            rename,
+            abi_rename,
+            special_method,
+            demo_attrs: _,
+        } = &self;
+
+        if *disable && matches!(context, AttributeContext::EnumVariant(..)) {
+            errors.push(LoweringError::Other(
+                "`disable` cannot be used on enum variants".into(),
+            ))
+        }
+
+        if let Some(ref special) = special_method {
+            if let AttributeContext::Method(method, self_id, ref mut special_method_presence) =
+                context
+            {
+                match special {
+                    SpecialMethod::Constructor | SpecialMethod::NamedConstructor(..) => {
+                        if method.param_self.is_some() {
+                            errors.push(LoweringError::Other(
+                                "Constructors must not accept a self parameter".to_string(),
+                            ))
+                        }
+                        let output = method.output.success_type();
+                        match method.output {
+                            ReturnType::Infallible(_) => (),
+                            ReturnType::Fallible(..) => {
+                                if !validator.attrs_supported().fallible_constructors {
+                                    errors.push(LoweringError::Other(
+                                        "This backend doesn't support fallible constructors"
+                                            .to_string(),
+                                    ))
+                                }
+                            }
+                            ReturnType::Nullable(..) => {
+                                errors.push(LoweringError::Other("Diplomat doesn't support turning nullable methods into constructors".to_string()));
+                            }
+                        }
+
+                        if let SuccessType::OutType(t) = &output {
+                            if t.id() != Some(self_id) {
+                                errors.push(LoweringError::Other(
+                                    "Constructors must return Self!".to_string(),
+                                ));
+                            }
+                        } else {
+                            errors.push(LoweringError::Other(
+                                "Constructors must return Self!".to_string(),
+                            ));
+                        }
+                    }
+                    SpecialMethod::Getter(_) => {
+                        if !method.params.is_empty() {
+                            errors
+                                .push(LoweringError::Other("Getter cannot have parameters".into()));
+                        }
+
+                        // Currently does not forbid nullable getters, could if desired
+                    }
+
+                    SpecialMethod::Setter(_) => {
+                        if !matches!(method.output.success_type(), SuccessType::Unit) {
+                            errors.push(LoweringError::Other("Setters must return unit".into()));
+                        }
+                        if method.params.len() != 1 {
+                            errors.push(LoweringError::Other(
+                                "Setter must have exactly one parameter".into(),
+                            ))
+                        }
+
+                        // Currently does not forbid fallible setters, could if desired
+                    }
+                    SpecialMethod::Stringifier => {
+                        if !method.params.is_empty() {
+                            errors
+                                .push(LoweringError::Other("Getter cannot have parameters".into()));
+                        }
+                        if !matches!(method.output.success_type(), SuccessType::Write) {
+                            errors.push(LoweringError::Other(
+                                "Stringifier must return string".into(),
+                            ));
+                        }
+                    }
+                    SpecialMethod::Comparison => {
+                        if method.params.len() != 1 {
+                            errors.push(LoweringError::Other(
+                                "Comparator must have single parameter".into(),
+                            ));
+                        }
+                        if special_method_presence.comparator {
+                            errors.push(LoweringError::Other(
+                                "Cannot define two comparators on the same type".into(),
+                            ));
+                        }
+                        special_method_presence.comparator = true;
+                        // In the long run we can actually support heterogeneous comparators. Not a priority right now.
+                        const COMPARATOR_ERROR: &str =
+                            "Comparator's parameter must be identical to self";
+                        if let Some(ref selfty) = method.param_self {
+                            if let Some(param) = method.params.first() {
+                                match (&selfty.ty, &param.ty) {
+                                    (SelfType::Opaque(p), Type::Opaque(p2)) => {
+                                        if p.tcx_id != p2.tcx_id {
+                                            errors.push(LoweringError::Other(
+                                                COMPARATOR_ERROR.into(),
+                                            ));
+                                        }
+
+                                        if p.owner.mutability != Mutability::Immutable
+                                            || p2.owner.mutability != Mutability::Immutable
+                                        {
+                                            errors.push(LoweringError::Other(
+                                                "comparators must accept immutable parameters"
+                                                    .into(),
+                                            ));
+                                        }
+
+                                        if p2.optional.0 {
+                                            errors.push(LoweringError::Other(
+                                                "comparators must accept non-optional parameters"
+                                                    .into(),
+                                            ));
+                                        }
+                                    }
+                                    (SelfType::Struct(p), Type::Struct(p2)) => {
+                                        if p.tcx_id != p2.tcx_id {
+                                            errors.push(LoweringError::Other(
+                                                COMPARATOR_ERROR.into(),
+                                            ));
+                                        }
+                                    }
+                                    (SelfType::Enum(p), Type::Enum(p2)) => {
+                                        if p.tcx_id != p2.tcx_id {
+                                            errors.push(LoweringError::Other(
+                                                COMPARATOR_ERROR.into(),
+                                            ));
+                                        }
+                                    }
+                                    _ => {
+                                        errors.push(LoweringError::Other(COMPARATOR_ERROR.into()));
+                                    }
+                                }
+                            }
+                        } else {
+                            errors
+                                .push(LoweringError::Other("Comparator must be non-static".into()));
+                        }
+                    }
+                    SpecialMethod::Iterator => {
+                        if special_method_presence.iterator.is_some() {
+                            errors.push(LoweringError::Other(
+                                "Cannot mark type as iterator twice".into(),
+                            ));
+                        }
+                        if !method.params.is_empty() {
+                            errors.push(LoweringError::Other(
+                                "Iterators cannot take parameters".into(),
+                            ))
+                        }
+                        // In theory we could support struct and enum iterators. The benefit is slight:
+                        // it generates probably inefficient code whilst being rather weird when it comes to the
+                        // "structs and enums convert across the boundary" norm for backends.
+                        //
+                        // Essentially, the `&mut self` behavior won't work right.
+                        //
+                        // Furthermore, in some backends (like Dart) defining an iterator may requiring adding fields,
+                        // which may not be possible for enums, and would still be an odd-one-out field for structs.g s
+                        if let Some(this) = &method.param_self {
+                            if !matches!(this.ty, SelfType::Opaque(..)) {
+                                errors.push(LoweringError::Other(
+                                    "Iterators only allowed on opaques".into(),
+                                ))
+                            }
+                        } else {
+                            errors.push(LoweringError::Other("Iterators must take self".into()))
+                        }
+
+                        if let ReturnType::Nullable(ref o) = method.output {
+                            if let SuccessType::Unit = o {
+                                errors.push(LoweringError::Other(
+                                    "Iterator method must return something".into(),
+                                ));
+                            }
+                            special_method_presence.iterator = Some(o.clone());
+                        } else if let ReturnType::Infallible(SuccessType::OutType(
+                            crate::hir::OutType::Opaque(
+                                ref o @ crate::hir::OpaquePath {
+                                    optional: crate::hir::Optional(true),
+                                    ..
+                                },
+                            ),
+                        )) = method.output
+                        {
+                            let mut o = o.clone();
+                            o.optional = crate::hir::Optional(false);
+
+                            special_method_presence.iterator =
+                                Some(SuccessType::OutType(crate::hir::OutType::Opaque(o)));
+                        } else {
+                            errors.push(LoweringError::Other(
+                                "Iterator method must return nullable value".into(),
+                            ));
+                        }
+                    }
+                    SpecialMethod::Iterable => {
+                        if special_method_presence.iterable.is_some() {
+                            errors.push(LoweringError::Other(
+                                "Cannot mark type as iterable twice".into(),
+                            ));
+                        }
+                        if !method.params.is_empty() {
+                            errors.push(LoweringError::Other(
+                                "Iterables cannot take parameters".into(),
+                            ))
+                        }
+                        if method.param_self.is_none() {
+                            errors.push(LoweringError::Other("Iterables must take self".into()))
+                        }
+
+                        match method.output.success_type() {
+                            SuccessType::OutType(ty) => {
+                                if let Some(TypeId::Opaque(id)) = ty.id() {
+                                    special_method_presence.iterable = Some(id);
+                                } else {
+                                    errors.push(LoweringError::Other(
+                                        "Iterables must return a custom opaque type".into(),
+                                    ))
+                                }
+                            }
+                            _ => errors.push(LoweringError::Other(
+                                "Iterables must return a custom type".into(),
+                            )),
+                        }
+                    }
+                    SpecialMethod::Indexer => {
+                        if method.params.len() != 1 {
+                            errors.push(LoweringError::Other(
+                                "Indexer must have exactly one parameter".into(),
+                            ));
+                        }
+
+                        if method.output.success_type().is_unit() {
+                            errors.push(LoweringError::Other("Indexer must return a value".into()));
+                        }
+                    }
+                }
+            } else {
+                errors.push(LoweringError::Other(format!("Special method (type {special:?}) not allowed on non-method context {context:?}")))
+            }
+        }
+
+        if namespace.is_some()
+            && matches!(
+                context,
+                AttributeContext::Method(..) | AttributeContext::EnumVariant(..)
+            )
+        {
+            errors.push(LoweringError::Other(
+                "`namespace` can only be used on types".to_string(),
+            ));
+        }
+
+        if matches!(
+            context,
+            AttributeContext::Param | AttributeContext::SelfParam | AttributeContext::Field
+        ) {
+            if *disable {
+                errors.push(LoweringError::Other(format!(
+                    "`disable`s cannot be used on an {context:?}."
+                )));
+            }
+
+            if namespace.is_some() {
+                errors.push(LoweringError::Other(format!(
+                    "`namespace` cannot be used on an {context:?}."
+                )));
+            }
+
+            if !rename.is_empty() || !abi_rename.is_empty() {
+                errors.push(LoweringError::Other(format!(
+                    "`rename`s cannot be used on an {context:?}."
+                )));
+            }
+
+            if special_method.is_some() {
+                errors.push(LoweringError::Other(format!(
+                    "{context:?} cannot be special methods."
+                )));
+            }
+        }
+    }
+
+    pub(crate) fn for_inheritance(&self, context: AttrInheritContext) -> Attrs {
+        let rename = self.rename.attrs_for_inheritance(context, false);
+
+        // Disabling shouldn't inherit to variants
+        let disable = if context == AttrInheritContext::Variant {
+            false
+        } else {
+            self.disable
+        };
+        let namespace = if matches!(
+            context,
+            AttrInheritContext::Module | AttrInheritContext::Type
+        ) {
+            self.namespace.clone()
+        } else {
+            None
+        };
+
+        Attrs {
+            disable,
+            rename,
+            namespace,
+            // Was already inherited on the AST side
+            abi_rename: Default::default(),
+            // Never inherited
+            special_method: None,
+            demo_attrs: Default::default(),
+        }
+    }
+}
+
+/// Non-exhaustive list of what attributes and other features your backend is able to handle, based on #[diplomat::attr(...)] contents.
+/// Set this through an [`AttributeValidator`].
+///
+/// See [`SpecialMethod`] and [`Attrs`] for your specific implementation needs.
+///
+/// For example, the current dart backend supports [`BackendAttrSupport::constructors`]. So when it encounters:
+/// ```ignore
+/// struct Sample {}
+/// impl Sample {
+///     #[diplomat::attr(constructor)]
+///     pub fn new() -> Box<Self> {
+///         Box::new(Sample{})
+///     }
+/// }
+///
+/// ```
+///
+/// It generates
+/// ```dart
+/// factory Sample()
+/// ```
+///
+/// If a backend does not support a specific `#[diplomat::attr(...)]`, it may error.
+#[non_exhaustive]
+#[derive(Copy, Clone, Debug, Default)]
+pub struct BackendAttrSupport {
+    /// Namespacing types, e.g. C++ `namespace`.
+    pub namespacing: bool,
+    /// Rust can directly acccess the memory of this language, like C and C++.
+    /// This is not supported in any garbage-collected language.
+    pub memory_sharing: bool,
+    /// This language's structs are non-exhaustive by default, i.e. adding
+    /// fields is not a breaking change.
+    pub non_exhaustive_structs: bool,
+    /// Whether the language supports method overloading
+    pub method_overloading: bool,
+    /// Whether the language uses UTF-8 strings
+    pub utf8_strings: bool,
+    /// Whether the language uses UTF-16 strings
+    pub utf16_strings: bool,
+    /// Whether the language supports using slices with 'static lifetimes.
+    pub static_slices: bool,
+
+    // Special methods
+    /// Marking a method as a constructor to generate special constructor methods.
+    pub constructors: bool,
+    /// Marking a method as a named constructor to generate special named constructor methods.
+    pub named_constructors: bool,
+    /// Marking constructors as being able to return errors. This is possible in languages where
+    /// errors are thrown as exceptions (Dart), but not for example in C++, where errors are
+    /// returned as values (constructors usually have to return the type itself).
+    pub fallible_constructors: bool,
+    /// Marking methods as field getters and setters, see [`SpecialMethod::Getter`] and [`SpecialMethod::Setter`]
+    pub accessors: bool,
+    /// Marking a method as the `to_string` method, which is special in this language.
+    pub stringifiers: bool,
+    /// Marking a method as the `compare_to` method, which is special in this language.
+    pub comparators: bool,
+    /// Marking a method as the `next` method, which is special in this language.
+    pub iterators: bool,
+    /// Marking a method as the `iterator` method, which is special in this language.
+    pub iterables: bool,
+    /// Marking a method as the `[]` operator, which is special in this language.
+    pub indexing: bool,
+
+    /// Support for Option<Struct> and Option<Primitive>
+    pub option: bool,
+    /// Allowing callback arguments
+    pub callbacks: bool,
+    /// Allowing traits
+    pub traits: bool,
+}
+
+impl BackendAttrSupport {
+    #[cfg(test)]
+    fn all_true() -> Self {
+        Self {
+            namespacing: true,
+            memory_sharing: true,
+            non_exhaustive_structs: true,
+            method_overloading: true,
+            utf8_strings: true,
+            utf16_strings: true,
+            static_slices: true,
+
+            constructors: true,
+            named_constructors: true,
+            fallible_constructors: true,
+            accessors: true,
+            stringifiers: true,
+            comparators: true,
+            iterators: true,
+            iterables: true,
+            indexing: true,
+            option: true,
+            callbacks: true,
+            traits: true,
+        }
+    }
+}
+
+/// Defined by backends when validating attributes
+pub trait AttributeValidator {
+    /// The primary name of the backend, for use in diagnostics
+    fn primary_name(&self) -> &str;
+    /// Does this backend satisfy `cfg(backend_name)`?
+    /// (Backends are allowed to satisfy multiple backend names, useful when there
+    /// are multiple backends for a language)
+    fn is_backend(&self, backend_name: &str) -> bool;
+    /// does this backend satisfy cfg(name = value)?
+    fn is_name_value(&self, name: &str, value: &str) -> Result<bool, LoweringError>;
+    /// What backedn attrs does this support?
+    fn attrs_supported(&self) -> BackendAttrSupport;
+
+    /// Provided, checks if type satisfies a `DiplomatBackendAttrCfg`
+    ///
+    /// auto_found helps check for `auto`, which is only allowed within `any` and at the top level. When `None`,
+    /// `auto` is not allowed.
+    fn satisfies_cfg(
+        &self,
+        cfg: &DiplomatBackendAttrCfg,
+        mut auto_found: Option<&mut bool>,
+    ) -> Result<bool, LoweringError> {
+        Ok(match *cfg {
+            DiplomatBackendAttrCfg::Not(ref c) => !self.satisfies_cfg(c, None)?,
+            DiplomatBackendAttrCfg::Any(ref cs) => {
+                #[allow(clippy::needless_option_as_deref)]
+                // False positive: we need this for reborrowing
+                for c in cs {
+                    if self.satisfies_cfg(c, auto_found.as_deref_mut())? {
+                        return Ok(true);
+                    }
+                }
+                false
+            }
+            DiplomatBackendAttrCfg::All(ref cs) => {
+                for c in cs {
+                    if !self.satisfies_cfg(c, None)? {
+                        return Ok(false);
+                    }
+                }
+                true
+            }
+            DiplomatBackendAttrCfg::Auto => {
+                if let Some(found) = auto_found {
+                    *found = true;
+                    return Ok(true);
+                } else {
+                    return Err(LoweringError::Other("auto in diplomat::attr() is only allowed at the top level and within `any`".into()));
+                }
+            }
+            DiplomatBackendAttrCfg::Star => true,
+            DiplomatBackendAttrCfg::BackendName(ref n) => self.is_backend(n),
+            DiplomatBackendAttrCfg::NameValue(ref n, ref v) => self.is_name_value(n, v)?,
+        })
+    }
+
+    // Provided, constructs an attribute
+    fn attr_from_ast(
+        &self,
+        ast: &ast::Attrs,
+        parent_attrs: &Attrs,
+        errors: &mut ErrorStore,
+    ) -> Attrs {
+        Attrs::from_ast(ast, self, parent_attrs, errors)
+    }
+
+    // Provided: validates an attribute in the context in which it was constructed
+    fn validate(&self, attrs: &Attrs, context: AttributeContext, errors: &mut ErrorStore) {
+        attrs.validate(self, context, errors)
+    }
+}
+
+/// A basic attribute validator
+#[non_exhaustive]
+#[derive(Default)]
+pub struct BasicAttributeValidator {
+    /// The primary name of this backend (should be unique, ideally)
+    pub backend_name: String,
+    /// The attributes supported
+    pub support: BackendAttrSupport,
+    /// Additional names for this backend
+    pub other_backend_names: Vec<String>,
+    /// override is_name_value()
+    #[allow(clippy::type_complexity)] // dyn fn is not that complex
+    pub is_name_value: Option<Box<dyn Fn(&str, &str) -> bool>>,
+}
+
+impl BasicAttributeValidator {
+    pub fn new(backend_name: &str) -> Self {
+        BasicAttributeValidator {
+            backend_name: backend_name.into(),
+            ..Self::default()
+        }
+    }
+}
+
+impl AttributeValidator for BasicAttributeValidator {
+    fn primary_name(&self) -> &str {
+        &self.backend_name
+    }
+    fn is_backend(&self, backend_name: &str) -> bool {
+        self.backend_name == backend_name
+            || self.other_backend_names.iter().any(|n| n == backend_name)
+    }
+    fn is_name_value(&self, name: &str, value: &str) -> Result<bool, LoweringError> {
+        Ok(if name == "supports" {
+            // destructure so new fields are forced to be added
+            let BackendAttrSupport {
+                namespacing,
+                memory_sharing,
+                non_exhaustive_structs,
+                method_overloading,
+                utf8_strings,
+                utf16_strings,
+                static_slices,
+
+                constructors,
+                named_constructors,
+                fallible_constructors,
+                accessors,
+                stringifiers,
+                comparators,
+                iterators,
+                iterables,
+                indexing,
+                option,
+                callbacks,
+                traits,
+            } = self.support;
+            match value {
+                "namespacing" => namespacing,
+                "memory_sharing" => memory_sharing,
+                "non_exhaustive_structs" => non_exhaustive_structs,
+                "method_overloading" => method_overloading,
+                "utf8_strings" => utf8_strings,
+                "utf16_strings" => utf16_strings,
+                "static_slices" => static_slices,
+
+                "constructors" => constructors,
+                "named_constructors" => named_constructors,
+                "fallible_constructors" => fallible_constructors,
+                "accessors" => accessors,
+                "stringifiers" => stringifiers,
+                "comparators" => comparators,
+                "iterators" => iterators,
+                "iterables" => iterables,
+                "indexing" => indexing,
+                "option" => option,
+                "callbacks" => callbacks,
+                "traits" => traits,
+                _ => {
+                    return Err(LoweringError::Other(format!(
+                        "Unknown supports = value found: {value}"
+                    )))
+                }
+            }
+        } else if let Some(ref nv) = self.is_name_value {
+            nv(name, value)
+        } else {
+            false
+        })
+    }
+    fn attrs_supported(&self) -> BackendAttrSupport {
+        self.support
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use crate::hir;
+    use std::fmt::Write;
+
+    macro_rules! uitest_lowering_attr {
+        ($attrs:expr, $($file:tt)*) => {
+            let parsed: syn::File = syn::parse_quote! { $($file)* };
+
+            let mut output = String::new();
+
+            let mut attr_validator = hir::BasicAttributeValidator::new("tests");
+            attr_validator.support = $attrs;
+            match hir::TypeContext::from_syn(&parsed, attr_validator) {
+                Ok(_context) => (),
+                Err(e) => {
+                    for (ctx, err) in e {
+                        writeln!(&mut output, "Lowering error in {ctx}: {err}").unwrap();
+                    }
+                }
+            };
+            insta::with_settings!({}, {
+                insta::assert_snapshot!(output)
+            });
+        }
+    }
+
+    #[test]
+    fn test_auto() {
+        uitest_lowering_attr! { hir::BackendAttrSupport { comparators: true, ..Default::default()},
+            #[diplomat::bridge]
+            mod ffi {
+                use std::cmp;
+
+                #[diplomat::opaque]
+                #[diplomat::attr(auto, namespace = "should_not_show_up")]
+                struct Opaque;
+
+
+                impl Opaque {
+                    #[diplomat::attr(auto, comparison)]
+                    pub fn comparator_static(&self, other: &Opaque) -> cmp::Ordering {
+                        todo!()
+                    }
+                    #[diplomat::attr(*, iterator)]
+                    pub fn next(&mut self) -> Option<u8> {
+                        self.0.next()
+                    }
+                    #[diplomat::attr(auto, rename = "bar")]
+                    pub fn auto_doesnt_work_on_renames(&self) {
+                    }
+                    #[diplomat::attr(auto, disable)]
+                    pub fn auto_doesnt_work_on_disables(&self) {
+                    }
+                }
+
+            }
+        }
+    }
+
+    #[test]
+    fn test_comparator() {
+        uitest_lowering_attr! { hir::BackendAttrSupport::all_true(),
+            #[diplomat::bridge]
+            mod ffi {
+                use std::cmp;
+
+                #[diplomat::opaque]
+                struct Opaque;
+
+                struct Struct {
+                    field: u8
+                }
+
+
+                impl Opaque {
+                    #[diplomat::attr(auto, comparison)]
+                    pub fn comparator_static(other: &Opaque) -> cmp::Ordering {
+                        todo!()
+                    }
+                    #[diplomat::attr(auto, comparison)]
+                    pub fn comparator_none(&self) -> cmp::Ordering {
+                        todo!()
+                    }
+                    #[diplomat::attr(auto, comparison)]
+                    pub fn comparator_othertype(other: Struct) -> cmp::Ordering {
+                        todo!()
+                    }
+                    #[diplomat::attr(auto, comparison)]
+                    pub fn comparator_badreturn(&self, other: &Opaque) -> u8 {
+                        todo!()
+                    }
+                    #[diplomat::attr(auto, comparison)]
+                    pub fn comparison_correct(&self, other: &Opaque) -> cmp::Ordering {
+                        todo!()
+                    }
+                    pub fn comparison_unmarked(&self, other: &Opaque) -> cmp::Ordering {
+                        todo!()
+                    }
+                    pub fn ordering_wrong(&self, other: cmp::Ordering) {
+                        todo!()
+                    }
+                    #[diplomat::attr(auto, comparison)]
+                    pub fn comparison_mut(&self, other: &mut Opaque) -> cmp::Ordering {
+                        todo!()
+                    }
+                    #[diplomat::attr(auto, comparison)]
+                    pub fn comparison_opt(&self, other: Option<&Opaque>) -> cmp::Ordering {
+                        todo!()
+                    }
+                }
+
+                impl Struct {
+                    #[diplomat::attr(auto, comparison)]
+                    pub fn comparison_other(self, other: &Opaque) -> cmp::Ordering {
+                        todo!()
+                    }
+                    #[diplomat::attr(auto, comparison)]
+                    pub fn comparison_correct(self, other: Self) -> cmp::Ordering {
+                        todo!()
+                    }
+                }
+            }
+        }
+    }
+
+    #[test]
+    fn test_iterator() {
+        uitest_lowering_attr! { hir::BackendAttrSupport::all_true(),
+            #[diplomat::bridge]
+            mod ffi {
+
+                #[diplomat::opaque]
+                struct Opaque(Vec<u8>);
+                #[diplomat::opaque]
+                struct OpaqueIterator<'a>(std::slice::Iter<'a>);
+
+
+                impl Opaque {
+                    #[diplomat::attr(auto, iterable)]
+                    pub fn iterable<'a>(&'a self) -> Box<OpaqueIterator<'a>> {
+                        Box::new(OpaqueIterator(self.0.iter()))
+                    }
+                }
+
+                impl OpaqueIterator {
+                    #[diplomat::attr(auto, iterator)]
+                    pub fn next(&mut self) -> Option<u8> {
+                        self.0.next()
+                    }
+                }
+
+                #[diplomat::opaque]
+                struct Broken;
+
+                impl Broken {
+                    #[diplomat::attr(auto, iterable)]
+                    pub fn iterable_no_return(&self) {}
+                    #[diplomat::attr(auto, iterable)]
+                    pub fn iterable_no_self() -> Box<BrokenIterator> { todo!() }
+
+                    #[diplomat::attr(auto, iterable)]
+                    pub fn iterable_non_custom(&self) -> u8 { todo!() }
+                }
+
+                #[diplomat::opaque]
+                struct BrokenIterator;
+
+                impl BrokenIterator {
+                    #[diplomat::attr(auto, iterator)]
+                    pub fn iterator_no_return(&self) {}
+                    #[diplomat::attr(auto, iterator)]
+                    pub fn iterator_no_self() -> Option<u8> { todo!() }
+
+                    #[diplomat::attr(auto, iterator)]
+                    pub fn iterator_no_option(&self) -> u8 { todo!() }
+                }
+            }
+        }
+    }
+
+    #[test]
+    fn test_unsupported_features() {
+        uitest_lowering_attr! { hir::BackendAttrSupport::default(),
+            #[diplomat::bridge]
+            mod ffi {
+                use std::cmp;
+                use diplomat_runtime::DiplomatOption;
+
+                #[diplomat::opaque]
+                struct Opaque;
+
+                struct Struct {
+                    pub a: u8,
+                    pub b: u8,
+                    pub c: DiplomatOption<u8>,
+                }
+
+                struct Struct2 {
+                    pub a: DiplomatOption<Struct>,
+                }
+
+                #[diplomat::out]
+                struct OutStruct {
+                    pub option: DiplomatOption<u8>
+                }
+
+
+                impl Opaque {
+                    pub fn take_option(&self, option: DiplomatOption<u8>) {
+                        todo!()
+                    }
+                    // Always ok since this translates to a Resulty return
+                    pub fn returning_option_is_ok(&self) -> Option<u8> {
+                        todo!()
+                    }
+                }
+
+            }
+        }
+    }
+}
diff --git a/crates/diplomat_core/src/hir/defs.rs b/crates/diplomat_core/src/hir/defs.rs
new file mode 100644
index 0000000..90f1960
--- /dev/null
+++ b/crates/diplomat_core/src/hir/defs.rs
@@ -0,0 +1,253 @@
+//! Type definitions for structs, output structs, opaque structs, and enums.
+
+use super::lifetimes::LifetimeEnv;
+use super::{
+    Attrs, Callback, Everywhere, IdentBuf, Method, OutputOnly, SpecialMethodPresence, TyPosition,
+    Type,
+};
+use crate::ast::Docs;
+
+#[non_exhaustive]
+pub enum ReturnableStructDef<'tcx> {
+    Struct(&'tcx StructDef),
+    OutStruct(&'tcx OutStructDef),
+}
+
+#[derive(Copy, Clone, Debug)]
+#[non_exhaustive]
+pub enum TypeDef<'tcx> {
+    Struct(&'tcx StructDef),
+    OutStruct(&'tcx OutStructDef),
+    Opaque(&'tcx OpaqueDef),
+    Enum(&'tcx EnumDef),
+}
+
+#[derive(Debug)]
+#[non_exhaustive]
+pub struct TraitDef {
+    // TyPosition: InputOnly
+    pub docs: Docs,
+    pub name: IdentBuf,
+    pub methods: Vec<Callback>,
+    pub attrs: Attrs,
+    pub lifetimes: LifetimeEnv,
+}
+
+/// Structs that can only be returned from methods.
+pub type OutStructDef = StructDef<OutputOnly>;
+
+/// Structs that can be either inputs or outputs in methods.
+#[derive(Debug)]
+#[non_exhaustive]
+pub struct StructDef<P: TyPosition = Everywhere> {
+    pub docs: Docs,
+    pub name: IdentBuf,
+    pub fields: Vec<StructField<P>>,
+    pub methods: Vec<Method>,
+    pub attrs: Attrs,
+    pub lifetimes: LifetimeEnv,
+    pub special_method_presence: SpecialMethodPresence,
+}
+
+/// A struct whose contents are opaque across the FFI boundary, and can only
+/// cross when behind a pointer.
+///
+/// All opaques can be inputs or outputs when behind a reference, but owned
+/// opaques can only be returned since there isn't a general way for most languages
+/// to give up ownership.
+///
+/// A struct marked with `#[diplomat::opaque]`.
+#[derive(Debug)]
+#[non_exhaustive]
+pub struct OpaqueDef {
+    pub docs: Docs,
+    pub name: IdentBuf,
+    pub methods: Vec<Method>,
+    pub attrs: Attrs,
+    pub lifetimes: LifetimeEnv,
+    pub special_method_presence: SpecialMethodPresence,
+
+    /// The ABI name of the generated destructor
+    pub dtor_abi_name: IdentBuf,
+}
+
+/// The enum type.
+#[derive(Debug)]
+#[non_exhaustive]
+pub struct EnumDef {
+    pub docs: Docs,
+    pub name: IdentBuf,
+    pub variants: Vec<EnumVariant>,
+    pub methods: Vec<Method>,
+    pub attrs: Attrs,
+    pub special_method_presence: SpecialMethodPresence,
+}
+
+/// A field on a [`OutStruct`]s.
+pub type OutStructField = StructField<OutputOnly>;
+
+/// A field on a [`Struct`]s.
+#[derive(Debug)]
+#[non_exhaustive]
+pub struct StructField<P: TyPosition = Everywhere> {
+    pub docs: Docs,
+    pub name: IdentBuf,
+    pub ty: Type<P>,
+    pub attrs: Attrs,
+}
+
+/// A variant of an [`Enum`].
+#[derive(Debug)]
+#[non_exhaustive]
+pub struct EnumVariant {
+    pub docs: Docs,
+    pub name: IdentBuf,
+    pub discriminant: isize,
+    pub attrs: Attrs,
+}
+
+impl TraitDef {
+    pub(super) fn new(
+        docs: Docs,
+        name: IdentBuf,
+        methods: Vec<Callback>,
+        attrs: Attrs,
+        lifetimes: LifetimeEnv,
+    ) -> Self {
+        Self {
+            docs,
+            name,
+            methods,
+            attrs,
+            lifetimes,
+        }
+    }
+}
+
+impl<P: TyPosition> StructDef<P> {
+    pub(super) fn new(
+        docs: Docs,
+        name: IdentBuf,
+        fields: Vec<StructField<P>>,
+        methods: Vec<Method>,
+        attrs: Attrs,
+        lifetimes: LifetimeEnv,
+        special_method_presence: SpecialMethodPresence,
+    ) -> Self {
+        Self {
+            docs,
+            name,
+            fields,
+            methods,
+            attrs,
+            lifetimes,
+            special_method_presence,
+        }
+    }
+}
+
+impl OpaqueDef {
+    pub(super) fn new(
+        docs: Docs,
+        name: IdentBuf,
+        methods: Vec<Method>,
+        attrs: Attrs,
+        lifetimes: LifetimeEnv,
+        special_method_presence: SpecialMethodPresence,
+        dtor_abi_name: IdentBuf,
+    ) -> Self {
+        Self {
+            docs,
+            name,
+            methods,
+            attrs,
+            lifetimes,
+            special_method_presence,
+            dtor_abi_name,
+        }
+    }
+}
+
+impl EnumDef {
+    pub(super) fn new(
+        docs: Docs,
+        name: IdentBuf,
+        variants: Vec<EnumVariant>,
+        methods: Vec<Method>,
+        attrs: Attrs,
+        special_method_presence: SpecialMethodPresence,
+    ) -> Self {
+        Self {
+            docs,
+            name,
+            variants,
+            methods,
+            attrs,
+            special_method_presence,
+        }
+    }
+}
+
+impl<'a, P: TyPosition> From<&'a StructDef<P>> for TypeDef<'a> {
+    fn from(x: &'a StructDef<P>) -> Self {
+        P::wrap_struct_def(x)
+    }
+}
+
+impl<'a> From<&'a OpaqueDef> for TypeDef<'a> {
+    fn from(x: &'a OpaqueDef) -> Self {
+        TypeDef::Opaque(x)
+    }
+}
+
+impl<'a> From<&'a EnumDef> for TypeDef<'a> {
+    fn from(x: &'a EnumDef) -> Self {
+        TypeDef::Enum(x)
+    }
+}
+
+impl<'tcx> TypeDef<'tcx> {
+    pub fn name(&self) -> &'tcx IdentBuf {
+        match *self {
+            Self::Struct(ty) => &ty.name,
+            Self::OutStruct(ty) => &ty.name,
+            Self::Opaque(ty) => &ty.name,
+            Self::Enum(ty) => &ty.name,
+        }
+    }
+
+    pub fn docs(&self) -> &'tcx Docs {
+        match *self {
+            Self::Struct(ty) => &ty.docs,
+            Self::OutStruct(ty) => &ty.docs,
+            Self::Opaque(ty) => &ty.docs,
+            Self::Enum(ty) => &ty.docs,
+        }
+    }
+    pub fn methods(&self) -> &'tcx [Method] {
+        match *self {
+            Self::Struct(ty) => &ty.methods,
+            Self::OutStruct(ty) => &ty.methods,
+            Self::Opaque(ty) => &ty.methods,
+            Self::Enum(ty) => &ty.methods,
+        }
+    }
+
+    pub fn attrs(&self) -> &'tcx Attrs {
+        match *self {
+            Self::Struct(ty) => &ty.attrs,
+            Self::OutStruct(ty) => &ty.attrs,
+            Self::Opaque(ty) => &ty.attrs,
+            Self::Enum(ty) => &ty.attrs,
+        }
+    }
+
+    pub fn special_method_presence(&self) -> &'tcx SpecialMethodPresence {
+        match *self {
+            Self::Struct(ty) => &ty.special_method_presence,
+            Self::OutStruct(ty) => &ty.special_method_presence,
+            Self::Opaque(ty) => &ty.special_method_presence,
+            Self::Enum(ty) => &ty.special_method_presence,
+        }
+    }
+}
diff --git a/crates/diplomat_core/src/hir/elision.rs b/crates/diplomat_core/src/hir/elision.rs
new file mode 100644
index 0000000..90387fb
--- /dev/null
+++ b/crates/diplomat_core/src/hir/elision.rs
@@ -0,0 +1,614 @@
+//! This module provides the functionality for lowering lifetimes from the AST
+//! to the HIR, while simultaneously inferencing elided lifetimes.
+//!
+//! Full elision rules can be found in the [Nomicon].
+//!
+//! The key factor about lifetime elision is that all elision in the output of
+//! the method (if there is any) corresponds to exactly one lifetime in the method
+//! arguments, which may or may not be elided. Therefore, our task is to find this
+//! potential lifetime first, so that if we encounter an elided lifetime while
+//! lowering the output, we know which lifetime it corresponds to.
+//!
+//! # Unspoken Rules of Elision.
+//!
+//! Broadly speaking, the Nomicon defines the elision rules are such:
+//! 1. If there's a `&self` or `&mut self`, the lifetime of that borrow
+//!    corresponds to elision in the output.
+//! 2. Otherwise, if there's exactly one lifetime in the input, then that lifetime
+//!    corresponds to elision in the output.
+//! 3. If neither of these cases hold, then the output cannot contain elision.
+//!
+//! What the Nomicon doesn't tell you is that there are weird corner cases around
+//! using the `Self` type. Specifically, lifetimes in the `Self` type and in the
+//! type of the `self` argument (optional) aren't considered when figuring out
+//! which lifetime should correspond to elision in the output.
+//!
+//! Check out the following code:
+//! ```compile_fail
+//! struct Foo<'a>(&'a str);
+//!
+//! impl<'a> Foo<'a> {
+//!     fn get(self) -> &str { self.0 }
+//! }
+//! ```
+//! This code will fail to compile because it doesn't look at the `'a` in the
+//! `Foo<'a>`, which is what the type of `self` expands to. Therefore, it will
+//! conclude that there's nothing for the output to borrow from.
+//! This can be fixed by returning `&'a str` though. Many of the design
+//! decisions in this module were made to be able to replicate this behavior.
+//!
+//! You may be asking "why would we care about rejecting code that rustc rejects
+//! before it reaches us?" And the answer is this:
+//! ```rust
+//! # struct Foo<'a>(&'a str);
+//! impl<'a> Foo<'a> {
+//!     fn get(self, s: &str) -> &str { s }
+//! }
+//! ```
+//! This code is accepted by rustc, since it only considers the lifetime of `s`
+//! when searching for a lifetime that corresponds to output elision. If we were
+//! to naively look at all the lifetimes, we would see the lifetime in the `self`
+//! argument and the lifetime of `s`, making us reject this method. Therefore, we
+//! have to be extremely careful when traversing lifetimes, and make sure that
+//! lifetimes of `Self` are lowered but _not_ considered for elision, while other
+//! lifetimes are lowered while also being considered for elision.
+//!
+//! # Lowering and Inference
+//!
+//! Lowering and elision inference is broken into three distinct stages:
+//! 1. Lowering the borrow in `&self` or `&mut self`, if there is one.
+//! 2. Lowering lifetimes of other params.
+//! 3. Lowering lifetimes of the output.
+//!
+//! Although each stage fundementally lowers lifetimes, they behave differently
+//! when lowering elided lifetimes. Naturally, this module represents each stage
+//! as a state in a state machine.
+//!
+//! The first state is represented by the [`SelfParamLifetimeLowerer`] type.
+//! Since there is either zero or one occurrences of `&self` or `&mut self`, it
+//! exposes the `.no_self_ref()` and `.lower_self_ref(lt)` methods respectively,
+//! which consume the `SelfParamLifetimeLowerer` and return the next state,
+//! [`ParamLifetimeLowerer`], as well as the lowered lifetime. The reason these
+//! are two distinct types is that the lifetime in `&self` and `&mut self` takes
+//! precedence over any other lifetimes in the input, so `.lower_self_ref(lt)`
+//! tells the next state that the candidate lifetime is already found, and to
+//! generate fresh anonymous lifetimes for any elided lifetimes.
+//!
+//! The second state is represented by the [`ParamLifetimeLowerer`] type.
+//! It implements a helper trait, [`LifetimeLowerer`], which abstracts the lowering
+//! of references and generic lifetimes. Internally, it wraps an [`ElisionSource`],
+//! which acts as a state machine for tracking candidate lifetimes to correspond
+//! to elision in the output. When a lifetime that's not in the type of the `self`
+//! argument or in the expanded generics of the `Self` type is visited, this
+//! state machine is potentially updated to another state. If the lifetime is
+//! anonymous, it's added to the internal list of nodes that go into the final
+//! [`LifetimeEnv`] after lowering. Once all the lifetimes in the input are
+//! lowered, the `into_return_ltl()` method is called to transition into the
+//! final state.
+//!
+//! The third and final state is represented by the [`ReturnLifetimeLowerer`] type.
+//! Similar to `ParamLifetimeLowerer`, it also implements the [`LifetimeLowerer`]
+//! helper trait. However, it differs from `ParamLifetimeLowerer` since instead
+//! of potentially updating the internal `ElisionSource` when visiting a lifetime,
+//! it instead reads from it when an elided lifetime occurs. Once all the output
+//! lifetimes are lowered, `.finish()` is called to return the finalized
+//! [`LifetimeEnv`].
+//!
+//! [Nomicon]: https://doc.rust-lang.org/nomicon/lifetime-elision.html
+
+use super::lifetimes::{BoundedLifetime, Lifetime, LifetimeEnv, Lifetimes, MaybeStatic};
+use super::LoweringContext;
+use crate::ast;
+use smallvec::SmallVec;
+
+/// Lower [`ast::Lifetime`]s to [`Lifetime`]s.
+///
+/// This helper traits allows the [`lower_type`] and [`lower_out_type`] methods
+/// to abstractly lower lifetimes without concern for what sort of tracking
+/// goes on. In particular, elision inference requires updating internal state
+/// when visiting lifetimes in the input.
+pub trait LifetimeLowerer {
+    /// Lowers an [`ast::Lifetime`].
+    fn lower_lifetime(&mut self, lifetime: &ast::Lifetime) -> MaybeStatic<Lifetime>;
+
+    /// Lowers a slice of [`ast::Lifetime`]s by calling
+    /// [`LifetimeLowerer::lower_lifetime`] repeatedly.
+    ///
+
+    /// `type_generics` is the full list of generics on the type definition of the type
+    /// this lifetimes list is found on (needed for generating anon lifetimes)
+    fn lower_lifetimes(
+        &mut self,
+        lifetimes: &[ast::Lifetime],
+        type_generics: &ast::LifetimeEnv,
+    ) -> Lifetimes {
+        let mut lifetimes = Lifetimes::from_fn(lifetimes, |lifetime| self.lower_lifetime(lifetime));
+
+        for _ in lifetimes.as_slice().len()..type_generics.nodes.len() {
+            lifetimes.append_lifetime(self.lower_lifetime(&ast::Lifetime::Anonymous))
+        }
+        lifetimes
+    }
+
+    /// Lowers a slice of [`ast::Lifetime`], where the strategy may vary depending
+    /// on whether or not the lifetimes are expanded from the `Self` type.
+    ///
+    /// The distinction between this and [`LifetimeLowerer::lower_lifetimes`] is
+    /// that if `Self` expands to a type with anonymous lifetimes like `Foo<'_>`,
+    /// then multiple instances of `Self` should expand to have the same anonymous
+    /// lifetime, and this lifetime can be cached inside of the `self` argument.
+    /// Additionally, elision inferences knows to not search inside the generics
+    /// of `Self` types for candidate lifetimes to correspond to elided lifetimes
+    /// in the output.
+    ///
+    /// `type_generics` is the full list of generics on the type definition of the type
+    /// this generics list is found on (needed for generating anon lifetimes)
+
+    fn lower_generics(
+        &mut self,
+        lifetimes: &[ast::Lifetime],
+        type_generics: &ast::LifetimeEnv,
+        is_self: bool,
+    ) -> Lifetimes;
+}
+
+/// A state machine for tracking which lifetime in a function's parameters
+/// may correspond to elided lifetimes in the output.
+#[derive(Copy, Clone)]
+enum ElisionSource {
+    /// No borrows in the input, no elision.
+    NoBorrows,
+    /// `&self` or `&mut self`, elision allowed.
+    SelfParam(MaybeStatic<Lifetime>),
+    /// One param contains a borrow, elision allowed.
+    OneParam(MaybeStatic<Lifetime>),
+    /// Multiple borrows and no self borrow, no elision.
+    MultipleBorrows,
+}
+
+impl ElisionSource {
+    /// Potentially transition to a new state.
+    fn visit_lifetime(&mut self, lifetime: MaybeStatic<Lifetime>) {
+        match self {
+            ElisionSource::NoBorrows => *self = ElisionSource::OneParam(lifetime),
+            ElisionSource::SelfParam(_) => {
+                // References to self have the highest precedence, do nothing.
+            }
+            ElisionSource::OneParam(_) => *self = ElisionSource::MultipleBorrows,
+            ElisionSource::MultipleBorrows => {
+                // There's ambiguity. This is valid when there's no elision in
+                // the output.
+            }
+        };
+    }
+}
+
+/// A type for storing shared information between the different states of elision
+/// inference.
+///
+/// This contains data for generating fresh elided lifetimes, looking up named
+/// lifetimes, and caching lifetimes of `Self`.
+pub(super) struct BaseLifetimeLowerer<'ast> {
+    lifetime_env: &'ast ast::LifetimeEnv,
+    self_lifetimes: Option<Lifetimes>,
+    nodes: SmallVec<[BoundedLifetime; super::lifetimes::INLINE_NUM_LIFETIMES]>,
+    num_lifetimes: usize,
+}
+
+/// The first phase of output elision inference.
+///
+/// In the first phase, the type signature of the `&self` or `&mut self` type
+/// is lowered into its HIR representation, if present. According to elision
+/// rules, this reference has the highest precedence as the lifetime that
+/// goes into elision in the output, and so it's checked first.
+pub(super) struct SelfParamLifetimeLowerer<'ast> {
+    base: BaseLifetimeLowerer<'ast>,
+}
+
+/// The second phase of output elision inference.
+///
+/// In the second phase, all lifetimes in the parameter type signatures
+/// (besides the lifetime of self, if present) are lowered. If a self param
+/// didn't claim the potential output elided lifetime, then if there's a
+/// single lifetime (elided or not) in the inputs, it will claim the
+/// potential output elided lifetime.
+pub(super) struct ParamLifetimeLowerer<'ast> {
+    elision_source: ElisionSource,
+    base: BaseLifetimeLowerer<'ast>,
+}
+
+/// The third and final phase of output elision inference.
+///
+/// In the third phase, the type signature of the output type is lowered into
+/// its HIR representation. If one of the input lifetimes were marked as
+/// responsible for any elision in the output, then anonymous lifetimes get
+/// that lifetime. If none did and there is elision in the output, then
+/// rustc should have errored and said the elision was ambiguous, meaning
+/// that state should be impossible so it panics.
+pub(super) struct ReturnLifetimeLowerer<'ast> {
+    elision_source: ElisionSource,
+    base: BaseLifetimeLowerer<'ast>,
+}
+
+impl<'ast> BaseLifetimeLowerer<'ast> {
+    /// Returns a [`Lifetime`] representing a new anonymous lifetime, and
+    /// pushes it to the nodes vector.
+    fn new_elided(&mut self) -> Lifetime {
+        let index = self.num_lifetimes;
+        self.num_lifetimes += 1;
+        Lifetime::new(index)
+    }
+
+    /// Lowers a single [`ast::Lifetime`]. If the lifetime is elided, then a fresh
+    /// [`ImplicitLifetime`] is generated.
+    fn lower_lifetime(&mut self, lifetime: &ast::Lifetime) -> MaybeStatic<Lifetime> {
+        match lifetime {
+            ast::Lifetime::Static => MaybeStatic::Static,
+            ast::Lifetime::Named(named) => {
+                MaybeStatic::NonStatic(Lifetime::from_ast(named, self.lifetime_env))
+            }
+            ast::Lifetime::Anonymous => MaybeStatic::NonStatic(self.new_elided()),
+        }
+    }
+
+    /// Retrieves the cached  `Self` lifetimes, or caches newly generated
+    /// lifetimes and returns those.
+    fn self_lifetimes_or_new(&mut self, ast_lifetimes: &[ast::Lifetime]) -> Lifetimes {
+        if let Some(lifetimes) = &self.self_lifetimes {
+            lifetimes.clone()
+        } else {
+            let lifetimes = Lifetimes::from_fn(ast_lifetimes, |lt| self.lower_lifetime(lt));
+            self.self_lifetimes = Some(lifetimes.clone());
+            lifetimes
+        }
+    }
+}
+
+impl<'ast> SelfParamLifetimeLowerer<'ast> {
+    /// Returns a new [`SelfParamLifetimeLowerer`].
+    pub fn new(
+        lifetime_env: &'ast ast::LifetimeEnv,
+        ctx: &mut LoweringContext,
+    ) -> Result<Self, ()> {
+        let mut hir_nodes = Ok(SmallVec::new());
+
+        for ast_node in lifetime_env.nodes.iter() {
+            let lifetime = ctx.lower_ident(ast_node.lifetime.name(), "named lifetime");
+            match (lifetime, &mut hir_nodes) {
+                (Ok(lifetime), Ok(hir_nodes)) => {
+                    hir_nodes.push(BoundedLifetime::new(
+                        lifetime,
+                        ast_node.longer.iter().map(|i| Lifetime::new(*i)).collect(),
+                        ast_node.shorter.iter().map(|i| Lifetime::new(*i)).collect(),
+                    ));
+                }
+                _ => hir_nodes = Err(()),
+            }
+        }
+
+        hir_nodes.map(|nodes| Self {
+            base: BaseLifetimeLowerer {
+                lifetime_env,
+                self_lifetimes: None,
+                num_lifetimes: nodes.len(),
+                nodes,
+            },
+        })
+    }
+
+    /// Lowers the lifetime of `&self` or `&mut self`.
+    ///
+    /// The lifetimes of `&self` and `&mut self` are special, because they
+    /// automatically take priority over any other lifetime in the input for
+    /// being tied to any elided lifetimes in the output.
+    ///
+    /// Along with returning the lowered lifetime, this method also returns the
+    /// next state in elision inference, the [`ParamLifetimeLowerer`].
+    pub fn lower_self_ref(
+        mut self,
+        lifetime: &ast::Lifetime,
+    ) -> (MaybeStatic<Lifetime>, ParamLifetimeLowerer<'ast>) {
+        let self_lifetime = self.base.lower_lifetime(lifetime);
+
+        (
+            self_lifetime,
+            self.into_param_ltl(ElisionSource::SelfParam(self_lifetime)),
+        )
+    }
+
+    /// Acknowledges that there's no `&self` or `&mut self`, and transitions
+    /// to the next state, [`ParamLifetimeLowerer`].
+    pub fn no_self_ref(self) -> ParamLifetimeLowerer<'ast> {
+        self.into_param_ltl(ElisionSource::NoBorrows)
+    }
+
+    /// Transition into the next state, [`ParamLifetimeLowerer`].
+    fn into_param_ltl(self, elision_source: ElisionSource) -> ParamLifetimeLowerer<'ast> {
+        ParamLifetimeLowerer {
+            elision_source,
+            base: self.base,
+        }
+    }
+}
+
+impl<'ast> ParamLifetimeLowerer<'ast> {
+    /// Once all lifetimes in the parameters are lowered, this function is
+    /// called to transition to the next state, [`ReturnLifetimeLowerer`].
+    pub fn into_return_ltl(self) -> ReturnLifetimeLowerer<'ast> {
+        ReturnLifetimeLowerer {
+            elision_source: self.elision_source,
+            base: self.base,
+        }
+    }
+}
+
+impl<'ast> LifetimeLowerer for ParamLifetimeLowerer<'ast> {
+    fn lower_lifetime(&mut self, borrow: &ast::Lifetime) -> MaybeStatic<Lifetime> {
+        let lifetime = self.base.lower_lifetime(borrow);
+        self.elision_source.visit_lifetime(lifetime);
+        lifetime
+    }
+
+    fn lower_generics(
+        &mut self,
+        lifetimes: &[ast::Lifetime],
+        type_generics: &ast::LifetimeEnv,
+        is_self: bool,
+    ) -> Lifetimes {
+        if is_self {
+            self.base.self_lifetimes_or_new(lifetimes)
+        } else {
+            self.lower_lifetimes(lifetimes, type_generics)
+        }
+    }
+}
+
+impl<'ast> ReturnLifetimeLowerer<'ast> {
+    /// Finalize the lifetimes in the method, returning the resulting [`LifetimeEnv`].
+    pub fn finish(self) -> LifetimeEnv {
+        LifetimeEnv::new(self.base.nodes, self.base.num_lifetimes)
+    }
+}
+
+impl<'ast> LifetimeLowerer for ReturnLifetimeLowerer<'ast> {
+    fn lower_lifetime(&mut self, borrow: &ast::Lifetime) -> MaybeStatic<Lifetime> {
+        match borrow {
+            ast::Lifetime::Static => MaybeStatic::Static,
+            ast::Lifetime::Named(named) => {
+                MaybeStatic::NonStatic(Lifetime::from_ast(named, self.base.lifetime_env))
+            }
+            ast::Lifetime::Anonymous => match self.elision_source {
+                ElisionSource::SelfParam(lifetime) | ElisionSource::OneParam(lifetime) => lifetime,
+                ElisionSource::NoBorrows => {
+                    panic!("nothing to borrow from, this shouldn't pass rustc's checks")
+                }
+                ElisionSource::MultipleBorrows => {
+                    panic!("source of elision is ambiguous, this shouldn't pass rustc's checks")
+                }
+            },
+        }
+    }
+
+    fn lower_generics(
+        &mut self,
+        lifetimes: &[ast::Lifetime],
+        type_generics: &ast::LifetimeEnv,
+        is_self: bool,
+    ) -> Lifetimes {
+        if is_self {
+            self.base.self_lifetimes_or_new(lifetimes)
+        } else {
+            self.lower_lifetimes(lifetimes, type_generics)
+        }
+    }
+}
+
+impl LifetimeLowerer for &ast::LifetimeEnv {
+    fn lower_lifetime(&mut self, lifetime: &ast::Lifetime) -> MaybeStatic<Lifetime> {
+        match lifetime {
+            ast::Lifetime::Static => MaybeStatic::Static,
+            ast::Lifetime::Named(named) => MaybeStatic::NonStatic(Lifetime::from_ast(named, self)),
+            ast::Lifetime::Anonymous => {
+                panic!("anonymous lifetime inside struct, this shouldn't pass rustc's checks")
+            }
+        }
+    }
+
+    fn lower_generics(
+        &mut self,
+        lifetimes: &[ast::Lifetime],
+        type_generics: &ast::LifetimeEnv,
+        _: bool,
+    ) -> Lifetimes {
+        self.lower_lifetimes(lifetimes, type_generics)
+    }
+}
+
+// Things to test:
+// 1. ensure that if there are multiple inputs that are `Self`, where `Self` has
+//    an elided lifetime, all expansions of `Self` have the same anonymous lifetimes.
+
+#[cfg(test)]
+mod tests {
+    use strck::IntoCk;
+
+    /// Convert a syntax tree into a [`TypeContext`].
+    macro_rules! tcx {
+        ($($tokens:tt)*) => {{
+            let m = crate::ast::Module::from_syn(&syn::parse_quote! { $($tokens)* }, true);
+
+            let mut env = crate::Env::default();
+            let mut top_symbols = crate::ModuleEnv::new(Default::default());
+
+            m.insert_all_types(crate::ast::Path::empty(), &mut env);
+            top_symbols.insert(m.name.clone(), crate::ast::ModSymbol::SubModule(m.name.clone()));
+
+            env.insert(crate::ast::Path::empty(), top_symbols);
+
+            let mut backend = crate::hir::BasicAttributeValidator::new("test-backend");
+            backend.support.static_slices = true;
+
+            // Don't run validation: it will error on elision. We want this code to support
+            // elision even if we don't actually allow it, since good diagnostics involve understanding
+            // broken code.
+            let (_, tcx) = crate::hir::TypeContext::from_ast_without_validation(&env, backend).unwrap();
+
+            tcx
+        }}
+    }
+
+    macro_rules! do_test {
+        ($($tokens:tt)*) => {{
+            let mut settings = insta::Settings::new();
+            settings.set_sort_maps(true);
+
+            settings.bind(|| {
+                let tcx = tcx! { $($tokens)* };
+
+                insta::assert_debug_snapshot!(tcx);
+            })
+        }}
+    }
+
+    #[test]
+    fn simple_mod() {
+        do_test! {
+            mod ffi {
+                #[diplomat::opaque]
+                struct Opaque<'a> {
+                    s: DiplomatStrSlice<'a>,
+                }
+
+                struct Struct<'a> {
+                    s:  DiplomatStrSlice<'a>,
+                }
+
+                #[diplomat::out]
+                struct OutStruct<'a> {
+                    inner: Box<Opaque<'a>>,
+                }
+
+                impl<'a> OutStruct<'a> {
+                    pub fn new(s: &'a DiplomatStr) -> Self {
+                        Self { inner: Box::new(Opaque { s }) }
+                    }
+
+                }
+
+                impl<'a> Struct<'a> {
+                    pub fn rustc_elision(self, s: &DiplomatStr) -> &DiplomatStr {
+                        s
+                    }
+                }
+            }
+        }
+    }
+
+    #[test]
+    fn test_elision_in_struct() {
+        let tcx = tcx! {
+            mod ffi {
+                #[diplomat::opaque]
+                struct Opaque;
+
+                #[diplomat::opaque]
+                struct Opaque2<'a>(&'a str);
+
+                impl Opaque {
+                    // This should have two elided lifetimes
+                    pub fn elided(&self, x: &Opaque2) {
+
+                    }
+                }
+            }
+        };
+
+        let method = &tcx
+            .opaques()
+            .iter()
+            .find(|def| def.name == "Opaque")
+            .unwrap()
+            .methods[0];
+
+        assert_eq!(
+            method.lifetime_env.num_lifetimes(),
+            3,
+            "elided() must have three anon lifetimes"
+        );
+        insta::assert_debug_snapshot!(method);
+    }
+
+    #[test]
+    fn test_borrowing_fields() {
+        use std::collections::BTreeMap;
+        use std::fmt;
+
+        let tcx = tcx! {
+            mod ffi {
+                #[diplomat::opaque]
+                pub struct Opaque;
+
+                struct Input<'p, 'q> {
+                    p_data: &'p Opaque,
+                    q_data: &'q Opaque,
+                    name: DiplomatStrSlice<'static>,
+                    inner: Inner<'q>,
+                }
+
+                struct Inner<'a> {
+                    more_data: DiplomatStrSlice<'a>,
+                }
+
+                struct Output<'p,'q> {
+                    p_data: &'p Opaque,
+                    q_data: &'q Opaque,
+                }
+
+                impl<'a, 'b> Input<'a, 'b> {
+                    pub fn as_output(self, _s: &'static DiplomatStr) -> Output<'b, 'a> {
+                        Output { data: self.data }
+                    }
+
+                }
+            }
+        };
+
+        let method = &tcx
+            .structs()
+            .iter()
+            .find(|def| def.name == "Input")
+            .unwrap()
+            .methods[0];
+
+        let visitor = method.borrowing_field_visitor(&tcx, "this".ck().unwrap());
+        let mut lt_to_borrowing_fields: BTreeMap<_, Vec<_>> = BTreeMap::new();
+        visitor.visit_borrowing_fields(|lt, bf| {
+            lt_to_borrowing_fields
+                .entry(lt)
+                .or_default()
+                .push(DebugBorrowingField(bf));
+        });
+
+        struct DebugBorrowingField<'m>(crate::hir::borrowing_field::BorrowingField<'m>);
+
+        impl<'m> fmt::Debug for DebugBorrowingField<'m> {
+            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+                f.write_str("\"")?;
+                self.0.try_backtrace(|i, ident| {
+                    if i != 0 {
+                        f.write_str(".")?;
+                    }
+                    f.write_str(ident.as_str())
+                })?;
+                f.write_str("\"")
+            }
+        }
+
+        let mut settings = insta::Settings::new();
+        settings.set_sort_maps(true);
+
+        settings.bind(|| {
+            insta::assert_debug_snapshot!(lt_to_borrowing_fields);
+        })
+    }
+}
diff --git a/crates/diplomat_core/src/hir/lifetimes.rs b/crates/diplomat_core/src/hir/lifetimes.rs
new file mode 100644
index 0000000..2b461ba
--- /dev/null
+++ b/crates/diplomat_core/src/hir/lifetimes.rs
@@ -0,0 +1,454 @@
+//! Lifetime information for types.
+#![allow(dead_code)]
+
+use super::IdentBuf;
+use crate::ast;
+use core::fmt::Debug;
+use core::hash::Hash;
+
+use smallvec::{smallvec, SmallVec};
+use std::borrow::{Borrow, Cow};
+
+/// Convenience const representing the number of lifetimes a [`LifetimeEnv`]
+/// can hold inline before needing to dynamically allocate.
+pub(crate) const INLINE_NUM_LIFETIMES: usize = 4;
+
+/// The lifetimes and bounds found on a method or type definition
+#[derive(Debug)]
+pub struct LifetimeEnv {
+    /// List of named lifetimes in scope of the method, and their bounds
+    nodes: SmallVec<[BoundedLifetime; INLINE_NUM_LIFETIMES]>,
+
+    /// Only relevant for method LifetimeEnvs (otherwise this is nodes.len())
+    ///
+    /// The number of named _and_ anonymous lifetimes in the method.
+    /// We store the sum since it represents the upper bound on what indices
+    /// are in range of the graph. If we make a [`Lifetimes`] with
+    /// `num_lifetimes` entries, then `Lifetime`s that convert into
+    /// `Lifetime`s will fall into this range, and we'll know that it's
+    /// a named lifetime if it's < `nodes.len()`, or that it's an anonymous
+    /// lifetime if it's < `num_lifetimes`. Otherwise, we'd have to make a
+    /// distinction in `Lifetime` about which context it's in.
+    num_lifetimes: usize,
+}
+
+impl LifetimeEnv {
+    /// Format a lifetime indexing this env for use in code
+    pub fn fmt_lifetime(&self, lt: impl Borrow<Lifetime>) -> Cow<str> {
+        // we use Borrow here so that this can be used in templates where there's autoborrowing
+        let lt = *lt.borrow();
+        if let Some(lt) = self.nodes.get(lt.0) {
+            Cow::from(lt.ident.as_str())
+        } else if lt.0 < self.num_lifetimes {
+            format!("anon_{}", lt.0 - self.nodes.len()).into()
+        } else {
+            panic!("Found out of range lifetime: Got {lt:?} for env with {} nodes and {} total lifetimes", self.nodes.len(), self.num_lifetimes);
+        }
+    }
+
+    /// Get an iterator of all lifetimes that this must live as long as (including itself)
+    /// with the first lifetime always being returned first
+
+    pub fn all_shorter_lifetimes(
+        &self,
+        lt: impl Borrow<Lifetime>,
+    ) -> impl Iterator<Item = Lifetime> + '_ {
+        // we use Borrow here so that this can be used in templates where there's autoborrowing
+        let lt = *lt.borrow();
+        // longer = true, since we are looking for lifetimes this is longer than
+        LifetimeTransitivityIterator::new(self, lt.0, false)
+    }
+
+    /// Same as all_shorter_lifetimes but the other way
+    pub fn all_longer_lifetimes(
+        &self,
+        lt: impl Borrow<Lifetime>,
+    ) -> impl Iterator<Item = Lifetime> + '_ {
+        // we use Borrow here so that this can be used in templates where there's autoborrowing
+        let lt = *lt.borrow();
+        LifetimeTransitivityIterator::new(self, lt.0, true)
+    }
+
+    // List all named and unnamed lifetimes
+    pub fn num_lifetimes(&self) -> usize {
+        self.num_lifetimes
+    }
+
+    pub fn all_lifetimes(&self) -> impl ExactSizeIterator<Item = Lifetime> {
+        (0..self.num_lifetimes()).map(Lifetime::new)
+    }
+
+    /// Get the bounds for a named lifetime (none for unnamed lifetimes)
+    pub(super) fn get_bounds(&self, named_lifetime: Lifetime) -> Option<&BoundedLifetime> {
+        self.nodes.get(named_lifetime.0)
+    }
+
+    /// Returns a new [`LifetimeEnv`].
+    pub(super) fn new(
+        nodes: SmallVec<[BoundedLifetime; INLINE_NUM_LIFETIMES]>,
+        num_lifetimes: usize,
+    ) -> Self {
+        Self {
+            nodes,
+            num_lifetimes,
+        }
+    }
+
+    /// Returns a fresh [`Lifetimes`] corresponding to `self`.
+    pub fn lifetimes(&self) -> Lifetimes {
+        let indices = (0..self.num_lifetimes)
+            .map(|index| MaybeStatic::NonStatic(Lifetime::new(index)))
+            .collect();
+
+        Lifetimes { indices }
+    }
+
+    /// Returns a new [`SubtypeLifetimeVisitor`], which can visit all reachable
+    /// lifetimes
+    pub fn subtype_lifetimes_visitor<F>(&self, visit_fn: F) -> SubtypeLifetimeVisitor<'_, F>
+    where
+        F: FnMut(Lifetime),
+    {
+        SubtypeLifetimeVisitor::new(self, visit_fn)
+    }
+}
+
+/// A lifetime in a [`LifetimeEnv`], which keeps track of which lifetimes it's
+/// longer and shorter than.
+///
+/// Invariant: for a BoundedLifetime found inside a LifetimeEnv, all short/long connections
+/// should be bidirectional.
+#[derive(Debug)]
+pub(super) struct BoundedLifetime {
+    pub(super) ident: IdentBuf,
+    /// Lifetimes longer than this (not transitive)
+    ///
+    /// These are the inverse graph edges compared to `shorter`
+    pub(super) longer: SmallVec<[Lifetime; 2]>,
+    /// Lifetimes this is shorter than (not transitive)
+    ///
+    /// These match `'a: 'b + 'c` bounds
+    pub(super) shorter: SmallVec<[Lifetime; 2]>,
+}
+
+impl BoundedLifetime {
+    /// Returns a new [`BoundedLifetime`].
+    pub(super) fn new(
+        ident: IdentBuf,
+        longer: SmallVec<[Lifetime; 2]>,
+        shorter: SmallVec<[Lifetime; 2]>,
+    ) -> Self {
+        Self {
+            ident,
+            longer,
+            shorter,
+        }
+    }
+}
+
+/// Visit subtype lifetimes recursively, keeping track of which have already
+/// been visited.
+pub struct SubtypeLifetimeVisitor<'lt, F> {
+    lifetime_env: &'lt LifetimeEnv,
+    visited: SmallVec<[bool; INLINE_NUM_LIFETIMES]>,
+    visit_fn: F,
+}
+
+impl<'lt, F> SubtypeLifetimeVisitor<'lt, F>
+where
+    F: FnMut(Lifetime),
+{
+    fn new(lifetime_env: &'lt LifetimeEnv, visit_fn: F) -> Self {
+        Self {
+            lifetime_env,
+            visited: smallvec![false; lifetime_env.nodes.len()],
+            visit_fn,
+        }
+    }
+
+    /// Visit more sublifetimes. This method tracks which lifetimes have already
+    /// been visited, and uses this to not visit the same lifetime twice.
+    pub fn visit_subtypes(&mut self, method_lifetime: Lifetime) {
+        if let Some(visited @ false) = self.visited.get_mut(method_lifetime.0) {
+            *visited = true;
+
+            (self.visit_fn)(method_lifetime);
+
+            for longer in self.lifetime_env.nodes[method_lifetime.0].longer.iter() {
+                self.visit_subtypes(*longer)
+            }
+        } else {
+            debug_assert!(
+                method_lifetime.0 > self.lifetime_env.num_lifetimes,
+                "method lifetime has an internal index that's not in range of the lifetime env"
+            );
+        }
+    }
+}
+
+/// Wrapper type for `Lifetime` and `Lifetime`, indicating that it may
+/// be the `'static` lifetime.
+#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)]
+#[allow(clippy::exhaustive_enums)] // this will only ever have two variants
+pub enum MaybeStatic<T> {
+    Static,
+    NonStatic(T),
+}
+
+impl<T> MaybeStatic<T> {
+    /// Maps the lifetime, if it's not the `'static` lifetime, to another
+    /// non-static lifetime.
+    pub(super) fn map_nonstatic<F, R>(self, f: F) -> MaybeStatic<R>
+    where
+        F: FnOnce(T) -> R,
+    {
+        match self {
+            MaybeStatic::Static => MaybeStatic::Static,
+            MaybeStatic::NonStatic(lifetime) => MaybeStatic::NonStatic(f(lifetime)),
+        }
+    }
+
+    /// Maps the lifetime, if it's not the `'static` lifetime, to a potentially
+    /// static lifetime.
+    pub(super) fn flat_map_nonstatic<R, F>(self, f: F) -> MaybeStatic<R>
+    where
+        F: FnOnce(T) -> MaybeStatic<R>,
+    {
+        match self {
+            MaybeStatic::Static => MaybeStatic::Static,
+            MaybeStatic::NonStatic(lifetime) => f(lifetime),
+        }
+    }
+}
+
+/// A lifetime that exists as part of a type name, struct signature, or method signature.
+///
+/// This index only makes sense in the context of a surrounding type or method; since
+/// this is essentially an index into that type/method's lifetime list.
+#[derive(Copy, Clone, Hash, PartialEq, Eq, PartialOrd, Ord, Debug)]
+pub struct Lifetime(usize);
+
+/// A set of lifetimes found on a type name, struct signature, or method signature
+#[derive(Clone, Debug)]
+pub struct Lifetimes {
+    indices: SmallVec<[MaybeStatic<Lifetime>; 2]>,
+}
+
+impl Lifetime {
+    pub(super) const fn new(index: usize) -> Self {
+        Self(index)
+    }
+}
+
+impl Lifetimes {
+    /// Returns an iterator over the contained [`Lifetime`]s.
+    pub fn lifetimes(&self) -> impl ExactSizeIterator<Item = MaybeStatic<Lifetime>> + '_ {
+        self.indices.iter().copied()
+    }
+
+    pub(super) fn as_slice(&self) -> &[MaybeStatic<Lifetime>] {
+        self.indices.as_slice()
+    }
+}
+
+impl Lifetime {
+    /// Returns a [`Lifetime`] from its AST counterparts.
+    pub(super) fn from_ast(named: &ast::NamedLifetime, lifetime_env: &ast::LifetimeEnv) -> Self {
+        let index = lifetime_env
+            .id(named)
+            .unwrap_or_else(|| panic!("lifetime `{named}` not found in lifetime env"));
+        Self::new(index)
+    }
+
+    /// Returns a new [`MaybeStatic<Lifetime>`] representing `self` in the
+    /// scope of the method that it appears in.
+    ///
+    /// For example, if we have some `Foo<'a>` type with a field `&'a Bar`, then
+    /// we can call this on the `'a` on the field. If `Foo` was `Foo<'static>`
+    /// in the method, then this will return `MaybeStatic::Static`. But if it
+    /// was `Foo<'b>`, then this will return `MaybeStatic::NonStatic` containing
+    /// the `Lifetime` corresponding to `'b`.
+    pub fn as_method_lifetime(self, method_lifetimes: &Lifetimes) -> MaybeStatic<Lifetime> {
+        method_lifetimes.indices[self.0]
+    }
+}
+
+impl Lifetimes {
+    pub(super) fn from_fn<F>(lifetimes: &[ast::Lifetime], lower_fn: F) -> Self
+    where
+        F: FnMut(&ast::Lifetime) -> MaybeStatic<Lifetime>,
+    {
+        Self {
+            indices: lifetimes.iter().map(lower_fn).collect(),
+        }
+    }
+
+    /// Append an additional lifetime. Used to tack on anon lifetimes
+    pub(super) fn append_lifetime(&mut self, lifetime: MaybeStatic<Lifetime>) {
+        self.indices.push(lifetime)
+    }
+
+    /// Returns a new [`Lifetimes`] representing the lifetimes in the scope
+    /// of the method this type appears in.
+    ///
+    /// # Examples
+    ///
+    /// ```rust
+    /// # struct Alice<'a>(&'a ());
+    /// # struct Bob<'b>(&'b ());
+    /// struct Foo<'a, 'b> {
+    ///     alice: Alice<'a>,
+    ///     bob: Bob<'b>,
+    /// }
+    ///
+    /// fn bar<'x, 'y>(arg: Foo<'x, 'y>) {}
+    /// ```
+    /// Here, `Foo` will have a [`Lifetimes`] containing `['a, 'b]`,
+    /// and `bar` will have a [`Lifetimes`] containing `{'x: 'x, 'y: 'y}`.
+    /// When we enter the scope of `Foo` as a type, we use this method to combine
+    /// the two to get a new [`Lifetimes`] representing the mapping from
+    /// lifetimes in `Foo`'s scope to lifetimes in `bar`s scope: `{'a: 'x, 'b: 'y}`.
+    ///
+    /// This tells us that `arg.alice` has lifetime `'x` in the method, and
+    /// that `arg.bob` has lifetime `'y`.
+    pub fn as_method_lifetimes(&self, method_lifetimes: &Lifetimes) -> Lifetimes {
+        let indices = self
+            .indices
+            .iter()
+            .map(|maybe_static_lt| {
+                maybe_static_lt.flat_map_nonstatic(|lt| lt.as_method_lifetime(method_lifetimes))
+            })
+            .collect();
+
+        Lifetimes { indices }
+    }
+}
+
+struct LifetimeTransitivityIterator<'env> {
+    env: &'env LifetimeEnv,
+    visited: Vec<bool>,
+    queue: Vec<usize>,
+    longer: bool,
+}
+
+impl<'env> LifetimeTransitivityIterator<'env> {
+    // Longer is whether we are looking for lifetimes longer or shorter than this
+    fn new(env: &'env LifetimeEnv, starting: usize, longer: bool) -> Self {
+        Self {
+            env,
+            visited: vec![false; env.num_lifetimes()],
+            queue: vec![starting],
+            longer,
+        }
+    }
+}
+
+impl<'env> Iterator for LifetimeTransitivityIterator<'env> {
+    type Item = Lifetime;
+
+    fn next(&mut self) -> Option<Lifetime> {
+        while let Some(next) = self.queue.pop() {
+            if self.visited[next] {
+                continue;
+            }
+            self.visited[next] = true;
+
+            if let Some(named) = self.env.nodes.get(next) {
+                let edge_dir = if self.longer {
+                    &named.longer
+                } else {
+                    &named.shorter
+                };
+                self.queue.extend(edge_dir.iter().map(|i| i.0));
+            }
+
+            return Some(Lifetime::new(next));
+        }
+        None
+    }
+}
+
+/// Convenience type for linking the lifetimes found at a type *use* site (e.g. `&'c Foo<'a, 'b>`)
+/// with the lifetimes found at its *def* site (e.g. `struct Foo<'x, 'y>`).
+///
+/// Construct this by calling `.linked_lifetimes()` on a StructPath or OpaquePath
+pub struct LinkedLifetimes<'def, 'tcx> {
+    env: &'tcx LifetimeEnv,
+    self_lt: Option<MaybeStatic<Lifetime>>,
+    lifetimes: &'def Lifetimes,
+}
+
+impl<'def, 'tcx> LinkedLifetimes<'def, 'tcx> {
+    pub(crate) fn new(
+        env: &'tcx LifetimeEnv,
+        self_lt: Option<MaybeStatic<Lifetime>>,
+        lifetimes: &'def Lifetimes,
+    ) -> Self {
+        debug_assert_eq!(
+            lifetimes.lifetimes().len(),
+            env.all_lifetimes().len(),
+            "Should only link lifetimes between a type and its def"
+        );
+        Self {
+            env,
+            self_lt,
+            lifetimes,
+        }
+    }
+
+    /// Takes a lifetime at the def site and produces one at the use site
+    pub fn def_to_use(&self, def_lt: Lifetime) -> MaybeStatic<Lifetime> {
+        *self
+            .lifetimes
+            .as_slice()
+            .get(def_lt.0)
+            .expect("All def site lifetimes must be used!")
+    }
+
+    /// The lifetime env at the def site. Def lifetimes should be resolved
+    /// against this.
+    pub fn def_env(&self) -> &'tcx LifetimeEnv {
+        self.env
+    }
+
+    /// Link lifetimes from the use site to lifetimes from the def site, only including
+    /// lifetimes found at the def site.
+    ///
+    /// This will *not* include the self-lifetime, i.e. for an opaque use site `&'c Foo<'a, 'b>`
+    /// this will not include `'c` (but you can obtain it from [`Self::self_lifetime()`]))
+    ///
+    /// The return iterator returns pairs of (use_lt, def_lt), in order.
+    ///
+    /// This behaves identically to [`Self::lifetimes_all()`] for `LinkedLifetimes` constructed
+    /// from anything other than a borrowing opaque.
+    pub fn lifetimes_def_only(
+        &self,
+    ) -> impl Iterator<Item = (MaybeStatic<Lifetime>, Lifetime)> + '_ {
+        self.lifetimes.lifetimes().zip(self.env.all_lifetimes())
+    }
+
+    /// If there is a self-lifetime (e.g. `'c` on `&'c Foo<'a, 'b>`), return it. This lifetime
+    /// isn't found at the def site.
+    pub fn self_lifetime(&self) -> Option<MaybeStatic<Lifetime>> {
+        self.self_lt
+    }
+
+    /// Link lifetimes from the use site to lifetimes from the def site, including self lifetimes.
+    ///
+    /// This returns Options since self-lifetimes do not map to anything at the def site.
+    ///
+    /// The return iterator returns pairs of (use_lt, def_lt), in order, with the first entry potentially being
+    /// the self lifetime (which has a def_lt of None).
+    ///
+    /// This behaves identically to [`Self::lifetimes_all()`] for `LinkedLifetimes` constructed
+    /// from anything other than a borrowing opaque.
+    pub fn lifetimes_all(
+        &self,
+    ) -> impl Iterator<Item = (MaybeStatic<Lifetime>, Option<Lifetime>)> + '_ {
+        self.self_lt.iter().map(|i| (*i, None)).chain(
+            self.lifetimes
+                .lifetimes()
+                .zip(self.env.all_lifetimes().map(Some)),
+        )
+    }
+}
diff --git a/crates/diplomat_core/src/hir/lowering.rs b/crates/diplomat_core/src/hir/lowering.rs
new file mode 100644
index 0000000..f312eb0
--- /dev/null
+++ b/crates/diplomat_core/src/hir/lowering.rs
@@ -0,0 +1,1584 @@
+use super::{
+    AttributeContext, AttributeValidator, Attrs, Borrow, BoundedLifetime, Callback, CallbackParam,
+    EnumDef, EnumPath, EnumVariant, Everywhere, IdentBuf, InputOnly, IntType, Lifetime,
+    LifetimeEnv, LifetimeLowerer, LookupId, MaybeOwn, Method, NonOptional, OpaqueDef, OpaquePath,
+    Optional, OutStructDef, OutStructField, OutStructPath, OutType, Param, ParamLifetimeLowerer,
+    ParamSelf, PrimitiveType, ReturnLifetimeLowerer, ReturnType, ReturnableStructPath,
+    SelfParamLifetimeLowerer, SelfType, Slice, SpecialMethod, SpecialMethodPresence, StructDef,
+    StructField, StructPath, SuccessType, SymbolId, TraitDef, TraitParamSelf, TraitPath,
+    TyPosition, Type, TypeDef, TypeId,
+};
+use crate::ast::attrs::AttrInheritContext;
+use crate::{ast, Env};
+use core::fmt;
+use strck::IntoCk;
+
+/// An error from lowering the AST to the HIR.
+#[derive(Debug)]
+#[non_exhaustive]
+pub enum LoweringError {
+    /// The purpose of having this is that translating to the HIR has enormous
+    /// potential for really detailed error handling and giving suggestions.
+    ///
+    /// Unfortunately, working out what the error enum should look like to enable
+    /// this is really hard. The plan is that once the lowering code is completely
+    /// written, we ctrl+F for `"LoweringError::Other"` in the lowering code, and turn every
+    /// instance into an specialized enum variant, generalizing where possible
+    /// without losing any information.
+    Other(String),
+}
+
+impl fmt::Display for LoweringError {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        match *self {
+            Self::Other(ref s) => s.fmt(f),
+        }
+    }
+}
+
+#[derive(Default, Clone)]
+pub struct ErrorContext {
+    item: String,
+    subitem: Option<String>,
+}
+
+impl fmt::Display for ErrorContext {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        if let Some(ref subitem) = self.subitem {
+            write!(f, "{}::{subitem}", self.item)
+        } else {
+            self.item.fmt(f)
+        }
+    }
+}
+
+impl fmt::Debug for ErrorContext {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        fmt::Display::fmt(self, f)
+    }
+}
+
+/// An error store, which one can push errors to. It keeps track of the
+/// current "context" for the error, usually a type or a type::method. `'tree`
+/// is the AST/HIR tree it is temporarily borrowing from for the context.
+#[derive(Default)]
+pub struct ErrorStore<'tree> {
+    /// The errors
+    errors: Vec<ErrorAndContext>,
+    /// The current context (types, modules)
+    item: &'tree str,
+    /// The current sub-item context (methods, etc)
+    subitem: Option<&'tree str>,
+}
+
+pub type ErrorAndContext = (ErrorContext, LoweringError);
+
+impl<'tree> ErrorStore<'tree> {
+    /// Push an error to the error store
+    pub fn push(&mut self, error: LoweringError) {
+        let context = ErrorContext {
+            item: self.item.into(),
+            subitem: self.subitem.map(|s| s.into()),
+        };
+        self.errors.push((context, error));
+    }
+
+    pub(super) fn take_errors(&mut self) -> Vec<ErrorAndContext> {
+        core::mem::take(&mut self.errors)
+    }
+
+    pub(super) fn is_empty(&self) -> bool {
+        self.errors.is_empty()
+    }
+
+    pub(super) fn set_item(&mut self, item: &'tree str) {
+        self.item = item;
+        self.subitem = None;
+    }
+    pub(super) fn set_subitem(&mut self, subitem: &'tree str) {
+        self.subitem = Some(subitem);
+    }
+}
+
+pub(super) struct LoweringContext<'ast> {
+    pub lookup_id: LookupId<'ast>,
+    pub errors: ErrorStore<'ast>,
+    pub env: &'ast Env,
+    pub attr_validator: Box<dyn AttributeValidator>,
+}
+
+/// An item and the info needed to
+pub(crate) struct ItemAndInfo<'ast, Ast> {
+    pub(crate) item: &'ast Ast,
+    pub(crate) in_path: &'ast ast::Path,
+    /// Any parent attributes resolved from the module, for a type context
+    pub(crate) ty_parent_attrs: Attrs,
+
+    /// Any parent attributes resolved from the module, for a method context
+    pub(crate) method_parent_attrs: Attrs,
+    pub(crate) id: SymbolId,
+}
+
+impl<'ast> LoweringContext<'ast> {
+    /// Lowers an [`ast::Ident`]s into an [`hir::IdentBuf`].
+    ///
+    /// If there are any errors, they're pushed to `errors` and `Err` is returned.
+    pub(super) fn lower_ident(
+        &mut self,
+        ident: &ast::Ident,
+        context: &'static str,
+    ) -> Result<IdentBuf, ()> {
+        match ident.as_str().ck() {
+            Ok(name) => Ok(name.to_owned()),
+            Err(e) => {
+                self.errors.push(LoweringError::Other(format!(
+                    "Ident `{ident}` from {context} could not be turned into a Rust ident: {e}"
+                )));
+                Err(())
+            }
+        }
+    }
+
+    /// Lowers multiple items at once
+    fn lower_all<Ast: 'static, Hir>(
+        &mut self,
+        ast_defs: impl ExactSizeIterator<Item = ItemAndInfo<'ast, Ast>>,
+        lower: impl Fn(&mut Self, ItemAndInfo<'ast, Ast>) -> Result<Hir, ()>,
+    ) -> Result<Vec<Hir>, ()> {
+        let mut hir_types = Ok(Vec::with_capacity(ast_defs.len()));
+
+        for def in ast_defs {
+            let hir_type = lower(self, def);
+
+            match (hir_type, &mut hir_types) {
+                (Ok(hir_type), Ok(hir_types)) => hir_types.push(hir_type),
+                _ => hir_types = Err(()),
+            }
+        }
+
+        hir_types
+    }
+
+    pub(super) fn lower_all_enums(
+        &mut self,
+        ast_defs: impl ExactSizeIterator<Item = ItemAndInfo<'ast, ast::Enum>>,
+    ) -> Result<Vec<EnumDef>, ()> {
+        self.lower_all(ast_defs, Self::lower_enum)
+    }
+    pub(super) fn lower_all_structs(
+        &mut self,
+        ast_defs: impl ExactSizeIterator<Item = ItemAndInfo<'ast, ast::Struct>>,
+    ) -> Result<Vec<StructDef>, ()> {
+        self.lower_all(ast_defs, Self::lower_struct)
+    }
+    pub(super) fn lower_all_out_structs(
+        &mut self,
+        ast_defs: impl ExactSizeIterator<Item = ItemAndInfo<'ast, ast::Struct>>,
+    ) -> Result<Vec<OutStructDef>, ()> {
+        self.lower_all(ast_defs, Self::lower_out_struct)
+    }
+    pub(super) fn lower_all_opaques(
+        &mut self,
+        ast_defs: impl ExactSizeIterator<Item = ItemAndInfo<'ast, ast::OpaqueStruct>>,
+    ) -> Result<Vec<OpaqueDef>, ()> {
+        self.lower_all(ast_defs, Self::lower_opaque)
+    }
+    pub(super) fn lower_all_traits(
+        &mut self,
+        ast_defs: impl ExactSizeIterator<Item = ItemAndInfo<'ast, ast::Trait>>,
+    ) -> Result<Vec<TraitDef>, ()> {
+        self.lower_all(ast_defs, Self::lower_trait)
+    }
+
+    fn lower_enum(&mut self, item: ItemAndInfo<'ast, ast::Enum>) -> Result<EnumDef, ()> {
+        let ast_enum = item.item;
+        self.errors.set_item(ast_enum.name.as_str());
+        let name = self.lower_ident(&ast_enum.name, "enum name");
+        let attrs = self.attr_validator.attr_from_ast(
+            &ast_enum.attrs,
+            &item.ty_parent_attrs,
+            &mut self.errors,
+        );
+
+        let mut variants = Ok(Vec::with_capacity(ast_enum.variants.len()));
+        let variant_parent_attrs = attrs.for_inheritance(AttrInheritContext::Variant);
+        for (ident, discriminant, docs, attrs) in ast_enum.variants.iter() {
+            let name = self.lower_ident(ident, "enum variant");
+            let attrs =
+                self.attr_validator
+                    .attr_from_ast(attrs, &variant_parent_attrs, &mut self.errors);
+            match (name, &mut variants) {
+                (Ok(name), Ok(variants)) => {
+                    let variant = EnumVariant {
+                        docs: docs.clone(),
+                        name,
+                        discriminant: *discriminant,
+                        attrs,
+                    };
+                    self.attr_validator.validate(
+                        &variant.attrs,
+                        AttributeContext::EnumVariant(&variant),
+                        &mut self.errors,
+                    );
+                    variants.push(variant);
+                }
+                _ => variants = Err(()),
+            }
+        }
+
+        let mut special_method_presence = SpecialMethodPresence::default();
+        let methods = if attrs.disable {
+            Vec::new()
+        } else {
+            self.lower_all_methods(
+                &ast_enum.methods[..],
+                item.in_path,
+                &item.method_parent_attrs,
+                item.id.try_into()?,
+                &mut special_method_presence,
+            )?
+        };
+
+        let def = EnumDef::new(
+            ast_enum.docs.clone(),
+            name?,
+            variants?,
+            methods,
+            attrs,
+            special_method_presence,
+        );
+
+        self.attr_validator.validate(
+            &def.attrs,
+            AttributeContext::Type(TypeDef::from(&def)),
+            &mut self.errors,
+        );
+
+        Ok(def)
+    }
+
+    fn lower_opaque(
+        &mut self,
+        item: ItemAndInfo<'ast, ast::OpaqueStruct>,
+    ) -> Result<OpaqueDef, ()> {
+        let ast_opaque = item.item;
+        self.errors.set_item(ast_opaque.name.as_str());
+        let name = self.lower_ident(&ast_opaque.name, "opaque name");
+        let dtor_abi_name = self.lower_ident(&ast_opaque.dtor_abi_name, "opaque dtor abi name");
+
+        let attrs = self.attr_validator.attr_from_ast(
+            &ast_opaque.attrs,
+            &item.ty_parent_attrs,
+            &mut self.errors,
+        );
+        let mut special_method_presence = SpecialMethodPresence::default();
+        let methods = if attrs.disable {
+            Vec::new()
+        } else {
+            self.lower_all_methods(
+                &ast_opaque.methods[..],
+                item.in_path,
+                &item.method_parent_attrs,
+                item.id.try_into()?,
+                &mut special_method_presence,
+            )?
+        };
+        let lifetimes = self.lower_type_lifetime_env(&ast_opaque.lifetimes);
+
+        let def = OpaqueDef::new(
+            ast_opaque.docs.clone(),
+            name?,
+            methods,
+            attrs,
+            lifetimes?,
+            special_method_presence,
+            dtor_abi_name?,
+        );
+        self.attr_validator.validate(
+            &def.attrs,
+            AttributeContext::Type(TypeDef::from(&def)),
+            &mut self.errors,
+        );
+        Ok(def)
+    }
+
+    fn lower_struct(&mut self, item: ItemAndInfo<'ast, ast::Struct>) -> Result<StructDef, ()> {
+        let ast_struct = item.item;
+        self.errors.set_item(ast_struct.name.as_str());
+        let struct_name = self.lower_ident(&ast_struct.name, "struct name")?;
+
+        let mut fields = Ok(Vec::with_capacity(ast_struct.fields.len()));
+
+        let attrs = self.attr_validator.attr_from_ast(
+            &ast_struct.attrs,
+            &item.ty_parent_attrs,
+            &mut self.errors,
+        );
+        // Only compute fields if the type isn't disabled, otherwise we may encounter forbidden types
+        if !attrs.disable {
+            for (name, ty, docs, attrs) in ast_struct.fields.iter() {
+                let name = self.lower_ident(name, "struct field name")?;
+                if !ty.is_ffi_safe() {
+                    let ffisafe = ty.ffi_safe_version();
+                    self.errors.push(LoweringError::Other(format!(
+                        "Found FFI-unsafe type {ty} in struct field {struct_name}.{name}, consider using {ffisafe}",
+                    )));
+                }
+                let ty = self.lower_type::<Everywhere>(
+                    ty,
+                    &mut &ast_struct.lifetimes,
+                    false,
+                    item.in_path,
+                );
+
+                let field_attrs =
+                    self.attr_validator
+                        .attr_from_ast(attrs, &Attrs::default(), &mut self.errors);
+
+                self.attr_validator.validate(
+                    &field_attrs,
+                    AttributeContext::Field,
+                    &mut self.errors,
+                );
+
+                match (ty, &mut fields) {
+                    (Ok(ty), Ok(fields)) => fields.push(StructField {
+                        docs: docs.clone(),
+                        name,
+                        ty,
+                        attrs: field_attrs,
+                    }),
+                    _ => fields = Err(()),
+                }
+            }
+        }
+        let lifetimes = self.lower_type_lifetime_env(&ast_struct.lifetimes);
+
+        let mut special_method_presence = SpecialMethodPresence::default();
+        let methods = if attrs.disable {
+            Vec::new()
+        } else if ast_struct.fields.is_empty() {
+            if !ast_struct.methods.is_empty() {
+                self.errors.push(LoweringError::Other(format!(
+                    "Methods on ZST structs are not yet implemented: {}",
+                    ast_struct.name
+                )));
+                return Err(());
+            } else {
+                Vec::new()
+            }
+        } else {
+            self.lower_all_methods(
+                &ast_struct.methods[..],
+                item.in_path,
+                &item.method_parent_attrs,
+                item.id.try_into()?,
+                &mut special_method_presence,
+            )?
+        };
+        let def = StructDef::new(
+            ast_struct.docs.clone(),
+            struct_name,
+            fields?,
+            methods,
+            attrs,
+            lifetimes?,
+            special_method_presence,
+        );
+
+        self.attr_validator.validate(
+            &def.attrs,
+            AttributeContext::Type(TypeDef::from(&def)),
+            &mut self.errors,
+        );
+        Ok(def)
+    }
+
+    fn lower_trait(&mut self, item: ItemAndInfo<'ast, ast::Trait>) -> Result<TraitDef, ()> {
+        let ast_trait = item.item;
+        self.errors.set_item(ast_trait.name.as_str());
+        let trait_name = self.lower_ident(&ast_trait.name, "trait name")?;
+
+        let attrs = self.attr_validator.attr_from_ast(
+            &ast_trait.attrs,
+            &item.ty_parent_attrs,
+            &mut self.errors,
+        );
+
+        let fcts = if attrs.disable {
+            Vec::new()
+        } else {
+            let mut fcts = Vec::with_capacity(ast_trait.methods.len());
+            for ast_trait_method in ast_trait.methods.iter() {
+                fcts.push(self.lower_trait_method(ast_trait_method, item.in_path, &attrs)?);
+            }
+            fcts
+        };
+        let lifetimes = self.lower_type_lifetime_env(&ast_trait.lifetimes);
+        let def = TraitDef::new(ast_trait.docs.clone(), trait_name, fcts, attrs, lifetimes?);
+
+        self.attr_validator
+            .validate(&def.attrs, AttributeContext::Trait(&def), &mut self.errors);
+        Ok(def)
+    }
+
+    fn lower_trait_method(
+        &mut self,
+        ast_trait_method: &'ast ast::TraitMethod,
+        in_path: &ast::Path,
+        parent_trait_attrs: &Attrs,
+    ) -> Result<Callback, ()> {
+        self.errors.set_subitem(ast_trait_method.name.as_str());
+        let name = ast_trait_method.name.clone();
+        let self_param_ltl = SelfParamLifetimeLowerer::new(&ast_trait_method.lifetimes, self)?;
+        let (param_self, mut param_ltl) =
+            if let Some(self_param) = ast_trait_method.self_param.as_ref() {
+                let (param_self, param_ltl) =
+                    self.lower_trait_self_param(self_param, self_param_ltl, in_path)?;
+                (Some(param_self), param_ltl)
+            } else {
+                (None, SelfParamLifetimeLowerer::no_self_ref(self_param_ltl))
+            };
+
+        let params =
+            self.lower_many_callback_params(&ast_trait_method.params, &mut param_ltl, in_path)?;
+
+        let output = if let Some(out_ty) = &ast_trait_method.output_type {
+            Some(self.lower_type(out_ty, &mut param_ltl, false /* in_struct */, in_path)?)
+        } else {
+            None
+        };
+
+        let attrs = self.attr_validator.attr_from_ast(
+            &ast_trait_method.attrs,
+            parent_trait_attrs,
+            &mut self.errors,
+        );
+
+        Ok(Callback {
+            param_self,
+            params,
+            output: Box::new(output),
+            name: Some(self.lower_ident(&name, "trait name")?),
+            attrs: Some(attrs),
+            docs: Some(ast_trait_method.docs.clone()),
+        })
+    }
+
+    fn lower_out_struct(
+        &mut self,
+        item: ItemAndInfo<'ast, ast::Struct>,
+    ) -> Result<OutStructDef, ()> {
+        let ast_out_struct = item.item;
+        self.errors.set_item(ast_out_struct.name.as_str());
+        let name = self.lower_ident(&ast_out_struct.name, "out-struct name");
+
+        let attrs = self.attr_validator.attr_from_ast(
+            &ast_out_struct.attrs,
+            &item.ty_parent_attrs,
+            &mut self.errors,
+        );
+        let fields = if ast_out_struct.fields.is_empty() {
+            self.errors.push(LoweringError::Other(format!(
+                "struct `{}` is a ZST because it has no fields",
+                ast_out_struct.name
+            )));
+            Err(())
+        } else {
+            let mut fields = Ok(Vec::with_capacity(ast_out_struct.fields.len()));
+            // Only compute fields if the type isn't disabled, otherwise we may encounter forbidden types
+            if !attrs.disable {
+                for (name, ty, docs, attrs) in ast_out_struct.fields.iter() {
+                    let name = self.lower_ident(name, "out-struct field name");
+                    let ty = self.lower_out_type(
+                        ty,
+                        &mut &ast_out_struct.lifetimes,
+                        item.in_path,
+                        true,
+                        false,
+                    );
+
+                    match (name, ty, &mut fields) {
+                        (Ok(name), Ok(ty), Ok(fields)) => fields.push(OutStructField {
+                            docs: docs.clone(),
+                            name,
+                            ty,
+                            attrs: self.attr_validator.attr_from_ast(
+                                attrs,
+                                &Attrs::default(),
+                                &mut self.errors,
+                            ),
+                        }),
+                        _ => fields = Err(()),
+                    }
+                }
+            }
+
+            fields
+        };
+        let mut special_method_presence = SpecialMethodPresence::default();
+        let methods = if attrs.disable {
+            Vec::new()
+        } else {
+            self.lower_all_methods(
+                &ast_out_struct.methods[..],
+                item.in_path,
+                &item.method_parent_attrs,
+                item.id.try_into()?,
+                &mut special_method_presence,
+            )?
+        };
+
+        let lifetimes = self.lower_type_lifetime_env(&ast_out_struct.lifetimes);
+        let def = OutStructDef::new(
+            ast_out_struct.docs.clone(),
+            name?,
+            fields?,
+            methods,
+            attrs,
+            lifetimes?,
+            special_method_presence,
+        );
+
+        self.attr_validator.validate(
+            &def.attrs,
+            AttributeContext::Type(TypeDef::from(&def)),
+            &mut self.errors,
+        );
+        Ok(def)
+    }
+
+    /// Lowers an [`ast::Method`]s an [`hir::Method`].
+    ///
+    /// If there are any errors, they're pushed to `errors` and `None` is returned.
+    fn lower_method(
+        &mut self,
+        method: &'ast ast::Method,
+        in_path: &ast::Path,
+        attrs: Attrs,
+        self_id: TypeId,
+        special_method_presence: &mut SpecialMethodPresence,
+    ) -> Result<Method, ()> {
+        let name = self.lower_ident(&method.name, "method name");
+
+        let (ast_params, takes_write) = match method.params.split_last() {
+            Some((last, remaining)) if last.is_write() => (remaining, true),
+            _ => (&method.params[..], false),
+        };
+
+        let self_param_ltl = SelfParamLifetimeLowerer::new(&method.lifetime_env, self)?;
+
+        let (param_self, param_ltl) = if let Some(self_param) = method.self_param.as_ref() {
+            let (param_self, param_ltl) =
+                self.lower_self_param(self_param, self_param_ltl, &method.abi_name, in_path)?;
+            (Some(param_self), param_ltl)
+        } else {
+            (None, SelfParamLifetimeLowerer::no_self_ref(self_param_ltl))
+        };
+
+        let (params, return_ltl) = self.lower_many_params(ast_params, param_ltl, in_path)?;
+
+        let (output, lifetime_env) = self.lower_return_type(
+            method.return_type.as_ref(),
+            takes_write,
+            return_ltl,
+            in_path,
+        )?;
+
+        let abi_name = self.lower_ident(&method.abi_name, "method abi name")?;
+        let hir_method = Method {
+            docs: method.docs.clone(),
+            name: name?,
+            abi_name,
+            lifetime_env,
+            param_self,
+            params,
+            output,
+            attrs,
+        };
+
+        self.attr_validator.validate(
+            &hir_method.attrs,
+            AttributeContext::Method(&hir_method, self_id, special_method_presence),
+            &mut self.errors,
+        );
+
+        let is_comparison = matches!(
+            hir_method.attrs.special_method,
+            Some(SpecialMethod::Comparison)
+        );
+        if is_comparison && method.return_type != Some(ast::TypeName::Ordering) {
+            self.errors.push(LoweringError::Other(
+                "Found comparison method that does not return cmp::Ordering".into(),
+            ));
+            return Err(());
+        }
+
+        Ok(hir_method)
+    }
+
+    /// Lowers many [`ast::Method`]s into a vector of [`hir::Method`]s.
+    ///
+    /// If there are any errors, they're pushed to `errors` and `None` is returned.
+    fn lower_all_methods(
+        &mut self,
+        ast_methods: &'ast [ast::Method],
+        in_path: &ast::Path,
+        method_parent_attrs: &Attrs,
+        self_id: TypeId,
+        special_method_presence: &mut SpecialMethodPresence,
+    ) -> Result<Vec<Method>, ()> {
+        let mut methods = Ok(Vec::with_capacity(ast_methods.len()));
+
+        for method in ast_methods {
+            self.errors.set_subitem(method.name.as_str());
+            let attrs = self.attr_validator.attr_from_ast(
+                &method.attrs,
+                method_parent_attrs,
+                &mut self.errors,
+            );
+            if attrs.disable {
+                continue;
+            }
+            let method =
+                self.lower_method(method, in_path, attrs, self_id, special_method_presence);
+            match (method, &mut methods) {
+                (Ok(method), Ok(methods)) => {
+                    methods.push(method);
+                }
+                _ => methods = Err(()),
+            }
+        }
+
+        methods
+    }
+
+    /// Lowers an [`ast::TypeName`]s into a [`hir::Type`] (for non-output types)
+    ///
+    /// If there are any errors, they're pushed to `errors` and `None` is returned.
+    fn lower_type<P: TyPosition<StructPath = StructPath, OpaqueOwnership = Borrow>>(
+        &mut self,
+        ty: &ast::TypeName,
+        ltl: &mut impl LifetimeLowerer,
+        in_struct: bool,
+        in_path: &ast::Path,
+    ) -> Result<Type<P>, ()> {
+        match ty {
+            ast::TypeName::Primitive(prim) => Ok(Type::Primitive(PrimitiveType::from_ast(*prim))),
+            ast::TypeName::Ordering => {
+                self.errors.push(LoweringError::Other("Found cmp::Ordering in parameter or struct field, it is only allowed in return types".to_string()));
+                Err(())
+            }
+            ast::TypeName::Named(path) | ast::TypeName::SelfType(path) => match path
+                .resolve(in_path, self.env)
+            {
+                ast::CustomType::Struct(strct) => {
+                    if strct.fields.is_empty() {
+                        self.errors.push(LoweringError::Other(format!(
+                            "zero-size types are not allowed as method arguments: {ty} in {path}"
+                        )));
+                        return Err(());
+                    }
+                    if let Some(tcx_id) = self.lookup_id.resolve_struct(strct) {
+                        let lifetimes =
+                            ltl.lower_generics(&path.lifetimes[..], &strct.lifetimes, ty.is_self());
+
+                        Ok(Type::Struct(StructPath::new(lifetimes, tcx_id)))
+                    } else if self.lookup_id.resolve_out_struct(strct).is_some() {
+                        self.errors.push(LoweringError::Other(format!("found struct in input that is marked with #[diplomat::out]: {ty} in {path}")));
+                        Err(())
+                    } else {
+                        unreachable!("struct `{}` wasn't found in the set of structs or out-structs, this is a bug.", strct.name);
+                    }
+                }
+                ast::CustomType::Opaque(_) => {
+                    self.errors.push(LoweringError::Other(format!(
+                        "Opaque passed by value: {path}"
+                    )));
+                    Err(())
+                }
+                ast::CustomType::Enum(enm) => {
+                    let tcx_id = self
+                        .lookup_id
+                        .resolve_enum(enm)
+                        .expect("can't find enum in lookup map, which contains all enums from env");
+
+                    Ok(Type::Enum(EnumPath::new(tcx_id)))
+                }
+            },
+            ast::TypeName::ImplTrait(path) => {
+                if !self.attr_validator.attrs_supported().traits {
+                    self.errors.push(LoweringError::Other(
+                        "Traits are not supported by this backend".into(),
+                    ));
+                }
+                let trt = path.resolve_trait(in_path, self.env);
+                let tcx_id = self
+                    .lookup_id
+                    .resolve_trait(&trt)
+                    .expect("can't find trait in lookup map, which contains all traits from env");
+                let lifetimes =
+                    ltl.lower_generics(&path.lifetimes[..], &trt.lifetimes, ty.is_self());
+
+                Ok(Type::ImplTrait(P::build_trait_path(TraitPath::new(
+                    lifetimes, tcx_id,
+                ))))
+            }
+            ast::TypeName::Reference(lifetime, mutability, ref_ty) => match ref_ty.as_ref() {
+                ast::TypeName::Named(path) | ast::TypeName::SelfType(path) => {
+                    match path.resolve(in_path, self.env) {
+                        ast::CustomType::Opaque(opaque) => {
+                            let borrow = Borrow::new(ltl.lower_lifetime(lifetime), *mutability);
+                            let lifetimes = ltl.lower_generics(
+                                &path.lifetimes[..],
+                                &opaque.lifetimes,
+                                ref_ty.is_self(),
+                            );
+                            let tcx_id = self.lookup_id.resolve_opaque(opaque).expect(
+                            "can't find opaque in lookup map, which contains all opaques from env",
+                        );
+
+                            Ok(Type::Opaque(OpaquePath::new(
+                                lifetimes,
+                                Optional(false),
+                                borrow,
+                                tcx_id,
+                            )))
+                        }
+                        _ => {
+                            self.errors.push(LoweringError::Other(format!("found &T in input where T is a custom type, but not opaque. T = {ref_ty}")));
+                            Err(())
+                        }
+                    }
+                }
+                _ => {
+                    self.errors.push(LoweringError::Other(format!("found &T in input where T isn't a custom type and therefore not opaque. T = {ref_ty}")));
+                    Err(())
+                }
+            },
+            ast::TypeName::Box(box_ty) => {
+                self.errors.push(match box_ty.as_ref() {
+                ast::TypeName::Named(path) | ast::TypeName::SelfType(path) => {
+                    match path.resolve(in_path, self.env) {
+                        ast::CustomType::Opaque(_) => LoweringError::Other(format!("found Box<T> in input where T is an opaque, but owned opaques aren't allowed in inputs. try &T instead? T = {path}")),
+                        _ => LoweringError::Other(format!("found Box<T> in input where T is a custom type but not opaque. non-opaques can't be behind pointers, and opaques in inputs can't be owned. T = {path}")),
+                    }
+                }
+                _ => LoweringError::Other(format!("found Box<T> in input where T isn't a custom type. T = {box_ty}")),
+            });
+                Err(())
+            }
+            ast::TypeName::Option(opt_ty, stdlib) => {
+                match opt_ty.as_ref() {
+                    ast::TypeName::Reference(lifetime, mutability, ref_ty) => match ref_ty.as_ref()
+                    {
+                        ast::TypeName::Named(path) | ast::TypeName::SelfType(path) => match path
+                            .resolve(in_path, self.env)
+                        {
+                            ast::CustomType::Opaque(opaque) => {
+                                if *stdlib == ast::StdlibOrDiplomat::Diplomat {
+                                    self.errors.push(LoweringError::Other("found DiplomatOption<&T>, please use Option<&T> (DiplomatOption is for primitives, structs, and enums)".to_string()));
+                                    return Err(());
+                                }
+                                let borrow = Borrow::new(ltl.lower_lifetime(lifetime), *mutability);
+                                let lifetimes = ltl.lower_generics(
+                                    &path.lifetimes,
+                                    &opaque.lifetimes,
+                                    ref_ty.is_self(),
+                                );
+                                let tcx_id = self.lookup_id.resolve_opaque(opaque).expect(
+                                    "can't find opaque in lookup map, which contains all opaques from env",
+                                );
+
+                                Ok(Type::Opaque(OpaquePath::new(
+                                    lifetimes,
+                                    Optional(true),
+                                    borrow,
+                                    tcx_id,
+                                )))
+                            }
+                            _ => {
+                                self.errors.push(LoweringError::Other(format!("found Option<&T> in input where T is a custom type, but it's not opaque. T = {ref_ty}")));
+                                Err(())
+                            }
+                        },
+                        _ => {
+                            self.errors.push(LoweringError::Other(format!("found Option<&T> in input, but T isn't a custom type and therefore not opaque. T = {ref_ty}")));
+                            Err(())
+                        }
+                    },
+                    ast::TypeName::Named(path) | ast::TypeName::SelfType(path) => {
+                        match path.resolve(in_path, self.env) {
+                            ast::CustomType::Opaque(_) => {
+                                self.errors.push(LoweringError::Other("Found Option<T> where T is opaque, opaque types must be behind a reference".into()));
+                                Err(())
+                            }
+                            _ => {
+                                if in_struct && *stdlib == ast::StdlibOrDiplomat::Stdlib {
+                                    self.errors.push(LoweringError::Other("Found Option<T> for struct/enum T in a struct field, please use DiplomatOption<T>".into()));
+                                    return Err(());
+                                }
+                                if !self.attr_validator.attrs_supported().option {
+                                    self.errors.push(LoweringError::Other("Options of structs/enums/primitives not supported by this backend".into()));
+                                }
+                                let inner = self.lower_type(opt_ty, ltl, in_struct, in_path)?;
+                                Ok(Type::DiplomatOption(Box::new(inner)))
+                            }
+                        }
+                    }
+                    ast::TypeName::Primitive(prim) => {
+                        if in_struct && *stdlib == ast::StdlibOrDiplomat::Stdlib {
+                            self.errors.push(LoweringError::Other("Found Option<T> for primitive T in a struct field, please use DiplomatOption<T>".into()));
+                            return Err(());
+                        }
+                        if !self.attr_validator.attrs_supported().option {
+                            self.errors.push(LoweringError::Other(
+                                "Options of structs/enums/primitives not supported by this backend"
+                                    .into(),
+                            ));
+                        }
+                        Ok(Type::DiplomatOption(Box::new(Type::Primitive(
+                            PrimitiveType::from_ast(*prim),
+                        ))))
+                    }
+                    ast::TypeName::Box(box_ty) => {
+                        // we could see whats in the box here too
+                        self.errors.push(LoweringError::Other(format!("found Option<Box<T>> in input, but box isn't allowed in inputs. T = {box_ty}")));
+                        Err(())
+                    }
+                    _ => {
+                        self.errors.push(LoweringError::Other(format!("found Option<T> in input, where T isn't a reference but Option<T> in inputs requires that T is a reference to an opaque. T = {opt_ty}")));
+                        Err(())
+                    }
+                }
+            }
+            ast::TypeName::Result(_, _, _) => {
+                self.errors.push(LoweringError::Other(
+                    "Results can only appear as the top-level return type of methods".into(),
+                ));
+                Err(())
+            }
+            ast::TypeName::Write => {
+                self.errors.push(LoweringError::Other(
+                    "DiplomatWrite can only appear as the last parameter of a method".into(),
+                ));
+                Err(())
+            }
+            ast::TypeName::StrReference(lifetime, encoding, _stdlib) => {
+                let new_lifetime = lifetime.as_ref().map(|lt| ltl.lower_lifetime(lt));
+                if let Some(super::MaybeStatic::Static) = new_lifetime {
+                    if !self.attr_validator.attrs_supported().static_slices {
+                        self.errors.push(LoweringError::Other(
+                            "'static string slice types are not supported. Try #[diplomat::attr(not(supports = static_slices), disable)]".into()
+                        ));
+                    }
+                }
+                Ok(Type::Slice(Slice::Str(new_lifetime, *encoding)))
+            }
+            ast::TypeName::StrSlice(encoding, _stdlib) => Ok(Type::Slice(Slice::Strs(*encoding))),
+            ast::TypeName::PrimitiveSlice(lm, prim, _stdlib) => {
+                let new_lifetime = lm
+                    .as_ref()
+                    .map(|(lt, m)| Borrow::new(ltl.lower_lifetime(lt), *m));
+
+                if let Some(b) = new_lifetime {
+                    if let super::MaybeStatic::Static = b.lifetime {
+                        if !self.attr_validator.attrs_supported().static_slices {
+                            self.errors.push(LoweringError::Other(
+                                format!("'static {prim:?} slice types not supported. Try #[diplomat::attr(not(supports = static_slices), disable)]")
+                            ));
+                        }
+                    }
+                }
+
+                Ok(Type::Slice(Slice::Primitive(
+                    new_lifetime,
+                    PrimitiveType::from_ast(*prim),
+                )))
+            }
+            ast::TypeName::Function(input_types, out_type) => {
+                if !self.attr_validator.attrs_supported().callbacks {
+                    self.errors.push(LoweringError::Other(
+                        "Callback arguments are not supported by this backend".into(),
+                    ));
+                }
+                if in_struct {
+                    self.errors.push(LoweringError::Other(
+                        "Callbacks currently unsupported in structs".into(),
+                    ));
+                    return Err(());
+                }
+                let mut params: Vec<CallbackParam> = Vec::new();
+                for in_ty in input_types.iter() {
+                    let hir_in_ty = self
+                        .lower_out_type(in_ty, ltl, in_path, false, false)
+                        .unwrap();
+                    if hir_in_ty.lifetimes().next().is_some() {
+                        self.errors.push(LoweringError::Other("Callback parameters can't be borrowed, and therefore can't have lifetimes".into()));
+                        return Err(());
+                    }
+                    params.push(CallbackParam {
+                        ty: hir_in_ty,
+                        name: None,
+                    })
+                }
+                Ok(Type::Callback(P::build_callback(Callback {
+                    param_self: None,
+                    params,
+                    output: Box::new(match **out_type {
+                        ast::TypeName::Unit => None,
+                        _ => Some(self.lower_type(out_type, ltl, in_struct, in_path)?),
+                    }),
+                    name: None,
+                    attrs: None,
+                    docs: None,
+                })))
+            }
+            ast::TypeName::Unit => {
+                self.errors.push(LoweringError::Other("Unit types can only appear as the return value of a method, or as the Ok/Err variants of a returned result".into()));
+                Err(())
+            }
+        }
+    }
+
+    /// Lowers an [`ast::TypeName`]s into an [`hir::OutType`].
+    ///
+    /// If there are any errors, they're pushed to `errors` and `None` is returned.
+    fn lower_out_type(
+        &mut self,
+        ty: &ast::TypeName,
+        ltl: &mut impl LifetimeLowerer,
+        in_path: &ast::Path,
+        in_struct: bool,
+        in_result_option: bool,
+    ) -> Result<OutType, ()> {
+        match ty {
+            ast::TypeName::Primitive(prim) => {
+                Ok(OutType::Primitive(PrimitiveType::from_ast(*prim)))
+            }
+            ast::TypeName::Ordering => {
+                if in_struct {
+                    self.errors.push(LoweringError::Other(
+                        "Found cmp::Ordering in struct field, it is only allowed in return types"
+                            .to_string(),
+                    ));
+                    Err(())
+                } else {
+                    Ok(Type::Primitive(PrimitiveType::Int(IntType::I8)))
+                }
+            }
+            ast::TypeName::Named(path) | ast::TypeName::SelfType(path) => {
+                match path.resolve(in_path, self.env) {
+                    ast::CustomType::Struct(strct) => {
+                        if !in_result_option && strct.fields.is_empty() {
+                            self.errors.push(LoweringError::Other(format!("Found zero-size struct outside a `Result` or `Option`: {ty} in {in_path}")));
+                            return Err(());
+                        }
+                        let lifetimes =
+                            ltl.lower_generics(&path.lifetimes, &strct.lifetimes, ty.is_self());
+
+                        if let Some(tcx_id) = self.lookup_id.resolve_struct(strct) {
+                            Ok(OutType::Struct(ReturnableStructPath::Struct(
+                                StructPath::new(lifetimes, tcx_id),
+                            )))
+                        } else if let Some(tcx_id) = self.lookup_id.resolve_out_struct(strct) {
+                            Ok(OutType::Struct(ReturnableStructPath::OutStruct(
+                                OutStructPath::new(lifetimes, tcx_id),
+                            )))
+                        } else {
+                            unreachable!("struct `{}` wasn't found in the set of structs or out-structs, this is a bug.", strct.name);
+                        }
+                    }
+                    ast::CustomType::Opaque(_) => {
+                        self.errors.push(LoweringError::Other(format!(
+                            "Opaque passed by value in input: {path}"
+                        )));
+                        Err(())
+                    }
+                    ast::CustomType::Enum(enm) => {
+                        let tcx_id = self.lookup_id.resolve_enum(enm).expect(
+                            "can't find enum in lookup map, which contains all enums from env",
+                        );
+
+                        Ok(OutType::Enum(EnumPath::new(tcx_id)))
+                    }
+                }
+            }
+            ast::TypeName::Reference(lifetime, mutability, ref_ty) => match ref_ty.as_ref() {
+                ast::TypeName::Named(path) | ast::TypeName::SelfType(path) => {
+                    match path.resolve(in_path, self.env) {
+                        ast::CustomType::Opaque(opaque) => {
+                            let borrow = Borrow::new(ltl.lower_lifetime(lifetime), *mutability);
+                            let lifetimes = ltl.lower_generics(
+                                &path.lifetimes,
+                                &opaque.lifetimes,
+                                ref_ty.is_self(),
+                            );
+                            let tcx_id = self.lookup_id.resolve_opaque(opaque).expect(
+                            "can't find opaque in lookup map, which contains all opaques from env",
+                        );
+
+                            Ok(OutType::Opaque(OpaquePath::new(
+                                lifetimes,
+                                Optional(false),
+                                MaybeOwn::Borrow(borrow),
+                                tcx_id,
+                            )))
+                        }
+                        _ => {
+                            self.errors.push(LoweringError::Other(format!("found &T in output where T is a custom type, but not opaque. T = {ref_ty}")));
+                            Err(())
+                        }
+                    }
+                }
+                _ => {
+                    self.errors.push(LoweringError::Other(format!("found &T in output where T isn't a custom type and therefore not opaque. T = {ref_ty}")));
+                    Err(())
+                }
+            },
+            ast::TypeName::Box(box_ty) => match box_ty.as_ref() {
+                ast::TypeName::Named(path) | ast::TypeName::SelfType(path) => {
+                    match path.resolve(in_path, self.env) {
+                        ast::CustomType::Opaque(opaque) => {
+                            let lifetimes = ltl.lower_generics(
+                                &path.lifetimes,
+                                &opaque.lifetimes,
+                                box_ty.is_self(),
+                            );
+                            let tcx_id = self.lookup_id.resolve_opaque(opaque).expect(
+                            "can't find opaque in lookup map, which contains all opaques from env",
+                        );
+
+                            Ok(OutType::Opaque(OpaquePath::new(
+                                lifetimes,
+                                Optional(false),
+                                MaybeOwn::Own,
+                                tcx_id,
+                            )))
+                        }
+                        _ => {
+                            self.errors.push(LoweringError::Other(format!("found Box<T> in output where T is a custom type but not opaque. non-opaques can't be behind pointers. T = {path}")));
+                            Err(())
+                        }
+                    }
+                }
+                _ => {
+                    self.errors.push(LoweringError::Other(format!(
+                        "found Box<T> in output where T isn't a custom type. T = {box_ty}"
+                    )));
+                    Err(())
+                }
+            },
+            ast::TypeName::Option(opt_ty, stdlib) => match opt_ty.as_ref() {
+                ast::TypeName::Reference(lifetime, mutability, ref_ty) => match ref_ty.as_ref() {
+                    ast::TypeName::Named(path) | ast::TypeName::SelfType(path) => {
+                        match path.resolve(in_path, self.env) {
+                            ast::CustomType::Opaque(opaque) => {
+                                if *stdlib == ast::StdlibOrDiplomat::Diplomat {
+                                    self.errors.push(LoweringError::Other("found DiplomatOption<&T>, please use Option<&T> (DiplomatOption is for primitives, structs, and enums)".to_string()));
+                                    return Err(());
+                                }
+                                let borrow = Borrow::new(ltl.lower_lifetime(lifetime), *mutability);
+                                let lifetimes = ltl.lower_generics(
+                                    &path.lifetimes,
+                                    &opaque.lifetimes,
+                                    ref_ty.is_self(),
+                                );
+                                let tcx_id = self.lookup_id.resolve_opaque(opaque).expect(
+                                "can't find opaque in lookup map, which contains all opaques from env",
+                            );
+
+                                Ok(OutType::Opaque(OpaquePath::new(
+                                    lifetimes,
+                                    Optional(true),
+                                    MaybeOwn::Borrow(borrow),
+                                    tcx_id,
+                                )))
+                            }
+                            _ => {
+                                self.errors.push(LoweringError::Other(format!("found Option<&T> where T is a custom type, but it's not opaque. T = {ref_ty}")));
+                                Err(())
+                            }
+                        }
+                    }
+                    _ => {
+                        self.errors.push(LoweringError::Other(format!("found Option<&T>, but T isn't a custom type and therefore not opaque. T = {ref_ty}")));
+                        Err(())
+                    }
+                },
+                ast::TypeName::Box(box_ty) => match box_ty.as_ref() {
+                    ast::TypeName::Named(path) | ast::TypeName::SelfType(path) => {
+                        match path.resolve(in_path, self.env) {
+                            ast::CustomType::Opaque(opaque) => {
+                                if *stdlib == ast::StdlibOrDiplomat::Diplomat {
+                                    self.errors.push(LoweringError::Other("found DiplomatOption<Box<T>>, please use Option<Box<T>> (DiplomatOption is for primitives, structs, and enums)".to_string()));
+                                    return Err(());
+                                }
+                                let lifetimes = ltl.lower_generics(
+                                    &path.lifetimes,
+                                    &opaque.lifetimes,
+                                    box_ty.is_self(),
+                                );
+                                let tcx_id = self.lookup_id.resolve_opaque(opaque).expect(
+                            "can't find opaque in lookup map, which contains all opaques from env",
+                        );
+
+                                Ok(OutType::Opaque(OpaquePath::new(
+                                    lifetimes,
+                                    Optional(true),
+                                    MaybeOwn::Own,
+                                    tcx_id,
+                                )))
+                            }
+                            _ => {
+                                self.errors.push(LoweringError::Other(format!("found Option<Box<T>> where T is a custom type, but it's not opaque. T = {box_ty}")));
+                                Err(())
+                            }
+                        }
+                    }
+                    _ => {
+                        self.errors.push(LoweringError::Other(format!("found Option<Box<T>>, but T isn't a custom type and therefore not opaque. T = {box_ty}")));
+                        Err(())
+                    }
+                },
+                ast::TypeName::Named(path) | ast::TypeName::SelfType(path) => {
+                    match path.resolve(in_path, self.env) {
+                        ast::CustomType::Opaque(_) => {
+                            self.errors.push(LoweringError::Other("Found Option<T> where T is opaque, opaque types must be behind a reference".into()));
+                            Err(())
+                        }
+                        _ => {
+                            if in_struct && *stdlib == ast::StdlibOrDiplomat::Stdlib {
+                                self.errors.push(LoweringError::Other("Found Option<T> for struct/enum T in a struct field, please use DiplomatOption<T>".into()));
+                                return Err(());
+                            }
+                            if !self.attr_validator.attrs_supported().option {
+                                self.errors.push(LoweringError::Other("Options of structs/enums/primitives not supported by this backend".into()));
+                            }
+                            let inner =
+                                self.lower_out_type(opt_ty, ltl, in_path, in_struct, true)?;
+                            Ok(Type::DiplomatOption(Box::new(inner)))
+                        }
+                    }
+                }
+                ast::TypeName::Primitive(prim) => {
+                    if in_struct && *stdlib == ast::StdlibOrDiplomat::Stdlib {
+                        self.errors.push(LoweringError::Other("Found Option<T> for primitive T in a struct field, please use DiplomatOption<T>".into()));
+                        return Err(());
+                    }
+                    if !self.attr_validator.attrs_supported().option {
+                        self.errors.push(LoweringError::Other(
+                            "Options of structs/enums/primitives not supported by this backend"
+                                .into(),
+                        ));
+                    }
+                    Ok(Type::DiplomatOption(Box::new(Type::Primitive(
+                        PrimitiveType::from_ast(*prim),
+                    ))))
+                }
+                _ => {
+                    self.errors.push(LoweringError::Other(format!("found Option<T>, where T isn't a reference but Option<T> requires that T is a reference to an opaque. T = {opt_ty}")));
+                    Err(())
+                }
+            },
+            ast::TypeName::Result(_, _, _) => {
+                self.errors.push(LoweringError::Other(
+                    "Results can only appear as the top-level return type of methods".into(),
+                ));
+                Err(())
+            }
+            ast::TypeName::Write => {
+                self.errors.push(LoweringError::Other(
+                    "DiplomatWrite can only appear as the last parameter of a method".into(),
+                ));
+                Err(())
+            }
+            ast::TypeName::PrimitiveSlice(None, _, _stdlib)
+            | ast::TypeName::StrReference(None, _, _stdlib) => {
+                self.errors.push(LoweringError::Other(
+                    "Owned slices cannot be returned".into(),
+                ));
+                Err(())
+            }
+            ast::TypeName::StrReference(Some(l), encoding, _stdlib) => Ok(OutType::Slice(
+                Slice::Str(Some(ltl.lower_lifetime(l)), *encoding),
+            )),
+            ast::TypeName::StrSlice(.., _stdlib) => {
+                self.errors.push(LoweringError::Other(
+                    "String slices can only be an input type".into(),
+                ));
+                Err(())
+            }
+            ast::TypeName::PrimitiveSlice(Some((lt, m)), prim, _stdlib) => {
+                Ok(OutType::Slice(Slice::Primitive(
+                    Some(Borrow::new(ltl.lower_lifetime(lt), *m)),
+                    PrimitiveType::from_ast(*prim),
+                )))
+            }
+            ast::TypeName::Unit => {
+                self.errors.push(LoweringError::Other("Unit types can only appear as the return value of a method, or as the Ok/Err variants of a returned result".into()));
+                Err(())
+            }
+            ast::TypeName::Function(_, _) => {
+                self.errors.push(LoweringError::Other(
+                    "Function types can only be an input type".into(),
+                ));
+                Err(())
+            }
+            ast::TypeName::ImplTrait(_) => {
+                self.errors.push(LoweringError::Other(
+                    "Trait impls can only be an input type".into(),
+                ));
+                Err(())
+            }
+        }
+    }
+
+    /// Lowers an [`ast::SelfParam`] into an [`hir::ParamSelf`].
+    ///
+    /// If there are any errors, they're pushed to `errors` and `None` is returned.
+    fn lower_self_param(
+        &mut self,
+        self_param: &ast::SelfParam,
+        self_param_ltl: SelfParamLifetimeLowerer<'ast>,
+        method_full_path: &ast::Ident, // for better error msg
+        in_path: &ast::Path,
+    ) -> Result<(ParamSelf, ParamLifetimeLowerer<'ast>), ()> {
+        match self_param.path_type.resolve(in_path, self.env) {
+            ast::CustomType::Struct(strct) => {
+                if let Some(tcx_id) = self.lookup_id.resolve_struct(strct) {
+                    if self_param.reference.is_some() {
+                        self.errors.push(LoweringError::Other(format!("Method `{method_full_path}` takes a reference to a struct as a self parameter, which isn't allowed")));
+                        Err(())
+                    } else {
+                        let mut param_ltl = self_param_ltl.no_self_ref();
+
+                        // Even if we explicitly write out the type of `self` like
+                        // `self: Foo<'a>`, the `'a` is still not considered for
+                        // elision according to rustc, so is_self=true.
+                        let type_lifetimes = param_ltl.lower_generics(
+                            &self_param.path_type.lifetimes[..],
+                            &strct.lifetimes,
+                            true,
+                        );
+
+                        let attrs = self.attr_validator.attr_from_ast(
+                            &self_param.attrs,
+                            &Attrs::default(),
+                            &mut self.errors,
+                        );
+
+                        self.attr_validator.validate(
+                            &attrs,
+                            AttributeContext::SelfParam,
+                            &mut self.errors,
+                        );
+
+                        Ok((
+                            ParamSelf::new(
+                                SelfType::Struct(StructPath::new(type_lifetimes, tcx_id)),
+                                attrs,
+                            ),
+                            param_ltl,
+                        ))
+                    }
+                } else if self.lookup_id.resolve_out_struct(strct).is_some() {
+                    if let Some((lifetime, _)) = &self_param.reference {
+                        self.errors.push(LoweringError::Other(format!("Method `{method_full_path}` takes an out-struct as the self parameter, which isn't allowed. Also, it's behind a reference, `{lifetime}`, but only opaques can be behind references")));
+                        Err(())
+                    } else {
+                        self.errors.push(LoweringError::Other(format!("Method `{method_full_path}` takes an out-struct as the self parameter, which isn't allowed")));
+                        Err(())
+                    }
+                } else {
+                    unreachable!(
+                    "struct `{}` wasn't found in the set of structs or out-structs, this is a bug.",
+                    strct.name
+                );
+                }
+            }
+            ast::CustomType::Opaque(opaque) => {
+                let tcx_id = self
+                    .lookup_id
+                    .resolve_opaque(opaque)
+                    .expect("opaque is in env");
+
+                if let Some((lifetime, mutability)) = &self_param.reference {
+                    let (borrow_lifetime, mut param_ltl) = self_param_ltl.lower_self_ref(lifetime);
+                    let borrow = Borrow::new(borrow_lifetime, *mutability);
+                    let lifetimes = param_ltl.lower_generics(
+                        &self_param.path_type.lifetimes,
+                        &opaque.lifetimes,
+                        true,
+                    );
+
+                    let attrs = self.attr_validator.attr_from_ast(
+                        &self_param.attrs,
+                        &Attrs::default(),
+                        &mut self.errors,
+                    );
+
+                    self.attr_validator.validate(
+                        &attrs,
+                        AttributeContext::SelfParam,
+                        &mut self.errors,
+                    );
+
+                    Ok((
+                        ParamSelf::new(
+                            SelfType::Opaque(OpaquePath::new(
+                                lifetimes,
+                                NonOptional,
+                                borrow,
+                                tcx_id,
+                            )),
+                            attrs,
+                        ),
+                        param_ltl,
+                    ))
+                } else {
+                    self.errors.push(LoweringError::Other(format!("Method `{method_full_path}` takes an opaque by value as the self parameter, but opaques as inputs must be behind refs")));
+                    Err(())
+                }
+            }
+            ast::CustomType::Enum(enm) => {
+                let tcx_id = self.lookup_id.resolve_enum(enm).expect("enum is in env");
+
+                let attrs = self.attr_validator.attr_from_ast(
+                    &self_param.attrs,
+                    &Attrs::default(),
+                    &mut self.errors,
+                );
+
+                self.attr_validator
+                    .validate(&attrs, AttributeContext::SelfParam, &mut self.errors);
+
+                Ok((
+                    ParamSelf::new(SelfType::Enum(EnumPath::new(tcx_id)), attrs),
+                    self_param_ltl.no_self_ref(),
+                ))
+            }
+        }
+    }
+
+    fn lower_trait_self_param(
+        &mut self,
+        self_param: &ast::TraitSelfParam,
+        self_param_ltl: SelfParamLifetimeLowerer<'ast>,
+        in_path: &ast::Path,
+    ) -> Result<(TraitParamSelf, ParamLifetimeLowerer<'ast>), ()> {
+        let trt = self_param.path_trait.resolve_trait(in_path, self.env);
+        if let Some(tcx_id) = self.lookup_id.resolve_trait(&trt) {
+            // check this -- I think we should be able to have both self and non-self
+            if let Some((lifetime, _)) = &self_param.reference {
+                let (_, mut param_ltl) = self_param_ltl.lower_self_ref(lifetime);
+                let lifetimes = param_ltl.lower_generics(
+                    &self_param.path_trait.lifetimes,
+                    &trt.lifetimes,
+                    true,
+                );
+
+                Ok((
+                    TraitParamSelf::new(TraitPath::new(lifetimes, tcx_id)),
+                    param_ltl,
+                ))
+            } else {
+                let mut param_ltl = self_param_ltl.no_self_ref();
+
+                let type_lifetimes = param_ltl.lower_generics(
+                    &self_param.path_trait.lifetimes[..],
+                    &trt.lifetimes,
+                    true,
+                );
+
+                Ok((
+                    TraitParamSelf::new(TraitPath::new(type_lifetimes, tcx_id)),
+                    param_ltl,
+                ))
+            }
+        } else {
+            unreachable!(
+                "Trait `{}` wasn't found in the set of traits; this is a bug.",
+                trt.name
+            );
+        }
+    }
+
+    /// Lowers an [`ast::Param`] into an [`hir::Param`].
+    ///
+    /// If there are any errors, they're pushed to `errors` and `None` is returned.
+    ///
+    /// Note that this expects that if there was a DiplomatWrite param at the end in
+    /// the method, it's not passed into here.
+    fn lower_param(
+        &mut self,
+        param: &ast::Param,
+        ltl: &mut impl LifetimeLowerer,
+        in_path: &ast::Path,
+    ) -> Result<Param, ()> {
+        let name = self.lower_ident(&param.name, "param name");
+        let ty = self.lower_type::<InputOnly>(&param.ty, ltl, false, in_path);
+
+        // No parent attrs because parameters do not have a strictly clear parent.
+        let attrs =
+            self.attr_validator
+                .attr_from_ast(&param.attrs, &Attrs::default(), &mut self.errors);
+
+        self.attr_validator
+            .validate(&attrs, AttributeContext::Param, &mut self.errors);
+
+        Ok(Param::new(name?, ty?, attrs))
+    }
+
+    /// Lowers many [`ast::Param`]s into a vector of [`hir::Param`]s.
+    ///
+    /// If there are any errors, they're pushed to `errors` and `None` is returned.
+    ///
+    /// Note that this expects that if there was a DiplomatWrite param at the end in
+    /// the method, `ast_params` was sliced to not include it. This happens in
+    /// `self.lower_method`, the caller of this function.
+    fn lower_many_params(
+        &mut self,
+        ast_params: &[ast::Param],
+        mut param_ltl: ParamLifetimeLowerer<'ast>,
+        in_path: &ast::Path,
+    ) -> Result<(Vec<Param>, ReturnLifetimeLowerer<'ast>), ()> {
+        let mut params = Ok(Vec::with_capacity(ast_params.len()));
+
+        for param in ast_params {
+            let param = self.lower_param(param, &mut param_ltl, in_path);
+
+            match (param, &mut params) {
+                (Ok(param), Ok(params)) => {
+                    params.push(param);
+                }
+                _ => params = Err(()),
+            }
+        }
+
+        Ok((params?, param_ltl.into_return_ltl()))
+    }
+
+    fn lower_callback_param(
+        &mut self,
+        param: &ast::Param,
+        ltl: &mut impl LifetimeLowerer,
+        in_path: &ast::Path,
+    ) -> Result<CallbackParam, ()> {
+        let name = self.lower_ident(&param.name, "param name")?;
+        let ty = self.lower_out_type(
+            &param.ty, ltl, in_path, false, /* in_struct */
+            false, /* in_result_option */
+        )?;
+
+        Ok(CallbackParam {
+            name: Some(name),
+            ty,
+        })
+    }
+
+    fn lower_many_callback_params(
+        &mut self,
+        ast_params: &[ast::Param],
+        param_ltl: &mut ParamLifetimeLowerer<'ast>,
+        in_path: &ast::Path,
+    ) -> Result<Vec<CallbackParam>, ()> {
+        let mut params = Ok(Vec::with_capacity(ast_params.len()));
+
+        for param in ast_params {
+            let param = self.lower_callback_param(param, param_ltl, in_path);
+
+            match (param, &mut params) {
+                (Ok(param), Ok(params)) => {
+                    params.push(param);
+                }
+                _ => params = Err(()),
+            }
+        }
+        params
+    }
+
+    /// Lowers the return type of an [`ast::Method`] into a [`hir::ReturnFallability`].
+    ///
+    /// If there are any errors, they're pushed to `errors` and `None` is returned.
+    fn lower_return_type(
+        &mut self,
+        return_type: Option<&ast::TypeName>,
+        takes_write: bool,
+        mut return_ltl: ReturnLifetimeLowerer<'_>,
+        in_path: &ast::Path,
+    ) -> Result<(ReturnType, LifetimeEnv), ()> {
+        let write_or_unit = if takes_write {
+            SuccessType::Write
+        } else {
+            SuccessType::Unit
+        };
+        match return_type.unwrap_or(&ast::TypeName::Unit) {
+            ast::TypeName::Result(ok_ty, err_ty, _) => {
+                let ok_ty = match ok_ty.as_ref() {
+                    ast::TypeName::Unit => Ok(write_or_unit),
+                    ty => self
+                        .lower_out_type(ty, &mut return_ltl, in_path, false, true)
+                        .map(SuccessType::OutType),
+                };
+                let err_ty = match err_ty.as_ref() {
+                    ast::TypeName::Unit => Ok(None),
+                    ty => self
+                        .lower_out_type(ty, &mut return_ltl, in_path, false, true)
+                        .map(Some),
+                };
+
+                match (ok_ty, err_ty) {
+                    (Ok(ok_ty), Ok(err_ty)) => Ok(ReturnType::Fallible(ok_ty, err_ty)),
+                    _ => Err(()),
+                }
+            }
+            ty @ ast::TypeName::Option(value_ty, _stdlib) => match &**value_ty {
+                ast::TypeName::Box(..) | ast::TypeName::Reference(..) => self
+                    .lower_out_type(ty, &mut return_ltl, in_path, false, true)
+                    .map(SuccessType::OutType)
+                    .map(ReturnType::Infallible),
+                ast::TypeName::Unit => Ok(ReturnType::Nullable(write_or_unit)),
+                _ => self
+                    .lower_out_type(value_ty, &mut return_ltl, in_path, false, true)
+                    .map(SuccessType::OutType)
+                    .map(ReturnType::Nullable),
+            },
+            ast::TypeName::Unit => Ok(ReturnType::Infallible(write_or_unit)),
+            ty => self
+                .lower_out_type(ty, &mut return_ltl, in_path, false, false)
+                .map(|ty| ReturnType::Infallible(SuccessType::OutType(ty))),
+        }
+        .map(|r_ty| (r_ty, return_ltl.finish()))
+    }
+
+    fn lower_named_lifetime(
+        &mut self,
+        lifetime: &ast::lifetimes::LifetimeNode,
+    ) -> Result<BoundedLifetime, ()> {
+        Ok(BoundedLifetime {
+            ident: self.lower_ident(lifetime.lifetime.name(), "lifetime")?,
+            longer: lifetime.longer.iter().copied().map(Lifetime::new).collect(),
+            shorter: lifetime
+                .shorter
+                .iter()
+                .copied()
+                .map(Lifetime::new)
+                .collect(),
+        })
+    }
+
+    /// Lowers a lifetime env found on a type
+    ///
+    /// Should not be extended to return LifetimeEnv<Method>, which needs to use the lifetime
+    /// lowerers to handle elision.
+    fn lower_type_lifetime_env(&mut self, ast: &ast::LifetimeEnv) -> Result<LifetimeEnv, ()> {
+        let nodes = ast
+            .nodes
+            .iter()
+            .map(|lt| self.lower_named_lifetime(lt))
+            .collect::<Result<_, ()>>()?;
+
+        Ok(LifetimeEnv::new(nodes, ast.nodes.len()))
+    }
+}
diff --git a/crates/diplomat_core/src/hir/methods.rs b/crates/diplomat_core/src/hir/methods.rs
new file mode 100644
index 0000000..d4b35dd
--- /dev/null
+++ b/crates/diplomat_core/src/hir/methods.rs
@@ -0,0 +1,304 @@
+//! Methods for types and navigating lifetimes within methods.
+
+use std::collections::BTreeSet;
+use std::ops::Deref;
+
+use super::{
+    Attrs, Docs, Ident, IdentBuf, InputOnly, OutType, OutputOnly, SelfType, TraitPath, Type,
+    TypeContext,
+};
+
+use super::lifetimes::{Lifetime, LifetimeEnv, Lifetimes, MaybeStatic};
+
+use borrowing_field::BorrowingFieldVisitor;
+use borrowing_param::BorrowingParamVisitor;
+
+pub mod borrowing_field;
+pub mod borrowing_param;
+
+/// A method exposed to Diplomat.
+#[derive(Debug)]
+#[non_exhaustive]
+pub struct Method {
+    /// Documentation specified on the method
+    pub docs: Docs,
+    /// The name of the method as initially declared.
+    pub name: IdentBuf,
+    /// The name of the generated `extern "C"` function
+    pub abi_name: IdentBuf,
+    /// The lifetimes introduced in this method and surrounding impl block.
+    pub lifetime_env: LifetimeEnv,
+
+    /// An &self, &mut self, or Self parameter
+    pub param_self: Option<ParamSelf>,
+    /// The parameters of the method
+    pub params: Vec<Param>,
+    /// The output type, including whether it returns a Result/Option/Writeable/etc
+    pub output: ReturnType,
+    /// Resolved (and inherited) diplomat::attr attributes on this method
+    pub attrs: Attrs,
+}
+
+pub trait CallbackInstantiationFunctionality {
+    #[allow(clippy::result_unit_err)]
+    fn get_inputs(&self) -> Result<&[CallbackParam], ()>; // the types of the parameters
+    #[allow(clippy::result_unit_err)]
+    fn get_output_type(&self) -> Result<&Option<Type>, ()>;
+}
+
+#[derive(Debug)]
+#[non_exhaustive]
+// Note: we do not support borrowing across callbacks
+pub struct Callback {
+    pub param_self: Option<TraitParamSelf>, // this is None for callbacks as method arguments
+    pub params: Vec<CallbackParam>,
+    pub output: Box<Option<Type>>, // this will be used in Rust (note: can technically be a callback, or void)
+    pub name: Option<IdentBuf>,
+    pub attrs: Option<Attrs>,
+    pub docs: Option<Docs>,
+}
+
+// uninstantiatable; represents no callback allowed
+#[derive(Debug, Clone)]
+#[non_exhaustive]
+pub enum NoCallback {}
+
+impl CallbackInstantiationFunctionality for Callback {
+    fn get_inputs(&self) -> Result<&[CallbackParam], ()> {
+        Ok(&self.params)
+    }
+    fn get_output_type(&self) -> Result<&Option<Type>, ()> {
+        Ok(&self.output)
+    }
+}
+
+impl CallbackInstantiationFunctionality for NoCallback {
+    fn get_inputs(&self) -> Result<&[CallbackParam], ()> {
+        Err(())
+    }
+    fn get_output_type(&self) -> Result<&Option<Type>, ()> {
+        Err(())
+    }
+}
+
+/// Type that the method returns.
+#[derive(Debug, Clone)]
+#[non_exhaustive]
+pub enum SuccessType {
+    /// Conceptually returns a string, which gets written to the `write: DiplomatWrite` argument
+    Write,
+    /// A Diplomat type. Some types can be outputs, but not inputs, which is expressed by the `OutType` parameter.
+    OutType(OutType),
+    /// A `()` type in Rust.
+    Unit,
+}
+
+/// Whether or not the method returns a value or a result.
+#[derive(Debug)]
+#[allow(clippy::exhaustive_enums)] // this only exists for fallible/infallible, breaking changes for more complex returns are ok
+pub enum ReturnType {
+    Infallible(SuccessType),
+    Fallible(SuccessType, Option<OutType>),
+    Nullable(SuccessType),
+}
+
+/// The `self` parameter of a method.
+#[derive(Debug)]
+#[non_exhaustive]
+pub struct ParamSelf {
+    pub ty: SelfType,
+    pub attrs: Attrs,
+}
+
+#[derive(Debug, Clone)]
+#[non_exhaustive]
+pub struct TraitParamSelf {
+    pub trait_path: TraitPath,
+}
+
+/// A parameter in a method.
+#[derive(Debug)]
+#[non_exhaustive]
+pub struct Param {
+    pub name: IdentBuf,
+    pub ty: Type<InputOnly>,
+    pub attrs: Attrs,
+}
+
+/// A parameter in a callback
+/// No name, since all we get is the callback type signature
+#[derive(Debug)]
+#[non_exhaustive]
+pub struct CallbackParam {
+    pub ty: Type<OutputOnly>,
+    pub name: Option<IdentBuf>,
+}
+
+impl SuccessType {
+    /// Returns whether the variant is `Write`.
+    pub fn is_write(&self) -> bool {
+        matches!(self, SuccessType::Write)
+    }
+
+    /// Returns whether the variant is `Unit`.
+    pub fn is_unit(&self) -> bool {
+        matches!(self, SuccessType::Unit)
+    }
+
+    pub fn as_type(&self) -> Option<&OutType> {
+        match self {
+            SuccessType::OutType(ty) => Some(ty),
+            _ => None,
+        }
+    }
+}
+
+impl Deref for ReturnType {
+    type Target = SuccessType;
+
+    fn deref(&self) -> &Self::Target {
+        match self {
+            ReturnType::Infallible(ret) | ReturnType::Fallible(ret, _) | Self::Nullable(ret) => ret,
+        }
+    }
+}
+
+impl ReturnType {
+    /// Returns `true` if the FFI function returns `void`. Not that this is different from `is_unit`,
+    /// which will be true for `DiplomatResult<(), E>` and false for infallible write.
+    pub fn is_ffi_unit(&self) -> bool {
+        matches!(
+            self,
+            ReturnType::Infallible(SuccessType::Unit | SuccessType::Write)
+        )
+    }
+
+    /// The "main" return type of this function: the Ok, Some, or regular type
+    pub fn success_type(&self) -> &SuccessType {
+        match &self {
+            Self::Infallible(s) => s,
+            Self::Fallible(s, _) => s,
+            Self::Nullable(s) => s,
+        }
+    }
+
+    /// Get the list of method lifetimes actually used by the method return type
+    ///
+    /// Most input lifetimes aren't actually used. An input lifetime is generated
+    /// for each borrowing parameter but is only important if we use it in the return.
+    pub fn used_method_lifetimes(&self) -> BTreeSet<Lifetime> {
+        let mut set = BTreeSet::new();
+
+        let mut add_to_set = |ty: &OutType| {
+            for lt in ty.lifetimes() {
+                if let MaybeStatic::NonStatic(lt) = lt {
+                    set.insert(lt);
+                }
+            }
+        };
+
+        match self {
+            ReturnType::Infallible(SuccessType::OutType(ref ty))
+            | ReturnType::Nullable(SuccessType::OutType(ref ty)) => add_to_set(ty),
+            ReturnType::Fallible(ref ok, ref err) => {
+                if let SuccessType::OutType(ref ty) = ok {
+                    add_to_set(ty)
+                }
+                if let Some(ref ty) = err {
+                    add_to_set(ty)
+                }
+            }
+            _ => (),
+        }
+
+        set
+    }
+
+    pub fn with_contained_types(&self, mut f: impl FnMut(&OutType)) {
+        match self {
+            Self::Infallible(SuccessType::OutType(o))
+            | Self::Nullable(SuccessType::OutType(o))
+            | Self::Fallible(SuccessType::OutType(o), None) => f(o),
+            Self::Fallible(SuccessType::OutType(o), Some(o2)) => {
+                f(o);
+                f(o2)
+            }
+            Self::Fallible(_, Some(o)) => f(o),
+            _ => (),
+        }
+    }
+}
+
+impl ParamSelf {
+    pub(super) fn new(ty: SelfType, attrs: Attrs) -> Self {
+        Self { ty, attrs }
+    }
+
+    /// Return the number of fields and leaves that will show up in the [`BorrowingFieldVisitor`].
+    ///
+    /// This method is used to calculate how much space to allocate upfront.
+    fn field_leaf_lifetime_counts(&self, tcx: &TypeContext) -> (usize, usize) {
+        match self.ty {
+            SelfType::Opaque(_) => (1, 1),
+            SelfType::Struct(ref ty) => ty.resolve(tcx).fields.iter().fold((1, 0), |acc, field| {
+                let inner = field.ty.field_leaf_lifetime_counts(tcx);
+                (acc.0 + inner.0, acc.1 + inner.1)
+            }),
+            SelfType::Enum(_) => (0, 0),
+        }
+    }
+}
+
+impl TraitParamSelf {
+    pub(super) fn new(trait_path: TraitPath) -> Self {
+        Self { trait_path }
+    }
+}
+
+impl Param {
+    pub(super) fn new(name: IdentBuf, ty: Type<InputOnly>, attrs: Attrs) -> Self {
+        Self { name, ty, attrs }
+    }
+}
+
+impl Method {
+    /// Returns a fresh [`Lifetimes`] corresponding to `self`.
+    pub fn method_lifetimes(&self) -> Lifetimes {
+        self.lifetime_env.lifetimes()
+    }
+
+    /// Returns a new [`BorrowingParamVisitor`], which can *shallowly* link output lifetimes
+    /// to the parameters they borrow from.
+    ///
+    /// This is useful for backends which wish to have lifetime codegen for methods only handle the local
+    /// method lifetime, and delegate to generated code on structs for handling the internals of struct lifetimes.
+    pub fn borrowing_param_visitor<'tcx>(
+        &'tcx self,
+        tcx: &'tcx TypeContext,
+    ) -> BorrowingParamVisitor<'tcx> {
+        BorrowingParamVisitor::new(self, tcx)
+    }
+
+    /// Returns a new [`BorrowingFieldVisitor`], which allocates memory to
+    /// efficiently represent all fields (and their paths!) of the inputs that
+    /// have a lifetime.
+    ///
+    /// This is useful for backends which wish to "splat out" lifetime edge codegen for methods,
+    /// linking each borrowed input param/field (however deep it may be in a struct) to a borrowed output param/field.
+    ///
+    /// ```ignore
+    /// # use std::collections::BTreeMap;
+    /// let visitor = method.borrowing_field_visitor(&tcx, "this".ck().unwrap());
+    /// let mut map = BTreeMap::new();
+    /// visitor.visit_borrowing_fields(|lifetime, field| {
+    ///     map.entry(lifetime).or_default().push(field);
+    /// })
+    /// ```
+    pub fn borrowing_field_visitor<'m>(
+        &'m self,
+        tcx: &'m TypeContext,
+        self_name: &'m Ident,
+    ) -> BorrowingFieldVisitor<'m> {
+        BorrowingFieldVisitor::new(self, tcx, self_name)
+    }
+}
diff --git a/crates/diplomat_core/src/hir/methods/borrowing_field.rs b/crates/diplomat_core/src/hir/methods/borrowing_field.rs
new file mode 100644
index 0000000..d03ba5c
--- /dev/null
+++ b/crates/diplomat_core/src/hir/methods/borrowing_field.rs
@@ -0,0 +1,357 @@
+//! Tools for traversing all borrows in method parameters and return types, transitively
+//!
+//! This is useful for backends which wish to "splat out" lifetime edge codegen for methods,
+//! linking each borrowed input param/field (however deep it may be in a struct) to a borrowed output param/field.
+
+use std::fmt::{self, Write};
+
+use smallvec::SmallVec;
+
+use crate::hir::{
+    paths, Borrow, Ident, Method, SelfType, Slice, StructPath, TyPosition, Type, TypeContext,
+};
+
+use crate::hir::lifetimes::{Lifetime, Lifetimes, MaybeStatic};
+
+/// An id for indexing into a [`BorrowingFieldsVisitor`].
+#[derive(Copy, Clone, Debug)]
+struct ParentId(usize);
+
+impl ParentId {
+    /// Pushes a new parent to the vec, returning the corresponding [`ParentId`].
+    fn new<'m>(
+        parent: Option<ParentId>,
+        name: &'m Ident,
+        parents: &mut SmallVec<[(Option<ParentId>, &'m Ident); 4]>,
+    ) -> Self {
+        let this = ParentId(parents.len());
+        parents.push((parent, name));
+        this
+    }
+}
+
+/// Convenience const representing the number of nested structs a [`BorrowingFieldVisitor`]
+/// can hold inline before needing to dynamically allocate.
+const INLINE_NUM_PARENTS: usize = 4;
+
+/// Convenience const representing the number of borrowed fields a [`BorrowingFieldVisitor`]
+/// can hold inline before needing to dynamically allocate.
+const INLINE_NUM_LEAVES: usize = 8;
+
+/// A tree of lifetimes mapping onto a specific instantiation of a type tree.
+///
+/// Each `BorrowingFieldsVisitor` corresponds to the type of an input of a method.
+///
+/// Obtain from [`Method::borrowing_field_visitor()`].
+pub struct BorrowingFieldVisitor<'m> {
+    parents: SmallVec<[(Option<ParentId>, &'m Ident); INLINE_NUM_PARENTS]>,
+    leaves: SmallVec<[BorrowingFieldVisitorLeaf; INLINE_NUM_LEAVES]>,
+}
+
+/// A leaf of a lifetime tree capable of tracking its parents.
+#[derive(Copy, Clone)]
+pub struct BorrowingField<'m> {
+    /// All inner nodes in the tree. When tracing from the root, we jump around
+    /// this slice based on indices, but don't necessarily use all of them.
+    parents: &'m [(Option<ParentId>, &'m Ident)],
+
+    /// The unpacked field that is a leaf on the tree.
+    leaf: &'m BorrowingFieldVisitorLeaf,
+}
+
+/// Non-recursive input-output types that contain lifetimes
+enum BorrowingFieldVisitorLeaf {
+    Opaque(ParentId, MaybeStatic<Lifetime>, Lifetimes),
+    Slice(ParentId, MaybeStatic<Lifetime>),
+}
+
+impl<'m> BorrowingFieldVisitor<'m> {
+    /// Visits every borrowing field and method lifetime that it uses.
+    ///
+    /// The idea is that you could use this to construct a mapping from
+    /// `Lifetime`s to `BorrowingField`s. We choose to use a visitor
+    /// pattern to avoid having to
+    ///
+    /// This would be convenient in the JavaScript backend where if you're
+    /// returning an `NonOpaque<'a, 'b>` from Rust, you need to pass a
+    /// `[[all input borrowing fields with 'a], [all input borrowing fields with 'b]]`
+    /// array into `NonOpaque`'s constructor.
+    ///
+    /// Alternatively, you could use such a map in the C++ backend by recursing
+    /// down the return type and keeping track of which fields you've recursed
+    /// into so far, and when you hit some lifetime 'a, generate docs saying
+    /// "path.to.current.field must be outlived by {borrowing fields of input that
+    /// contain 'a}".
+    pub fn visit_borrowing_fields<'a, F>(&'a self, mut visit: F)
+    where
+        F: FnMut(MaybeStatic<Lifetime>, BorrowingField<'a>),
+    {
+        for leaf in self.leaves.iter() {
+            let borrowing_field = BorrowingField {
+                parents: self.parents.as_slice(),
+                leaf,
+            };
+
+            match leaf {
+                BorrowingFieldVisitorLeaf::Opaque(_, lt, method_lifetimes) => {
+                    visit(*lt, borrowing_field);
+                    for lt in method_lifetimes.lifetimes() {
+                        visit(lt, borrowing_field);
+                    }
+                }
+                BorrowingFieldVisitorLeaf::Slice(_, lt) => {
+                    visit(*lt, borrowing_field);
+                }
+            }
+        }
+    }
+
+    /// Returns a new `BorrowingFieldsVisitor` containing all the lifetime trees of the arguments
+    /// in only two allocations.
+    pub(crate) fn new(method: &'m Method, tcx: &'m TypeContext, self_name: &'m Ident) -> Self {
+        let (parents, leaves) = method
+            .param_self
+            .as_ref()
+            .map(|param_self| param_self.field_leaf_lifetime_counts(tcx))
+            .into_iter()
+            .chain(
+                method
+                    .params
+                    .iter()
+                    .map(|param| param.ty.field_leaf_lifetime_counts(tcx)),
+            )
+            .reduce(|acc, x| (acc.0 + x.0, acc.1 + x.1))
+            .map(|(num_fields, num_leaves)| {
+                let num_params = method.params.len() + usize::from(method.param_self.is_some());
+                let mut parents = SmallVec::with_capacity(num_fields + num_params);
+                let mut leaves = SmallVec::with_capacity(num_leaves);
+                let method_lifetimes = method.method_lifetimes();
+
+                if let Some(param_self) = method.param_self.as_ref() {
+                    let parent = ParentId::new(None, self_name, &mut parents);
+                    match &param_self.ty {
+                        SelfType::Opaque(ty) => {
+                            Self::visit_opaque(
+                                &ty.lifetimes,
+                                &ty.borrowed().lifetime,
+                                parent,
+                                &method_lifetimes,
+                                &mut leaves,
+                            );
+                        }
+                        SelfType::Struct(ty) => {
+                            Self::visit_struct(
+                                ty,
+                                tcx,
+                                parent,
+                                &method_lifetimes,
+                                &mut parents,
+                                &mut leaves,
+                            );
+                        }
+                        SelfType::Enum(_) => {}
+                    }
+                }
+
+                for param in method.params.iter() {
+                    let parent = ParentId::new(None, param.name.as_ref(), &mut parents);
+                    Self::from_type(
+                        &param.ty,
+                        tcx,
+                        parent,
+                        &method_lifetimes,
+                        &mut parents,
+                        &mut leaves,
+                    );
+                }
+
+                // sanity check that the preallocations were correct
+                debug_assert_eq!(
+                    parents.capacity(),
+                    std::cmp::max(INLINE_NUM_PARENTS, num_fields + num_params)
+                );
+                debug_assert_eq!(
+                    leaves.capacity(),
+                    std::cmp::max(INLINE_NUM_LEAVES, num_leaves)
+                );
+                (parents, leaves)
+            })
+            .unwrap_or_default();
+
+        Self { parents, leaves }
+    }
+
+    /// Returns a new [`BorrowingFieldsVisitor`] corresponding to a type.
+    fn from_type<P: TyPosition<StructPath = StructPath, OpaqueOwnership = Borrow>>(
+        ty: &'m Type<P>,
+        tcx: &'m TypeContext,
+        parent: ParentId,
+        method_lifetimes: &Lifetimes,
+        parents: &mut SmallVec<[(Option<ParentId>, &'m Ident); 4]>,
+        leaves: &mut SmallVec<[BorrowingFieldVisitorLeaf; 8]>,
+    ) {
+        match ty {
+            Type::Opaque(path) => {
+                Self::visit_opaque(
+                    &path.lifetimes,
+                    &path.borrowed().lifetime,
+                    parent,
+                    method_lifetimes,
+                    leaves,
+                );
+            }
+            Type::Slice(slice) => {
+                Self::visit_slice(slice, parent, method_lifetimes, leaves);
+            }
+            Type::Struct(path) => {
+                Self::visit_struct(path, tcx, parent, method_lifetimes, parents, leaves);
+            }
+            _ => {}
+        }
+    }
+
+    /// Add an opaque as a leaf during construction of a [`BorrowingFieldsVisitor`].
+    fn visit_opaque(
+        lifetimes: &'m Lifetimes,
+        borrow: &'m MaybeStatic<Lifetime>,
+        parent: ParentId,
+        method_lifetimes: &Lifetimes,
+        leaves: &mut SmallVec<[BorrowingFieldVisitorLeaf; 8]>,
+    ) {
+        let method_borrow_lifetime =
+            borrow.flat_map_nonstatic(|lt| lt.as_method_lifetime(method_lifetimes));
+        let method_type_lifetimes = lifetimes.as_method_lifetimes(method_lifetimes);
+        leaves.push(BorrowingFieldVisitorLeaf::Opaque(
+            parent,
+            method_borrow_lifetime,
+            method_type_lifetimes,
+        ));
+    }
+
+    /// Add a slice as a leaf during construction of a [`BorrowingFieldsVisitor`].
+    fn visit_slice(
+        slice: &Slice,
+        parent: ParentId,
+        method_lifetimes: &Lifetimes,
+        leaves: &mut SmallVec<[BorrowingFieldVisitorLeaf; 8]>,
+    ) {
+        if let Some(lifetime) = slice.lifetime() {
+            let method_lifetime =
+                lifetime.flat_map_nonstatic(|lt| lt.as_method_lifetime(method_lifetimes));
+            leaves.push(BorrowingFieldVisitorLeaf::Slice(parent, method_lifetime));
+        }
+    }
+
+    /// Add a struct as a parent and recurse down leaves during construction of a
+    /// [`BorrowingFieldsVisitor`].
+    fn visit_struct(
+        ty: &paths::StructPath,
+        tcx: &'m TypeContext,
+        parent: ParentId,
+        method_lifetimes: &Lifetimes,
+        parents: &mut SmallVec<[(Option<ParentId>, &'m Ident); 4]>,
+        leaves: &mut SmallVec<[BorrowingFieldVisitorLeaf; 8]>,
+    ) {
+        let method_type_lifetimes = ty.lifetimes.as_method_lifetimes(method_lifetimes);
+        for field in ty.resolve(tcx).fields.iter() {
+            Self::from_type(
+                &field.ty,
+                tcx,
+                ParentId::new(Some(parent), field.name.as_ref(), parents),
+                &method_type_lifetimes,
+                parents,
+                leaves,
+            );
+        }
+    }
+}
+
+impl<'m> BorrowingField<'m> {
+    /// Visit fields in order.
+    ///
+    /// If `self` represents the field `param.first.second`, then calling [`BorrowingField::trace`]
+    /// will visit the following in order: `"param"`, `"first"`, `"second"`.
+    pub fn backtrace<F>(&self, mut visit: F)
+    where
+        F: FnMut(usize, &'m Ident),
+    {
+        let (parent, ident) = match self.leaf {
+            BorrowingFieldVisitorLeaf::Opaque(id, ..) | BorrowingFieldVisitorLeaf::Slice(id, _) => {
+                self.parents[id.0]
+            }
+        };
+
+        self.backtrace_rec(parent, ident, &mut visit);
+    }
+
+    /// Recursively visits fields in order from root to leaf by building up the
+    /// stack, and then visiting fields as it unwinds.
+    fn backtrace_rec<F>(&self, parent: Option<ParentId>, ident: &'m Ident, visit: &mut F) -> usize
+    where
+        F: FnMut(usize, &'m Ident),
+    {
+        let from_end = if let Some(id) = parent {
+            let (parent, ident) = self.parents[id.0];
+            self.backtrace_rec(parent, ident, visit)
+        } else {
+            0
+        };
+
+        visit(from_end, ident);
+
+        from_end + 1
+    }
+
+    /// Fallibly visits fields in order.
+    ///
+    /// This method is similar to [`BorrowinfField::backtrace`], but short-circuits
+    /// when an `Err` is returned.
+    pub fn try_backtrace<F, E>(&self, mut visit: F) -> Result<(), E>
+    where
+        F: FnMut(usize, &'m Ident) -> Result<(), E>,
+    {
+        let (parent, ident) = match self.leaf {
+            BorrowingFieldVisitorLeaf::Opaque(id, ..) | BorrowingFieldVisitorLeaf::Slice(id, _) => {
+                self.parents[id.0]
+            }
+        };
+
+        self.try_backtrace_rec(parent, ident, &mut visit)?;
+
+        Ok(())
+    }
+
+    /// Recursively visits fields in order from root to leaf by building up the
+    /// stack, and then visiting fields as it unwinds.
+    fn try_backtrace_rec<F, E>(
+        &self,
+        parent: Option<ParentId>,
+        ident: &'m Ident,
+        visit: &mut F,
+    ) -> Result<usize, E>
+    where
+        F: FnMut(usize, &'m Ident) -> Result<(), E>,
+    {
+        let from_end = if let Some(id) = parent {
+            let (parent, ident) = self.parents[id.0];
+            self.try_backtrace_rec(parent, ident, visit)?
+        } else {
+            0
+        };
+
+        visit(from_end, ident)?;
+
+        Ok(from_end + 1)
+    }
+}
+
+impl<'m> fmt::Display for BorrowingField<'m> {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        self.try_backtrace(|i, ident| {
+            if i != 0 {
+                f.write_char('.')?;
+            }
+            f.write_str(ident.as_str())
+        })
+    }
+}
diff --git a/crates/diplomat_core/src/hir/methods/borrowing_param.rs b/crates/diplomat_core/src/hir/methods/borrowing_param.rs
new file mode 100644
index 0000000..4069d70
--- /dev/null
+++ b/crates/diplomat_core/src/hir/methods/borrowing_param.rs
@@ -0,0 +1,354 @@
+//! Tools for traversing all borrows in method parameters and return types, shallowly
+//!
+//! This is useful for backends which wish to figure out the borrowing relationships between parameters
+//! and return values,
+//! and then delegate how lifetimes get mapped to fields to the codegen for those types respectively.
+//!
+//!
+//! # Typical usage
+//!
+//! In managed languages, to ensure the GC doesn't prematurely clean up the borrowed-from object,
+//! we use the technique of stashing a borrowed-from object as a field of a borrowed-to object.
+//! This is called a "lifetime edge" (since it's an edge in the GC graph). An "edge array" is typically
+//! a stash of all the edges on an object, or all the edges corresponding to a particular lifetime.
+//!
+//! This stashing is primarily driven by method codegen, which is where actual borrow relationships can be set up.
+//!
+//! Typically, codegen for a method should instantiate a [`BorrowedParamVisitor`] and uses it to [`BorrowedParamVisitor::visit_param()`] each input parameter to determine if it needs to be linked into an edge.
+//! Whilst doing so, it may return useful [`ParamBorrowInfo`] which provides additional information on handling structs and slices. More on structs later.
+//! At the end of the visiting, [`BorrowedLifetimeInfo::borrow_map()`] can be called to get a list of relevant input edges: each lifetime that participates in borrowing,
+//! and a list of parameter names that it borrows from.
+//!
+//! This visitor will automatically handle transitive lifetime relationships as well.
+//!
+//! These edges should be put together into "edge arrays" which then get passed down to output types, handling transitive lifetimes as necessary.
+//!
+//! ## Method codegen for slices
+//!
+//! Slices are strange: in managed languages a host slice will not be a Rust slice, unlike other borrowed host values (a borrowed host opaque is just a borrowed Rust
+//! opaque). We need to convert these across the boundary by allocating a temporary arena or something.
+//!
+//! If the slice doesn't participate in borrowing, this is easy: just allocate a temporary arena. However, if not, we need to allocate an arena with a finalizer (or something similar)
+//! and add that arena to the edge instead. [`LifetimeEdgeKind`] has special handling for this.
+//!
+//! Whether or not the slice participates in borrowing is found out from the [`ParamBorrowInfo`] returned by [`BorrowedParamVisitor::visit_param()`].
+//!
+//! # Method codegen for struct params
+//!
+//! Structs can contain many lifetimes and have relationships between them. Generally, a Diplomat struct is a "bag of stuff"; it is converted to a host value that is structlike, with fields
+//! individually converted.
+//!
+//! Diplomat enforces that struct lifetime bounds never imply additional bounds on methods during HIR validation, so the relationships between struct
+//! lifetimes are not relevant for code here (and as such you should hopefully never need to call [`LifetimeEnv::all_longer_lifetimes()`])
+//! for a struct env).
+//!
+//! The borrowing technique in this module allows a lot of things to be delegated to structs. As will be explained below, structs will have:
+//!
+//! - A `fields_for_lifetime_foo()` set of methods that returns all non-slice fields corresponding to a lifetime
+//! - "append array" outparams for stashing edges when converting from host-to-Rust
+//! - Some way of passing down edge arrays to individual borrowed fields when constructing Rust-to-host
+//!
+//! In methods, when constructing the edge arrays, `fields_for_lifetime_foo()` for every borrowed param can be appended to each
+//! one whenever [`LifetimeEdgeKind::StructLifetime`] asks you to do so. The code needs to handle lifetime
+//! transitivity, since the struct will not be doing so. Fortunately the edges produced by [`BorrowedParamVisitor`] already do so.
+//!
+//! Then, when converting structs host-to-Rust, every edge array relevant to a struct lifetime should be passed in for an append array. Append arrays
+//! are nested arrays: they are an array of edge arrays. The struct will append further things to the edge array.
+//!
+//! Finally, when converting Rust-to-host, if producing a struct, make sure to pass in all the edge arrays.
+//!
+//! # Struct codegen
+//!
+//! At a high level, struct codegen needs to deal with lifetimes in three places:
+//!
+//! - A `fields_for_lifetime_foo()` set of methods that returns all non-slice fields corresponding to a lifetime
+//! - "append array" outparams for stashing edges when converting from host-to-Rust
+//! - Some way of passing down edge arrays to individual borrowed fields when constructing Rust-to-host
+//!
+//! The backend may choose to handle just slices via append arrays, or handle all borrowing there, in which case `fields_for_lifetime_foo()` isn't necessary.
+//!
+//! `fields_for_lifetime_foo()` should return an iterator/list/etc corresponding to each field that *directly* borrows from lifetime foo.
+//! This may include calling `fields_for_lifetime_bar()` on fields that are themselves structs. As mentioned previously, lifetime relationships
+//! are handled by methods and don't need to be dealt with here. There are no helpers for doing this but it's just a matter of
+//! filtering for fields using the lifetime, except for handling nested structs ([`StructBorrowInfo::compute_for_struct_field()`])
+//!
+//! Append arrays are similarly straightforward: for each lifetime on the struct, the host-to-Rust constructor should accept a list of edge arrays. For slice fields,
+//! if the appropriate list is nonempty, the slice's arena edge should be appended to all of the edge arrays in it. These arrays should also be passed down to struct fields
+//! appropriately: [`StructBorrowInfo::compute_for_struct_field()`] is super helpful for getting a list edges something should get passed down to.
+//!
+//! Finally, when converting Rust-to-host, the relevant lifetime edges should be proxied down to the final host type constructors who can stash them wherever needed.
+//! This is one again a matter of filtering fields by the lifetimes they use, and for nested structs you can use [`StructBorrowInfo::compute_for_struct_field()`].
+
+use std::collections::{BTreeMap, BTreeSet};
+
+use crate::hir::{self, Method, StructDef, StructPath, TyPosition, TypeContext};
+
+use crate::hir::lifetimes::{Lifetime, LifetimeEnv, MaybeStatic};
+use crate::hir::ty_position::StructPathLike;
+
+/// A visitor for processing method parameters/returns and understanding their borrowing relationships, shallowly.
+///
+/// This produces a list of lifetime "edges" per lifetime in the output producing a borrow.
+///
+/// Each `BorrowingFieldsVisitor` corresponds to the type of an input of a method.
+///
+/// Obtain from [`Method::borrowing_param_visitor()`].
+pub struct BorrowingParamVisitor<'tcx> {
+    tcx: &'tcx TypeContext,
+    used_method_lifetimes: BTreeSet<Lifetime>,
+    borrow_map: BTreeMap<Lifetime, BorrowedLifetimeInfo<'tcx>>,
+}
+
+/// A single lifetime "edge" from a parameter to a value
+#[non_exhaustive]
+#[derive(Clone, Debug)]
+pub struct LifetimeEdge<'tcx> {
+    pub param_name: String,
+    pub kind: LifetimeEdgeKind<'tcx>,
+}
+
+#[non_exhaustive]
+#[derive(Copy, Clone, Debug)]
+pub enum LifetimeEdgeKind<'tcx> {
+    /// Just an opaque parameter directly being borrowed.
+    OpaqueParam,
+    /// A slice being converted and then borrowed. These often need to be handled differently
+    /// when they are borrowed as the borrow will need to create an edge
+    SliceParam,
+    /// A lifetime parameter of a struct, given the lifetime context and the struct-def lifetime for that struct.
+    ///
+    /// The boolean is whether or not the struct is optional.
+    ///
+    /// Using this, you can generate code that "asks" the struct for the lifetime-relevant field edges
+    StructLifetime(&'tcx LifetimeEnv, Lifetime, bool),
+}
+
+#[non_exhaustive]
+#[derive(Clone, Debug)]
+pub struct BorrowedLifetimeInfo<'tcx> {
+    // Initializers for all inputs to the edge array from parameters, except for slices (slices get handled
+    // differently)
+    pub incoming_edges: Vec<LifetimeEdge<'tcx>>,
+    // All lifetimes longer than this. When this lifetime is borrowed from, data corresponding to
+    // the other lifetimes may also be borrowed from.
+    pub all_longer_lifetimes: BTreeSet<Lifetime>,
+}
+
+impl<'tcx> BorrowingParamVisitor<'tcx> {
+    pub(crate) fn new(method: &'tcx Method, tcx: &'tcx TypeContext) -> Self {
+        let used_method_lifetimes = method.output.used_method_lifetimes();
+        let borrow_map = used_method_lifetimes
+            .iter()
+            .map(|lt| {
+                (
+                    *lt,
+                    BorrowedLifetimeInfo {
+                        incoming_edges: Vec::new(),
+                        all_longer_lifetimes: method
+                            .lifetime_env
+                            .all_longer_lifetimes(lt)
+                            .collect(),
+                    },
+                )
+            })
+            .collect();
+        BorrowingParamVisitor {
+            tcx,
+            used_method_lifetimes,
+            borrow_map,
+        }
+    }
+
+    /// Get the cached list of used method lifetimes. Same as calling `.used_method_lifetimes()` on `method.output`
+    pub fn used_method_lifetimes(&self) -> &BTreeSet<Lifetime> {
+        &self.used_method_lifetimes
+    }
+
+    /// Get the final borrow map, listing lifetime edges for each output lfietime
+    pub fn borrow_map(self) -> BTreeMap<Lifetime, BorrowedLifetimeInfo<'tcx>> {
+        self.borrow_map
+    }
+
+    /// Processes a parameter, adding it to the borrow_map for any lifetimes it references. Returns further information about the type of borrow.
+    ///
+    /// This basically boils down to: For each lifetime that is actually relevant to borrowing in this method, check if that
+    /// lifetime or lifetimes longer than it are used by this parameter. In other words, check if
+    /// it is possible for data in the return type with this lifetime to have been borrowed from this parameter.
+    /// If so, add code that will yield the ownership-relevant parts of this object to incoming_edges for that lifetime.
+    pub fn visit_param<P: TyPosition<StructPath = StructPath>>(
+        &mut self,
+        ty: &hir::Type<P>,
+        param_name: &str,
+    ) -> ParamBorrowInfo<'tcx> {
+        let mut is_borrowed = false;
+        if self.used_method_lifetimes.is_empty() {
+            if let hir::Type::Slice(..) = *ty {
+                return ParamBorrowInfo::TemporarySlice;
+            } else {
+                return ParamBorrowInfo::NotBorrowed;
+            }
+        }
+
+        // Structs have special handling: structs are purely Dart-side, so if you borrow
+        // from a struct, you really are borrowing from the internal fields.
+        if let hir::Type::Struct(s) = ty {
+            let mut borrowed_struct_lifetime_map = BTreeMap::<Lifetime, BTreeSet<Lifetime>>::new();
+            let link = s.link_lifetimes(self.tcx);
+            for (method_lifetime, method_lifetime_info) in &mut self.borrow_map {
+                // Note that ty.lifetimes()/s.lifetimes() is lifetimes
+                // in the *use* context, i.e. lifetimes on the Type that reference the
+                // indices of the method's lifetime arrays. Their *order* references
+                // the indices of the underlying struct def. We need to link the two,
+                // since the _fields_for_lifetime_foo() methods are named after
+                // the *def* context lifetime.
+                //
+                // Concretely, if we have struct `Foo<'a, 'b>` and our method
+                // accepts `Foo<'x, 'y>`, we need to output _fields_for_lifetime_a()/b not x/y.
+                //
+                // This is a struct so lifetimes_def_only() is fine to call
+                for (use_lt, def_lt) in link.lifetimes_def_only() {
+                    if let MaybeStatic::NonStatic(use_lt) = use_lt {
+                        if method_lifetime_info.all_longer_lifetimes.contains(&use_lt) {
+                            let edge = LifetimeEdge {
+                                param_name: param_name.into(),
+                                kind: LifetimeEdgeKind::StructLifetime(
+                                    link.def_env(),
+                                    def_lt,
+                                    ty.is_option(),
+                                ),
+                            };
+                            method_lifetime_info.incoming_edges.push(edge);
+
+                            is_borrowed = true;
+
+                            borrowed_struct_lifetime_map
+                                .entry(def_lt)
+                                .or_default()
+                                .insert(*method_lifetime);
+                            // Do *not* break the inner loop here: even if we found *one* matching lifetime
+                            // in this struct that may not be all of them, there may be some other fields that are borrowed
+                        }
+                    }
+                }
+            }
+            if is_borrowed {
+                ParamBorrowInfo::Struct(StructBorrowInfo {
+                    env: link.def_env(),
+                    borrowed_struct_lifetime_map,
+                })
+            } else {
+                ParamBorrowInfo::NotBorrowed
+            }
+        } else {
+            for method_lifetime in self.borrow_map.values_mut() {
+                for lt in ty.lifetimes() {
+                    if let MaybeStatic::NonStatic(lt) = lt {
+                        if method_lifetime.all_longer_lifetimes.contains(&lt) {
+                            let kind = match ty {
+                                hir::Type::Slice(..) => LifetimeEdgeKind::SliceParam,
+                                hir::Type::Opaque(..) => LifetimeEdgeKind::OpaqueParam,
+                                _ => unreachable!("Types other than slices, opaques, and structs cannot have lifetimes")
+                            };
+
+                            let edge = LifetimeEdge {
+                                param_name: param_name.into(),
+                                kind,
+                            };
+
+                            method_lifetime.incoming_edges.push(edge);
+                            is_borrowed = true;
+                            // Break the inner loop: we've already determined this
+                            break;
+                        }
+                    }
+                }
+            }
+            match (is_borrowed, ty) {
+                (true, &hir::Type::Slice(..)) => ParamBorrowInfo::BorrowedSlice,
+                (false, &hir::Type::Slice(..)) => ParamBorrowInfo::TemporarySlice,
+                (false, _) => ParamBorrowInfo::NotBorrowed,
+                (true, _) => ParamBorrowInfo::BorrowedOpaque,
+            }
+        }
+    }
+}
+
+/// Information relevant to borrowing for producing conversions
+#[derive(Clone, Debug)]
+#[non_exhaustive]
+pub enum ParamBorrowInfo<'tcx> {
+    /// No borrowing constraints. This means the parameter
+    /// is not borrowed by the output and also does not need temporary borrows
+    NotBorrowed,
+    /// A slice that is not borrowed by the output (but will still need temporary allocation)
+    TemporarySlice,
+    /// A slice that is borrowed by the output
+    BorrowedSlice,
+    /// A struct parameter that is borrowed by the output
+    Struct(StructBorrowInfo<'tcx>),
+    /// An opaque type that is borrowed
+    BorrowedOpaque,
+}
+
+/// Information about the lifetimes of a struct parameter that are borrowed by a method output or by a wrapping struct
+#[derive(Clone, Debug)]
+#[non_exhaustive]
+pub struct StructBorrowInfo<'tcx> {
+    /// This is the struct's lifetime environment
+    pub env: &'tcx LifetimeEnv,
+    /// A map from (borrow-relevant) struct lifetimes to lifetimes in the method (or wrapping struct) that may flow from it
+    pub borrowed_struct_lifetime_map: BTreeMap<Lifetime, BTreeSet<Lifetime>>,
+}
+
+impl<'tcx> StructBorrowInfo<'tcx> {
+    /// Get borrowing info for a struct field, if it does indeed borrow
+    ///
+    /// The lifetime map produced here does not handle lifetime dependencies: the expectation is that the struct
+    /// machinery generated by this will be called by method code that handles these dependencies. We try to handle
+    /// lifetime dependencies in ONE place.
+    pub fn compute_for_struct_field<P: TyPosition>(
+        struc: &StructDef<P>,
+        field: &P::StructPath,
+        tcx: &'tcx TypeContext,
+    ) -> Option<Self> {
+        if field.lifetimes().as_slice().is_empty() {
+            return None;
+        }
+
+        let mut borrowed_struct_lifetime_map = BTreeMap::<Lifetime, BTreeSet<Lifetime>>::new();
+
+        let link = field.link_lifetimes(tcx);
+
+        for outer_lt in struc.lifetimes.all_lifetimes() {
+            // Note that field.lifetimes()
+            // in the *use* context, i.e. lifetimes on the Type that reference the
+            // indices of the outer struct's lifetime arrays. Their *order* references
+            // the indices of the underlying struct def. We need to link the two,
+            // since the _fields_for_lifetime_foo() methods are named after
+            // the *def* context lifetime.
+            //
+            // This is a struct so lifetimes_def_only() is fine to call
+            for (use_lt, def_lt) in link.lifetimes_def_only() {
+                if let MaybeStatic::NonStatic(use_lt) = use_lt {
+                    // We do *not* need to transitively check for longer lifetimes here:
+                    //
+                    if outer_lt == use_lt {
+                        borrowed_struct_lifetime_map
+                            .entry(def_lt)
+                            .or_default()
+                            .insert(outer_lt);
+                    }
+                }
+            }
+        }
+        if borrowed_struct_lifetime_map.is_empty() {
+            // if the inner struct is only statics
+            None
+        } else {
+            Some(StructBorrowInfo {
+                env: link.def_env(),
+                borrowed_struct_lifetime_map,
+            })
+        }
+    }
+}
diff --git a/crates/diplomat_core/src/hir/mod.rs b/crates/diplomat_core/src/hir/mod.rs
new file mode 100644
index 0000000..4a0959d
--- /dev/null
+++ b/crates/diplomat_core/src/hir/mod.rs
@@ -0,0 +1,31 @@
+//! Experimental high-level representation (HIR) for Diplomat.
+//!
+//! Enabled with the `"hir"` Cargo feature
+
+mod attrs;
+mod defs;
+mod elision;
+mod lifetimes;
+mod lowering;
+mod methods;
+mod paths;
+mod primitives;
+mod ty_position;
+mod type_context;
+mod types;
+pub use attrs::*;
+pub use defs::*;
+pub(super) use elision::*;
+pub use lifetimes::*;
+pub(super) use lowering::*;
+pub use methods::*;
+pub use paths::*;
+pub use primitives::*;
+pub use ty_position::*;
+pub use type_context::*;
+pub use types::*;
+
+pub use lowering::{ErrorAndContext, ErrorContext, LoweringError};
+
+pub use crate::ast::{Docs, DocsUrlGenerator};
+pub use strck::ident::rust::{Ident, IdentBuf};
diff --git a/crates/diplomat_core/src/hir/paths.rs b/crates/diplomat_core/src/hir/paths.rs
new file mode 100644
index 0000000..6c2125e
--- /dev/null
+++ b/crates/diplomat_core/src/hir/paths.rs
@@ -0,0 +1,220 @@
+use super::lifetimes::{Lifetimes, LinkedLifetimes};
+use super::{
+    Borrow, EnumDef, EnumId, Everywhere, OpaqueDef, OpaqueId, OpaqueOwner, OutStructDef,
+    OutputOnly, ReturnableStructDef, StructDef, TraitId, TyPosition, TypeContext,
+};
+
+/// Path to a struct that may appear as an output.
+#[derive(Debug, Clone)]
+#[non_exhaustive]
+pub enum ReturnableStructPath {
+    Struct(StructPath),
+    OutStruct(OutStructPath),
+}
+
+/// Path to a struct that can only be used as an output.
+pub type OutStructPath = StructPath<OutputOnly>;
+
+/// Path to a struct that can be used in inputs and outputs.
+#[derive(Debug, Clone)]
+#[non_exhaustive]
+pub struct StructPath<P: TyPosition = Everywhere> {
+    pub lifetimes: Lifetimes,
+    pub tcx_id: P::StructId,
+}
+
+#[derive(Debug, Clone)]
+#[non_exhaustive]
+pub struct TraitPath {
+    pub lifetimes: Lifetimes,
+    pub tcx_id: TraitId,
+}
+
+/// Non-instantiable enum to denote the trait path in
+/// TyPositions that don't allow traits (anything not InputOnly)
+#[derive(Debug, Clone)]
+#[non_exhaustive]
+pub enum NoTraitPath {}
+
+/// Path to an opaque.
+///
+/// There are three kinds of opaques that Diplomat uses, so this type has two
+/// generic arguments to differentiate between the three, while still showing
+/// that the three are all paths to opaques. The monomorphized versions that
+/// Diplomat uses are:
+///
+/// 1. `OpaquePath<Optional, MaybeOwn>`: Opaques in return types,
+///    which can be optional and either owned or borrowed.
+/// 2. `OpaquePath<Optional, Borrow>`: Opaques in method parameters, which can
+///    be optional but must be borrowed, since most languages don't have a way to
+///    entirely give up ownership of a value.
+/// 3. `OpaquePath<NonOptional, Borrow>`: Opaques in the `&self` position, which
+///    cannot be optional and must be borrowed for the same reason as above.
+#[derive(Debug, Clone)]
+#[non_exhaustive]
+pub struct OpaquePath<Opt, Owner> {
+    pub lifetimes: Lifetimes,
+    pub optional: Opt,
+    pub owner: Owner,
+    pub tcx_id: OpaqueId,
+}
+
+#[derive(Debug, Copy, Clone)]
+pub struct Optional(pub(super) bool);
+
+#[derive(Debug, Copy, Clone)]
+#[allow(clippy::exhaustive_structs)] // marker type
+pub struct NonOptional;
+
+impl<Owner: OpaqueOwner> OpaquePath<Optional, Owner> {
+    pub fn is_optional(&self) -> bool {
+        self.optional.0
+    }
+}
+
+impl<Owner: OpaqueOwner> OpaquePath<NonOptional, Owner> {
+    pub fn wrap_optional(self) -> OpaquePath<Optional, Owner> {
+        OpaquePath {
+            lifetimes: self.lifetimes,
+            optional: Optional(false),
+            owner: self.owner,
+            tcx_id: self.tcx_id,
+        }
+    }
+}
+
+impl<Opt> OpaquePath<Opt, MaybeOwn> {
+    pub fn as_borrowed(&self) -> Option<&Borrow> {
+        self.owner.as_borrowed()
+    }
+}
+
+impl<Opt> OpaquePath<Opt, Borrow> {
+    pub fn borrowed(&self) -> &Borrow {
+        &self.owner
+    }
+}
+
+/// Path to an enum.
+#[derive(Debug, Clone)]
+#[non_exhaustive]
+pub struct EnumPath {
+    pub tcx_id: EnumId,
+}
+
+/// Determine whether a pointer to an opaque type is owned or borrowed.
+///
+/// Since owned opaques cannot be used as inputs, this only appears in output types.
+#[derive(Copy, Clone, Debug)]
+#[allow(clippy::exhaustive_enums)] // only two answers to this question
+pub enum MaybeOwn {
+    Own,
+    Borrow(Borrow),
+}
+
+impl MaybeOwn {
+    pub fn as_borrowed(&self) -> Option<&Borrow> {
+        match self {
+            MaybeOwn::Own => None,
+            MaybeOwn::Borrow(borrow) => Some(borrow),
+        }
+    }
+}
+
+impl ReturnableStructPath {
+    pub fn resolve<'tcx>(&self, tcx: &'tcx TypeContext) -> ReturnableStructDef<'tcx> {
+        match self {
+            ReturnableStructPath::Struct(path) => ReturnableStructDef::Struct(path.resolve(tcx)),
+            ReturnableStructPath::OutStruct(path) => {
+                ReturnableStructDef::OutStruct(path.resolve(tcx))
+            }
+        }
+    }
+
+    pub(crate) fn lifetimes(&self) -> &Lifetimes {
+        match self {
+            Self::Struct(p) => &p.lifetimes,
+            Self::OutStruct(p) => &p.lifetimes,
+        }
+    }
+}
+
+impl<P: TyPosition> StructPath<P> {
+    /// Returns a new [`EnumPath`].
+    pub(super) fn new(lifetimes: Lifetimes, tcx_id: P::StructId) -> Self {
+        Self { lifetimes, tcx_id }
+    }
+}
+impl StructPath {
+    /// Returns the [`StructDef`] that this path references.
+    pub fn resolve<'tcx>(&self, tcx: &'tcx TypeContext) -> &'tcx StructDef {
+        tcx.resolve_struct(self.tcx_id)
+    }
+}
+
+impl OutStructPath {
+    /// Returns the [`OutStructDef`] that this path references.
+    pub fn resolve<'tcx>(&self, tcx: &'tcx TypeContext) -> &'tcx OutStructDef {
+        tcx.resolve_out_struct(self.tcx_id)
+    }
+
+    /// Get a map of lifetimes used on this path to lifetimes as named in the def site. See [`LinkedLifetimes`]
+    /// for more information.
+    pub fn link_lifetimes<'def, 'tcx>(
+        &'def self,
+        tcx: &'tcx TypeContext,
+    ) -> LinkedLifetimes<'def, 'tcx> {
+        let struc = self.resolve(tcx);
+        let env = &struc.lifetimes;
+        LinkedLifetimes::new(env, None, &self.lifetimes)
+    }
+}
+
+impl<Opt, Owner> OpaquePath<Opt, Owner> {
+    /// Returns a new [`OpaquePath`].
+    pub(super) fn new(lifetimes: Lifetimes, optional: Opt, owner: Owner, tcx_id: OpaqueId) -> Self {
+        Self {
+            lifetimes,
+            optional,
+            owner,
+            tcx_id,
+        }
+    }
+
+    /// Returns the [`OpaqueDef`] that this path references.
+    pub fn resolve<'tcx>(&self, tcx: &'tcx TypeContext) -> &'tcx OpaqueDef {
+        tcx.resolve_opaque(self.tcx_id)
+    }
+}
+
+impl<Opt, Owner: OpaqueOwner> OpaquePath<Opt, Owner> {
+    /// Get a map of lifetimes used on this path to lifetimes as named in the def site. See [`LinkedLifetimes`]
+    /// for more information.
+    pub fn link_lifetimes<'def, 'tcx>(
+        &'def self,
+        tcx: &'tcx TypeContext,
+    ) -> LinkedLifetimes<'def, 'tcx> {
+        let opaque = self.resolve(tcx);
+        let env = &opaque.lifetimes;
+        LinkedLifetimes::new(env, self.owner.lifetime(), &self.lifetimes)
+    }
+}
+
+impl EnumPath {
+    /// Returns a new [`EnumPath`].
+    pub(super) fn new(tcx_id: EnumId) -> Self {
+        Self { tcx_id }
+    }
+
+    /// Returns the [`EnumDef`] that this path references.
+    pub fn resolve<'tcx>(&self, tcx: &'tcx TypeContext) -> &'tcx EnumDef {
+        tcx.resolve_enum(self.tcx_id)
+    }
+}
+
+impl TraitPath {
+    /// Returns a new [`TraitPath`].
+    pub(super) fn new(lifetimes: Lifetimes, tcx_id: TraitId) -> Self {
+        Self { lifetimes, tcx_id }
+    }
+}
diff --git a/crates/diplomat_core/src/hir/primitives.rs b/crates/diplomat_core/src/hir/primitives.rs
new file mode 100644
index 0000000..0966409
--- /dev/null
+++ b/crates/diplomat_core/src/hir/primitives.rs
@@ -0,0 +1,138 @@
+//! Primitives types that can cross the FFI boundary.
+use crate::ast;
+
+/// 8, 16, 32, and 64-bit signed and unsigned integers.
+#[derive(Copy, Clone, Debug)]
+#[allow(clippy::exhaustive_enums)] // there are only these
+pub enum IntType {
+    I8,
+    I16,
+    I32,
+    I64,
+    U8,
+    U16,
+    U32,
+    U64,
+}
+
+/// Platform-dependent signed and unsigned size types.
+#[derive(Copy, Clone, Debug)]
+#[allow(clippy::exhaustive_enums)] // there are only these
+pub enum IntSizeType {
+    Isize,
+    Usize,
+}
+
+/// 128-bit signed and unsigned integers.
+#[derive(Copy, Clone, Debug)]
+#[allow(clippy::exhaustive_enums)] // there are only these
+pub enum Int128Type {
+    I128,
+    U128,
+}
+
+/// 32 and 64-bit floating point numbers.
+#[derive(Copy, Clone, Debug)]
+#[allow(clippy::exhaustive_enums)] // there are only these
+pub enum FloatType {
+    F32,
+    F64,
+}
+
+/// All primitive types.
+#[derive(Copy, Clone, Debug)]
+#[allow(clippy::exhaustive_enums)] // there are only these
+pub enum PrimitiveType {
+    Bool,
+    Char,
+    /// a primitive byte that is not meant to be interpreted numerically
+    /// in languages that don't have fine-grained integer types
+    Byte,
+    Int(IntType),
+    IntSize(IntSizeType),
+    Int128(Int128Type),
+    Float(FloatType),
+}
+
+impl IntType {
+    /// Returns the string representation of `self`.
+    pub fn as_str(&self) -> &'static str {
+        match self {
+            IntType::I8 => "i8",
+            IntType::I16 => "i16",
+            IntType::I32 => "i32",
+            IntType::I64 => "i64",
+            IntType::U8 => "u8",
+            IntType::U16 => "u16",
+            IntType::U32 => "u32",
+            IntType::U64 => "u64",
+        }
+    }
+}
+
+impl IntSizeType {
+    /// Returns the string representation of `self`.
+    pub fn as_str(&self) -> &'static str {
+        match self {
+            IntSizeType::Isize => "isize",
+            IntSizeType::Usize => "usize",
+        }
+    }
+}
+
+impl Int128Type {
+    /// Returns the string representation of `self`.
+    pub fn as_str(&self) -> &'static str {
+        match self {
+            Int128Type::I128 => "i128",
+            Int128Type::U128 => "u128",
+        }
+    }
+}
+
+impl FloatType {
+    /// Returns the string representation of `self`.
+    pub fn as_str(&self) -> &'static str {
+        match self {
+            FloatType::F32 => "f32",
+            FloatType::F64 => "f64",
+        }
+    }
+}
+
+impl PrimitiveType {
+    pub(super) fn from_ast(prim: ast::PrimitiveType) -> Self {
+        match prim {
+            ast::PrimitiveType::i8 => PrimitiveType::Int(IntType::I8),
+            ast::PrimitiveType::u8 => PrimitiveType::Int(IntType::U8),
+            ast::PrimitiveType::i16 => PrimitiveType::Int(IntType::I16),
+            ast::PrimitiveType::u16 => PrimitiveType::Int(IntType::U16),
+            ast::PrimitiveType::i32 => PrimitiveType::Int(IntType::I32),
+            ast::PrimitiveType::u32 => PrimitiveType::Int(IntType::U32),
+            ast::PrimitiveType::i64 => PrimitiveType::Int(IntType::I64),
+            ast::PrimitiveType::u64 => PrimitiveType::Int(IntType::U64),
+            ast::PrimitiveType::isize => PrimitiveType::IntSize(IntSizeType::Isize),
+            ast::PrimitiveType::usize => PrimitiveType::IntSize(IntSizeType::Usize),
+            ast::PrimitiveType::i128 => PrimitiveType::Int128(Int128Type::I128),
+            ast::PrimitiveType::u128 => PrimitiveType::Int128(Int128Type::U128),
+            ast::PrimitiveType::f32 => PrimitiveType::Float(FloatType::F32),
+            ast::PrimitiveType::f64 => PrimitiveType::Float(FloatType::F64),
+            ast::PrimitiveType::bool => PrimitiveType::Bool,
+            ast::PrimitiveType::char => PrimitiveType::Char,
+            ast::PrimitiveType::byte => PrimitiveType::Byte,
+        }
+    }
+
+    /// Returns the string representation of `self`.
+    pub fn as_str(&self) -> &'static str {
+        match self {
+            PrimitiveType::Bool => "bool",
+            PrimitiveType::Char => "char",
+            PrimitiveType::Byte => "byte",
+            PrimitiveType::Int(ty) => ty.as_str(),
+            PrimitiveType::IntSize(ty) => ty.as_str(),
+            PrimitiveType::Int128(ty) => ty.as_str(),
+            PrimitiveType::Float(ty) => ty.as_str(),
+        }
+    }
+}
diff --git a/crates/diplomat_core/src/hir/snapshots/diplomat_core__hir__attrs__tests__auto.snap b/crates/diplomat_core/src/hir/snapshots/diplomat_core__hir__attrs__tests__auto.snap
new file mode 100644
index 0000000..16f185d
--- /dev/null
+++ b/crates/diplomat_core/src/hir/snapshots/diplomat_core__hir__attrs__tests__auto.snap
@@ -0,0 +1,7 @@
+---
+source: core/src/hir/attrs.rs
+expression: output
+---
+Lowering error in Opaque::next: `iterator` not supported in backend tests
+Lowering error in Opaque::auto_doesnt_work_on_renames: Diplomat attribute rename gated on 'auto' but is not one that works with 'auto'
+Lowering error in Opaque::auto_doesnt_work_on_disables: Diplomat attribute disable gated on 'auto' but is not one that works with 'auto'
diff --git a/crates/diplomat_core/src/hir/snapshots/diplomat_core__hir__attrs__tests__comparator.snap b/crates/diplomat_core/src/hir/snapshots/diplomat_core__hir__attrs__tests__comparator.snap
new file mode 100644
index 0000000..a19891d
--- /dev/null
+++ b/crates/diplomat_core/src/hir/snapshots/diplomat_core__hir__attrs__tests__comparator.snap
@@ -0,0 +1,19 @@
+---
+source: core/src/hir/attrs.rs
+expression: output
+---
+Lowering error in Struct::comparison_other: Comparator's parameter must be identical to self
+Lowering error in Struct::comparison_correct: Cannot define two comparators on the same type
+Lowering error in Opaque::comparator_static: Comparator must be non-static
+Lowering error in Opaque::comparator_none: Comparator must have single parameter
+Lowering error in Opaque::comparator_none: Cannot define two comparators on the same type
+Lowering error in Opaque::comparator_othertype: Cannot define two comparators on the same type
+Lowering error in Opaque::comparator_othertype: Comparator must be non-static
+Lowering error in Opaque::comparator_badreturn: Cannot define two comparators on the same type
+Lowering error in Opaque::comparator_badreturn: Found comparison method that does not return cmp::Ordering
+Lowering error in Opaque::comparison_correct: Cannot define two comparators on the same type
+Lowering error in Opaque::ordering_wrong: Found cmp::Ordering in parameter or struct field, it is only allowed in return types
+Lowering error in Opaque::comparison_mut: Cannot define two comparators on the same type
+Lowering error in Opaque::comparison_mut: comparators must accept immutable parameters
+Lowering error in Opaque::comparison_opt: Cannot define two comparators on the same type
+Lowering error in Opaque::comparison_opt: comparators must accept non-optional parameters
diff --git a/crates/diplomat_core/src/hir/snapshots/diplomat_core__hir__attrs__tests__iterator.snap b/crates/diplomat_core/src/hir/snapshots/diplomat_core__hir__attrs__tests__iterator.snap
new file mode 100644
index 0000000..8cb0929
--- /dev/null
+++ b/crates/diplomat_core/src/hir/snapshots/diplomat_core__hir__attrs__tests__iterator.snap
@@ -0,0 +1,13 @@
+---
+source: core/src/hir/attrs.rs
+expression: output
+---
+Lowering error in Broken::iterable_no_return: Iterables must return a custom type
+Lowering error in Broken::iterable_no_self: Iterables must take self
+Lowering error in Broken::iterable_non_custom: Cannot mark type as iterable twice
+Lowering error in Broken::iterable_non_custom: Iterables must return a custom opaque type
+Lowering error in BrokenIterator::iterator_no_return: Iterator method must return nullable value
+Lowering error in BrokenIterator::iterator_no_self: Iterators must take self
+Lowering error in BrokenIterator::iterator_no_option: Cannot mark type as iterator twice
+Lowering error in BrokenIterator::iterator_no_option: Iterator method must return nullable value
+
diff --git a/crates/diplomat_core/src/hir/snapshots/diplomat_core__hir__attrs__tests__unsupported_features.snap b/crates/diplomat_core/src/hir/snapshots/diplomat_core__hir__attrs__tests__unsupported_features.snap
new file mode 100644
index 0000000..f66b609
--- /dev/null
+++ b/crates/diplomat_core/src/hir/snapshots/diplomat_core__hir__attrs__tests__unsupported_features.snap
@@ -0,0 +1,8 @@
+---
+source: core/src/hir/attrs.rs
+expression: output
+---
+Lowering error in OutStruct: Options of structs/enums/primitives not supported by this backend
+Lowering error in Struct: Options of structs/enums/primitives not supported by this backend
+Lowering error in Struct2: Options of structs/enums/primitives not supported by this backend
+Lowering error in Opaque::take_option: Options of structs/enums/primitives not supported by this backend
diff --git a/crates/diplomat_core/src/hir/snapshots/diplomat_core__hir__elision__tests__borrowing_fields.snap b/crates/diplomat_core/src/hir/snapshots/diplomat_core__hir__elision__tests__borrowing_fields.snap
new file mode 100644
index 0000000..4728442
--- /dev/null
+++ b/crates/diplomat_core/src/hir/snapshots/diplomat_core__hir__elision__tests__borrowing_fields.snap
@@ -0,0 +1,25 @@
+---
+source: core/src/hir/elision.rs
+expression: lt_to_borrowing_fields
+---
+{
+    Static: [
+        "this.name",
+        "_s",
+    ],
+    NonStatic(
+        Lifetime(
+            0,
+        ),
+    ): [
+        "this.p_data",
+    ],
+    NonStatic(
+        Lifetime(
+            1,
+        ),
+    ): [
+        "this.q_data",
+        "this.inner.more_data",
+    ],
+}
diff --git a/crates/diplomat_core/src/hir/snapshots/diplomat_core__hir__elision__tests__elision_in_struct.snap b/crates/diplomat_core/src/hir/snapshots/diplomat_core__hir__elision__tests__elision_in_struct.snap
new file mode 100644
index 0000000..3fec47a
--- /dev/null
+++ b/crates/diplomat_core/src/hir/snapshots/diplomat_core__hir__elision__tests__elision_in_struct.snap
@@ -0,0 +1,137 @@
+---
+source: core/src/hir/elision.rs
+expression: method
+---
+Method {
+    docs: Docs(
+        "",
+        [],
+    ),
+    name: "elided",
+    abi_name: "Opaque_elided",
+    lifetime_env: LifetimeEnv {
+        nodes: [],
+        num_lifetimes: 3,
+    },
+    param_self: Some(
+        ParamSelf {
+            ty: Opaque(
+                OpaquePath {
+                    lifetimes: Lifetimes {
+                        indices: [],
+                    },
+                    optional: NonOptional,
+                    owner: Borrow {
+                        lifetime: NonStatic(
+                            Lifetime(
+                                0,
+                            ),
+                        ),
+                        mutability: Immutable,
+                    },
+                    tcx_id: OpaqueId(
+                        0,
+                    ),
+                },
+            ),
+            attrs: Attrs {
+                disable: false,
+                namespace: None,
+                rename: RenameAttr {
+                    pattern: None,
+                },
+                abi_rename: RenameAttr {
+                    pattern: None,
+                },
+                special_method: None,
+                demo_attrs: DemoInfo {
+                    generate: false,
+                    default_constructor: false,
+                    external: false,
+                    custom_func: None,
+                    input_cfg: DemoInputCFG {
+                        label: "",
+                        default_value: "",
+                    },
+                },
+            },
+        },
+    ),
+    params: [
+        Param {
+            name: "x",
+            ty: Opaque(
+                OpaquePath {
+                    lifetimes: Lifetimes {
+                        indices: [
+                            NonStatic(
+                                Lifetime(
+                                    2,
+                                ),
+                            ),
+                        ],
+                    },
+                    optional: Optional(
+                        false,
+                    ),
+                    owner: Borrow {
+                        lifetime: NonStatic(
+                            Lifetime(
+                                1,
+                            ),
+                        ),
+                        mutability: Immutable,
+                    },
+                    tcx_id: OpaqueId(
+                        1,
+                    ),
+                },
+            ),
+            attrs: Attrs {
+                disable: false,
+                namespace: None,
+                rename: RenameAttr {
+                    pattern: None,
+                },
+                abi_rename: RenameAttr {
+                    pattern: None,
+                },
+                special_method: None,
+                demo_attrs: DemoInfo {
+                    generate: false,
+                    default_constructor: false,
+                    external: false,
+                    custom_func: None,
+                    input_cfg: DemoInputCFG {
+                        label: "",
+                        default_value: "",
+                    },
+                },
+            },
+        },
+    ],
+    output: Infallible(
+        Unit,
+    ),
+    attrs: Attrs {
+        disable: false,
+        namespace: None,
+        rename: RenameAttr {
+            pattern: None,
+        },
+        abi_rename: RenameAttr {
+            pattern: None,
+        },
+        special_method: None,
+        demo_attrs: DemoInfo {
+            generate: false,
+            default_constructor: false,
+            external: false,
+            custom_func: None,
+            input_cfg: DemoInputCFG {
+                label: "",
+                default_value: "",
+            },
+        },
+    },
+}
diff --git a/crates/diplomat_core/src/hir/snapshots/diplomat_core__hir__elision__tests__simple_mod.snap b/crates/diplomat_core/src/hir/snapshots/diplomat_core__hir__elision__tests__simple_mod.snap
new file mode 100644
index 0000000..a19cedd
--- /dev/null
+++ b/crates/diplomat_core/src/hir/snapshots/diplomat_core__hir__elision__tests__simple_mod.snap
@@ -0,0 +1,475 @@
+---
+source: core/src/hir/elision.rs
+expression: tcx
+---
+TypeContext {
+    out_structs: [
+        StructDef {
+            docs: Docs(
+                "",
+                [],
+            ),
+            name: "OutStruct",
+            fields: [
+                StructField {
+                    docs: Docs(
+                        "",
+                        [],
+                    ),
+                    name: "inner",
+                    ty: Opaque(
+                        OpaquePath {
+                            lifetimes: Lifetimes {
+                                indices: [
+                                    NonStatic(
+                                        Lifetime(
+                                            0,
+                                        ),
+                                    ),
+                                ],
+                            },
+                            optional: Optional(
+                                false,
+                            ),
+                            owner: Own,
+                            tcx_id: OpaqueId(
+                                0,
+                            ),
+                        },
+                    ),
+                    attrs: Attrs {
+                        disable: false,
+                        namespace: None,
+                        rename: RenameAttr {
+                            pattern: None,
+                        },
+                        abi_rename: RenameAttr {
+                            pattern: None,
+                        },
+                        special_method: None,
+                        demo_attrs: DemoInfo {
+                            generate: false,
+                            default_constructor: false,
+                            external: false,
+                            custom_func: None,
+                            input_cfg: DemoInputCFG {
+                                label: "",
+                                default_value: "",
+                            },
+                        },
+                    },
+                },
+            ],
+            methods: [
+                Method {
+                    docs: Docs(
+                        "",
+                        [],
+                    ),
+                    name: "new",
+                    abi_name: "OutStruct_new",
+                    lifetime_env: LifetimeEnv {
+                        nodes: [
+                            BoundedLifetime {
+                                ident: "a",
+                                longer: [],
+                                shorter: [],
+                            },
+                        ],
+                        num_lifetimes: 1,
+                    },
+                    param_self: None,
+                    params: [
+                        Param {
+                            name: "s",
+                            ty: Slice(
+                                Str(
+                                    Some(
+                                        NonStatic(
+                                            Lifetime(
+                                                0,
+                                            ),
+                                        ),
+                                    ),
+                                    UnvalidatedUtf8,
+                                ),
+                            ),
+                            attrs: Attrs {
+                                disable: false,
+                                namespace: None,
+                                rename: RenameAttr {
+                                    pattern: None,
+                                },
+                                abi_rename: RenameAttr {
+                                    pattern: None,
+                                },
+                                special_method: None,
+                                demo_attrs: DemoInfo {
+                                    generate: false,
+                                    default_constructor: false,
+                                    external: false,
+                                    custom_func: None,
+                                    input_cfg: DemoInputCFG {
+                                        label: "",
+                                        default_value: "",
+                                    },
+                                },
+                            },
+                        },
+                    ],
+                    output: Infallible(
+                        OutType(
+                            Struct(
+                                OutStruct(
+                                    StructPath {
+                                        lifetimes: Lifetimes {
+                                            indices: [
+                                                NonStatic(
+                                                    Lifetime(
+                                                        0,
+                                                    ),
+                                                ),
+                                            ],
+                                        },
+                                        tcx_id: OutStructId(
+                                            0,
+                                        ),
+                                    },
+                                ),
+                            ),
+                        ),
+                    ),
+                    attrs: Attrs {
+                        disable: false,
+                        namespace: None,
+                        rename: RenameAttr {
+                            pattern: None,
+                        },
+                        abi_rename: RenameAttr {
+                            pattern: None,
+                        },
+                        special_method: None,
+                        demo_attrs: DemoInfo {
+                            generate: false,
+                            default_constructor: false,
+                            external: false,
+                            custom_func: None,
+                            input_cfg: DemoInputCFG {
+                                label: "",
+                                default_value: "",
+                            },
+                        },
+                    },
+                },
+            ],
+            attrs: Attrs {
+                disable: false,
+                namespace: None,
+                rename: RenameAttr {
+                    pattern: None,
+                },
+                abi_rename: RenameAttr {
+                    pattern: None,
+                },
+                special_method: None,
+                demo_attrs: DemoInfo {
+                    generate: false,
+                    default_constructor: false,
+                    external: false,
+                    custom_func: None,
+                    input_cfg: DemoInputCFG {
+                        label: "",
+                        default_value: "",
+                    },
+                },
+            },
+            lifetimes: LifetimeEnv {
+                nodes: [
+                    BoundedLifetime {
+                        ident: "a",
+                        longer: [],
+                        shorter: [],
+                    },
+                ],
+                num_lifetimes: 1,
+            },
+            special_method_presence: SpecialMethodPresence {
+                comparator: false,
+                iterator: None,
+                iterable: None,
+            },
+        },
+    ],
+    structs: [
+        StructDef {
+            docs: Docs(
+                "",
+                [],
+            ),
+            name: "Struct",
+            fields: [
+                StructField {
+                    docs: Docs(
+                        "",
+                        [],
+                    ),
+                    name: "s",
+                    ty: Slice(
+                        Str(
+                            Some(
+                                NonStatic(
+                                    Lifetime(
+                                        0,
+                                    ),
+                                ),
+                            ),
+                            UnvalidatedUtf8,
+                        ),
+                    ),
+                    attrs: Attrs {
+                        disable: false,
+                        namespace: None,
+                        rename: RenameAttr {
+                            pattern: None,
+                        },
+                        abi_rename: RenameAttr {
+                            pattern: None,
+                        },
+                        special_method: None,
+                        demo_attrs: DemoInfo {
+                            generate: false,
+                            default_constructor: false,
+                            external: false,
+                            custom_func: None,
+                            input_cfg: DemoInputCFG {
+                                label: "",
+                                default_value: "",
+                            },
+                        },
+                    },
+                },
+            ],
+            methods: [
+                Method {
+                    docs: Docs(
+                        "",
+                        [],
+                    ),
+                    name: "rustc_elision",
+                    abi_name: "Struct_rustc_elision",
+                    lifetime_env: LifetimeEnv {
+                        nodes: [
+                            BoundedLifetime {
+                                ident: "a",
+                                longer: [],
+                                shorter: [],
+                            },
+                        ],
+                        num_lifetimes: 2,
+                    },
+                    param_self: Some(
+                        ParamSelf {
+                            ty: Struct(
+                                StructPath {
+                                    lifetimes: Lifetimes {
+                                        indices: [
+                                            NonStatic(
+                                                Lifetime(
+                                                    0,
+                                                ),
+                                            ),
+                                        ],
+                                    },
+                                    tcx_id: StructId(
+                                        0,
+                                    ),
+                                },
+                            ),
+                            attrs: Attrs {
+                                disable: false,
+                                namespace: None,
+                                rename: RenameAttr {
+                                    pattern: None,
+                                },
+                                abi_rename: RenameAttr {
+                                    pattern: None,
+                                },
+                                special_method: None,
+                                demo_attrs: DemoInfo {
+                                    generate: false,
+                                    default_constructor: false,
+                                    external: false,
+                                    custom_func: None,
+                                    input_cfg: DemoInputCFG {
+                                        label: "",
+                                        default_value: "",
+                                    },
+                                },
+                            },
+                        },
+                    ),
+                    params: [
+                        Param {
+                            name: "s",
+                            ty: Slice(
+                                Str(
+                                    Some(
+                                        NonStatic(
+                                            Lifetime(
+                                                1,
+                                            ),
+                                        ),
+                                    ),
+                                    UnvalidatedUtf8,
+                                ),
+                            ),
+                            attrs: Attrs {
+                                disable: false,
+                                namespace: None,
+                                rename: RenameAttr {
+                                    pattern: None,
+                                },
+                                abi_rename: RenameAttr {
+                                    pattern: None,
+                                },
+                                special_method: None,
+                                demo_attrs: DemoInfo {
+                                    generate: false,
+                                    default_constructor: false,
+                                    external: false,
+                                    custom_func: None,
+                                    input_cfg: DemoInputCFG {
+                                        label: "",
+                                        default_value: "",
+                                    },
+                                },
+                            },
+                        },
+                    ],
+                    output: Infallible(
+                        OutType(
+                            Slice(
+                                Str(
+                                    Some(
+                                        NonStatic(
+                                            Lifetime(
+                                                1,
+                                            ),
+                                        ),
+                                    ),
+                                    UnvalidatedUtf8,
+                                ),
+                            ),
+                        ),
+                    ),
+                    attrs: Attrs {
+                        disable: false,
+                        namespace: None,
+                        rename: RenameAttr {
+                            pattern: None,
+                        },
+                        abi_rename: RenameAttr {
+                            pattern: None,
+                        },
+                        special_method: None,
+                        demo_attrs: DemoInfo {
+                            generate: false,
+                            default_constructor: false,
+                            external: false,
+                            custom_func: None,
+                            input_cfg: DemoInputCFG {
+                                label: "",
+                                default_value: "",
+                            },
+                        },
+                    },
+                },
+            ],
+            attrs: Attrs {
+                disable: false,
+                namespace: None,
+                rename: RenameAttr {
+                    pattern: None,
+                },
+                abi_rename: RenameAttr {
+                    pattern: None,
+                },
+                special_method: None,
+                demo_attrs: DemoInfo {
+                    generate: false,
+                    default_constructor: false,
+                    external: false,
+                    custom_func: None,
+                    input_cfg: DemoInputCFG {
+                        label: "",
+                        default_value: "",
+                    },
+                },
+            },
+            lifetimes: LifetimeEnv {
+                nodes: [
+                    BoundedLifetime {
+                        ident: "a",
+                        longer: [],
+                        shorter: [],
+                    },
+                ],
+                num_lifetimes: 1,
+            },
+            special_method_presence: SpecialMethodPresence {
+                comparator: false,
+                iterator: None,
+                iterable: None,
+            },
+        },
+    ],
+    opaques: [
+        OpaqueDef {
+            docs: Docs(
+                "",
+                [],
+            ),
+            name: "Opaque",
+            methods: [],
+            attrs: Attrs {
+                disable: false,
+                namespace: None,
+                rename: RenameAttr {
+                    pattern: None,
+                },
+                abi_rename: RenameAttr {
+                    pattern: None,
+                },
+                special_method: None,
+                demo_attrs: DemoInfo {
+                    generate: false,
+                    default_constructor: false,
+                    external: false,
+                    custom_func: None,
+                    input_cfg: DemoInputCFG {
+                        label: "",
+                        default_value: "",
+                    },
+                },
+            },
+            lifetimes: LifetimeEnv {
+                nodes: [
+                    BoundedLifetime {
+                        ident: "a",
+                        longer: [],
+                        shorter: [],
+                    },
+                ],
+                num_lifetimes: 1,
+            },
+            special_method_presence: SpecialMethodPresence {
+                comparator: false,
+                iterator: None,
+                iterable: None,
+            },
+            dtor_abi_name: "Opaque_destroy",
+        },
+    ],
+    enums: [],
+    traits: [],
+}
diff --git a/crates/diplomat_core/src/hir/snapshots/diplomat_core__hir__type_context__tests__basic_lowering.snap b/crates/diplomat_core/src/hir/snapshots/diplomat_core__hir__type_context__tests__basic_lowering.snap
new file mode 100644
index 0000000..03be000
--- /dev/null
+++ b/crates/diplomat_core/src/hir/snapshots/diplomat_core__hir__type_context__tests__basic_lowering.snap
@@ -0,0 +1,16 @@
+---
+source: core/src/hir/type_context.rs
+expression: output
+---
+Lowering error in BadStructFields: Found FFI-unsafe type Option<u8> in struct field BadStructFields.field1, consider using DiplomatOption<u8>
+Lowering error in BadStructFields: Found FFI-unsafe type Result<u8, u8> in struct field BadStructFields.field2, consider using Result<u8, u8>
+Lowering error in BadStructFields: Results can only appear as the top-level return type of methods
+Lowering error in InStructWithOutField: found Box<T> in input where T is an opaque, but owned opaques aren't allowed in inputs. try &T instead? T = OtherOpaque
+Lowering error in InStructWithOutField: found struct in input that is marked with #[diplomat::out]: OutStruct in OutStruct
+Lowering error in Opaque::use_foo_ref: found &T in input where T is a custom type, but not opaque. T = Foo
+Lowering error in Opaque::return_foo_box: found Box<T> in output where T is a custom type but not opaque. non-opaques can't be behind pointers. T = Foo
+Lowering error in Opaque::use_self: Method `Opaque_use_self` takes an opaque by value as the self parameter, but opaques as inputs must be behind refs
+Lowering error in Opaque::return_self: Method `Opaque_return_self` takes an opaque by value as the self parameter, but opaques as inputs must be behind refs
+Lowering error in Opaque::use_opaque_owned: Opaque passed by value: OtherOpaque
+Lowering error in Opaque::return_opaque_owned: Opaque passed by value in input: OtherOpaque
+Lowering error in Opaque::use_out_as_in: found struct in input that is marked with #[diplomat::out]: OutStruct in OutStruct
diff --git a/crates/diplomat_core/src/hir/snapshots/diplomat_core__hir__type_context__tests__lifetime_in_return.snap b/crates/diplomat_core/src/hir/snapshots/diplomat_core__hir__type_context__tests__lifetime_in_return.snap
new file mode 100644
index 0000000..f052d18
--- /dev/null
+++ b/crates/diplomat_core/src/hir/snapshots/diplomat_core__hir__type_context__tests__lifetime_in_return.snap
@@ -0,0 +1,7 @@
+---
+source: core/src/hir/type_context.rs
+expression: output
+---
+Lowering error in Opaque::returns_self: Found elided lifetime in return type, please explicitly specify
+Lowering error in Opaque::returns_foo: Found elided lifetime in return type, please explicitly specify
+
diff --git a/crates/diplomat_core/src/hir/snapshots/diplomat_core__hir__type_context__tests__non_opaque_move.snap b/crates/diplomat_core/src/hir/snapshots/diplomat_core__hir__type_context__tests__non_opaque_move.snap
new file mode 100644
index 0000000..283f6b1
--- /dev/null
+++ b/crates/diplomat_core/src/hir/snapshots/diplomat_core__hir__type_context__tests__non_opaque_move.snap
@@ -0,0 +1,9 @@
+---
+source: core/src/hir/type_context.rs
+expression: output
+---
+Lowering error in NonOpaque::foo: Method `NonOpaque_foo` takes a reference to a struct as a self parameter, which isn't allowed
+Lowering error in Opaque::bar: found &T in output where T is a custom type, but not opaque. T = NonOpaque
+Lowering error in Opaque::baz: found &T in input where T is a custom type, but not opaque. T = NonOpaque
+Lowering error in Opaque::quux: found Box<T> in output where T is a custom type but not opaque. non-opaques can't be behind pointers. T = NonOpaque
+
diff --git a/crates/diplomat_core/src/hir/snapshots/diplomat_core__hir__type_context__tests__opaque_checks_with_error.snap b/crates/diplomat_core/src/hir/snapshots/diplomat_core__hir__type_context__tests__opaque_checks_with_error.snap
new file mode 100644
index 0000000..76bf3f0
--- /dev/null
+++ b/crates/diplomat_core/src/hir/snapshots/diplomat_core__hir__type_context__tests__opaque_checks_with_error.snap
@@ -0,0 +1,7 @@
+---
+source: core/src/hir/type_context.rs
+expression: output
+---
+Lowering error in OpaqueStruct::new: Opaque passed by value in input: OpaqueStruct
+Lowering error in OpaqueStruct::get_i32: Method `OpaqueStruct_get_i32` takes an opaque by value as the self parameter, but opaques as inputs must be behind refs
+
diff --git a/crates/diplomat_core/src/hir/snapshots/diplomat_core__hir__type_context__tests__opaque_checks_with_safe_use.snap b/crates/diplomat_core/src/hir/snapshots/diplomat_core__hir__type_context__tests__opaque_checks_with_safe_use.snap
new file mode 100644
index 0000000..ea2e6fc
--- /dev/null
+++ b/crates/diplomat_core/src/hir/snapshots/diplomat_core__hir__type_context__tests__opaque_checks_with_safe_use.snap
@@ -0,0 +1,5 @@
+---
+source: core/src/hir/type_context.rs
+expression: output
+---
+Lowering error in NonOpaqueStruct: Methods on ZST structs are not yet implemented: NonOpaqueStruct
diff --git a/crates/diplomat_core/src/hir/snapshots/diplomat_core__hir__type_context__tests__opaque_ffi.snap b/crates/diplomat_core/src/hir/snapshots/diplomat_core__hir__type_context__tests__opaque_ffi.snap
new file mode 100644
index 0000000..1d470de
--- /dev/null
+++ b/crates/diplomat_core/src/hir/snapshots/diplomat_core__hir__type_context__tests__opaque_ffi.snap
@@ -0,0 +1,7 @@
+---
+source: core/src/hir/type_context.rs
+expression: output
+---
+Lowering error in MyOpaqueStruct::new_broken: Opaque passed by value in input: MyOpaqueStruct
+Lowering error in MyOpaqueStruct::do_thing_broken: Method `MyOpaqueStruct_do_thing_broken` takes an opaque by value as the self parameter, but opaques as inputs must be behind refs
+
diff --git a/crates/diplomat_core/src/hir/snapshots/diplomat_core__hir__type_context__tests__option.snap b/crates/diplomat_core/src/hir/snapshots/diplomat_core__hir__type_context__tests__option.snap
new file mode 100644
index 0000000..12937b2
--- /dev/null
+++ b/crates/diplomat_core/src/hir/snapshots/diplomat_core__hir__type_context__tests__option.snap
@@ -0,0 +1,8 @@
+---
+source: core/src/hir/type_context.rs
+expression: output
+---
+Lowering error in BrokenStruct: Found FFI-unsafe type Option<u8> in struct field BrokenStruct.regular_option, consider using DiplomatOption<u8>
+Lowering error in BrokenStruct: Found FFI-unsafe type Option<CustomStruct> in struct field BrokenStruct.regular_option, consider using DiplomatOption<CustomStruct>
+Lowering error in Foo::diplo_option_ref: found DiplomatOption<&T>, please use Option<&T> (DiplomatOption is for primitives, structs, and enums)
+Lowering error in Foo::diplo_option_box: found DiplomatOption<Box<T>>, please use Option<Box<T>> (DiplomatOption is for primitives, structs, and enums)
diff --git a/crates/diplomat_core/src/hir/snapshots/diplomat_core__hir__type_context__tests__option_invalid.snap b/crates/diplomat_core/src/hir/snapshots/diplomat_core__hir__type_context__tests__option_invalid.snap
new file mode 100644
index 0000000..f6fc63b
--- /dev/null
+++ b/crates/diplomat_core/src/hir/snapshots/diplomat_core__hir__type_context__tests__option_invalid.snap
@@ -0,0 +1,7 @@
+---
+source: core/src/hir/type_context.rs
+expression: output
+---
+Lowering error in Foo: Found FFI-unsafe type Option<u8> in struct field Foo.field, consider using DiplomatOption<u8>
+Lowering error in Foo::do_thing: found Option<T> in input, where T isn't a reference but Option<T> in inputs requires that T is a reference to an opaque. T = Option<u16>
+Lowering error in Foo::do_thing2: Results can only appear as the top-level return type of methods
diff --git a/crates/diplomat_core/src/hir/snapshots/diplomat_core__hir__type_context__tests__option_valid.snap b/crates/diplomat_core/src/hir/snapshots/diplomat_core__hir__type_context__tests__option_valid.snap
new file mode 100644
index 0000000..fbd4d2c
--- /dev/null
+++ b/crates/diplomat_core/src/hir/snapshots/diplomat_core__hir__type_context__tests__option_valid.snap
@@ -0,0 +1,8 @@
+---
+source: core/src/hir/type_context.rs
+expression: output
+---
+Lowering error in Foo: found Option<Box<T>> in input, but box isn't allowed in inputs. T = u8
+Lowering error in Foo::do_thing: found Option<Box<T>> in input, but box isn't allowed in inputs. T = u32
+Lowering error in Foo::do_thing2: found Option<&T> in input, but T isn't a custom type and therefore not opaque. T = u32
+
diff --git a/crates/diplomat_core/src/hir/snapshots/diplomat_core__hir__type_context__tests__required_implied_bounds.snap b/crates/diplomat_core/src/hir/snapshots/diplomat_core__hir__type_context__tests__required_implied_bounds.snap
new file mode 100644
index 0000000..d57f9ea
--- /dev/null
+++ b/crates/diplomat_core/src/hir/snapshots/diplomat_core__hir__type_context__tests__required_implied_bounds.snap
@@ -0,0 +1,11 @@
+---
+source: core/src/hir/type_context.rs
+expression: output
+---
+Lowering error in Opaque::use_foo: Method should explicitly include this lifetime bound from param foo: 'y: 'x (comes from source type's 'b: 'a)
+Lowering error in Opaque::use_foo: Method should explicitly include this lifetime bound from param foo: 'z: 'y (comes from source type's 'c: 'b)
+Lowering error in Opaque::return_foo: Method should explicitly include this lifetime bound from return type: 'y: 'x (comes from source type's 'b: 'a)
+Lowering error in Opaque::return_foo: Method should explicitly include this lifetime bound from return type: 'z: 'y (comes from source type's 'c: 'b)
+Lowering error in Opaque::return_result_foo: Method should explicitly include this lifetime bound from return type: 'y: 'x (comes from source type's 'b: 'a)
+Lowering error in Opaque::return_result_foo: Method should explicitly include this lifetime bound from return type: 'z: 'y (comes from source type's 'c: 'b)
+
diff --git a/crates/diplomat_core/src/hir/snapshots/diplomat_core__hir__type_context__tests__struct_forbidden.snap b/crates/diplomat_core/src/hir/snapshots/diplomat_core__hir__type_context__tests__struct_forbidden.snap
new file mode 100644
index 0000000..41ee009
--- /dev/null
+++ b/crates/diplomat_core/src/hir/snapshots/diplomat_core__hir__type_context__tests__struct_forbidden.snap
@@ -0,0 +1,10 @@
+---
+source: core/src/hir/type_context.rs
+expression: output
+---
+Lowering error in Crimes: Found FFI-unsafe type &'a str in struct field Crimes.slice1, consider using DiplomatUtf8Str<'a>
+Lowering error in Crimes: Found FFI-unsafe type &'a DiplomatStr in struct field Crimes.slice1, consider using DiplomatStrSlice<'a>
+Lowering error in Crimes: Found FFI-unsafe type &'a [u8] in struct field Crimes.slice2, consider using DiplomatSlice<'a,u8>
+Lowering error in Crimes: Found FFI-unsafe type Box<str> in struct field Crimes.slice3, consider using DiplomatOwnedUtf8Str
+Lowering error in Crimes: Found FFI-unsafe type Box<DiplomatStr> in struct field Crimes.slice3, consider using DiplomatOwnedStrSlice
+Lowering error in Crimes: Found FFI-unsafe type Box<[u8]> in struct field Crimes.slice4, consider using Box<[u8]>
diff --git a/crates/diplomat_core/src/hir/snapshots/diplomat_core__hir__type_context__tests__zst_non_opaque.snap b/crates/diplomat_core/src/hir/snapshots/diplomat_core__hir__type_context__tests__zst_non_opaque.snap
new file mode 100644
index 0000000..b7c6e27
--- /dev/null
+++ b/crates/diplomat_core/src/hir/snapshots/diplomat_core__hir__type_context__tests__zst_non_opaque.snap
@@ -0,0 +1,5 @@
+---
+source: core/src/hir/type_context.rs
+expression: output
+---
+Lowering error in NonOpaque: Methods on ZST structs are not yet implemented: NonOpaque
diff --git a/crates/diplomat_core/src/hir/ty_position.rs b/crates/diplomat_core/src/hir/ty_position.rs
new file mode 100644
index 0000000..173f857
--- /dev/null
+++ b/crates/diplomat_core/src/hir/ty_position.rs
@@ -0,0 +1,326 @@
+use super::lifetimes::{Lifetime, Lifetimes, MaybeStatic};
+use super::{
+    Borrow, Callback, CallbackInstantiationFunctionality, LinkedLifetimes, MaybeOwn, Mutability,
+    NoCallback, NoTraitPath, OutStructId, ReturnableStructPath, StructDef, StructId, StructPath,
+    TraitId, TraitPath, TypeContext, TypeDef, TypeId,
+};
+use core::fmt::Debug;
+
+/// Abstraction over where a type can appear in a function signature.
+///
+/// # "Output only" and "everywhere" types
+///
+/// While Rust is able to give up ownership of values, languages that Diplomat
+/// supports (C++, Javascript, etc.) generally cannot. For example, we can
+/// construct a `Box<MyOpaque>` in a Rust function and _return_ it to the other
+/// language as a pointer. However, we cannot _accept_ `Box<MyOpaque>` as an input
+/// because there's nothing stopping other languages from using that value again.
+/// Therefore, we classify boxed opaques as "output only" types, since they can
+/// only be returned from Rust but not taken as inputs.
+///
+/// Furthermore, Diplomat also supports "bag o' stuff" structs where all fields get
+/// translated at the boundary. If one contains an "output only" type as a field,
+/// then the whole struct must also be "output only". In particular, this means
+/// that if a boxed opaque is nested in a bunch of "bag o' stuff" structs, than
+/// all of those structs must also be "output only".
+///
+/// Currently, there are only two classes of structs: those that are "output only",
+/// and those that are not. These are represented by the types [`OutputOnly`]
+/// and [`Everywhere`] marker types respectively, which are the _only_ two types
+/// that implement [`TyPosition`].
+///
+/// # How does abstraction help?
+///
+/// The HIR was designed around the idea of making invalid states unrepresentable.
+/// Since "output only" types can contain values that "everywhere" types cannot,
+/// it doesn't make sense for them to be represented in the same type, even if
+/// they're mostly the same. One of these differences is that opaques (which are
+/// always behind a pointer) can only be represented as a borrow in "everywhere"
+/// types, but can additionally be represented as owned in "output only" types.
+/// If we were to use the same type for both, then backends working with "everywhere"
+/// types would constantly have unreachable statements for owned opaque cases.
+///
+/// That being said, "output only" and "everywhere" types are still mostly the
+/// same, so this trait allows us to describe the differences. For example, the
+/// HIR uses a singular [`Type`](super::Type) type for representing both
+/// "output only" types and "everywhere" types, since it takes advantage of this
+/// traits associated types to "fill in" the different parts:
+/// ```ignore
+/// pub enum Type<P: TyPosition = Everywhere> {
+///     Primitive(PrimitiveType),
+///     Opaque(OpaquePath<Optional, P::OpaqueOwnership>),
+///     Struct(P::StructPath),
+///     Enum(EnumPath),
+///     Slice(Slice),
+/// }
+/// ```
+///
+/// When `P` takes on [`Everywhere`], this signature becomes:
+/// ```ignore
+/// pub enum Type {
+///     Primitive(PrimitiveType),
+///     Opaque(OpaquePath<Optional, Borrow>),
+///     Struct(StructPath),
+///     Enum(EnumPath),
+///     Slice(Slice),
+/// }
+/// ```
+///
+/// This allows us to represent any kind of type that can appear "everywhere"
+/// i.e. in inputs or outputs. Notice how the second generic in the `Opaque`
+/// variant becomes [`Borrow`]. This describes the semantics of the pointer that
+/// the opaque lives behind, and shows that for "everywhere" types, opaques
+/// can _only_ be represented as living behind a borrow.
+///
+/// Contrast this to when `P` takes on [`OutputOnly`]:
+/// ```ignore
+/// pub enum Type {
+///     Primitive(PrimitiveType),
+///     Opaque(OpaquePath<Optional, MaybeOwn>),
+///     Struct(OutStructPath),
+///     Enum(EnumPath),
+///     Slice(Slice),
+/// }
+/// ```
+/// Here, the second generic of the `Opaque` variant becomes [`MaybeOwn`], meaning
+/// that "output only" types can contain opaques that are either borrowed _or_ owned.
+///
+/// Therefore, this trait allows be extremely precise about making invalid states
+/// unrepresentable, while also reducing duplicated code.
+///
+pub trait TyPosition: Debug + Copy
+where
+    for<'tcx> TypeDef<'tcx>: From<&'tcx StructDef<Self>>,
+{
+    const IN_OUT_STATUS: InputOrOutput;
+    type CallbackInstantiation: Debug + CallbackInstantiationFunctionality;
+
+    /// Type representing how we can point to opaques, which must always be behind a pointer.
+    ///
+    /// The types represented by [`OutputOnly`] are capable of either owning or
+    /// borrowing opaques, and so the associated type for that impl is [`MaybeOwn`].
+    ///
+    /// On the other hand, types represented by [`Everywhere`] can only contain
+    /// borrowes, so the associated type for that impl is [`Borrow`].
+    type OpaqueOwnership: Debug + OpaqueOwner;
+
+    type StructId: Debug;
+
+    type StructPath: Debug + StructPathLike;
+
+    type TraitPath: Debug + TraitIdGetter;
+
+    fn wrap_struct_def<'tcx>(def: &'tcx StructDef<Self>) -> TypeDef<'tcx>;
+    fn build_callback(cb: Callback) -> Self::CallbackInstantiation;
+    fn build_trait_path(trait_path: TraitPath) -> Self::TraitPath;
+}
+
+/// Directionality of the type
+#[non_exhaustive]
+pub enum InputOrOutput {
+    Input,
+    Output,
+    InputOutput,
+}
+
+pub trait TraitIdGetter {
+    fn id(&self) -> TraitId;
+}
+
+/// One of 3 types implementing [`TyPosition`], representing types that can be
+/// used as both input and output to functions.
+///
+/// The restricted versions of this type are [`OutputOnly`] and [`InputOnly`].
+#[derive(Debug, Copy, Clone)]
+#[non_exhaustive]
+pub struct Everywhere;
+
+/// One of 3 types implementing [`TyPosition`], representing types that can
+/// only be used as return types in functions.
+///
+/// The directional opposite of this type is [`InputOnly`].
+#[derive(Debug, Copy, Clone)]
+#[non_exhaustive]
+pub struct OutputOnly;
+
+/// One of 3 types implementing [`TyPosition`], representing types that can
+/// only be used as input types in functions.
+///
+/// The directional opposite of this type is [`OutputOnly`].
+#[derive(Debug, Copy, Clone)]
+#[non_exhaustive]
+pub struct InputOnly;
+
+impl TyPosition for Everywhere {
+    const IN_OUT_STATUS: InputOrOutput = InputOrOutput::InputOutput;
+    type OpaqueOwnership = Borrow;
+    type StructId = StructId;
+    type StructPath = StructPath;
+    type CallbackInstantiation = NoCallback;
+    type TraitPath = NoTraitPath;
+
+    fn wrap_struct_def<'tcx>(def: &'tcx StructDef<Self>) -> TypeDef<'tcx> {
+        TypeDef::Struct(def)
+    }
+    fn build_callback(_cb: Callback) -> Self::CallbackInstantiation {
+        panic!("Callbacks must be input-only");
+    }
+    fn build_trait_path(_trait_path: TraitPath) -> Self::TraitPath {
+        panic!("Traits must be input-only");
+    }
+}
+
+impl TyPosition for OutputOnly {
+    const IN_OUT_STATUS: InputOrOutput = InputOrOutput::Output;
+    type OpaqueOwnership = MaybeOwn;
+    type StructId = OutStructId;
+    type StructPath = ReturnableStructPath;
+    type CallbackInstantiation = NoCallback;
+    type TraitPath = NoTraitPath;
+
+    fn wrap_struct_def<'tcx>(def: &'tcx StructDef<Self>) -> TypeDef<'tcx> {
+        TypeDef::OutStruct(def)
+    }
+    fn build_callback(_cb: Callback) -> Self::CallbackInstantiation {
+        panic!("Callbacks must be input-only");
+    }
+    fn build_trait_path(_trait_path: TraitPath) -> Self::TraitPath {
+        panic!("Traits must be input-only");
+    }
+}
+
+impl TyPosition for InputOnly {
+    const IN_OUT_STATUS: InputOrOutput = InputOrOutput::Input;
+    type OpaqueOwnership = Borrow;
+    type StructId = StructId;
+    type StructPath = StructPath;
+    type CallbackInstantiation = Callback;
+    type TraitPath = TraitPath;
+
+    fn wrap_struct_def<'tcx>(_def: &'tcx StructDef<Self>) -> TypeDef<'tcx> {
+        panic!("Input-only structs are not currently supported");
+    }
+    fn build_callback(cb: Callback) -> Self::CallbackInstantiation {
+        cb
+    }
+    fn build_trait_path(trait_path: TraitPath) -> Self::TraitPath {
+        trait_path
+    }
+}
+
+pub trait StructPathLike {
+    fn lifetimes(&self) -> &Lifetimes;
+    fn id(&self) -> TypeId;
+
+    /// Get a map of lifetimes used on this path to lifetimes as named in the def site. See [`LinkedLifetimes`]
+    /// for more information.
+    fn link_lifetimes<'def, 'tcx>(
+        &'def self,
+        tcx: &'tcx TypeContext,
+    ) -> LinkedLifetimes<'def, 'tcx>;
+}
+
+impl StructPathLike for StructPath {
+    fn lifetimes(&self) -> &Lifetimes {
+        &self.lifetimes
+    }
+    fn id(&self) -> TypeId {
+        self.tcx_id.into()
+    }
+
+    fn link_lifetimes<'def, 'tcx>(
+        &'def self,
+        tcx: &'tcx TypeContext,
+    ) -> LinkedLifetimes<'def, 'tcx> {
+        let struc = self.resolve(tcx);
+        let env = &struc.lifetimes;
+        LinkedLifetimes::new(env, None, &self.lifetimes)
+    }
+}
+
+impl StructPathLike for ReturnableStructPath {
+    fn lifetimes(&self) -> &Lifetimes {
+        self.lifetimes()
+    }
+    fn id(&self) -> TypeId {
+        match self {
+            ReturnableStructPath::Struct(p) => p.tcx_id.into(),
+            ReturnableStructPath::OutStruct(p) => p.tcx_id.into(),
+        }
+    }
+
+    fn link_lifetimes<'def, 'tcx>(
+        &'def self,
+        tcx: &'tcx TypeContext,
+    ) -> LinkedLifetimes<'def, 'tcx> {
+        match self {
+            Self::Struct(p) => p.link_lifetimes(tcx),
+            Self::OutStruct(p) => p.link_lifetimes(tcx),
+        }
+    }
+}
+
+impl TraitIdGetter for TraitPath {
+    fn id(&self) -> TraitId {
+        self.tcx_id
+    }
+}
+
+impl TraitIdGetter for NoTraitPath {
+    fn id(&self) -> TraitId {
+        panic!("Trait path not allowed here, no trait ID valid");
+    }
+}
+
+/// Abstraction over how a type can hold a pointer to an opaque.
+///
+/// This trait is designed as a helper abstraction for the `OpaqueOwnership`
+/// associated type in the [`TyPosition`] trait. As such, only has two implementing
+/// types: [`MaybeOwn`] and [`Borrow`] for the [`OutputOnly`] and [`Everywhere`]
+/// implementations of [`TyPosition`] respectively.
+pub trait OpaqueOwner {
+    /// Return the mutability of this owner
+    fn mutability(&self) -> Option<Mutability>;
+
+    fn is_owned(&self) -> bool;
+
+    /// Return the lifetime of the borrow, if any.
+    fn lifetime(&self) -> Option<MaybeStatic<Lifetime>>;
+}
+
+impl OpaqueOwner for MaybeOwn {
+    fn mutability(&self) -> Option<Mutability> {
+        match self {
+            MaybeOwn::Own => None,
+            MaybeOwn::Borrow(b) => b.mutability(),
+        }
+    }
+
+    fn is_owned(&self) -> bool {
+        match self {
+            MaybeOwn::Own => true,
+            MaybeOwn::Borrow(_) => false,
+        }
+    }
+
+    fn lifetime(&self) -> Option<MaybeStatic<Lifetime>> {
+        match self {
+            MaybeOwn::Own => None,
+            MaybeOwn::Borrow(b) => b.lifetime(),
+        }
+    }
+}
+
+impl OpaqueOwner for Borrow {
+    fn mutability(&self) -> Option<Mutability> {
+        Some(self.mutability)
+    }
+
+    fn is_owned(&self) -> bool {
+        false
+    }
+
+    fn lifetime(&self) -> Option<MaybeStatic<Lifetime>> {
+        Some(self.lifetime)
+    }
+}
diff --git a/crates/diplomat_core/src/hir/type_context.rs b/crates/diplomat_core/src/hir/type_context.rs
new file mode 100644
index 0000000..7472617
--- /dev/null
+++ b/crates/diplomat_core/src/hir/type_context.rs
@@ -0,0 +1,931 @@
+//! Store all the types contained in the HIR.
+
+use super::lowering::{ErrorAndContext, ErrorStore, ItemAndInfo};
+use super::ty_position::StructPathLike;
+use super::{
+    AttributeValidator, Attrs, EnumDef, LoweringContext, LoweringError, MaybeStatic, OpaqueDef,
+    OutStructDef, StructDef, TraitDef, TypeDef,
+};
+use crate::ast::attrs::AttrInheritContext;
+#[allow(unused_imports)] // use in docs links
+use crate::hir;
+use crate::{ast, Env};
+use core::fmt::{self, Display};
+use smallvec::SmallVec;
+use std::borrow::Cow;
+use std::collections::HashMap;
+use std::ops::Index;
+
+/// A context type owning all types exposed to Diplomat.
+#[derive(Debug)]
+pub struct TypeContext {
+    out_structs: Vec<OutStructDef>,
+    structs: Vec<StructDef>,
+    opaques: Vec<OpaqueDef>,
+    enums: Vec<EnumDef>,
+    traits: Vec<TraitDef>,
+}
+
+/// Key used to index into a [`TypeContext`] representing a struct.
+#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
+pub struct StructId(usize);
+
+/// Key used to index into a [`TypeContext`] representing an out struct.
+#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
+pub struct OutStructId(usize);
+
+/// Key used to index into a [`TypeContext`] representing a opaque.
+#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
+pub struct OpaqueId(usize);
+
+/// Key used to index into a [`TypeContext`] representing an enum.
+#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
+pub struct EnumId(usize);
+
+/// Key used to index into a [`TypeContext`] representing a trait.
+#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
+pub struct TraitId(usize);
+
+#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
+#[non_exhaustive]
+pub enum TypeId {
+    Struct(StructId),
+    OutStruct(OutStructId),
+    Opaque(OpaqueId),
+    Enum(EnumId),
+}
+
+#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
+#[non_exhaustive]
+pub enum SymbolId {
+    TypeId(TypeId),
+    TraitId(TraitId),
+}
+
+enum Param<'a> {
+    Input(&'a str),
+    Return,
+}
+
+impl Display for Param<'_> {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        if let Param::Input(s) = *self {
+            write!(f, "param {s}")
+        } else {
+            write!(f, "return type")
+        }
+    }
+}
+
+impl TypeContext {
+    pub fn all_types<'tcx>(&'tcx self) -> impl Iterator<Item = (TypeId, TypeDef<'tcx>)> {
+        self.structs
+            .iter()
+            .enumerate()
+            .map(|(i, ty)| (TypeId::Struct(StructId(i)), TypeDef::Struct(ty)))
+            .chain(
+                self.out_structs
+                    .iter()
+                    .enumerate()
+                    .map(|(i, ty)| (TypeId::OutStruct(OutStructId(i)), TypeDef::OutStruct(ty))),
+            )
+            .chain(
+                self.opaques
+                    .iter()
+                    .enumerate()
+                    .map(|(i, ty)| (TypeId::Opaque(OpaqueId(i)), TypeDef::Opaque(ty))),
+            )
+            .chain(
+                self.enums
+                    .iter()
+                    .enumerate()
+                    .map(|(i, ty)| (TypeId::Enum(EnumId(i)), TypeDef::Enum(ty))),
+            )
+    }
+
+    pub fn all_traits<'tcx>(&'tcx self) -> impl Iterator<Item = (TraitId, &TraitDef)> {
+        self.traits
+            .iter()
+            .enumerate()
+            .map(|(i, trt)| (TraitId(i), trt))
+    }
+
+    pub fn out_structs(&self) -> &[OutStructDef] {
+        &self.out_structs
+    }
+
+    pub fn structs(&self) -> &[StructDef] {
+        &self.structs
+    }
+
+    pub fn opaques(&self) -> &[OpaqueDef] {
+        &self.opaques
+    }
+
+    pub fn enums(&self) -> &[EnumDef] {
+        &self.enums
+    }
+
+    pub fn traits(&self) -> &[TraitDef] {
+        &self.traits
+    }
+
+    pub fn resolve_type<'tcx>(&'tcx self, id: TypeId) -> TypeDef<'tcx> {
+        match id {
+            TypeId::Struct(i) => TypeDef::Struct(self.resolve_struct(i)),
+            TypeId::OutStruct(i) => TypeDef::OutStruct(self.resolve_out_struct(i)),
+            TypeId::Opaque(i) => TypeDef::Opaque(self.resolve_opaque(i)),
+            TypeId::Enum(i) => TypeDef::Enum(self.resolve_enum(i)),
+        }
+    }
+
+    /// Helper methods for resolving different IDs.
+    ///
+    /// Prefer using `resolve_type()` for simplicity.
+    pub fn resolve_out_struct(&self, id: OutStructId) -> &OutStructDef {
+        self.out_structs.index(id.0)
+    }
+
+    /// Helper methods for resolving different IDs.
+    ///
+    /// Prefer using `resolve_type()` for simplicity.
+    pub fn resolve_struct(&self, id: StructId) -> &StructDef {
+        self.structs.index(id.0)
+    }
+
+    /// Helper methods for resolving different IDs.
+    ///
+    /// Prefer using `resolve_type()` for simplicity.
+    pub fn resolve_opaque(&self, id: OpaqueId) -> &OpaqueDef {
+        self.opaques.index(id.0)
+    }
+
+    /// Helper methods for resolving different IDs.
+    ///
+    /// Prefer using `resolve_type()` for simplicity.
+    pub fn resolve_enum(&self, id: EnumId) -> &EnumDef {
+        self.enums.index(id.0)
+    }
+
+    pub fn resolve_trait(&self, id: TraitId) -> &TraitDef {
+        self.traits.index(id.0)
+    }
+
+    /// Resolve and format a named type for use in diagnostics
+    /// (don't apply rename rules and such)
+    pub fn fmt_type_name_diagnostics(&self, id: TypeId) -> Cow<str> {
+        self.resolve_type(id).name().as_str().into()
+    }
+
+    pub fn fmt_symbol_name_diagnostics(&self, id: SymbolId) -> Cow<str> {
+        match id {
+            SymbolId::TypeId(id) => self.fmt_type_name_diagnostics(id),
+            SymbolId::TraitId(id) => self.resolve_trait(id).name.as_str().into(),
+        }
+    }
+
+    /// Lower the AST to the HIR while simultaneously performing validation.
+    pub fn from_syn<'ast>(
+        s: &'ast syn::File,
+        attr_validator: impl AttributeValidator + 'static,
+    ) -> Result<Self, Vec<ErrorAndContext>> {
+        let types = ast::File::from(s).all_types();
+        let (mut ctx, hir) = Self::from_ast_without_validation(&types, attr_validator)?;
+        ctx.errors.set_item("(validation)");
+        hir.validate(&mut ctx.errors);
+        if !ctx.errors.is_empty() {
+            return Err(ctx.errors.take_errors());
+        }
+        Ok(hir)
+    }
+
+    /// Lower the AST to the HIR, without validation. For testing
+    pub(super) fn from_ast_without_validation<'ast>(
+        env: &'ast Env,
+        attr_validator: impl AttributeValidator + 'static,
+    ) -> Result<(LoweringContext, Self), Vec<ErrorAndContext>> {
+        let mut ast_out_structs = SmallVec::<[_; 16]>::new();
+        let mut ast_structs = SmallVec::<[_; 16]>::new();
+        let mut ast_opaques = SmallVec::<[_; 16]>::new();
+        let mut ast_enums = SmallVec::<[_; 16]>::new();
+        let mut ast_traits = SmallVec::<[_; 16]>::new();
+
+        let mut errors = ErrorStore::default();
+
+        for (path, mod_env) in env.iter_modules() {
+            errors.set_item(
+                path.elements
+                    .last()
+                    .map(|m| m.as_str())
+                    .unwrap_or("root module"),
+            );
+            let mod_attrs = Attrs::from_ast(
+                &mod_env.attrs,
+                &attr_validator,
+                &Default::default(),
+                &mut errors,
+            );
+            let ty_attrs = mod_attrs.for_inheritance(AttrInheritContext::Type);
+            let method_attrs =
+                mod_attrs.for_inheritance(AttrInheritContext::MethodOrImplFromModule);
+
+            for sym in mod_env.items() {
+                match sym {
+                    ast::ModSymbol::CustomType(custom_type) => match custom_type {
+                        ast::CustomType::Struct(strct) => {
+                            let id = if strct.output_only {
+                                TypeId::OutStruct(OutStructId(ast_out_structs.len()))
+                            } else {
+                                TypeId::Struct(StructId(ast_structs.len()))
+                            };
+                            let item = ItemAndInfo {
+                                item: strct,
+                                in_path: path,
+                                ty_parent_attrs: ty_attrs.clone(),
+                                method_parent_attrs: method_attrs.clone(),
+                                id: id.into(),
+                            };
+                            if strct.output_only {
+                                ast_out_structs.push(item);
+                            } else {
+                                ast_structs.push(item);
+                            }
+                        }
+                        ast::CustomType::Opaque(opaque) => {
+                            let item = ItemAndInfo {
+                                item: opaque,
+                                in_path: path,
+                                ty_parent_attrs: ty_attrs.clone(),
+                                method_parent_attrs: method_attrs.clone(),
+                                id: TypeId::Opaque(OpaqueId(ast_opaques.len())).into(),
+                            };
+                            ast_opaques.push(item)
+                        }
+                        ast::CustomType::Enum(enm) => {
+                            let item = ItemAndInfo {
+                                item: enm,
+                                in_path: path,
+                                ty_parent_attrs: ty_attrs.clone(),
+                                method_parent_attrs: method_attrs.clone(),
+                                id: TypeId::Enum(EnumId(ast_enums.len())).into(),
+                            };
+                            ast_enums.push(item)
+                        }
+                    },
+                    ast::ModSymbol::Trait(trt) => {
+                        let item = ItemAndInfo {
+                            item: trt,
+                            in_path: path,
+                            ty_parent_attrs: ty_attrs.clone(),
+                            method_parent_attrs: method_attrs.clone(),
+                            id: TraitId(ast_traits.len()).into(),
+                        };
+                        ast_traits.push(item)
+                    }
+                    _ => {}
+                }
+            }
+        }
+
+        let lookup_id = LookupId::new(
+            &ast_out_structs[..],
+            &ast_structs[..],
+            &ast_opaques[..],
+            &ast_enums[..],
+            &ast_traits[..],
+        );
+        let attr_validator = Box::new(attr_validator);
+
+        let mut ctx = LoweringContext {
+            lookup_id,
+            env,
+            errors,
+            attr_validator,
+        };
+
+        let out_structs = ctx.lower_all_out_structs(ast_out_structs.into_iter());
+        let structs = ctx.lower_all_structs(ast_structs.into_iter());
+        let opaques = ctx.lower_all_opaques(ast_opaques.into_iter());
+        let enums = ctx.lower_all_enums(ast_enums.into_iter());
+        let traits = ctx.lower_all_traits(ast_traits.into_iter()).unwrap();
+
+        match (out_structs, structs, opaques, enums) {
+            (Ok(out_structs), Ok(structs), Ok(opaques), Ok(enums)) => {
+                let res = Self {
+                    out_structs,
+                    structs,
+                    opaques,
+                    enums,
+                    traits,
+                };
+
+                if !ctx.errors.is_empty() {
+                    return Err(ctx.errors.take_errors());
+                }
+                Ok((ctx, res))
+            }
+            _ => {
+                assert!(
+                    !ctx.errors.is_empty(),
+                    "Lowering failed without error messages"
+                );
+                Err(ctx.errors.take_errors())
+            }
+        }
+    }
+
+    /// Run validation phase
+    ///
+    /// Currently validates that methods are not inheriting any transitive bounds from parameters
+    ///    Todo: Automatically insert these bounds during HIR construction in a second phase
+    fn validate<'hir>(&'hir self, errors: &mut ErrorStore<'hir>) {
+        // Lifetime validity check
+        for (_id, ty) in self.all_types() {
+            errors.set_item(ty.name().as_str());
+            for method in ty.methods() {
+                errors.set_subitem(method.name.as_str());
+
+                // This check must occur before validate_ty_in_method is called
+                // since validate_ty_in_method calls link_lifetimes which does not
+                // work for structs with elision
+                let mut failed = false;
+                method.output.with_contained_types(|out_ty| {
+                    for lt in out_ty.lifetimes() {
+                        if let MaybeStatic::NonStatic(lt) = lt {
+                            if method.lifetime_env.get_bounds(lt).is_none() {
+                                errors.push(LoweringError::Other(
+                                    "Found elided lifetime in return type, please explicitly specify".into(),
+                                ));
+
+                                failed = true;
+                                break;
+                            }
+                        }
+                    }
+                });
+
+                if failed {
+                    // link_lifetimes will fail if elision exists
+                    continue;
+                }
+
+                for param in &method.params {
+                    self.validate_ty_in_method(
+                        errors,
+                        Param::Input(param.name.as_str()),
+                        &param.ty,
+                        method,
+                    )
+                }
+
+                method.output.with_contained_types(|out_ty| {
+                    self.validate_ty_in_method(errors, Param::Return, out_ty, method);
+                })
+            }
+        }
+    }
+
+    /// Ensure that a given method's input our output type does not implicitly introduce bounds that are not
+    /// already specified on the method
+    fn validate_ty_in_method<P: hir::TyPosition>(
+        &self,
+        errors: &mut ErrorStore,
+        param: Param,
+        param_ty: &hir::Type<P>,
+        method: &hir::Method,
+    ) {
+        let linked = match &param_ty {
+            hir::Type::Opaque(p) => p.link_lifetimes(self),
+            hir::Type::Struct(p) => p.link_lifetimes(self),
+            _ => return,
+        };
+
+        for (use_lt, def_lt) in linked.lifetimes_all() {
+            let MaybeStatic::NonStatic(use_lt) = use_lt else {
+                continue;
+            };
+            let Some(use_bounds) = &method.lifetime_env.get_bounds(use_lt) else {
+                continue;
+            };
+            let use_longer_lifetimes = &use_bounds.longer;
+            let anchor;
+            let def_longer_lifetimes = if let Some(def_lt) = def_lt {
+                let Some(def_bounds) = &linked.def_env().get_bounds(def_lt) else {
+                    continue;
+                };
+                &def_bounds.longer
+            } else {
+                anchor = linked.def_env().all_lifetimes().collect();
+                &anchor
+            };
+
+            for def_longer in def_longer_lifetimes {
+                let MaybeStatic::NonStatic(corresponding_use) = linked.def_to_use(*def_longer)
+                else {
+                    continue;
+                };
+
+                // In the case of stuff like <'a, 'a> passed to Foo<'x, 'y: 'x> the bound
+                // is trivially fulfilled
+                if corresponding_use == use_lt {
+                    continue;
+                }
+
+                if !use_longer_lifetimes.contains(&corresponding_use) {
+                    let use_name = method.lifetime_env.fmt_lifetime(use_lt);
+                    let use_longer_name = method.lifetime_env.fmt_lifetime(corresponding_use);
+                    let def_cause = if let Some(def_lt) = def_lt {
+                        let def_name = linked.def_env().fmt_lifetime(def_lt);
+                        let def_longer_name = linked.def_env().fmt_lifetime(def_longer);
+                        format!("comes from source type's '{def_longer_name}: '{def_name}")
+                    } else {
+                        // This case is technically already handled in the lifetime lowerer, we're being careful
+                        "comes from &-ref's lifetime in parameter".into()
+                    };
+                    errors.push(LoweringError::Other(format!("Method should explicitly include this \
+                                        lifetime bound from {param}: '{use_longer_name}: '{use_name} ({def_cause})")))
+                }
+            }
+        }
+    }
+}
+
+/// Struct that just wraps the mapping from AST custom types to their IDs that
+/// will show up in the final [`TypeContext`].
+///
+/// The entire point of this type is to reduce the number of arguments in helper
+/// functions which need to look up IDs for structs. It does nothing fancy and
+/// is only ever used when constructing a [`TypeContext`].
+pub(super) struct LookupId<'ast> {
+    out_struct_map: HashMap<&'ast ast::Struct, OutStructId>,
+    struct_map: HashMap<&'ast ast::Struct, StructId>,
+    opaque_map: HashMap<&'ast ast::OpaqueStruct, OpaqueId>,
+    enum_map: HashMap<&'ast ast::Enum, EnumId>,
+    trait_map: HashMap<&'ast ast::Trait, TraitId>,
+}
+
+impl<'ast> LookupId<'ast> {
+    /// Returns a new [`LookupId`].
+    fn new(
+        out_structs: &[ItemAndInfo<'ast, ast::Struct>],
+        structs: &[ItemAndInfo<'ast, ast::Struct>],
+        opaques: &[ItemAndInfo<'ast, ast::OpaqueStruct>],
+        enums: &[ItemAndInfo<'ast, ast::Enum>],
+        traits: &[ItemAndInfo<'ast, ast::Trait>],
+    ) -> Self {
+        Self {
+            out_struct_map: out_structs
+                .iter()
+                .enumerate()
+                .map(|(index, item)| (item.item, OutStructId(index)))
+                .collect(),
+            struct_map: structs
+                .iter()
+                .enumerate()
+                .map(|(index, item)| (item.item, StructId(index)))
+                .collect(),
+            opaque_map: opaques
+                .iter()
+                .enumerate()
+                .map(|(index, item)| (item.item, OpaqueId(index)))
+                .collect(),
+            enum_map: enums
+                .iter()
+                .enumerate()
+                .map(|(index, item)| (item.item, EnumId(index)))
+                .collect(),
+            trait_map: traits
+                .iter()
+                .enumerate()
+                .map(|(index, item)| (item.item, TraitId(index)))
+                .collect(),
+        }
+    }
+
+    pub(super) fn resolve_out_struct(&self, strct: &ast::Struct) -> Option<OutStructId> {
+        self.out_struct_map.get(strct).copied()
+    }
+
+    pub(super) fn resolve_struct(&self, strct: &ast::Struct) -> Option<StructId> {
+        self.struct_map.get(strct).copied()
+    }
+
+    pub(super) fn resolve_opaque(&self, opaque: &ast::OpaqueStruct) -> Option<OpaqueId> {
+        self.opaque_map.get(opaque).copied()
+    }
+
+    pub(super) fn resolve_enum(&self, enm: &ast::Enum) -> Option<EnumId> {
+        self.enum_map.get(enm).copied()
+    }
+
+    pub(super) fn resolve_trait(&self, trt: &ast::Trait) -> Option<TraitId> {
+        self.trait_map.get(trt).copied()
+    }
+}
+
+impl From<StructId> for TypeId {
+    fn from(x: StructId) -> Self {
+        TypeId::Struct(x)
+    }
+}
+
+impl From<OutStructId> for TypeId {
+    fn from(x: OutStructId) -> Self {
+        TypeId::OutStruct(x)
+    }
+}
+
+impl From<OpaqueId> for TypeId {
+    fn from(x: OpaqueId) -> Self {
+        TypeId::Opaque(x)
+    }
+}
+
+impl From<EnumId> for TypeId {
+    fn from(x: EnumId) -> Self {
+        TypeId::Enum(x)
+    }
+}
+
+impl From<TypeId> for SymbolId {
+    fn from(x: TypeId) -> Self {
+        SymbolId::TypeId(x)
+    }
+}
+
+impl From<TraitId> for SymbolId {
+    fn from(x: TraitId) -> Self {
+        SymbolId::TraitId(x)
+    }
+}
+
+impl TryInto<TypeId> for SymbolId {
+    type Error = ();
+    fn try_into(self) -> Result<TypeId, Self::Error> {
+        match self {
+            SymbolId::TypeId(id) => Ok(id),
+            _ => Err(()),
+        }
+    }
+}
+
+impl TryInto<TraitId> for SymbolId {
+    type Error = ();
+    fn try_into(self) -> Result<TraitId, Self::Error> {
+        match self {
+            SymbolId::TraitId(id) => Ok(id),
+            _ => Err(()),
+        }
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use crate::hir;
+    use std::fmt::Write;
+
+    macro_rules! uitest_lowering {
+        ($($file:tt)*) => {
+            let parsed: syn::File = syn::parse_quote! { $($file)* };
+
+            let mut output = String::new();
+
+            let mut attr_validator = hir::BasicAttributeValidator::new("tests");
+            attr_validator.support.option = true;
+            match hir::TypeContext::from_syn(&parsed, attr_validator) {
+                Ok(_context) => (),
+                Err(e) => {
+                    for (ctx, err) in e {
+                        writeln!(&mut output, "Lowering error in {ctx}: {err}").unwrap();
+                    }
+                }
+            };
+            insta::with_settings!({}, {
+                insta::assert_snapshot!(output)
+            });
+        }
+    }
+
+    #[test]
+    fn test_required_implied_bounds() {
+        uitest_lowering! {
+            #[diplomat::bridge]
+            mod ffi {
+                #[diplomat::opaque]
+                struct Foo<'a, 'b: 'a, 'c: 'b> (&'a u8, &'b u8, &'c u8);
+
+                #[diplomat::opaque]
+                struct Opaque;
+
+
+                #[diplomat::opaque]
+                struct OneLifetime<'a>(&'a u8);
+
+                impl Opaque {
+                    pub fn use_foo<'x, 'y, 'z>(&self, foo: &Foo<'x, 'y, 'z>) {}
+                    pub fn return_foo<'x, 'y, 'z>(&'x self) -> Box<Foo<'x, 'y, 'z>> {}
+                    pub fn return_result_foo<'x, 'y, 'z>(&'x self) -> Result<Box<Foo<'x, 'y, 'z>>, ()> {}
+                    // This doesn't actually error since the lowerer inserts the implicit bound
+                    pub fn implied_ref_bound<'a, 'b>(&self, one_lt: &'a OneLifetime<'b>) {}
+                }
+            }
+        }
+    }
+
+    /// This is a buch of tests put together
+    #[test]
+    fn test_basic_lowering() {
+        uitest_lowering! {
+            #[diplomat::bridge]
+            mod other_ffi {
+
+                struct Foo {
+                    field: u8
+                }
+
+                #[diplomat::out]
+                struct OutStruct {
+                    field: Box<OtherOpaque>,
+                }
+
+                #[diplomat::opaque]
+                struct OtherOpaque;
+            }
+            #[diplomat::bridge]
+            mod ffi {
+                use crate::other_ffi::{Foo, OutStruct, OtherOpaque};
+
+                #[diplomat::opaque]
+                struct Opaque;
+
+                struct EmptyStruct;
+
+                enum EmptyEnum {}
+
+                struct InStructWithOutField {
+                    field: Box<OtherOpaque>,
+                    out_struct: OutStruct,
+                }
+
+                struct BadStructFields {
+                    field1: Option<u8>,
+                    field2: Result<u8, u8>,
+                }
+
+                impl Opaque {
+                    pub fn use_foo_ref(&self, foo: &Foo) {}
+                    pub fn return_foo_box(&self) -> Box<Foo> {}
+                    pub fn use_self(self) {}
+                    pub fn return_self(self) -> Self {}
+                    pub fn use_opaque_owned(&self, opaque: OtherOpaque) {}
+                    pub fn return_opaque_owned(&self) -> OtherOpaque {}
+                    pub fn use_out_as_in(&self, out: OutStruct) {}
+                }
+
+            }
+        }
+    }
+
+    #[test]
+    fn test_opaque_ffi() {
+        uitest_lowering! {
+            #[diplomat::bridge]
+            mod ffi {
+                #[diplomat::opaque]
+                struct MyOpaqueStruct(UnknownType);
+
+                impl MyOpaqueStruct {
+                    pub fn new() -> Box<MyOpaqueStruct> {}
+                    pub fn new_broken() -> MyOpaqueStruct {}
+                    pub fn do_thing(&self) {}
+                    pub fn do_thing_broken(self) {}
+                    pub fn broken_differently(&self, x: &MyOpaqueStruct) {}
+                }
+            }
+        }
+    }
+
+    #[test]
+    fn opaque_checks_with_safe_use() {
+        uitest_lowering! {
+            #[diplomat::bridge]
+            mod ffi {
+                struct NonOpaqueStruct {}
+
+                impl NonOpaqueStruct {
+                    pub fn new(x: i32) -> NonOpaqueStruct {
+                        unimplemented!();
+                    }
+                }
+
+                #[diplomat::opaque]
+                struct OpaqueStruct {}
+
+                impl OpaqueStruct {
+                    pub fn new() -> Box<OpaqueStruct> {
+                        unimplemented!();
+                    }
+
+                    pub fn get_i32(&self) -> i32 {
+                        unimplemented!()
+                    }
+                }
+            }
+        };
+    }
+
+    #[test]
+    fn opaque_checks_with_error() {
+        uitest_lowering! {
+            #[diplomat::bridge]
+            mod ffi {
+                #[diplomat::opaque]
+                struct OpaqueStruct {}
+
+                impl OpaqueStruct {
+                    pub fn new() -> OpaqueStruct {
+                        unimplemented!();
+                    }
+
+                    pub fn get_i32(self) -> i32 {
+                        unimplemented!()
+                    }
+                }
+            }
+        };
+    }
+
+    #[test]
+    fn zst_non_opaque() {
+        uitest_lowering! {
+            #[diplomat::bridge]
+            mod ffi {
+                struct NonOpaque;
+
+                impl NonOpaque {
+                    pub fn foo(self) {}
+                }
+            }
+        };
+    }
+
+    #[test]
+    fn option_invalid() {
+        uitest_lowering! {
+            #[diplomat::bridge]
+            mod ffi {
+                use diplomat_runtime::DiplomatResult;
+                struct Foo {
+                    field: Option<u8>,
+                }
+
+                impl Foo {
+                    pub fn do_thing(opt: Option<Option<u16>>) {
+
+                    }
+
+                    pub fn do_thing2(opt: DiplomatResult<Option<DiplomatChar>, u8>) {
+
+                    }
+                    pub fn do_thing2(opt: Option<u16>) {
+
+                    }
+
+                    pub fn do_thing3() -> Option<u16> {
+
+                    }
+                }
+            }
+        };
+    }
+
+    #[test]
+    fn option_valid() {
+        uitest_lowering! {
+            #[diplomat::bridge]
+            mod ffi {
+                struct Foo {
+                    field: Option<Box<u8>>,
+                }
+
+                impl Foo {
+                    pub fn do_thing(opt: Option<Box<u32>>) {
+
+                    }
+                    pub fn do_thing2(opt: Option<&u32>) {
+
+                    }
+                }
+            }
+        };
+    }
+
+    #[test]
+    fn non_opaque_move() {
+        uitest_lowering! {
+            #[diplomat::bridge]
+            mod ffi {
+                struct NonOpaque {
+                    num: u8,
+                }
+
+                impl NonOpaque {
+                    pub fn foo(&self) {}
+                }
+
+                #[diplomat::opaque]
+                struct Opaque;
+
+                impl Opaque {
+                    pub fn bar<'a>(&'a self) -> &'a NonOpaque {}
+                    pub fn baz<'a>(&'a self, x: &'a NonOpaque) {}
+                    pub fn quux(&self) -> Box<NonOpaque> {}
+                }
+            }
+        };
+    }
+
+    #[test]
+    fn test_lifetime_in_return() {
+        uitest_lowering! {
+            #[diplomat::bridge]
+            mod ffi {
+                #[diplomat::opaque]
+                struct Opaque;
+
+                struct Foo<'a> {
+                    x: &'a Opaque,
+                }
+
+                impl Opaque {
+                    pub fn returns_self(&self) -> &Self {}
+                    pub fn returns_foo(&self) -> Foo {}
+                }
+            }
+        };
+    }
+    #[test]
+    fn test_struct_forbidden() {
+        uitest_lowering! {
+            #[diplomat::bridge]
+            mod ffi {
+                struct Crimes<'a> {
+                    slice1: &'a str,
+                    slice1: &'a DiplomatStr,
+                    slice2: &'a [u8],
+                    slice3: Box<str>,
+                    slice3: Box<DiplomatStr>,
+                    slice4: Box<[u8]>,
+                }
+            }
+        };
+    }
+
+    #[test]
+    fn test_option() {
+        uitest_lowering! {
+            #[diplomat::bridge]
+                mod ffi {
+                use diplomat_runtime::DiplomatOption;
+                #[diplomat::opaque]
+                struct Foo {}
+                struct CustomStruct {
+                    num: u8,
+                    b: bool,
+                    diplo_option: DiplomatOption<u8>,
+                }
+
+                struct BrokenStruct {
+                    regular_option: Option<u8>,
+                    regular_option: Option<CustomStruct>,
+                }
+                impl Foo {
+                    pub fn diplo_option_u8(x: DiplomatOption<u8>) -> DiplomatOption<u8> {
+                        x
+                    }
+                    pub fn diplo_option_ref(x: DiplomatOption<&Foo>) -> DiplomatOption<&Foo> {
+                        x
+                    }
+                    pub fn diplo_option_box() -> DiplomatOption<Box<Foo>> {
+                        x
+                    }
+                    pub fn diplo_option_struct(x: DiplomatOption<CustomStruct>) -> DiplomatOption<CustomStruct> {
+                        x
+                    }
+                    pub fn option_u8(x: Option<u8>) -> Option<u8> {
+                        x
+                    }
+                    pub fn option_ref(x: Option<&Foo>) -> Option<&Foo> {
+                        x
+                    }
+                    pub fn option_box() -> Option<Box<Foo>> {
+                        x
+                    }
+                    pub fn option_struct(x: Option<CustomStruct>) -> Option<CustomStruct> {
+                        x
+                    }
+                }
+            }
+        };
+    }
+}
diff --git a/crates/diplomat_core/src/hir/types.rs b/crates/diplomat_core/src/hir/types.rs
new file mode 100644
index 0000000..c8c01ef
--- /dev/null
+++ b/crates/diplomat_core/src/hir/types.rs
@@ -0,0 +1,209 @@
+//! Types that can be exposed in Diplomat APIs.
+
+use super::lifetimes::{Lifetime, MaybeStatic};
+use super::{
+    EnumPath, Everywhere, NonOptional, OpaqueOwner, OpaquePath, Optional, OutputOnly,
+    PrimitiveType, StructPath, StructPathLike, TyPosition, TypeContext, TypeId,
+};
+use crate::ast;
+pub use ast::Mutability;
+pub use ast::StringEncoding;
+use either::Either;
+
+/// Type that can only be used as an output.
+pub type OutType = Type<OutputOnly>;
+
+/// Type that may be used as input or output.
+#[derive(Debug, Clone)]
+#[non_exhaustive]
+pub enum Type<P: TyPosition = Everywhere> {
+    Primitive(PrimitiveType),
+    Opaque(OpaquePath<Optional, P::OpaqueOwnership>),
+    Struct(P::StructPath),
+    ImplTrait(P::TraitPath),
+    Enum(EnumPath),
+    Slice(Slice),
+    Callback(P::CallbackInstantiation), // only a Callback if P == InputOnly
+    /// `DiplomatOption<T>`, for  a primitive, struct, or enum `T`.
+    ///
+    /// In some cases this can be specified as `Option<T>`, but under the hood it gets translated to
+    /// the DiplomatOption FFI-compatible union type.
+    ///
+    /// This is *different* from `Option<&T>` and `Option<Box<T>` for an opaque `T`: That is represented as
+    /// a nullable pointer.
+    ///
+    /// This does not get used when the user writes `-> Option<T>` (for non-opaque T):
+    /// that will always use [`ReturnType::Nullable`](crate::hir::ReturnType::Nullable).
+    DiplomatOption(Box<Type<P>>),
+}
+
+/// Type that can appear in the `self` position.
+#[derive(Debug, Clone)]
+#[non_exhaustive]
+pub enum SelfType {
+    Opaque(OpaquePath<NonOptional, Borrow>),
+    Struct(StructPath),
+    Enum(EnumPath),
+}
+
+#[derive(Copy, Clone, Debug)]
+#[non_exhaustive]
+pub enum Slice {
+    /// A string slice, e.g. `&DiplomatStr` or `Box<DiplomatStr>`.
+    ///
+    /// Owned slices are useful for garbage-collected languages that have to
+    /// reallocate into non-gc memory anyway. For example for Dart it's more
+    /// efficient to accept `Box<str>` than to accept `&str` and then
+    /// allocate in Rust, as Dart will have to create the `Box<str`> to
+    /// pass `&str` anyway.
+    Str(Option<MaybeStatic<Lifetime>>, StringEncoding),
+
+    /// A primitive slice, e.g. `&mut [u8]` or `Box<[usize]>`.
+    ///
+    /// Owned slices are useful for garbage-collected languages that have to
+    /// reallocate into non-gc memory anyway. For example for Dart it's more
+    /// efficient to accept `Box<[bool]>` than to accept `&[bool]` and then
+    /// allocate in Rust, as Dart will have to create the `Box<[bool]`> to
+    /// pass `&[bool]` anyway.
+    Primitive(Option<Borrow>, PrimitiveType),
+
+    /// A `&[&DiplomatStr]]`. This type of slice always needs to be
+    /// allocated before passing it into Rust, as it has to conform to the
+    /// Rust ABI. In other languages this is the idiomatic list of string
+    /// views, i.e. `std::span<std::string_view>` or `core.List<core.String>`.
+    Strs(StringEncoding),
+}
+
+// For now, the lifetime in not optional. This is because when you have references
+// as fields of structs, the lifetime must always be present, and we want to uphold
+// this invariant at the type level within the HIR.
+//
+// The only time when a lifetime is optional in Rust code is in function signatures,
+// where implicit lifetimes are allowed. Getting this to all fit together will
+// involve getting the implicit lifetime thing to be understood by Diplomat, but
+// should be doable.
+#[derive(Copy, Clone, Debug)]
+#[non_exhaustive]
+pub struct Borrow {
+    pub lifetime: MaybeStatic<Lifetime>,
+    pub mutability: Mutability,
+}
+
+// This is implemented on InputOnly and Everywhere types. Could be extended
+// if we genericize .resolve() later.
+impl<P: TyPosition<StructPath = StructPath>> Type<P> {
+    /// Return the number of fields and leaves that will show up in the [`LifetimeTree`]
+    /// returned by [`Param::lifetime_tree`] and [`ParamSelf::lifetime_tree`].
+    ///
+    /// This method is used to calculate how much space to allocate upfront.
+    pub(super) fn field_leaf_lifetime_counts(&self, tcx: &TypeContext) -> (usize, usize) {
+        match self {
+            Type::Struct(ty) => ty.resolve(tcx).fields.iter().fold((1, 0), |acc, field| {
+                let inner = field.ty.field_leaf_lifetime_counts(tcx);
+                (acc.0 + inner.0, acc.1 + inner.1)
+            }),
+            Type::Opaque(_) | Type::Slice(_) | Type::Callback(_) | Type::ImplTrait(_) => (1, 1),
+            Type::Primitive(_) | Type::Enum(_) => (0, 0),
+            Type::DiplomatOption(ty) => ty.field_leaf_lifetime_counts(tcx),
+        }
+    }
+}
+
+impl<P: TyPosition> Type<P> {
+    /// Get all lifetimes "contained" in this type
+    pub fn lifetimes(&self) -> impl Iterator<Item = MaybeStatic<Lifetime>> + '_ {
+        match self {
+            Type::Opaque(opaque) => Either::Right(
+                opaque
+                    .lifetimes
+                    .as_slice()
+                    .iter()
+                    .copied()
+                    .chain(opaque.owner.lifetime()),
+            ),
+            Type::Struct(struct_) => Either::Left(struct_.lifetimes().as_slice().iter().copied()),
+            Type::Slice(slice) => Either::Left(
+                slice
+                    .lifetime()
+                    .map(|lt| std::slice::from_ref(lt).iter().copied())
+                    .unwrap_or([].iter().copied()),
+            ),
+            // TODO the Callback case
+            _ => Either::Left([].iter().copied()),
+        }
+    }
+
+    // For custom types, get the type id
+    pub fn id(&self) -> Option<TypeId> {
+        Some(match self {
+            Self::Opaque(p) => TypeId::Opaque(p.tcx_id),
+            Self::Enum(p) => TypeId::Enum(p.tcx_id),
+            Self::Struct(p) => p.id(),
+            _ => return None,
+        })
+    }
+
+    /// Unwrap to the inner type if `self` is `DiplomatOption`
+    pub fn unwrap_option(&self) -> &Type<P> {
+        match self {
+            Self::DiplomatOption(ref o) => o,
+            _ => self,
+        }
+    }
+
+    /// Whether this type is a `DiplomatOption` or optional Opaque
+    pub fn is_option(&self) -> bool {
+        match self {
+            Self::DiplomatOption(..) => true,
+            Self::Opaque(ref o) if o.is_optional() => true,
+            _ => false,
+        }
+    }
+}
+
+impl SelfType {
+    /// Returns whether the self parameter is borrowed immutably.
+    ///
+    /// Curently this can only happen with opaque types.
+    pub fn is_immutably_borrowed(&self) -> bool {
+        match self {
+            SelfType::Opaque(opaque_path) => opaque_path.owner.mutability == Mutability::Immutable,
+            _ => false,
+        }
+    }
+}
+
+impl Slice {
+    /// Returns the [`Lifetime`] contained in either the `Str` or `Primitive`
+    /// variant.
+    pub fn lifetime(&self) -> Option<&MaybeStatic<Lifetime>> {
+        match self {
+            Slice::Str(lifetime, ..) => lifetime.as_ref(),
+            Slice::Primitive(Some(reference), ..) => Some(&reference.lifetime),
+            Slice::Primitive(..) => None,
+            Slice::Strs(..) => Some({
+                const X: MaybeStatic<Lifetime> = MaybeStatic::NonStatic(Lifetime::new(usize::MAX));
+                &X
+            }),
+        }
+    }
+}
+
+impl Borrow {
+    pub(super) fn new(lifetime: MaybeStatic<Lifetime>, mutability: Mutability) -> Self {
+        Self {
+            lifetime,
+            mutability,
+        }
+    }
+}
+
+impl From<SelfType> for Type {
+    fn from(s: SelfType) -> Type {
+        match s {
+            SelfType::Opaque(o) => Type::Opaque(o.wrap_optional()),
+            SelfType::Struct(s) => Type::Struct(s),
+            SelfType::Enum(e) => Type::Enum(e),
+        }
+    }
+}
diff --git a/crates/diplomat_core/src/lib.rs b/crates/diplomat_core/src/lib.rs
new file mode 100644
index 0000000..8485372
--- /dev/null
+++ b/crates/diplomat_core/src/lib.rs
@@ -0,0 +1,15 @@
+//! The Diplomat core module contains common logic between
+//! the macro expansion and code generation. Right now, this
+//! is primarily the AST types that Diplomat generates while
+//! extracting APIs.
+
+#![allow(clippy::needless_lifetimes)] // we use named lifetimes for clarity
+#![warn(clippy::exhaustive_structs, clippy::exhaustive_enums)]
+
+pub mod ast;
+#[cfg(feature = "hir")]
+pub mod hir;
+
+mod environment;
+
+pub use environment::{Env, ModuleEnv};
diff --git a/pseudo_crate/Cargo.lock b/pseudo_crate/Cargo.lock
index d274e01..e544806 100644
--- a/pseudo_crate/Cargo.lock
+++ b/pseudo_crate/Cargo.lock
@@ -182,6 +182,7 @@
  "der_derive",
  "derive_arbitrary",
  "diplomat-runtime",
+ "diplomat_core",
  "displaydoc",
  "document-features",
  "downcast",
@@ -1705,6 +1706,20 @@
 checksum = "bc1708f176e12755d6d6571ad9b0ebbd3b428223b5cdf63a38eecf1479c13e70"
 
 [[package]]
+name = "diplomat_core"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "58e5ba87fee6b8b9dcc575cfbc84ae97b8b9f891fa27f670996a4684e20bd178"
+dependencies = [
+ "proc-macro2 1.0.93",
+ "quote 1.0.38",
+ "serde",
+ "smallvec",
+ "strck",
+ "syn 2.0.96",
+]
+
+[[package]]
 name = "dirs"
 version = "5.0.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -5075,6 +5090,9 @@
 version = "1.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "42316e70da376f3d113a68d138a60d8a9883c604fe97942721ec2068dab13a9f"
+dependencies = [
+ "unicode-ident",
+]
 
 [[package]]
 name = "strsim"
diff --git a/pseudo_crate/Cargo.toml b/pseudo_crate/Cargo.toml
index 0fea9bd..36ad7fb 100644
--- a/pseudo_crate/Cargo.toml
+++ b/pseudo_crate/Cargo.toml
@@ -91,6 +91,7 @@
 der_derive = "=0.7.3"
 derive_arbitrary = "=1.4.1"
 diplomat-runtime = "=0.9.0"
+diplomat_core = "=0.9.0"
 displaydoc = "=0.2.5"
 document-features = "=0.2.10"
 downcast = "=0.11.0"
diff --git a/pseudo_crate/crate-list.txt b/pseudo_crate/crate-list.txt
index 3594c8d..037d0c6 100644
--- a/pseudo_crate/crate-list.txt
+++ b/pseudo_crate/crate-list.txt
@@ -83,6 +83,7 @@
 der_derive
 derive_arbitrary
 diplomat-runtime
+diplomat_core
 displaydoc
 document-features
 downcast