tinymux/testcases/tools
Stephen Dennis 0a934a7d78 build(ragel): guard date_scan and strip #line from the remaining outputs (#2029)
Two remainders from #2025.

## date_scan.cpp was unguarded

hooks/pre-commit knew about four Ragel outputs; mux/lib/date_scan.cpp was
absent from GENERATED_GENS entirely, so it could be hand-edited and
committed without its .rl and nothing objected -- while being listed in
docs/generated-files.md and shipped via unix/TOC.patchable. Added to all
three parallel arrays, which now hold 27 each.

Verified by alignment rather than by count: equal lengths prove nothing
about correspondence, so the check confirms every ragel triple's basenames
match and its rule lives in the same directory as its output. Then
exercised: staging date_scan.cpp alone is now blocked, naming date_scan.rl
as its source.

## Six more outputs carried #line

#2025's scope was right for what the mux build regenerates and dounix.sh
ships, but these have live rules of their own and could still dirty a tree:

  testcases/tools/unformat.c    28    testcases/tools/Makefile  (%.c: %.rl)
  testcases/tools/reformat.c    23    same
  ragel/trigger_match.c         16    ragel/Makefile
  tools/ansify/ansify.c         19    tools/ansify/Makefile  (make regen)
  client/tf/src/script_lex.cpp  67    client/tf/CMakeLists.txt
  client/tf/src/input_lex.cpp   62    same

The issue reported "no rule found" for trigger_match.c and ansify.c. Both
were wrong: ragel/Makefile:25 generates trigger_match.c on dependency, and
tools/ansify/Makefile has an explicit `regen` target. Neither is inert --
ansify's is opt-in rather than implicit, which is why it reads as absent.

Each regenerated through its OWN rule rather than by running sed by hand,
so the rule is what is under test. All six are idempotent across a second
regeneration, and the diffs are 215 deletions, every one a #line, nothing
added.

ragel/color_ops.c is generated from the same ../mux/lib/color_ops.rl as the
shipped copy but is not tracked, so it could not dirty anything; its rule
gets the strip anyway, so the two generations of one source cannot disagree
if it is ever checked in.

client/tf could not be built here -- src/regex_utils.h needs pcre2.h, which
is not on that target's include path on macOS -- but that is a pre-existing
gap unrelated to this change. The Ragel custom commands themselves DID run
(cmake configure succeeds once NCURSESW_LIB is pointed at a macOS ncurses),
and both outputs came back stripped, so the CMake edit is exercised rather
than assumed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 00:59:11 -06:00
..
jit_diff test(jit_diff): add float, branch and nested-evaluation corpus shapes 2026-07-25 02:18:15 -06:00
jit_ifelse fix(jit): decide ifelse()/if() conditions with xlate(), not atol() (#1157) 2026-07-25 00:11:29 -06:00
jit_qreg jit: flip jit_eval_brackets default ON, retire jiteval (Phase 5 flip) 2026-07-21 09:09:39 -06:00
jit_recursion fix(jit): stop the shared heap re-entering its own DBT context (#1994) 2026-08-03 13:11:28 -06:00
.gitignore Add Makefile for testcases/tools Ragel utilities 2026-03-19 19:17:54 -06:00
benchsetup.sh docs: add June 2026 JIT/eval performance survey + bench harness 2026-07-09 09:30:15 -06:00
build-msvc.sh build(win32): add /utf-8 to the MSVC projects (#1499) 2026-07-26 20:58:42 -06:00
Build.sh test(blob): fail when softlib.rv64 is stale or unbuildable (#1924) 2026-07-31 23:18:47 -06:00
BuildAndSmoke Switch Makesmoke and Smoke to muxscript, fix interactive flag 2026-03-26 10:50:08 -06:00
check_vacuous.py test(smoke): guard against cases that cannot fail (#1434 family) 2026-07-26 21:00:15 -06:00
Clean.sh Fix test infrastructure bugs found during first run 2026-03-26 10:03:28 -06:00
FnCoverage test(tools): FnCoverage counted builtins this build does not have (#1160) 2026-07-27 11:26:46 -06:00
generate_smoke_suite.py test(smoke): guard against cases that cannot fail (#1434 family) 2026-07-26 21:00:15 -06:00
Makefile build(ragel): guard date_scan and strip #line from the remaining outputs (#2029) 2026-08-04 00:59:11 -06:00
Makesmoke Merge master into preserve-smoke-fail; keep collector + early-exit call 2026-07-27 12:55:52 +00:00
msvc_compat.h fix(tests): build the smoke tools without make, on Windows (#1347) (#1414) 2026-07-26 14:07:47 -06:00
PerfSmoke Eliminate 4MB memset per compilation, hash function lookup 2026-03-26 14:52:19 -06:00
PerfSmokeWin test(perf): PerfSmokeWin — ETW CPU-sampling analog of PerfSmoke (#1920 W1) 2026-07-31 22:50:46 -06:00
README.md fix(tests): build the smoke tools without make, on Windows (#1347) (#1414) 2026-07-26 14:07:47 -06:00
reformat.c build(ragel): guard date_scan and strip #line from the remaining outputs (#2029) 2026-08-04 00:59:11 -06:00
reformat.rl Tools: add reformat softcode formatter 2026-03-15 23:07:32 -06:00
scrubflat.sed Avoid using SED's extended regular expressions because the command-line isn't portable between Linux and FreeBSD. 2010-09-12 14:24:59 -07:00
SlaveDNS Fix reverse DNS slave: strip trailing newline from input 2026-03-18 20:12:22 -06:00
Smoke test(smoke): report the skip count instead of absorbing it into PASSED 2026-08-01 21:34:29 -06:00
SmokeParallel Add isolated parallel smoke runners 2026-04-10 20:30:21 -06:00
test_unicode_icu.cpp Fix Unicode table pipeline and ICU root collation test 2026-03-24 19:11:22 -06:00
unformat.c build(ragel): guard date_scan and strip #line from the remaining outputs (#2029) 2026-08-04 00:59:11 -06:00
unformat.pl Add testcases tree at top-level. 2007-05-03 23:53:58 -07:00
unformat.rl Replace unformat.pl with Ragel -G2 C implementation 2026-03-15 20:45:48 -06:00
upload.tcl Strip all MEMORY_BASED and SQLITE_STORAGE preprocessor conditionals. 2026-03-05 10:10:00 -07:00

MUX Softcode Formatting Tools

Two Ragel -G2 tools for working with MUX softcode (.mux files):

  • unformat -- joins indented, human-readable softcode into the single-line format that MUX servers expect (replaces unformat.pl).
  • reformat -- the inverse: takes single-line commands and re-introduces indentation based on brace structure.

Together they provide a useful mostly-round-trip workflow for ordinary softcode:

unformat *.mux | reformat > readable.mux   # flatten then re-indent
unformat readable.mux                      # identical to original

Building

ragel -G2 -o unformat.c unformat.rl && cc -O2 -o unformat unformat.c
ragel -G2 -o reformat.c reformat.rl && cc -O2 -o reformat reformat.c

Makesmoke builds unformat automatically if the binary is missing or stale.

ragel is only needed to regenerate the .c files from the .rl sources. The .c files are checked in, so a box without ragel builds fine.

Windows

A Windows box that can build TinyMUX has Visual Studio but generally no make, gcc or cc, so tools/Makefile cannot run. Makesmoke detects this and falls back to tools/build-msvc.sh, which locates the compiler via vswhere and builds unformat.exe with cl. You can also run it directly:

./tools/build-msvc.sh

Two Windows-specific details it handles, both of which produce misleading errors if you compile by hand instead:

  • ragel's -G2 output declares const char *eof __attribute__((unused)), which cl rejects. msvc_compat.h is force-included to define the keyword away. The declaration comes from ragel rather than from unformat.rl, so there is nothing to fix in the .rl, and the generated .c must not be hand-edited.
  • Makesmoke assigns its scratch file to the shell variable TMP, which cl inherits as its temp directory and then fails with cannot create linker response file. The generated build batch resets TMP/TEMP first.

reformat does not build under MSVC — it uses POSIX getline() and ssize_t. Nothing in the smoke path needs it.

Prerequisites for running the suite on Windows, beyond a TinyMUX build:

need why
Git Bash or MSYS Makesmoke and Smoke are shell scripts
Python 3 generate_smoke_suite.py discovers the corpus and builds the cleanup manifest
Visual Studio C++ builds unformat via the fallback above

muxscript.exe and dbconvert.exe must both be present in the game bin directory. dbconvert is netmux under another name — it dispatches on argv[0] — so copying netmux.exe to dbconvert.exe is sufficient and is what the Windows build does not do for you.


unformat

unformat joins continuation lines into single-line commands.

Usage

./unformat file1.mux [file2.mux ...] > output.txt

Output goes to stdout. Warnings and errors go to stderr.

Formatting Rules

Commands

A line that starts with a non-whitespace character begins a new command. Subsequent lines that start with whitespace are continuation lines: the leading whitespace is stripped and the content is appended directly to the current command with no space inserted.

A line containing only - (dash) ends the current command.

# Example: the following formatted softcode...
&CMD-TEST me=$@test:
    @switch hasflag(%#, wizard)=1, {
        @pemit %#=
            Test worked.
    }, {
        @pemit %#=You ain't no wizard!
    }
-

# ...produces this single line:
# &CMD-TEST me=$@test:@switch hasflag(%#, wizard)=1, {@pemit %#=Test worked.}, {@pemit %#=You ain't no wizard!}

Space-Backslash Continuation ( \)

Because continuation lines join without inserting a space, you need a way to signal "there should be a space here." A trailing space followed by backslash ( \) at the end of a line inserts a space at the join point. The \ itself is consumed and not emitted.

&greeting obj=You have 5 \
    coins remaining.
-
# produces: &greeting obj=You have 5 coins remaining.

A bare backslash at the end of a line (no preceding space) is emitted literally. Backslash is a valid softcode escape character (\n, \t, etc.), so only the \ (space-backslash) form is special.

&test obj=line one\n \
    line two
-
# produces: &test obj=line one\n line two
#                          ^^ literal \n    ^ space from ' \'

Comments

Lines starting with # are comments and are skipped entirely. The exception is #include:

# This is a comment.
#include helpers.mux

#include

#include <filename> inserts the contents of <filename> at that point. Cycle detection prevents the same file from being included twice; a warning is emitted on stderr if a duplicate include is attempted.

Empty Lines

Blank lines (empty or whitespace-only) are skipped and do not affect the output.

Warnings

unformat emits warnings to stderr for common mistakes. These do not affect the output -- the file is still processed normally.

Missing - Terminator

If a new command starts while the previous command has no - end marker, or if a file ends with an unterminated command:

foo.mux:12: warning: command has no '-' terminator

The command is still emitted, but the missing marker often indicates a formatting mistake (e.g., the next command was accidentally swallowed as a continuation).

Digit-Alpha Merge

If a continuation join produces a digit immediately followed by a letter with no space between them:

foo.mux:15: warning: digit-alpha merge '5c' at join -- did you mean to end the previous line with ' \'?

This catches the most common class of accidental token merges. The output is still produced as-is, but the warning tells you where to look.

#include File Not Found

foo.mux:3: error: can't open 'missing.mux'

Processing continues with the remaining input.

Migration from unformat.pl

unformat is a drop-in replacement. It produces byte-identical output to the Perl script on all existing .mux files. The two new features ( \ continuation and warnings) are purely additive -- no existing files need to change.

To adopt \ in place of fragile trailing spaces, change lines like:

# Old style: trailing space (invisible, tools strip it)
&attr obj=You have 5
    coins.
-

to:

# New style: explicit ' \' (visible, tools leave it alone)
&attr obj=You have 5 \
    coins.
-

reformat

reformat is the inverse of unformat: it takes single-line MUX commands and re-introduces indentation based on brace and semicolon structure.

Usage

./reformat [file ...]              # reads stdin if no files given
./unformat *.mux | ./reformat      # re-indent from flat form

How It Works

reformat walks each input line character by character, tracking three depth counters:

Counter Characters Purpose
depth { } Brace nesting -- drives indentation
pdepth ( ) Parenthesis nesting -- suppresses breaks inside function calls
bdepth [ ] Bracket nesting -- suppresses breaks inside eval brackets

Breaks are inserted only when pdepth == 0 and bdepth == 0:

Pattern Action
{ Emit {, increase depth, break to new line
} Decrease depth, break to new line, emit }
; Emit ;, break to new line (same depth)
},{ Kept together on one line at the outer depth (common @if/@switch pattern)
{} Kept together, no break (empty braces)

All continuation lines are indented with 4 + depth * 4 spaces, ensuring they start with whitespace for unformat compatibility.

Example

Input (single line):

&test obj=@if expr={@log ok;@trig me/done},{@log fail;@trig me/done}

Output:

&test obj=@if expr={
        @log ok;
        @trig me/done
    },{
        @log fail;
        @trig me/done
    }
-

Nested example:

&test obj=@if outer={@if inner={deep;deeper},{shallow}},{other}

Becomes:

&test obj=@if outer={
        @if inner={
            deep;
            deeper
        },{
            shallow
        }
    },{
        other
    }
-

Round-Trip

The reformat output is a valid .mux file. Running it back through unformat produces the original single-line commands:

# Start with flat commands
echo '@if 1={a;b},{c}' > flat.txt

# reformat, then unformat -- identical to original
cat flat.txt | ./reformat | ./unformat /dev/stdin 2>/dev/null | head -1
# Output: @if 1={a;b},{c}

The round-trip is exact for many common command shapes, but reformat is still a structural pretty-printer rather than a full softcode parser. Content with literal top-level braces or semicolons may be re-indented differently. Blank-line spacing between commands may also differ (unformat's "extraspace" vs reformat's single-line - terminators).

Error-path assertions: no parentheses in expected strings

MUX error text contains a parenthesised function name:

#-1 FUNCTION (RIGHT) EXPECTS 2 ARGUMENTS

Do not put that verbatim inside another function argument. Write the pattern with a wildcard in place of the name:

strmatch(right(hello,3,x), #-1 FUNCTION * EXPECTS 2 ARGUMENTS)

Why: an expected string carrying ( has to survive argument scanning intact. When it did not (#1219), the pattern truncated at the first ), the leaked tail closed the enclosing cand() early, and the failing condition was silently dropped from the assertion list — so the arity and error-path cases passed vacuously, on exactly the tests written to catch those bugs. That was only noticed because fixing the parser made the suite's pass count go up.

The wildcard form keeps the part that carries the meaning (EXPECTS 2 ARGUMENTS still distinguishes 2 from 3) while removing the structural hazard. #-1* alone is acceptable where the specific text does not matter.

Greppable check — this should return only comments:

grep -n 'FUNCTION (' testcases/*.mux