import {
  Dict,
  Opaque
} from '@glimmer/util';

import {
  PathReference,
  TagWrapper,
  RevisionTag
} from "@glimmer/reference";

import {
  Arguments,
  CapturedArguments,
  Helper as GlimmerHelper,
  VM
} from "@glimmer/runtime";

import {
  RootReference
} from '@glimmer/component';

export type UserHelper = (args: ReadonlyArray<Opaque>, named: Dict<Opaque>) => any;

export default function buildUserHelper(helperFunc): GlimmerHelper {
  return (_vm: VM, args: Arguments) => new HelperReference(helperFunc, args);
}

export class HelperReference implements PathReference<Opaque> {
  private helper: UserHelper;
  private args: CapturedArguments;
  public tag: TagWrapper<RevisionTag>;

  constructor(helper: UserHelper, args: Arguments) {
    this.helper = helper;
    this.tag = args.tag;
    this.args = args.capture();
  }

  value() {
    let { helper, args } = this;

    return helper(args.positional.value(), args.named.value());
  }

  get(): RootReference {
    return new RootReference(this);
  }
}

import {
  Dict,
  Opaque
} from '@glimmer/util';

import {
  TagWrapper,
  RevisionTag
} from "@glimmer/reference";

import {
  Arguments,
  CapturedArguments,
  Helper as GlimmerHelper,
  VM
} from "@glimmer/runtime";

import {
  CachedReference
} from '@glimmer/component';

export type UserHelper = (args: ReadonlyArray<Opaque>, named: Dict<Opaque>) => any;

export default function buildUserHelper(helperFunc): GlimmerHelper {
  return (_vm: VM, args: Arguments) => new HelperReference(helperFunc, args);
}

export class HelperReference extends CachedReference<Opaque> {
  public tag: TagWrapper<RevisionTag>;
  private args: CapturedArguments;

  constructor(private helper: UserHelper, args: Arguments) {
    super();

    this.tag = args.tag;
    this.args = args.capture();
  }

  compute() {
    let { helper, args } = this;

    return helper(args.positional.value(), args.named.value());
  }
}

Make user helper references cached
Make user helper references cached

Recent changes in the Glimmer VM cause references to be reified (i.e. have their value() method called) multiple times per invocation, which is potentially expensive as well as very surprising to users. Users have an intuition that helper calls are functions calls, and a single invocation of the helper should produce a single invocation of the function.

While this is something we may want to try to address in the VM, by reifying the helper value once per invocation and pushing that value on to the stack, ultimately Glimmer’s model is that references should be cheap to reify, and those that are not are responsible for implementing their own caching or other optimizations.

This commit applies a blunt force fix to the problem of HelperReference reification being user visible by making them cached. With this change, the return value from a helper is saved and re-used unless the tags of the helper inputs are invalid. This at least allows the tests to pass with the current release version of Glimmer VM, at the cost of extra memory consumption. If we can get more reliable tests in the VM to prevent unnecessary reification of helper references, we can consider reverting this change in the future.
