export const description = `Validation tests for function restrictions`;

import { makeTestGroup } from '../../../../common/framework/test_group.js';
import { keysOf } from '../../../../common/util/data_tables.js';
import { ShaderValidationTest } from '../shader_validation_test.js';

export const g = makeTestGroup(ShaderValidationTest);

interface VertexPosCase {
  name: string;
  value: string;
  valid: boolean;
}

const kCCommonTypeDecls = `
struct runtime_array_struct {
  arr : array<u32>
}

struct constructible {
  a : i32,
  b : u32,
  c : f32,
  d : bool,
}

struct host_shareable {
  a : i32,
  b : u32,
  c : f32,
}

struct struct_with_array {
  a : array<constructible, 4>
}

`;

const kVertexPosCases: Record<string, VertexPosCase> = {
  bare_position: { name: `@builtin(position) vec4f`, value: `vec4f()`, valid: true },
  nested_position: { name: `pos_struct`, value: `pos_struct()`, valid: true },
  no_bare_position: { name: `vec4f`, value: `vec4f()`, valid: false },
  no_nested_position: { name: `no_pos_struct`, value: `no_pos_struct()`, valid: false },
};

g.test('vertex_returns_position')
  .specURL('https://gpuweb.github.io/gpuweb/wgsl/#function-restriction')
  .desc(`Test that a vertex shader should return position`)
  .params(u => u.combine('case', keysOf(kVertexPosCases)))
  .fn(t => {
    const testcase = kVertexPosCases[t.params.case];
    const code = `
struct pos_struct {
  @builtin(position) pos : vec4f
}

struct no_pos_struct {
  @location(0) x : vec4f
}

@vertex
fn main() -> ${testcase.name} {
  return ${testcase.value};
}`;

    t.expectCompileResult(testcase.valid, code);
  });

g.test('entry_point_call_target')
  .specURL('https://gpuweb.github.io/gpuweb/wgsl/#function-restriction')
  .desc(`Test that an entry point cannot be the target of a function call`)
  .params(u =>
    u
      .combine('stage', ['@fragment', '@vertex', '@compute @workgroup_size(1,1,1)'] as const)
      .combine('entry_point', ['with', 'without'] as const)
  )
  .fn(t => {
    const use_attr = t.params.entry_point === 'with';
    let ret_attr = '';
    if (use_attr && t.params.stage === '@vertex') {
      ret_attr = '@builtin(position)';
    }
    const ret = t.params.stage.indexOf('@vertex') === 0 ? `-> ${ret_attr} vec4f` : '';
    const ret_value = t.params.stage.indexOf('@vertex') === 0 ? `return vec4f();` : '';
    const call = t.params.stage.indexOf('@vertex') === 0 ? 'let tmp = bar();' : 'bar();';
    const stage_attr = use_attr ? t.params.stage : '';
    const code = `
${stage_attr}
fn bar() ${ret} {
  ${ret_value}
}

fn foo() {
  ${call}
}
`;
    t.expectCompileResult(!use_attr, code);
  });

interface RetTypeCase {
  name: string;
  value: string;
  valid: boolean;
}

const kFunctionRetTypeCases: Record<string, RetTypeCase> = {
  // Constructible types,
  u32: { name: `u32`, value: ``, valid: true },
  i32: { name: `i32`, value: ``, valid: true },
  f32: { name: `f32`, value: ``, valid: true },
  bool: { name: `bool`, value: ``, valid: true },
  f16: { name: `f16`, value: ``, valid: true },
  vec2: { name: `vec2u`, value: ``, valid: true },
  vec3: { name: `vec3i`, value: ``, valid: true },
  vec4: { name: `vec4f`, value: ``, valid: true },
  mat2x2: { name: `mat2x2f`, value: ``, valid: true },
  mat2x3: { name: `mat2x3f`, value: ``, valid: true },
  mat2x4: { name: `mat2x4f`, value: ``, valid: true },
  mat3x2: { name: `mat3x2f`, value: ``, valid: true },
  mat3x3: { name: `mat3x3f`, value: ``, valid: true },
  mat3x4: { name: `mat3x4f`, value: ``, valid: true },
  mat4x2: { name: `mat4x2f`, value: ``, valid: true },
  mat4x3: { name: `mat4x3f`, value: ``, valid: true },
  mat4x4: { name: `mat4x4f`, value: ``, valid: true },
  array1: { name: `array<u32, 4>`, value: ``, valid: true },
  array2: { name: `array<vec2f, 2>`, value: ``, valid: true },
  array3: { name: `array<constructible, 4>`, value: ``, valid: true },
  array4: { name: `array<mat2x2f, 4>`, value: ``, valid: true },
  array5: { name: `array<bool, 4>`, value: ``, valid: true },
  struct1: { name: `constructible`, value: ``, valid: true },
  struct2: { name: `struct_with_array`, value: ``, valid: true },

  // Non-constructible types.
  runtime_array: { name: `array<u32>`, value: ``, valid: false },
  runtime_struct: { name: `runtime_array_struct`, value: ``, valid: false },
  override_array: { name: `array<u32, override_size>`, value: ``, valid: false },
  atomic_u32: { name: `atomic<u32>`, value: `atomic_wg`, valid: false },
  atomic_struct: { name: `atomic_struct`, value: ``, valid: false },
  texture_sample: { name: `texture_2d<f32>`, value: `t`, valid: false },
  texture_depth: { name: `texture_depth_2d`, value: `t_depth`, valid: false },
  texture_multisampled: {
    name: `texture_multisampled_2d<f32>`,
    value: `t_multisampled`,
    valid: false,
  },
  texture_storage: {
    name: `texture_storage_2d<rgba8unorm, write>`,
    value: `t_storage`,
    valid: false,
  },
  sampler: { name: `sampler`, value: `s`, valid: false },
  sampler_comparison: { name: `sampler_comparison`, value: `s_depth`, valid: false },
  ptr: { name: `ptr<workgroup, atomic<u32>>`, value: `&atomic_wg`, valid: false },
};

g.test('function_return_types')
  .specURL('https://gpuweb.github.io/gpuweb/wgsl/#function-restriction')
  .desc(`Test that function return types must be constructible`)
  .params(u => u.combine('case', keysOf(kFunctionRetTypeCases)))
  .fn(t => {
    const testcase = kFunctionRetTypeCases[t.params.case];
    const enable = testcase.name === 'f16' ? 'enable f16;' : '';
    const value = testcase.value === '' ? `${testcase.name}()` : testcase.value;
    const code = `
${enable}

${kCCommonTypeDecls}

struct atomic_struct {
  a : atomic<u32>
};

override override_size : u32;

var<workgroup> atomic_wg : atomic<u32>;

@group(0) @binding(0)
var t : texture_2d<f32>;
@group(0) @binding(1)
var s : sampler;
@group(0) @binding(2)
var s_depth : sampler_comparison;
@group(0) @binding(3)
var t_storage : texture_storage_2d<rgba8unorm, write>;
@group(0) @binding(4)
var t_depth : texture_depth_2d;
@group(0) @binding(5)
var t_multisampled : texture_multisampled_2d<f32>;
@group(0) @binding(6)
var t_external : texture_external;

fn foo() -> ${testcase.name} {
  return ${value};
}`;

    t.expectCompileResult(testcase.valid, code);
  });

interface ParamTypeCase {
  name: string;
  valid: boolean | 'with_unrestricted_pointer_parameters';
}

const kFunctionParamTypeCases: Record<string, ParamTypeCase> = {
  // Constructible types,
  u32: { name: `u32`, valid: true },
  i32: { name: `i32`, valid: true },
  f32: { name: `f32`, valid: true },
  bool: { name: `bool`, valid: true },
  f16: { name: `f16`, valid: true },
  vec2: { name: `vec2u`, valid: true },
  vec3: { name: `vec3i`, valid: true },
  vec4: { name: `vec4f`, valid: true },
  mat2x2: { name: `mat2x2f`, valid: true },
  mat2x3: { name: `mat2x3f`, valid: true },
  mat2x4: { name: `mat2x4f`, valid: true },
  mat3x2: { name: `mat3x2f`, valid: true },
  mat3x3: { name: `mat3x3f`, valid: true },
  mat3x4: { name: `mat3x4f`, valid: true },
  mat4x2: { name: `mat4x2f`, valid: true },
  mat4x3: { name: `mat4x3f`, valid: true },
  mat4x4: { name: `mat4x4f`, valid: true },
  array1: { name: `array<u32, 4>`, valid: true },
  array2: { name: `array<vec2f, 2>`, valid: true },
  array3: { name: `array<constructible, 4>`, valid: true },
  array4: { name: `array<mat2x2f, 4>`, valid: true },
  array5: { name: `array<bool, 4>`, valid: true },
  struct1: { name: `constructible`, valid: true },
  struct2: { name: `struct_with_array`, valid: true },

  // Non-constructible types.
  runtime_array: { name: `array<u32>`, valid: false },
  runtime_struct: { name: `runtime_array_struct`, valid: false },
  override_array: { name: `array<u32, override_size>`, valid: false },
  atomic_u32: { name: `atomic<u32>`, valid: false },
  atomic_struct: { name: `atomic_struct`, valid: false },

  // Textures and samplers.
  texture_sample: { name: `texture_2d<f32>`, valid: true },
  texture_depth: { name: `texture_depth_2d`, valid: true },
  texture_multisampled: {
    name: `texture_multisampled_2d<f32>`,
    valid: true,
  },
  texture_storage: { name: `texture_storage_2d<rgba8unorm, write>`, valid: true },
  sampler: { name: `sampler`, valid: true },
  sampler_comparison: { name: `sampler_comparison`, valid: true },

  // Valid pointers.
  ptr1: { name: `ptr<function, u32>`, valid: true },
  ptr2: { name: `ptr<function, constructible>`, valid: true },
  ptr3: { name: `ptr<private, u32>`, valid: true },
  ptr4: { name: `ptr<private, constructible>`, valid: true },

  // Pointers only valid with unrestricted_pointer_parameters
  ptr5: { name: `ptr<storage, u32>`, valid: 'with_unrestricted_pointer_parameters' },
  ptr6: { name: `ptr<storage, u32, read>`, valid: 'with_unrestricted_pointer_parameters' },
  ptr7: { name: `ptr<storage, u32, read_write>`, valid: 'with_unrestricted_pointer_parameters' },
  ptr8: { name: `ptr<uniform, u32>`, valid: 'with_unrestricted_pointer_parameters' },
  ptr9: { name: `ptr<workgroup, u32>`, valid: 'with_unrestricted_pointer_parameters' },
  ptr10: {
    name: `ptr<storage, host_shareable, read_write>`,
    valid: 'with_unrestricted_pointer_parameters',
  },
  ptr11: {
    name: `ptr<storage, host_shareable, read>`,
    valid: 'with_unrestricted_pointer_parameters',
  },
  ptr12: {
    name: `ptr<uniform, host_shareable>`,
    valid: 'with_unrestricted_pointer_parameters',
  },
  ptrWorkgroupAtomic: {
    name: `ptr<workgroup, atomic<u32>>`,
    valid: 'with_unrestricted_pointer_parameters',
  },
  ptrWorkgroupNestedAtomic: {
    name: `ptr<workgroup, array<atomic<u32>,1>>`,
    valid: 'with_unrestricted_pointer_parameters',
  },

  // Invalid pointers.
  invalid_ptr1: { name: `ptr<handle, u32>`, valid: false }, // Can't spell handle address space
  invalid_ptr2: { name: `ptr<not_an_address_space, u32>`, valid: false },
  invalid_ptr3: { name: `ptr<storage>`, valid: false }, // No store type
  invalid_ptr4: { name: `ptr<private,u32,read>`, valid: false }, // Can't specify access mode
  invalid_ptr5: { name: `ptr<private,u32,write>`, valid: false }, // Can't specify access mode
  invalid_ptr6: { name: `ptr<private,u32,read_write>`, valid: false }, // Can't specify access mode
  invalid_ptr7: { name: `ptr<private,clamp>`, valid: false }, // Invalid store type
  invalid_ptr8: { name: `ptr<function, texture_external>`, valid: false }, // non-constructible pointer type
};

g.test('function_parameter_types')
  .specURL('https://gpuweb.github.io/gpuweb/wgsl/#function-restriction')
  .desc(`Test validation of user-declared function parameter types`)
  .params(u => u.combine('case', keysOf(kFunctionParamTypeCases)))
  .fn(t => {
    const testcase = kFunctionParamTypeCases[t.params.case];
    const enable = testcase.name === 'f16' ? 'enable f16;' : '';
    const code = `
${enable}

${kCCommonTypeDecls}

fn foo(param : ${testcase.name}) {
}`;

    let isValid = testcase.valid;
    if (isValid === 'with_unrestricted_pointer_parameters') {
      isValid = t.hasLanguageFeature('unrestricted_pointer_parameters');
    }

    t.expectCompileResult(isValid, code);
  });

interface ParamValueCase {
  value: string;
  matches: string[];
  needsUnrestrictedPointerParameters?: boolean;
}

const kFunctionParamValueCases: Record<string, ParamValueCase> = {
  // Values
  u32_literal: { value: `0u`, matches: ['u32'] },
  i32_literal: { value: `0i`, matches: ['i32'] },
  f32_literal: { value: `0f`, matches: ['f32'] },
  bool_literal: { value: `false`, matches: ['bool'] },
  abstract_int_literal: { value: `0`, matches: ['u32', 'i32', 'f32', 'f16'] },
  abstract_float_literal: { value: `0.0`, matches: ['f32', 'f16'] },
  vec2u_constructor: { value: `vec2u()`, matches: ['vec2'] },
  vec2i_constructor: { value: `vec2i()`, matches: [] },
  vec2f_constructor: { value: `vec2f()`, matches: [] },
  vec2b_constructor: { value: `vec2<bool>()`, matches: [] },
  vec3u_constructor: { value: `vec3u()`, matches: [] },
  vec3i_constructor: { value: `vec3i()`, matches: ['vec3'] },
  vec3f_constructor: { value: `vec3f()`, matches: [] },
  vec3b_constructor: { value: `vec3<bool>()`, matches: [] },
  vec4u_constructor: { value: `vec4u()`, matches: [] },
  vec4i_constructor: { value: `vec4i()`, matches: [] },
  vec4f_constructor: { value: `vec4f()`, matches: ['vec4'] },
  vec4b_constructor: { value: `vec4<bool>()`, matches: [] },
  vec2_abstract_int: { value: `vec2(0,0)`, matches: ['vec2'] },
  vec2_abstract_float: { value: `vec2(0.0,0)`, matches: [] },
  vec3_abstract_int: { value: `vec3(0,0,0)`, matches: ['vec3'] },
  vec3_abstract_float: { value: `vec3(0.0,0,0)`, matches: [] },
  vec4_abstract_int: { value: `vec4(0,0,0,0)`, matches: ['vec4'] },
  vec4_abstract_float: { value: `vec4(0.0,0,0,0)`, matches: ['vec4'] },
  mat2x2_constructor: { value: `mat2x2f()`, matches: ['mat2x2'] },
  mat2x3_constructor: { value: `mat2x3f()`, matches: ['mat2x3'] },
  mat2x4_constructor: { value: `mat2x4f()`, matches: ['mat2x4'] },
  mat3x2_constructor: { value: `mat3x2f()`, matches: ['mat3x2'] },
  mat3x3_constructor: { value: `mat3x3f()`, matches: ['mat3x3'] },
  mat3x4_constructor: { value: `mat3x4f()`, matches: ['mat3x4'] },
  mat4x2_constructor: { value: `mat4x2f()`, matches: ['mat4x2'] },
  mat4x3_constructor: { value: `mat4x3f()`, matches: ['mat4x3'] },
  mat4x4_constructor: { value: `mat4x4f()`, matches: ['mat4x4'] },
  array1_constructor: { value: `array<u32, 4>()`, matches: ['array1'] },
  array2_constructor: { value: `array<vec2f, 2>()`, matches: ['array2'] },
  array3_constructor: { value: `array<constructible, 4>()`, matches: ['array3'] },
  array4_constructor: { value: `array<mat2x2f, 4>()`, matches: ['array4'] },
  array5_constructor: { value: `array<bool, 4>()`, matches: ['array5'] },
  struct1_constructor: { value: `constructible()`, matches: ['struct1'] },
  struct2_constructor: { value: `struct_with_array()`, matches: ['struct2'] },

  // Variable references
  g_u32: { value: `g_u32`, matches: ['u32'] },
  g_i32: { value: `g_i32`, matches: ['i32'] },
  g_f32: { value: `g_f32`, matches: ['f32'] },
  g_bool: { value: `g_bool`, matches: ['bool'] },
  g_vec2: { value: `g_vec2`, matches: ['vec2'] },
  g_vec3: { value: `g_vec3`, matches: ['vec3'] },
  g_vec4: { value: `g_vec4`, matches: ['vec4'] },
  g_mat2x2: { value: `g_mat2x2`, matches: ['mat2x2'] },
  g_mat2x3: { value: `g_mat2x3`, matches: ['mat2x3'] },
  g_mat2x4: { value: `g_mat2x4`, matches: ['mat2x4'] },
  g_mat3x2: { value: `g_mat3x2`, matches: ['mat3x2'] },
  g_mat3x3: { value: `g_mat3x3`, matches: ['mat3x3'] },
  g_mat3x4: { value: `g_mat3x4`, matches: ['mat3x4'] },
  g_mat4x2: { value: `g_mat4x2`, matches: ['mat4x2'] },
  g_mat4x3: { value: `g_mat4x3`, matches: ['mat4x3'] },
  g_mat4x4: { value: `g_mat4x4`, matches: ['mat4x4'] },
  g_array1: { value: `g_array1`, matches: ['array1'] },
  g_array2: { value: `g_array2`, matches: ['array2'] },
  g_array3: { value: `g_array3`, matches: ['array3'] },
  g_array4: { value: `g_array4`, matches: ['array4'] },
  g_array5: { value: `g_array5`, matches: ['array5'] },
  g_constructible: { value: `g_constructible`, matches: ['struct1'] },
  g_struct_with_array: { value: `g_struct_with_array`, matches: ['struct2'] },
  f_u32: { value: `f_u32`, matches: ['u32'] },
  f_i32: { value: `f_i32`, matches: ['i32'] },
  f_f32: { value: `f_f32`, matches: ['f32'] },
  f_bool: { value: `f_bool`, matches: ['bool'] },
  f_vec2: { value: `f_vec2`, matches: ['vec2'] },
  f_vec3: { value: `f_vec3`, matches: ['vec3'] },
  f_vec4: { value: `f_vec4`, matches: ['vec4'] },
  f_mat2x2: { value: `f_mat2x2`, matches: ['mat2x2'] },
  f_mat2x3: { value: `f_mat2x3`, matches: ['mat2x3'] },
  f_mat2x4: { value: `f_mat2x4`, matches: ['mat2x4'] },
  f_mat3x2: { value: `f_mat3x2`, matches: ['mat3x2'] },
  f_mat3x3: { value: `f_mat3x3`, matches: ['mat3x3'] },
  f_mat3x4: { value: `f_mat3x4`, matches: ['mat3x4'] },
  f_mat4x2: { value: `f_mat4x2`, matches: ['mat4x2'] },
  f_mat4x3: { value: `f_mat4x3`, matches: ['mat4x3'] },
  f_mat4x4: { value: `f_mat4x4`, matches: ['mat4x4'] },
  f_array1: { value: `f_array1`, matches: ['array1'] },
  f_array2: { value: `f_array2`, matches: ['array2'] },
  f_array3: { value: `f_array3`, matches: ['array3'] },
  f_array4: { value: `f_array4`, matches: ['array4'] },
  f_array5: { value: `f_array5`, matches: ['array5'] },
  f_constructible: { value: `f_constructible`, matches: ['struct1'] },
  f_struct_with_array: { value: `f_struct_with_array`, matches: ['struct2'] },
  g_index_u32: { value: `g_constructible.b`, matches: ['u32'] },
  g_index_i32: { value: `g_constructible.a`, matches: ['i32'] },
  g_index_f32: { value: `g_constructible.c`, matches: ['f32'] },
  g_index_bool: { value: `g_constructible.d`, matches: ['bool'] },
  f_index_u32: { value: `f_constructible.b`, matches: ['u32'] },
  f_index_i32: { value: `f_constructible.a`, matches: ['i32'] },
  f_index_f32: { value: `f_constructible.c`, matches: ['f32'] },
  f_index_bool: { value: `f_constructible.d`, matches: ['bool'] },
  g_array_index_u32: { value: `g_struct_with_array.a[0].b`, matches: ['u32'] },
  g_array_index_i32: { value: `g_struct_with_array.a[1].a`, matches: ['i32'] },
  g_array_index_f32: { value: `g_struct_with_array.a[2].c`, matches: ['f32'] },
  g_array_index_bool: { value: `g_struct_with_array.a[3].d`, matches: ['bool'] },
  f_array_index_u32: { value: `f_struct_with_array.a[0].b`, matches: ['u32'] },
  f_array_index_i32: { value: `f_struct_with_array.a[1].a`, matches: ['i32'] },
  f_array_index_f32: { value: `f_struct_with_array.a[2].c`, matches: ['f32'] },
  f_array_index_bool: { value: `f_struct_with_array.a[3].d`, matches: ['bool'] },

  // Textures and samplers
  texture_sample: { value: `t`, matches: ['texture_sample'] },
  texture_depth: { value: `t_depth`, matches: ['texture_depth'] },
  texture_multisampled: { value: `t_multisampled`, matches: ['texture_multisampled'] },
  texture_storage: { value: `t_storage`, matches: ['texture_storage'] },
  texture_external: { value: `t_external`, matches: ['texture_external'] },
  sampler: { value: `s`, matches: ['sampler'] },
  sampler_comparison: { value: `s_depth`, matches: ['sampler_comparison'] },

  // Pointers
  ptr1: { value: `&f_u32`, matches: ['ptr1'] },
  ptr2: { value: `&f_constructible`, matches: ['ptr2'] },
  ptr3: { value: `&g_u32`, matches: ['ptr3'] },
  ptr4: { value: `&g_constructible`, matches: ['ptr4'] },

  ptr_let1: { value: `ptr_f_u32`, matches: ['ptr1'] },
  ptr_let2: { value: `ptr_f_constructible`, matches: ['ptr2'] },
  ptr_let3: { value: `ptr_g_u32`, matches: ['ptr3'] },
  ptr_let4: { value: `ptr_g_constructible`, matches: ['ptr4'] },
  ptr_let5: { value: `let_let_f_u32`, matches: ['ptr1'] },

  // Requires 'unrestricted_pointer_parameters' WGSL feature
  ptr5: {
    value: `&f_constructible.b`,
    matches: ['ptr1'],
    needsUnrestrictedPointerParameters: true,
  },
  ptr6: {
    value: `&g_constructible.b`,
    matches: ['ptr3'],
    needsUnrestrictedPointerParameters: true,
  },
  ptr7: {
    value: `&f_struct_with_array.a[1].b`,
    matches: ['ptr1'],
    needsUnrestrictedPointerParameters: true,
  },
  ptr8: {
    value: `&g_struct_with_array.a[2]`,
    matches: ['ptr4'],
    needsUnrestrictedPointerParameters: true,
  },
  ptr9: {
    value: `&ro_host_shareable.b`,
    matches: ['ptr5', 'ptr6'],
    needsUnrestrictedPointerParameters: true,
  },
  ptr10: {
    value: `&rw_host_shareable`,
    matches: ['ptr10'],
    needsUnrestrictedPointerParameters: true,
  },
  ptr11: {
    value: `&ro_host_shareable`,
    matches: ['ptr11'],
    needsUnrestrictedPointerParameters: true,
  },
  ptr12: {
    value: `&uniform_host_shareable`,
    matches: ['ptr12'],
    needsUnrestrictedPointerParameters: true,
  },
};

function parameterMatches(decl: string, matches: string[]): boolean {
  for (const val of matches) {
    if (decl === val) {
      return true;
    }
  }
  return false;
}

g.test('function_parameter_matching')
  .specURL('https://gpuweb.github.io/gpuweb/wgsl/#function-restriction')
  .desc(
    `Test that function parameter types match function parameter type on user-declared functions`
  )
  .params(u =>
    u
      .combine('decl', keysOf(kFunctionParamTypeCases))
      .filter(u => {
        return kFunctionParamTypeCases[u.decl].valid !== false;
      })
      .beginSubcases()
      .combine('arg', keysOf(kFunctionParamValueCases))
  )
  .fn(t => {
    const param = kFunctionParamTypeCases[t.params.decl];
    const arg = kFunctionParamValueCases[t.params.arg];
    const enable = param.name === 'f16' ? 'enable f16;' : '';
    const code = `
${enable}

${kCCommonTypeDecls}
@group(0) @binding(0)
var t : texture_2d<f32>;
@group(0) @binding(1)
var s : sampler;
@group(0) @binding(2)
var s_depth : sampler_comparison;
@group(0) @binding(3)
var t_storage : texture_storage_2d<rgba8unorm, write>;
@group(0) @binding(4)
var t_depth : texture_depth_2d;
@group(0) @binding(5)
var t_multisampled : texture_multisampled_2d<f32>;
@group(0) @binding(6)
var t_external : texture_external;

@group(1) @binding(0)
var<storage> ro_host_shareable : host_shareable;
@group(1) @binding(1)
var<storage, read_write> rw_host_shareable : host_shareable;
@group(1) @binding(2)
var<uniform> uniform_host_shareable : host_shareable;

fn bar(param : ${param.name}) { }

var<private> g_u32 : u32;
var<private> g_i32 : i32;
var<private> g_f32 : f32;
var<private> g_bool : bool;
var<private> g_vec2 : vec2u;
var<private> g_vec3 : vec3i;
var<private> g_vec4 : vec4f;
var<private> g_mat2x2 : mat2x2f;
var<private> g_mat2x3 : mat2x3f;
var<private> g_mat2x4 : mat2x4f;
var<private> g_mat3x2 : mat3x2f;
var<private> g_mat3x3 : mat3x3f;
var<private> g_mat3x4 : mat3x4f;
var<private> g_mat4x2 : mat4x2f;
var<private> g_mat4x3 : mat4x3f;
var<private> g_mat4x4 : mat4x4f;
var<private> g_array1 : array<u32, 4>;
var<private> g_array2 : array<vec2f, 2>;
var<private> g_array3 : array<constructible, 4>;
var<private> g_array4 : array<mat2x2f, 4>;
var<private> g_array5 : array<bool, 4>;
var<private> g_constructible : constructible;
var<private> g_struct_with_array : struct_with_array;

fn foo() {
  var f_u32 : u32;
  var f_i32 : i32;
  var f_f32 : f32;
  var f_bool : bool;
  var f_vec2 : vec2u;
  var f_vec3 : vec3i;
  var f_vec4 : vec4f;
  var f_mat2x2 : mat2x2f;
  var f_mat2x3 : mat2x3f;
  var f_mat2x4 : mat2x4f;
  var f_mat3x2 : mat3x2f;
  var f_mat3x3 : mat3x3f;
  var f_mat3x4 : mat3x4f;
  var f_mat4x2 : mat4x2f;
  var f_mat4x3 : mat4x3f;
  var f_mat4x4 : mat4x4f;
  var f_array1 : array<u32, 4>;
  var f_array2 : array<vec2f, 2>;
  var f_array3 : array<constructible, 4>;
  var f_array4 : array<mat2x2f, 4>;
  var f_array5 : array<bool, 4>;
  var f_constructible : constructible;
  var f_struct_with_array : struct_with_array;
  let ptr_f_u32 = &f_u32;
  let ptr_f_constructible = &f_constructible;
  let ptr_g_u32 = &g_u32;
  let ptr_g_constructible = &g_constructible;
  let let_let_f_u32 = ptr_f_u32;

  bar(${arg.value});
}
`;

    const needsUnrestrictedPointerParameters =
      (kFunctionParamTypeCases[t.params.decl].valid === 'with_unrestricted_pointer_parameters' ||
        arg.needsUnrestrictedPointerParameters) ??
      false;

    let isValid = parameterMatches(t.params.decl, arg.matches);
    if (isValid && needsUnrestrictedPointerParameters) {
      isValid = t.hasLanguageFeature('unrestricted_pointer_parameters');
    }

    t.expectCompileResult(isValid, code);
  });

g.test('no_direct_recursion')
  .specURL('https://gpuweb.github.io/gpuweb/wgsl/#function-restriction')
  .desc(`Test that functions cannot be directly recursive`)
  .fn(t => {
    const code = `
fn foo() {
  foo();
}`;

    t.expectCompileResult(false, code);
  });

g.test('no_indirect_recursion')
  .specURL('https://gpuweb.github.io/gpuweb/wgsl/#function-restriction')
  .desc(`Test that functions cannot be indirectly recursive`)
  .fn(t => {
    const code = `
fn bar() {
  foo();
}
fn foo() {
  bar();
}`;

    t.expectCompileResult(false, code);
  });

g.test('param_names_must_differ')
  .specURL('https://gpuweb.github.io/gpuweb/wgsl/#function-declaration-sec')
  .desc(`Test that function parameters must have different names`)
  .params(u => u.combine('p1', ['a', 'b', 'c'] as const).combine('p2', ['a', 'b', 'c'] as const))
  .fn(t => {
    const code = `fn foo(${t.params.p1} : u32, ${t.params.p2} : f32) { }`;
    t.expectCompileResult(t.params.p1 !== t.params.p2, code);
  });

const kParamUseCases: Record<string, string> = {
  body: `fn foo(param : u32) {
    let tmp = param;
  }`,
  var: `var<private> v : u32 = param;
  fn foo(param : u32) { }`,
  const: `const c : u32 = param;
  fn foo(param : u32) { }`,
  override: `override o : u32 = param;
  fn foo(param : u32) { }`,
  function: `fn bar() { let tmp = param; }
  fn foo(param : u32) { }`,
};

g.test('param_scope_is_function_body')
  .specURL('https://gpuweb.github.io/gpuweb/wgsl/#function-declaration-sec')
  .desc(`Test that function parameters are only in scope in the function body`)
  .params(u => u.combine('use', keysOf(kParamUseCases)))
  .fn(t => {
    t.expectCompileResult(t.params.use === 'body', kParamUseCases[t.params.use]);
  });

g.test('param_number_matches_call')
  .specURL('https://gpuweb.github.io/gpuweb/wgsl/#function-calls')
  .desc(`Test that function calls have an equal number of arguments as the number of parameters`)
  .params(u =>
    u
      .combine('num_args', [0, 1, 2, 3, 4, 255] as const)
      .combine('num_params', [0, 1, 2, 3, 4, 255] as const)
  )
  .fn(t => {
    let code = `
    fn bar(`;
    for (let i = 0; i < t.params.num_params; i++) {
      code += `p${i} : u32,`;
    }
    code += `) { }\n`;
    code += `fn foo() {\nbar(`;
    for (let i = 0; i < t.params.num_args; i++) {
      code += `0,`;
    }
    code += `);\n}`;
    t.expectCompileResult(t.params.num_args === t.params.num_params, code);
  });

const kParamsTypes = ['u32', 'i32', 'f32'];

interface ArgValue {
  value: string;
  matches: string[];
}

const kArgValues: Record<string, ArgValue> = {
  abstract_int: {
    value: '0',
    matches: ['u32', 'i32', 'f32'],
  },
  abstract_float: {
    value: '0.0',
    matches: ['f32'],
  },
  unsigned_int: {
    value: '0u',
    matches: ['u32'],
  },
  signed_int: {
    value: '0i',
    matches: ['i32'],
  },
  float: {
    value: '0f',
    matches: ['f32'],
  },
};

function checkArgTypeMatch(param_type: string, arg_matches: string[]): boolean {
  for (const match of arg_matches) {
    if (match === param_type) {
      return true;
    }
  }
  return false;
}

g.test('call_arg_types_match_1_param')
  .specURL('https://gpuweb.github.io/gpuweb/wgsl/#function-calls')
  .desc(`Test that the argument types match in order`)
  .params(u =>
    u
      .combine('p1_type', kParamsTypes) //
      .beginSubcases()
      .combine('arg1_value', keysOf(kArgValues))
  )
  .fn(t => {
    const code = `
fn bar(p1 : ${t.params.p1_type}) { }
fn foo() {
  bar(${kArgValues[t.params.arg1_value].value});
}`;

    const res = checkArgTypeMatch(t.params.p1_type, kArgValues[t.params.arg1_value].matches);
    t.expectCompileResult(res, code);
  });

g.test('call_arg_types_match_2_params')
  .specURL('https://gpuweb.github.io/gpuweb/wgsl/#function-calls')
  .desc(`Test that the argument types match in order`)
  .params(u =>
    u
      .combine('p1_type', kParamsTypes)
      .combine('p2_type', kParamsTypes)
      .beginSubcases()
      .combine('arg1_value', keysOf(kArgValues))
      .combine('arg2_value', keysOf(kArgValues))
  )
  .fn(t => {
    const code = `
fn bar(p1 : ${t.params.p1_type}, p2 : ${t.params.p2_type}) { }
fn foo() {
  bar(${kArgValues[t.params.arg1_value].value}, ${kArgValues[t.params.arg2_value].value});
}`;

    const res =
      checkArgTypeMatch(t.params.p1_type, kArgValues[t.params.arg1_value].matches) &&
      checkArgTypeMatch(t.params.p2_type, kArgValues[t.params.arg2_value].matches);
    t.expectCompileResult(res, code);
  });

g.test('call_arg_types_match_3_params')
  .specURL('https://gpuweb.github.io/gpuweb/wgsl/#function-calls')
  .desc(`Test that the argument types match in order`)
  .params(u =>
    u
      .combine('p1_type', kParamsTypes)
      .combine('p2_type', kParamsTypes)
      .combine('p3_type', kParamsTypes)
      .beginSubcases()
      .combine('arg1_value', keysOf(kArgValues))
      .combine('arg2_value', keysOf(kArgValues))
      .combine('arg3_value', keysOf(kArgValues))
  )
  .fn(t => {
    const code = `
fn bar(p1 : ${t.params.p1_type}, p2 : ${t.params.p2_type}, p3 : ${t.params.p3_type}) { }
fn foo() {
  bar(${kArgValues[t.params.arg1_value].value},
      ${kArgValues[t.params.arg2_value].value},
      ${kArgValues[t.params.arg3_value].value});
}`;

    const res =
      checkArgTypeMatch(t.params.p1_type, kArgValues[t.params.arg1_value].matches) &&
      checkArgTypeMatch(t.params.p2_type, kArgValues[t.params.arg2_value].matches) &&
      checkArgTypeMatch(t.params.p3_type, kArgValues[t.params.arg3_value].matches);
    t.expectCompileResult(res, code);
  });

g.test('param_name_can_shadow_function_name')
  .specURL('https://www.w3.org/TR/WGSL/#function-declaration-sec')
  .desc(`Tests that a function parameter can shadow the function name`)
  .fn(t => {
    const code = `
fn foo(foo: i32) -> i32 {
  return foo;
}
`;
    t.expectCompileResult(true, code);
  });

g.test('param_name_can_shadow_alias')
  .specURL('https://www.w3.org/TR/WGSL/#function-declaration-sec')
  .desc(`Tests that a function parameter can shadow an alias`)
  .fn(t => {
    const code = `
alias foo = f32;
fn test(foo: i32) -> i32 {
  return foo;
}
`;
    t.expectCompileResult(true, code);
  });

g.test('param_name_can_shadow_global')
  .specURL('https://www.w3.org/TR/WGSL/#function-declaration-sec')
  .desc(`Tests that a function parameter can shadow a global`)
  .fn(t => {
    const code = `
const foo: f32 = 1.2f;

fn test(foo: i32) -> i32 {
  return foo;
}
`;
    t.expectCompileResult(true, code);
  });

g.test('param_comma_placement')
  .specURL('https://www.w3.org/TR/WGSL/#function-declaration-sec')
  .desc(`Tests validation of commas in function parameter lists`)
  .params(u =>
    u
      .combine('param_1', [true, false])
      .combine('param_2', [true, false])
      .combine('comma', [true, false])
  )
  .fn(t => {
    const has_p1 = t.params.param_1;
    const has_p2 = t.params.param_2;
    const has_c = t.params.comma;

    const p1 = has_p1 ? 'foo: i32' : '';
    const p2 = has_p2 ? 'bar: f32' : '';
    const comma = has_c ? ', ' : ' ';

    const code = `
fn test(${p1}${comma}${p2}) {
}
`;

    const success =
      (!has_p1 && !has_p2 && !has_c) || // no params
      (has_p1 && !has_p2) || // only p1, which can have a trailing comma or not
      (!has_p1 && has_p2 && !has_c) || // just p2
      (has_p1 && has_p2 && has_c); // both params comma separated
    t.expectCompileResult(success, code);
  });

g.test('param_type_can_be_alias')
  .specURL('https://www.w3.org/TR/WGSL/#function-declaration-sec')
  .desc(`Tests that a function parameter type can be an alias`)
  .fn(t => {
    const code = `
alias foo = f32;
fn test(foo: foo) -> foo {
  return foo;
}
`;
    t.expectCompileResult(true, code);
  });

g.test('function_name_required')
  .specURL('https://www.w3.org/TR/WGSL/#function-declaration-sec')
  .desc(`Tests the function name is required`)
  .params(u => u.combine('name', [true, false]))
  .fn(t => {
    const has_name = t.params.name;
    const name = has_name ? 'name' : '';
    const code = `
fn ${name}() -> i32 {
  return 1;
}
`;
    t.expectCompileResult(has_name, code);
  });

g.test('param_type_required')
  .specURL('https://www.w3.org/TR/WGSL/#function-declaration-sec')
  .desc(`Tests the parameter type is required`)
  .params(u => u.combine('ty', [true, false]).combine('colon', [true, false]))
  .fn(t => {
    const has_ty = t.params.ty;
    const has_colon = t.params.colon;
    const ty = has_ty ? 'i32' : '';
    const colon = has_colon ? ':' : ' ';
    const code = `
fn f(foo${colon}${ty}) -> i32 {
  return 1;
}
`;
    t.expectCompileResult(has_ty && has_colon, code);
  });

g.test('body_required')
  .specURL('https://www.w3.org/TR/WGSL/#function-declaration-sec')
  .desc(`Tests the function body is required`)
  .params(u => u.combine('body', ['braces', 'semi', '']))
  .fn(t => {
    const body = t.params.body === 'braces' ? '{}' : t.params.body === 'semi' ? ';' : '';

    const code = `
fn f() ${body}

fn other() {}
`;
    t.expectCompileResult(body === '{}', code);
  });

g.test('parens_required')
  .specURL('https://www.w3.org/TR/WGSL/#function-declaration-sec')
  .desc(`Tests that the parens for a function are required`)
  .params(u => u.combine('parens', [true, false]).combine('param', [true, false]))
  .fn(t => {
    const has_parens = t.params.parens;
    const has_param = t.params.param;

    let args = '';
    if (has_parens) {
      args += '(';
    }
    if (has_param) {
      args += 'foo: i32';
    }

    if (has_parens) {
      args += ')';
    }

    const code = `
fn f ${args} {}
`;
    t.expectCompileResult(has_parens, code);
  });

g.test('non_module_scoped_function')
  .specURL('https://www.w3.org/TR/WGSL/#function-declaration-sec')
  .desc(`Tests that a non-module-scope function is rejected`)
  .params(u => u.combine('loc', ['inner', 'outer']))
  .fn(t => {
    const o = `fn a() -> i32 { return 1; }`;

    let inner = '';
    let outer = '';

    if (t.params.loc === 'inner') {
      inner = o;
    } else {
      outer = o;
    }

    const code = `
${outer}
fn b() {
  ${inner}
}
`;
    t.expectCompileResult(t.params.loc === 'outer', code);
  });

const kAttributes = {
  align: {
    attr: '@align(5)',
    pass: {
      func: false,
      param: false,
      ret: false,
    },
  },
  binding: {
    attr: '@binding(5)',
    pass: {
      func: false,
      param: false,
      ret: false,
    },
  },
  builtin: {
    attr: '@builtin(position)',
    pass: {
      func: false,
      param: false,
      ret: false,
    },
  },
  compute: {
    attr: `@compute`,
    pass: {
      func: false,
      param: false,
      ret: false,
    },
  },
  const: {
    attr: '@const',
    pass: {
      func: false,
      param: false,
      ret: false,
    },
  },
  diagnostic: {
    attr: `@diagnostic(off, derivative_uniformity)`,
    pass: {
      func: true,
      param: false,
      ret: false,
    },
  },
  fragment: {
    attr: `@fragment`,
    pass: {
      func: false,
      param: false,
      ret: false,
    },
  },
  group: {
    attr: `@group(1)`,
    pass: {
      func: false,
      param: false,
      ret: false,
    },
  },
  id: {
    attr: `@id(1)`,
    pass: {
      func: false,
      param: false,
      ret: false,
    },
  },
  interpolate: {
    attr: `@interpolate(linear, center)`,
    pass: {
      func: false,
      param: false,
      ret: false,
    },
  },
  invariant: {
    attr: `@invariant`,
    pass: {
      func: false,
      param: false,
      ret: false,
    },
  },
  location: {
    attr: `@location(0)`,
    pass: {
      func: false,
      param: false,
      ret: false,
    },
  },
  must_use: {
    attr: `@must_use`,
    pass: {
      func: true,
      param: false,
      ret: false,
    },
  },
  size: {
    attr: `@size(10)`,
    pass: {
      func: false,
      param: false,
      ret: false,
    },
  },
  vertex: {
    attr: `@vertex`,
    pass: {
      func: false,
      param: false,
      ret: false,
    },
  },
  workgroup_size: {
    attr: `@workgroup_size(1)`,
    pass: {
      func: false,
      param: false,
      ret: false,
    },
  },
};

g.test('function_attributes')
  .specURL('https://www.w3.org/TR/WGSL/#function-declaration-sec')
  .desc(`Tests the attributes for a function`)
  .params(u =>
    u.combine('case', keysOf(kAttributes)).combine('placement', ['func', 'param', 'ret'])
  )
  .fn(t => {
    const d = kAttributes[t.params.case];
    const func = t.params.placement === 'func';
    const param = t.params.placement === 'param';
    const ret = t.params.placement === 'ret';

    const code = `
${func ? d.attr : ''}
fn b(${param ? d.attr : ''} foo: i32) -> ${ret ? d.attr : ''} i32{
  return 1;
}
`;
    const succeed =
      t.params.placement === 'func'
        ? d.pass.func
        : t.params.placement === 'params'
        ? d.pass.param
        : d.pass.ret;
    t.expectCompileResult(succeed, code);
  });

g.test('must_use_requires_return')
  .specURL('https://www.w3.org/TR/WGSL/#function-declaration-sec')
  .desc(`Tests the must_use attribute requires a return`)
  .params(u => u.combine('ret', [true, false]))
  .fn(t => {
    let ret = '';
    let ret_stmt = '';

    if (t.params.ret) {
      ret = '-> i32';
      ret_stmt = 'return 1;';
    }

    const code = `
@must_use
fn b() ${ret} {
  ${ret_stmt}
}
`;
    t.expectCompileResult(t.params.ret, code);
  });

g.test('overload')
  .specURL('https://www.w3.org/TR/WGSL/#function-declaration-sec')
  .desc(`Tests that user functions can not overload `)
  .params(u => u.combine('overload', [true, false]))
  .fn(t => {
    let code = 'fn a(f: i32) {}\n';

    if (t.params.overload) {
      code += 'fn a(f: u32) {}';
    }
    t.expectCompileResult(t.params.overload === false, code);
  });
