Skip to content

[BUG] Crash when using template literal call pattern on a returned function #1637

Closed
@SirWrexes

Description

@SirWrexes

Issue description

Functions in JS and TS can be called with template strings as their arguments, which passes the literal parts as an array and the variables inserted into the pattern as vaargs.
I've been using this in LÖVE2D for shaders, with this simple function that pieces together the bits of the shader code and the variables evaluated at runtime into a single string then passed into the newShader function.

export const glsl = (code: TemplateStringsArray, ...vars: unknown[]) => {
  const len = code.length

  if (len === 1) return code[0]

  let i = 0
  let buff = ''
  do {
    buff += code[i] + tostring(vars[i])
  } while (++i < len - 1)
  buff += code[i]

  return buff
}

const code = glsl`
  vec4 effect(vec4 colour, Image tex, vec2 texpos, vec2 scrpos) {
    vec4 r;
   
    // shader logic ...
    // Since this is a template string, can conveniently pull variables from the scope with ${}

    return r
  }
`
const shader = love.graphics.newShader()

It's great because it pairs with glsl-literal to provide syntactic highlighting inside my TS sources.

Wanting to experiment with this pattern, I tried to use it for simple tile map creation with a factory that would return a function that works similarly. Something along the lines of

const buildMap = (displaySize: WH, cellSize: WH) => {
  if (displaySize.width % cellSize.width !== 0)
    error('Cell width is not an exact denominator of display width')
  if (displaySize.height % cellSize.height !== 0)
    error('Cell height is not an exact denominator of display height')

  return ((template: TemplateStringsArray) => {
    const canvas = love.graphics.newCanvas()
    const str = template.raw.join().replace('%s', '') //strip all whitespaces
    const map: Nullable<Rectangle>[][] = []

    // Build the tile map and stuff

    const update = () => {/* ... */}
  
    const draw = () => { /* ... */ }
  
    return {
      map,
      update,
      draw,
    }
  })
}

const { map, update, draw } = buildMap({width: 10, height: 10}, {width: 2, height: 2})/* CRASHES HERE */`
  +++++
  +...+
  +.#.+
  +...+
  +++++
`

But TSTL doesn't seem to like immediately calling the returned function. It does however work if you take the function returned by the factory and then use the template literal call patten with it

const mapLambda = buildMap({width: 10, height: 10}, {width: 2, height: 2})
const { map, update, draw } = mapLambda`
  +++++
  +...+
  +.#.+
  +...+
  +++++
`

Minimal reproduction

Here's a minimal reproduction setup:
package.json:

{
  "devDependencies": {
    "typescript": "^5.8.2",
    "typescript-to-lua": "^1.31.1"
  }
}

reproduction.ts

declare function print(...msg: string[]): void

// Note: crashes regardless of using the `function` keyword or arrow pattern.
const factory = () => {
  return (template: TemplateStringsArray) => {
    for (let part of template) print(part)
  }
}

factory()`Some string template`

Stack trace:

PS [REDACTED]> pnpm tstl
[REDACTED]\node_modules\.pnpm\[email protected]\node_modules\typescript\lib\typescript.js:126991
      throw e;
      ^

Error: Unsupported LeftHandSideExpression kind: CallExpression
    at transformContextualCallExpression ([REDACTED]\node_modules\.pnpm\[email protected][email protected]\node_modules\typescript-to-lua\dist\transformation\visitors\call.js:120:15)
    at transformTaggedTemplateExpression ([REDACTED]\node_modules\.pnpm\[email protected][email protected]\node_modules\typescript-to-lua\dist\transformation\visitors\template.js:71:61)
    at TransformationContext.transformNodeRaw ([REDACTED]\node_modules\.pnpm\[email protected][email protected]\node_modules\typescript-to-lua\dist\transformation\context\context.js:67:24)
    at TransformationContext.transformExpression ([REDACTED]\node_modules\.pnpm\[email protected][email protected]\node_modules\typescript-to-lua\dist\transformation\context\context.js:83:29)
    at transformExpressionStatement ([REDACTED]\node_modules\.pnpm\[email protected][email protected]\node_modules\typescript-to-lua\dist\transformation\visitors\expression-statement.js:18:36)
    at TransformationContext.transformNodeRaw ([REDACTED]\node_modules\.pnpm\[email protected][email protected]\node_modules\typescript-to-lua\dist\transformation\context\context.js:67:24)
    at TransformationContext.transformNode ([REDACTED]\repro\node_modules\.pnpm\[email protected][email protected]\node_modules\typescript-to-lua\dist\transformation\context\context.js:47:56)
    at [REDACTED]\node_modules\.pnpm\[email protected][email protected]\node_modules\typescript-to-lua\dist\transformation\context\context.js:102:37
    at Array.flatMap (<anonymous>)
    at TransformationContext.transformStatements ([REDACTED]\node_modules\.pnpm\[email protected][email protected]\node_modules\typescript-to-lua\dist\transformation\context\context.js:100:45)

Node.js v24.0.2

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions