Skip to content

frontend: split staging tests up into many small file tests#469

Merged
mikesamuel merged 8 commits into
mainfrom
stage-tests-in-files
Jul 24, 2026
Merged

frontend: split staging tests up into many small file tests#469
mikesamuel merged 8 commits into
mainfrom
stage-tests-in-files

Conversation

@mikesamuel

@mikesamuel mikesamuel commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

This commit starts the process of splitting tests that use the assertModuleAtStage test harness into using many small files so that changes to the IR that affect many tests in cosmetic ways can be handled more efficiently.

The process for deriving this change is more informative than the exact changes, so a step by step description of that process follows.

This commit aims to define the file conventions, introduce the machinery for running test from file trees, and creates the files from the existing tests, but it leaves using the file trees to specify test behaviour to a follow-on commit in the same PR.


I created a README for the resources sub-directory containing stage test files so that the plan lives somewhere well documented.

I added an argument to express the subdirectory to assertModuleAtStage and a function to decide whether to regenerate test files for test to AssertModuleAtStage.kt. I threaded that through both assertModuleAtStage variants. I also adjusted the module provisioning block function to take a list of edits so that I can leverage calls to assertModuleAtStage to regenerate both input files and outputs.

I factored out the default module provisioning block into a function and refactored most tests that used the custom provisioning block to delegate to it. The custom provisioning block is what will change to read input files from under work.

I looked for existing test files.

git ls-files | grep frontend | xargs egrep  -c assertModuleAtStage | \
  egrep -v :0 | perl -pe 's/:\d+$//' > /tmp/stage-test-files

I manually edited that to remove the test harness file, AssertModuleAtStage.kt, and removed EndToEndTest.kt after moving its one useful test into GenerateCodeStageTest.kt.

I wrote a Perl file to insert the extra directory argument into each test case. It started off just making sure that it can find files.

my $f = $ARGV[0];

open(IN, "<$f") or die "$!";

while (<IN>) {
    if (m/assertModuleAtStage\(/) {
        if (!m/^(\s*)fun (\w+)\(\) = assertModuleAtStage\($/) {
            print "$f:$.: $_";
        }
    }
}

close(IN) or die "$!";

This just prints out a list of line:file entries for each one that does not match an obvious pattern.

I ran it on each of the files from above:

for f in $(cat /tmp/stage-test-files); do perl /tmp/add-stage-dirs.perl $f; done

I iterated on that until there was nothing that did not match the pattern.

In the cases where a test was calling assertModuleAtStage inside its body instead of the same line as the function name, I used the following pattern:

 @Test
 fun myTest() {
-  assertModuleAtStage(
+  fun myTest() = assertModuleAtStage(
     ...
   )
+  myTest().doNotCommit
 }

Then I adjusted the perl script to pick a test directory for each test case.

my $f = $ARGV[0];

open(IN, "<$f") or die "$!";

my $stage = $f;
$stage =~ s/^.*[\/]//;
$stage =~ s/StageTest.kt$//;
$stage = "\l$stage";
$stage =~ s/[A-Z]/-\l$&/g;

my $content = "";
my $changed = 0;
while (<IN>) {
    $content .= $_;
    if (m/^(\s*)fun (\w+)\(\) = assertModuleAtStage\($/) {
        $changed = 1;
        my $ws = $1;
        my $testName = $2;
        $testName =~ s/[A-Z]/-\l$&/g;
        my $std = "$stage/$testName";
        $content .= qq'$ws    stageTestDir = StageTestDir("$std"),\n';
    }
}

close(IN) or die "$!";

if ($changed) {
    my $outf = "$f.changed";
    open(OUT, ">$outf") or die "$!";
    print OUT $content;
    close(OUT) or die "$!";
}

That spits out a $f.changed file for each input. Given a test named myTest in ParseStageTest it will make these changes

 fun myTest() = assertModuleAtStage(
+    stageTestDir = StageTestDir("parse/my-test"),
     input = """

I manually diffed and replaced the test files with the *.changed variants, ran the tests to make sure they still passed, and backed out the .doNotCommit tweaks.

I made the new stageTestDir input mandatory and reran gradle frontend:check.

I added some machinery for scanning test directories into file bundles to test-helpers/. This includes definitions so that a test run can inspect everything it has:

  1. It can use the input: String and languageConfig parameters to spit out one or more *.temper or *.temper.md files.
  2. Once that's done, we can get rid of those parameters and read files under work/ to provision modules.
  3. It can parse the want: String as JSON and factor that out into files under expect/ files until we can get rid of that input.
  4. Once we have the output files, it can look at those to figure out the maximum stage to advance to, getting rid of the stage parameter, and use the existence of files to drive building the got JSON bundle, and combine them into a want bundle to compare to got.

I reworked assertModuleAtStage and its helpers to do 1 and 3. I ran it on some inputs in a mode where instead of writing files, it just dumps the collected files to the console.

For step 1, I reused the code that splits a single input string into multiple mutually importable modules and just spit them out.

For step 3, I started with code like the below and iterated on it.

        // key is like ".parse.body"
        // Look for leaves and fail hard.
        // I'll add entries until all tests pass.
        // Then I'll have something that produces the files,
        // and I'll have a function to reverse to read the
        // files in.
        fun walk(stage: Stage?, key: String, value: JsonValue) {
            if (value is JsonLeaf<*>) {
                when (value) {
                    is JsonString -> when (key) {
                        else -> TODO("$key got string")
                    }
                    is JsonBoolean -> when (key) {
                        else -> TODO("$key got bool")
                    }
                    is JsonDouble -> when (key) {
                        else -> TODO("$key got double")
                    }
                    is JsonLong -> when (key) {
                        else -> TODO("$key got long")
                    }
                    JsonNull -> when (key) {
                        else -> TODO("$key got null")
                    }
                }
            } else if (value is JsonObject) {
                for (p in value) {
                    var nextStage = stage
                    var keySuffix = ".${p.key}"
                    if (key == "" && nextStage == null) {
                        nextStage = try {
                            Stage.valueOf(p.key.asciiTitleCase())
                        } catch (_: IllegalArgumentException) {
                            null
                        }
                        if (nextStage != null) {
                            keySuffix = ""
                        }
                    }
                    val nextKey = "$key$keySuffix"
                    walk(nextStage, nextKey, p.value)
                }
            } else {
                TODO("$key $wantJson")
            }
        }
        walk("", wantJson)

That let me piece together enough rules to decompose existing want JSON dumps into strings.

I realized that .stripDoubleHashCommentLinesToPutCommentsInlineBelow commentary is important in input and want strings. The below reports 35 uses.

egrep -n '[.]stripDoubleHash' $(cat /tmp/stage-test-files) | \
  grep -v import | wc -l

On inspection, all occurred inside the expected Temper pseudocode string blocks.

The convention I established is, for stage.temper pseudocode files, if the file is indented by two spaces ignoring ## lines, then I'll just strip out lines matching /^##/ on reading and strip the indented spaces off too.

I hacked together a JSON with triple-backquoted-string munger to re-indent the ## lines that appear inside them to get the right output when generating files.

Future commits will switch to reading from the set of files under the stage test root, use those to reconstruct the input and outputs, and retire those input parameters to assertStageTest.


I refactored assertModuleAtStage to use input files instead of input strings.


After switching parsing to use the input files, any TestFileBundle
entry that starts with work, I cut out the input strings.

I ran the following Perl script over the stage test Kotlin files.

use strict;

my $f = $ARGV[0];

open(IN, "<$f") or die "$f: $!";

my $content = "";
my $inParam = 0;
my $deferred = "";
while (<IN>) {
    if (!$inParam && m/^\s*input\s*=\s*[\$]*"""$/) {
        #print "switching to in param: $.: $_";
        $inParam = 1;
        $deferred = $_;
    } elsif ($inParam) {
        $deferred .= $_;
        if (m/^\s*"""/) {
            #print "switching to out param: $.: $_";
            $inParam = 0;
            if (m/^\s*"""(?:[.].*)?,$/) {
                #print "dropping param";
                $deferred = "";
            } else {
                $content .= $deferred;
            }
        }
    } elsif (m/^\s*input\s*=\s*\$*"(?:[^"\\]|\\.)*",$/) {
        # Skip it
    } elsif (m/^\s*input\s*=\s*\$*""".*?""",$/) {
        # Skip it
    } else {
        if (m/^\s*input\s*=/) {
            print "$f:$.: Not matched: $_";
        }
        $content .= $_;
    }
}
$content .= $deferred;

close(IN) or die "$f: $!";

open(OUT, ">$f.changed") or die "$f.changed: $!";
print OUT $content;
close(OUT) or die "$f: $!";

I used that on each of the stage test files manually.

perl /tmp/excise-inputs.perl frontend/src/commonTest/kotlin/lang/temper/frontend/GenerateCodeStageTest.kt
diff -uw frontend/src/commonTest/kotlin/lang/temper/frontend/GenerateCodeStageTest.kt{.changed,} | less
mv frontend/src/commonTest/kotlin/lang/temper/frontend/GenerateCodeStageTest.kt{.changed,}

Some inputs I had to fix up by hand like the below from GenerateCodeStageTest:

        input = buildString {
            append("export let numbers: Map<String, Int> = new Map<String, Int>([")
            for (i in 0 until 1000) {
                append("  new Pair<String, Int>(\"$i\", $i),")
            }
            append("]);")
        },

After implementing support for building the wanted JSON from the data
files under expect, I modified the excise-inputs perl script to get
rid of existing want and stage inputs.

use strict;

my $f = $ARGV[0];

open(IN, "<$f") or die "$f: $!";

my $content = "";
my $inParam = 0;
my $deferred = "";
while (<IN>) {
    if (!$inParam && m/^\s*want\s*=\s*[\$]*"""$/) {
        #print "switching to in param: $.: $_";
        $inParam = 1;
        $deferred = $_;
    } elsif ($inParam) {
        $deferred .= $_;
        if (m/\s*\@Test/) {
            print "$f:$.: probable error $_";
        }
        if (m/^\s*"""/) {
            #print "switching to out param: $.: $_";
            $inParam = 0;
            if (m/^\s*"""(?:[.].*)?,$/) {
                #print "dropping param";
                $deferred = "";
            } else {
                $content .= $deferred;
            }
        }
    } elsif (m/^\s*want\s*=\s*\$*"(?:[^"\\]|\\.)*",$/) {
        # Skip it
    } elsif (m/^\s*want\s*=\s*\$*""".*?""",$/) {
        # Skip it
    } elsif (m/^\s*stage\s*=\s*\$*Stage[.]\w+,$/) {
        # Skip it
    } else {
        if (m/^\s*want\s*=/) {
            print "$f:$.: Not matched: $_";
        }
        $content .= $_;
    }
}
$content .= $deferred;

close(IN) or die "$f: $!";

open(OUT, ">$f.changed") or die "$f.changed: $!";
print OUT $content;
close(OUT) or die "$f: $!";

I implemented regeneration of expect/* staging test files from the "got" JSON.


I added a test that all stage test dirs are used and revivified test
directories for @Ignored tests that were not extracted during earlier
steps.

This commit starts the process of splitting tests that use the
*assertModuleAtStage* test harness into using many small files
so that changes to the IR that affect many tests in cosmetic ways
can be handled more efficiently.

The process for deriving this change is more informative than the
exact changes, so a step by step description of that process follows.

This commit aims to define the file conventions, introduce the
machinery for running test from file trees, and creates the files
from the existing tests, but it leaves using the file trees to
specify test behaviour to a follow-on commit in the same PR.

----

I created a README for the resources sub-directory containing stage
test files so that the plan lives somewhere well documented.

I added an argument to express the subdirectory to assertModuleAtStage
and a function to decide whether to regenerate test files for test to
AssertModuleAtStage.kt.  I threaded that through both
assertModuleAtStage variants.  I also adjusted the module provisioning
block function to take a list of edits so that I can leverage calls to
assertModuleAtStage to regenerate both input files and outputs.

I factored out the default module provisioning block into a function
and refactored most tests that used the custom provisioning block to
delegate to it.  The custom provisioning block is what will change to
read input files from under work.

I looked for existing test files.

```sh
git ls-files | grep frontend | xargs egrep  -c assertModuleAtStage | \
  egrep -v :0 | perl -pe 's/:\d+$//' > /tmp/stage-test-files
```

I manually edited that to remove the test harness file,
*AssertModuleAtStage.kt*, and removed *EndToEndTest.kt* after moving
its one useful test into *GenerateCodeStageTest.kt*.

I wrote a Perl file to insert the extra directory argument into each
test case.  It started off just making sure that it can find files.

```perl
my $f = $ARGV[0];

open(IN, "<$f") or die "$!";

while (<IN>) {
    if (m/assertModuleAtStage\(/) {
        if (!m/^(\s*)fun (\w+)\(\) = assertModuleAtStage\($/) {
            print "$f:$.: $_";
        }
    }
}

close(IN) or die "$!";
```

This just prints out a list of line:file entries for each one
that does not match an obvious pattern.

I ran it on each of the files from above:

```sh
for f in $(cat /tmp/stage-test-files); do perl /tmp/add-stage-dirs.perl $f; done
```

I iterated on that until there was nothing that did not match the pattern.

In the cases where a test was calling *assertModuleAtStage* inside its
body instead of the same line as the function name, I used the following
pattern:

```diff
 @test
 fun myTest() {
-  assertModuleAtStage(
+  fun myTest() = assertModuleAtStage(
     ...
   )
+  myTest().doNotCommit
 }
```

Then I adjusted the perl script to pick a test directory for each test case.

```perl
my $f = $ARGV[0];

open(IN, "<$f") or die "$!";

my $stage = $f;
$stage =~ s/^.*[\/]//;
$stage =~ s/StageTest.kt$//;
$stage = "\l$stage";
$stage =~ s/[A-Z]/-\l$&/g;

my $content = "";
my $changed = 0;
while (<IN>) {
    $content .= $_;
    if (m/^(\s*)fun (\w+)\(\) = assertModuleAtStage\($/) {
        $changed = 1;
        my $ws = $1;
        my $testName = $2;
        $testName =~ s/[A-Z]/-\l$&/g;
        my $std = "$stage/$testName";
        $content .= qq'$ws    stageTestDir = StageTestDir("$std"),\n';
    }
}

close(IN) or die "$!";

if ($changed) {
    my $outf = "$f.changed";
    open(OUT, ">$outf") or die "$!";
    print OUT $content;
    close(OUT) or die "$!";
}
```

That spits out a `$f.changed` file for each input.
Given a test named `myTest` in `ParseStageTest` it will make these changes

```diff
 fun myTest() = assertModuleAtStage(
+    stageTestDir = StageTestDir("parse/my-test"),
     input = """
```

I manually diffed and replaced the test files with the `*.changed`
variants, ran the tests to make sure they still passed, and backed out
the *.doNotCommit* tweaks.

I made the new *stageTestDir* input mandatory and reran `gradle
frontend:check`.

I added some machinery for scanning test directories into file bundles
to `test-helpers/`.  This includes definitions so that a test run can
inspect everything it has:

1. It can use the `input: String` and `languageConfig` parameters to spit out one or more `*.temper` or `*.temper.md` files.
2. Once that's done, we can get rid of those parameters and read files under `work/` to provision modules.
3. It can parse the `want: String` as JSON and factor that out into files under `expect/` files until we can get rid of that input.
4. Once we have the output files, it can look at those to figure out the maximum stage to advance to, getting rid of the `stage` parameter, and use the existence of files to drive building the `got` JSON bundle, and combine them into a `want` bundle to compare to `got`.

I reworked *assertModuleAtStage* and its helpers to do 1 and 3.  I ran
it on some inputs in a mode where instead of writing files, it just
dumps the collected files to the console.

For step 1, I reused the code that splits a single input string into multiple mutually importable modules and just spit them out.

For step 3, I started with code like the below and iterated on it.

```kt
        // key is like ".parse.body"
        // Look for leaves and fail hard.
        // I'll add entries until all tests pass.
        // Then I'll have something that produces the files,
        // and I'll have a function to reverse to read the
        // files in.
        fun walk(stage: Stage?, key: String, value: JsonValue) {
            if (value is JsonLeaf<*>) {
                when (value) {
                    is JsonString -> when (key) {
                        else -> TODO("$key got string")
                    }
                    is JsonBoolean -> when (key) {
                        else -> TODO("$key got bool")
                    }
                    is JsonDouble -> when (key) {
                        else -> TODO("$key got double")
                    }
                    is JsonLong -> when (key) {
                        else -> TODO("$key got long")
                    }
                    JsonNull -> when (key) {
                        else -> TODO("$key got null")
                    }
                }
            } else if (value is JsonObject) {
                for (p in value) {
                    var nextStage = stage
                    var keySuffix = ".${p.key}"
                    if (key == "" && nextStage == null) {
                        nextStage = try {
                            Stage.valueOf(p.key.asciiTitleCase())
                        } catch (_: IllegalArgumentException) {
                            null
                        }
                        if (nextStage != null) {
                            keySuffix = ""
                        }
                    }
                    val nextKey = "$key$keySuffix"
                    walk(nextStage, nextKey, p.value)
                }
            } else {
                TODO("$key $wantJson")
            }
        }
        walk("", wantJson)
```

That let me piece together enough rules to decompose existing *want*
JSON dumps into strings.

I realized that `.stripDoubleHashCommentLinesToPutCommentsInlineBelow`
commentary is important in *input* and *want* strings.
The below reports 35 uses.

```sh
egrep -n '[.]stripDoubleHash' $(cat /tmp/stage-test-files) | \
  grep -v import | wc -l
```

On inspection, all occurred inside the expected Temper pseudocode
string blocks.

The convention I established is, for `stage.temper` pseudocode files,
if the file is indented by two spaces ignoring `##` lines, then I'll
just strip out lines matching `/^##/` on reading and strip the
indented spaces off too.

I hacked together a JSON with triple-backquoted-string munger to
re-indent the `##` lines that appear inside them to get the right
output when generating files.

Future commits will switch to reading from the set of files under
the stage test root, use those to reconstruct the input and outputs,
and retire those input parameters to assertStageTest.

Signed-off-by: Mike Samuel <mikesamuel@gmail.com>
@tjpalmer

Copy link
Copy Markdown
Contributor

Seeing lots of 1-3 line files, I'm wondering if pick-and-choose for whether to break out a file might be nice.

Signed-off-by: Mike Samuel <mikesamuel@gmail.com>
After switching parsing to use the input files, any TestFileBundle
entry that starts with `work`, I cut out the input strings.

I ran the following Perl script over the stage test Kotlin files.

```perl
use strict;

my $f = $ARGV[0];

open(IN, "<$f") or die "$f: $!";

my $content = "";
my $inParam = 0;
my $deferred = "";
while (<IN>) {
    if (!$inParam && m/^\s*input\s*=\s*[\$]*"""$/) {
        #print "switching to in param: $.: $_";
        $inParam = 1;
        $deferred = $_;
    } elsif ($inParam) {
        $deferred .= $_;
        if (m/^\s*"""/) {
            #print "switching to out param: $.: $_";
            $inParam = 0;
            if (m/^\s*"""(?:[.].*)?,$/) {
                #print "dropping param";
                $deferred = "";
            } else {
                $content .= $deferred;
            }
        }
    } elsif (m/^\s*input\s*=\s*\$*"(?:[^"\\]|\\.)*",$/) {
        # Skip it
    } elsif (m/^\s*input\s*=\s*\$*""".*?""",$/) {
        # Skip it
    } else {
        if (m/^\s*input\s*=/) {
            print "$f:$.: Not matched: $_";
        }
        $content .= $_;
    }
}
$content .= $deferred;

close(IN) or die "$f: $!";

open(OUT, ">$f.changed") or die "$f.changed: $!";
print OUT $content;
close(OUT) or die "$f: $!";
```

I used that on each of the stage test files manually.

```sh
perl /tmp/excise-inputs.perl frontend/src/commonTest/kotlin/lang/temper/frontend/GenerateCodeStageTest.kt
diff -uw frontend/src/commonTest/kotlin/lang/temper/frontend/GenerateCodeStageTest.kt{.changed,} | less
mv frontend/src/commonTest/kotlin/lang/temper/frontend/GenerateCodeStageTest.kt{.changed,}
```

Some inputs I had to fix up by hand like the below from GenerateCodeStageTest:

```kt
        input = buildString {
            append("export let numbers: Map<String, Int> = new Map<String, Int>([")
            for (i in 0 until 1000) {
                append("  new Pair<String, Int>(\"$i\", $i),")
            }
            append("]);")
        },
```

Signed-off-by: Mike Samuel <mikesamuel@gmail.com>
After implementing support for building the wanted JSON from the data
files under expect, I modified the excise-inputs perl script to get
rid of existing want and stage inputs.

```perl
use strict;

my $f = $ARGV[0];

open(IN, "<$f") or die "$f: $!";

my $content = "";
my $inParam = 0;
my $deferred = "";
while (<IN>) {
    if (!$inParam && m/^\s*want\s*=\s*[\$]*"""$/) {
        #print "switching to in param: $.: $_";
        $inParam = 1;
        $deferred = $_;
    } elsif ($inParam) {
        $deferred .= $_;
        if (m/\s*\@Test/) {
            print "$f:$.: probable error $_";
        }
        if (m/^\s*"""/) {
            #print "switching to out param: $.: $_";
            $inParam = 0;
            if (m/^\s*"""(?:[.].*)?,$/) {
                #print "dropping param";
                $deferred = "";
            } else {
                $content .= $deferred;
            }
        }
    } elsif (m/^\s*want\s*=\s*\$*"(?:[^"\\]|\\.)*",$/) {
        # Skip it
    } elsif (m/^\s*want\s*=\s*\$*""".*?""",$/) {
        # Skip it
    } elsif (m/^\s*stage\s*=\s*\$*Stage[.]\w+,$/) {
        # Skip it
    } else {
        if (m/^\s*want\s*=/) {
            print "$f:$.: Not matched: $_";
        }
        $content .= $_;
    }
}
$content .= $deferred;

close(IN) or die "$f: $!";

open(OUT, ">$f.changed") or die "$f.changed: $!";
print OUT $content;
close(OUT) or die "$f: $!";
```

Signed-off-by: Mike Samuel <mikesamuel@gmail.com>
Signed-off-by: Mike Samuel <mikesamuel@gmail.com>
Signed-off-by: Mike Samuel <mikesamuel@gmail.com>
@mikesamuel

Copy link
Copy Markdown
Contributor Author

I tweaked a test file, changing an f to a g.

$ git diff

Δ frontend/src/commonTest/resources/lang/temper/frontend/stage-tests/syntax-macro/for-of-loop-var-available-in-body/work/test/test.temper
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

─────┐
• 1: │
─────┘
│  1 │let x = f();                                                  │  1 │let x = g();
│  2 │for (let x of x) {                                            │  2 │for (let x of x) {
│  3 │  x                                                           │  3 │  x
│  4 │}                                                             │  4 │}

I changed fun shouldRegenerateStageTest to return true and reran the tests and now there is a diff for an additional file

Δ frontend/src/commonTest/resources/lang/temper/frontend/stage-tests/syntax-macro/for-of-loop-var-available-in-body/expect/syntaxMacro.temper
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

─────┐
• 1: │
─────┘
│  1 │let x__0 = f();                                               │  1 │let x__0 = g();
│  2 │do_call_forEach(x__0, fn (x__1) {                             │  2 │do_call_forEach(x__0, fn (x__1) {
│  3 │    x__1                                                      │  3 │    x__1
│  4 │});                                                           │  4 │});

@mikesamuel

Copy link
Copy Markdown
Contributor Author

Adding a test that the set of defined test directories matched the set of used ones and found out that my export of directories did not include @Ignored tests which makes sense because I exported the directories by abusing side effects of test running.

…ctories for ignored tests that were not extracted during earlier steps

Signed-off-by: Mike Samuel <mikesamuel@gmail.com>
@mikesamuel
mikesamuel marked this pull request as ready for review July 24, 2026 17:31
import lang.temper.common.structure.Hints
import kotlin.math.min

class JsonNestedObjectBuilder {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I combine a bunch of test files from the expect directory into a JSON object so that we can still get one-diff-to-expose-all-problems style failure.

To that end, I keep a mapping from relative file paths to JSON property chains (and a bit of other info) and feed them into this.

The set of small file definitions are in the README added in this which is probably a good place to start any review.


private val crlfOrLfPattern = Regex("""\r\n?|\n""")

fun CharSequence.splitLinesPreservingTerminators(): List<String> {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Kotlin's split lines drops terminators so I just thought I'd write one that's good.

pseudoCodeDetail: PseudoCodeDetail,
) {
sink.key("code", Hints.s) {
sink.key("code", Hints.su) {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make it more flexible as to which of lispy/pseudocode form is required.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here's where the bulk of the changes happen.

* and comparing which allows using `git diff` to understand the consequences of a change to details of
* the frontend's intermediate representation.
*/
data class StageTestDir(val url: Url) {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

StageTestDir is just a wrapper around a relative file path. It's the primary and only required input for stage tests now.

}
}

internal fun provisionModuleForStageTest(

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tests that want to customize the builtin environment were doing a bunch of different hacks to provision the test module, but now they just call into here.

That cleanup was useful to extract test inputs out into files, but seems generally cleaner.

val converter: DataFileConverter,
)

private val testResourceFileRelationships: Map<FilePath, TestResourceFileRelationship> =

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This mimics the file structure explained in the README file farther down.

@Test
fun compileLogExecutionOrder() = assertModuleAtStage(
stageTestDir = StageTestDir("function-macro/compile-log-execution-order"),
stage = Stage.FunctionMacro,

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still specifies a stage so that it doesn't conclude Stage.Run just because there's an expected stdout.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A lot of added test files. I describe the process for extracting them so that's something you could look at instead of looking at each file.

The README file is worth a look though.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And here's the README I keep banging on about.

Signed-off-by: Mike Samuel <mikesamuel@gmail.com>
ignore(stageTestDir)
ignore(isEmpty)

return false

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can flip this to true and the test runner will regenerate expect/* files for tests that fail.

So shouldRegenerateStageTest becomes an important name to remember?

theCount__0 = type (TheCount__0);
do {
let accumulator#0;
## The tag is used to create an accumulator

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I skimmed the description but likely missed some info. How do ## comments get generated?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I skimmed the description but likely missed some info. How do ## comments get generated?

Went back to the description and found this now:

I hacked together a JSON with triple-backquoted-string munger to re-indent the ## lines that appear inside them to get the right output when generating files.

@mikesamuel
mikesamuel merged commit f70b62d into main Jul 24, 2026
2 checks passed
@mikesamuel
mikesamuel deleted the stage-tests-in-files branch July 24, 2026 20:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants