/*
 * Copyright 2016 WebAssembly Community Group participants
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include "validator.h"
#include "config.h"

#include <cassert>
#include <cinttypes>
#include <cstdarg>
#include <cstdio>

#include "binary-reader.h"
#include "type-checker.h"
#include "wast-parser-lexer-shared.h"

namespace wabt {

namespace {

enum class ActionResultKind {
  Error,
  Types,
  Type,
};

struct ActionResult {
  ActionResultKind kind;
  union {
    const TypeVector* types;
    Type type;
  };
};

struct Context {
  WABT_DISALLOW_COPY_AND_ASSIGN(Context);
  Context(SourceErrorHandler*, WastLexer*, const Script*);

  SourceErrorHandler* error_handler = nullptr;
  WastLexer* lexer = nullptr;
  const Script* script = nullptr;
  const Module* current_module = nullptr;
  const Func* current_func = nullptr;
  int current_table_index = 0;
  int current_memory_index = 0;
  int current_global_index = 0;
  int num_imported_globals = 0;
  TypeChecker typechecker;
  /* Cached for access by on_typechecker_error */
  const Location* expr_loc = nullptr;
  Result result = Result::Ok;
};

Context::Context(SourceErrorHandler* error_handler,
                 WastLexer* lexer,
                 const Script* script)
    : error_handler(error_handler), lexer(lexer), script(script) {}

}  // namespace

static void WABT_PRINTF_FORMAT(3, 4)
    print_error(Context* ctx, const Location* loc, const char* fmt, ...) {
  ctx->result = Result::Error;
  va_list args;
  va_start(args, fmt);
  wast_format_error(ctx->error_handler, loc, ctx->lexer, fmt, args);
  va_end(args);
}

static void on_typechecker_error(const char* msg, void* user_data) {
  Context* ctx = static_cast<Context*>(user_data);
  print_error(ctx, ctx->expr_loc, "%s", msg);
}

static bool is_power_of_two(uint32_t x) {
  return x && ((x & (x - 1)) == 0);
}

static uint32_t get_opcode_natural_alignment(Opcode opcode) {
  uint32_t memory_size = get_opcode_memory_size(opcode);
  assert(memory_size != 0);
  return memory_size;
}

static Result check_var(Context* ctx,
                        int max_index,
                        const Var* var,
                        const char* desc,
                        int* out_index) {
  assert(var->type == VarType::Index);
  if (var->index >= 0 && var->index < max_index) {
    if (out_index)
      *out_index = var->index;
    return Result::Ok;
  }
  print_error(ctx, &var->loc, "%s variable out of range (max %d)", desc,
              max_index);
  return Result::Error;
}

static Result check_func_var(Context* ctx,
                             const Var* var,
                             const Func** out_func) {
  int index;
  if (WABT_FAILED(check_var(ctx, ctx->current_module->funcs.size(), var,
                            "function", &index))) {
    return Result::Error;
  }

  if (out_func)
    *out_func = ctx->current_module->funcs[index];
  return Result::Ok;
}

static Result check_global_var(Context* ctx,
                               const Var* var,
                               const Global** out_global,
                               int* out_global_index) {
  int index;
  if (WABT_FAILED(check_var(ctx, ctx->current_module->globals.size(), var,
                            "global", &index))) {
    return Result::Error;
  }

  if (out_global)
    *out_global = ctx->current_module->globals[index];
  if (out_global_index)
    *out_global_index = index;
  return Result::Ok;
}

static Type get_global_var_type_or_any(Context* ctx, const Var* var) {
  const Global* global;
  if (WABT_SUCCEEDED(check_global_var(ctx, var, &global, nullptr)))
    return global->type;
  return Type::Any;
}

static Result check_func_type_var(Context* ctx,
                                  const Var* var,
                                  const FuncType** out_func_type) {
  int index;
  if (WABT_FAILED(check_var(ctx, ctx->current_module->func_types.size(), var,
                            "function type", &index))) {
    return Result::Error;
  }

  if (out_func_type)
    *out_func_type = ctx->current_module->func_types[index];
  return Result::Ok;
}

static Result check_table_var(Context* ctx,
                              const Var* var,
                              const Table** out_table) {
  int index;
  if (WABT_FAILED(check_var(ctx, ctx->current_module->tables.size(), var,
                            "table", &index))) {
    return Result::Error;
  }

  if (out_table)
    *out_table = ctx->current_module->tables[index];
  return Result::Ok;
}

static Result check_memory_var(Context* ctx,
                               const Var* var,
                               const Memory** out_memory) {
  int index;
  if (WABT_FAILED(check_var(ctx, ctx->current_module->memories.size(), var,
                            "memory", &index))) {
    return Result::Error;
  }

  if (out_memory)
    *out_memory = ctx->current_module->memories[index];
  return Result::Ok;
}

static Result check_local_var(Context* ctx, const Var* var, Type* out_type) {
  const Func* func = ctx->current_func;
  int max_index = get_num_params_and_locals(func);
  int index = get_local_index_by_var(func, var);
  if (index >= 0 && index < max_index) {
    if (out_type) {
      int num_params = get_num_params(func);
      if (index < num_params) {
        *out_type = get_param_type(func, index);
      } else {
        *out_type = ctx->current_func->local_types[index - num_params];
      }
    }
    return Result::Ok;
  }

  if (var->type == VarType::Name) {
    print_error(ctx, &var->loc,
                "undefined local variable \"" PRIstringslice "\"",
                WABT_PRINTF_STRING_SLICE_ARG(var->name));
  } else {
    print_error(ctx, &var->loc, "local variable out of range (max %d)",
                max_index);
  }
  return Result::Error;
}

static Type get_local_var_type_or_any(Context* ctx, const Var* var) {
  Type type = Type::Any;
  check_local_var(ctx, var, &type);
  return type;
}

static void check_align(Context* ctx,
                        const Location* loc,
                        uint32_t alignment,
                        uint32_t natural_alignment) {
  if (alignment != WABT_USE_NATURAL_ALIGNMENT) {
    if (!is_power_of_two(alignment))
      print_error(ctx, loc, "alignment must be power-of-two");
    if (alignment > natural_alignment) {
      print_error(ctx, loc,
                  "alignment must not be larger than natural alignment (%u)",
                  natural_alignment);
    }
  }
}

static void check_offset(Context* ctx, const Location* loc, uint64_t offset) {
  if (offset > UINT32_MAX) {
    print_error(ctx, loc, "offset must be less than or equal to 0xffffffff");
  }
}

static void check_type(Context* ctx,
                       const Location* loc,
                       Type actual,
                       Type expected,
                       const char* desc) {
  if (expected != actual) {
    print_error(ctx, loc, "type mismatch at %s. got %s, expected %s", desc,
                get_type_name(actual), get_type_name(expected));
  }
}

static void check_type_index(Context* ctx,
                             const Location* loc,
                             Type actual,
                             Type expected,
                             const char* desc,
                             int index,
                             const char* index_kind) {
  if (expected != actual && expected != Type::Any && actual != Type::Any) {
    print_error(ctx, loc, "type mismatch for %s %d of %s. got %s, expected %s",
                index_kind, index, desc, get_type_name(actual),
                get_type_name(expected));
  }
}

static void check_types(Context* ctx,
                        const Location* loc,
                        const TypeVector& actual,
                        const TypeVector& expected,
                        const char* desc,
                        const char* index_kind) {
  if (actual.size() == expected.size()) {
    for (size_t i = 0; i < actual.size(); ++i) {
      check_type_index(ctx, loc, actual[i], expected[i], desc, i, index_kind);
    }
  } else {
    print_error(ctx, loc, "expected %" PRIzd " %ss, got %" PRIzd,
                expected.size(), index_kind, actual.size());
  }
}

static void check_const_types(Context* ctx,
                              const Location* loc,
                              const TypeVector& actual,
                              const ConstVector& expected,
                              const char* desc) {
  if (actual.size() == expected.size()) {
    for (size_t i = 0; i < actual.size(); ++i) {
      check_type_index(ctx, loc, actual[i], expected[i].type, desc, i,
                       "result");
    }
  } else {
    print_error(ctx, loc, "expected %" PRIzd " results, got %" PRIzd,
                expected.size(), actual.size());
  }
}

static void check_const_type(Context* ctx,
                             const Location* loc,
                             Type actual,
                             const ConstVector& expected,
                             const char* desc) {
  TypeVector actual_types;
  if (actual != Type::Void)
    actual_types.push_back(actual);
  check_const_types(ctx, loc, actual_types, expected, desc);
}

static void check_assert_return_nan_type(Context* ctx,
                                         const Location* loc,
                                         Type actual,
                                         const char* desc) {
  /* when using assert_return_nan, the result can be either a f32 or f64 type
   * so we special case it here. */
  if (actual != Type::F32 && actual != Type::F64) {
    print_error(ctx, loc, "type mismatch at %s. got %s, expected f32 or f64",
                desc, get_type_name(actual));
  }
}

static void check_expr(Context* ctx, const Expr* expr);

static void check_expr_list(Context* ctx,
                            const Location* loc,
                            const Expr* first) {
  if (first) {
    for (const Expr* expr = first; expr; expr = expr->next)
      check_expr(ctx, expr);
  }
}

static void check_has_memory(Context* ctx, const Location* loc, Opcode opcode) {
  if (ctx->current_module->memories.size() == 0) {
    print_error(ctx, loc, "%s requires an imported or defined memory.",
                get_opcode_name(opcode));
  }
}

static void check_expr(Context* ctx, const Expr* expr) {
  ctx->expr_loc = &expr->loc;

  switch (expr->type) {
    case ExprType::Binary:
      typechecker_on_binary(&ctx->typechecker, expr->binary.opcode);
      break;

    case ExprType::Block:
      typechecker_on_block(&ctx->typechecker, &expr->block->sig);
      check_expr_list(ctx, &expr->loc, expr->block->first);
      typechecker_on_end(&ctx->typechecker);
      break;

    case ExprType::Br:
      typechecker_on_br(&ctx->typechecker, expr->br.var.index);
      break;

    case ExprType::BrIf:
      typechecker_on_br_if(&ctx->typechecker, expr->br_if.var.index);
      break;

    case ExprType::BrTable: {
      typechecker_begin_br_table(&ctx->typechecker);
      for (Var& var: *expr->br_table.targets) {
        typechecker_on_br_table_target(&ctx->typechecker, var.index);
      }
      typechecker_on_br_table_target(&ctx->typechecker,
                                     expr->br_table.default_target.index);
      typechecker_end_br_table(&ctx->typechecker);
      break;
    }

    case ExprType::Call: {
      const Func* callee;
      if (WABT_SUCCEEDED(check_func_var(ctx, &expr->call.var, &callee))) {
        typechecker_on_call(&ctx->typechecker, &callee->decl.sig.param_types,
                            &callee->decl.sig.result_types);
      }
      break;
    }

    case ExprType::CallIndirect: {
      const FuncType* func_type;
      if (ctx->current_module->tables.size() == 0) {
        print_error(ctx, &expr->loc,
                    "found call_indirect operator, but no table");
      }
      if (WABT_SUCCEEDED(
              check_func_type_var(ctx, &expr->call_indirect.var, &func_type))) {
        typechecker_on_call_indirect(&ctx->typechecker,
                                     &func_type->sig.param_types,
                                     &func_type->sig.result_types);
      }
      break;
    }

    case ExprType::Compare:
      typechecker_on_compare(&ctx->typechecker, expr->compare.opcode);
      break;

    case ExprType::Const:
      typechecker_on_const(&ctx->typechecker, expr->const_.type);
      break;

    case ExprType::Convert:
      typechecker_on_convert(&ctx->typechecker, expr->convert.opcode);
      break;

    case ExprType::Drop:
      typechecker_on_drop(&ctx->typechecker);
      break;

    case ExprType::GetGlobal:
      typechecker_on_get_global(
          &ctx->typechecker,
          get_global_var_type_or_any(ctx, &expr->get_global.var));
      break;

    case ExprType::GetLocal:
      typechecker_on_get_local(
          &ctx->typechecker,
          get_local_var_type_or_any(ctx, &expr->get_local.var));
      break;

    case ExprType::GrowMemory:
      check_has_memory(ctx, &expr->loc, Opcode::GrowMemory);
      typechecker_on_grow_memory(&ctx->typechecker);
      break;

    case ExprType::If:
      typechecker_on_if(&ctx->typechecker, &expr->if_.true_->sig);
      check_expr_list(ctx, &expr->loc, expr->if_.true_->first);
      if (expr->if_.false_) {
        typechecker_on_else(&ctx->typechecker);
        check_expr_list(ctx, &expr->loc, expr->if_.false_);
      }
      typechecker_on_end(&ctx->typechecker);
      break;

    case ExprType::Load:
      check_has_memory(ctx, &expr->loc, expr->load.opcode);
      check_align(ctx, &expr->loc, expr->load.align,
                  get_opcode_natural_alignment(expr->load.opcode));
      check_offset(ctx, &expr->loc, expr->load.offset);
      typechecker_on_load(&ctx->typechecker, expr->load.opcode);
      break;

    case ExprType::Loop:
      typechecker_on_loop(&ctx->typechecker, &expr->loop->sig);
      check_expr_list(ctx, &expr->loc, expr->loop->first);
      typechecker_on_end(&ctx->typechecker);
      break;

    case ExprType::CurrentMemory:
      check_has_memory(ctx, &expr->loc, Opcode::CurrentMemory);
      typechecker_on_current_memory(&ctx->typechecker);
      break;

    case ExprType::Nop:
      break;

    case ExprType::Return:
      typechecker_on_return(&ctx->typechecker);
      break;

    case ExprType::Select:
      typechecker_on_select(&ctx->typechecker);
      break;

    case ExprType::SetGlobal:
      typechecker_on_set_global(
          &ctx->typechecker,
          get_global_var_type_or_any(ctx, &expr->set_global.var));
      break;

    case ExprType::SetLocal:
      typechecker_on_set_local(
          &ctx->typechecker,
          get_local_var_type_or_any(ctx, &expr->set_local.var));
      break;

    case ExprType::Store:
      check_has_memory(ctx, &expr->loc, expr->store.opcode);
      check_align(ctx, &expr->loc, expr->store.align,
                  get_opcode_natural_alignment(expr->store.opcode));
      check_offset(ctx, &expr->loc, expr->store.offset);
      typechecker_on_store(&ctx->typechecker, expr->store.opcode);
      break;

    case ExprType::TeeLocal:
      typechecker_on_tee_local(
          &ctx->typechecker,
          get_local_var_type_or_any(ctx, &expr->tee_local.var));
      break;

    case ExprType::Unary:
      typechecker_on_unary(&ctx->typechecker, expr->unary.opcode);
      break;

    case ExprType::Unreachable:
      typechecker_on_unreachable(&ctx->typechecker);
      break;
  }
}

static void check_func_signature_matches_func_type(Context* ctx,
                                                   const Location* loc,
                                                   const FuncSignature& sig,
                                                   const FuncType* func_type) {
  check_types(ctx, loc, sig.result_types, func_type->sig.result_types,
              "function", "result");
  check_types(ctx, loc, sig.param_types, func_type->sig.param_types, "function",
              "argument");
}

static void check_func(Context* ctx, const Location* loc, const Func* func) {
  ctx->current_func = func;
  if (get_num_results(func) > 1) {
    print_error(ctx, loc, "multiple result values not currently supported.");
    /* don't run any other checks, the won't test the result_type properly */
    return;
  }
  if (decl_has_func_type(&func->decl)) {
    const FuncType* func_type;
    if (WABT_SUCCEEDED(
            check_func_type_var(ctx, &func->decl.type_var, &func_type))) {
      check_func_signature_matches_func_type(ctx, loc, func->decl.sig,
                                             func_type);
    }
  }

  ctx->expr_loc = loc;
  typechecker_begin_function(&ctx->typechecker, &func->decl.sig.result_types);
  check_expr_list(ctx, loc, func->first_expr);
  typechecker_end_function(&ctx->typechecker);
  ctx->current_func = nullptr;
}

static void print_const_expr_error(Context* ctx,
                                   const Location* loc,
                                   const char* desc) {
  print_error(ctx, loc,
              "invalid %s, must be a constant expression; either *.const or "
              "get_global.",
              desc);
}

static void check_const_init_expr(Context* ctx,
                                  const Location* loc,
                                  const Expr* expr,
                                  Type expected_type,
                                  const char* desc) {
  Type type = Type::Void;
  if (expr) {
    if (expr->next) {
      print_const_expr_error(ctx, loc, desc);
      return;
    }

    switch (expr->type) {
      case ExprType::Const:
        type = expr->const_.type;
        break;

      case ExprType::GetGlobal: {
        const Global* ref_global = nullptr;
        int ref_global_index;
        if (WABT_FAILED(check_global_var(ctx, &expr->get_global.var,
                                         &ref_global, &ref_global_index))) {
          return;
        }

        type = ref_global->type;
        /* globals can only reference previously defined, internal globals */
        if (ref_global_index >= ctx->current_global_index) {
          print_error(ctx, loc,
                      "initializer expression can only reference a previously "
                      "defined global");
        } else if (ref_global_index >= ctx->num_imported_globals) {
          print_error(
              ctx, loc,
              "initializer expression can only reference an imported global");
        }

        if (ref_global->mutable_) {
          print_error(
              ctx, loc,
              "initializer expression cannot reference a mutable global");
        }
        break;
      }

      default:
        print_const_expr_error(ctx, loc, desc);
        return;
    }
  }

  check_type(ctx, expr ? &expr->loc : loc, type, expected_type, desc);
}

static void check_global(Context* ctx,
                         const Location* loc,
                         const Global* global) {
  check_const_init_expr(ctx, loc, global->init_expr, global->type,
                        "global initializer expression");
}

static void check_limits(Context* ctx,
                         const Location* loc,
                         const Limits* limits,
                         uint64_t absolute_max,
                         const char* desc) {
  if (limits->initial > absolute_max) {
    print_error(ctx, loc, "initial %s (%" PRIu64 ") must be <= (%" PRIu64 ")",
                desc, limits->initial, absolute_max);
  }

  if (limits->has_max) {
    if (limits->max > absolute_max) {
      print_error(ctx, loc, "max %s (%" PRIu64 ") must be <= (%" PRIu64 ")",
                  desc, limits->max, absolute_max);
    }

    if (limits->max < limits->initial) {
      print_error(ctx, loc,
                  "max %s (%" PRIu64 ") must be >= initial %s (%" PRIu64 ")",
                  desc, limits->max, desc, limits->initial);
    }
  }
}

static void check_table(Context* ctx, const Location* loc, const Table* table) {
  if (ctx->current_table_index == 1)
    print_error(ctx, loc, "only one table allowed");
  check_limits(ctx, loc, &table->elem_limits, UINT32_MAX, "elems");
}

static void check_elem_segments(Context* ctx, const Module* module) {
  for (ModuleField* field = module->first_field; field; field = field->next) {
    if (field->type != ModuleFieldType::ElemSegment)
      continue;

    ElemSegment* elem_segment = field->elem_segment;
    const Table* table;
    if (!WABT_SUCCEEDED(check_table_var(ctx, &elem_segment->table_var, &table)))
      continue;

    for (const Var& var: elem_segment->vars) {
      if (!WABT_SUCCEEDED(check_func_var(ctx, &var, nullptr)))
        continue;
    }

    check_const_init_expr(ctx, &field->loc, elem_segment->offset, Type::I32,
                          "elem segment offset");
  }
}

static void check_memory(Context* ctx,
                         const Location* loc,
                         const Memory* memory) {
  if (ctx->current_memory_index == 1)
    print_error(ctx, loc, "only one memory block allowed");
  check_limits(ctx, loc, &memory->page_limits, WABT_MAX_PAGES, "pages");
}

static void check_data_segments(Context* ctx, const Module* module) {
  for (ModuleField* field = module->first_field; field; field = field->next) {
    if (field->type != ModuleFieldType::DataSegment)
      continue;

    DataSegment* data_segment = field->data_segment;
    const Memory* memory;
    if (!WABT_SUCCEEDED(
            check_memory_var(ctx, &data_segment->memory_var, &memory)))
      continue;

    check_const_init_expr(ctx, &field->loc, data_segment->offset, Type::I32,
                          "data segment offset");
  }
}

static void check_import(Context* ctx,
                         const Location* loc,
                         const Import* import) {
  switch (import->kind) {
    case ExternalKind::Func:
      if (decl_has_func_type(&import->func->decl))
        check_func_type_var(ctx, &import->func->decl.type_var, nullptr);
      break;
    case ExternalKind::Table:
      check_table(ctx, loc, import->table);
      ctx->current_table_index++;
      break;
    case ExternalKind::Memory:
      check_memory(ctx, loc, import->memory);
      ctx->current_memory_index++;
      break;
    case ExternalKind::Global:
      if (import->global->mutable_) {
        print_error(ctx, loc, "mutable globals cannot be imported");
      }
      ctx->num_imported_globals++;
      ctx->current_global_index++;
      break;
  }
}

static void check_export(Context* ctx, const Export* export_) {
  switch (export_->kind) {
    case ExternalKind::Func:
      check_func_var(ctx, &export_->var, nullptr);
      break;
    case ExternalKind::Table:
      check_table_var(ctx, &export_->var, nullptr);
      break;
    case ExternalKind::Memory:
      check_memory_var(ctx, &export_->var, nullptr);
      break;
    case ExternalKind::Global: {
      const Global* global;
      if (WABT_SUCCEEDED(
              check_global_var(ctx, &export_->var, &global, nullptr))) {
        if (global->mutable_) {
          print_error(ctx, &export_->var.loc,
                      "mutable globals cannot be exported");
        }
      }
      break;
    }
  }
}

static void on_duplicate_binding(const BindingHash::value_type& a,
                                 const BindingHash::value_type& b,
                                 void* user_data) {
  Context* ctx = static_cast<Context*>(user_data);
  /* choose the location that is later in the file */
  const Location& a_loc = a.second.loc;
  const Location& b_loc = b.second.loc;
  const Location& loc = a_loc.line > b_loc.line ? a_loc : b_loc;
  print_error(ctx, &loc, "redefinition of export \"%s\"", a.first.c_str());
}

static void check_duplicate_export_bindings(Context* ctx,
                                            const Module* module) {
  module->export_bindings.find_duplicates(on_duplicate_binding, ctx);
}

static void check_module(Context* ctx, const Module* module) {
  bool seen_start = false;

  ctx->current_module = module;
  ctx->current_table_index = 0;
  ctx->current_memory_index = 0;
  ctx->current_global_index = 0;
  ctx->num_imported_globals = 0;

  for (ModuleField* field = module->first_field; field; field = field->next) {
    switch (field->type) {
      case ModuleFieldType::Func:
        check_func(ctx, &field->loc, field->func);
        break;

      case ModuleFieldType::Global:
        check_global(ctx, &field->loc, field->global);
        ctx->current_global_index++;
        break;

      case ModuleFieldType::Import:
        check_import(ctx, &field->loc, field->import);
        break;

      case ModuleFieldType::Export:
        check_export(ctx, field->export_);
        break;

      case ModuleFieldType::Table:
        check_table(ctx, &field->loc, field->table);
        ctx->current_table_index++;
        break;

      case ModuleFieldType::ElemSegment:
        /* checked below */
        break;

      case ModuleFieldType::Memory:
        check_memory(ctx, &field->loc, field->memory);
        ctx->current_memory_index++;
        break;

      case ModuleFieldType::DataSegment:
        /* checked below */
        break;

      case ModuleFieldType::FuncType:
        break;

      case ModuleFieldType::Start: {
        if (seen_start) {
          print_error(ctx, &field->loc, "only one start function allowed");
        }

        const Func* start_func = nullptr;
        check_func_var(ctx, &field->start, &start_func);
        if (start_func) {
          if (get_num_params(start_func) != 0) {
            print_error(ctx, &field->loc, "start function must be nullary");
          }

          if (get_num_results(start_func) != 0) {
            print_error(ctx, &field->loc,
                        "start function must not return anything");
          }
        }
        seen_start = true;
        break;
      }
    }
  }

  check_elem_segments(ctx, module);
  check_data_segments(ctx, module);
  check_duplicate_export_bindings(ctx, module);
}

/* returns the result type of the invoked function, checked by the caller;
 * returning nullptr means that another error occured first, so the result type
 * should be ignored. */
static const TypeVector* check_invoke(Context* ctx, const Action* action) {
  const ActionInvoke* invoke = action->invoke;
  const Module* module = get_module_by_var(ctx->script, &action->module_var);
  if (!module) {
    print_error(ctx, &action->loc, "unknown module");
    return nullptr;
  }

  Export* export_ = get_export_by_name(module, &action->name);
  if (!export_) {
    print_error(ctx, &action->loc,
                "unknown function export \"" PRIstringslice "\"",
                WABT_PRINTF_STRING_SLICE_ARG(action->name));
    return nullptr;
  }

  Func* func = get_func_by_var(module, &export_->var);
  if (!func) {
    /* this error will have already been reported, just skip it */
    return nullptr;
  }

  size_t actual_args = invoke->args.size();
  size_t expected_args = get_num_params(func);
  if (expected_args != actual_args) {
    print_error(ctx, &action->loc, "too %s parameters to function. got %" PRIzd
                                   ", expected %" PRIzd,
                actual_args > expected_args ? "many" : "few", actual_args,
                expected_args);
    return nullptr;
  }
  for (size_t i = 0; i < actual_args; ++i) {
    const Const* const_ = &invoke->args[i];
    check_type_index(ctx, &const_->loc, const_->type, get_param_type(func, i),
                     "invoke", i, "argument");
  }

  return &func->decl.sig.result_types;
}

static Result check_get(Context* ctx, const Action* action, Type* out_type) {
  const Module* module = get_module_by_var(ctx->script, &action->module_var);
  if (!module) {
    print_error(ctx, &action->loc, "unknown module");
    return Result::Error;
  }

  Export* export_ = get_export_by_name(module, &action->name);
  if (!export_) {
    print_error(ctx, &action->loc,
                "unknown global export \"" PRIstringslice "\"",
                WABT_PRINTF_STRING_SLICE_ARG(action->name));
    return Result::Error;
  }

  Global* global = get_global_by_var(module, &export_->var);
  if (!global) {
    /* this error will have already been reported, just skip it */
    return Result::Error;
  }

  *out_type = global->type;
  return Result::Ok;
}

static ActionResult check_action(Context* ctx, const Action* action) {
  ActionResult result;
  WABT_ZERO_MEMORY(result);

  switch (action->type) {
    case ActionType::Invoke:
      result.types = check_invoke(ctx, action);
      result.kind =
          result.types ? ActionResultKind::Types : ActionResultKind::Error;
      break;

    case ActionType::Get:
      if (WABT_SUCCEEDED(check_get(ctx, action, &result.type)))
        result.kind = ActionResultKind::Type;
      else
        result.kind = ActionResultKind::Error;
      break;
  }

  return result;
}

static void check_assert_return_nan_command(Context* ctx,
                                            const Action* action) {
  ActionResult result = check_action(ctx, action);

  /* a valid result type will either be f32 or f64; convert a TYPES result
   * into a TYPE result, so it is easier to check below. Type::Any is
   * used to specify a type that should not be checked (because an earlier
   * error occurred). */
  if (result.kind == ActionResultKind::Types) {
    if (result.types->size() == 1) {
      result.kind = ActionResultKind::Type;
      result.type = (*result.types)[0];
    } else {
      print_error(ctx, &action->loc, "expected 1 result, got %" PRIzd,
                  result.types->size());
      result.type = Type::Any;
    }
  }

  if (result.kind == ActionResultKind::Type && result.type != Type::Any)
    check_assert_return_nan_type(ctx, &action->loc, result.type, "action");
}

static void check_command(Context* ctx, const Command* command) {
  switch (command->type) {
    case CommandType::Module:
      check_module(ctx, command->module);
      break;

    case CommandType::Action:
      /* ignore result type */
      check_action(ctx, command->action);
      break;

    case CommandType::Register:
    case CommandType::AssertMalformed:
    case CommandType::AssertInvalid:
    case CommandType::AssertInvalidNonBinary:
    case CommandType::AssertUnlinkable:
    case CommandType::AssertUninstantiable:
      /* ignore */
      break;

    case CommandType::AssertReturn: {
      const Action* action = command->assert_return.action;
      ActionResult result = check_action(ctx, action);
      switch (result.kind) {
        case ActionResultKind::Types:
          check_const_types(ctx, &action->loc, *result.types,
                            *command->assert_return.expected, "action");
          break;

        case ActionResultKind::Type:
          check_const_type(ctx, &action->loc, result.type,
                           *command->assert_return.expected, "action");
          break;

        case ActionResultKind::Error:
          /* error occurred, don't do any further checks */
          break;
      }
      break;
    }

    case CommandType::AssertReturnCanonicalNan:
      check_assert_return_nan_command(
          ctx, command->assert_return_canonical_nan.action);
      break;

    case CommandType::AssertReturnArithmeticNan:
      check_assert_return_nan_command(
          ctx, command->assert_return_arithmetic_nan.action);
      break;

    case CommandType::AssertTrap:
    case CommandType::AssertExhaustion:
      /* ignore result type */
      check_action(ctx, command->assert_trap.action);
      break;
  }
}

Result validate_script(WastLexer* lexer,
                       const struct Script* script,
                       SourceErrorHandler* error_handler) {
  Context ctx(error_handler, lexer, script);

  TypeCheckerErrorHandler tc_error_handler;
  tc_error_handler.on_error = on_typechecker_error;
  tc_error_handler.user_data = &ctx;
  ctx.typechecker.error_handler = &tc_error_handler;

  for (const std::unique_ptr<Command>& command : script->commands)
    check_command(&ctx, command.get());
  return ctx.result;
}

}  // namespace wabt
