tests/testthat/test-reader-edge-cases.R

# Tests for reader/tokenizer edge cases
# Covers unicode, partial input, numeric edge cases, escape sequences

thin <- make_cran_thinner()

test_that("unicode characters in symbols", {
  thin()
  engine <- make_engine(load_prelude = FALSE)

  # Should handle unicode in symbols
  result <- engine$eval_text("(define café 42)")
  expect_no_error(result)

  result <- engine$eval_text("café")
  expect_equal(result, 42)
})

test_that("unicode in strings", {
  thin()
  engine <- make_engine(load_prelude = FALSE)

  result <- engine$eval_text('"Hello 世界"')
  expect_equal(result, "Hello 世界")

  result <- engine$eval_text('"Emoji: 🎉"')
  expect_equal(result, "Emoji: 🎉")
})

test_that("escape sequences in strings", {
  thin()
  engine <- make_engine(load_prelude = FALSE)

  result <- engine$eval_text('"Hello\\nWorld"')
  expect_equal(result, "Hello\nWorld")

  result <- engine$eval_text('"Tab\\there"')
  expect_equal(result, "Tab\there")

  result <- engine$eval_text('"Quote: \\"test\\""')
  expect_equal(result, 'Quote: "test"')

  result <- engine$eval_text('"Backslash: \\\\"')
  expect_equal(result, "Backslash: \\")
})

test_that("numeric literals - integers", {
  thin()
  engine <- make_engine(load_prelude = FALSE)

  result <- engine$eval_text("42")
  expect_equal(result, 42)

  result <- engine$eval_text("-17")
  expect_equal(result, -17)

  result <- engine$eval_text("0")
  expect_equal(result, 0)
})

test_that("numeric literals - floats", {
  thin()
  engine <- make_engine(load_prelude = FALSE)

  result <- engine$eval_text("3.14")
  expect_equal(result, 3.14)

  result <- engine$eval_text("-2.5")
  expect_equal(result, -2.5)

  result <- engine$eval_text("0.0")
  expect_equal(result, 0.0)
})

test_that("numeric literals - scientific notation", {
  thin()
  engine <- make_engine(load_prelude = FALSE)

  # Basic scientific notation
  result <- engine$eval_text("1e5")
  expect_equal(result, 1e5)

  result <- engine$eval_text("2.5e3")
  expect_equal(result, 2.5e3)

  # Uppercase E
  result <- engine$eval_text("1E5")
  expect_equal(result, 1E5)

  # Negative exponents
  result <- engine$eval_text("1e-5")
  expect_equal(result, 1e-5)

  result <- engine$eval_text("3.14e-2")
  expect_equal(result, 3.14e-2)

  # Positive exponents with explicit sign
  result <- engine$eval_text("1e+5")
  expect_equal(result, 1e+5)

  # Negative numbers with scientific notation
  result <- engine$eval_text("-2.5e3")
  expect_equal(result, -2.5e3)

  result <- engine$eval_text("-1e-5")
  expect_equal(result, -1e-5)

  result <- engine$eval_text("-3.14e+10")
  expect_equal(result, -3.14e+10)

  # Zero with exponents
  result <- engine$eval_text("0e0")
  expect_equal(result, 0)

  result <- engine$eval_text("0.0e10")
  expect_equal(result, 0)

  result <- engine$eval_text("0e-5")
  expect_equal(result, 0)

  # Integer part with exponent (no decimal)
  result <- engine$eval_text("123e2")
  expect_equal(result, 123e2)

  # Decimal without integer part is not valid in standard notation,
  # but our regex requires at least one digit before optional decimal
  result <- engine$eval_text("5.0e0")
  expect_equal(result, 5.0)

  # Works in expressions
  result <- engine$eval_text("(+ 1e2 2e2)")
  expect_equal(result, 300)

  result <- engine$eval_text("(* 2e3 3e2)")
  expect_equal(result, 2e3 * 3e2)
})

test_that("scientific notation - more edge cases", {
  thin()
  engine <- make_engine(load_prelude = FALSE)

  # Multiple digit exponents
  result <- engine$eval_text("1e10")
  expect_equal(result, 1e10)

  result <- engine$eval_text("1e100")
  expect_equal(result, 1e100)

  result <- engine$eval_text("1e-10")
  expect_equal(result, 1e-10)

  # Negative base with various exponents
  result <- engine$eval_text("-1e0")
  expect_equal(result, -1)

  result <- engine$eval_text("-9.99e99")
  expect_equal(result, -9.99e99)

  result <- engine$eval_text("-1.23e-45")
  expect_equal(result, -1.23e-45)

  # Positive sign on base
  result <- engine$eval_text("+1e5")
  expect_equal(result, 1e5)

  result <- engine$eval_text("+2.5e-3")
  expect_equal(result, 2.5e-3)
})

test_that("scientific notation - invalid formats treated as symbols", {
  thin()
  engine <- make_engine(load_prelude = FALSE)

  # These should be parsed as symbols, not numbers

  # Missing exponent digits (e with no digits after)
  # This will be tokenized as two separate tokens or fail
  expect_error(engine$eval_text("1e"))

  # Double e
  expect_error(engine$eval_text("1ee5"))

  # Decimal in exponent
  expect_error(engine$eval_text("1e5.5"))

  # Space in scientific notation
  expect_error(engine$eval_text("1e 5"))

  # Multiple signs
  expect_error(engine$eval_text("1e+-5"))
  expect_error(engine$eval_text("1e++5"))
})

test_that("numeric edge cases - very large numbers", {
  thin()
  engine <- make_engine(load_prelude = FALSE)

  # Very large numbers using scientific notation
  result <- engine$eval_text("1e100")
  expect_equal(result, 1e100)

  result <- engine$eval_text("9.99e307")
  expect_equal(result, 9.99e307)

  # Avogadro's number
  result <- engine$eval_text("6.022e23")
  expect_equal(result, 6.022e23)

  # Should work in arithmetic
  result <- engine$eval_text("(+ 1e10 1e10)")
  expect_equal(result, 2e10)

  # Comparisons
  result <- engine$eval_text("(> 1e100 1e99)")
  expect_equal(result, TRUE)

  result <- engine$eval_text("(< 1e100 1e101)")
  expect_equal(result, TRUE)
})

test_that("numeric edge cases - very small numbers", {
  thin()
  engine <- make_engine(load_prelude = FALSE)

  # Very small numbers using scientific notation
  result <- engine$eval_text("1e-100")
  expect_equal(result, 1e-100)

  result <- engine$eval_text("2.5e-308")
  expect_equal(result, 2.5e-308)

  # Planck's constant (approximately)
  result <- engine$eval_text("6.626e-34")
  expect_equal(result, 6.626e-34)

  # Should work in arithmetic
  result <- engine$eval_text("(+ 1e-10 1e-10)")
  expect_equal(result, 2e-10)

  # Multiplication with very small numbers
  result <- engine$eval_text("(* 2e-5 3e-5)")
  expect_equal(result, 2e-5 * 3e-5)

  # Division creating small numbers
  result <- engine$eval_text("(/ 1 1e10)")
  expect_equal(result, 1e-10)
})

test_that("special numeric values", {
  thin()
  engine <- make_engine(load_prelude = FALSE)

  # Inf
  result <- engine$eval_text("(/ 1 0)")
  expect_equal(result, Inf)

  # -Inf
  result <- engine$eval_text("(/ -1 0)")
  expect_equal(result, -Inf)

  # NaN (0/0)
  result <- engine$eval_text("(/ 0 0)")
  expect_true(is.nan(result))
})

test_that("empty list", {
  thin()
  engine <- make_engine(load_prelude = FALSE)

  result <- engine$eval_text("'()")
  expect_equal(length(result), 0)
})

test_that("whitespace handling", {
  thin()
  engine <- make_engine(load_prelude = FALSE)

  # Multiple spaces
  result <- engine$eval_text("(+    1    2)")
  expect_equal(result, 3)

  # Tabs
  result <- engine$eval_text("(+\t1\t2)")
  expect_equal(result, 3)

  # Mixed whitespace
  result <- engine$eval_text("(+ \t\n  1  \n\t  2)")
  expect_equal(result, 3)
})

test_that("comment handling", {
  thin()
  engine <- make_engine(load_prelude = FALSE)

  # Line comment
  result <- engine$eval_text("; comment\n(+ 1 2)")
  expect_equal(result, 3)

  # Inline comment
  result <- engine$eval_text("(+ 1 2) ; result is 3")
  expect_equal(result, 3)

  # Multiple comments
  code <- "; first comment\n; second comment\n(+ 1 2) ; inline\n; trailing"
  result <- engine$eval_text(code)
  expect_equal(result, 3)
})

test_that("partial input errors", {
  thin()
  engine <- make_engine(load_prelude = FALSE)

  # Unclosed paren - should error
  expect_error(engine$eval_text("(+ 1 2"))

  # Unclosed string - should error
  expect_error(engine$eval_text('"unclosed string'))

  # Unclosed list - should error
  expect_error(engine$eval_text("'(1 2 3"))
})

test_that("symbols with special characters", {
  thin()
  engine <- make_engine(load_prelude = FALSE)

  # Symbols can have hyphens
  engine$eval_text("(define test-var 42)")
  result <- engine$eval_text("test-var")
  expect_equal(result, 42)

  # Symbols can have question marks
  engine$eval_text("(import math :refer (%))
                    (define even? (lambda (x) (= (% x 2) 0)))")
  result <- engine$eval_text("(even? 4)")
  expect_equal(result, TRUE)

  # Symbols can have exclamation marks
  engine$eval_text("(define important! 100)")
  result <- engine$eval_text("important!")
  expect_equal(result, 100)
})

test_that("keywords", {
  thin()
  engine <- make_engine(load_prelude = FALSE)

  # Keywords start with colon and are self-evaluating
  result <- engine$eval_text(":keyword")
  expect_true(!is.null(result))

  # Quoted keywords can be compared as values
  result <- engine$eval_text("(= ':foo ':foo)")
  expect_equal(result, TRUE)

  result <- engine$eval_text("(= ':foo ':bar)")
  expect_equal(result, FALSE)

  # Bare keywords in argument position are treated as named argument syntax
  # :foo :foo becomes a named arg foo=(symbol foo), so = sees 1 arg (vacuously true)
  result <- engine$eval_text("(= :foo :foo)")
  expect_true(result)
})

test_that("dotted pairs / improper lists", {
  thin()
  engine <- make_engine(load_prelude = FALSE)

  # This may not be supported, but test that it doesn't crash
  expect_error(
    engine$eval_text("'(1 . 2)"),
    NA  # Just ensure it doesn't crash silently
  )
})

test_that("nested quotes", {
  thin()
  engine <- make_engine(load_prelude = FALSE)

  result <- engine$eval_text("''x")
  # Should be (quote x) - the outer quote is evaluated, returning the inner quote form
  expect_true(is.call(result))
})

test_that("empty string", {
  thin()
  engine <- make_engine(load_prelude = FALSE)

  result <- engine$eval_text('""')
  expect_equal(result, "")
})

test_that("string with only whitespace", {
  thin()
  engine <- make_engine(load_prelude = FALSE)

  result <- engine$eval_text('"   "')
  expect_equal(result, "   ")

  result <- engine$eval_text('"\n\t"')
  expect_equal(result, "\n\t")
})

test_that("very long symbol names", {
  thin()
  engine <- make_engine(load_prelude = FALSE)

  long_name <- paste(rep("a", 1000), collapse = "")
  code <- sprintf("(define %s 42)", long_name)
  engine$eval_text(code)

  result <- engine$eval_text(long_name)
  expect_equal(result, 42)
})

test_that("very long strings", {
  thin()
  engine <- make_engine(load_prelude = FALSE)

  long_string <- paste(rep("a", 10000), collapse = "")
  code <- sprintf('"%s"', long_string)
  result <- engine$eval_text(code)
  expect_equal(nchar(result), 10000)
})

test_that("deeply nested lists", {
  thin()
  engine <- make_engine(load_prelude = FALSE)

  # Create deeply nested list
  nested <- "'("
  for (i in 1:100) {
    nested <- paste0(nested, "(")
  }
  nested <- paste0(nested, "x")
  for (i in 1:100) {
    nested <- paste0(nested, ")")
  }
  nested <- paste0(nested, ")")

  expect_no_error({
    result <- engine$eval_text(nested)
  })
})

Try the arl package in your browser

Any scripts or data that you put into this service are public.

arl documentation built on March 19, 2026, 5:09 p.m.