///|
suberror MakeError {
  MakeError(String)
}

///|
struct Variable {
  value : String
  immediate : Bool
}

///|
struct Rule {
  target : String
  dependencies : Array[String]
  groups : Array[Prerequisites]
  recipes : Array[String]
  mut phony : Bool
  mut stem : String?
}

///|
struct Database {
  cwd : String
  variables : Map[String, Variable]
  command_line_variables : Map[String, Bool]
  exported : ExportPolicy
  rules : Map[String, Rule]
  mut default_target : String?
  mut secondary_expansion : Bool
  mut invocation_level : Int32
  mut serial_children : Bool
}

///|
struct Settings {
  jobs : Int32
  jobserver : Jobserver?
  expansion_lock : @async.Mutex
  cwd : String
  silent : Bool
  dry_run : Bool
  always_make : Bool
  keep_going : Bool
  what_if : Map[String, Bool]
}

///|
async fn conditional_value(line : String, database : Database) -> Bool {
  if line.has_prefix("ifdef ") {
    let name = line[6:].trim().to_owned()
    let value = lookup_variable(
      name,
      database.variables,
      {},
      0,
      cwd=database.cwd,
      exported=database.exported,
      state=database.expansion(),
    )
    value != ""
  } else if line.has_prefix("ifndef ") {
    let name = line[7:].trim().to_owned()
    let value = lookup_variable(
      name,
      database.variables,
      {},
      0,
      cwd=database.cwd,
      exported=database.exported,
      state=database.expansion(),
    )
    value == ""
  } else {
    let negated = line.has_prefix("ifneq ")
    let prefix = if negated { "ifneq " } else { "ifeq " }
    if !line.has_prefix(prefix) {
      false
    } else {
      let rest = line[prefix.length():].trim()
      let rest = if rest.has_prefix("(") && rest.has_suffix(")") {
        rest[1:rest.length() - 1].to_owned()
      } else {
        rest.to_owned()
      }
      let parts : Array[String] = rest
        .split(",")
        .map(x => x.trim().to_owned())
        .collect()
      let equal = if parts.length() == 2 {
        let left = expand(
          parts[0],
          database.variables,
          {},
          0,
          cwd=database.cwd,
          exported=database.exported,
          state=database.expansion(),
        )
        let right = expand(
          parts[1],
          database.variables,
          {},
          0,
          cwd=database.cwd,
          exported=database.exported,
          state=database.expansion(),
        )
        left == right
      } else {
        false
      }
      if negated {
        !equal
      } else {
        equal
      }
    }
  }
}

///|
fn valid_variable_name(name : String) -> Bool {
  if name == "" {
    return false
  }
  for char in name {
    if char == ' ' || char == '\t' || char == ':' || char == '#' {
      return false
    }
  }
  true
}

///|
fn remove_comment(line : String) -> String {
  let output = StringBuilder()
  let mut escaped = false
  for char in line {
    if char == '#' && !escaped {
      break
    }
    if escaped && char != '#' {
      output.write_char('\\')
    }
    if char == '\\' {
      escaped = !escaped
    } else {
      output.write_char(char)
      escaped = false
    }
  }
  if escaped {
    output.write_char('\\')
  }
  output.to_string()
}

///|
fn split_words(value : String) -> Array[String] {
  let result : Array[String] = []
  let word = StringBuilder()
  for char in value {
    if char.is_whitespace() {
      if !word.is_empty() {
        result.push(word.to_string())
        word.reset()
      }
    } else {
      word.write_char(char)
    }
  }
  if !word.is_empty() {
    result.push(word.to_string())
  }
  result
}

///|
async fn lookup_variable(
  name : String,
  variables : Map[String, Variable],
  automatic : Map[String, String],
  depth : Int32,
  cwd~ : String = @env.current_dir().unwrap_or("."),
  exported~ : ExportPolicy = ExportPolicy::new([]),
  state~ : Expansion = Expansion::new(),
) -> String {
  if automatic.get(name) is Some(value) {
    return value
  }
  match variables.get(name) {
    Some({ value, immediate: true, }) => value
    Some({ value, immediate: false, }) => {
      if state.active.contains(name) {
        if state.environment_depth > 0 {
          return state.original.get(name).unwrap_or("")
        }
        raise MakeError("recursive variable '\{name}' references itself")
      }
      state.active.add(name)
      defer state.active.remove(name)
      expand(value, variables, automatic, depth + 1, cwd~, exported~, state~)
    }
    None => ""
  }
}

///|
async fn expand(
  value : String,
  variables : Map[String, Variable],
  automatic : Map[String, String],
  depth : Int32,
  cwd~ : String = @env.current_dir().unwrap_or("."),
  exported~ : ExportPolicy = ExportPolicy::new([]),
  state~ : Expansion = Expansion::new(),
) -> String {
  if depth > 64 {
    raise MakeError("variable expansion exceeds the recursion limit")
  }
  let chars : Array[Char] = value.iter().collect()
  let output = StringBuilder()
  let mut index = 0
  while index < chars.length() {
    if chars[index] != '$' {
      output.write_char(chars[index])
      index += 1
      continue
    }
    if index + 1 >= chars.length() {
      output.write_char('$')
      break
    }
    match chars[index + 1] {
      '$' => {
        output.write_char('$')
        index += 2
      }
      '(' | '{' as open => {
        let close = if open == '(' { ')' } else { '}' }
        let mut end = index + 2
        let mut nesting = 1
        while end < chars.length() {
          if chars[end] == open {
            nesting += 1
          }
          if chars[end] == close {
            nesting -= 1
            if nesting == 0 {
              break
            }
          }
          end += 1
        }
        if end >= chars.length() {
          raise MakeError("unterminated variable reference")
        }
        let name = String::from_array(chars[index + 2:end])
        reject_unsupported_function(name)
        if make_function_name(name) == Some("shell") {
          let script = expand(
            name[6:].to_owned(),
            variables,
            automatic,
            depth + 1,
            cwd~,
            exported~,
            state~,
          )
          output.write_string(
            shell_result(
              script,
              variables,
              automatic,
              depth,
              cwd,
              exported,
              state,
              trim_newlines=true,
            ),
          )
        } else {
          let variable = expand(
            name,
            variables,
            automatic,
            depth + 1,
            cwd~,
            exported~,
            state~,
          )
          if variable.contains(":") && variable.contains("=") {
            raise MakeError(
              "substitution references are outside the supported make profile",
            )
          }
          output.write_string(
            lookup_variable(
              variable,
              variables,
              automatic,
              depth,
              cwd~,
              exported~,
              state~,
            ),
          )
        }
        index = end + 1
      }
      char => {
        output.write_string(
          lookup_variable(
            char.to_string(),
            variables,
            automatic,
            depth,
            cwd~,
            exported~,
            state~,
          ),
        )
        index += 2
      }
    }
  }
  output.to_string()
}

///|
fn pattern_match(pattern : String, target : String) -> String? {
  guard pattern.split_once("%") is Some((prefix, suffix)) else { return None }
  if target.has_prefix(prefix) &&
    target.has_suffix(suffix) &&
    target.length() >= prefix.length() + suffix.length() {
    Some(target[prefix.length():target.length() - suffix.length()].to_owned())
  } else {
    None
  }
}

///|
fn instantiate_pattern(rule : Rule, target : String) -> Rule? {
  guard rule.target.contains("%") else { return None }
  guard pattern_match(rule.target, target) is Some(stem) else { return None }
  Some({
    target,
    dependencies: rule.dependencies.map(dep => replace_stem(dep, stem)),
    groups: rule.groups.map(group => {
      ..group,
      text: replace_stems(group.text, stem),
    }),
    recipes: rule.recipes.copy(),
    phony: rule.phony,
    stem: Some(stem),
  })
}

///|
fn trim_make_space_start(text : String) -> String {
  let mut start = 0
  while start < text.length() &&
        text[start] is (' ' | '\t' | '\r' | '\n' | '\u000b' | '\u000c') {
    start += 1
  }
  text[start:].to_owned()
}

///|
fn parse_assignment(line : String) -> (String, String, String)? {
  guard split_rule_separator(line, '=') is Some((left, right)) else {
    return None
  }
  let mut left = left
  let mut operator = "="
  for prefix in [":::", "::", ":", "!", "?", "+"] {
    if left.has_suffix(prefix) {
      left = left[:left.length() - prefix.length()].to_owned()
      operator = prefix + "="
      break
    }
  }
  let mut start = 0
  let mut end = left.length()
  while start < end && left[start] is (' ' | '\t') {
    start += 1
  }
  while end > start && left[end - 1] is (' ' | '\t') {
    end -= 1
  }
  let name = left[start:end].to_owned()
  // An assignment operator may contain colons; after removing it, any outer
  // colon belongs to a rule. Its recipe must not become a computed LHS.
  if split_rule_separator(name, ':') is Some(_) ||
    split_rule_separator(name, ' ') is Some(_) ||
    split_rule_separator(name, '\t') is Some(_) {
    return None
  }
  if valid_variable_name(name) || name.contains("$(") || name.contains("${") {
    Some((name, operator, trim_make_space_start(right)))
  } else {
    None
  }
}

///|
async fn assign_variable(
  database : Database,
  name : String,
  operator : String,
  value : String,
) -> Unit {
  let state = database.expansion()
  let name = expand(
    name,
    database.variables,
    {},
    0,
    cwd=database.cwd,
    exported=database.exported,
    state~,
  )
  assign_resolved_variable(database, name, operator, value)
}

///|
async fn assign_resolved_variable(
  database : Database,
  name : String,
  operator : String,
  value : String,
) -> Unit {
  let state = database.expansion()
  if !valid_variable_name(name) {
    raise MakeError("invalid expanded variable name '\{name}'")
  }
  if database.command_line_variables.contains(name) {
    return
  }
  let variables = database.variables
  if operator == "?=" && variables.contains(name) {
    return
  }
  validate_special_assignment(name, operator, value)
  match operator {
    "!=" => {
      let script = expand(
        value,
        variables,
        {},
        0,
        cwd=database.cwd,
        exported=database.exported,
        state~,
      )
      variables[name] = {
        value: shell_result(
          script,
          variables,
          {},
          0,
          database.cwd,
          database.exported,
          state,
          trim_newlines=false,
        ),
        immediate: false,
      }
    }
    "?=" =>
      if !variables.contains(name) {
        variables[name] = { value, immediate: false, }
      }
    ":=" | "::=" | ":::=" => {
      let expanded = expand(
        value,
        variables,
        {},
        0,
        cwd=database.cwd,
        exported=database.exported,
        state~,
      )
      variables[name] = {
        value: if operator == ":::=" {
          expanded.replace_all(old="$", new="$$")
        } else {
          expanded
        },
        immediate: operator != ":::=",
      }
    }
    "+=" =>
      match variables.get(name) {
        Some(previous) => {
          let separator = if previous.value == "" || value == "" {
            ""
          } else {
            " "
          }
          variables[name] = {
            value: previous.value +
            separator +
            (if previous.immediate {
              expand(
                value,
                variables,
                {},
                0,
                cwd=database.cwd,
                exported=database.exported,
                state~,
              )
            } else {
              value
            }),
            immediate: previous.immediate,
          }
        }
        None => variables[name] = { value, immediate: false, }
      }
    _ => variables[name] = { value, immediate: false, }
  }
  database.exported.defaults.remove(name)
}

///|
fn logical_lines(contents : String) -> Array[String] raise MakeError {
  let result : Array[String] = []
  let pending = StringBuilder()
  for raw_line in contents.split("\n") {
    let line = if raw_line.has_suffix("\r") {
      raw_line[:raw_line.length() - 1].to_owned()
    } else {
      raw_line.to_owned()
    }
    if line.has_prefix("\t") {
      if !pending.is_empty() {
        raise MakeError("recipe encountered during a continued logical line")
      }
      result.push(line)
    } else if line.has_suffix("\\") {
      pending.write_string(line[:line.length() - 1].to_owned())
      pending.write_char(' ')
    } else if pending.is_empty() {
      result.push(line)
    } else {
      pending.write_string(line)
      result.push(pending.to_string())
      pending.reset()
    }
  }
  if !pending.is_empty() {
    raise MakeError("Makefile ends with a continued line")
  }
  result
}

///|
async fn parse_makefile(
  contents : String,
  database : Database,
  source_directory : String,
  depth : Int32,
) -> Unit {
  if depth > 64 {
    raise MakeError("include nesting exceeds the recursion limit")
  }
  let mut current_rules : Array[Rule] = []
  let mut current_groups : Array[Prerequisites] = []
  let active : Array[Bool] = [true]
  for line_number, source_line in logical_lines(contents) {
    let directive = remove_comment(source_line).trim().to_owned()
    if directive.has_prefix("ifeq ") ||
      directive.has_prefix("ifneq ") ||
      directive.has_prefix("ifdef ") ||
      directive.has_prefix("ifndef ") {
      active.push(
        active.last().unwrap_or(false) && conditional_value(directive, database),
      )
      current_rules = []
      continue
    }
    if directive == "else" {
      if active.length() <= 1 {
        raise MakeError("unexpected else")
      }
      let previous = active.pop().unwrap()
      active.push(active.last().unwrap_or(false) && !previous)
      current_rules = []
      continue
    }
    if directive == "endif" {
      if active.length() <= 1 {
        raise MakeError("unexpected endif")
      }
      ignore(active.pop())
      current_rules = []
      continue
    }
    if !active.last().unwrap_or(false) {
      continue
    }
    if source_line.has_prefix("\t") {
      if current_rules.is_empty() {
        raise MakeError("recipe without a target at line \{line_number + 1}")
      }
      let recipe = source_line[1:].to_owned()
      for index, rule in current_rules {
        add_recipe(rule, current_groups[index], recipe)
      }
      continue
    }
    current_rules = []
    current_groups = []
    let line = trim_make_space_start(remove_comment(source_line))
    if line.trim().is_empty() {
      continue
    }
    if line.has_prefix("include ") ||
      line.has_prefix("-include ") ||
      line.has_prefix("sinclude ") {
      let optional = line.has_prefix("-include ") ||
        line.has_prefix("sinclude ")
      let names = if line.has_prefix("-include ") {
        line[9:].to_owned()
      } else if line.has_prefix("sinclude ") {
        line[9:].to_owned()
      } else {
        line[8:].to_owned()
      }
      let expanded = expand(
        names,
        database.variables,
        {},
        0,
        cwd=database.cwd,
        exported=database.exported,
        state=database.expansion(),
      )
      for name in split_words(expanded) {
        let included = path_in(source_directory, name)
        if @fs.exists(included) {
          parse_makefile(
            @fs.read_file(included).text(),
            database,
            @path.Path(included).dirname().to_string(),
            depth + 1,
          )
        } else if !optional {
          raise MakeError("included makefile not found: \{name}")
        }
      }
      continue
    }
    if export_directive(line) is Some((exporting, contents)) {
      if contents.is_empty() {
        database.exported.all = exporting
        continue
      }
      match export_assignment(contents, exporting) {
        Some((name, operator, value, exporting)) => {
          let name = expand(
            name,
            database.variables,
            {},
            0,
            cwd=database.cwd,
            exported=database.exported,
            state=database.expansion(),
          )
          assign_resolved_variable(database, name, operator, value)
          mark_export(database, name, exporting)
        }
        _ =>
          for
            name in split_words(
              expand(
                contents,
                database.variables,
                {},
                0,
                cwd=database.cwd,
                exported=database.exported,
                state=database.expansion(),
              ),
            ) {
            mark_export(database, name, exporting)
          }
      }
      continue
    }
    if parse_assignment(line) is Some((name, operator, value)) {
      assign_variable(database, name, operator, value)
      continue
    }
    let (rule_line, inline_recipe) = split_inline_recipe(source_line)
    let rule_line = remove_comment(rule_line).trim_start().to_owned()
    guard split_rule_separator(rule_line, ':')
      is Some((targets_text, deps_text)) else {
      raise MakeError(
        "expected a rule or assignment at line \{line_number + 1}",
      )
    }
    if deps_text.has_prefix(":") {
      raise MakeError(
        "double-colon rules are outside the supported make profile",
      )
    }
    if parse_assignment(deps_text.trim_start().to_owned()) is Some(_) {
      raise MakeError(
        "target-specific variables are outside the supported make profile",
      )
    }
    if targets_text.trim_end().has_suffix("&") {
      raise MakeError("grouped targets are outside the supported make profile")
    }
    if split_rule_separator(deps_text, '|') is Some(_) {
      raise MakeError(
        "order-only prerequisites are outside the supported make profile",
      )
    }
    let static_pattern = split_rule_separator(deps_text, ':')
    let prerequisite_text = match static_pattern {
      Some((pattern, prerequisites)) if pattern.trim().contains("%") =>
        prerequisites
      _ => deps_text
    }
    let targets = split_words(
      expand(
        targets_text,
        database.variables,
        {},
        0,
        cwd=database.cwd,
        exported=database.exported,
        state=database.expansion(),
      ),
    )
    validate_rule_words(targets, [])
    let dependency_text = expand(
      prerequisite_text,
      database.variables,
      {},
      0,
      cwd=database.cwd,
      exported=database.exported,
      state=database.expansion(),
    )
    let dependencies = split_words(dependency_text)
    validate_rule_words(targets, dependencies)
    if targets.is_empty() {
      raise MakeError("rule has no target at line \{line_number + 1}")
    }
    if targets == [".SECONDEXPANSION"] {
      database.secondary_expansion = true
      continue
    }
    if targets == [".PHONY"] {
      for target in dependencies {
        match database.rules.get(target) {
          Some(rule) => rule.phony = true
          None =>
            database.rules[target] = {
              target,
              dependencies: [],
              groups: [],
              recipes: [],
              phony: true,
              stem: None,
            }
        }
      }
      continue
    }
    for target in targets {
      if target == ".EXPORT_ALL_VARIABLES" {
        database.exported.target_all = true
      }
      let (dependencies, stem) = match static_pattern {
        Some((pattern, _)) if pattern.trim().contains("%") => {
          let pattern = pattern.trim().to_owned()
          match pattern_match(pattern, target) {
            Some(stem) =>
              (dependencies.map(dep => replace_stem(dep, stem)), Some(stem))
            None => (dependencies.copy(), None)
          }
        }
        _ => (dependencies.copy(), None)
      }
      let group : Prerequisites = {
        text: match stem {
          Some(stem) => replace_stems(dependency_text, stem)
          None => dependency_text
        },
        secondary: database.secondary_expansion && dependency_text.contains("$"),
        has_recipe: false,
        stem,
      }
      let rule = match database.rules.get(target) {
        Some(existing) => {
          existing.dependencies.append(dependencies)
          existing.groups.push(group)
          if stem is Some(value) {
            existing.stem = Some(value)
          }
          existing
        }
        None => {
          let created : Rule = {
            target,
            dependencies: dependencies.copy(),
            groups: [group],
            recipes: [],
            phony: false,
            stem,
          }
          database.rules[target] = created
          created
        }
      }
      if database.default_target is None && !target.has_prefix(".") {
        database.default_target = Some(target)
      }
      current_rules.push(rule)
      current_groups.push(group)
      if inline_recipe is Some(recipe) {
        add_recipe(rule, group, recipe)
      }
    }
  }
  if active.length() != 1 {
    raise MakeError("missing endif")
  }
}

///|
fn path_in(cwd : String, path : String) -> String {
  if path == "" || @path.Path(path).is_absolute() {
    path
  } else {
    // Path.join would erase missing components and symlink-parent traversal.
    cwd + "/" + path
  }
}

///|
async fn newer(left : String, right : String) -> Bool {
  let (left_s, left_ns) = @fs.mtime(left)
  let (right_s, right_ns) = @fs.mtime(right)
  left_s > right_s || (left_s == right_s && left_ns > right_ns)
}

///|
async fn command_environment(
  database : Database,
  automatic~ : Map[String, String] = Map([]),
) -> Map[String, String] {
  let result : Map[String, String] = Map([])
  let state = database.expansion()
  if state.original.get("SHELL") is Some(shell) {
    result["SHELL"] = shell
  }
  for name in database.exported.names(database.variables) {
    if database.variables.contains(name) {
      result[name] = lookup_variable(
        name,
        database.variables,
        automatic,
        0,
        cwd=database.cwd,
        exported=database.exported,
        state~,
      )
    }
  }
  result["MAKELEVEL"] = (database.invocation_level + 1).to_string()
  if database.serial_children && result.get("MAKEFLAGS") is Some(flags) {
    result["MAKEFLAGS"] = serial_child_flags(flags)
  }
  if result.get("MAKEFLAGS") is Some(flags) {
    result["MAKEFLAGS"] = flags.replace_all(old="$", new="$$")
  }
  result
}

///|
async fn choose_makefile(cwd : String, requested : String?) -> String {
  match requested {
    Some(path) => {
      let resolved = path_in(cwd, path)
      if !@fs.exists(resolved) {
        raise MakeError("Makefile not found: \{path}")
      }
      resolved
    }
    None => {
      for candidate in ["GNUmakefile", "makefile", "Makefile"] {
        let resolved = path_in(cwd, candidate)
        if @fs.exists(resolved) {
          return resolved
        }
      }
      raise MakeError("no Makefile found")
    }
  }
}

///|
fn positive_job_count(value : String) -> Bool {
  if value.is_empty() || value.iter().any(char => char < '0' || char > '9') {
    return false
  }
  let count = @string.parse_int(value) catch { _ => return false }
  count > 0
}

///|
async fn run(arguments : Array[String]) -> Unit {
  let mut requested_file : String? = None
  let mut cwd = @env.current_dir().unwrap_or(".")
  let directories : Array[String] = []
  let mut silent = false
  let mut dry_run = false
  let mut always_make = false
  let mut keep_going = false
  let mut jobs = 1
  let mut explicit_jobs = false
  let original_environment = @process.default_environment()
  let environment_variables : Map[String, Variable] = Map([])
  for name, value in original_environment {
    environment_variables[name] = { value, immediate: name != "MAKEFLAGS", }
  }
  let decoded_flags = lookup_variable(
    "MAKEFLAGS",
    environment_variables,
    {},
    0,
    cwd~,
    exported=ExportPolicy::new(environment_variables.keys().collect()),
    state=Expansion::new(original=original_environment),
  )
  let inherited_flags = makeflag_words(decoded_flags)
  let inherited_arguments = inherited_make_arguments(
    inherited_flags,
    include_assignments=false,
  )
  let arguments = inherited_arguments + arguments
  let touch_targets : Map[String, Bool] = Map([])
  let mut options = true
  let targets : Array[String] = []
  let cli_assignments = inherited_make_assignments(inherited_flags)
  let mut index = 0
  while index < arguments.length() {
    let argument = arguments[index]
    if options && argument == "--" {
      options = false
    } else if options && (argument == "-f" || argument == "--file") {
      if index + 1 >= arguments.length() {
        raise MakeError("\{argument} requires a file")
      }
      index += 1
      requested_file = Some(arguments[index])
    } else if options && argument.has_prefix("--file=") {
      requested_file = Some(argument[7:].to_owned())
    } else if options && argument == "-C" {
      if index + 1 >= arguments.length() {
        raise MakeError("-C requires a directory")
      }
      index += 1
      directories.push(arguments[index])
    } else if options && (argument == "-s" || argument == "--silent") {
      silent = true
    } else if options && (argument == "-n" || argument == "--just-print") {
      dry_run = true
    } else if options && (argument == "-B" || argument == "--always-make") {
      always_make = true
    } else if options && (argument == "-k" || argument == "--keep-going") {
      keep_going = true
    } else if options && (argument == "-W" || argument == "--what-if") {
      if index + 1 >= arguments.length() {
        raise MakeError("\{argument} requires a file")
      }
      index += 1
      touch_targets[arguments[index]] = true
    } else if options && argument.has_prefix("--what-if=") {
      let value = argument[10:].to_owned()
      if value == "" {
        raise MakeError("--what-if requires a file")
      }
      touch_targets[value] = true
    } else if options && argument.has_prefix("-W") && argument.length() > 2 {
      touch_targets[argument[2:].to_owned()] = true
    } else if options && (argument == "-j" || argument == "--jobs") {
      explicit_jobs = index >= inherited_arguments.length()
      jobs = 0
      if index + 1 < arguments.length() &&
        positive_job_count(arguments[index + 1]) {
        index += 1
        jobs = @string.parse_int(arguments[index])
      }
    } else if options && argument.has_prefix("--jobs=") {
      explicit_jobs = index >= inherited_arguments.length()
      let value = argument[7:].to_owned()
      if !positive_job_count(value) {
        raise MakeError("the '-j' option requires a positive integer")
      }
      jobs = @string.parse_int(value)
    } else if options && argument.has_prefix("-j") && argument.length() > 2 {
      explicit_jobs = index >= inherited_arguments.length()
      if !positive_job_count(argument[2:].to_owned()) {
        raise MakeError("the '-j' option requires a positive integer")
      }
      jobs = @string.parse_int(argument[2:])
    } else if options && (argument == "-h" || argument == "--help") {
      @stdio.stdout.write(
        "Usage: make [-B] [-k] [-n] [-s] [-j [N]] [-W FILE] [-C DIR] [-f FILE] [VARIABLE=VALUE] [TARGET...]\n      --help      display this help and exit\n      --version   display version and exit\nSupports computed variables, named/default export and unexport, .EXPORT_ALL_VARIABLES, .SECONDEXPANSION and .SHELLSTATUS.\nMAKEFLAGS can inherit an existing POSIX FIFO jobserver; descriptor pipes and Windows semaphores are unavailable.\nWithout an inherited FIFO, recursive make runs one job per parent slot.\nShell test -s follows target metadata; unavailable target sizes are rejected.\nShell captures preserve status and output before errors; ignored recipe errors continue.\nOn POSIX, every file-test operand ending in a slash must name a directory.\nMakefile and dependency paths retain filesystem traversal; -C resolves each requested directory.\n",
      )
      return
    } else if options && argument.has_prefix("-") && argument != "-" {
      raise MakeError("unsupported option '\{argument}'")
    } else {
      match parse_assignment(argument) {
        Some(assignment) => cli_assignments.push(assignment)
        None => targets.push(argument)
      }
    }
    index += 1
  }
  for requested in directories {
    let directory = path_in(cwd, requested)
    if @fs.kind(directory) != Directory {
      raise MakeError("not a directory: \{requested}")
    }
    cwd = @fs.realpath(directory)
  }
  let variables : Map[String, Variable] = Map([])
  for name, value in @process.default_environment() {
    if name != "SHELL" && (value != "" || name == ".SHELLFLAGS") {
      validate_special_assignment(name, "=", value)
    }
    variables[name] = { value, immediate: true, }
  }
  // GNU ignores an inherited SHELL as an interpreter selector, while children
  // still receive its original environment value unless SHELL is exported.
  variables["SHELL"] = { value: "/bin/sh", immediate: true, }
  if !variables.contains(".SHELLFLAGS") {
    variables[".SHELLFLAGS"] = { value: "-c", immediate: true, }
  }
  variables["MAKEFLAGS"] = { value: decoded_flags, immediate: true, }
  let invocation = @env.args().get(0).unwrap_or("make")
  let own_path = path_in(@env.current_dir().unwrap_or("."), invocation)
  let quoted_path = "'" + own_path.replace_all(old="'", new="'\\''") + "'"
  variables["MAKE"] = {
    value: if invocation.has_suffix(".wasm") {
      "moonrun " + quoted_path + " --"
    } else {
      quoted_path
    },
    immediate: true,
  }
  variables["MAKELEVEL"] = {
    value: makelevel(variables).to_string(),
    immediate: true,
  }
  let command_line_variables : Map[String, Bool] = Map([])
  let database : Database = {
    cwd,
    variables,
    command_line_variables,
    exported: ExportPolicy::new(original_environment.keys().collect()),
    rules: {},
    default_target: None,
    secondary_expansion: false,
    invocation_level: 0,
    serial_children: true,
  }
  for name in ["SHELL", ".SHELLFLAGS", "MAKE"] {
    database.exported.defaults.add(name)
  }
  if original_environment.contains("SHELL") {
    database.exported.remove("SHELL")
  }
  for (raw_name, operator, value) in cli_assignments {
    let name = expand(
      raw_name,
      variables,
      {},
      0,
      cwd~,
      exported=database.exported,
    )
    if operator == "?=" && variables.contains(name) {
      continue
    }
    command_line_variables.remove(name)
    assign_resolved_variable(database, name, operator, value)
    command_line_variables[name] = true
    if name != "SHELL" && default_export_name(name) {
      database.exported.add(name)
    }
  }
  database.invocation_level = makelevel(variables)
  let authentication = if explicit_jobs {
    None
  } else {
    inherited_jobserver(inherited_flags)
  }
  database.serial_children = authentication is None
  let propagated_flags : Array[String] = []
  if silent {
    propagated_flags.push("-s")
  }
  if dry_run {
    propagated_flags.push("-n")
  }
  if always_make {
    propagated_flags.push("-B")
  }
  if keep_going {
    propagated_flags.push("-k")
  }
  match authentication {
    Some(value) => {
      propagated_flags.push("-j")
      propagated_flags.push(escape_makeflag("--jobserver-auth=" + value))
    }
    // Without a public FIFO creation API this process cannot create a GNU
    // jobserver. Recursive children keep one job per already occupied slot.
    None =>
      propagated_flags.push(
        if jobs == 0 {
          "-j"
        } else {
          "-j" + jobs.to_string()
        },
      )
  }
  if !command_line_variables.is_empty() {
    propagated_flags.push("--")
    let names = command_line_variables.keys().collect()
    names.sort()
    for name in names {
      let variable = variables[name]
      let assignment = if variable.immediate {
        name + ":=" + variable.value
      } else {
        name + "=" + variable.value
      }
      propagated_flags.push(escape_makeflag(assignment))
    }
  }
  variables["MAKEFLAGS"] = {
    value: propagated_flags.join(" "),
    immediate: true,
  }
  database.exported.add("MAKEFLAGS")
  let makefile = choose_makefile(cwd, requested_file)
  parse_makefile(
    @fs.read_file(makefile).text(),
    database,
    @path.Path(makefile).dirname().to_string(),
    0,
  )
  database.exported.finish_reading()
  let requested_targets = if targets.is_empty() {
    match database.default_target {
      Some(value) => [value]
      None => raise MakeError("Makefile contains no targets")
    }
  } else {
    targets
  }
  let final_flags = makeflag_words(
    lookup_variable(
      "MAKEFLAGS",
      variables,
      {},
      0,
      cwd~,
      exported=database.exported,
      state=database.expansion(),
    ),
  )
  let final_authentication = if explicit_jobs {
    None
  } else {
    match inherited_jobserver(final_flags) {
      Some(value) => Some(value)
      None => authentication
    }
  }
  database.serial_children = final_authentication is None
  let jobserver = match final_authentication {
    Some(value) => Some(Jobserver::open(value))
    None => None
  }
  defer (if jobserver is Some(server) { server.close() })
  let settings : Settings = apply_makefile_flags(
    {
      jobs,
      jobserver,
      expansion_lock: @async.Mutex(),
      cwd,
      silent,
      dry_run,
      always_make,
      keep_going,
      what_if: touch_targets,
    },
    inherited_make_arguments(final_flags, include_assignments=false),
  )
  build_targets(requested_targets, database, settings)
}

///|
async fn main {
  let metadata_args = @env.args()[1:]
  if metadata_args.length() == 1 && metadata_args[0] == "--version" {
    @stdio.stdout.write("make 0.2.9\n")
    return
  }
  run(@env.args()[1:].to_owned()) catch {
    ExpansionFailure(message) => {
      @stdio.stderr.write("make: \{message}\n")
      @sys.exit(2)
    }
    MakeError(message) => {
      if message == "unexpected endif" {
        @stdio.stderr.write("Makefile:1: *** extraneous 'endif'.  Stop.\n")
      } else {
        @stdio.stderr.write("make: \{message}\n")
      }
      @sys.exit(2)
    }
    err => {
      @stdio.stderr.write("make: \{err}\n")
      @sys.exit(2)
    }
  }
}