Skip to content

Commit 8966f8b

Browse files
authored
Merge pull request #959 from hmiddelk/option-d-gate-review-958
autotranslate: address review #958 (gate correctness + polish)
2 parents 283ba80 + 690e8bb commit 8966f8b

3 files changed

Lines changed: 34 additions & 11 deletions

File tree

autotranslate/Latex.scala

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -145,13 +145,28 @@ object Latex:
145145
* The mirror's code-env / inline-code translation uses this to LEAVE hand-clamped code alone: both branches
146146
* of a `\ifswedish<sv>\else<en>\fi` clamp are already final, so translating inside one would mutate the
147147
* Swedish branch (e.g. rename `väsnas`->`makeNoise` while its neighbours stay Swedish -> a mixed listing). */
148-
def ifswedishRanges(s: String): Seq[(Int, Int)] =
149-
val out = scala.collection.mutable.ArrayBuffer[(Int, Int)]()
148+
def ifswedishRanges(s: String): Seq[(start: Int, end: Int)] =
149+
val out = scala.collection.mutable.ArrayBuffer[(start: Int, end: Int)]()
150150
val tag = "\\ifswedish"; var i = 0
151151
while i >= 0 && i < s.length do
152152
val k = s.indexOf(tag, i)
153153
if k < 0 then i = -1
154-
else { val end = matchFi(s, k + tag.length); out += ((k, end)); i = end }
154+
else
155+
val e = matchFi(s, k + tag.length)
156+
// #958.3: reached EOF without a closing \fi -> unbalanced. A stray literal \ifswedish in a comment or
157+
// verbatim block opens a bogus range that swallows the rest of the file (envs after it silently go
158+
// untranslated & ungated). Conservative (skip, never mistranslate). Only warn when the swallowed region
159+
// actually holds a code env / inline code — otherwise it has no consequence (e.g. top-level scaffolding
160+
// files with \ifswedish/\ifPreSolution conditionals whose \fi lands across an \input boundary: matchFi
161+
// over-counts, but there's nothing to skip, so the warning would be pure noise).
162+
if e >= s.length && !(e >= 3 && s.substring(e - 3, e) == "\\fi") then
163+
val swallowed = s.substring(k, e)
164+
val hasCode = verbatimEnvs.exists(env => swallowed.contains("\\begin{" + env + "}")) ||
165+
swallowed.contains("\\code{") || swallowed.contains("\\jcode{") || swallowed.contains("\\lstinline{")
166+
if hasCode then
167+
System.err.println(s"[Latex.ifswedishRanges] WARNING: \\ifswedish at offset $k has no matching \\fi " +
168+
"(range runs to end of file) and swallows code env(s): they go untranslated and ungated.")
169+
out += ((start = k, end = e)); i = e
155170
out.toSeq
156171

157172
/** Mask the source. Returns (maskedText, spans, itemIdx) where itemIdx is the set of placeholder

autotranslate/Translate.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -668,7 +668,7 @@ object Translate:
668668
// hand-clamped Fyle example). Ranges computed on the ORIGINAL text; matches keep their original offsets
669669
// because replaceAllIn scans left-to-right and we test m.start against the pre-substitution positions.
670670
val protectedRanges = Latex.ifswedishRanges(tex)
671-
def clamped(pos: Int): Boolean = protectedRanges.exists((a, b) => pos >= a && pos < b)
671+
def clamped(pos: Int): Boolean = protectedRanges.exists(r => pos >= r.start && pos < r.end)
672672
val envAlt = codeEnvs.toSeq.map(java.util.regex.Pattern.quote).mkString("|")
673673
val re = ("(?s)(\\\\begin\\{(" + envAlt + ")\\})(.*?)(\\\\end\\{\\2\\})").r
674674
val withEnvs = re.replaceAllIn(tex, m =>
@@ -680,7 +680,7 @@ object Translate:
680680
else
681681
// the env pass changed lengths, so recompute clamp ranges against `withEnvs` for the inline pass.
682682
val inlineRanges = Latex.ifswedishRanges(withEnvs)
683-
def clampedInline(pos: Int): Boolean = inlineRanges.exists((a, b) => pos >= a && pos < b)
683+
def clampedInline(pos: Int): Boolean = inlineRanges.exists(r => pos >= r.start && pos < r.end)
684684
inlineCodeRe.replaceAllIn(withEnvs, m =>
685685
if clampedInline(m.start) then java.util.regex.Matcher.quoteReplacement(m.matched)
686686
else java.util.regex.Matcher.quoteReplacement(s"\\${m.group(1)}{" + CodeGlossary.renderCodeIds(m.group(2), extraId) + "}"))

autotranslate/scratch/verify-mirror-examples.scala

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,9 @@
2626
// compiled under the same regression rule (SV/EN classification is symmetric, so a mis-split only skips).
2727
// See those sections below.
2828
//
29-
// scala-cli run autotranslate/scratch/verify-mirror-examples.scala -- <introprog-root>
29+
// scala-cli run autotranslate/scratch/verify-mirror-examples.scala -- [introprog-root] [--list]
30+
// introprog-root repo root to scan (optional; defaults to ".")
31+
// --list also print the per-body/-transcript OK/skip breakdown for phases 1 and 2
3032
// (exit 0 = clean, exit 1 = at least one regression or an untranslated ratified code-string)
3133

3234
@main def verifyMirrorExamples(args: String*): Unit =
@@ -69,6 +71,9 @@
6971
val texFiles = Seq("compendium", "slides").map(root / _).filter(os.exists)
7072
.flatMap(d => os.walk(d)).filter(f => os.isFile(f) && f.ext == "tex").sortBy(_.toString)
7173
val leaks = collection.mutable.ArrayBuffer[String]()
74+
// NB (#958.6): the per-file setup (read tex, rel, extraId, opt-out, clampRanges) recurs in the two gates below.
75+
// Kept as deliberate duplication — this is a scratch tool and the three loops differ enough (leak-check needs
76+
// no clamp/extraId) that a shared higher-order helper would add more indirection than it removes.
7277
for f <- texFiles do
7378
val tex = os.read(f)
7479
def scan(body: String): Unit =
@@ -102,7 +107,7 @@
102107
val extraId = CodeGlossary.overridesFor(rel)
103108
if !CodeGlossary.isOptedOut(rel) then
104109
val clampRanges = Latex.ifswedishRanges(tex)
105-
def clamped(pos: Int): Boolean = clampRanges.exists((a, b) => pos >= a && pos < b)
110+
def clamped(pos: Int): Boolean = clampRanges.exists(r => pos >= r.start && pos < r.end)
106111
for m <- envRe.findAllMatchIn(tex) if !clamped(m.start) do
107112
val env = m.group(1)
108113
val body = if env == "lstlisting" then stripLstOpt(m.group(2)) else m.group(2)
@@ -112,8 +117,8 @@
112117
else if !phase1Envs(env) then nonCode += 1 // Trace/Output — not compilable Scala
113118
else if compiles(en, "Inline.scala") then { inChecked += 1; inOk += s"$rel:${lineOf(tex, m.start)} ($env)" }
114119
else if compiles(body, "Inline.scala") then
115-
inRegressions += s"$rel ($env)"
116-
println(s" INLINE REGRESSION: $rel ($env) — compiles in Swedish but NOT after rename")
120+
inRegressions += s"$rel:${lineOf(tex, m.start)} ($env)"
121+
println(s" INLINE REGRESSION: $rel:${lineOf(tex, m.start)} ($env) — compiles in Swedish but NOT after rename")
117122
else { inSkipped += 1; inSkip += s"$rel:${lineOf(tex, m.start)} ($env)" }
118123
println(s"\n=== inline .tex compile gate (phase 1): $inChecked ok, $inSkipped skipped (not standalone), " +
119124
s"$replDeferred REPL deferred to phase 2, $nonCode Trace/Output not gated, ${inRegressions.size} REGRESSIONS ===")
@@ -144,7 +149,10 @@
144149
case None =>
145150
val t = line.trim
146151
val isOutput = t.isEmpty || replOutRe.findFirstMatchIn(line).isDefined ||
147-
t.matches("(val|var)\\s+\\w+:.*=.*") // result-echo `val x: T = …`
152+
t.matches("(?U)(val|var)\\s+\\w+:.*=.*") // result-echo `val x: T = …`; (?U) so \w
153+
// matches åäö — else `var räknaLäte:` (SV)
154+
// and `var callCount:` (EN) classify
155+
// differently, breaking SV/EN symmetry (#958)
148156
if inInput && !isOutput then prog += line // continuation of a multi-line input
149157
else inInput = false
150158
prog.mkString("\n")
@@ -160,7 +168,7 @@
160168
val extraId = CodeGlossary.overridesFor(rel)
161169
if !CodeGlossary.isOptedOut(rel) then
162170
val clampRanges = Latex.ifswedishRanges(tex)
163-
def clamped(pos: Int): Boolean = clampRanges.exists((a, b) => pos >= a && pos < b)
171+
def clamped(pos: Int): Boolean = clampRanges.exists(r => pos >= r.start && pos < r.end)
164172
for m <- envRe.findAllMatchIn(tex) if replEnvs(m.group(1)) && !clamped(m.start) do
165173
val body = stripLstOpt(m.group(2)) // strip a leading [numbers=none] optional arg
166174
val en = CodeGlossary.renderCodeIds(body, extraId)

0 commit comments

Comments
 (0)