Skip to main content

Command Palette

Search for a command to run...

TaskLog: IntoOwned for lightningcss

Updated
8 min read

Recently, I created a clever trait that can be beneficial to all users of lightningcss. I don't want to brag about that fact, but I'm writing a post because I think it'll help many people who want to be good at programming. The key to programming is the way you think.


Problem

I want to use lightningcss for one of my projects. But AST nodes are not 'static, so I can't use them when I need 'static lifetime. So... I tried implementing them by recursively copying items.

But I don't want to do this. And it seems like some types have a method like .into_owned() , which can be used to get the lifetime I want.

So, my question was

Why don't all types have such a method?

and it seems like it's because CssRule is defined as

pub enum CssRule<'i, R = DefaultAtRule> {
  /// A `@media` rule.
  #[cfg_attr(feature = "serde", serde(borrow))]
  Media(MediaRule<'i, R>),
  /// An `@import` rule.
  Import(ImportRule<'i>),
  /// A style rule.
  Style(StyleRule<'i, R>),
  /// A `@keyframes` rule.
  Keyframes(KeyframesRule<'i>),
  /// A `@font-face` rule.
  FontFace(FontFaceRule<'i>),
  /// A `@font-palette-values` rule.
  FontPaletteValues(FontPaletteValuesRule<'i>),
  /// A `@page` rule.
  Page(PageRule<'i>),
  /// A `@supports` rule.
  Supports(SupportsRule<'i, R>),
  /// A `@counter-style` rule.
  CounterStyle(CounterStyleRule<'i>),
  /// A `@namespace` rule.
  Namespace(NamespaceRule<'i>),
  /// A `@-moz-document` rule.
  MozDocument(MozDocumentRule<'i, R>),
  /// A `@nest` rule.
  Nesting(NestingRule<'i, R>),
  /// A `@viewport` rule.
  Viewport(ViewportRule<'i>),
  /// A `@custom-media` rule.
  CustomMedia(CustomMediaRule<'i>),
  /// A `@layer` statement rule.
  LayerStatement(LayerStatementRule<'i>),
  /// A `@layer` block rule.
  LayerBlock(LayerBlockRule<'i, R>),
  /// A `@property` rule.
  Property(PropertyRule<'i>),
  /// A `@container` rule.
  Container(ContainerRule<'i, R>),
  /// A `@starting-style` rule.
  StartingStyle(StartingStyleRule<'i, R>),
  /// A placeholder for a rule that was removed.
  Ignored,
  /// An unknown at-rule.
  Unknown(UnknownAtRule<'i>),
  /// A custom at-rule.
  Custom(R),
}

I think R would be the problem. This is a guess, so I need to verify.

  • MediaRule contains R as a generic parameter, and does not have .into_owned() .

  • ImportRule does not have a generic parameter, and have .into_owned().

  • .into_owned() of ImportRule (see CssRule::Import(r.clone().into_owned()) above) is implemented as direct implementation instead of using a trait.

So, I can avoid writing code for a recursive copy if I can make a trait that is implemented for types regardless of generic parameters.

But lightingcss is not my project. I don't have permission to add such a trait. But this trait will benefit everyone! Let's ask the maintainer if they are open to a PR.

IntoOwned as a trait, and implement it for nodes when R implements IntoOwned

Will you accept a PR for this?
If so, which crate should have IntoOwned?

And the response was

I think I originally tried to implement it as a trait and couldn't get it to work but I can't remember why. But if you get it working that sounds good to me. We can put the trait in here https://github.com/parcel-bundler/lightningcss/blob/master/src/traits.rs

Yeah, it makes sense. Lifetime is very tricky if it's mixed with a proc-macro and a trait. But I don't want to write recursive copy code. That's more important, at least to me.

Solutions

Of course, I didn't get to the answer from the start. Instead, I started with something super dumb. But who cares? I did it. And that's what matters. I'll describe my approaches in order. Don't swear at me even if I look too stupid.

#1: Self -> Self

I declared the trait like

/// A trait for things that can be cloned with a new lifetime.
pub trait IntoOwned {
  /// Make lifetime of `self` `'static`.
  fn into_owned(self) -> Self;
}

Umm... This is super stupid, but it's okay if you don't see the problem with this definition. I was the same.

I happily wrote a simple macro to implement this trait for already-owned types.


macro_rules! impl_into_owned {
  ($t: ty) => {
    impl<'i> IntoOwned for $t {
      #[inline]
      fn into_owned(self) -> Self {
        self
      }
    }
  };
  ($($t:ty),*) => {
    $(impl_into_owned!($t);)*
  };
}

impl_into_owned!(bool, f32, f64, u8, u16, u32, u64, u128, i8, i16, i32, i64, i128, usize, isize);

Thanks to GitHub Copilot, this was dead simple. I only typed macro_rul , ($($t and impl_into_owned!(bool, f32 . Actually, this kind of code is extremely common in the Rust world.

Time to patch derive macro to generate impl IntoOwned for Type {} instead of impl Type { } . The original definition was

    quote! {
      impl #impl_generics #self_name #ty_generics #where_clause {
        /// Consumes the value and returns an owned clone.
        pub fn into_owned<'x>(self) -> #self_name<'x, #(#params),*> {
          #res
        }
      }
    }

and umm... My trait is so dumb that it cannot return anything other than Self, which has exactly the same lifetime and generic parameters as self.

#2: Self -> Self<'static>

Okay. I agree that we need to return something other than Self. However, the return type is determined by the shape of the type. So the output should be an associated type of the trait.

So, I declared the trait like this.

pub trait IntoOwned {
  type Owned: 'static;

  /// Make lifetime of `self` `'static`.
  fn into_owned(self) -> Self::Owned;
}

This does not look stupid, at least. So I was satisfied. I patched the derive macro and existing implementations while thinking

This is too easy, considering the comment by the maintainer.

It seems to work, except for exactly one item.

lifetime may not live long enough requirement occurs because of the type properties::Property<'_>, which makes the generic argument '_ invariant the enum properties::Property<'i> is invariant over the parameter 'i see https://doc.rust-lang.org/nomicon/subtyping.html for more information about variance

The lifetime was invariant, so a 'static lifetime was not enough, and rustc wants an exact match. But well... this error came from a code that worked previously.

The previous definition of Property::into_owned(self) should be generated from

pub fn into_owned<'x>(self) -> #self_name<'x, #(#params),*>

so it should be something like into_owned<'x>(self) -> Property<'x, ...> . It worked? Okay... I don't like the decision of rustc, but I decided to mimic the previous code anyway.

#3: Self -> Self<'any>

pub trait IntoOwned<'any> {
  type Owned: 'any;

  fn into_owned(self) -> Self::Owned;
}
  • Previously, 'x in into_owned<'x> is input, so I declared it as a generic lifetime parameter.

  • But I don't like the name 'x. I wanted it to be more explicit, so I chose 'any.

  • The return type can use 'any, just as the previous code could use 'x in the return type.

Direct implementations of such traits are quite trivial, but always the problem is proc-macro. I need to add 'any to various type definitions, and AST work is always error-prone.

But I know the most important thing. I need to use 'any in the derived definition of Owned. So I just wrote there. The previous code was

      impl #impl_generics lightningcss::traits::IntoOwned for #self_name #ty_generics #where_clause {
        type Owned = #self_name<'static, #(#params),*>;
        /// Consumes the value and returns an owned clone.
        fn into_owned(self) -> Self::Owned {
          use lightningcss::traits::IntoOwned;

          #res
        }
      }

so it became

      impl #impl_generics lightningcss::traits::IntoOwned<'any> for #self_name #ty_generics #where_clause {
        type Owned = #self_name<'any, #(#params),*>;
        /// Consumes the value and returns an owned clone.
        fn into_owned(self) -> Self::Owned {
          use lightningcss::traits::IntoOwned;

          #res
        }
      }

Now the problem collapses. I need to add 'any to #impl_generics because rustc complains that 'any is used, but not declared anywhere.

impl_generics is declared as

let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();

so I prepended 'any to generics. Of course, it was broken. it? Many things were broken. But I made progress, so I didn't care. As 'any is added to generics, ty_generics also has something new, which is a bomb for types without generics in the definition.

#[derive(IntoOwned)]
enum Foo {}

resulted in

impl IntoOwned<'any> for Foo<'any>

so it's clearly wrong.

struct Foo<'a>

resulted in

impl IntoOwned<'any> for Foo<'any, 'a>

so it's also clearly wrong. I fixed this one first. I made it replace instead of prepend if a lifetime parameter exists.

But this was also a stupid decision. If you spot the problem, you are a genius. The problem here is that

impl IntoOwned<'any> for Foo<'any> {
    type Owned = Foo<'any>;
}

is generated, which is... identical to my bad approach. I didn't notice this fact. I didn't have confidence from the start, so I thought #3 might be fundamentally wrong like the others, but I was going to try many things at least for 2 hours. And after some triage, I found that I was replacing 'a in for Foo<'a> to 'any, resulting in for Foo<'any>.

After some thinking, I found that

let orig_generics = generics.clone();

// Modify `generics`

let (impl_generics, _, where_clause) = generics.split_for_impl();
let (_, ty_generics, _) = orig_generics.split_for_impl();

is the way to go.


So after fixing small issues, I undrafted the PR, and it's good for review now. Many people, including my friends, ask me about the way to write code like me. I think the key is always the way you think.

The noticeable points in this task were

  • I classified enum variants based on characteristics to verify my guess about the reasoning behind no implementation.
  • When 'static, which should work according to my intuition, didn't work, I tried to find a definition that is a 1-to-1 mapping of something that worked before.
  • I'm sure that 'any is required in the output, so even if adding 'any to some places resulted in lots of errors, I considered it progress.

Also, trial and error is one of the most important things in programming. One of the best points of almost all programming is that you can try.

P.S.

Most of the people who asked me about coding are Koreans, so I don't know how many of them will read this, but I write in English because I think writing in English will help more people.

More from this blog

한국의 학벌에 대한 생각

내 블로그의 제목이 kdy1: The way I think 인만큼 앞으로는 내 생각을 더 자주 올리려고 한다. 한국 기준으로, 학벌은 사람을 볼 때 꽤나 유용한 지표이지만, 절대적이지는 않다. 경험적인 얘기일 뿐이지만, 성균관대학교 자퇴생으로서 느낀 것들이 몇 가지 있다. 대학까지 간 사람의 학벌은 학습 능력 x 성실함 에 대체로 비례한다. 그래서 의미가

Apr 3, 20261 min read

인간 지능에 대한 메모장

최종 업데이트: 2026/03/15 지능의 유전 현재 인류 기준으로, 고지능자는 고지능 유전자가 많이 겹친 사람이다. 지능의 유전엔 X 염색체가 매우 중요한 역할을 한다. 그리고 이게 남자와 여자의 지능 분포 차이를 만든다. 극상위권에 여자가 거의 없는 이유가 이것이다. 고지능 X 염색체가 여자한테서 발현되려면 2개가 있어야 한다. 이는 인간의 생

Mar 15, 20262 min read

Ai 코딩 팁 2 (한국어)

발표 자료: https://gamma.app/docs/AI--2a52e7tk3eb1ch1 AI 활용법 관련해서 간단하게 발표를 했다. 발표 자료 앞쪽은 전에 블로그에 올린 글이랑 같은 내용이다. 이 글에서는 기존 글에서 다루지 않은 내용들을 다루겠다. 에러 메시지 및 로깅 구체적 타입 및 스키마 활용 any 타입은 사람에게도 위험하지만, AI에게는 더 위험하다. 마찬가지로, JSON.parse처럼 아무 제약 없는 파싱 느슨한 인터페이스 ...

Jan 30, 20265 min read

kdy1: The way I think

233 posts