From c85c65ed49af940735cbc1722b7d7ee3204672ee Mon Sep 17 00:00:00 2001 From: Mikhail Kornilovich Date: Wed, 15 Apr 2026 21:24:11 +0300 Subject: [PATCH] refactor: modernize stdlib usage and move matchers into dsl - Move Matcher types and matching logic from runner/matcher.go into the dsl package as methods on the Matcher types. Runner now calls t.Stdout.Match(label, actual) instead of type-switching via a package-level applyMatcher helper. - Replace custom contains/containsString helpers with slices.Contains in dsl/build.go and runner/compiler.go. - Use maps.Copy instead of manual map copy in BuildConfig.MergeFrom. - Adopt strings.SplitSeq, strings.CutPrefix and the `for range N` loop form in runner/limiter_linux.go. - Ignore example/imdb build artifact. --- .gitignore | 5 +- dsl/ast.go | 43 +--------- dsl/build.go | 22 ++--- dsl/lexer.go | 2 - {runner => dsl}/matcher.go | 164 ++++++++++++++++++++++--------------- runner/compiler.go | 12 +-- runner/limiter_linux.go | 10 +-- runner/runner.go | 4 +- 8 files changed, 123 insertions(+), 139 deletions(-) rename {runner => dsl}/matcher.go (53%) diff --git a/.gitignore b/.gitignore index 36cd02e..5aa09e5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,5 @@ example/c-sum/solution -example/solution/solution \ No newline at end of file +example/solution/solution +.DS_Store +*/.DS_Store +.claude diff --git a/dsl/ast.go b/dsl/ast.go index 0daf82f..9b2c248 100644 --- a/dsl/ast.go +++ b/dsl/ast.go @@ -1,6 +1,8 @@ package dsl -import "time" +import ( + "time" +) type File struct { Build string @@ -74,42 +76,3 @@ type Test struct { Stderr Matcher OutFiles map[string]string } - -type Matcher interface { - matcherNode() -} - -type ExactMatcher struct { - Value string -} - -func (ExactMatcher) matcherNode() {} - -type ContainsMatcher struct { - Substr string -} - -func (ContainsMatcher) matcherNode() {} - -type RegexMatcher struct { - Pattern string -} - -func (RegexMatcher) matcherNode() {} - -type NumericEpsMatcher struct { - Epsilon float64 - Value string -} - -func (NumericEpsMatcher) matcherNode() {} - -type AnyOrderMatcher struct { - Lines []string -} - -func (AnyOrderMatcher) matcherNode() {} - -type NoMatcher struct{} - -func (NoMatcher) matcherNode() {} diff --git a/dsl/build.go b/dsl/build.go index c0c1453..cc642c4 100644 --- a/dsl/build.go +++ b/dsl/build.go @@ -1,5 +1,10 @@ package dsl +import ( + "maps" + "slices" +) + type BuildProfile int const ( @@ -105,9 +110,7 @@ func (dst *BuildConfig) MergeFrom(src *BuildConfig) { if dst.Defines == nil { dst.Defines = map[string]string{} } - for k, v := range src.Defines { - dst.Defines[k] = v - } + maps.Copy(dst.Defines, src.Defines) } } @@ -132,24 +135,15 @@ func (b *BuildConfig) Resolve(defaults *BuildConfig, os string) BuildConfig { } func (b *BuildConfig) AppliesTo(os, compiler string) bool { - if len(b.Platforms) > 0 && !contains(b.Platforms, os) { + if len(b.Platforms) > 0 && !slices.Contains(b.Platforms, os) { return false } - if len(b.Compilers) > 0 && !contains(b.Compilers, compiler) { + if len(b.Compilers) > 0 && !slices.Contains(b.Compilers, compiler) { return false } return true } -func contains(xs []string, x string) bool { - for _, v := range xs { - if v == x { - return true - } - } - return false -} - type ToolchainSpec struct { Name string Platforms []string diff --git a/dsl/lexer.go b/dsl/lexer.go index 8d48673..800b95a 100644 --- a/dsl/lexer.go +++ b/dsl/lexer.go @@ -371,8 +371,6 @@ func (l *Lexer) readNumberOrDuration(line, col int) (Token, error) { return Token{TOKEN_INT, buf.String(), line, col}, nil } -// tryReadSizeSuffix reads memory size suffixes: B, K, KB, KiB, M, MB, MiB, G, GB, GiB. -// Units are case-sensitive uppercase to avoid collision with duration "m" (minutes). func (l *Lexer) tryReadSizeSuffix() string { ch, ok := l.peek() if !ok { diff --git a/runner/matcher.go b/dsl/matcher.go similarity index 53% rename from runner/matcher.go rename to dsl/matcher.go index 8b25336..11d4862 100644 --- a/runner/matcher.go +++ b/dsl/matcher.go @@ -1,4 +1,4 @@ -package runner +package dsl import ( "fmt" @@ -7,57 +7,90 @@ import ( "sort" "strconv" "strings" - - "github.com/Mond1c/judge/dsl" ) -func applyMatcher(label string, m dsl.Matcher, actual string) []string { - switch m := m.(type) { - case dsl.NoMatcher: - return nil - case dsl.ExactMatcher: - if actual != m.Value { - return []string{fmt.Sprintf( - "%s mismatch:\n expected: %q\n actual: %q", - label, m.Value, actual, - )} - } - return nil - case dsl.ContainsMatcher: - if !strings.Contains(actual, m.Substr) { - return []string{fmt.Sprintf( - "%s: expected to contain %q, got %q", - label, m.Substr, actual, - )} - } - return nil - case dsl.RegexMatcher: - re, err := regexp.Compile(m.Pattern) - if err != nil { - return []string{fmt.Sprintf("%s: invalid regex %q: %v", label, m.Pattern, err)} - } - if !re.MatchString(actual) { - return []string{fmt.Sprintf( - "%s: %q does not match regex %q", - label, actual, m.Pattern, - )} - } - return nil +// TODO: maybe move to ast.go +type Matcher interface { + matcherNode() - case dsl.NumericEpsMatcher: - errs := matchNumericEps(label, m, actual) - return errs - - case dsl.AnyOrderMatcher: - return matchAnyOrder(label, m, actual) - - default: - return []string{fmt.Sprintf("unknown matcher type %T", m)} - - } + Match(label, actual string) []string } -func matchNumericEps(label string, m dsl.NumericEpsMatcher, actual string) []string { +type ExactMatcher struct { + Value string +} + +func (ExactMatcher) matcherNode() {} + +// TODO: think about pointer receivers +func (m ExactMatcher) Match(label, actual string) []string { + if actual != m.Value { + return []string{fmt.Sprintf( + "%s mismatch:\n expected: %q\n actual: %q", + label, m.Value, actual, + )} + } + return nil +} + +type ContainsMatcher struct { + Substr string +} + +func (ContainsMatcher) matcherNode() {} + +func (m ContainsMatcher) Match(label, actual string) []string { + if !strings.Contains(actual, m.Substr) { + return []string{fmt.Sprintf( + "%s: expected to contain %q, got %q", + label, m.Substr, actual, + )} + } + return nil +} + +type RegexMatcher struct { + Pattern string +} + +func (RegexMatcher) matcherNode() {} + +func (m RegexMatcher) Match(label, actual string) []string { + re, err := regexp.Compile(m.Pattern) + if err != nil { + return []string{fmt.Sprintf("%s: invalid regex %q: %v", label, m.Pattern, err)} + } + + if !re.MatchString(actual) { + return []string{fmt.Sprintf( + "%s: %q does not match regex %q", + label, actual, m.Pattern, + )} + } + return nil +} + +type NumericEpsMatcher struct { + Epsilon float64 + Value string +} + +func (NumericEpsMatcher) matcherNode() {} + +func parseNumbers(s string) ([]float64, error) { + fields := strings.Fields(s) + nums := make([]float64, 0, len(fields)) + for _, f := range fields { + n, err := strconv.ParseFloat(f, 64) + if err != nil { + return nil, fmt.Errorf("not a number: %q", f) + } + nums = append(nums, n) + } + return nums, nil +} + +func (m NumericEpsMatcher) Match(label, actual string) []string { expectedNums, err := parseNumbers(m.Value) if err != nil { return []string{fmt.Sprintf("%s: cannot parse expected numbers %q: %v", label, m.Value, err)} @@ -85,20 +118,21 @@ func matchNumericEps(label string, m dsl.NumericEpsMatcher, actual string) []str return errs } -func parseNumbers(s string) ([]float64, error) { - fields := strings.Fields(s) - nums := make([]float64, 0, len(fields)) - for _, f := range fields { - n, err := strconv.ParseFloat(f, 64) - if err != nil { - return nil, fmt.Errorf("not a number: %q", f) - } - nums = append(nums, n) - } - return nums, nil +type AnyOrderMatcher struct { + Lines []string } -func matchAnyOrder(label string, m dsl.AnyOrderMatcher, actual string) []string { +func (AnyOrderMatcher) matcherNode() {} + +func splitLines(s string) []string { + s = strings.TrimRight(s, "\n") + if s == "" { + return []string{} + } + return strings.Split(s, "\n") +} + +func (m AnyOrderMatcher) Match(label, actual string) []string { actualLines := splitLines(actual) expectedLines := make([]string, len(m.Lines)) copy(expectedLines, m.Lines) @@ -125,10 +159,10 @@ func matchAnyOrder(label string, m dsl.AnyOrderMatcher, actual string) []string return errs } -func splitLines(s string) []string { - s = strings.TrimRight(s, "\n") - if s == "" { - return []string{} - } - return strings.Split(s, "\n") +type NoMatcher struct{} + +func (NoMatcher) matcherNode() {} + +func (NoMatcher) Match(label, actual string) []string { + return nil } diff --git a/runner/compiler.go b/runner/compiler.go index e4b2b29..bdc3421 100644 --- a/runner/compiler.go +++ b/runner/compiler.go @@ -3,6 +3,7 @@ package runner import ( "fmt" "runtime" + "slices" "sort" "strings" @@ -161,7 +162,7 @@ func compileMSVC(cfg dsl.BuildConfig, tc Toolchain, outputPath string) []string argv = append(argv, "/W4", "/permissive-") } - if containsString(cfg.Sanitize, "address") && cfg.Profile != dsl.ProfileSanitized { + if slices.Contains(cfg.Sanitize, "address") && cfg.Profile != dsl.ProfileSanitized { argv = append(argv, "/fsanitize=address") } @@ -195,12 +196,3 @@ func sortedKeys(m map[string]string) []string { sort.Strings(keys) return keys } - -func containsString(xs []string, x string) bool { - for _, v := range xs { - if v == x { - return true - } - } - return false -} diff --git a/runner/limiter_linux.go b/runner/limiter_linux.go index 6b3e9f2..a882547 100644 --- a/runner/limiter_linux.go +++ b/runner/limiter_linux.go @@ -102,9 +102,9 @@ func createScopeCgroup() (string, error) { return "", fmt.Errorf("read /proc/self/cgroup: %w", err) } var rel string - for _, line := range strings.Split(strings.TrimSpace(string(data)), "\n") { - if strings.HasPrefix(line, "0::") { - rel = strings.TrimPrefix(line, "0::") + for line := range strings.SplitSeq(strings.TrimSpace(string(data)), "\n") { + if after, ok := strings.CutPrefix(line, "0::"); ok { + rel = after break } } @@ -200,7 +200,7 @@ func (l *linuxLimiter) collect() limitStats { } } if data, err := os.ReadFile(filepath.Join(l.cgPath, "memory.events")); err == nil { - for _, line := range strings.Split(string(data), "\n") { + for line := range strings.SplitSeq(string(data), "\n") { fields := strings.Fields(line) if len(fields) != 2 { continue @@ -217,7 +217,7 @@ func (l *linuxLimiter) cleanup() { if l.cgPath == "" { return } - for i := 0; i < 10; i++ { + for range 10 { err := os.Remove(l.cgPath) if err == nil || os.IsNotExist(err) { l.cgPath = "" diff --git a/runner/runner.go b/runner/runner.go index 6d33a45..540a559 100644 --- a/runner/runner.go +++ b/runner/runner.go @@ -558,10 +558,10 @@ func (r *Runner) runTest(t *dsl.Test) *TestResult { tr.addFailure("exit code: expected %d, got %d", *t.ExitCode, actualCode) } - for _, f := range applyMatcher("stdout", t.Stdout, tr.ActualStdout) { + for _, f := range t.Stdout.Match("stdout", tr.ActualStdout) { tr.addFailure("%s", f) } - for _, f := range applyMatcher("stderr", t.Stderr, tr.ActualStderr) { + for _, f := range t.Stderr.Match("stderr", tr.ActualStderr) { tr.addFailure("%s", f) }