packages feed

vintage-basic-1.0.3: test/Language/VintageBasic/Interpreter_test.hs

module Language.VintageBasic.Interpreter_test where

import Test.HUnit
import Language.VintageBasic.Asserts
import Language.VintageBasic.Executer
import Language.VintageBasic.BasicMonad
import Control.Monad.CPST.DurableTraps

testProgramOutput source expected = TestCase $ do
    i <- noInput
    o <- mkOutput
    runProgram i o (execute "" source >> done)
    assertOutputEq o expected

testProgramOutputWithInput source input expected = TestCase $ do
    i <- mkInput input
    o <- mkOutput
    runProgram i o (execute "" source >> done)
    assertOutputEq o expected

testExpression source expected = testProgramOutput ("1?" ++ source ++ "\n") (expected ++ "\n")

testExpressions ts = TestList $ map (uncurry testExpression) ts

test_does_nothing = testProgramOutput "" ""

test_executes_lines_in_sequence = testProgramOutput "10 ?\"A\"\n20 ?\"B\"\n" "A\nB\n"

test_var_defaults = testProgramOutput "10 ?A:?A%:?A$\n" " 0 \n 0 \n\n"

test_float_assignment = testProgramOutput "10 A=5:PRINTA\n" " 5 \n"

test_int_assignment_pos = testProgramOutput "10 A%=5.9:?A%\n" " 5 \n"

test_int_assignment_neg = testProgramOutput "10 A%=-5.1:?A%\n" "-6 \n"

test_string_assignment = testProgramOutput "10 A$=\"WOW\":?A$\n" "WOW\n"

test_variables_of_different_types_with_the_same_name_are_different = testProgramOutput
    "10 A=4.5:A%=3:A$=\"HI\"\n20 ?A:?A%:?A$\n"
    " 4.5 \n 3 \nHI\n"

test_var_names_are_only_significant_to_two_characters = testProgramOutput
    "10 SOMETHING=4:SAMETHING=2:?SOWHAT:?SALLY\n"
    " 4 \n 2 \n"

test_first_digit_of_var_name_is_significant_too = testProgramOutput
    "10 SOMETHING12=4:SOMETHING22=2:?SOMETHING13:?SOMETHING24\n"
    " 4 \n 2 \n"

test_dim = testProgramOutput "10 DIMA(20):A(0)=5:A(20)=6:PRINTA(20);A(0)\n" " 6  5 \n"

test_dim_default_size_is_at_least_10 = testProgramOutput "10 A(10)=6:PRINTA(10)\n" " 6 \n"

test_dim_default_size_is_at_most_10 = testProgramOutput
    "10 A(11)=6:PRINTA(11)\n" "!OUT OF ARRAY BOUNDS IN LINE 10\n"

test_dim_with_negative_dimension = testProgramOutput
    "10 DIMA(-1)\n"
    "!NEGATIVE ARRAY DIM IN LINE 10\n"

test_multiple_dimensions = testProgramOutput "10 DIMA(1,3):A(1,3)=12:PRINTA(1,3)\n" " 12 \n"

test_array_dim_rounding = testProgramOutput
    "10 DIMA(11.9):A(11.9)=5:?A(11.1):A(12)=6:?A(6)\n"
    " 5 \n!OUT OF ARRAY BOUNDS IN LINE 10\n"

test_for_loop_with_array = testProgramOutput "10 FORI=1TO10:A(I)=10-I:NEXT:PRINTA(3)\n" " 7 \n"

test_int_array = testProgramOutput "10 A%(5)=2.8:?A%(1);A%(5)\n" " 0  2 \n"

test_string_array = testProgramOutput "10 A$(5)=\"HI\":?A$(1);A$(5)\n" "HI\n"

test_arithmetic_expression = testProgramOutput "10 A=5:?A^2+3*A-1/2\n" " 39.5 \n"

test_parentheses = testProgramOutput "10 A=5:?A^(2+3)*(A-1)/2\n" " 6250 \n"

test_relational_operators_on_numbers = TestList $ [
    "eq" ~: testProgramOutput "10 ?1= 1:?1= 2:?2= 1\n" "-1 \n 0 \n 0 \n",
    "ne" ~: testProgramOutput "10 ?1<>1:?1<>2:?2<>1\n" " 0 \n-1 \n-1 \n",
    "lt" ~: testProgramOutput "10 ?1< 1:?1< 2:?2< 1\n" " 0 \n-1 \n 0 \n",
    "le" ~: testProgramOutput "10 ?1<=1:?1<=2:?2<=1\n" "-1 \n-1 \n 0 \n",
    "gt" ~: testProgramOutput "10 ?1> 1:?1> 2:?2> 1\n" " 0 \n 0 \n-1 \n",
    "ge" ~: testProgramOutput "10 ?1>=1:?1>=2:?2>=1\n" "-1 \n 0 \n-1 \n"
  ]

test_relational_operators_on_strings = TestList $ [
    "eq" ~: testProgramOutput "10 ?\"A\"= \"A\":?\"A\"= \"B\":?\"B\"= \"A\"\n" "-1 \n 0 \n 0 \n",
    "ne" ~: testProgramOutput "10 ?\"A\"<>\"A\":?\"A\"<>\"B\":?\"B\"<>\"A\"\n" " 0 \n-1 \n-1 \n",
    "lt" ~: testProgramOutput "10 ?\"A\"< \"A\":?\"A\"< \"B\":?\"B\"< \"A\"\n" " 0 \n-1 \n 0 \n",
    "le" ~: testProgramOutput "10 ?\"A\"<=\"A\":?\"A\"<=\"B\":?\"B\"<=\"A\"\n" "-1 \n-1 \n 0 \n",
    "gt" ~: testProgramOutput "10 ?\"A\"> \"A\":?\"A\"> \"B\":?\"B\"> \"A\"\n" " 0 \n 0 \n-1 \n",
    "ge" ~: testProgramOutput "10 ?\"A\">=\"A\":?\"A\">=\"B\":?\"B\">=\"A\"\n" "-1 \n 0 \n-1 \n"
  ]

test_boolean_logic = TestList $ [
    "and" ~: testProgramOutput "10 ?0AND0:?0AND-1:?-1AND0:?-1AND-1\n" " 0 \n 0 \n 0 \n-1 \n",
    "or"  ~: testProgramOutput "10 ?0OR 0:?0OR -1:?-1OR 0:?-1OR -1\n" " 0 \n-1 \n-1 \n-1 \n",
    "not" ~: testProgramOutput "10 ?NOT0:?NOT-1:?NOT2\n" "-1 \n 0 \n 0 \n"
  ]

test_boolean_logic_with_nonintegers = TestList $ [
    "and" ~: testProgramOutput "10 ?0.1AND-0.1:?0.1AND0:?0AND0.1\n" " .1 \n 0 \n 0 \n",
    "or"  ~: testProgramOutput "10 ?0OR-0.1:?0.1OR0:?0OR0\n" "-.1 \n .1 \n 0 \n",
    "not" ~: testProgramOutput "10 ?NOT-0.9:?NOT-0.1:?NOT0.1:?NOT0.9:?NOT1.1\n" " 0 \n 0 \n 0 \n 0 \n 0 \n"
  ]

test_negation = testProgramOutput "10 A=5:?-A:?-(-A)\n" "-5 \n 5 \n"

test_abs = testExpressions [
    ("ABS()", "!WRONG NUMBER OF ARGUMENTS IN LINE 1"),
    ("ABS(\"A\")", "!TYPE MISMATCH IN LINE 1"),
    ("ABS( 5.7)", " 5.7 "),
    ("ABS( 0  )", " 0 "),
    ("ABS(-5.7)", " 5.7 "),
    ("ABS(1,1)", "!WRONG NUMBER OF ARGUMENTS IN LINE 1")
  ]

test_asc = testExpressions [
    ("ASC()",            "!WRONG NUMBER OF ARGUMENTS IN LINE 1"),
    ("ASC(65)",          "!TYPE MISMATCH IN LINE 1"),
    ("ASC(\"\")",        "!INVALID ARGUMENT IN LINE 1"),
    ("ASC(\"A\")",       " 65 "),
    ("ASC(\"ABC\")",     " 65 "),
    ("ASC(\"0\")",       " 48 "),
    ("ASC(\"0\",\"0\")", "!WRONG NUMBER OF ARGUMENTS IN LINE 1")
  ]

test_atn = testExpressions [
    ("ATN()", "!WRONG NUMBER OF ARGUMENTS IN LINE 1"),
    ("ATN(\"A\")", "!TYPE MISMATCH IN LINE 1"),
    ("ATN(0)",   " 0 "       ),
    ("ATN(1)",   " .7853982 "),
    ("ATN(10)",  " 1.4711276 "),
    ("ATN(-1)",  "-.7853982 "),
    ("ATN(1,1)", "!WRONG NUMBER OF ARGUMENTS IN LINE 1")
  ]

test_chr = testExpressions [
    ("CHR$()", "!WRONG NUMBER OF ARGUMENTS IN LINE 1"),
    ("CHR$(\"A\")", "!TYPE MISMATCH IN LINE 1"),
    ("CHR$(-1)", "!INVALID ARGUMENT IN LINE 1"),
    ("CHR$(10)", "\n"),
    ("CHR$(13)", "\r"),
    ("CHR$(32)", " "),
    ("CHR$(48)", "0"),
    ("CHR$(65)", "A"),
    ("CHR$(256)", "!INVALID ARGUMENT IN LINE 1"),
    ("CHR$(1,1)", "!WRONG NUMBER OF ARGUMENTS IN LINE 1")
  ]

test_cos = testExpressions [
    ("COS()", "!WRONG NUMBER OF ARGUMENTS IN LINE 1"),
    ("COS(\"A\")", "!TYPE MISMATCH IN LINE 1"),
    ("COS(0)", " 1 "),
    ("COS(1)", " .5403023 "),
    ("COS(1.5707964)", "-4.371139E-8 "),
    ("COS(3.1415927)", "-1 "),
    ("COS(4.712389)",  " 1.1924881E-8 "),
    ("COS(6.2831855)", " 1 "),
    ("COS(1,1)", "!WRONG NUMBER OF ARGUMENTS IN LINE 1")
  ]

test_exp = testExpressions [
    ("EXP()", "!WRONG NUMBER OF ARGUMENTS IN LINE 1"),
    ("EXP(\"A\")", "!TYPE MISMATCH IN LINE 1"),
    ("EXP(-1)", " .36787945 "),
    ("EXP(0)",  " 1 "),
    ("EXP(1)",  " 2.7182817 "),
    ("EXP(2)",  " 7.389056 "),
    ("EXP(1,1)", "!WRONG NUMBER OF ARGUMENTS IN LINE 1")
  ]

test_int = testExpressions [
    ("INT()", "!WRONG NUMBER OF ARGUMENTS IN LINE 1"),
    ("INT(\"A\")", "!TYPE MISMATCH IN LINE 1"),
    ("INT(-23.1)", "-24 "),
    ("INT(23)",    " 23 "),
    ("INT(23.1)",  " 23 "),
    ("INT(23.9)",  " 23 "),
    ("INT(24)",    " 24 "),
    ("INT(1,1)", "!WRONG NUMBER OF ARGUMENTS IN LINE 1")
  ]

test_left = testExpressions [
    ("LEFT$()", "!WRONG NUMBER OF ARGUMENTS IN LINE 1"),
    ("LEFT$(\"A\")", "!WRONG NUMBER OF ARGUMENTS IN LINE 1"),
    ("LEFT$(\"ABC\",\"A\")", "!TYPE MISMATCH IN LINE 1"),
    ("LEFT$(1,1)", "!TYPE MISMATCH IN LINE 1"),
    ("LEFT$(\"ABC\",-1)", "!INVALID ARGUMENT IN LINE 1"),
    ("LEFT$(\"ABC\",-.1)", "!INVALID ARGUMENT IN LINE 1"),
    ("LEFT$(\"ABC\", 0)", ""),
    ("LEFT$(\"ABC\", 1)", "A"),
    ("LEFT$(\"ABC\", 2)", "AB"),
    ("LEFT$(\"ABC\", 2.9)", "AB"),
    ("LEFT$(\"ABC\", 3)", "ABC"),
    ("LEFT$(\"ABC\", 4)", "ABC"),
    ("LEFT$(\"A\",1,1)", "!WRONG NUMBER OF ARGUMENTS IN LINE 1")
  ]

test_len = testExpressions [
    ("LEN()", "!WRONG NUMBER OF ARGUMENTS IN LINE 1"),
    ("LEN(1)", "!TYPE MISMATCH IN LINE 1"),
    ("LEN(\"\")",  " 0 "),
    ("LEN(\"A\")", " 1 "),
    ("LEN(\" \" + CHR$(0) + \"23$!\")", " 6 "),
    ("LEN(\"A\", \"B\")", "!WRONG NUMBER OF ARGUMENTS IN LINE 1")
  ]

test_log = testExpressions [
    ("LOG()", "!WRONG NUMBER OF ARGUMENTS IN LINE 1"),
    ("LOG(\"A\")", "!TYPE MISMATCH IN LINE 1"),
    ("LOG(-1)",        "!INVALID ARGUMENT IN LINE 1"),
    ("LOG( 0)",        "!INVALID ARGUMENT IN LINE 1"),
    ("LOG( 0.1)",      "-2.3025851 "),
    ("LOG( 1)",        " 0 "),
    ("LOG(2.7182817)", " .99999994 "),
    ("LOG(1,1)", "!WRONG NUMBER OF ARGUMENTS IN LINE 1")
  ]

test_mid = testExpressions [
    ("MID$()", "!WRONG NUMBER OF ARGUMENTS IN LINE 1"),
    ("MID$(\"ABC\")", "!WRONG NUMBER OF ARGUMENTS IN LINE 1"),
    ("MID$(1,1)", "!TYPE MISMATCH IN LINE 1"),
    ("MID$(\"ABC\",\"A\")", "!TYPE MISMATCH IN LINE 1"),
    ("MID$(\"ABC\",-1)", "!INVALID ARGUMENT IN LINE 1"),
    ("MID$(\"ABC\", 0)", "!INVALID ARGUMENT IN LINE 1"),
    ("MID$(\"ABC\", 1)", "ABC"),
    ("MID$(\"ABC\", 2)", "BC"),
    ("MID$(\"ABC\", 3)", "C"),
    ("MID$(\"ABC\", 4)", ""),
    ("MID$(1,1)", "!TYPE MISMATCH IN LINE 1"),
    ("MID$(\"ABC\",\"A\",1)", "!TYPE MISMATCH IN LINE 1"),
    ("MID$(\"ABC\",1,\"A\")", "!TYPE MISMATCH IN LINE 1"),
    ("MID$(\"ABC\",-1, 1)", "!INVALID ARGUMENT IN LINE 1"),
    ("MID$(\"ABC\", 0, 1)", "!INVALID ARGUMENT IN LINE 1"),
    ("MID$(\"ABC\", 0,-1)", "!INVALID ARGUMENT IN LINE 1"),
    ("MID$(\"ABC\", 1,0)", ""),
    ("MID$(\"ABC\", 1,1)", "A"),
    ("MID$(\"ABC\", 1,2)", "AB"),
    ("MID$(\"ABC\", 1,3)", "ABC"),
    ("MID$(\"ABC\", 1,4)", "ABC"),
    ("MID$(\"ABC\", 2,0)", ""),
    ("MID$(\"ABC\", 2,1)", "B"),
    ("MID$(\"ABC\", 2,2)", "BC"),
    ("MID$(\"ABC\", 2.9, 2.9)", "BC"),
    ("MID$(\"ABC\", 2,3)", "BC"),
    ("MID$(\"ABC\", 3,0)", ""),
    ("MID$(\"ABC\", 3,1)", "C"),
    ("MID$(\"ABC\", 3,2)", "C"),
    ("MID$(\"ABC\", 4,0)", ""),
    ("MID$(\"ABC\", 4,1)", ""),
    ("MID$(\"ABC\", 1, 1, 1)", "!WRONG NUMBER OF ARGUMENTS IN LINE 1")
  ]

test_right = testExpressions [
    ("RIGHT$()", "!WRONG NUMBER OF ARGUMENTS IN LINE 1"),
    ("RIGHT$(\"A\")", "!WRONG NUMBER OF ARGUMENTS IN LINE 1"),
    ("RIGHT$(\"ABC\",\"A\")", "!TYPE MISMATCH IN LINE 1"),
    ("RIGHT$(1,1)", "!TYPE MISMATCH IN LINE 1"),
    ("RIGHT$(\"ABC\",-1)", "!INVALID ARGUMENT IN LINE 1"),
    ("RIGHT$(\"ABC\", 0)", ""),
    ("RIGHT$(\"ABC\", 1)", "C"),
    ("RIGHT$(\"ABC\", 2)", "BC"),
    ("RIGHT$(\"ABC\", 2.9)", "BC"),
    ("RIGHT$(\"ABC\", 3)", "ABC"),
    ("RIGHT$(\"ABC\", 4)", "ABC"),
    ("RIGHT$(\"A\",1,1)", "!WRONG NUMBER OF ARGUMENTS IN LINE 1")
  ]

test_rnd = testProgramOutput
    "1 ?RND(-1):?RND(1):?RND(1):?RND(0):?RND(10):?RND(-2.5)\n"
    " .72774446 \n .6806801 \n .2628854 \n .2628854 \n .8841321 \n .2695619 \n"

test_rnd_errors = testExpressions [
    ("RND()", "!WRONG NUMBER OF ARGUMENTS IN LINE 1"),
    ("RND(\"A\")", "!TYPE MISMATCH IN LINE 1"),
    ("RND(1,1)", "!WRONG NUMBER OF ARGUMENTS IN LINE 1")
  ]

-- RANDOMIZE is difficult to test because it depends on system time

test_sgn = testExpressions [
    ("SGN()", "!WRONG NUMBER OF ARGUMENTS IN LINE 1"),
    ("SGN(\"A\")", "!TYPE MISMATCH IN LINE 1"),
    ("SGN( 5.7)", " 1 "),
    ("SGN( 0  )", " 0 "),
    ("SGN(-5.7)", "-1 "),
    ("SGN(1,1)", "!WRONG NUMBER OF ARGUMENTS IN LINE 1")
  ]

test_sin = testExpressions [
    ("SIN()", "!WRONG NUMBER OF ARGUMENTS IN LINE 1"),
    ("SIN(\"A\")", "!TYPE MISMATCH IN LINE 1"),
    ("SIN(0)", " 0 "),
    ("SIN(1)", " .84147096 "),
    ("SIN(1.5707964)", " 1 "),
    ("SIN(3.1415927)", "-8.742278E-8 "),
    ("SIN(4.712389)",  "-1 "),
    ("SIN(6.2831855)", " 1.7484555E-7 "),
    ("SIN(1,1)", "!WRONG NUMBER OF ARGUMENTS IN LINE 1")
  ]

test_spc = testExpressions [
    ("SPC()", "!WRONG NUMBER OF ARGUMENTS IN LINE 1"),
    ("SPC(\"A\")", "!TYPE MISMATCH IN LINE 1"),
    ("SPC(-1)", "!INVALID ARGUMENT IN LINE 1"),
    ("SPC(0)", ""),
    ("SPC(1)", " "),
    ("SPC(2.9)", "  "),
    ("SPC(10)", "          "),
    ("SPC(1,1)", "!WRONG NUMBER OF ARGUMENTS IN LINE 1")
  ]

test_sqr = testExpressions [
    ("SQR()", "!WRONG NUMBER OF ARGUMENTS IN LINE 1"),
    ("SQR(\"A\")", "!TYPE MISMATCH IN LINE 1"),
    ("SQR(-1)", "!INVALID ARGUMENT IN LINE 1"),
    ("SQR(0)", " 0 "),
    ("SQR(2)", " 1.4142135 "),
    ("SQR(4)", " 2 "),
    ("SQR(1,1)", "!WRONG NUMBER OF ARGUMENTS IN LINE 1")
  ]

test_str = testExpressions [
    ("STR$()", "!WRONG NUMBER OF ARGUMENTS IN LINE 1"),
    ("STR$(\"ABC\")", "!TYPE MISMATCH IN LINE 1"),
    ("STR$(1E2)", " 100"),
    ("STR$(.000000001)", " 1.E-9"),
    ("STR$(1,1)", "!WRONG NUMBER OF ARGUMENTS IN LINE 1")
  ]

test_tan = testExpressions [
    ("TAN()", "!WRONG NUMBER OF ARGUMENTS IN LINE 1"),
    ("TAN(\"A\")", "!TYPE MISMATCH IN LINE 1"),
    ("TAN(0)", " 0 "),
    ("TAN(.7853982)", " 1 "),
    ("TAN(1)", " 1.5574077 "),
    ("TAN(1.5707964)", "-22877332 "),
    ("TAN(3.1415927)", " 8.742278E-8 "),
    ("TAN(4.712389)",  "-83858280 "),
    ("TAN(6.2831855)", " 1.7484555E-7 "),
    ("TAN(1,1)", "!WRONG NUMBER OF ARGUMENTS IN LINE 1")
  ]

test_val = testExpressions [
    ("VAL()", "!WRONG NUMBER OF ARGUMENTS IN LINE 1"),
    ("VAL(1)", "!TYPE MISMATCH IN LINE 1"),
    ("VAL(\"\")", " 0 "),
    ("VAL(\"123\")", " 123 "),
    ("VAL(\" 123 \")", " 123 "),
    ("VAL(\" 1 2\")", " 1 "),
    ("VAL(\"1A\")", " 1 "),
    ("VAL(\"A\")", " 0 "),
    ("VAL(\"1\",\"1\")", "!WRONG NUMBER OF ARGUMENTS IN LINE 1")
  ]

test_goto_forwards = testProgramOutput "10GOTO30\n20?20\n30?30\n" " 30 \n"

test_goto_backwards = testProgramOutput "10GOTO50\n20?20\n30?30\n40END\n50GOTO30\n" " 30 \n"

test_gosub = testProgramOutput "1?1\n2GOSUB5\n3?3\n4END\n5?5\n6RETURN\n" " 1 \n 5 \n 3 \n"

test_nested_gosub = testProgramOutput (unlines [
    "  1 ?1     ",
    "  2 GOSUB5 ",
    "  3 ?3     ",
    "  4 END    ",
    "  5 ?5     ",
    "  6 GOSUB9 ",
    "  7 ?7     ",
    "  8 RETURN ",
    "  9 ?9     ",
    " 10 RETURN "
  ])
  " 1 \n 5 \n 9 \n 7 \n 3 \n"

test_return_does_double_duty = testProgramOutput (unlines [
    "10 ?10:GOSUB30",
    "20 ?20:END",
    "30 ?30:GOSUB40",
    "40 ?40:RETURN"
  ])
  " 10 \n 30 \n 40 \n 40 \n 20 \n"

test_on_goto = TestList [
    testProgramOutput "1 ON\"A\"GOTO2\n" "!TYPE MISMATCH IN LINE 1\n",
    testProgramOutput "1 A=-1  :ONAGOTO3,4,5\n2 ?2\n3 ?3\n4 ?4\n5 ?5\n6 ?6\n" " 2 \n 3 \n 4 \n 5 \n 6 \n",
    testProgramOutput "1 A=-0.1:ONAGOTO3,4,5\n2 ?2\n3 ?3\n4 ?4\n5 ?5\n6 ?6\n" " 2 \n 3 \n 4 \n 5 \n 6 \n",
    testProgramOutput "1 A= 0  :ONAGOTO3,4,5\n2 ?2\n3 ?3\n4 ?4\n5 ?5\n6 ?6\n" " 2 \n 3 \n 4 \n 5 \n 6 \n",
    testProgramOutput "1 A= 1  :ONAGOTO3,4,5\n2 ?2\n3 ?3\n4 ?4\n5 ?5\n6 ?6\n" " 3 \n 4 \n 5 \n 6 \n",
    testProgramOutput "1 A= 2  :ONAGOTO3,4,5\n2 ?2\n3 ?3\n4 ?4\n5 ?5\n6 ?6\n" " 4 \n 5 \n 6 \n",
    testProgramOutput "1 A= 2.9:ONAGOTO3,4,5\n2 ?2\n3 ?3\n4 ?4\n5 ?5\n6 ?6\n" " 4 \n 5 \n 6 \n",
    testProgramOutput "1 A= 3  :ONAGOTO3,4,5\n2 ?2\n3 ?3\n4 ?4\n5 ?5\n6 ?6\n" " 5 \n 6 \n",
    testProgramOutput "1 A= 4  :ONAGOTO3,4,5\n2 ?2\n3 ?3\n4 ?4\n5 ?5\n6 ?6\n" " 2 \n 3 \n 4 \n 5 \n 6 \n",
    testProgramOutput "1 A= 2  :ONAGOTO3,5,4\n2 ?2\n3 ?3\n4 ?4\n5 ?5\n6 ?6\n" " 5 \n 6 \n"
  ]

test_on_gosub = TestList [
    testProgramOutput "1 ON\"A\"GOSUB2\n" "!TYPE MISMATCH IN LINE 1\n",
    testProgramOutput "1 A=-1  :ONAGOSUB3,4,5:END\n2 ?2:RETURN\n3 ?3:RETURN\n4 ?4:RETURN\n5 ?5:RETURN\n6 ?6:RETURN\n" "",
    testProgramOutput "1 A=-0.1:ONAGOSUB3,4,5:END\n2 ?2:RETURN\n3 ?3:RETURN\n4 ?4:RETURN\n5 ?5:RETURN\n6 ?6:RETURN\n" "",
    testProgramOutput "1 A= 0  :ONAGOSUB3,4,5:END\n2 ?2:RETURN\n3 ?3:RETURN\n4 ?4:RETURN\n5 ?5:RETURN\n6 ?6:RETURN\n" "",
    testProgramOutput "1 A= 1  :ONAGOSUB3,4,5:END\n2 ?2:RETURN\n3 ?3:RETURN\n4 ?4:RETURN\n5 ?5:RETURN\n6 ?6:RETURN\n" " 3 \n",
    testProgramOutput "1 A= 2  :ONAGOSUB3,4,5:END\n2 ?2:RETURN\n3 ?3:RETURN\n4 ?4:RETURN\n5 ?5:RETURN\n6 ?6:RETURN\n" " 4 \n",
    testProgramOutput "1 A= 2.9:ONAGOSUB3,4,5:END\n2 ?2:RETURN\n3 ?3:RETURN\n4 ?4:RETURN\n5 ?5:RETURN\n6 ?6:RETURN\n" " 4 \n",
    testProgramOutput "1 A= 3  :ONAGOSUB3,4,5:END\n2 ?2:RETURN\n3 ?3:RETURN\n4 ?4:RETURN\n5 ?5:RETURN\n6 ?6:RETURN\n" " 5 \n",
    testProgramOutput "1 A= 4  :ONAGOSUB3,4,5:END\n2 ?2:RETURN\n3 ?3:RETURN\n4 ?4:RETURN\n5 ?5:RETURN\n6 ?6:RETURN\n" "",
    testProgramOutput "1 A= 2  :ONAGOSUB3,5,4:END\n2 ?2:RETURN\n3 ?3:RETURN\n4 ?4:RETURN\n5 ?5:RETURN\n6 ?6:RETURN\n" " 5 \n"
  ]

test_if = TestList [
    testProgramOutput "1 IF\"A\"THEN?1\n" "!TYPE MISMATCH IN LINE 1\n",
    testProgramOutput "1 IF-11THEN?1:?2\n2 ?3\n" " 1 \n 2 \n 3 \n",
    testProgramOutput "1 IF 0THEN?1:?2\n2 ?3\n" " 3 \n",
    testProgramOutput "1 IF 1THEN?1:?2\n2 ?3\n" " 1 \n 2 \n 3 \n",
    testProgramOutput "1 IF .1THEN?1:?2\n2 ?3\n" " 1 \n 2 \n 3 \n",
    testProgramOutput "1 IF -.1THEN?1:?2\n2 ?3\n" " 1 \n 2 \n 3 \n",
    testProgramOutput "1 IF 2.1THEN?1:?2\n2 ?3\n" " 1 \n 2 \n 3 \n"
  ]

test_for = TestList [
    testProgramOutput "1 FORA$=1TO10\n" "!TYPE MISMATCH IN LINE 1\n",
    testProgramOutput "1 FORA%=1TO10\n" "!TYPE MISMATCH IN LINE 1\n",
    testProgramOutput "1 FORA=\"A\"TO3\n" "!TYPE MISMATCH IN LINE 1\n",
    testProgramOutput "1 FORA=1TO\"A\"\n" "!TYPE MISMATCH IN LINE 1\n",
    testProgramOutput "1 FORA=1TO3:?A:NEXT:?99:?A\n" " 1 \n 2 \n 3 \n 99 \n 4 \n",
    testProgramOutput "1 FORA=2TO8STEP2:?A:NEXT:?99:?A\n" " 2 \n 4 \n 6 \n 8 \n 99 \n 10 \n",
    testProgramOutput "1 FORA=8TO2STEP-2:?A:NEXT:?99:?A\n" " 8 \n 6 \n 4 \n 2 \n 99 \n 0 \n",
    testProgramOutput "1 FORA=1TO0:?A:NEXT:?99:?A\n" " 1 \n 99 \n 2 \n",
    testProgramOutput "1 FORA=1TO3\n2 ?A\n3 NEXT\n" " 1 \n 2 \n 3 \n",
    testProgramOutput "1 FORA=1TO3\n2 ?A\n3 NEXTA\n" " 1 \n 2 \n 3 \n",
    testProgramOutput "1 FORA=1TO3\n2 ?A\n3 NEXTB\n" " 1 \n!NEXT WITHOUT FOR ERROR (VAR B) IN LINE 3\n"
  ]

test_nested_for = testProgramOutput (unlines [
    "1 FORA=10TO20STEP10",
    "2 FORB=1TO3",
    "3 ?A+B",
    "4 NEXT",
    "5 NEXT"
  ])
  " 11 \n 12 \n 13 \n 21 \n 22 \n 23 \n"

test_improperly_nested_for = testProgramOutput (unlines [
    "1 FORA=10TO20STEP10",
    "2 FORB=1TO3",
    "3 ?A+B",
    "4 NEXTA",
    "5 NEXTB"
  ])
  " 11 \n 21 \n 32 \n!NEXT WITHOUT FOR ERROR (VAR A) IN LINE 4\n"

test_return_in_for = testProgramOutput (unlines [
    "10 GOSUB40",
    "20 NEXT",
    "30 END",
    "40 FORI=1TO3",
    "50 PRINT\"HELLO\"",
    "60 RETURN"
  ])
  "HELLO\nHELLO\n!RETURN WITHOUT GOSUB ERROR IN LINE 60\n"

test_print_literals = testProgramOutput "10 PRINT\"NUMBER\";5;\"AND\";-2\n" "NUMBER 5 AND-2 \n"

test_float_representation = testProgramOutput "10 ?1E17:?5.55555\n" " 1.E+17 \n 5.55555 \n"

test_tab = testProgramOutput
    "10 ?TAB(4);\"A\";TAB(10);\"B\";TAB(11);\"C\";TAB(4);\"D\"\n"
    "    A     BCD\n"

test_tab_with_nonintegers = testProgramOutput "10 ?TAB(2.9);\"A\"\n" "  A\n"

test_tab_with_newlines_and_linefeeds = testProgramOutput
    "10 ?\"HI\";CHR$(13);\"TO\";TAB(5);\"YOU\";CHR$(10);\"AND\";TAB(10);\"ME\"\n"
    "HI\rTO   YOU\nAND       ME\n"

test_tab_wrongargs = testExpressions [
    ("TAB()", "!WRONG NUMBER OF ARGUMENTS IN LINE 1"),
    ("TAB(\"A\")", "!TYPE MISMATCH IN LINE 1"),
    ("TAB(-1)", "!INVALID ARGUMENT IN LINE 1"),
    ("TAB(1,1)", "!WRONG NUMBER OF ARGUMENTS IN LINE 1")
  ]

test_print_zones = testProgramOutput
    "1 ?\"A\",\"B\",,\"C\":?\"123456789012345\",\"X\":?1,;:?2,:?,3\n"
    "A             B                           C\n123456789012345             X\n 1             2                           3 \n"

test_input = TestList [
    "multiple values and types" ~: testProgramOutputWithInput
        "1 INPUT\"WHAT\";A$,B,CD$(9),M%\n2 ?:?A$:?B:?CD$(9):?M%\n"
        "  Bob 4 , 3  ,2  1, 5.2\n"
        "WHAT? \nBob 4\n 3 \n2  1\n 5 \n",
    "trims whitespace" ~: testProgramOutputWithInput
        "1 INPUTA$:?:?A$\n"
        "  \t   Bob 4 \n"
        "? \nBob 4\n",
    "quoted strings" ~: testProgramOutputWithInput
        "1 INPUTA$\n2 ?:?A$\n"
        "  \t\"   Bob 4 \"         \t   \n"
        "? \n   Bob 4 \n",
    "re-prompts for invalid number" ~: testProgramOutputWithInput
        "1 INPUTA:?:?A\n"
        "X\n4\n"
        "? !NUMBER EXPECTED - RETRY INPUT LINE\n? \n 4 \n",
    "re-prompts if not enough data on line" ~: testProgramOutputWithInput
        "1 INPUT\"NUM\";A,B:?:?A:?B\n"
        "3\n4\n"
        "NUM? ?? \n 3 \n 4 \n",
    "extra input ignored" ~: testProgramOutputWithInput
        "1 INPUTA\n"
        "1,2\n"
        "? !EXTRA INPUT IGNORED\n",
    "error if we reach eof without enough data" ~: testProgramOutputWithInput
        "1 INPUTA,B:?:?A:?B\n"
        "4\n"
        "? ?? !END OF INPUT IN LINE 1\n",
    "error if we reach eof without enough data (no newline)" ~: testProgramOutputWithInput
        "1 INPUTA:?:?A\n"
        ""
        "? !END OF INPUT IN LINE 1\n"
  ]

test_read_data = TestList [
    "multiple values and types" ~: testProgramOutput
        "1 READ A$,B,CD$(9),M%\n2 ?A$:?B:?CD$(9):?M%\n3 DATA  Bob 4 , 3  ,2  1, 5.2\n"
        "Bob 4\n 3 \n2  1\n 5 \n",
    "trims whitespace" ~: testProgramOutput
        "1 READA$:?A$\n2 DATA  \t   Bob 4 \n"
        "Bob 4\n",
    "quoted strings" ~: testProgramOutput
        "1 READA$\n2 ?A$\n3 DATA  \t\"   Bob 4 \"         \t   \n"
        "   Bob 4 \n",
    "type mismatch for invalid number" ~: testProgramOutput
        "1 READA:?:?A\n2 DATAX\n"
        "!TYPE MISMATCH IN LINE 1\n",
    "data on multiple lines" ~: testProgramOutput
        "1 READA,B:?A:?B\n2 DATA3\n3 DATA4\n"
        " 3 \n 4 \n",
    "extra data ignored" ~: testProgramOutput
        "1 READA:?A\n2 DATA1,2\n"
        " 1 \n",
    "error if we reach eof without enough data" ~: testProgramOutput
        "1 READA,B:?A:?B\n2 DATA4\n"
        "!OUT OF DATA IN LINE 1\n",
    "don't care where data appears" ~: testProgramOutput
        (unlines [
            "1 DATA A:?7",
            "2 READA$,B$,C:DATAmore",
            "3 DATA 5",
            "4 ?A$:?B$:?C"
        ])
        "A:?7\nmore\n 5 \n"
  ]

test_restore = testProgramOutput
    (unlines [
        "1 READ A,B,C:RESTORE 3:READ D,E:RESTORE:READ F,G,H:?A:?B:?C:?D:?E:?F:?G:?H\n",
        "2 DATA1",
        "3 DATA2",
        "4 DATA3"
    ])
    " 1 \n 2 \n 3 \n 2 \n 3 \n 1 \n 2 \n 3 \n"

test_def_fn = TestList [
    "simple float" ~: testProgramOutput
        "1 DEFFNA(X)=X^2+1:?FNA(4)\n"
        " 17 \n",
    "multi-arg float" ~: testProgramOutput
        "1 DEFFNA(X,Y,Z$)=LEN(Z$)^2+2*X+Y:?FNA(2,3,\"HELLO\")\n"
        " 32 \n",
    "multi-arg string" ~: testProgramOutput
        "1 DEFFNA$(X$,Y,Z)=MID$(X$,Y-1,Z+1):?FNA$(\"MAGILLICUDDY\",3,4)\n"
        "AGILL\n",
    "wrong num args" ~: testProgramOutput
        "1 DEFFNA(X)=X*2:?FNA()\n"
        "!WRONG NUMBER OF ARGUMENTS IN LINE 1\n",
    "type mismatch" ~: testProgramOutput
        "1 DEFFNA(X)=X*2:?A(\"X\")\n"
        "!TYPE MISMATCH IN LINE 1\n",
    "undefined" ~: testProgramOutput
        "1 ?FNA(5):DEFFNA(X)=X*2\n"
        "!UNDEFINED FUNCTION A IN LINE 1\n"
  ]

test_stop = testProgramOutput "1 ?1:STOP:?2\n" " 1 \n"

test_rem = testProgramOutput "1 ?1:REM COMMENT:?2\n2 ?3\n" " 1 \n 3 \n"