Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Issue with TypeScript Generics Causing Panic in encore run #1682

Open
theGlenn opened this issue Jan 6, 2025 · 2 comments
Open

Issue with TypeScript Generics Causing Panic in encore run #1682

theGlenn opened this issue Jan 6, 2025 · 2 comments

Comments

@theGlenn
Copy link

theGlenn commented Jan 6, 2025

It seems that encore run does not handle TypeScript generics well.

The following code causes a panic when SearchResult has a generic parameter.

Non-Generic Version:

interface SearchResult {
  id: string | number;
  vectors?: number[] | null;
  payload?: any;
}

export const search = api({ method: "POST", path: "/qdrant" },
  async function (args: SeachArgs): Promise<{ results: SearchResult[] }> {
    return { results: [] };
  }
);

Generic Version:

interface SearchResult<T = any> {
  id: string | number;
  vectors?: number[] | null;
  payload?: T;
}

Panic Output:

thread '<unnamed>' panicked at runtimes/core/src/api/schema/encoding.rs:327:46:
index out of bounds: the len is 0 but the index is 0
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
thread '<unnamed>' panicked at core/src/panicking.rs:221:5:
panic in a function that cannot unwind
stack backtrace:
   0:        0x11ccdffc0 - <std::sys::backtrace::BacktraceLock::print::DisplayBacktrace as core::fmt::Display>::fmt::h7adefaad4a31afc0
   1:        0x11c23ddfc - core::fmt::write::h381c0b0ce6ab972a
   2:        0x11ccb52bc - std::io::Write::write_fmt::h75af97148630d8d3
   3:        0x11cce3b04 - std::sys::backtrace::BacktraceLock::print::h8baf33e22611de71
   4:        0x11cce3980 - std::panicking::default_hook::{{closure}}::h369c7295ef58c5b1
   5:        0x11cce4258 - std::panicking::rust_panic_with_hook::h7d795911432661cb
   6:        0x11cce3c3c - std::panicking::begin_panic_handler::{{closure}}::h36f15310ecbde379
   7:        0x11cce3bd4 - std::sys::backtrace::__rust_end_short_backtrace::heed121414170e0c7
   8:        0x11cce3bc8 - _rust_begin_unwind
   9:        0x11c242e3c - core::panicking::panic_nounwind_fmt::h5d86a478a9d7437c
  10:        0x11c242e98 - core::panicking::panic_nounwind::he171ad52d328e60b
  11:        0x11c242e50 - core::panicking::panic_cannot_unwind::h2dd8cd735765e065
  12:        0x11c288b5c - encore_js_runtime::runtime::__napi_impl_helper__Runtime__9::__napi__new::ha23a791975f51618
thread caused non-unwinding panic. aborting.

Steps to Reproduce:

Define an interface such as SearchResult with a generic parameter.
Run the encore run command.

Expected Behavior: The encore run command should handle TypeScript generics without causing a panic.

Actual Behavior: The encore run command causes a panic when SearchResult has a generic parameter.

Additional Context:

Encore.dev version: v1.45.2
Node.js version: v21.7.3
OS: Apple M1 Pro - Sonoma 14.6.1

@Mina-Sayed
Copy link

Suggested Fix:

  • To handle TypeScript generics properly, we need to modify the function causing the panic.

Suggested Code Changes

fn resolve_type<'a>(meta: &'a meta::Data, typ: &'a Typ) -> anyhow::Result<Cow<'a, Typ>> {
    let resolver = TypeArgResolver {
        meta,
        resolved_args: vec![],
        decls: vec![],
    };
    resolver.resolve(typ)
}

struct TypeArgResolver<'a> {
    meta: &'a meta::Data,
    resolved_args: Vec<Cow<'a, Typ>>,
    decls: Vec<u32>,
}

impl<'a> TypeArgResolver<'a> {
    fn resolve(&self, typ: &'a Typ) -> anyhow::Result<Cow<'a, Typ>> {
        match typ {
            Typ::Named(named) => {
                let decl = &self.meta.decls[named.id as usize];
                if self.decls.contains(&decl.id) {
                    return Ok(Cow::Borrowed(typ));
                }
                let args = self.resolve_types(&named.type_arguments)?;
                let nested = TypeArgResolver {
                    meta: self.meta,
                    resolved_args: args,
                    decls: {
                        let mut decls = self.decls.clone();
                        decls.push(decl.id);
                        decls
                    },
                };
                let typ = decl.r#type.as_ref().context("decl without type")?;
                let typ = typ.typ.as_ref().context("type without type")?;
                nested.resolve(typ)
            }
            Typ::Struct(strukt) => {
                let mut cows = Vec::with_capacity(strukt.fields.len());
                for field in &strukt.fields {
                    let t = field.typ.as_ref().context("field without type")?;
                    let typ = t.typ.as_ref().context("type without type")?;
                    let resolved = self.resolve(typ)?;
                    cows.push((resolved, t.validation.as_ref()));
                }
                let mut fields = Vec::with_capacity(strukt.fields.len());
                for (field, (typ, v)) in strukt.fields.iter().zip(cows) {
                    fields.push(schema::Field {
                        typ: Some(schema::Type {
                            typ: Some(typ.into_owned()),
                            validation: v.cloned(),
                        }),
                        ..field.clone()
                    });
                }
                Ok(Cow::Owned(Typ::Struct(schema::Struct { fields })))
            }
            Typ::Map(map) => {
                let key = map.key.as_ref().context("map without key")?;
                let key_typ = key.typ.as_ref().context("key without type")?;
                let value = map.value.as_ref().context("map without value")?;
                let val_typ = value.typ.as_ref().context("value without type")?;
                let key_typ = self.resolve(key_typ)?;
                let val_typ = self.resolve(val_typ)?;
                if matches!((&key_typ, &val_typ), (Cow::Borrowed(_), Cow::Borrowed(_))) {
                    Ok(Cow::Borrowed(typ))
                } else {
                    Ok(Cow::Owned(Typ::Map(Box::new(schema::Map {
                        key: Some(Box::new(schema::Type {
                            typ: Some(key_typ.into_owned()),
                            validation: key.validation.clone(),
                        })),
                        value: Some(Box::new(schema::Type {
                            typ: Some(val_typ.into_owned()),
                            validation: value.validation.clone(),
                        })),
                    }))))
                }
            }
            Typ::List(list) => {
                let elem = list.elem.as_ref().context("list without elem")?;
                let elem_typ = elem.typ.as_ref().context("elem without type")?;
                let elem_typ = self.resolve(elem_typ)?;
                if matches!(elem_typ, Cow::Borrowed(_)) {
                    Ok(Cow::Borrowed(typ))
                } else {
                    Ok(Cow::Owned(Typ::List(Box::new(schema::List {
                        elem: Some(Box::new(schema::Type {
                            typ: Some(elem_typ.into_owned()),
                            validation: elem.validation.clone(),
                        })),
                    }))))
                }
            }
            Typ::Union(union) => {
                let types = self.resolve_types(&union.types)?;
                let types = types
                    .into_iter()
                    .zip(&union.types)
                    .map(|(typ, t)| schema::Type {
                        typ: Some(typ.into_owned()),
                        validation: t.validation.clone(),
                    })
                    .collect::<Vec<_>>();
                Ok(Cow::Owned(Typ::Union(schema::Union { types })))
            }
            Typ::Builtin(_) => Ok(Cow::Borrowed(typ)),
            Typ::Literal(_) => Ok(Cow::Borrowed(typ)),
            Typ::Pointer(ptr) => {
                let base = ptr.base.as_ref().context("pointer without base")?;
                let typ = base.typ.as_ref().context("base without type")?;
                self.resolve(typ)
            }
            Typ::TypeParameter(param) => {
                let idx = param.param_idx as usize;
                let typ = &self.resolved_args[idx];
                Ok(typ.clone())
            }
            Typ::Config(_cfg) => anyhow::bail!("config types are not supported"),
        }
    }

    fn resolve_types(&self, types: &'a [schema::Type]) -> anyhow::Result<Vec<Cow<'a, Typ>>> {
        types
            .iter()
            .map(|typ| {
                let typ = typ.typ.as_ref().context("type without type")?;
                self.resolve(typ)
            })
            .collect()
    }
}

@eandre
Copy link
Member

eandre commented Jan 27, 2025

@theGlenn sorry about that. The issue is that the parser doesn't handle generics with default type parameters at the moment. It should work if you pass in the generic type instead of relying on a default type.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants