feat: add stress test projects and testing documentation

- Add String.fromChar function to convert Char to String
- Create four stress test projects demonstrating Lux features:
  - json-parser: recursive descent parsing with Char handling
  - markdown-converter: string manipulation and ADTs
  - todo-app: list operations and pattern matching
  - mini-interpreter: AST evaluation and environments
- Add comprehensive testing documentation (docs/testing.md)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-13 18:53:27 -05:00
parent a6eb349d59
commit 730112a917
7 changed files with 900 additions and 0 deletions

View File

@@ -0,0 +1,226 @@
// JSON Parser - Stress tests recursion, ADTs, and string manipulation
//
// This demonstrates:
// - Recursive descent parsing
// - Complex ADTs for AST representation
// - Character handling with Char type
// - Error handling with Result type
// JSON Value representation
type JsonValue =
| JsonNull
| JsonBool(Bool)
| JsonNumber(Int)
| JsonString(String)
| JsonArray(List<JsonValue>)
// Parser state: input chars and position
type ParseState =
| ParseState(List<Char>, Int)
// Parser result
type ParseResult<T> =
| ParseOk(T, ParseState)
| ParseErr(String)
// Get current character
fn peek(state: ParseState): Option<Char> =
match state {
ParseState(chars, pos) => List.get(chars, pos)
}
// Advance position
fn advance(state: ParseState): ParseState =
match state {
ParseState(chars, pos) => ParseState(chars, pos + 1)
}
// Skip whitespace
fn skipWhitespace(state: ParseState): ParseState =
match peek(state) {
None => state,
Some(c) =>
if c == ' ' || c == '\t' || c == '\n' then
skipWhitespace(advance(state))
else state
}
// Check if character is a digit
fn isDigit(c: Char): Bool =
c == '0' || c == '1' || c == '2' || c == '3' || c == '4' ||
c == '5' || c == '6' || c == '7' || c == '8' || c == '9'
fn digitValue(c: Char): Int =
if c == '0' then 0
else if c == '1' then 1
else if c == '2' then 2
else if c == '3' then 3
else if c == '4' then 4
else if c == '5' then 5
else if c == '6' then 6
else if c == '7' then 7
else if c == '8' then 8
else if c == '9' then 9
else 0
// Parse a number
fn parseNumber(state: ParseState): ParseResult<Int> =
parseDigits(state, 0, false)
fn parseDigits(state: ParseState, acc: Int, hasDigits: Bool): ParseResult<Int> =
match peek(state) {
None =>
if hasDigits then ParseOk(acc, state)
else ParseErr("Expected digit"),
Some(c) =>
if isDigit(c) then
parseDigits(advance(state), acc * 10 + digitValue(c), true)
else if hasDigits then ParseOk(acc, state)
else ParseErr("Expected digit")
}
// Parse a string (simple version - no escapes)
fn parseString(state: ParseState): ParseResult<String> =
match peek(state) {
None => ParseErr("Unexpected end"),
Some(c) =>
if c != '"' then ParseErr("Expected quote")
else parseStringContent(advance(state), "")
}
fn charToString(c: Char): String =
String.fromChar(c)
fn parseStringContent(state: ParseState, acc: String): ParseResult<String> =
match peek(state) {
None => ParseErr("Unterminated string"),
Some(c) =>
if c == '"' then ParseOk(acc, advance(state))
else parseStringContent(advance(state), acc + charToString(c))
}
// Check if next chars match a keyword
fn matchKeyword(state: ParseState, keyword: String): Bool =
matchKeywordChars(state, String.chars(keyword), 0)
fn matchKeywordChars(state: ParseState, keywordChars: List<Char>, idx: Int): Bool =
match List.get(keywordChars, idx) {
None => true,
Some(kc) =>
match state {
ParseState(chars, pos) =>
match List.get(chars, pos + idx) {
None => false,
Some(ic) => if ic == kc then matchKeywordChars(state, keywordChars, idx + 1) else false
}
}
}
fn advanceBy(state: ParseState, n: Int): ParseState =
if n <= 0 then state
else advanceBy(advance(state), n - 1)
// Main value parser
fn parseValue(state: ParseState): ParseResult<JsonValue> = {
let s = skipWhitespace(state)
match peek(s) {
None => ParseErr("Unexpected end of input"),
Some(c) =>
if matchKeyword(s, "null") then
ParseOk(JsonNull, advanceBy(s, 4))
else if matchKeyword(s, "true") then
ParseOk(JsonBool(true), advanceBy(s, 4))
else if matchKeyword(s, "false") then
ParseOk(JsonBool(false), advanceBy(s, 5))
else if c == '"' then
match parseString(s) {
ParseErr(e) => ParseErr(e),
ParseOk(str, newState) => ParseOk(JsonString(str), newState)
}
else if c == '[' then parseArray(s)
else if isDigit(c) then
match parseNumber(s) {
ParseErr(e) => ParseErr(e),
ParseOk(num, newState) => ParseOk(JsonNumber(num), newState)
}
else ParseErr("Unexpected character")
}
}
// Parse array
fn parseArray(state: ParseState): ParseResult<JsonValue> =
match peek(state) {
None => ParseErr("Unexpected end"),
Some(c) =>
if c != '[' then ParseErr("Expected [")
else parseArrayElements(skipWhitespace(advance(state)), [])
}
fn parseArrayElements(state: ParseState, acc: List<JsonValue>): ParseResult<JsonValue> =
match peek(state) {
None => ParseErr("Unterminated array"),
Some(c) =>
if c == ']' then ParseOk(JsonArray(acc), advance(state))
else match parseValue(state) {
ParseErr(e) => ParseErr(e),
ParseOk(value, newState) => {
let afterComma = skipWhitespace(newState)
match peek(afterComma) {
None => ParseErr("Unterminated array"),
Some(next) =>
if next == ']' then ParseOk(JsonArray(List.concat(acc, [value])), advance(afterComma))
else if next == ',' then parseArrayElements(skipWhitespace(advance(afterComma)), List.concat(acc, [value]))
else ParseErr("Expected , or ]")
}
}
}
}
// Public parse function
fn parse(input: String): Result<JsonValue, String> =
match parseValue(ParseState(String.chars(input), 0)) {
ParseErr(e) => Err(e),
ParseOk(value, _) => Ok(value)
}
// Pretty print JSON
fn valueToString(value: JsonValue): String =
match value {
JsonNull => "null",
JsonBool(b) => if b then "true" else "false",
JsonNumber(n) => toString(n),
JsonString(s) => "\"" + s + "\"",
JsonArray(items) => "[" + String.join(List.map(items, valueToString), ", ") + "]"
}
// Test the parser
fn main(): Unit with {Console} = {
Console.print("=== JSON Parser Test ===")
Console.print("")
Console.print("Test 1: Simple values")
testParse("null")
testParse("true")
testParse("false")
testParse("42")
testParse("\"hello\"")
Console.print("")
Console.print("Test 2: Arrays")
testParse("[]")
testParse("[1, 2, 3]")
testParse("[true, false, null]")
testParse("[1, [2, 3], 4]")
Console.print("")
Console.print("Test 3: Nested arrays")
testParse("[[1, 2], [3, 4]]")
}
fn testParse(input: String): Unit with {Console} =
match parse(input) {
Ok(value) => Console.print(" " + input + " => " + valueToString(value)),
Err(e) => Console.print(" ERROR: " + e)
}
let output = run main() with {}

View File

@@ -0,0 +1,79 @@
// Markdown to HTML Converter - Stress tests string manipulation and parsing
//
// This demonstrates:
// - String parsing and manipulation
// - ADTs for document structure
// - Recursive processing
// - Pattern matching on strings
// Markdown AST
type MarkdownNode =
| MHeading(Int, String)
| MParagraph(String)
| MList(Bool, List<String>)
| MHorizontalRule
// Parse a line to determine block type
fn parseBlockType(line: String): MarkdownNode =
if String.startsWith(line, "# ") then
MHeading(1, String.substring(line, 2, String.length(line)))
else if String.startsWith(line, "## ") then
MHeading(2, String.substring(line, 3, String.length(line)))
else if String.startsWith(line, "### ") then
MHeading(3, String.substring(line, 4, String.length(line)))
else if String.startsWith(line, "---") then
MHorizontalRule
else if String.startsWith(line, "- ") then
MList(false, [String.substring(line, 2, String.length(line))])
else
MParagraph(line)
// Convert to HTML
fn nodeToHtml(node: MarkdownNode): String =
match node {
MHeading(level, text) => "<h" + toString(level) + ">" + text + "</h" + toString(level) + ">",
MParagraph(text) => if text == "" then "" else "<p>" + text + "</p>",
MList(ordered, items) => {
let tag = if ordered then "ol" else "ul"
let itemsHtml = List.map(items, fn(item: String): String => "<li>" + item + "</li>")
"<" + tag + ">" + String.join(itemsHtml, "") + "</" + tag + ">"
},
MHorizontalRule => "<hr />"
}
// Convert markdown string to HTML
fn convert(markdown: String): String = {
let lines = String.lines(markdown)
let nodes = List.map(lines, parseBlockType)
let htmlLines = List.filter(List.map(nodes, nodeToHtml), fn(s: String): Bool => s != "")
String.join(htmlLines, "\n")
}
// Test the converter
fn main(): Unit with {Console} = {
Console.print("========================================")
Console.print(" MARKDOWN TO HTML CONVERTER")
Console.print("========================================")
Console.print("")
Console.print("Test 1: Headings")
Console.print(convert("# Heading 1"))
Console.print(convert("## Heading 2"))
Console.print(convert("### Heading 3"))
Console.print("")
Console.print("Test 2: Paragraphs")
Console.print(convert("This is a paragraph."))
Console.print(convert("Another paragraph here."))
Console.print("")
Console.print("Test 3: Lists")
Console.print(convert("- Item 1"))
Console.print(convert("- Item 2"))
Console.print("")
Console.print("Test 4: Horizontal rule")
Console.print(convert("---"))
}
let output = run main() with {}

View File

@@ -0,0 +1,180 @@
// Mini Interpreter - Stress tests complex ADTs, recursion, and pattern matching
//
// This demonstrates:
// - Abstract syntax trees
// - Recursive evaluation
// - Pattern matching on complex types
// - Error handling
// Value types in our mini language
type Value =
| VInt(Int)
| VBool(Bool)
| VString(String)
| VUnit
// Expression AST
type Expr =
| EInt(Int)
| EBool(Bool)
| EString(String)
| EVar(String)
| EAdd(Expr, Expr)
| ESub(Expr, Expr)
| EMul(Expr, Expr)
| EDiv(Expr, Expr)
| EEq(Expr, Expr)
| ELt(Expr, Expr)
| EGt(Expr, Expr)
| EIf(Expr, Expr, Expr)
| ELet(String, Expr, Expr)
// Environment (variable bindings)
type Env =
| EmptyEnv
| ExtendEnv(String, Value, Env)
// Environment operations
fn envLookup(env: Env, name: String): Option<Value> =
match env {
EmptyEnv => None,
ExtendEnv(n, v, rest) => if n == name then Some(v) else envLookup(rest, name)
}
fn envExtend(env: Env, name: String, value: Value): Env =
ExtendEnv(name, value, env)
// Value pretty printing
fn valueToString(v: Value): String =
match v {
VInt(n) => toString(n),
VBool(b) => if b then "true" else "false",
VString(s) => "\"" + s + "\"",
VUnit => "()"
}
// Main evaluator
fn eval(expr: Expr, env: Env): Result<Value, String> =
match expr {
EInt(n) => Ok(VInt(n)),
EBool(b) => Ok(VBool(b)),
EString(s) => Ok(VString(s)),
EVar(name) => match envLookup(env, name) {
None => Err("Unbound variable: " + name),
Some(v) => Ok(v)
},
EAdd(left, right) => evalBinOp(left, right, env, fn(a: Int, b: Int): Int => a + b),
ESub(left, right) => evalBinOp(left, right, env, fn(a: Int, b: Int): Int => a - b),
EMul(left, right) => evalBinOp(left, right, env, fn(a: Int, b: Int): Int => a * b),
EDiv(left, right) => evalDiv(left, right, env),
EEq(left, right) => evalCompare(left, right, env, fn(a: Int, b: Int): Bool => a == b),
ELt(left, right) => evalCompare(left, right, env, fn(a: Int, b: Int): Bool => a < b),
EGt(left, right) => evalCompare(left, right, env, fn(a: Int, b: Int): Bool => a > b),
EIf(cond, thenBranch, elseBranch) => evalIf(cond, thenBranch, elseBranch, env),
ELet(name, valueExpr, bodyExpr) => evalLet(name, valueExpr, bodyExpr, env)
}
fn evalBinOp(left: Expr, right: Expr, env: Env, op: fn(Int, Int): Int): Result<Value, String> =
match eval(left, env) {
Err(e) => Err(e),
Ok(leftVal) => match eval(right, env) {
Err(e) => Err(e),
Ok(rightVal) => match (leftVal, rightVal) {
(VInt(a), VInt(b)) => Ok(VInt(op(a, b))),
(_, _) => Err("Type error: expected Int")
}
}
}
fn evalDiv(left: Expr, right: Expr, env: Env): Result<Value, String> =
match eval(left, env) {
Err(e) => Err(e),
Ok(leftVal) => match eval(right, env) {
Err(e) => Err(e),
Ok(rightVal) => match (leftVal, rightVal) {
(VInt(a), VInt(b)) => if b == 0 then Err("Division by zero") else Ok(VInt(a / b)),
(_, _) => Err("Type error: expected Int")
}
}
}
fn evalCompare(left: Expr, right: Expr, env: Env, op: fn(Int, Int): Bool): Result<Value, String> =
match eval(left, env) {
Err(e) => Err(e),
Ok(leftVal) => match eval(right, env) {
Err(e) => Err(e),
Ok(rightVal) => match (leftVal, rightVal) {
(VInt(a), VInt(b)) => Ok(VBool(op(a, b))),
(_, _) => Err("Type error: expected Int")
}
}
}
fn evalIf(cond: Expr, thenBranch: Expr, elseBranch: Expr, env: Env): Result<Value, String> =
match eval(cond, env) {
Err(e) => Err(e),
Ok(condVal) => match condVal {
VBool(true) => eval(thenBranch, env),
VBool(false) => eval(elseBranch, env),
_ => Err("Condition must be boolean")
}
}
fn evalLet(name: String, valueExpr: Expr, bodyExpr: Expr, env: Env): Result<Value, String> =
match eval(valueExpr, env) {
Err(e) => Err(e),
Ok(value) => eval(bodyExpr, envExtend(env, name, value))
}
// Run a program and print result
fn runProgram(expr: Expr): Unit with {Console} =
match eval(expr, EmptyEnv) {
Ok(value) => Console.print("=> " + valueToString(value)),
Err(e) => Console.print("Error: " + e)
}
// Test programs
fn program1(): Expr = ELet("x", EInt(10), ELet("y", EInt(20), EAdd(EVar("x"), EVar("y"))))
fn program2(): Expr = ELet("a", EInt(5), EMul(EVar("a"), EAdd(EVar("a"), EInt(1))))
fn program3(): Expr = EIf(EGt(EInt(10), EInt(5)), EString("yes"), EString("no"))
fn program4(): Expr = ELet("n", EInt(5), ELet("a", EMul(EVar("n"), ESub(EVar("n"), EInt(1))), ELet("b", EMul(EVar("a"), ESub(EVar("n"), EInt(2))), ELet("c", EMul(EVar("b"), ESub(EVar("n"), EInt(3))), EMul(EVar("c"), ESub(EVar("n"), EInt(4)))))))
fn program5(): Expr = EDiv(EInt(10), EInt(0))
fn program6(): Expr = EVar("undefined")
// Main
fn main(): Unit with {Console} = {
Console.print("========================================")
Console.print(" MINI INTERPRETER DEMO")
Console.print("========================================")
Console.print("")
Console.print("Program 1: let x = 10 in let y = 20 in x + y")
runProgram(program1())
Console.print("")
Console.print("Program 2: let a = 5 in a * (a + 1)")
runProgram(program2())
Console.print("")
Console.print("Program 3: if 10 > 5 then \"yes\" else \"no\"")
runProgram(program3())
Console.print("")
Console.print("Program 4: 5! computed iteratively")
runProgram(program4())
Console.print("")
Console.print("Program 5: Division by zero error")
runProgram(program5())
Console.print("")
Console.print("Program 6: Unbound variable error")
runProgram(program6())
}
let output = run main() with {}

123
projects/todo-app/main.lux Normal file
View File

@@ -0,0 +1,123 @@
// Todo App - Stress tests ADTs, pattern matching, and list operations
//
// This demonstrates:
// - ADTs for data modeling
// - Pattern matching
// - List operations
// - Recursive display
// Todo item
type Priority =
| Low
| Medium
| High
type TodoItem =
| TodoItem(Int, String, Bool, Priority)
// Helper functions for TodoItem
fn getId(item: TodoItem): Int =
match item { TodoItem(id, _, _, _) => id }
fn getDescription(item: TodoItem): String =
match item { TodoItem(_, desc, _, _) => desc }
fn isCompleted(item: TodoItem): Bool =
match item { TodoItem(_, _, completed, _) => completed }
fn getPriority(item: TodoItem): Priority =
match item { TodoItem(_, _, _, priority) => priority }
fn setCompleted(item: TodoItem, completed: Bool): TodoItem =
match item { TodoItem(id, desc, _, priority) => TodoItem(id, desc, completed, priority) }
// Priority helpers
fn priorityToString(p: Priority): String =
match p {
Low => "Low",
Medium => "Medium",
High => "High"
}
fn priorityToSymbol(p: Priority): String =
match p {
Low => ".",
Medium => "*",
High => "!"
}
// Format a todo item for display
fn formatItem(item: TodoItem): String =
toString(getId(item)) + ". " + (if isCompleted(item) then "[x]" else "[ ]") + " " + priorityToSymbol(getPriority(item)) + " " + getDescription(item)
// Display functions using recursion
fn printItemsHelper(items: List<TodoItem>): Unit with {Console} =
match List.head(items) {
None => (),
Some(item) => {
Console.print(" " + formatItem(item))
match List.tail(items) {
None => (),
Some(rest) => printItemsHelper(rest)
}
}
}
fn displayItems(items: List<TodoItem>): Unit with {Console} =
if List.isEmpty(items) then Console.print(" (no items)")
else printItemsHelper(items)
fn countCompleted(items: List<TodoItem>): Int =
List.length(List.filter(items, isCompleted))
fn displayStats(items: List<TodoItem>): Unit with {Console} =
Console.print("Total: " + toString(List.length(items)) + " | Completed: " + toString(countCompleted(items)) + " | Pending: " + toString(List.length(items) - countCompleted(items)))
// Sample data - all on one line to avoid multiline list parse issues
fn createSampleData(): List<TodoItem> = [TodoItem(1, "Learn Lux basics", true, High), TodoItem(2, "Build a project", false, High), TodoItem(3, "Read documentation", true, Medium), TodoItem(4, "Write tests", false, Medium), TodoItem(5, "Share with team", false, Low)]
// Filter functions
fn filterCompleted(items: List<TodoItem>): List<TodoItem> =
List.filter(items, isCompleted)
fn filterPending(items: List<TodoItem>): List<TodoItem> =
List.filter(items, fn(item: TodoItem): Bool => isCompleted(item) == false)
// Main demo
fn main(): Unit with {Console} = {
Console.print("========================================")
Console.print(" TODO APP DEMO")
Console.print("========================================")
Console.print("")
let items = createSampleData()
Console.print("All Todo Items:")
Console.print("---------------")
displayItems(items)
Console.print("")
displayStats(items)
Console.print("")
Console.print("Completed Items:")
Console.print("----------------")
displayItems(filterCompleted(items))
Console.print("")
Console.print("Pending Items:")
Console.print("--------------")
displayItems(filterPending(items))
Console.print("")
Console.print("Marking item 2 as complete...")
let updatedItems = List.map(items, fn(item: TodoItem): TodoItem => if getId(item) == 2 then setCompleted(item, true) else item)
Console.print("")
Console.print("Updated Todo List:")
Console.print("------------------")
displayItems(updatedItems)
Console.print("")
displayStats(updatedItems)
}
let output = run main() with {}