#!/usr/bin/env ruby
# Copyright (c) 2024 Darren Smith. All rights reserved.
# 
# BSD 3 License
# 
# Redistribution and use in source and binary forms are permitted provided that
# the above copyright notice and this paragraph are duplicated in all such forms
# and that any documentation, advertising materials, and other materials related
# to such distribution and use acknowledge that the software was developed by
# Darren Smith. The name of Darren Smith may not be used to endorse or promote
# products derived from this software without specific prior written permission.
# THIS SOFTWARE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR IMPLIED
# WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTIES OF
# MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.

# lex.rb #######################################################################
class Token < Struct.new(:str, :type, :char_no, :line_no, :explicit_end, :surrounded_by_whitespace)
  def split
    trailing_commas = str[/,*$/]
    str_base = $`
    c = char_no
    results = str_base.scan(/[^1-9]|[0-9]+/).map{|s|
      t = Token.new(s, s=~/[0-9]/ ? :int : :id, c, line_no)
      c += s.size
      t
    }

    if !trailing_commas.empty?
      if results[-1].str =~ /[a-z][A-Z]/
        results[-1].str += trailing_commas
      else
        results << Token.new(trailing_commas, :commas, c, line_no)
      end
    end

    results
  end
end
def split_tokens(tokens, str)
  tokens.slice_when{|t| t.str == str }.
    map{|ts| ts.reject{|t| t.str == str } }
end
AllSymbols='@!?`~#%^&*-_=+[]|;<>.,()\'"{}$/\\:'.chars.map{|c|Regexp.escape c}

TokenTypes = {
  :comment => /# .*|\n#\n/, # also match \n#\n since many editors remove trailing spaces on save
  :int => /0|[0-9]+/,
  :char => /'(\\n|\\0|\\x[0-9a-fA-F][0-9a-fA-F]|.)?/m,
  :str => /"(\\.|[^"])*"?/m,
  :space => /[ \t]/,
  :newline => /\r\n|\r|\n/,
  :id => /[^#{AllSymbols.join} \t\n\r0-9][^#{AllSymbols.join} \t\n\r]*,*/,
  :commas => /,+/,
  :sym => /.,*/,
}
NewlineRx = TokenTypes[:newline]

LexRx = /#{TokenTypes.map{|name, rx|"(?<#{name}>#{rx.to_s})"}*"|"}/

def lex(code, line_no=1)
	tokens = []
  char_no = 1
  code.scan(LexRx) {
    match = $&
    type = $~.named_captures.select{|k, v|v}.keys[0].to_sym
    token=Token.new(match, type, char_no, line_no)
    token.surrounded_by_whitespace = [$`[-1], $'[0]].all?{|s| (s||" ") =~ /\s/ }
    line_no += match.scan(NewlineRx).size
    if match[NewlineRx]
      char_no = match.size-match.rindex(NewlineRx)
    else
      # FYI this counts tab as 1, and utf8 characters as len of their bytes, could be misleading
      char_no += match.size
  	end
    tokens << token if ![:space, :newline, :comment].include? type
  }
  tokens
end

# error.rb #####################################################################
def warn(msg, from=nil)
  $stderr.puts "WARNING: ".red + to_location(from) + msg
end

def to_location(from)
  op = nil
  token = case from
    when Token
      from
    when AST
      from.token
    when IR
      op = from.op.name
      from.token
    when NilClass
      nil
    else
      raise "unknown location type %p " % from
    end

  if token
    "%s:%s (%s) " % [token.line_no||"?", token.char_no||"?", op && op.downcase != token.str.downcase ? "#{token.str} #{op}" : token.str]
  else
    ""
  end
end

class String
  def red
    "\e[31m#{self}\e[0m"
  end
end

class IogiiError < StandardError
  def initialize(message,from=nil)
    @message = message
    @from = from
  end
  def message
    "ERROR: ".red + to_location(@from) + @message
  end
end

# escape.rb ####################################################################
def inspect_char(char)
  return "'\"" if char=='"'.ord # Don't escape this char (we would in a string)
  "'" + escape_str_char(char)
end

def escape_str_char(char)
  return "\\0" if char == "\0".ord
  return "\\n" if char == "\n".ord
  return "\\\\" if char == "\\".ord
  return "\\\"" if char == "\"".ord
  return char.to_i.chr if char >= " ".ord && char <= "~".ord # all ascii printables
  return "\\x0%s" % char.to_i.to_s(16) if char < 16 && char >= 0
  return "\\x%s" % (char%256).to_i.to_s(16) if char < 256 && char >= -2
  return "\\{%d}" % char
end

def parse_char(s)
  ans,offset=parse_str_char(s,0)
  raise "internal char error" if ans.size != 1 || offset != s.size
  ans[0].ord
end

def parse_str_char(s,i) # parse char in a string starting at position i
  if s[i]=="\\"
    if s.size <= i+1
      ["\\",i+1]
    elsif s[i+1] == "n"
      ["\n",i+2]
    elsif s[i+1] == "0"
      ["\0",i+2]
    elsif s[i+1] == "\\"
      ["\\",i+2]
    elsif s[i+1] == "\""
      ["\"",i+2]
    elsif s[i+1,3] =~ /x[0-9a-fA-F][0-9a-fA-F]/
      [s[i+2,2].to_i(16).chr,i+4]
    else
      ["\\",i+1]
    end
  else
    [s[i],i+1]
  end
end

def parse_str(s)
  i=0
  r=""
  while i<s.size
    c,i=parse_str_char(s,i)
    r<<c
  end
  r
end

# type.rb ######################################################################
# Prefixed so that they can be compared
EmptyType = :t0_empty # the type of an empty list, it can be anything
IntType = :t1_int
CharType = :t2_char
XSpec = :t3_s_x
ASpec = :t4_s_a
BSpec = :t5_s_b

def type_spec_parts(spec)
  spec.gsub("->"," ").split
end

def concrete_rank(spec)
  !(spec == ASpec || spec == BSpec) # todo what about empty?
end

def concrete_type(spec)
  !(spec == ASpec || spec == BSpec || spec == XSpec)
end

def spec_rank(part)
  t = spec_type(part)
  r = part.scan('[').size
  concrete_rank(t) ? r : ~r
end

def spec_type(part)
  case part.tr('[]','')
  when "x"
    XSpec
  when "a"
    ASpec
  when "b"
    BSpec
  when "int"
    IntType
  when "char"
    CharType
  when ""
    EmptyType
  else raise "unknown arg type" % part
  end
end

def type_to_str(sym)
  sym.to_s.sub(/^.*_/,'')
end
def type_and_rank_to_str(type,rank)
  r = "["*rank + type_to_str(type) + "]"*rank
  r == "[empty]" ? "[]" : r
end

def spec_matches(specs, args)
  specs.zip(args, coerce?(specs, args)).map{|s, a, c|
    spec_match(s, a) ? (c ? :m1_coerces : :m0_matches) : :m2_no_match
  }
end

def spec_match(spec, arg)
  return false if arg == EmptyType && (spec == IntType || spec == CharType)
  return false if spec == EmptyType && arg != EmptyType
  !(spec == IntType && arg == CharType)
end

def same_spec(specs, args, target)
  args.filter.with_index{|a,i| specs[i] == target }
end

def spec_to_ret(way, args)
  if concrete_type(way.ret_type)
    way.ret_type
  else
    same_spec(way.arg_types, args,  way.ret_type).max || EmptyType
  end
end

def coerce?(specs,args)
  specs.zip(args).map{|s,a|
    if s == CharType && a == IntType
      true
    else
      !concrete_type(s) && a == IntType && same_spec(specs, args, s).any?{|o| o == CharType }
    end
  }
end


# version.rb ###################################################################
Version = "0.0-alpha+ (unstable)"

# ops.rb #######################################################################

class Op < Struct.new(:name, :parse_nargs, :block, :ways)
  def dup
    Op.new(name.dup, parse_nargs, block, ways.map(&:dup))
  end
  def dup_mod(mod)
    return self if mod <= 0
    r = dup
    r.ways.each{|w| w.mod += mod if w.can_mod? }
    r
  end
end

class Way < Struct.new(:arg_ranks, :ret_rank, :arg_types, :ret_type, :mod, :vectorize, :promote, :impl)
  def mod_arg_ranks
    arg_ranks.map{|r| r < 0 ? ~r + mod : r }
  end
  def mod_ret_rank
    ret_rank < 0 ? ~ret_rank + mod : ret_rank
  end
  def dup
    Way.new(arg_ranks.dup, ret_rank, arg_types.dup, ret_type, mod, vectorize, promote, impl)
  end
  def can_mod?
    (arg_ranks + [ret_rank]).any?{|r| r < 0 }
  end
end

def lookup_op(name)
  (0..).each{|mod|
    op = Ops[name]
    return nil if op && mod>0 && !op.ways.any?(&:vectorize)
    return op.dup_mod(mod) if op
    return nil if !name[/,$/]
    name = $`
  }
end

def new_op(name, type_sig, block: false, vectorize: true, promote: true, &impl)
  promote = [promote] unless Array === promote
  parts = type_spec_parts(type_sig)
  *arg_ranks, ret_rank = parts.map{|arg| spec_rank(arg) }
  *arg_types, ret_type = parts.map{|arg| spec_type(arg) }

  way = Way.new(arg_ranks, ret_rank, arg_types, ret_type, 0, vectorize, promote, impl)
  Op.new(name, arg_ranks.size, block, [way])
end

Ops = {}
OpExamples = {}

if !defined? mk_op
  def mk_op(names, type_sig, example="", block: false, vectorize: true, promote: true, &impl)
    OpExamples[names] = example if example != ""
    names = names[/.[^(]*/].split

    op = new_op(names[-1], type_sig, block: block, vectorize: vectorize, promote: promote, &impl)

    names.each{|name|
      if Ops[name]
        Ops[name].ways << op.ways[0]
      else
        Ops[name] = op.dup
      end
    }

    if op.ways[0].can_mod? && op.ways[0].vectorize
      mod_op = op.dup
      mod_op.ways[0].mod = 1

      names.each{|name|
        if name =~ /^[a-z]/
          name = name.sub(/^./){|first|first.upcase}
          if Ops[name]
            Ops[name].ways << mod_op.ways[0]
          else
            Ops[name] = mod_op.dup
          end
        end
      }
    end
  end

  def category(name); end
  def quickref_op(names, desc, example)
    OpExamples[names] = example if example != ""
  end
end

NotFirst = :not_first
RepSnd = :rep_second

category("mathy ops")
mk_op("+ add", "int x -> x", '1 2+ -> 3', &add=->a,b{ a.value+b.value })
mk_op("+ add", "char int -> char", "'a 1+ -> 'b", &add)
mk_op("- sub", "x int -> x", '5 3- -> 2', &subt=->a,b{ a.value-b.value })
mk_op("- sub", "char char -> int", "'b 'a- -> 1", &subt)
mk_op("* mult", "int int -> int", '2 3* -> 6'){|a, b| a.value*b.value }
mk_op("/ div", "int int -> int", '7 2/ -> 3'){|a, b| b.value == 0 ? 0 : a.value/b.value }
mk_op("% mod", "x int -> int", '7 2% -> 1'){|a, b| b.value == 0 ? 0 : a.value%b.value }
mk_op("^ pow", "int int -> int", '2 3^ -> 8'){|a, b| pow(a.value, b.value) }
mk_op("~ negate", "int -> int", '1~ -> -1'){|a| -a.value }
mk_op("| abs", "int -> int", '5~| -> 5'){|a| a.value.abs }
mk_op("` str", "int -> [char]", '5` -> "5"'){|a| str(a) }
mk_op("( pred", "x -> x", '4( -> 3'){|a| a.value-1 }
mk_op(") succ", "x -> x", "'a) -> 'b"){|a| a.value+1 }
mk_op("} countTo", "int -> [int]", "3} -> [1,2,3]"){|a| range(1,a.value+1) }
mk_op("{ wholes", "[int]", "{ -> [0,1,2,..."){ range_from(0) }
mk_op("# toBase", "int int -> [int]", '6 2 # -> [1,1,0]'){|a,b| to_base(a.value,b.value) }
mk_op("#, fromBase", "[int] int -> int", '1,1,0 2 #, -> 6', promote: false){|a,b| from_base(a.value,b.value) }
mk_op("_ sum", "[x] -> int", '1,2,3,4 _ -> 10', promote: false){|a| to_strict_list(a).sum }

category("char ops")
mk_op("~ read", "[char] -> int", '"23"~ -> 23'){|a| read_num(a)[0] }
mk_op("} readAll", "[char] -> [int]", '"1,2 3"} -> [1,2,3]'){|a| split_non_digits(a) }
mk_op("| ord", "char -> int", '\'d| -> 100'){|a| a.value }
mk_op("` strip", "[char] -> [char]", '" ab\n"` -> "ab"'){|a| strip(a,$at,$rank) }
mk_op("* replicate", "[char] int -> [char]", '"ab"3* -> "ababab"'){|a, b|
  concat(take(b.value,repeat(a).const).const) }
mk_op("* join", "[[char]] [[char]] -> [char]", '1,2,3 " "* -> "1 2 3"', promote: RepSnd){|a, b| join(a,b)}
mk_op("/ split", "[char] [char] -> [[char]]", '"a  b c " " "/ -> ["a","b","c"]'){|a, b| s=cut(a,repeat(b).const).const; filter(s,s,nil,1) }
mk_op("% cut splitKeepEmpties", "[char] [[char]] -> [[char]]",'"a  b c " " "% -> ["a","","b","c",""]', promote: RepSnd){|a, b| cut(a,b) }

category("generic ops")
mk_op("a append", "[a] [a] -> [a]", '"ab" "cd"a -> "abcd"'){|a, b| append(a,b) }
mk_op("b backwards reverse", "[a] -> [a]", '"abc"b -> "cba"', promote: false){|a| reverse(a) }
mk_op("c cons", "[a] a -> [a]", '1,2 0c -> [0,1,2]'){|a, b| [b, a] }
mk_op("d drop", "[a] int -> [a]", '"abcde" 2d -> "cde"'){|a, b| drop(b.value, a) }
mk_op("g generate ncopies", "a int -> [a]", '4 3g -> [4,4,4]'){|a,b| take(b.value,repeat(a).const) }
mk_op("h head", "[a] -> a", '"abc"h -> \'a', promote: false){|a| z=zero($rank,$at); a.empty ? z : a.value[0].value }
mk_op("j just", "a -> [a]", "1j -> [1]"){|a|[a, [].const]}
mk_op("k keep take", "[a] int -> [a]", '"abcde" 2k -> "ab"'){|a, b| take(b.value, a) }
mk_op("l len", "[a] -> int", '"abc"l -> 3', promote: false){|a| len(a.value) }
mk_op("n not", "a -> int", '0,1,2n -> [1,0,0]', promote: false){|a| truthy($at, $rank, a.value) ? 0 : 1 }
mk_op("O Or o, or,", "int [char] -> [char]", promote: NotFirst){|a,b| truthy($at, 0, a.value) ? str(a) : b.value }
mk_op("o or", "a a -> a", '" bc" \'- o -> "-bc"', promote: NotFirst){|a,b| truthy($at, $rank, a.value) ? a.value : b.value }
mk_op("p nfoldProd", "[[a]] -> [[a]]", '"ab","12" p -> ["a1","a2","b1","b2"]', promote: false){|a| cart_prod(a) }
mk_op("q equal", "a a -> int", "1,2,3 1q -> [1,0,0]"){|a,b| spaceship(a, b, $rank) == 0 ? 1 : 0 }
mk_op("r repeat", "a -> [a]", "1r -> [1,1,1,1,1,1..."){|a| repeat(a) }
mk_op("s sortBy", "[a] [b] -> [a]", '"abc","d":len S -> ["d","abc"]', promote: false){|a,b| sort_by(a,b,$rank) }
mk_op("t tail", "[a] -> [a]", '"abc"t -> "bc"', promote: false){|a| a.empty ? [] : a.value[1].value }
mk_op("u unite concat", "[[a]] -> [a]", '"ab","cd","e"u -> "abcde"', promote: false){|a| concat(a) }
mk_op("v vet filter", "[a] [b] -> [a]", '"abcdef":2%v -> "ace"'){|a,b| filter(a,b,$bt,$rank) }
mk_op("w while takeWhile", "[a] [b] -> [a]", '"abc def":w -> "abc"', promote: false){|a,b| take_while(a,b,$bt,$rank) }
mk_op("x exclude setDiff", "[a] [a] -> [a]", '"abra" "ra"x -> "ba"'){|a,b| set_diff(a,b) }
mk_op("y and", "a b -> b", '" ab" "123"y -> " 23"', promote: NotFirst){|a,b| z=zero($rank,$bt); truthy($at, $rank, a.value) ? b.value : z }
mk_op("z zenith last", "[a] -> a", '"abc"z -> \'c', promote: false){|a| z=zero($rank,$at); last(a, z.const) }
mk_op("< lessThan", "a a -> int", '1,2,3 2< -> [1,0,0]'){|a,b| spaceship(a,b,$rank) == -1 ? 1 : 0 }
mk_op("\\ transpose", "[[a]] -> [[a]]", '"abc","def"\\ -> ["ad","be","cf"]', promote: false){|a| transpose(a) }
mk_op("& reshape", "[a] [int] -> [[a]]", '"abcdef" 2& -> ["ab","cd","ef"]', promote: RepSnd){|a,b| reshape(a,b) }
mk_op("? debut isFirst", "[a] -> [int]", '"aardvark"? -> [1,0,1,1,1,0,0,1]', promote: false){|a| isuniq(a) }
mk_op(". chunkWhen", "[a] [b] -> [[a]]", '"ab cd e":. -> ["ab ","cd ","e"]', promote: false){|a,b| chunk_while(a,b,$bt,$rank) }
mk_op("if", "a b b -> b", '0 1 2 if -> 2', promote: NotFirst){|a,b,c| truthy($at, $rank, a.value) ? b.value : c.value }
mk_op("nil", "[a]", 'nil -> []'){ [] }

category("folding ops") # (return type is type yielded)
mk_op("i iterate", "a -> [a]", '0 i 1,2,3+ -> [0,1,3,6]', block: true)
mk_op("e expand iterate0", "[a]", 'e 1,2,3+ -> [0,1,3,6]', block: true)
mk_op("f foldr", "a -> [a]", '0 f 1,2,3+ -> 6', block: true)
mk_op("m meld foldr0", "[a]", 'm 1,2,3+ -> 6', block: true)

category("stack ops")
mk_op(": dup", "a -> a", "5: -> 5 5")
mk_op("; mdup", "a -> a", "5;1- -> 4 5", block: true)

category("special symbols")
quickref_op("$", "arg", "5;$+ -> 10 5")
quickref_op("!", "pop from outer stack", "2 5;!+ -> 7 5")
quickref_op(">", "end block", "3;))> -> 5 3")
quickref_op(",", "unvec", '"ab"j, -> ["ab"]')
quickref_op("@", "register get/set", "@( 5@) -> 4 6")
quickref_op("[ ]", "push pop", "5[( ]) -> 4 6")
quickref_op("=", "assignment", "5=a( a) -> 4 6")

category("low rank overloads") # available W H and likely more
mk_op("B rangeFrom rangeBegin", "x -> [x]", "2B -> [2,3,4,..."){|a| range_from(a.value) }
mk_op("T rangeTo", "int -> [int]", "3T -> [0,1,2]"){|a| range(0,a.value) }
mk_op("H sqrt", "int -> int", "9H -> 3"){|a| square_root(a.value, 2)  }
mk_op("A min", "x x -> x", '4 5 A -> 4'){|a,b| spaceship(a,b,$rank) <= 0 ? a.value : b.value }
mk_op("X max", "x x -> x", '"bc" "ad" X -> "bd"'){|a,b| spaceship(a,b,$rank) >= 0 ? a.value : b.value }
mk_op("Z pow10", "int -> int", "2Z -> 100"){|a| pow(10, a.value) }
mk_op("P prod", "[int] -> int", '1,2,3,4 P -> 24', promote: false){|a| to_strict_list(a).inject(1,&:*) }
mk_op("\\ digits", "int -> [int]", '123 \\ -> [1,2,3]'){|a| v=to_base(a.value,10); v.empty? ? [0.const, Null] : v }
mk_op("U undigits", "[int] -> int", '1,2,3 U -> 123', promote: false){|a| undigits(a) }

category("debugging ops")
mk_op("stdin", "[char]", "stdin => <stdin>"){ ReadStdin.value }
mk_op("pad", "[a] a -> [a]", '1,2 3 pad -> [1,2,3,3,3...'){|a, b| padv(a, b) }
mk_op("show", "a -> [char]", '1,2 show -> "[1,2]"', vectorize: false, &show=->a{ inspectv($at, $rank, a) })
mk_op("type", "a -> [char]", '1,2 type -> "[int]"', vectorize: false){|a| str_to_lazy_list(type_and_rank_to_str($at, $rank)) }
mk_op("version", "[char]", "version -> \"#{Version}\""){ str_to_lazy_list(Version) }
mk_op("pp", "a -> [char]", '1,2 => prints [1,2]', vectorize: false, &show)
mk_op("nuke", "a -> a", '1,2 nuke -> ', vectorize: false){|a| raise}

category("internal ops")
mk_op("lines", "[char] -> [[char]]"){|a| lines(a) }
mk_op("superpad", "[a] -> [a]", promote: false){ raise }
mk_op("zero", "[] -> int"){|a| 0 } # default to int in fold0s
mk_op("zerom", "[a] -> a"){|a| zero($rank,$at) } # this is used for rank1+ fold0s
mk_op("zero", "[a] -> a"){|a| zero($rank,$at) }

# todo do this without duplicating logic/names
category("intuitive special promotes (don't need to show them in quickref)")
mk_op("* join", "[char] [[char]] -> [char]", promote: [RepSnd,NotFirst]){|a, b| join(map(a){|i| [i,Null] }.const,b)} # ideally don't promote first at all
mk_op("S SortBy sortBy, s,", "[[a]] [b] -> [[a]]", promote: false){|a,b| sort_by(a,b,$rank) }
mk_op("V Vet Filter vet, filter, v,", "[[a]] [b] -> [[a]]", promote: false){|a,b| filter(a,b,$bt,$rank) }
mk_op("W While TakeWhile w, while, takeWhile,", "[[a]] [b] -> [[a]]", promote: false){|a,b| take_while(a,b,$bt,$rank-1) }
mk_op("., ChunkWhen chunkWhen,", "[[a]] [b] -> [[[a]]]", promote: false){|a,b| chunk_while(a,b,$bt,$rank) }
mk_op("If if,", "[a] b b -> b", promote: NotFirst){|a,b,c| truthy($at, $rank+1, a.value) ? b.value : c.value }

# parse.rb #####################################################################

AST = Struct.new(:token, :args)
BlockAST = Struct.new(:token, :args, :block, :block_arg)
ArgNode = Struct.new(:arg_ind)
VarNode = Struct.new(:name)
DataNode = Struct.new(:impl)

class Stack
  def initialize(arg_optional, arg_ind)
    @stack = [arg_ind]
    @arg_needed = !arg_optional
    @negative_used = 0
  end
  def initialize_copy(from)
    @stack = from.stack
    @arg_needed = from.arg_needed
    @negative_used = negative_used
  end
  def push(ind)
    @stack << ind
    self
  end
  attr_reader :stack, :arg_needed, :negative_used
  def pop
      # return node inds lazily since negative stack size means future values that don't exist yet
    if @stack.empty?
      stack_ind = @negative_used += 1
      ->{@stack[-stack_ind]} # only valid once stack size is finalized
    else
      @arg_needed |= @stack.size == 1
      @stack.pop
    end
  end
  def peek
    r = pop
    push r
    r
  end
  def size
    @stack.size - @negative_used
  end
  def finalize(is_main)
    @stack.shift if !@arg_needed
    raise IogiiError.new "stack size must be >=0" if is_main && size < 0
    is_main ? @stack[0,size] : size == 1 && @stack[0]
  end
end

def parse_main(tokens)
  registers = get_var_ids_set(tokens)
  tokens = split_ids(tokens, registers)
  add_implicit_ends(tokens, registers)
  nodes = []
  preprints = []
  main_inds, block_arg_ind, token_ind=*parse(tokens, 0, nodes, true, nil, [], registers, false, true, preprints)
  raise if block_arg_ind != 0 # ir assumes 0 => 0
  raise IogiiError.new "program has an extra >", tokens[token_ind-1] if token_ind<=tokens.size
  nodes.each{|node|
    if AST === node || BlockAST === node
      node.args.map!{|a|a=a.call while Proc===a; a}
    end
    if BlockAST === node
      node.block = node.block.call while Proc === node.block
      node.block_arg = node.block_arg.call while Proc === node.block_arg
    end
    if ArgNode === node
      node.arg_ind = node.arg_ind.call while Proc === node.arg_ind
    end
  }
  main_inds.map!{|a| a=a.call while Proc===a; a}
  preprints.map!{|a| a=a.call while Proc===a; a}
  registers.each{|k,v| registers[k] = v = v.call while Proc===v }
  [nodes, main_inds, registers, preprints]
end

def split_ids(tokens, registers)
  tokens.map{|t|
    if t.type==:id && !lookup_op(t.str) && !registers[t.str]
      warn("splitting %p into chars because no op/var by that name, but it is surrounded by whitespace indicating this might not be the intent" % t.str, t) if t.surrounded_by_whitespace
      t.split
    else
      t
    end
  }.flatten
end

def get_var_ids_set(tokens)
  set = {}
  tokens.size.times{|i|
    if tokens[i].str == "="
      raise IogiiError.new "a token must follow assignment operator", tokens[i] if !tokens[i+1]
      raise IogiiError.new "cannot set =", tokens[i] if tokens[i+1].str == "="
      set[tokens[i+1].str] = true
    end
  }
  set
end

class DummyStack
  def pop; 0; end
  def peek; 0; end
  def <<(a); self; end
  def empty?; false; end
end
DUMMY_STACK = DummyStack.new

# start at leftmost >
# try each block opener starting at right most, if parse matches, then move on to next >, else mark that opener as needing implicit end, and try previous opener
def add_implicit_ends(tokens, registers)
  begins = []
  ends = registers['>'] ? [] : tokens.size.times.filter{|ti| tokens[ti].str == ">" }
  prev_end = 0
  ends.each.with_index{|e,ei|
    begins += (prev_end...e).filter{|ti| t=tokens[ti]
      !registers[t.str] && (o=lookup_op(t.str)) && o.block
    }
    begin
      raise IogiiError.new "unmatched >", tokens[e] if begins.empty?
      begin_token = tokens[begins.last]
      block_ind, _, _ = *parse(tokens, begins.last + 1, [], false, DUMMY_STACK, DUMMY_STACK, registers, false, false, [])
      begins.pop
    end while !block_ind
    begin_token.explicit_end = true
    prev_end = e
  }
end

def parse(tokens, token_ind, nodes, is_main, outer, var_stack, registers, implicit_end, is_real, preprints)
  stack=Stack.new(is_main, nodes.size)
  nodes << ArgNode.new(arg_ind = nodes.size)

  loop {
    t=tokens[token_ind]
    token_ind+=1
    break if token_ind > tokens.size
    if registers[t.str]
      stack.push nodes.size
      nodes << VarNode.new(t.str)
      next
    end
    if t.str == ">"
      raise IogiiError.new "block needs implicit end but stack size never 1 before explicit >", t if implicit_end
      break
    end
    if t.str == "!"
      raise IogiiError.new "no outer stack in main", t if !outer
      stack.push outer.pop
    elsif t.str == "$"
      stack.push arg_ind
    elsif t.str == "["
      var_stack << stack.peek
    elsif t.str == "]"
      raise IogiiError.new "var stack is empty, cannot pop", t if var_stack.empty?
      stack.push var_stack.pop
    elsif t.str == "="
      registers[tokens[token_ind].str] = stack.peek
      token_ind += 1
    elsif t.str == "@"
      if tokens[token_ind..-1].any?{|t|t.str == "@"} # get
        stack.push nodes.size
        nodes << VarNode.new("@ register")
      else # set
        registers["@ register"] = stack.peek
      end
    else case t.type
    when :int, :char, :str
      data_impl, token_ind = *parse_data(tokens, token_ind)
      stack.push nodes.size
      nodes << DataNode.new(data_impl)
    when :id, :sym
      op = lookup_op(t.str)
      raise IogiiError.new "unknown op", t if !op
      args = op.parse_nargs.times.map{ stack.pop }.reverse
      if op.block
        block_ind, block_arg_ind, token_ind=*parse(tokens, token_ind, nodes, false, stack, var_stack, registers, !t.explicit_end, is_real, preprints)
        nodes << BlockAST.new(t, args, block_ind, block_arg_ind)
      else
        nodes << AST.new(t, args)
      end
      stack.push nodes.size-1
      stack.push args[0] if op.name == "mdup" || op.name == "dup"
      if "pp" == t.str
        preprints << stack.pop
      elsif "nuke" == t.str
        stack.pop
      end
    when :commas
      raise IogiiError.new "commas must follow data or op that can vectorize", t
    else raise "unknown token type %p" % t.type.to_s
    end end
    break if implicit_end && stack.size == 1
  }
  ret_ind = stack.finalize(is_main)
  raise IogiiError.new "block does not reach stack size 1 before program ends" if is_real && !ret_ind
  [ret_ind, arg_ind, token_ind]
end

def parse_data(tokens, token_ind)
  prev_is_datum = false
  tokens = tokens[token_ind-1..-1].take_while{|t|
    if [:int,:char,:str].include?(t.type)
      if prev_is_datum
        false
      else
        prev_is_datum = true
        true
      end
    else
      prev_is_datum = false
      t.type == :commas
    end
  }
  token_ind = token_ind + tokens.size - 1

  depth = tokens.select{|t|t.type == :commas}.map{|t|t.str.size}.max||0
  token_type = nil
  tokens.reject{|t|t.type == :commas}.each{|t|
    token_type ||= t.type
    token_type = :str if token_type != t.type
  }

  value = parse_list(tokens,depth,token_type)
  case token_type
  when :str
    type = CharType
    depth += 1
  when :char
    type = CharType
  when :int
    type = IntType
  else; raise
  end
  [new_op("data", type_and_rank_to_str(type,depth)){ value }, token_ind]
end

def parse_list(tokens,depth,to_type)
  if depth == 0
    raise IogiiError.new 'found ", ," invalid data format' if tokens.empty?
    parse_datum(tokens[0],to_type)
  else
    to_lazy_list(split_tokens(tokens, ","*depth).map{|ts|
      parse_list(ts, depth-1,to_type)
    })
  end
end

def parse_datum(token,to_type)
  case token.type
  when :int
    to_type == :str ? str(token.str.to_i.const) : token.str.to_i
  when :str
    raise IogiiError.new "unterminated string", token if token.str[-1] != '"' || token.str.size==1
    str_to_lazy_list(parse_str(token.str[1...-1]))
  when :char
    raise IogiiError.new "empty char", token if token.str.size < 2
    x = parse_char(token.str[1..-1]).ord
    to_type == :str ? [x.const, Null] : x
  end
end

# ir.rb ########################################################################
IR = Struct.new(:op, :args, :token)

def to_ir(ast_nodes, arg, registers)
  nodes = [IR.new(arg, [], nil)]
  ast2ir = { 0 => 0 }
  ast_nodes.size.times{|ast_ind| to_ir_h(ast_nodes, ast_ind, nodes, ast2ir, registers) }
  replace_vars = lambda{|a| a=a[] while Proc === a; a }
  nodes.each{|n|n.args.map! &replace_vars }
  ast2ir.each{|k,v| ast2ir[k] = replace_vars[v] }
  [nodes, ast2ir]
end

def to_ir_h(ast_nodes, ast_ind, nodes, ast2ir, registers)
  return ast2ir[ast_ind] if ast2ir[ast_ind]
  ir_ind = nil
  ast2ir[ast_ind] = ->{ir_ind}
  ast_node = ast_nodes[ast_ind]
  ir_ind = case ast_node
    when ArgNode
      ->{ ast2ir[ast_node.arg_ind] }
    when VarNode
      ->{ ast2ir[registers[ast_node.name]] }
    when DataNode
      nodes << IR.new(ast_node.impl, [], nil)
      nodes.size - 1
    when AST
      args = ast_node.args.map{|a|to_ir_h(ast_nodes, a, nodes, ast2ir, registers)}
      op = lookup_op(ast_node.token.str)

      if op.name == "dup"
        args[0]
      else
        nodes << new_node = IR.new(op, args, ast_node.token)
        nodes.size - 1
      end
    when BlockAST
      args = ast_node.args.map{|a|to_ir_h(ast_nodes, a, nodes, ast2ir, registers)}
      op = lookup_op(ast_node.token.str)
      mod = op.ways[0].mod

      if op.name == "mdup"
        ast2ir[ast_node.block_arg] = args[0]
        to_ir_h(ast_nodes, ast_node.block, nodes, ast2ir, registers)
      elsif op.name == "iterate"
        nodes << IR.new(Ops["cons"].dup_mod(mod), cons_args=[nil, args[0]], ast_node.token)
        consed_ind = nodes.size - 1
        nodes << IR.new(Ops["superpad"].dup_mod(mod), [nodes.size-1], ast_node.token)
        ast2ir[ast_node.block_arg] = nodes.size - 1
        cons_args[0] = to_ir_h(ast_nodes, ast_node.block, nodes, ast2ir, registers)
        consed_ind
      elsif op.name == "iterate0"
        nodes << IR.new(mod > 0 ? Ops["zerom"].dup_mod(mod) : Ops["zero"], zero_args=[nil], ast_node.token)
        nodes << IR.new(Ops["cons"].dup_mod(mod), cons_args=[nil, nodes.size-1], ast_node.token)
        consed_ind = nodes.size - 1
        nodes << IR.new(Ops["superpad"].dup_mod(mod), [nodes.size-1], ast_node.token)
        ast2ir[ast_node.block_arg] = nodes.size - 1
        cons_args[0] = zero_args[0] = to_ir_h(ast_nodes, ast_node.block, nodes, ast2ir, registers)
        consed_ind
      elsif op.name == "foldr"
        nodes << IR.new(Ops["superpad"].dup_mod(mod), superpad_args=[nil], ast_node.token)
        nodes << IR.new(Ops["pad"].dup_mod(mod), [nodes.size-1, args[0]], ast_node.token)
        nodes << IR.new(Ops["tail"].dup_mod(mod), [nodes.size-1], ast_node.token)
        ast2ir[ast_node.block_arg] = nodes.size - 1
        superpad_args[0] = block_result = to_ir_h(ast_nodes, ast_node.block, nodes, ast2ir, registers)
        nodes << IR.new(Ops["head"].dup_mod(mod), [block_result], ast_node.token)
        nodes.size - 1
      elsif op.name == "foldr0"
        nodes << IR.new(Ops["superpad"].dup_mod(mod), superpad_args=[nil], ast_node.token)
        nodes << IR.new(mod > 0 ? Ops["zerom"].dup_mod(mod) : Ops["zero"], [nodes.size-1], ast_node.token)
        nodes << IR.new(Ops["pad"].dup_mod(mod), [nodes.size-2, nodes.size-1], ast_node.token)
        nodes << IR.new(Ops["tail"].dup_mod(mod), [nodes.size-1], ast_node.token)
        ast2ir[ast_node.block_arg] = nodes.size - 1
        superpad_args[0] = block_result = to_ir_h(ast_nodes, ast_node.block, nodes, ast2ir, registers)
        nodes << IR.new(Ops["head"].dup_mod(mod), [block_result], ast_node.token)
        nodes.size - 1
      else raise
      end
    else raise "unhandled node type %p" % ast_node.class
  end
  ir_ind
end

# infer.rb #####################################################################
def solve_types(ir)
  queue = ir.size.times.to_a

  deps = ir.map{[]}
  queue.each{|i|ir[i].args.each{|a|deps[a]<<i}}
  deps.map!(&:uniq)

  types = ir.map{EmptyType}

  queue.each{|i|
    op = ir[i].op
    arg_types = ir[i].args.map{|a|types[a]}
    begin
      way = find_way(ir[i], op, arg_types)
    rescue IogiiError # no match yet
      next
    end
    result_type = spec_to_ret(way, ir[i].args.map{|a|types[a]})
    # todo handle case where it is < (char char -)
    if result_type > types[i]
      types[i] = result_type
      queue.concat(deps[i])
    elsif result_type < types[i]
      raise IogiiError.new "type invariant has been violated", ir[i] #todo see nitty gritty, test it
    end
  }
  types
end

def find_way(node, op, arg_types, arg_ranks = [0]*arg_types.size)
  x = op.ways.map{|way|
    excess = excess_ranks(way, arg_ranks, arg_types)

    # Prefer to promote 1 arg rather than use vectorized low rank overload
    # todo: don't do this here if there become more specific cases
    excess = [0,0] if ["Append","SetDiff"].any?{|n|Ops[n].ways[0] == way} && excess.sort == [-1,0]

    [ spec_matches(way.arg_types, arg_types).sort.reverse, # most weight on no_matches
      excess.map{|e| [-e, 0].max }.sort.reverse, # only care about when rank too low
      excess.map{|e| [e, 0].max }.sort.reverse, # but still prefer no excessive rank (for special promotes)
      way.arg_types.sort, # for ops that have special case for empty (zero)
      way.mod_arg_ranks.sort # finally look at spec ranks since empty type never has excess
    ]
  }
#   p x if op.name == "zero"
  xmin_at = (0...x.size).min_by{|i| x[i] }
  xmin = x[xmin_at]
  raise(IogiiError.new "op is not defined for base types: " + arg_types.map{|t|type_to_str(t)}*" ", node) if xmin[0][0] == :m2_no_match
  effects = (0...x.size).filter{|i| x[i] == xmin }.map{|i|
    way = op.ways[i]
    [way.impl, way.ret_type, way.ret_rank] }
  raise "internal error, ambiguous op %p: %p" % [node.token.str, op.name] if effects.uniq.size > 1
  op.ways[xmin_at]
end

def find_valid_way(node, arg_types, arg_ranks)
  way = find_way(node, node.op, arg_types, arg_ranks)

  excess = excess_ranks(way, arg_ranks, arg_types)
  excess.each.with_index{|e,i|
    raise IogiiError.new "arg #{i+1} rank too low and op does not promote (see #{NittyGritty}#promotion )", node if e < 0 && (way.promote.include?(false) || way.promote.include?(NotFirst) && i==0) }

  if !way.can_mod? && node.token && node.token.str[-1] == ","
    begin
      simpler_op = lookup_op(node.token.str[0...-1])
      simpler_way = find_way(node, simpler_op, arg_types, arg_ranks) if simpler_op
    rescue IogiiError
    end
    raise IogiiError.new "extra , used", node if simpler_way == way
  end

  way
end

def solve_ranks(ir,types)
  queue = ir.size.times.to_a

  deps = ir.map{[]}
  queue.each{|i|ir[i].args.each{|a|deps[a]<<i}}
  deps.map!(&:uniq)

  ranks = ir.map{0}

  queue.each{|i|
    op = ir[i].op
    arg_types = ir[i].args.map{|a|types[a]}
    arg_ranks = ir[i].args.map{|a|ranks[a]}
    way = find_way(ir[i], op, arg_types, arg_ranks)
    result_rank = zip_level(way, arg_ranks, arg_types) + way.mod_ret_rank
    if result_rank > 100 # todo distinguish between this and legitimate cases (which for all practical purposes will never exist)
      raise IogiiError.new "cannot construct the infinite type", ir[i]
    elsif result_rank > ranks[i]
      ranks[i] = result_rank
      queue.concat(deps[i])
    elsif result_rank < ranks[i]
      raise IogiiError.new "rank invariant has been violated", ir[i] #todo see nitty gritty, test it
    end
  }
  ranks
end

def excess_ranks(way, ranks, types)
  ranks.zip(types, way.mod_arg_ranks, coerce?(way.arg_types,types)).map{|r, t, o, c|
    e = r-o+(c ? 1 : 0)
    e = [0, e].min if !way.vectorize
    e = [0, e].max if t == EmptyType
    e
  }
end

def zip_level(way, ranks, types)
  (excess_ranks(way, ranks, types)<<0).max
end

# lazylib.rb ###################################################################

def zipn(n, a, f)
  return f[*a] if n <= 0 || a==[]
  return [] if a.any?{|i|i.empty}
  [promise{zipn(n-1, a.map{|i|i.value[0]}, f) },
   promise{zipn(n, a.map{|i|i.value[1]}, f) }]
end

class Promise
  def initialize(&block)
    @impl=block
  end
  def value
    @impl=@impl[] if Proc===@impl
    @impl
  end
  def empty
    value.empty?
  end
end

def promise(&b)
  Promise.new(&b)
end
class Object
  def const
    Promise.new{self}
  end
end

def repeat(a)
  (ret = [a]) << ret.const
end

def repn(n, a)
  n<=0 ? a : promise{ repeat(repn(n-1, a)) }
end

def promoten(n,a)
  if n<=0
    a
  else
    [promoten(n-1,a),Null].const
  end
end

def concat_map(v,rhs=Null,first=true,&b)
  if v.empty
    rhs.value
  else
    b[v.value[0],Promise.new{concat_map(v.value[1],rhs,false,&b)},first]
  end
end

def append(v,r)
  v.empty ? r.value : [v.value[0],promise{append(v.value[1], r)}]
end

def join(a,b)
  return [] if a.empty
  append(a.value[0],promise{
    if b.empty || a.value[1].empty
      []
    else
      append(b.value[0], promise{ join(a.value[1], b.value[1]) })
    end
  })
end

def starts_with(a,b) # assumed to be strs, returns remainder if match otherwise nil
  return a if b.empty
  return nil if a.empty || a.value[0].value != b.value[0].value
  starts_with(a.value[1],b.value[1])
end

def cut(a,b) # fyi could be lazier since we know we return a list always
  return [a,Null] if a.empty || b.empty
  if (remainder=starts_with(a,b.value[0]))
    [Null,Promise.new{cut(remainder,b.value[1])}]
  else
    rhs=Promise.new{cut(a.value[1],b)}
    [promise{[a.value[0],Promise.new{rhs.value[0].value}]},promise{rhs.value[1].value}]
  end
end

def last(a, prev)
  until a.empty
    prev = a.value[0]
    a = a.value[1]
  end
  prev.value
end

FalseChars = {0=>1,9=>1,10=>1,11=>1,12=>1,13=>1,32=>1}
def truthy(type, rank, value)
  return !value.empty? if rank > 0
  case type
    when IntType
      return value != 0
    when CharType
      !FalseChars.include?(value)
    else raise
  end
end

def reverse(a,sofar=[])
  return sofar if a.empty
  reverse(a.value[1],[a.value[0],sofar.const])
end

def superpad(n, a)
  if n > 0
    pad(promise { map(a){|e| superpad(n-1, e) } } )
  else
    a.value
  end
end

def pad(a)
  [
    promise{ a.empty ? raise(IogiiError.new("pad value used (see #{NittyGritty}#superpad )")) : a.value[0].value },
    promise{ pad( promise{ a.empty ? [] : a.value[1].value }) }
  ]
end

def padv(a,b)
  [
    promise{ a.empty ? b.value : a.value[0].value },
    promise{ padv( promise{ a.empty ? [] : a.value[1].value }, b) }
  ]
end

def pow(a,b)
  if a == 0 && b < 0
    0
  else
    begin
      (a**b).to_i
    rescue FloatDomainError # e.g. 10000000000 ** 10000000000
      raise IogiiError.new "Overflow"
    end
  end
end

def printv(a, type, rank)
  srank = rank + (type == IntType ? 1 : 0)
  n = [srank == 2 ? 1 : srank - 2, 0].max
  printn(a, n, type, srank - n)
end

def printn(a, n, type, srank)
  if n == 0
    if srank == 0
      prints([a.const, Null])
    elsif srank == 1
      case type
      when EmptyType

      when IntType
        print a
      when CharType
        prints a
      else raise
      end
    elsif srank == 2
      first = true
      until a.empty?
        print " " if !first
        printn(a[0].value, 0, type, 1)
        a = a[1].value
        first = false
      end
    else
      raise
    end
  else
    until a.empty?
      printn(a[0].value, n-1, type, srank)
      puts
      a = a[1].value
    end
  end
end

def prints(value)
  while !value.empty?
    v = value[0].value
    # raw print, multiple bytes will show up as one if utf is used. output will match source code so will look correct if editor encoding=terminal encoding
    raise IogiiError.new("char value, %s, outside of byte range" % v, nil) if v<0 || v>255
    putc v
    value = value[1].value
  end
end

Null = [].const

def str_to_lazy_list(s,rhs=Null)
  to_lazy_list(s.chars.map(&:ord), rhs)
end

def to_lazy_list(l, rhs=Null, ind=0)
  ind >= l.size ? rhs.value : [l[ind].const, Promise.new{to_lazy_list(l, rhs, ind+1)}]
end

def to_eager_str(v)
  to_strict_list(v.const).map{|i|(i.to_i%256).chr}.join
end

def to_strict_list(a)
  sofar=[]
  loop {
    a=a.value
    return a if !(Array===a)
    return sofar if a==[]
    sofar<<to_strict_list(a[0])
    a=a[1]
  }
end

def str(i)
  to_lazy_list(i.value.to_s.bytes.to_a)
end

def is_digit(i)
  i>=48 && i<58
end

def split_non_digits(s)
  return [] if s.empty
  v,found,s2=read_num(s)
  return [] if !found
  [v.const,Promise.new{split_non_digits(s2)}]
end

def read_num(s)
  multiplier=1
  until s.empty || is_digit(s.value[0].value)
    if s.value[0].value == ?-.ord
      multiplier *= -1
    else
      multiplier = 1
    end
    s = s.value[1]
  end
  v = 0
  found_int = false
  until s.empty || !is_digit(s.value[0].value)
    found_int = true
    v = v*10+s.value[0].value-48
    s = s.value[1]
  end

  [multiplier * v, found_int, s]
end

def lines(s)
  return [] if s.empty

  after = Promise.new{lines(s.value[1])}
  if s.value[0].value == 10
    [Null, after]
  else
    [Promise.new{
      after.empty ? [s.value[0], Null] : [s.value[0], after.value[0]]
     },
     Promise.new{
      after.empty ? [] : after.value[1].value
     }]
   end
end

def take(n, a)
  return [] if n < 1 || a.empty
  [a.value[0], Promise.new{ take(n-1, a.value[1]) }]
end

def drop(n, a)
  while n>=1 && !a.empty
    n-=1
    a=a.value[1]
  end
  a.value
end

def len(a)
  return 0 if a.empty?
  return 1+len(a[1].value)
end

def sort_by(a,b,rank)
  fromby(sort(toby(a,b),rank,true))
end

def toby(a,b)
  return Null if a.empty || b.empty
  Promise.new{ [[a.value[0], b.value[0]], toby(a.value[1], b.value[1])] }
end

def fromby(a)
  return [] if a == []
  [Promise.new{a[0][0].value}, Promise.new{fromby(a[1].value)}]
end

# It would be very interesting and useful to design a more lazy sorting algorithm
# so that you can select ith element in O(n) total time after sorting a list.
def sort(a,rank,by=false)
  return [] if a.empty
  return a.value if a.value[1].empty
  n=len(a.value)
  left=take(n/2, a).const
  right=drop(n/2, a).const
  merge(sort(left,rank,by),sort(right,rank,by),rank,by)
end

def merge(a,b,rank,by)
  return b if a==[]
  return a if b==[]
  if (by ? spaceship(a[0][1], b[0][1], rank) : spaceship(a[0], b[0],rank)) <= 0
    [a[0], Promise.new{merge(a[1].value,b,rank,by)}]
  else
    [b[0], Promise.new{merge(a,b[1].value,rank,by)}]
  end
end

def spaceship(a,b,rank)
  if rank>0
    until a.empty && b.empty
      return -1 if a.empty
      return 1 if b.empty
      s0 = spaceship(a.value[0],b.value[0],rank-1)
      return s0 if s0 != 0
      a = a.value[1]
      b = b.value[1]
    end
    return 0
  else
    a.value<=>b.value
  end
end

def inspectv(t,rank,value,rhs=Null)
  if t == CharType && rank == 1
    ['"'.ord.const, Promise.new{
      concat_map(value,Promise.new{str_to_lazy_list('"',rhs)}){|v,r,first|
       str_to_lazy_list(escape_str_char(v.value),r)
      }
    }]
  elsif rank > 0 #List
    ["[".ord.const, Promise.new{
      concat_map(value,Promise.new{str_to_lazy_list("]",rhs)}){|v,r,first|
        first ?
          inspectv(t,rank-1,v,r) :
          [','.ord.const,Promise.new{inspectv(t,rank-1,v,r)}]
      }
    }]
  elsif t == EmptyType
    value.value # take the value so it can throw
    raise
  elsif t == IntType
      str_to_lazy_list(value.value.to_s,rhs)
  elsif t == CharType
    str_to_lazy_list(inspect_char(value.value),rhs)
  else
    raise
  end
end

def filter(a,b,bt,rank)
  return [] if a.empty || b.empty
  if truthy(bt,rank,b.value[0].value)
   [a.value[0],Promise.new{ filter(a.value[1],b.value[1],bt,rank) }]
  else
    filter(a.value[1],b.value[1],bt,rank)
  end
end

def take_while(a,b,bt,rank)
  return [] if a.empty || b.empty
  if truthy(bt,rank,b.value[0].value)
   [a.value[0],Promise.new{ take_while(a.value[1],b.value[1],bt,rank) }]
  else
    []
  end
end

def drop_until(a,b,bt,rank)
  until a.empty || b.empty
    if truthy(bt,rank,b.value[0].value)
      return a.value
    else
      a = a.value[1]
      b = b.value[1]
    end
  end
  []
end

def chunk_while(a,b,btype,rank)
  return [Null, Null] if a.empty
  rest = promise { b.empty ? [Null, Null] : chunk_while(a.value[1], b.value[1],btype,rank) }
  truthy = promise { b.empty || truthy(btype,rank,b.value[0].value) }
  [promise{ [a.value[0], promise{ truthy.value ? rest.value[0].value : []}] },
   promise{ truthy.value ? rest.value[1].value : rest.value }]
end

def strip(a,at,rank)
  lstrip = drop_until(a,a,at,rank)
  b = reverse(lstrip.const).const
  reverse(drop_until(b,b,at,rank).const)
end

# these aren't as lazy as possible, but it gets to use hashes
def isuniq(a,h=Hash.new(-1))
  return [] if a.empty
  [(h[to_strict_list(a.value[0])]+=1) == 0 ? 1.const : 0.const, Promise.new{isuniq(a.value[1], h)}]
end

def set_diff(a,b)
  counts = Hash.new(0)
  to_strict_list(b).each{|be| counts[be] += 1 }
  diff_helper(a, counts)
end

def diff_helper(a, counts)
  while !a.empty && counts[ae=to_strict_list(a.value[0])] > 0
    a = a.value[1]
    counts[ae] -= 1
  end
  return [] if a.empty
  [a.value[0], Promise.new{ diff_helper(a.value[1], counts) }]
end

def range(a,b)
  return [] if a>=b
  [a.const, Promise.new{range(a+1,b)}]
end

def range_from(a)
  [a.const, Promise.new{range_from(a+1)}]
end

def reshape(a,b)
  return [] if a.empty || b.empty
  [Promise.new{ take(b.value[0].value,a) },
   Promise.new{ reshape(Promise.new{ drop(b.value[0].value,a) }, b.value[1]) }]
end

# truncate as soon as encounter empty list
def trunc(a)
  a.empty || a.value[0].empty ? [] : [a.value[0], Promise.new{trunc(a.value[1])}]
end

def map(a,&b)
  a.empty ? [] : [Promise.new{b[a.value[0]]}, Promise.new{map(a.value[1],&b)}]
end

def zero(rank,type=nil)
  return [] if rank > 0
  raise IogiiError.new("use of default value for empty type") if type == EmptyType
  type == CharType ? 32 : 0
end

def concat(a)
  concat_map(a){|i,r,first|append(i,r)}
end

def cart_prod(a)
  return [Null, Null] if a.empty
  rest = promise{ cart_prod(a.value[1]) }
  concat(map(a.value[0]){|e| map(rest){|f|[e, f]} }.const)
end

def transpose(a)
  return [] if a.empty
  return transpose(a.value[1]) if a.value[0].empty
  broken = trunc(a.value[1]).const
  hds = Promise.new{ map(broken){|v|v.value[0].value} }
  tls = Promise.new{ map(broken){|v|v.value[1].value} }
  [Promise.new{[a.value[0].value[0],hds]},
   Promise.new{transpose [a.value[0].value[1],tls].const}]
end

def square_root(x,n)
  raise IogiiError.new "negative square root" if x < 0
  return 0 if x==0
  guess = x
  while (r=(x/guess+guess)/n) < guess
    guess = r
  end
  guess
end

def to_base(a,b)
  raise IogiiError.new "base 0", nil if b==0
  reverse(to_base_h(a.abs,b,a<=>0).const)
end

def to_base_h(a,b,sign)
  return [] if a==0
  digit = a%b*sign
  [digit.const, Promise.new{to_base_h((a-digit*sign)/b,b,sign)}]
end

def from_base(a,b)
  x=0
  until a.empty?
    x=x*b+a[0].value
    a=a[1].value
  end
  x
end

def undigits(a)
  from_base(concat(map(a){|i|
    x = to_base(i.value,10)
    x.empty? ? [0.const, Null] : x
  }.const), 10)
end

ReadStdin = Promise.new{ read_stdin }
def read_stdin
  c=STDIN.getc
  if c.nil?
    []
  else
    [c.ord.const, Promise.new{ read_stdin }]
  end
end

# promise.rb ###################################################################

def make_promises(ir, ranks, types)
  promises = ir.map.with_index{|node,i|
    arg_types = node.args.map{|a|types[a]}
    arg_ranks = node.args.map{|a|ranks[a]}
    way = find_valid_way(ir[i], arg_types, arg_ranks)

    coerces = coerce?(way.arg_types,arg_types)
    zip_level = zip_level(way, arg_ranks, arg_types)
    excess = excess_ranks(way, arg_ranks, arg_types)

    impl = lambda{|*args|
      $rank = way.vectorize ? way.mod : ranks[ir[i].args[0]]
      $at = coerces[0] ? CharType : arg_types[0]
      $bt = coerces[1] ? CharType : arg_types[1]
      way.impl[*args]
    }

    promise{
      args = node.args.map{|a| promises[a] }
      args = args.zip(coerces,arg_ranks).map{|a,c,r|
        c ? promise{ zipn(r, [a], ->aa{ str(aa) }) } : a }
      special_rep = way.promote.include?(RepSnd) && excess[1] < 0
      args = args.zip(excess,0..).map{|a,e,ind| promoten(-e - (special_rep && ind==1 ? 1 : 0), a) }
      new_excess = excess.map{|e| e = [e, 0].max }
      args = args.zip(new_excess,0..).map{|a,e,ind| repn(zip_level - e + (special_rep && ind==1 ? 1 : 0), a)}
      if node.op.name == "superpad"
        superpad(zip_level, args[0])
      else
        zipn(zip_level, args, impl)
      end
    }
  }
  promises
end

# run.rb #######################################################################

NittyGritty = "https://golfscript.com/iogii/nittygritty.html"

def run(source, arg)
  tokens = lex(source)
  ast, ast_inds, registers, preprints=*parse_main(tokens)
  ir, ast2ir = *to_ir(ast, arg, registers)
  types=solve_types(ir)
  ranks=solve_ranks(ir, types)
  promises=make_promises(ir, ranks, types)
  stack_inds = ast_inds.map{|i| ast2ir[i] }
  preprint_inds = preprints.map{|i| ast2ir[i] }
  return [ir, stack_inds, types, ranks, promises, preprint_inds]
end

def run_and_do_io(source)
  ir, ir_inds, types, ranks, promises, preprint_inds = run(source, new_op("stdinLines", "[[char]]"){ lines(ReadStdin) })
  begin
    preprint_inds.each{|ir_ind|
      printv(promises[ir_ind].value, types[ir_ind], ranks[ir_ind])
      puts
    }

    ir_inds.each{|ir_ind|
      printv(promises[ir_ind].value, types[ir_ind], ranks[ir_ind])
    }
  rescue SystemStackError => e
    raise IogiiError.new "Stack Overflow"
  end
end

# main.rb ######################################################################
Encoding.default_external="iso-8859-1"
Encoding.default_internal="iso-8859-1"


if ["-v", "--v", "-version", "--version"].include? $*[0]
  puts Version
else
  begin
    run_and_do_io(gets(nil)||raise(IogiiError.new"empty program"))
  rescue IogiiError => e
    STDERR.puts e.message
  rescue => e
    STDERR.puts "This is an internal iogii error, please report it!\n".red
    raise e
  end
end
