var parse = function(tokens, src, filename, tokenizer) {
  var _src_len = length(src)

  var template_escape_map = {
    n: "\n", t: "\t", r: "\r", "\\": "\\",
    "`": "`", "$": "$", "0": character(0)
  }

  // ============================================================
  // Parser Cursor
  // ============================================================

  var cursor = 0
  var tok = null
  var got_lf = false
  var prev_tok = null
  var _control_depth = 0
  var _control_type = null
  var _expecting_body = false

  var advance = function() {
    var t = null
    var k = null
    prev_tok = tok
    cursor = cursor + 1
    got_lf = false
    while (cursor < length(tokens)) {
      t = tokens[cursor]
      k = t.kind
      if (k == "space" || k == "comment") {
        cursor = cursor + 1
        continue
      }
      if (k == "newline") {
        got_lf = true
        cursor = cursor + 1
        continue
      }
      tok = t
      return null
    }
    tok = tokens[length(tokens) - 1]
  }

  var peek_ahead = function(n) {
    var c = cursor + 1
    var count = 0
    var t = null
    var k = null
    while (c < length(tokens)) {
      t = tokens[c]
      k = t.kind
      if (k != "space" && k != "comment" && k != "newline") {
        count = count + 1
        if (count == n) return t
      }
      c = c + 1
    }
    return tokens[length(tokens) - 1]
  }

  var init_cursor = function() {
    cursor = -1
    advance()
  }

  // ============================================================
  // AST Helpers
  // ============================================================

  var errors = []
  var error_count = 0
  var fn_counter = 1

  var ast_node = function(kind, token) {
    return {
      kind: kind,
      at: token.at,
      from_row: token.from_row,
      from_column: token.from_column
    }
  }

  var ast_node_end = function(node) {
    node.to_row = prev_tok.to_row
    node.to_column = prev_tok.to_column
    return node
  }

  var parse_error = function(token, msg) {
    if (error_count >= 5) return null
    error_count = error_count + 1
    push(errors, {
      message: msg,
      line: token.from_row + 1,
      column: token.from_column + 1,
      offset: token.at
    })
  }

  var _keywords = {
    "if": true, in: true, "do": true, go: true,
    "var": true, def: true, "for": true,
    "else": true, "this": true, "null": true, "true": true,
    "false": true, "while": true, "break": true,
    "return": true, "delete": true,
    disrupt: true, "function": true, "continue": true,
    disruption: true
  }

  var is_keyword = function(kind) {
    return _keywords[kind] == true
  }

  // ============================================================
  // Expression Parsing
  // ============================================================

  // Forward declarations via var
  var parse_expr = null
  var parse_assign_expr = null
  var parse_assign = null
  var parse_statement = null
  var parse_block_statements = null
  var parse_function_inner = null
  var parse_arrow_function = null

  var is_arrow_function = function() {
    if (tok.kind != "(") return false
    var c = cursor + 1
    var depth = 1
    var k = null
    while (c < length(tokens) && depth > 0) {
      k = tokens[c].kind
      if (k == "(") { depth = depth + 1 }
      else if (k == ")") { depth = depth - 1 }
      else if (k == "text" || k == "number") { null }
      c = c + 1
    }
    while (c < length(tokens)) {
      k = tokens[c].kind
      if (k != "space" && k != "newline" && k != "comment") break
      c = c + 1
    }
    if (c >= length(tokens)) return false
    return tokens[c].kind == "=>"
  }

  var parse_primary = function() {
    var start = tok
    var node = null
    var k = tok.kind
    var list = null
    var pair = null
    var left = null
    var right = null
    var is_ident = false
    var is_kw = false
    var p1 = null
    var elem = null
    var fn_start = null
    var fn = null
    var name_item = null
    var params = null
    var param = null
    var rpos = 0
    var pattern_parts = null
    var flags_parts = null
    var tv = null
    var has_interp = false
    var ti = 0
    var tpl_list = null
    var fmt_parts = null
    var idx = 0
    var tvi = 0
    var tvlen = 0
    var depth = 0
    var expr_parts = null
    var expr_str = null
    var tc = null
    var tq = null
    var esc_ch = null
    var esc_val = null
    var expr_tokens = null
    var sub_ast = null
    var sub_stmt = null
    var sub_expr = null
    var meth_old_cd = 0
    var meth_old_ct = null
    var meth_old_eb = false

    if (k == "number") {
      node = ast_node("number", start)
      node.value = tok.value
      node.number = tok.number
      advance()
      ast_node_end(node)
      return node
    }
    if (k == "text") {
      // Check for template interpolation: ${...}
      tv = tok.value
      has_interp = false
      ti = 0
      while (ti < length(tv) - 1) {
        if (tv[ti] == "$" && tv[ti + 1] == "{") {
          if (ti == 0 || tv[ti - 1] != "\\") {
            has_interp = true
            break
          }
        }
        ti = ti + 1
      }
      if (!has_interp || tokenizer == null) {
        node = ast_node("text", start)
        node.value = tok.value
        advance()
        ast_node_end(node)
        return node
      }
      // Template literal with interpolation
      node = ast_node("text literal", start)
      tpl_list = []
      node.list = tpl_list
      fmt_parts = []
      idx = 0
      tvi = 0
      tvlen = length(tv)
      while (tvi < tvlen) {
        if (tv[tvi] == "\\" && tvi + 1 < tvlen) {
          esc_ch = tv[tvi + 1]
          esc_val = template_escape_map[esc_ch]
          if (esc_val != null) { push(fmt_parts, esc_val) }
          else { push(fmt_parts, esc_ch) }
          tvi = tvi + 2
        } else if (tv[tvi] == "$" && tvi + 1 < tvlen && tv[tvi + 1] == "{") {
          tvi = tvi + 2
          depth = 1
          expr_parts = []
          while (tvi < tvlen && depth > 0) {
            tc = tv[tvi]
            if (tc == "{") { depth = depth + 1; push(expr_parts, tc); tvi = tvi + 1 }
            else if (tc == "}") {
              depth = depth - 1
              if (depth > 0) { push(expr_parts, tc) }
              tvi = tvi + 1
            }
            else if (tc == "'" || tc == "\"" || tc == "`") {
              tq = tc
              push(expr_parts, tc)
              tvi = tvi + 1
              while (tvi < tvlen && tv[tvi] != tq) {
                if (tv[tvi] == "\\" && tvi + 1 < tvlen) {
                  push(expr_parts, tv[tvi])
                  tvi = tvi + 1
                }
                push(expr_parts, tv[tvi])
                tvi = tvi + 1
              }
              if (tvi < tvlen) { push(expr_parts, tv[tvi]); tvi = tvi + 1 }
            } else {
              push(expr_parts, tc)
              tvi = tvi + 1
            }
          }
          expr_str = text(expr_parts)
          expr_tokens = tokenizer(expr_str, "<template>").tokens
          sub_ast = parse(expr_tokens, expr_str, "<template>", tokenizer)
          if (sub_ast != null && sub_ast.statements != null && length(sub_ast.statements) > 0) {
            sub_stmt = sub_ast.statements[0]
            sub_expr = null
            if (sub_stmt.kind == "call") {
              sub_expr = sub_stmt.expression
            } else {
              sub_expr = sub_stmt
            }
            push(tpl_list, sub_expr)
          }
          push(fmt_parts, "{")
          push(fmt_parts, text(idx))
          push(fmt_parts, "}")
          idx = idx + 1
        } else {
          push(fmt_parts, tv[tvi])
          tvi = tvi + 1
        }
      }
      node.value = text(fmt_parts)
      advance()
      ast_node_end(node)
      return node
    }
    if (k == "name") {
      p1 = peek_ahead(1)
      if (p1.kind == "=>") {
        return parse_arrow_function()
      }
      node = ast_node("name", start)
      node.name = tok.value
      advance()
      ast_node_end(node)
      return node
    }
    if (k == "null") {
      node = ast_node("null", start)
      advance()
      ast_node_end(node)
      return node
    }
    if (k == "true") {
      node = ast_node("true", start)
      advance()
      ast_node_end(node)
      return node
    }
    if (k == "false") {
      node = ast_node("false", start)
      advance()
      ast_node_end(node)
      return node
    }
    if (k == "this") {
      node = ast_node("this", start)
      advance()
      ast_node_end(node)
      return node
    }
    if (k == "[") {
      node = ast_node("array", start)
      list = []
      node.list = list
      advance()
      while (tok.kind != "]" && tok.kind != "eof") {
        elem = parse_assign_expr()
        if (elem != null) push(list, elem)
        if (tok.kind == ",") advance()
        else break
      }
      ast_node_end(node)
      if (tok.kind == "]") advance()
      else if (tok.kind == "eof") parse_error(tok, "unterminated array literal, expected ']'")
      return node
    }
    if (k == "{") {
      node = ast_node("record", start)
      list = []
      node.list = list
      advance()
      while (tok.kind != "}" && tok.kind != "eof") {
        pair = {}
        is_ident = (tok.kind == "name")
        is_kw = is_keyword(tok.kind)
        if (is_ident || is_kw || tok.kind == "text" || tok.kind == "number") {
          if (is_kw) {
            left = ast_node("name", tok)
            left.name = tok.kind
            advance()
            ast_node_end(left)
          } else {
            left = parse_primary()
          }
          pair.left = left
        } else if (tok.kind == "[") {
          advance()
          left = parse_assign_expr()
          pair.left = left
          pair.computed = true
          if (tok.kind == "]") advance()
          else parse_error(tok, "expected ']' after computed property")
        } else {
          parse_error(tok, "expected property name in object literal")
          break
        }
        if (tok.kind == ":") {
          advance()
          right = parse_assign_expr()
          pair.right = right
        } else if (tok.kind == "(") {
          fn_start = tok
          fn = ast_node("function", fn_start)
          name_item = pair.left
          if (name_item != null && name_item.name != null) {
            fn.name = name_item.name
          }
          params = []
          fn.list = params
          advance()
          while (tok.kind != ")" && tok.kind != "eof") {
            if (tok.kind == "name") {
              param = ast_node("name", tok)
              param.name = tok.value
              advance()
              ast_node_end(param)
              if (tok.kind == "=" || tok.kind == "|") {
                advance()
                param.expression = parse_assign_expr()
              }
              push(params, param)
            } else {
              parse_error(tok, "expected parameter name")
              break
            }
            if (tok.kind == ",") advance()
            else break
          }
          if (tok.kind == ")") advance()
          else if (tok.kind == "eof") parse_error(tok, "unterminated method parameter list")
          if (length(params) > 4) parse_error(tok, "functions cannot have more than 4 parameters")
          fn.arity = length(params)
          meth_old_cd = _control_depth
          meth_old_ct = _control_type
          meth_old_eb = _expecting_body
          _control_depth = 0
          _control_type = null
          _expecting_body = false
          if (tok.kind == "{") {
            advance()
            fn.statements = parse_block_statements()
            if (tok.kind == "}") advance()
            else if (tok.kind == "eof") parse_error(tok, "unterminated method body")
          } else {
            parse_error(tok, "expected '{' for method body")
          }
          _control_depth = meth_old_cd
          _control_type = meth_old_ct
          _expecting_body = meth_old_eb
          fn.function_nr = fn_counter
          fn_counter = fn_counter + 1
          ast_node_end(fn)
          pair.right = fn
        } else if (is_ident && (tok.kind == "," || tok.kind == "}")) {
          right = ast_node("name", pair.left)
          right.name = pair.left.name
          ast_node_end(right)
          pair.right = right
        } else {
          parse_error(tok, "expected ':' after property name")
        }
        push(list, pair)
        if (tok.kind == ",") advance()
        else if (tok.kind == "{") {
          if (right && right.kind == "(") {
            parse_error(tok, "unexpected '{' after property value; use method shorthand `name(args) { ... }` or `name: function(args) { ... }`")
          } else {
            parse_error(tok, "expected ',' or '}' in object literal")
          }
          break
        } else break
      }
      ast_node_end(node)
      if (tok.kind == "}") advance()
      else if (tok.kind == "eof") parse_error(tok, "unterminated object literal, expected '}'")
      return node
    }
    if (k == "(") {
      if (is_arrow_function()) {
        return parse_arrow_function()
      }
      advance()
      node = parse_expr()
      if (tok.kind == ")") advance()
      else if (tok.kind == "eof") parse_error(tok, "unterminated parenthesized expression, expected ')'")
      else parse_error(tok, "expected ')' after expression")
      return node
    }
    if (k == "function") {
      return parse_function_inner()
    }
    if (k == "/") {
      node = ast_node("regexp", start)
      rpos = tok.at + 1
      pattern_parts = []
      flags_parts = []
      while (rpos < _src_len && src[rpos] != "/") {
        if (src[rpos] == "\\" && rpos + 1 < _src_len) {
          push(pattern_parts, src[rpos])
          push(pattern_parts, src[rpos + 1])
          rpos = rpos + 2
        } else {
          push(pattern_parts, src[rpos])
          rpos = rpos + 1
        }
      }
      if (rpos < _src_len) rpos = rpos + 1
      while (rpos < _src_len && is_letter(src[rpos])) {
        push(flags_parts, src[rpos])
        rpos = rpos + 1
      }
      node.pattern = text(pattern_parts)
      if (length(flags_parts) > 0) node.flags = text(flags_parts)
      // Skip all tokens consumed by the regex re-scan
      while (true) {
        advance()
        if (tok.kind == "eof" || tok.at >= rpos) break
      }
      ast_node_end(node)
      return node
    }

    if (k == "eof") {
      parse_error(start, "unexpected end of input")
    } else {
      parse_error(start, "unexpected token where expression expected")
    }
    advance()
    return null
  }

  var parse_postfix = function() {
    var node = parse_primary()
    var start = null
    var new_node = null
    var index = null
    var arg = null
    var args_list = null
    var one_node = null
    var binop_node = null
    var op = null
    if (node == null) return null
    while (true) {
      start = tok
      if (tok.kind == ".") {
        advance()
        new_node = ast_node(".", start)
        new_node.left = node
        if (tok.kind == "name" || is_keyword(tok.kind)) {
          if (tok.kind == "name") {
            new_node.right = tok.value
          } else {
            new_node.right = tok.kind
          }
          advance()
        } else {
          parse_error(tok, "expected property name after '.'")
        }
        ast_node_end(new_node)
        node = new_node
      } else if (tok.kind == "[") {
        advance()
        new_node = ast_node("[", start)
        new_node.left = node
        if (tok.kind == "]") {
          advance()
        } else {
          index = parse_assign_expr()
          new_node.right = index
          if (tok.kind == "]") advance()
          else parse_error(tok, "expected ']'")
        }
        ast_node_end(new_node)
        node = new_node
      } else if (tok.kind == "(") {
        advance()
        new_node = ast_node("(", start)
        new_node.expression = node
        args_list = []
        new_node.list = args_list
        while (tok.kind != ")" && tok.kind != "eof") {
          arg = parse_assign_expr()
          if (arg != null) push(args_list, arg)
          if (tok.kind == ",") advance()
          else break
        }
        if (tok.kind == ")") advance()
        else parse_error(tok, "unterminated argument list, expected ')'")
        ast_node_end(new_node)
        node = new_node
      } else if (tok.kind == "++") {
        new_node = ast_node("++", start)
        new_node.expression = node
        new_node.postfix = true
        advance()
        ast_node_end(new_node)
        node = new_node
      } else if (tok.kind == "--") {
        new_node = ast_node("--", start)
        new_node.expression = node
        new_node.postfix = true
        advance()
        ast_node_end(new_node)
        node = new_node
      } else {
        break
      }
    }
    return node
  }

  var parse_unary = function() {
    var start = tok
    var node = null
    var expr = null
    var k = tok.kind
    var operand = null
    var one_node = null
    var binop_node = null
    var op = null
    if (k == "!") {
      advance()
      node = ast_node("!", start)
      node.expression = parse_unary()
      ast_node_end(node)
      return node
    }
    if (k == "~") {
      advance()
      node = ast_node("~", start)
      node.expression = parse_unary()
      ast_node_end(node)
      return node
    }
    if (k == "+") {
      advance()
      node = ast_node("+unary", start)
      node.expression = parse_unary()
      ast_node_end(node)
      return node
    }
    if (k == "-") {
      advance()
      node = ast_node("-unary", start)
      node.expression = parse_unary()
      ast_node_end(node)
      return node
    }
    if (k == "++" || k == "--") {
      advance()
      operand = parse_unary()
      one_node = ast_node("number", start)
      one_node.number = 1
      one_node.value = "1"
      ast_node_end(one_node)
      op = "+"
      if (k == "--") op = "-"
      binop_node = ast_node(op, start)
      binop_node.left = operand
      binop_node.right = one_node
      ast_node_end(binop_node)
      node = ast_node("assign", start)
      node.left = operand
      node.right = binop_node
      ast_node_end(node)
      return node
    }
    if (k == "delete") {
      advance()
      node = ast_node("delete", start)
      node.expression = parse_unary()
      ast_node_end(node)
      return node
    }
    return parse_postfix()
  }

  // Binary operator precedence
  var binop_prec = {
    "**": 14,
    "*": 13, "/": 13, "%": 13,
    "+": 12, "-": 12,
    "<<": 11, ">>": 11, ">>>": 11,
    "<": 10, ">": 10, "<=": 10, ">=": 10, in: 10,
    "==": 9, "!=": 9, "===": 9, "!==": 9,
    "&": 8, "^": 7, "|": 6,
    "&&": 5, "||": 4
  }

  var parse_binary = function(min_prec) {
    var left_node = parse_unary()
    var start = null
    var op = null
    var prec = null
    var next_prec = 0
    var right_node = null
    var node = null
    if (left_node == null) return null
    while (true) {
      start = tok
      op = tok.kind
      prec = binop_prec[op]
      if (prec == null || prec < min_prec) break
      advance()
      next_prec = prec + 1
      if (prec == 14) next_prec = prec // right-assoc for **
      right_node = parse_binary(next_prec)
      node = ast_node(op, start)
      node.left = left_node
      node.right = right_node
      ast_node_end(node)
      left_node = node
    }
    return left_node
  }

  var parse_ternary = function() {
    var cond = parse_binary(1)
    var start = null
    var then_expr = null
    var else_expr = null
    var node = null
    if (cond == null) return null
    if (tok.kind == "?") {
      start = tok
      advance()
      then_expr = parse_assign_expr()
      if (tok.kind == ":") advance()
      else parse_error(tok, "expected ':' in ternary expression")
      else_expr = parse_assign_expr()
      node = ast_node("then", start)
      node.expression = cond
      node.then = then_expr
      node.else = else_expr
      ast_node_end(node)
      return node
    }
    return cond
  }

  // Assign operators
  var assign_ops = {
    "=": "assign", "+=": "+=", "-=": "-=", "*=": "*=", "/=": "/=", "%=": "%=",
    "<<=": "<<=", ">>=": ">>=", ">>>=": ">>>=",
    "&=": "&=", "^=": "^=", "|=": "|=", "**=": "**=",
    "&&=": "&&=", "||=": "||="
  }

  var compound_binop = {
    "+=": "+", "-=": "-", "*=": "*", "/=": "/", "%=": "%",
    "<<=": "<<", ">>=": ">>", ">>>=": ">>>",
    "&=": "&", "^=": "^", "|=": "|", "**=": "**",
    "&&=": "&&", "||=": "||"
  }

  parse_assign = function(unused) {
    var left_node = parse_ternary()
    var start = null
    var kind = null
    var right_node = null
    var node = null
    var left_kind = null
    var right_kind = null
    var binop = null
    var binop_node = null
    if (left_node == null) return null
    start = tok
    kind = assign_ops[tok.kind]
    if (kind == null) return left_node

    left_kind = left_node.kind
    if (left_kind != "name" && left_kind != "." && left_kind != "[") {
      parse_error(start, "invalid assignment left-hand side")
    }

    advance()
    right_node = parse_assign()

    binop = compound_binop[kind]
    if (binop != null) {
      binop_node = ast_node(binop, start)
      binop_node.left = left_node
      binop_node.right = right_node
      ast_node_end(binop_node)
      node = ast_node("assign", start)
      node.left = left_node
      node.right = binop_node
    } else {
      node = ast_node(kind, start)
      node.left = left_node
      node.right = right_node
      if (left_node.kind == "[" && left_node.right == null) node.push = true
      if (right_node != null && right_node.kind == "[" && right_node.right == null) node.pop = true
    }

    ast_node_end(node)
    return node
  }

  parse_assign_expr = function(unused) {
    return parse_assign()
  }

  parse_expr = function(unused) {
    var left_node = parse_assign()
    var start = null
    var right_node = null
    var node = null
    if (left_node == null) return null
    while (tok.kind == ",") {
      start = tok
      advance()
      right_node = parse_assign()
      node = ast_node(",", start)
      node.left = left_node
      node.right = right_node
      ast_node_end(node)
      left_node = node
    }
    return left_node
  }

  // ============================================================
  // Statement Parsing
  // ============================================================

  var in_disruption = 0

  var expect_semi = function() {
    if (tok.kind == ";") { advance(); return null }
    if (tok.kind == "eof" || tok.kind == "}" || got_lf || tok.kind == "else") return null
    parse_error(tok, "expecting ';'")
  }

  var sync_to_statement = function() {
    var k = null
    while (tok.kind != "eof") {
      k = tok.kind
      if (k == ";") { advance(); return null }
      if (k == "}") return null
      if (k == "var" || k == "def" || k == "if" || k == "while" ||
          k == "for" || k == "return" || k == "disrupt" ||
          k == "function" || k == "break" || k == "continue" || k == "do") return null
      advance()
    }
  }

  parse_block_statements = function(unused) {
    var stmts = []
    var before = null
    var stmt = null
    while (tok.kind != "}" && tok.kind != "eof") {
      before = cursor
      stmt = parse_statement()
      if (stmt != null) {
        push(stmts, stmt)
      } else if (cursor == before) {
        sync_to_statement()
      }
    }
    return stmts
  }

  parse_function_inner = function(unused) {
    var start = tok
    var node = ast_node("function", start)
    var params = []
    var stmts = null
    var param = null
    var prev_names = null
    var pname = null
    var old_dis = 0
    var old_cd = _control_depth
    var old_ct = _control_type
    var old_eb = _expecting_body

    if (in_disruption) {
      parse_error(tok, "cannot define function inside disruption clause")
    }

    advance() // skip 'function'

    if (tok.kind == "name") {
      node.name = tok.value
      advance()
    }

    node.list = params
    if (tok.kind == "(") {
      advance()
      prev_names = []
      while (tok.kind != ")" && tok.kind != "eof") {
        if (tok.kind == "name") {
          param = ast_node("name", tok)
          param.name = tok.value
          pname = tok.value
          if (find(prev_names, pname) != null) parse_error(tok, "duplicate parameter name '" + pname + "'")
          push(prev_names, pname)
          advance()
          ast_node_end(param)
          if (tok.kind == "=" || tok.kind == "|") {
            advance()
            param.expression = parse_assign_expr()
          }
          push(params, param)
        } else {
          parse_error(tok, "expected parameter name")
          break
        }
        if (tok.kind == ",") advance()
        else break
      }
      if (tok.kind == ")") advance()
      else if (tok.kind == "eof") parse_error(tok, "unterminated function parameter list, expected ')'")
    } else {
      parse_error(tok, "expected '(' after function name")
    }

    if (length(params) > 4) parse_error(tok, "functions cannot have more than 4 parameters")
    node.arity = length(params)

    _control_depth = 0
    _control_type = null
    _expecting_body = false
    if (tok.kind == "{") {
      advance()
      stmts = parse_block_statements()
      node.statements = stmts
      if (tok.kind == "}") advance()
      else if (tok.kind == "eof") parse_error(tok, "unterminated function body, expected '}'")
    } else {
      parse_error(tok, "expected '{' for function body")
    }

    if (tok.kind == "disruption") {
      advance()
      if (tok.kind == "{") {
        advance()
        old_dis = in_disruption
        in_disruption = 1
        node.disruption = parse_block_statements()
        in_disruption = old_dis
        if (tok.kind == "}") advance()
        else if (tok.kind == "eof") parse_error(tok, "unterminated disruption clause, expected '}'")
      } else {
        parse_error(tok, "expected '{' after disruption")
      }
    }

    _control_depth = old_cd
    _control_type = old_ct
    _expecting_body = old_eb
    node.function_nr = fn_counter
    fn_counter = fn_counter + 1
    ast_node_end(node)
    return node
  }

  parse_arrow_function = function(unused) {
    var start = tok
    var node = ast_node("function", start)
    var params = []
    var param = null
    var stmts = null
    var ret = null
    var expr = null
    var prev_names = null
    var pname = null
    var old_cd = _control_depth
    var old_ct = _control_type
    var old_eb = _expecting_body
    node.arrow = true

    if (in_disruption) {
      parse_error(tok, "cannot define function inside disruption clause")
    }

    node.list = params

    if (tok.kind == "name") {
      param = ast_node("name", tok)
      param.name = tok.value
      advance()
      ast_node_end(param)
      push(params, param)
    } else if (tok.kind == "(") {
      advance()
      prev_names = []
      while (tok.kind != ")" && tok.kind != "eof") {
        if (tok.kind == "name") {
          param = ast_node("name", tok)
          param.name = tok.value
          pname = tok.value
          if (find(prev_names, pname) != null) parse_error(tok, "duplicate parameter name '" + pname + "'")
          push(prev_names, pname)
          advance()
          ast_node_end(param)
          if (tok.kind == "=" || tok.kind == "|") {
            advance()
            param.expression = parse_assign_expr()
          }
          push(params, param)
        } else {
          parse_error(tok, "expected parameter name")
          break
        }
        if (tok.kind == ",") advance()
        else break
      }
      if (tok.kind == ")") advance()
    }

    if (length(params) > 4) parse_error(tok, "functions cannot have more than 4 parameters")
    node.arity = length(params)

    if (tok.kind != "=>") {
      parse_error(tok, "expected '=>' in arrow function")
    } else {
      advance()
    }

    _control_depth = 0
    _control_type = null
    _expecting_body = false
    if (tok.kind == "{") {
      advance()
      stmts = parse_block_statements()
      node.statements = stmts
      if (tok.kind == "}") advance()
    } else {
      stmts = []
      ret = ast_node("return", tok)
      expr = parse_assign_expr()
      ret.expression = expr
      ast_node_end(ret)
      push(stmts, ret)
      node.statements = stmts
    }

    _control_depth = old_cd
    _control_type = old_ct
    _expecting_body = old_eb
    node.function_nr = fn_counter
    fn_counter = fn_counter + 1
    ast_node_end(node)
    return node
  }

  parse_statement = function(unused) {
    var start = tok
    var node = null
    var k = tok.kind
    var stmts = null
    var cond = null
    var then_stmts = null
    var else_stmts = null
    var else_ifs = null
    var body = null
    var expr = null
    var init = null
    var test = null
    var update = null
    var left_node = null
    var right_node = null
    var kind_name = null
    var is_def = false
    var decls = null
    var decl_count = 0
    var var_name = null
    var right_kind = null
    var elif = null
    var p1_tok = null
    var labeled_stmt = null
    var depth = 0
    var saved_ct = null
    var saved_cd = 0
    var saved_eb = false

    if (k == "{") {
      if (!_expecting_body) {
        parse_error(start, "bare block '{ ... }' is not a valid statement; use a function, if, while, or for instead")
        advance()
        depth = 1
        while (tok.kind != "eof" && depth > 0) {
          if (tok.kind == "{") depth = depth + 1
          else if (tok.kind == "}") depth = depth - 1
          if (depth > 0) advance()
        }
        if (tok.kind == "}") advance()
        return null
      }
      _expecting_body = false
      node = ast_node("block", start)
      advance()
      stmts = parse_block_statements()
      node.statements = stmts
      if (tok.kind == "}") advance()
      ast_node_end(node)
      return node
    }

    if (k == "var" || k == "def") {
      if (_control_depth > 0) {
        parse_error(start, "'" + k + "' declarations must appear at function body level, not inside '" + _control_type + "'; move this declaration before the '" + _control_type + "' statement")
      }
      kind_name = k
      is_def = (k == "def")
      advance()
      if (tok.kind != "name") {
        parse_error(tok, "expected identifier after '" + kind_name + "'")
        return null
      }
      decls = []
      decl_count = 0
      while (tok.kind == "name") {
        node = ast_node(kind_name, start)
        left_node = ast_node("name", tok)
        left_node.name = tok.value
        var_name = tok.value
        advance()
        ast_node_end(left_node)
        node.left = left_node
        if (tok.kind == "=") {
          advance()
          right_node = parse_assign_expr()
          node.right = right_node
          if (right_node != null && right_node.kind == "[" && right_node.right == null) {
            node.pop = true
          }
        } else if (is_def) {
          parse_error(start, "missing initializer for constant '" + var_name + "'")
        } else {
          parse_error(start, "'var' declarations must be initialized; use 'var " + var_name + " = null' if no value is needed")
        }
        ast_node_end(node)
        push(decls, node)
        decl_count = decl_count + 1
        if (tok.kind == ",") advance()
        else break
      }
      expect_semi()
      if (decl_count == 1) {
        return decls[0]
      }
      node = ast_node("var_list", start)
      node.list = decls
      ast_node_end(node)
      return node
    }

    if (k == "if") {
      node = ast_node("if", start)
      advance()
      if (tok.kind == "(") advance()
      else parse_error(tok, "expected '(' before condition")
      cond = parse_expr()
      node.expression = cond
      if (tok.kind == ")") advance()
      else parse_error(tok, "expected ')' after if condition")
      then_stmts = []
      node.then = then_stmts
      saved_ct = _control_type
      saved_cd = _control_depth
      _control_type = "if"
      _control_depth = _control_depth + 1
      _expecting_body = true
      body = parse_statement()
      if (body != null) push(then_stmts, body)
      else_ifs = []
      node.list = else_ifs
      if (tok.kind == "else") {
        advance()
        if (tok.kind == "if") {
          _control_depth = saved_cd
          _control_type = saved_ct
          elif = parse_statement()
          if (elif != null) push(else_ifs, elif)
          ast_node_end(node)
          return node
        } else {
          else_stmts = []
          node.else = else_stmts
          _expecting_body = true
          body = parse_statement()
          if (body != null) push(else_stmts, body)
        }
      }
      _control_depth = saved_cd
      _control_type = saved_ct
      ast_node_end(node)
      return node
    }

    if (k == "while") {
      node = ast_node("while", start)
      advance()
      if (tok.kind == "(") advance()
      else parse_error(tok, "expected '(' before condition")
      cond = parse_expr()
      node.expression = cond
      if (tok.kind == ")") advance()
      else parse_error(tok, "expected ')' after while condition")
      stmts = []
      node.statements = stmts
      saved_ct = _control_type
      saved_cd = _control_depth
      _control_type = "while"
      _control_depth = _control_depth + 1
      _expecting_body = true
      body = parse_statement()
      if (body != null) push(stmts, body)
      _control_depth = saved_cd
      _control_type = saved_ct
      ast_node_end(node)
      return node
    }

    if (k == "do") {
      node = ast_node("do", start)
      advance()
      stmts = []
      node.statements = stmts
      saved_ct = _control_type
      saved_cd = _control_depth
      _control_type = "do"
      _control_depth = _control_depth + 1
      _expecting_body = true
      body = parse_statement()
      if (body != null) push(stmts, body)
      _control_depth = saved_cd
      _control_type = saved_ct
      if (tok.kind == "while") advance()
      else parse_error(tok, "expected 'while' after do body")
      if (tok.kind == "(") advance()
      else parse_error(tok, "expected '(' before condition")
      cond = parse_expr()
      node.expression = cond
      if (tok.kind == ")") advance()
      else parse_error(tok, "expected ')' after do-while condition")
      expect_semi()
      ast_node_end(node)
      return node
    }

    if (k == "for") {
      node = ast_node("for", start)
      advance()
      if (tok.kind == "(") advance()
      else parse_error(tok, "expected '(' after for")
      if (tok.kind != ";") {
        if (tok.kind == "var" || tok.kind == "def") {
          parse_error(tok, "'" + tok.kind + "' declarations cannot appear in the for initializer; declare variables before the for loop")
          init = parse_statement()
          node.init = init
        } else {
          init = parse_expr()
          node.init = init
          if (tok.kind == ";") advance()
        }
      } else {
        advance()
      }
      if (tok.kind != ";") {
        test = parse_expr()
        node.test = test
      }
      if (tok.kind == ";") advance()
      if (tok.kind != ")") {
        update = parse_expr()
        node.update = update
      }
      if (tok.kind == ")") advance()
      else parse_error(tok, "expected ')' after for clauses")
      stmts = []
      node.statements = stmts
      saved_ct = _control_type
      saved_cd = _control_depth
      _control_type = "for"
      _control_depth = _control_depth + 1
      _expecting_body = true
      body = parse_statement()
      if (body != null) push(stmts, body)
      _control_depth = saved_cd
      _control_type = saved_ct
      ast_node_end(node)
      return node
    }

    if (k == "return") {
      node = ast_node("return", start)
      advance()
      if (tok.kind != ";" && tok.kind != "}" && !got_lf) {
        expr = parse_expr()
        node.expression = expr
      }
      expect_semi()
      ast_node_end(node)
      return node
    }

    if (k == "go") {
      node = ast_node("go", start)
      advance()
      if (tok.kind != ";" && tok.kind != "}" && !got_lf) {
        expr = parse_expr()
        node.expression = expr
      }
      expect_semi()
      ast_node_end(node)
      return node
    }

    if (k == "disrupt") {
      node = ast_node("disrupt", start)
      advance()
      expect_semi()
      ast_node_end(node)
      return node
    }

    if (k == "break") {
      node = ast_node("break", start)
      advance()
      if (tok.kind == "name" && !got_lf) {
        node.name = tok.value
        advance()
      }
      expect_semi()
      ast_node_end(node)
      return node
    }

    if (k == "continue") {
      node = ast_node("continue", start)
      advance()
      if (tok.kind == "name" && !got_lf) {
        node.name = tok.value
        advance()
      }
      expect_semi()
      ast_node_end(node)
      return node
    }

    if (k == "function") {
      return parse_function_inner()
    }

    if (k == ";") {
      advance()
      return null
    }

    if (k == "name") {
      if (tok.value == "try" || tok.value == "catch" || tok.value == "finally") {
        parse_error(start, "'" + tok.value + "' is not supported; use disrupt/disruption instead")
        sync_to_statement()
        return null
      }
      if (tok.value == "throw") {
        parse_error(start, "'throw' is not supported; use disrupt instead")
        sync_to_statement()
        return null
      }
      if (tok.value == "class") {
        parse_error(start, "'class' is not supported; use meme()/proto() instead")
        sync_to_statement()
        return null
      }
      if (tok.value == "new") {
        parse_error(start, "'new' is not supported; use meme()/proto() instead")
        sync_to_statement()
        return null
      }
      if (tok.value == "switch" || tok.value == "case") {
        parse_error(start, "'" + tok.value + "' is not supported; use if/else instead")
        sync_to_statement()
        return null
      }
      if (tok.value == "let" || tok.value == "const") {
        parse_error(start, "'" + tok.value + "' is not supported; use var/def instead")
        sync_to_statement()
        return null
      }
      p1_tok = peek_ahead(1)
      if (p1_tok.kind == ":") {
        node = ast_node("label", start)
        node.name = tok.value
        advance() // skip identifier
        advance() // skip colon
        labeled_stmt = parse_statement()
        node.statement = labeled_stmt
        ast_node_end(node)
        return node
      }
    }

    expr = parse_expr()
    if (expr != null) {
      node = ast_node("call", start)
      node.expression = expr
      ast_node_end(node)
      expect_semi()
      return node
    }
    parse_error(start, "unexpected token at start of statement")
    return null
  }

  // ============================================================
  // Program
  // ============================================================

  var parse_program = function() {
    var root = {kind: "program", filename: filename}
    var functions = []
    var statements = []
    var before = 0
    var stmt = null
    root.functions = functions
    root.statements = statements

    while (tok.kind != "eof") {
      before = cursor
      stmt = parse_statement()
      if (stmt != null) {
        if (stmt.kind == "function") {
          push(functions, stmt)
        } else {
          push(statements, stmt)
        }
      } else if (cursor == before) {
        sync_to_statement()
      }
    }
    return root
  }

  // ============================================================
  // Semantic Analysis
  // ============================================================

  var sem_errors = []
  var scopes_array = []
  var intrinsics = []

  var sem_error = function(node, msg) {
    var err = {message: msg}
    if (node.from_row != null) err.line = node.from_row + 1
    if (node.from_column != null) err.column = node.from_column + 1
    push(sem_errors, err)
  }

  var make_scope = function(parent, fn_nr, opts) {
    return {
      parent: parent,
      vars: [],
      in_loop: opts.in_loop == true,
      function_nr: fn_nr,
      is_function_scope: opts.is_func == true,
      func_node: null,
      has_inner_func: false
    }
  }

  var sem_add_var = function(scope, name, make_opts) {
    push(scope.vars, {
      name: name,
      is_const: make_opts.is_const == true,
      make: make_opts.make,
      function_nr: make_opts.fn_nr,
      nr_uses: 0,
      closure: 0
    })
  }

  var sem_lookup_var = function(scope, name) {
    var result = {v: null, level: 0, def_function_nr: -1}
    var cur_fn = scope.function_nr
    var s = scope
    var i = 0
    while (s != null) {
      i = 0
      while (i < length(s.vars)) {
        if (s.vars[i].name == name) {
          result.v = s.vars[i]
          result.def_function_nr = s.vars[i].function_nr
          return result
        }
        i = i + 1
      }
      if (s.parent != null && s.parent.function_nr != cur_fn) {
        result.level = result.level + 1
        cur_fn = s.parent.function_nr
      }
      s = s.parent
    }
    return result
  }

  var sem_find_var = function(scope, name) {
    var r = sem_lookup_var(scope, name)
    return r.v
  }

  var sem_in_loop = function(scope) {
    var s = scope
    while (s != null) {
      if (s.in_loop) return true
      s = s.parent
    }
    return false
  }

  var sem_find_func_scope = function(scope) {
    var s = scope
    while (s != null) {
      if (s.is_function_scope) return s
      s = s.parent
    }
    return null
  }

  var sem_add_intrinsic = function(name) {
    if (find(intrinsics, name) == null) push(intrinsics, name)
  }

  var functino_names = {
    "+!": true, "-!": true, "*!": true, "/!": true, "%!": true, "**!": true,
    "<!": true, ">!": true, "<=!": true, ">=!": true, "=!": true, "!=!": true,
    "&!": true, "|!": true, "^!": true, "<<!": true, ">>!": true, ">>>!": true,
    "&&!": true, "||!": true, "~!": true, "[]!": true
  }

  var is_functino_name = function(name) {
    return functino_names[name] == true
  }

  var derive_type_tag = function(expr) {
    if (expr == null) return null
    var k = expr.kind
    if (k == "array") return "array"
    if (k == "record") return "record"
    if (k == "function") return "function"
    if (k == "text" || k == "text literal") return "text"
    if (k == "number") {
      if (is_integer(expr.number)) return "integer"
      return "number"
    }
    if (k == "true" || k == "false") return "logical"
    if (k == "null") return "null"
    return null
  }

  var _assign_kinds = {
    assign: true, "+=": true, "-=": true, "*=": true, "/=": true, "%=": true,
    "<<=": true, ">>=": true, ">>>=": true,
    "&=": true, "^=": true, "|=": true, "**=": true,
    "&&=": true, "||=": true
  }

  var sem_propagate_vars = function(parent, child) {
    parent.vars = array(parent.vars, child.vars)
  }

  var sem_build_scope_record = function(scope) {
    var rec = {function_nr: scope.function_nr}
    var slots = 0
    var close_slots = 0
    var i = 0
    var v = null
    while (i < length(scope.vars)) {
      v = scope.vars[i]
      rec[v.name] = {
        make: v.make,
        function_nr: v.function_nr,
        nr_uses: v.nr_uses,
        closure: v.closure == 1,
        level: 0,
        type_tag: v.type_tag
      }
      slots = slots + 1
      if (v.closure) close_slots = close_slots + 1
      i = i + 1
    }
    return {rec: rec, nr_slots: slots, nr_close: close_slots}
  }

  // Forward declarations
  var sem_check_expr = null
  var sem_check_stmt = null

  var sem_predeclare_vars = function(scope, stmts) {
    var i = 0
    var stmt = null
    var kind = null
    var name = null
    var item = null
    var ik = null
    var j = 0
    while (i < length(stmts)) {
      stmt = stmts[i]
      kind = stmt.kind
      if (kind == "function") {
        name = stmt.name
        if (name != null && sem_find_var(scope, name) == null) {
          sem_add_var(scope, name, {make: "function", fn_nr: scope.function_nr})
        }
      } else if (kind == "var") {
        name = stmt.left.name
        if (name != null && sem_find_var(scope, name) == null) {
          sem_add_var(scope, name, {make: "var", fn_nr: scope.function_nr})
        }
      } else if (kind == "var_list") {
        j = 0
        while (j < length(stmt.list)) {
          item = stmt.list[j]
          ik = item.kind
          if (ik == "var") {
            name = item.left.name
            if (name != null && sem_find_var(scope, name) == null) {
              sem_add_var(scope, name, {make: "var", fn_nr: scope.function_nr})
            }
          }
          j = j + 1
        }
      }
      i = i + 1
    }
  }

  var sem_check_assign_target = function(scope, left_node) {
    if (left_node == null) return null
    var kind = left_node.kind
    var name = null
    var v = null
    var r = null
    var obj_expr = null

    if (kind == "name") {
      name = left_node.name
      if (name == null) return null
      v = sem_find_var(scope, name)
      if (v == null) {
        sem_error(left_node, "cannot assign to unbound variable '" + name + "'")
      } else if (v.is_const) {
        sem_error(left_node, "cannot assign to constant '" + name + "'")
      }
      r = sem_lookup_var(scope, name)
      if (r.v != null) {
        left_node.level = r.level
        left_node.function_nr = r.def_function_nr
        if (r.level > 0) {
          r.v.nr_uses = r.v.nr_uses + 1
          r.v.closure = 1
        }
      } else {
        left_node.level = -1
      }
    } else if (kind == "." || kind == "[") {
      obj_expr = left_node.left
      sem_check_expr(scope, obj_expr)
      if (kind == "[" && left_node.right != null) {
        sem_check_expr(scope, left_node.right)
      }
      // Type error detection for known-type constant objects
      if (obj_expr != null && obj_expr.kind == "name" && obj_expr.name != null) {
        v = sem_find_var(scope, obj_expr.name)
        if (v != null && v.is_const && v.type_tag != null) {
          if (kind == ".") {
            if (v.type_tag == "array") {
              sem_error(left_node, "cannot set property on array '" + obj_expr.name + "'")
            }
          } else if (kind == "[") {
            if (left_node.right == null) {
              // Push: a[] = val
              if (v.type_tag != "array") {
                sem_error(left_node, "push only works on arrays, not " + v.type_tag + " '" + obj_expr.name + "'")
              }
            } else if (v.type_tag == "array") {
              if (left_node.right.kind == "text") {
                sem_error(left_node, "cannot use text key on array '" + obj_expr.name + "'")
              }
            } else if (v.type_tag == "record") {
              if (left_node.right.kind == "number" && is_integer(left_node.right.number)) {
                sem_error(left_node, "cannot use integer key on record '" + obj_expr.name + "'; use text key")
              }
            }
          }
        } else if (v != null && v.is_const && v.type_tag == null) {
          // Infer type_tag from usage pattern (def only)
          if (kind == ".") {
            v.type_tag = "record"
          } else if (kind == "[") {
            if (left_node.right == null) {
              // Push: a[] = val → array
              v.type_tag = "array"
            } else if (left_node.right.kind == "number" && is_integer(left_node.right.number)) {
              v.type_tag = "array"
            } else if (left_node.right.kind == "text") {
              v.type_tag = "record"
            }
          }
        }
      }
    }
  }

  sem_check_expr = function(scope, expr) {
    if (expr == null) return null
    var kind = expr.kind
    if (kind == null) return null
    var name = null
    var r = null
    var i = 0
    var operand = null
    var v = null
    var prop = null
    var val = null
    var fn_nr_val = null
    var fn_scope = null
    var pname = null
    var def_val = null
    var sr = null
    var enclosing = null

    if (_assign_kinds[kind] == true) {
      sem_check_assign_target(scope, expr.left)
      sem_check_expr(scope, expr.right)
      return null
    }

    if (kind == "++" || kind == "--") {
      operand = expr.expression
      if (operand != null && operand.kind == "name") {
        name = operand.name
        if (name != null) {
          v = sem_find_var(scope, name)
          if (v == null) {
            sem_error(expr, "cannot assign to unbound variable '" + name + "'")
          } else if (v.is_const) {
            sem_error(expr, "cannot assign to constant '" + name + "'")
          }
          r = sem_lookup_var(scope, name)
          if (r.v != null) {
            operand.level = r.level
            operand.function_nr = r.def_function_nr
            if (r.level > 0) {
              r.v.nr_uses = r.v.nr_uses + 1
              r.v.closure = 1
            }
          } else {
            operand.level = -1
          }
        }
      } else if (operand != null) {
        sem_check_assign_target(scope, operand)
      }
      return null
    }

    if (kind == "this") {
      if (scope.function_nr == 0) {
        sem_error(expr, "'this' cannot be used at the top level of a program")
      }
      return null
    }

    if (kind == "[") {
      sem_check_expr(scope, expr.left)
      sem_check_expr(scope, expr.right)
      if (expr.right != null) {
        if (expr.right.kind == "number" && is_integer(expr.right.number)) {
          expr.access_kind = "index"
        } else if (expr.right.kind == "text") {
          expr.access_kind = "field"
        }
      }
      return null
    }

    if (kind == "," || kind == "+" || kind == "-" || kind == "*" ||
        kind == "/" || kind == "%" || kind == "==" || kind == "!=" ||
        kind == "<" || kind == ">" || kind == "<=" || kind == ">=" ||
        kind == "&&" || kind == "||" || kind == "&" ||
        kind == "|" || kind == "^" || kind == "<<" || kind == ">>" ||
        kind == ">>>" || kind == "**" || kind == "in" ||
        kind == ".") {
      sem_check_expr(scope, expr.left)
      sem_check_expr(scope, expr.right)
      return null
    }

    if (kind == "then") {
      sem_check_expr(scope, expr.expression)
      sem_check_expr(scope, expr.then)
      sem_check_expr(scope, expr.else)
      return null
    }

    if (kind == "(") {
      sem_check_expr(scope, expr.expression)
      i = 0
      while (i < length(expr.list)) {
        sem_check_expr(scope, expr.list[i])
        i = i + 1
      }
      return null
    }

    if (kind == "!" || kind == "~" || kind == "delete" ||
        kind == "-unary" || kind == "+unary") {
      sem_check_expr(scope, expr.expression)
      return null
    }

    if (kind == "array") {
      i = 0
      while (i < length(expr.list)) {
        sem_check_expr(scope, expr.list[i])
        i = i + 1
      }
      return null
    }

    if (kind == "record") {
      i = 0
      while (i < length(expr.list)) {
        prop = expr.list[i]
        val = prop.right
        sem_check_expr(scope, val)
        if (prop.computed) {
          sem_check_expr(scope, prop.left)
        }
        i = i + 1
      }
      return null
    }

    if (kind == "function") {
      enclosing = sem_find_func_scope(scope)
      if (enclosing != null) enclosing.has_inner_func = true
      fn_nr_val = expr.function_nr
      if (fn_nr_val == null) fn_nr_val = scope.function_nr
      fn_scope = make_scope(scope, fn_nr_val, {is_func: true})
      fn_scope.func_node = expr
      expr.outer = scope.function_nr
      i = 0
      while (i < length(expr.list)) {
        pname = expr.list[i].name
        if (pname != null) sem_add_var(fn_scope, pname, {is_const: true, make: "input", fn_nr: fn_nr_val})
        def_val = expr.list[i].expression
        if (def_val != null) sem_check_expr(fn_scope, def_val)
        i = i + 1
      }
      if (expr.statements != null) {
        sem_predeclare_vars(fn_scope, expr.statements)
        i = 0
        while (i < length(expr.statements)) {
          sem_check_stmt(fn_scope, expr.statements[i])
          i = i + 1
        }
      }
      if (expr.disruption != null) {
        i = 0
        while (i < length(expr.disruption)) {
          sem_check_stmt(fn_scope, expr.disruption[i])
          i = i + 1
        }
      }
      sr = sem_build_scope_record(fn_scope)
      push(scopes_array, sr.rec)
      expr.nr_slots = sr.nr_slots
      expr.nr_close_slots = sr.nr_close
      return null
    }

    if (kind == "text literal") {
      i = 0
      while (i < length(expr.list)) {
        sem_check_expr(scope, expr.list[i])
        i = i + 1
      }
      return null
    }

    if (kind == "name") {
      name = expr.name
      if (name != null) {
        if (is_functino_name(name)) {
          expr.make = "functino"
          expr.level = -1
          return null
        }
        r = sem_lookup_var(scope, name)
        if (r.v != null) {
          expr.level = r.level
          expr.function_nr = r.def_function_nr
          r.v.nr_uses = r.v.nr_uses + 1
          if (r.level > 0) r.v.closure = 1
        } else {
          expr.level = -1
          expr.intrinsic = true
          sem_add_intrinsic(name)
        }
      }
      return null
    }
  }

  sem_check_stmt = function(scope, stmt) {
    if (stmt == null) return null
    var kind = stmt.kind
    if (kind == null) return null
    var name = null
    var existing = null
    var i = 0
    var loop_scope = null
    var do_scope = null
    var for_scope = null
    var init_kind = null
    var fn_nr_val = null
    var fn_scope = null
    var pname = null
    var def_val = null
    var sr = null
    var enclosing = null
    var func_scope = null
    var tt = null

    if (kind == "var_list") {
      i = 0
      while (i < length(stmt.list)) {
        sem_check_stmt(scope, stmt.list[i])
        i = i + 1
      }
      return null
    }

    if (kind == "var") {
      name = stmt.left.name
      if (name != null) {
        existing = sem_find_var(scope, name)
        if (existing != null && existing.is_const) {
          sem_error(stmt.left, "cannot redeclare constant '" + name + "'")
        }
        if (existing == null || existing.function_nr != scope.function_nr) {
          sem_add_var(scope, name, {make: "var", fn_nr: scope.function_nr})
        }
      }
      sem_check_expr(scope, stmt.right)
      return null
    }

    if (kind == "def") {
      name = stmt.left.name
      if (name != null) {
        existing = sem_find_var(scope, name)
        if (existing != null && existing.is_const) {
          sem_error(stmt.left, "cannot redeclare constant '" + name + "'")
        } else if (existing != null && !existing.is_const && existing.function_nr == scope.function_nr) {
          existing.is_const = 1
          existing.make = "def"
        } else {
          sem_add_var(scope, name, {is_const: true, make: "def", fn_nr: scope.function_nr})
        }
      }
      sem_check_expr(scope, stmt.right)
      if (name != null) {
        tt = derive_type_tag(stmt.right)
        if (tt != null && tt != "null") {
          existing = sem_find_var(scope, name)
          if (existing != null) existing.type_tag = tt
        }
      }
      return null
    }

    if (kind == "call") {
      sem_check_expr(scope, stmt.expression)
      return null
    }

    if (kind == "if") {
      sem_check_expr(scope, stmt.expression)
      i = 0
      while (i < length(stmt.then)) {
        sem_check_stmt(scope, stmt.then[i])
        i = i + 1
      }
      i = 0
      while (i < length(stmt.list)) {
        sem_check_stmt(scope, stmt.list[i])
        i = i + 1
      }
      if (stmt.else != null) {
        i = 0
        while (i < length(stmt.else)) {
          sem_check_stmt(scope, stmt.else[i])
          i = i + 1
        }
      }
      return null
    }

    if (kind == "while") {
      sem_check_expr(scope, stmt.expression)
      loop_scope = make_scope(scope, scope.function_nr, {in_loop: true})
      i = 0
      while (i < length(stmt.statements)) {
        sem_check_stmt(loop_scope, stmt.statements[i])
        i = i + 1
      }
      sem_propagate_vars(scope, loop_scope)
      return null
    }

    if (kind == "do") {
      do_scope = make_scope(scope, scope.function_nr, {in_loop: true})
      i = 0
      while (i < length(stmt.statements)) {
        sem_check_stmt(do_scope, stmt.statements[i])
        i = i + 1
      }
      sem_propagate_vars(scope, do_scope)
      sem_check_expr(scope, stmt.expression)
      return null
    }

    if (kind == "for") {
      for_scope = make_scope(scope, scope.function_nr, {in_loop: true})
      if (stmt.init != null) {
        init_kind = stmt.init.kind
        if (init_kind == "var" || init_kind == "def") {
          sem_check_stmt(for_scope, stmt.init)
        } else {
          sem_check_expr(for_scope, stmt.init)
        }
      }
      sem_check_expr(for_scope, stmt.test)
      sem_check_expr(for_scope, stmt.update)
      i = 0
      while (i < length(stmt.statements)) {
        sem_check_stmt(for_scope, stmt.statements[i])
        i = i + 1
      }
      sem_propagate_vars(scope, for_scope)
      return null
    }

    if (kind == "go") {
      sem_check_expr(scope, stmt.expression)
      if (stmt.expression == null || stmt.expression.kind != "(") {
        sem_error(stmt, "'go' must be followed by a function call")
      } else {
        func_scope = sem_find_func_scope(scope)
        if (func_scope != null && func_scope.func_node != null) {
          if (func_scope.func_node.disruption != null) {
            sem_error(stmt, "cannot use 'go' in a function with a disruption clause")
          }
          if (func_scope.has_inner_func) {
            sem_error(stmt, "cannot use 'go' in a function that defines inner functions")
          }
        }
        stmt.tail = true
      }
      return null
    }

    if (kind == "return") {
      sem_check_expr(scope, stmt.expression)
      if (stmt.expression != null && stmt.expression.kind == "(") {
        stmt.tail = true
      }
      return null
    }

    if (kind == "disrupt") {
      return null
    }

    if (kind == "break") {
      if (!sem_in_loop(scope)) {
        sem_error(stmt, "'break' used outside of loop")
      }
      return null
    }

    if (kind == "continue") {
      if (!sem_in_loop(scope)) {
        sem_error(stmt, "'continue' used outside of loop")
      }
      return null
    }

    if (kind == "block") {
      i = 0
      while (i < length(stmt.statements)) {
        sem_check_stmt(scope, stmt.statements[i])
        i = i + 1
      }
      return null
    }

    if (kind == "label") {
      sem_check_stmt(scope, stmt.statement)
      return null
    }

    if (kind == "function") {
      enclosing = sem_find_func_scope(scope)
      if (enclosing != null) enclosing.has_inner_func = true
      name = stmt.name
      if (name != null && sem_find_var(scope, name) == null) sem_add_var(scope, name, {make: "function", fn_nr: scope.function_nr})
      fn_nr_val = stmt.function_nr
      if (fn_nr_val == null) fn_nr_val = scope.function_nr
      fn_scope = make_scope(scope, fn_nr_val, {is_func: true})
      fn_scope.func_node = stmt
      stmt.outer = scope.function_nr
      i = 0
      while (i < length(stmt.list)) {
        pname = stmt.list[i].name
        if (pname != null) sem_add_var(fn_scope, pname, {is_const: true, make: "input", fn_nr: fn_nr_val})
        def_val = stmt.list[i].expression
        if (def_val != null) sem_check_expr(fn_scope, def_val)
        i = i + 1
      }
      sem_predeclare_vars(fn_scope, stmt.statements)
      i = 0
      while (i < length(stmt.statements)) {
        sem_check_stmt(fn_scope, stmt.statements[i])
        i = i + 1
      }
      if (stmt.disruption != null) {
        i = 0
        while (i < length(stmt.disruption)) {
          sem_check_stmt(fn_scope, stmt.disruption[i])
          i = i + 1
        }
      }
      sr = sem_build_scope_record(fn_scope)
      push(scopes_array, sr.rec)
      stmt.nr_slots = sr.nr_slots
      stmt.nr_close_slots = sr.nr_close
      return null
    }
  }

  var semantic_check = function(ast) {
    var global_scope = make_scope(null, 0, {is_func: true})
    var i = 0
    var stmt = null
    var name = null
    var sr = null
    var new_scopes = null

    i = 0
    while (i < length(ast.functions)) {
      name = ast.functions[i].name
      if (name != null) sem_add_var(global_scope, name, {make: "function", fn_nr: 0})
      i = i + 1
    }

    i = 0
    while (i < length(ast.statements)) {
      sem_check_stmt(global_scope, ast.statements[i])
      i = i + 1
    }

    i = 0
    while (i < length(ast.functions)) {
      sem_check_stmt(global_scope, ast.functions[i])
      i = i + 1
    }

    sr = sem_build_scope_record(global_scope)
    new_scopes = [sr.rec]
    i = 0
    while (i < length(scopes_array)) {
      push(new_scopes, scopes_array[i])
      i = i + 1
    }
    scopes_array = new_scopes

    ast.scopes = scopes_array
    ast.intrinsics = intrinsics
    if (length(sem_errors) > 0) {
      ast.errors = sem_errors
    }
  }

  // ============================================================
  // Main
  // ============================================================

  init_cursor()
  var ast = parse_program()

  if (error_count == 0) {
    semantic_check(ast)
  }

  // Merge parse errors
  var _mi = 0
  if (length(errors) > 0) {
    if (ast.errors != null) {
      _mi = 0
      while (_mi < length(errors)) {
        push(ast.errors, errors[_mi])
        _mi = _mi + 1
      }
    } else {
      ast.errors = errors
    }
  }

  return ast
}

return parse
