From 81df5c54b2579de39f920b3146ec4931100f33d8 Mon Sep 17 00:00:00 2001 From: OpenAI Codex Date: Mon, 10 Aug 2026 19:20:40 +0800 Subject: [PATCH 01/32] Add configurable compiler debug directives --- DBG_DIRECTIVE_PLAN.md | 41 ++++++ .../src/main/scala/hkmc2/CompilerCtx.scala | 49 +++++-- .../shared/src/main/scala/hkmc2/Config.scala | 87 ++++++++++++ .../main/scala/hkmc2/DebugOutputHandler.scala | 22 +++ .../src/main/scala/hkmc2/MLsCompiler.scala | 39 +++--- .../hkmc2/codegen/CompilationPipeline.scala | 36 +++++ .../scala/hkmc2/codegen/DeadParamElim.scala | 3 +- .../scala/hkmc2/codegen/EtaExpansion.scala | 2 +- .../main/scala/hkmc2/codegen/Lowering.scala | 47 ++++--- .../scala/hkmc2/semantics/Elaborator.scala | 132 ++++++++++-------- .../main/scala/hkmc2/semantics/Resolver.scala | 10 ++ .../src/main/scala/hkmc2/semantics/Term.scala | 11 +- .../src/main/scala/hkmc2/syntax/Lexer.scala | 6 +- .../main/scala/hkmc2/syntax/ParseRule.scala | 17 ++- .../src/main/scala/hkmc2/syntax/Parser.scala | 5 +- .../src/main/scala/utils/TraceLogger.scala | 31 +++- .../mlscript/codegen/BadDebugDirective.mls | 29 ++++ .../codegen/DebugDefinitionPhases.mls | 25 ++++ .../test/mlscript/codegen/DebugDirective.mls | 42 ++++++ .../codegen/DebugParsingDirective.mls | 57 ++++++++ .../test/scala/hkmc2/JSBackendDiffMaker.scala | 17 ++- .../src/test/scala/hkmc2/MLsDiffMaker.scala | 70 +++++++--- 22 files changed, 636 insertions(+), 142 deletions(-) create mode 100644 DBG_DIRECTIVE_PLAN.md create mode 100644 hkmc2/shared/src/main/scala/hkmc2/DebugOutputHandler.scala create mode 100644 hkmc2/shared/src/test/mlscript/codegen/BadDebugDirective.mls create mode 100644 hkmc2/shared/src/test/mlscript/codegen/DebugDefinitionPhases.mls create mode 100644 hkmc2/shared/src/test/mlscript/codegen/DebugDirective.mls create mode 100644 hkmc2/shared/src/test/mlscript/codegen/DebugParsingDirective.mls diff --git a/DBG_DIRECTIVE_PLAN.md b/DBG_DIRECTIVE_PLAN.md new file mode 100644 index 0000000000..12c05fe846 --- /dev/null +++ b/DBG_DIRECTIVE_PLAN.md @@ -0,0 +1,41 @@ +# `#dbg` directive progress + +## Requirements + +- Add a `#dbg` directive whose settings are stored in `Config`, analogously to `#lang`. +- Support the compiler phases represented by the existing diff-test debug commands: + - parsing (`:dp`), + - elaboration (`:de`), + - resolution (`:dr`), + - lowering (`:dl`), and + - optimizations (`:dopt`). +- Accept both shorthand flags and named booleans, including: + - `#dbg('parsing)`; + - `#dbg(elaboration: true)`. +- Support a selectable debug destination: + - `StdIO`, which is the default and may be omitted; + - `File("out.mls")`, resolved relative to the active configuration's base directory. +- Support the annotation form on individual definitions, for example + `@dbg('elaboration) fun foo() = ...`, without enabling the same phase for unrelated definitions. +- Keep the feature available in diff-test files as well as through `MLsCompiler`. +- Diagnose unknown flags, malformed boolean values, malformed destinations, and unsupported arguments. + +## Implementation plan + +1. Add immutable debug settings and output-destination types to `Config`, with all phase flags disabled and `StdIO` selected by default. +2. Extend `ConfigParser` with one parser shared by `#dbg(...)` and `@dbg(...)`; support quoted phase names, named phase booleans, and `out`. +3. Elaborate `#dbg` into the existing persistent `SetConfig` representation and `@dbg` into a per-definition configuration annotation. +4. Add destination-aware, dynamically scoped tracing so a definition annotation can temporarily enable only its requested phase and destination. +5. Discover module/block-level parsing debug settings after an initial parse and, when requested, repeat parsing with lexer/parser tracing sent to the selected destination. +6. Drive elaboration, resolution, lowering, and optimization trace loggers from the effective `Config`; retain the legacy diff-test flags by combining them with the new settings. +7. Add positive and negative diff tests covering shorthand/named syntax, persistence, destination selection, and definition-local elaboration output. +8. Run `ctest`, focused diff tests while iterating, and finally `hkmc2AllTests/test`; review and commit all intentional golden changes. + +## Progress + +- [x] Requirements summarized and implementation plan recorded. +- [x] Configuration model and parser implemented. +- [x] Compiler and diff-test tracing integrated. +- [x] Definition-local scoping implemented. +- [x] Regression tests and golden outputs added. +- [x] Full prescribed test suite passes. diff --git a/hkmc2/shared/src/main/scala/hkmc2/CompilerCtx.scala b/hkmc2/shared/src/main/scala/hkmc2/CompilerCtx.scala index ee1281e627..e94d32eb91 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/CompilerCtx.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/CompilerCtx.scala @@ -54,13 +54,21 @@ class CompilerCtx( (file: io.Path, prelude: Ctx) (using TL, Raise) : Artifact = + getElaboratedBlock(file, prelude, forceDebugParsing = false) + + def getElaboratedBlock + (file: io.Path, prelude: Ctx, forceDebugParsing: Bool) + (using TL, Raise) + : Artifact = // println(s"Cache has: ${cache.elabCache.contains(file)} ${cache.elabCache.keys}") val lastMod = fs.getLastChangedTimestamp(file) def mk = - val state = new Elaborator.State + var stateDebug = false + val state = new Elaborator.State: + override protected def doDbg: Bool = stateDebug given Elaborator.State = state // * Later, we can draw this from a global root configuration, @@ -93,22 +101,30 @@ class CompilerCtx( includeZero = true, )) ) - val etl = new TraceLogger{override def doTrace: Bool = false} - val ltl = new TraceLogger{override def doTrace: Bool = false} - val dtl = new TraceLogger{override def doTrace: Bool = false} - // val ltl = new TraceLogger{override def doTrace: Bool = true} - val rtl = new TraceLogger{override def doTrace: Bool = false} - - + val outputHandler = DebugOutputHandler(fs, rootConfig.baseDir, println) val mainParse = given CompilerCtx = this - ParserSetup(file, dbgParsing = false) + ParserSetup(file, forceDebugParsing, outputHandler) + val phaseConfig = mainParse.effectiveConfig + stateDebug = phaseConfig.debug.parsing || phaseConfig.debug.elaboration || + phaseConfig.debug.resolution || phaseConfig.debug.lowering || phaseConfig.debug.optimizations + def traceLogger(enabled: Bool) = new TraceLogger: + override def doTrace: Bool = enabled + override protected def defaultDebugOutput: Config.DebugOutput = phaseConfig.debug.out + override protected[hkmc2] def emitDbg(str: Str, out: Config.DebugOutput): Unit = + outputHandler.emit(out, str) + val etl = traceLogger(phaseConfig.debug.elaboration) + val ltl = traceLogger(phaseConfig.debug.lowering) + val dtl = traceLogger(phaseConfig.debug.optimizations) + val rtl = traceLogger(phaseConfig.debug.resolution) + + // given Elaborator.Ctx = prelude.copy(mode = Mode.Light).nestLocal("prelude") val artifactCtx = prelude given Elaborator.Ctx = artifactCtx val elab = given CompilerCtx = derive(mainParse.origin.fileName) - Elaborator(tl, file.up, prelude) + Elaborator(etl, file.up, prelude) // val elab = Elaborator(etl, wd, newCtx) val parsed = mainParse.resultBlk @@ -126,7 +142,7 @@ class CompilerCtx( if file.toString === paths.runtimeSourceFile.toString then state.initRuntimeSymbolsFromBlock(blk0) else - state.initRuntimeSymbolsFromFile(paths.runtimeSourceFile, prelude)(using tl, summon[Raise], this) + state.initRuntimeSymbolsFromFile(paths.runtimeSourceFile, prelude)(using etl, summon[Raise], this) val artifactConfig = Config.extractConfigFromStats(blk0) state.compilationUnitConfig = S(artifactConfig) @@ -214,8 +230,15 @@ class CompilerCtx( given Elaborator.State = state given Config = rootConfig given CompilerCtx = this - val parse = ParserSetup(file, dbgParsing) - val elab = Elaborator(tl, file.up, Ctx.empty) + val outputHandler = DebugOutputHandler(fs, rootConfig.baseDir, println) + val parse = ParserSetup(file, dbgParsing, outputHandler) + val phaseConfig = parse.effectiveConfig + val etl = new TraceLogger: + override def doTrace: Bool = phaseConfig.debug.elaboration + override protected def defaultDebugOutput: Config.DebugOutput = phaseConfig.debug.out + override protected[hkmc2] def emitDbg(str: Str, out: Config.DebugOutput): Unit = + outputHandler.emit(out, str) + val elab = Elaborator(etl, file.up, Ctx.empty) val initCtx = State.init.nestLocal("prelude") val (blk, ctx) = elab.importFrom(parse.resultBlk)(using initCtx) PreludeArtifact(parse.resultBlk, blk, ctx, state, rootConfig, lastMod) diff --git a/hkmc2/shared/src/main/scala/hkmc2/Config.scala b/hkmc2/shared/src/main/scala/hkmc2/Config.scala index 35f73c1294..7267b3f9c9 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/Config.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/Config.scala @@ -33,6 +33,7 @@ case class Config( commentGeneratedCode: Bool, noFreeze: Bool, noModuleCheck: Bool, + debug: Debug, optimizer: Optimizer, ): @@ -85,6 +86,7 @@ object Config: commentGeneratedCode = false, noFreeze = false, noModuleCheck = false, + debug = Debug.default, optimizer = Optimizer.FastOpt, ) object default: @@ -127,6 +129,28 @@ object Config: // TODO case class TypeChecking() + + enum DebugOutput: + case StdIO + case File(path: Str) + + case class Debug( + parsing: Bool, + elaboration: Bool, + resolution: Bool, + lowering: Bool, + optimizations: Bool, + out: DebugOutput, + ) + object Debug: + val default = Debug( + parsing = false, + elaboration = false, + resolution = false, + lowering = false, + optimizations = false, + out = DebugOutput.StdIO, + ) case class SanityChecks(light: Bool, checkUnreachable: Bool) @@ -480,6 +504,69 @@ object ConfigParser: cfg => cfg.copy(language = modify(cfg.language)) case N => identity + private def parseDebugOutput(tree: Tree)(using Raise): Opt[Config.DebugOutput] = tree match + case Ident("StdIO") | Call("StdIO", Nil) => + S(Config.DebugOutput.StdIO) + case Call("File", StrLit(path) :: Nil) => + S(Config.DebugOutput.File(path)) + case _ => + expect("StdIO or File(\"path\")")(tree) + N + + private def updateDebugFlag(name: Str, value: Bool, tree: Tree)(using Raise): Config.Debug => Config.Debug = + name match + case "parsing" => _.copy(parsing = value) + case "elaboration" => _.copy(elaboration = value) + case "resolution" => _.copy(resolution = value) + case "lowering" => _.copy(lowering = value) + case "optimizations" => _.copy(optimizations = value) + case _ => + raise(ErrorReport( + msg"Unknown debugging aspect '${name}'" -> tree.toLoc :: Nil, + source = Diagnostic.Source.Compilation)) + identity + + private def parseDebugArg(arg: Tree)(using Raise): Config.Debug => Config.Debug = arg match + case Quoted(Ident(name)) => + updateDebugFlag(name, true, arg) + case Ident(name) if name.startsWith("'") => + updateDebugFlag(name.drop(1), true, arg) + case NamedArg("out", value) => + parseDebugOutput(value) match + case S(out) => _.copy(out = out) + case N => identity + case NamedArg(name, value) => + parseBool(value) match + case S(enabled) => updateDebugFlag(name, enabled, arg) + case N => identity + case _ => + unsupported("debugging", arg) + identity + + /** Parse `#dbg(...)` and `@dbg(...)` arguments into a cumulative debug configuration. */ + def parseDebugDirective(args: Ls[Tree])(using Raise): Config => Config = + val modifyDebug = args.foldLeft(identity[Config.Debug]): (acc, arg) => + val modify = parseDebugArg(arg) + debug => modify(acc(debug)) + new (Config => Config): + def apply(cfg: Config): Config = cfg.copy(debug = modifyDebug(cfg.debug)) + override def toString: Str = "..." + + /** Apply top-level debug directives before phase execution begins. + * + * Parsing itself uses this after a quiet discovery parse, then repeats the parse when requested. + * Other phases use it directly before elaborating the parsed block. */ + def extractDebugFromTrees(trees: Ls[Tree])(using Raise, Config): Config = + trees.foldLeft(config): + case (cfg, Directive(Ident("dbg"), Tup(args))) => parseDebugDirective(args)(cfg) + case (cfg, App(Directive(Ident("dbg"), _), Tup(args))) => parseDebugDirective(args)(cfg) + case (cfg, _) => cfg + + /** Discovery variant used before normal elaboration reports directive diagnostics. */ + def discoverDebugFromTrees(trees: Ls[Tree])(using Config): Config = + given Raise = _ => () + extractDebugFromTrees(trees) + /** Parse the `None`/`Some(...)` syntax for optional config fields. * Also accepts unwrapped values as a convenience (treated as `Some(value)`). */ private def parseOpt[A](tree: Tree)(parseInner: Tree => Opt[A])(using Raise): Opt[Opt[A]] = tree match diff --git a/hkmc2/shared/src/main/scala/hkmc2/DebugOutputHandler.scala b/hkmc2/shared/src/main/scala/hkmc2/DebugOutputHandler.scala new file mode 100644 index 0000000000..c6588e3a40 --- /dev/null +++ b/hkmc2/shared/src/main/scala/hkmc2/DebugOutputHandler.scala @@ -0,0 +1,22 @@ +package hkmc2 + +import scala.collection.mutable + +import hkmc2.utils.*, shorthands.* + + +/** Routes compiler debugging output without putting callbacks or mutable streams in [[Config]]. + * + * File output is accumulated per handler and rewritten through the compiler file-system + * abstraction. This gives every compilation session truncate-on-first-use semantics and also + * works for the Scala.js in-memory file system, which has no append operation. */ +final class DebugOutputHandler(fs: io.FileSystem, baseDir: io.Path, stdIO: Str => Unit): + private val fileContents = mutable.Map.empty[io.Path, StringBuilder] + + def emit(out: Config.DebugOutput, line: Str): Unit = out match + case Config.DebugOutput.StdIO => stdIO(line) + case Config.DebugOutput.File(path) => + val file = baseDir / io.RelPath(path) + val contents = fileContents.getOrElseUpdate(file, new StringBuilder) + contents.append(line).append('\n') + fs.write(file, contents.result()) diff --git a/hkmc2/shared/src/main/scala/hkmc2/MLsCompiler.scala b/hkmc2/shared/src/main/scala/hkmc2/MLsCompiler.scala index fdfe633222..fc644bde2f 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/MLsCompiler.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/MLsCompiler.scala @@ -12,25 +12,29 @@ import hkmc2.syntax.Keyword.`override` import semantics.Elaborator.{Ctx, State} -class ParserSetup(file: io.Path, dbgParsing: Bool)(using state: Elaborator.State, raise: Raise, cctx: CompilerCtx): +class ParserSetup(file: io.Path, forceDebugParsing: Bool, outputHandler: DebugOutputHandler) +(using state: Elaborator.State, raise: Raise, cctx: CompilerCtx, config: Config): val block = cctx.fs.read(file) val fph = new FastParseHelpers(block) val origin = Origin(file, 0, fph) - val lexer = new syntax.Lexer(origin, dbg = dbgParsing) - val tokens = lexer.bracketedTokens - - // if showParse.isSet || dbgParsing.isSet then - // output(syntax.Lexer.printTokens(tokens)) - - val rules = syntax.ParseRules() - val parser = new syntax.Parser(origin, tokens, rules, raise, dbg = dbgParsing): - def doPrintDbg(msg: => Str): Unit = - // if dbg then output(msg) - if dbg then println(msg) - - val result = parser.parseAll(parser.block(allowNewlines = true)) + private def parse(dbg: Bool, out: Config.DebugOutput): Ls[syntax.Tree] = + val lexer = new syntax.Lexer(origin, dbg = dbg): + override protected def doPrintDbg(msg: => Str): Unit = outputHandler.emit(out, msg) + val tokens = lexer.bracketedTokens + if dbg then outputHandler.emit(out, syntax.Lexer.printTokens(tokens)) + val rules = syntax.ParseRules() + val parser = new syntax.Parser(origin, tokens, rules, raise, dbg = dbg): + def doPrintDbg(msg: => Str): Unit = if this.dbg then outputHandler.emit(out, msg) + parser.parseAll(parser.block(allowNewlines = true)) + + private val discoveryResult = parse(dbg = false, Config.DebugOutput.StdIO) + val effectiveConfig = ConfigParser.discoverDebugFromTrees(discoveryResult) + val result = + if forceDebugParsing || effectiveConfig.debug.parsing then + parse(dbg = true, effectiveConfig.debug.out) + else discoveryResult val resultBlk = new syntax.Tree.Block(result) @@ -70,9 +74,8 @@ class MLsCompiler given Raise = mkRaise(file) given Elaborator.State = new Elaborator.State: - override def dbg: Bool = dbgElab + override protected def doDbg: Bool = dbgElab - // TODO adapt logic given SymbolPrinter = new SymbolPrinter( Scope.empty(Scope.Cfg.default.copy( escapeChars = false, @@ -88,8 +91,6 @@ class MLsCompiler val preludeArtifact = cctx.getPrelude(preludeFile, dbgParsing)(using etl, summon[Raise]) val preludeCtx = preludeArtifact.ctx - val mainParse = ParserSetup(file, dbgParsing) - preludeCtx.nestLocal("file:"+file.baseName).givenIn: given CompilerCtx = cctx.derive(file) /* @@ -130,7 +131,7 @@ class MLsCompiler val printer = (p: codegen.Program) => p.showAsTree CompilationPipeline().run(lowered, printer, exportedSymbol.toSet, dtl) */ - val artifact = cctx.getElaboratedBlock(file, preludeCtx)(using etl) + val artifact = cctx.getElaboratedBlock(file, preludeCtx, dbgParsing)(using etl) val optimized = artifact.ir val nme = file.baseName diff --git a/hkmc2/shared/src/main/scala/hkmc2/codegen/CompilationPipeline.scala b/hkmc2/shared/src/main/scala/hkmc2/codegen/CompilationPipeline.scala index 45a1c97498..8bc8948553 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/codegen/CompilationPipeline.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/codegen/CompilationPipeline.scala @@ -25,13 +25,48 @@ class CompilationPipeline(using Config, Raise, State, Ctx, SymbolPrinter): private inline def blockPass(inline pass: Block => Block)(prog: Program): Program = val blk = pass(prog.main) if blk is prog.main then prog else Program(prog.imports, blk) + + private def collectDefinitions(block: Block): Ls[Defn] = block match + case Define(defn, rest) => + defn :: defn.subBlocks.flatMap(collectDefinitions) ::: collectDefinitions(rest) + case Match(_, arms, default, rest) => + arms.flatMap(arm => collectDefinitions(arm._2)) ::: + default.toList.flatMap(collectDefinitions) ::: collectDefinitions(rest) + case Label(_, _, body, rest) => collectDefinitions(body) ::: collectDefinitions(rest) + case Begin(sub, rest) => collectDefinitions(sub) ::: collectDefinitions(rest) + case TryBlock(sub, finallyDo, rest) => + collectDefinitions(sub) ::: collectDefinitions(finallyDo) ::: collectDefinitions(rest) + case Assign(_, _, rest) => collectDefinitions(rest) + case AssignField(_, _, _, rest) => collectDefinitions(rest) + case AssignDynField(_, _, _, _, rest) => collectDefinitions(rest) + case Scoped(_, body) => collectDefinitions(body) + case _: BlockTail => Nil + + /** Emit a compact per-pass delta for definitions carrying `@dbg('optimizations)`. + * This remains definition-local even though most optimization passes operate on a whole program. */ + private def reportDefinitionOptimizationDebug(passName: Str, before: Program, after: Program, otl: TL): Unit = + val afterBySymbol = collectDefinitions(after.main).map(defn => defn.sym -> defn).toMap + collectDefinitions(before.main).foreach: oldDefn => + oldDefn.configOverride.foreach: localConfig => + if localConfig.debug.optimizations then + otl.scopedDebug(enabled = true, localConfig.debug.out): + afterBySymbol.get(oldDefn.sym) match + case N => otl.log(s"Optimization $passName removed ${oldDefn.sym.nme}") + case S(newDefn) if newDefn == oldDefn => + otl.log(s"Optimization $passName left ${oldDefn.sym.nme} unchanged") + case S(newDefn) => + val details = newDefn match + case product: Product => product.showAsTree + otl.log(s"Optimization $passName changed ${oldDefn.sym.nme}:\n${details}") def run(prog: Program, printer: Program => Str, symbolsToPreserve: Set[BoundSymbol], otl: TL)(using TL): Program = var result = prog + var reportDefinitionDebug = false inline def runPass(passName: Str)(inline transform: Program => Program) = val before = result result = transform(before) passHook(passName, before, result) + if reportDefinitionDebug then reportDefinitionOptimizationDebug(passName, before, result, otl) runPass("LambdaRewriter")(LambdaRewriter.desugar) runPass("Deforest"): prog => @@ -63,6 +98,7 @@ class CompilationPipeline(using Config, Raise, State, Ctx, SymbolPrinter): runPass("ClassParamFlattener")(ClassParamFlattener.apply) runPass("ReflectionInstrumenter")(ReflectionInstrumenter(using summon).apply) preOptimizeHook(result) + reportDefinitionDebug = true // * We run this pass here first, before inlining so that the @tailrec/@tailcall annotations // * can be properly checked. diff --git a/hkmc2/shared/src/main/scala/hkmc2/codegen/DeadParamElim.scala b/hkmc2/shared/src/main/scala/hkmc2/codegen/DeadParamElim.scala index 3f8d2158e0..19da739b34 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/codegen/DeadParamElim.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/codegen/DeadParamElim.scala @@ -105,7 +105,7 @@ class DeadParamElimSolver(val constraintSolver: FlowConstraintSolver): case None => eliminableCallSiteArgsById(consFun.concreteId) = eliminable.toSet case S(existing) => assert(existing.toList.sorted === eliminable) - if tl.doTrace then + if tl.isTracing then def showRefSite(resultId: ResultId): Str = resultId.getReferredFun match case Some(fun) => s"${fun.nme}@$resultId" @@ -412,4 +412,3 @@ object DeadParamElim: val rewrite = new Rewrite(deadParamElimSolver) rewrite() - diff --git a/hkmc2/shared/src/main/scala/hkmc2/codegen/EtaExpansion.scala b/hkmc2/shared/src/main/scala/hkmc2/codegen/EtaExpansion.scala index 7ecf86765b..14a9b0ccd9 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/codegen/EtaExpansion.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/codegen/EtaExpansion.scala @@ -126,7 +126,7 @@ class EtaExpansionSolver(val constraintSolver: FlowConstraintSolver): funShape.exists: case (id, shape) => checkIsEtaExpanded(id, shape) - if tl.doTrace then + if tl.isTracing then tl.log(">>> eta-expansion targets shapes >>>") for (id, shape) <- funShape diff --git a/hkmc2/shared/src/main/scala/hkmc2/codegen/Lowering.scala b/hkmc2/shared/src/main/scala/hkmc2/codegen/Lowering.scala index 93eb537513..47e195e2de 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/codegen/Lowering.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/codegen/Lowering.scala @@ -84,6 +84,13 @@ object Lowering: import Lowering.* class Lowering()(using Config, TL, Raise, State, Ctx, SymbolPrinter): + + private def definitionConfigOverride(annotations: Ls[Annot]): Opt[Config] = + val modifiers = annotations.collect: + case Annot.Config(modify) => modify + case Annot.Debug(modify) => modify + if modifiers.isEmpty then N + else S(modifiers.foldLeft(config)((cfg, modify) => modify(cfg))) extension (t: Term) def instantiated = t match @@ -258,17 +265,27 @@ class Lowering()(using Config, TL, Raise, State, Ctx, SymbolPrinter): td.k match case knd: syntax.Val => assert(td.params.isEmpty) - val cfgOverride = td.extraAnnotations.collectFirst: - case Annot.Config(modify) => modify(config) - subTerm_nonTail(bod)(r => - // Assign(td.sym, r, - // term(st.Blk(stats, res))(k))) - Define(ValDefn(td.tsym, td.sym, r)(cfgOverride, td.annotations), - blockImpl(stats, res)))(using LoweringCtx.nestFunc) + val cfgOverride = definitionConfigOverride(td.extraAnnotations) + cfgOverride match + case S(localConfig) => + State.scopedDebug(localConfig.debug.lowering): + tl.scopedDebug(localConfig.debug.lowering, localConfig.debug.out): + subTerm_nonTail(bod)(r => + tl.inOuterDebugScope: + Define(ValDefn(td.tsym, td.sym, r)(cfgOverride, td.annotations), + blockImpl(stats, res)))(using LoweringCtx.nestFunc) + case N => + subTerm_nonTail(bod)(r => + Define(ValDefn(td.tsym, td.sym, r)(cfgOverride, td.annotations), + blockImpl(stats, res)))(using LoweringCtx.nestFunc) case syntax.Fun => - val (paramLists, bodyBlock) = setupFunctionOrByNameDef(td.params, bod, S(td.sym.nme)) - val cfgOverride = td.extraAnnotations.collectFirst: - case Annot.Config(modify) => modify(config) + val cfgOverride = definitionConfigOverride(td.extraAnnotations) + val (paramLists, bodyBlock) = cfgOverride match + case S(localConfig) => + State.scopedDebug(localConfig.debug.lowering): + tl.scopedDebug(localConfig.debug.lowering, localConfig.debug.out): + setupFunctionOrByNameDef(td.params, bod, S(td.sym.nme)) + case N => setupFunctionOrByNameDef(td.params, bod, S(td.sym.nme)) Define(FunDefn(td.owner, td.sym, td.tsym, paramLists, bodyBlock)(cfgOverride, td.annotations), blockImpl(stats, res)) case syntax.LetBind | syntax.HandlerBind => fail: @@ -370,8 +387,7 @@ class Lowering()(using Config, TL, Raise, State, Ctx, SymbolPrinter): case _ => N defn.ext match case N => - val cfgOverride = defn.extraAnnotations.collectFirst: - case Annot.Config(modify) => modify(config) + val cfgOverride = definitionConfigOverride(defn.extraAnnotations) Define( ClsLikeDefn(defn.owner, defn.sym, defn.bsym, defn.ctorSym, defn.kind, defn.paramsOpt, defn.auxParams, N, mtds, @@ -385,8 +401,7 @@ class Lowering()(using Config, TL, Raise, State, Ctx, SymbolPrinter): blockImpl(stats, res)) case S(ext) => assert(k isnt syntax.Mod) // modules can't extend things and can't have super calls - val cfgOverride = defn.extraAnnotations.collectFirst: - case Annot.Config(modify) => modify(config) + val cfgOverride = definitionConfigOverride(defn.extraAnnotations) subTerm(ext.cls): clsp => val pctor = inScopedBlock(parentConstructor(clsp, ext.cls, ext.args)) Define( @@ -1274,8 +1289,7 @@ class Lowering()(using Config, TL, Raise, State, Ctx, SymbolPrinter): td.body.map: bod => val (paramLists, bodyBlock) = setupFunctionDef(td.params, bod, S(td.sym.nme)) reportAnnotations(td, td.extraAnnotations) - val cfgOverride = td.extraAnnotations.collectFirst: - case Annot.Config(modify) => modify(config) + val cfgOverride = definitionConfigOverride(td.extraAnnotations) FunDefn(td.owner, td.sym, td.tsym, paramLists, bodyBlock)(cfgOverride, td.annotations) val publicFlds = clsBody.publicFlds.collect: case f if !f.tsym.isPrivate => @@ -1430,6 +1444,7 @@ class Lowering()(using Config, TL, Raise, State, Ctx, SymbolPrinter): case Annot.Modifier(syntax.Keyword("staged")) => () case Annot.MayNotRaiseEffects => () case _: Annot.Config => () // Config annotations are handled during FunDefn creation + case _: Annot.Debug => () // Debug annotations are handled during definition lowering case annot => warn(annot) def reportAnnotations(receiver: Term, annotations: Ls[Annot]): Unit = diff --git a/hkmc2/shared/src/main/scala/hkmc2/semantics/Elaborator.scala b/hkmc2/shared/src/main/scala/hkmc2/semantics/Elaborator.scala index 9bcd26432c..5be7f89ba2 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/semantics/Elaborator.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/semantics/Elaborator.scala @@ -495,7 +495,12 @@ object Elaborator: "globalThis" -> globalThisSymbol, )) val superSymbol = builtinOpsMap("super") - def dbg: Bool = false + private var debugOverrides = Ls.empty[Bool] + protected def doDbg: Bool = false + final def dbg: Bool = debugOverrides.headOption.getOrElse(doDbg) + def scopedDebug[T](enabled: Bool)(thunk: => T): T = + debugOverrides ::= enabled + try thunk finally debugOverrides = debugOverrides.tail def dbgRefNum(num: Int): Str = if dbg then s"#$num" else "" def dbgUid(uid: Uid[Symbol]): Str = @@ -535,6 +540,15 @@ class Elaborator(val tl: TraceLogger, val wd: io.Path, val prelude: Ctx) extends Importer: import tl.* given TraceLogger = tl + + private def debugElaboration[T](annotations: Ls[Annot])(thunk: => T): T = + val modifiers = annotations.collect: + case Annot.Debug(modify) => modify + if modifiers.isEmpty then thunk + else + val localConfig = modifiers.foldLeft(config)((cfg, modify) => modify(cfg)) + state.scopedDebug(localConfig.debug.elaboration): + tl.scopedDebug(localConfig.debug.elaboration, localConfig.debug.out)(thunk) lazy val illegalMemberNameTail = msg"Member names must start with a letter or underscore, followed by letters, digits, or underscores." -> N @@ -571,6 +585,9 @@ extends Importer: case App(Ident("config"), Tup(args)) => val modify = ConfigParser.parseOverrides(args) S(Annot.Config(modify)) + case App(Ident("dbg"), Tup(args)) => + val modify = ConfigParser.parseDebugDirective(args) + S(Annot.Debug(modify)) case _ => term(tree) match case Term.Error() => N case trm => @@ -1837,7 +1854,6 @@ extends Importer: -> lhs.toLoc :: Nil)) // TODO BE go(sts, Nil, Term.Error().withLocOf(lhs) :: acc) case (td @ TermDef(k, nme, rhs)) :: sts => - log(s"Processing term definition $nme") td.symbName match case S(L(d)) => raise(d) case _ => () @@ -1861,59 +1877,61 @@ extends Importer: :: illegalMemberNameTail return go(sts, Nil, acc) val isMethod = owner.exists(_.isInstanceOf[ClassSymbol]) - val tdf = ctx.nest(OuterCtx.NonReturnContext).givenIn: newCtx ?=> - // * Add type parameters to context - val (tps, newCtx1) = td.typeParams match - case S(t) => - val (tps, ctx) = typeParams(t) - (S(tps), ctx) - case N => (N, ctx) - // * Add parameters to context - var newCtx = newCtx1 - val pss = td.paramLists.map: ps => - val (res, newCtx2) = funParams(ps)(using newCtx) - newCtx = newCtx2 - res - // * Elaborate signature - val st = td.annotatedResultType.orElse(newSignatureTrees.get(id.name)) // FIXME: may elaborate external sig twice!! - val s = st.map: - // unwrap possible module modifier - // e.g, `fun f: module M` - // ^^^^^^ - case TypeDef(Mod, st, N) => term(st)(using newCtx) - case st => term(st)(using newCtx) - val body: Opt[Term] = rhs match - case N => N - case _ if ctx.mode is Mode.Light => S(Term.Missing) - case S(rhs) => S: - val nonLocalRetHandler = TempSymbol(N, s"nonLocalRetHandler$$${id.name}") - newCtx.nest(OuterCtx.Function(nonLocalRetHandler)).givenIn: newCtx ?=> - val b = term(rhs)(using newCtx) - if nonLocalRetHandler.directRefs.isEmpty then b else - mkEffectHandleAbortive( - nonLocalRetHandler, - "‹non-local return effect›", - EffectHandlerMethodSpec("ret", S("value"), requireEffectMethodValue("ret", _)) :: Nil, - b, - ) - val r = FlowSymbol(s"‹result of ${sym}›") - - val mfn = st match - // st.isModified(Mod) indicates if the function marks - // its result as "module". e.g, `fun f: module M` - // ^^^^^^ - case S(st) if st.isModified(Mod) => - Modulefulness.ofSign(s)(true) - case _ => - Modulefulness.none - - val tsym = TermSymbol(k, owner, id) // TODO? - val tdf = TermDefinition(k, sym, tsym, pss, tps, s, body, - TermDefFlags.empty.copy(isMethod = isMethod), mfn, annotations, N).withLocOf(td) - tsym.defn = S(tdf) - sym.tsym = S(tsym) - - tdf + val tdf = debugElaboration(annotations): + log(s"Processing term definition $nme") + ctx.nest(OuterCtx.NonReturnContext).givenIn: newCtx ?=> + // * Add type parameters to context + val (tps, newCtx1) = td.typeParams match + case S(t) => + val (tps, ctx) = typeParams(t) + (S(tps), ctx) + case N => (N, ctx) + // * Add parameters to context + var newCtx = newCtx1 + val pss = td.paramLists.map: ps => + val (res, newCtx2) = funParams(ps)(using newCtx) + newCtx = newCtx2 + res + // * Elaborate signature + val st = td.annotatedResultType.orElse(newSignatureTrees.get(id.name)) // FIXME: may elaborate external sig twice!! + val s = st.map: + // unwrap possible module modifier + // e.g, `fun f: module M` + // ^^^^^^ + case TypeDef(Mod, st, N) => term(st)(using newCtx) + case st => term(st)(using newCtx) + val body: Opt[Term] = rhs match + case N => N + case _ if ctx.mode is Mode.Light => S(Term.Missing) + case S(rhs) => S: + val nonLocalRetHandler = TempSymbol(N, s"nonLocalRetHandler$$${id.name}") + newCtx.nest(OuterCtx.Function(nonLocalRetHandler)).givenIn: newCtx ?=> + val b = term(rhs)(using newCtx) + if nonLocalRetHandler.directRefs.isEmpty then b else + mkEffectHandleAbortive( + nonLocalRetHandler, + "‹non-local return effect›", + EffectHandlerMethodSpec("ret", S("value"), requireEffectMethodValue("ret", _)) :: Nil, + b, + ) + val r = FlowSymbol(s"‹result of ${sym}›") + + val mfn = st match + // st.isModified(Mod) indicates if the function marks + // its result as "module". e.g, `fun f: module M` + // ^^^^^^ + case S(st) if st.isModified(Mod) => + Modulefulness.ofSign(s)(true) + case _ => + Modulefulness.none + + val tsym = TermSymbol(k, owner, id) // TODO? + val tdf = TermDefinition(k, sym, tsym, pss, tps, s, body, + TermDefFlags.empty.copy(isMethod = isMethod), mfn, annotations, N).withLocOf(td) + tsym.defn = S(tdf) + sym.tsym = S(tsym) + + tdf go(sts, Nil, tdf :: acc) case L(d) => reportUnusedAnnotations @@ -2239,6 +2257,10 @@ extends Importer: reportUnusedAnnotations val modify = ConfigParser.parseLanguageDirective(args) go(sts, Nil, SetConfig(modify) :: acc) + case Directive(Ident("dbg"), Tup(args)) :: sts => + reportUnusedAnnotations + val modify = ConfigParser.parseDebugDirective(args) + go(sts, Nil, SetConfig(modify) :: acc) case Directive(Ident(name), _) :: sts => raise(ErrorReport( msg"Unknown directive '#${name}'" -> sts.headOption.flatMap(_.toLoc) :: Nil, diff --git a/hkmc2/shared/src/main/scala/hkmc2/semantics/Resolver.scala b/hkmc2/shared/src/main/scala/hkmc2/semantics/Resolver.scala index c0e34d2d69..5e7fd3064e 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/semantics/Resolver.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/semantics/Resolver.scala @@ -357,6 +357,16 @@ class Resolver(tl: TraceLogger) end traverse def traverseDefn(defn: Definition)(using ICtx): ICtx = + val modifiers = defn.annotations.collect: + case Annot.Debug(modify) => modify + if modifiers.isEmpty then traverseDefnImpl(defn) + else + val localConfig = modifiers.foldLeft(cfg)((config, modify) => modify(config)) + state.scopedDebug(localConfig.debug.resolution): + tl.scopedDebug(localConfig.debug.resolution, localConfig.debug.out): + traverseDefnImpl(defn) + + private def traverseDefnImpl(defn: Definition)(using ICtx): ICtx = trace(s"Resolving definition: $defn"): def traverseTermDef(tdf: TermDefinition) = val TermDefinition(_k, _sym, _tsym, diff --git a/hkmc2/shared/src/main/scala/hkmc2/semantics/Term.scala b/hkmc2/shared/src/main/scala/hkmc2/semantics/Term.scala index 7c8e599fca..449658be9c 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/semantics/Term.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/semantics/Term.scala @@ -31,6 +31,7 @@ enum Annot extends AutoLocated: // Whether the function is guaranteed to not raise effects. case MayNotRaiseEffects case Config(modify: hkmc2.Config => hkmc2.Config) + case Debug(modify: hkmc2.Config => hkmc2.Config) // Marks if a function or lambda is one-shot, i.e. called at most once. // Functions with multiple parameter lists are considered here as a chain of // function values. `whichParamList` is the zero-based index of the parameter @@ -50,13 +51,13 @@ enum Annot extends AutoLocated: def subTerms: Vector[Term] = this match case Trm(trm) => Vector.single(trm) case _: Modifier | Untyped | TailRec | TailCall | Inline | NoInline - | MayNotRaiseEffects | _: Config | _: Affine => Vector.empty + | MayNotRaiseEffects | _: Config | _: Debug | _: Affine => Vector.empty def children: Vector[Located] = this match case Trm(trm) => Vector.single(trm) // case Modifier(kw) => Vector.single(kw) // TODO: make `kw` a `Keywrd` case _: Modifier | Untyped | TailRec | TailCall | Inline | NoInline - | MayNotRaiseEffects | _: Config | _: Affine => Vector.empty + | MayNotRaiseEffects | _: Config | _: Debug | _: Affine => Vector.empty def show(using Scope, ShowCfg, Raise): Document = this match case Untyped => doc"@untyped" @@ -69,6 +70,7 @@ enum Annot extends AutoLocated: case MayNotRaiseEffects => doc"@mayNotRaiseEffects" case Trm(trm) => doc"@${trm.show}" case Config(_) => doc"@config(...)" + case Debug(_) => doc"@dbg(...)" def mkClone(using State): Annot = this match case Untyped => Untyped @@ -80,6 +82,7 @@ enum Annot extends AutoLocated: case NoInline => NoInline case MayNotRaiseEffects => MayNotRaiseEffects case c: Config => c + case d: Debug => d case a: Affine => a object Annot: @@ -969,7 +972,8 @@ final case class DefineVar(sym: LocalSymbol | TermSymbol, rhs: Term) extends Sta /** A global configuration change directive (`#config(...)`). * Records a function that modifies the current compiler configuration. */ -final case class SetConfig(modify: hkmc2.Config => hkmc2.Config) extends Statement +final case class SetConfig(modify: hkmc2.Config => hkmc2.Config) extends Statement: + override def toString: Str = "SetConfig(...)" enum Visibility: case Public, Private @@ -1474,4 +1478,3 @@ trait BlkImpl: (stats ::: (res match case Lit(Tree.UnitLit(false)) => Nil case res => res :: Nil)).map(_.show).mkDocument(doc", # ") - diff --git a/hkmc2/shared/src/main/scala/hkmc2/syntax/Lexer.scala b/hkmc2/shared/src/main/scala/hkmc2/syntax/Lexer.scala index 99807142bb..b723132598 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/syntax/Lexer.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/syntax/Lexer.scala @@ -11,6 +11,8 @@ import Lexer._ import Tree.{IntLit, DecLit, StrLit} class Lexer(origin: Origin, dbg: Bool)(using raise: Raise): + + protected def doPrintDbg(msg: => Str): Unit = println(msg) val bytes: Array[Char] = origin.fph.blockStr.toArray private val length = bytes.length @@ -362,8 +364,8 @@ class Lexer(origin: Origin, dbg: Bool)(using raise: Raise): val hasNewIndent = newIndBase.headOption.forall(_ < nextInd) && nextInd > 0 val newInd = if hasNewIndent then nextInd :: newIndBase else newIndBase if dbg then - println("dbg: " + bytes.drop(i).take(10).map(escapeChar).mkString+"...") - println((ind, nextInd, newIndBase, droppedNum, hasNewIndent, newInd)) + doPrintDbg("dbg: " + bytes.drop(i).take(10).map(escapeChar).mkString+"...") + doPrintDbg((ind, nextInd, newIndBase, droppedNum, hasNewIndent, newInd).toString) lex(k, newInd, if droppedNum > 0 then { if hasNewIndent then (INDENT, loc(j, k)) diff --git a/hkmc2/shared/src/main/scala/hkmc2/syntax/ParseRule.scala b/hkmc2/shared/src/main/scala/hkmc2/syntax/ParseRule.scala index cc38bcd6e0..3788f266a0 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/syntax/ParseRule.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/syntax/ParseRule.scala @@ -409,7 +409,21 @@ class ParseRules(using State): modified(`data`), modified(`public`), modified(`private`), - modified(`out`), + Kw(`out`)( + ParseRule("'out' keyword")( + Kw(`:`)( + ParseRule("named 'out' argument colon")( + Expr(ParseRule("named 'out' argument value")(end(()))): + case (value, ()) => value + ) + ) { case (colon, value) => S(colon) -> value }, + Expr(ParseRule("'out' modifier body")(end(()))): + case (body, ()) => N -> body + ) + ): + case (outKw, (S(colon), value)) => + InfixApp(Ident("out").withLocOf(outKw), colon, value).withLocOf(outKw) + case (outKw, (N, body)) => Tree.Modified(outKw, body), modified(`staged`), singleKw(`true`)(BoolLit(true)), singleKw(`false`)(BoolLit(false)), @@ -496,4 +510,3 @@ class ParseRules(using State): ) end ParseRules - diff --git a/hkmc2/shared/src/main/scala/hkmc2/syntax/Parser.scala b/hkmc2/shared/src/main/scala/hkmc2/syntax/Parser.scala index b67b6d52d4..7a01c5a189 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/syntax/Parser.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/syntax/Parser.scala @@ -451,7 +451,10 @@ abstract class Parser( // N case (tok @ (id: IDENT), loc) :: _ => Keyword.all.get(id.name) match - case S(Keyword.`:`) | N => + case S(Keyword.`:`) if rule.getKwAlt(Keyword.`:`, S(loc)).isEmpty => + // encountering `:` should lead to parsing an expr (likely a pun) + tryParseExp(prec, tok, loc, rule, allowNewlines = allowNewlines) + case N => // encountering `:` should lead to parsing an expr (likely a pun) tryParseExp(prec, tok, loc, rule, allowNewlines = allowNewlines) case S(kw) => diff --git a/hkmc2/shared/src/main/scala/utils/TraceLogger.scala b/hkmc2/shared/src/main/scala/utils/TraceLogger.scala index 225573c568..5f76abdc1e 100644 --- a/hkmc2/shared/src/main/scala/utils/TraceLogger.scala +++ b/hkmc2/shared/src/main/scala/utils/TraceLogger.scala @@ -10,6 +10,12 @@ def tl(using TL): TL = summon abstract class TraceLogger(using val debugPrinter: DebugPrinter): def doTrace: Bool = true + protected def defaultDebugOutput: Config.DebugOutput = Config.DebugOutput.StdIO + + private var debugOverrides: Ls[(Bool, Config.DebugOutput)] = Nil + + /** Whether tracing is enabled after applying the innermost definition-local override. */ + final def isTracing: Bool = debugOverrides.headOption.fold(doTrace)(_._1) protected val noPostTrace: Any => Str = _ => "" @@ -27,15 +33,33 @@ abstract class TraceLogger(using val debugPrinter: DebugPrinter): inline def exit() = indent -= 1 protected[hkmc2] def emitDbg(str: Str): Unit = scala.Predef.println(str) + protected[hkmc2] def emitDbg(str: Str, out: Config.DebugOutput): Unit = emitDbg(str) inline def log(msg: => Any): Unit = log(msg, noIndent = false) def logs(msgs: => Any*): Unit = - if doTrace then msgs.foreach(log(_)) + if isTracing then msgs.foreach(log(_)) def log(msg: => Any, noIndent: Bool = false): Unit = - if doTrace then emitDbg(if noIndent then msg.toString - else "| " * indent + msg.toString.indentNewLines("| " * indent + "> ")) + if isTracing then emitDbg( + if noIndent then msg.toString + else "| " * indent + msg.toString.indentNewLines("| " * indent + "> "), + debugOverrides.headOption.fold(defaultDebugOutput)(_._2), + ) + + /** Temporarily select both tracing enablement and its destination. + * Definition annotations use this to avoid leaking debug settings to sibling definitions. */ + def scopedDebug[T](enabled: Bool, out: Config.DebugOutput)(thunk: => T): T = + debugOverrides ::= enabled -> out + try thunk finally debugOverrides = debugOverrides.tail + + /** Run a continuation under the debug scope that surrounded the current local scope. */ + def inOuterDebugScope[T](thunk: => T): T = debugOverrides match + case _ :: outer => + val current = debugOverrides + debugOverrides = outer + try thunk finally debugOverrides = current + case Nil => thunk protected var scope: Opt[Str] = N @@ -44,4 +68,3 @@ abstract class TraceLogger(using val debugPrinter: DebugPrinter): scope = S(flag) try thunk finally scope = oldScope - diff --git a/hkmc2/shared/src/test/mlscript/codegen/BadDebugDirective.mls b/hkmc2/shared/src/test/mlscript/codegen/BadDebugDirective.mls new file mode 100644 index 0000000000..305a6bd932 --- /dev/null +++ b/hkmc2/shared/src/test/mlscript/codegen/BadDebugDirective.mls @@ -0,0 +1,29 @@ +:noCodeGen + + +:e +#dbg('unknown) +//│ ╔══[COMPILATION ERROR] Unknown debugging aspect 'unknown' +//│ ║ l.5: #dbg('unknown) +//│ ╙── ^^^^^^^^ + + +:e +#dbg(elaboration: 1) +//│ ╔══[COMPILATION ERROR] Expected a boolean value +//│ ║ l.12: #dbg(elaboration: 1) +//│ ╙── ^ + + +:e +#dbg(out: File(1)) +//│ ╔══[COMPILATION ERROR] Expected StdIO or File("path") +//│ ║ l.19: #dbg(out: File(1)) +//│ ╙── ^^^^^^^ + + +:e +#dbg(out: Somewhere) +//│ ╔══[COMPILATION ERROR] Expected StdIO or File("path") +//│ ║ l.26: #dbg(out: Somewhere) +//│ ╙── ^^^^^^^^^ diff --git a/hkmc2/shared/src/test/mlscript/codegen/DebugDefinitionPhases.mls b/hkmc2/shared/src/test/mlscript/codegen/DebugDefinitionPhases.mls new file mode 100644 index 0000000000..3b0cdc82ba --- /dev/null +++ b/hkmc2/shared/src/test/mlscript/codegen/DebugDefinitionPhases.mls @@ -0,0 +1,25 @@ +@dbg('resolution, 'lowering, 'optimizations) +fun selected_debug(x) = x +fun unrelated(x) = x +//│ | Resolving definition: TermDefinition(Fun,member:selected_debug‹36›,term:selected_debug‹41›,List(ParamList(‹›,List(Param(‹›,x‹38›,None,Modulefulness(None))),None)),None,None,Some(Ref(x‹38›)),‹›,Modulefulness(None),List(Debug(...)),None) +//│ | | Resolving function definition TermDefinition(Fun,member:selected_debug‹36›,term:selected_debug‹41›,List(ParamList(‹›,List(Param(‹›,x‹38›,None,Modulefulness(None))),None)),None,None,Some(Ref(x‹38›)),‹›,Modulefulness(None),List(Debug(...)),None) +//│ | | Resolving parameter x‹38› +//│ | | Traversing term: Ref(x‹38›) (expect = NonModule) +//│ | | | Resolving resolvable term: Ref(x‹38›), (prefer = NonModule, inAppPrefix = false, inCtxPrefix = false, inTyPrefix = false) +//│ | | | | Resolving symbol for term: Ref(x‹38›) (prefer = NonModule) +//│ | | | | -> (sym = Some(x‹38›), typ = None) +//│ | | | | Resolving the type for term: Ref(x‹38›) (prefer = NonModule, sym = Some(x‹38›)) +//│ | | | | | Declaration: Some(Param(‹›,x‹38›,None,Modulefulness(None))) +//│ | | | | -> typ = None +//│ | | | | Resolving resolvable term Ref(x‹38›) with sym = Some(x‹38›), typ = None +//│ | | | ~> Ref(x‹38›) (sym = Some(x‹38›), typ = None) +//│ | | | Checking Ref(x‹38›): expect NonModule, evalsToModule = false, evalsToStaticClass = false +//│ Lowering.term x⁰‹38› – symbol x‹38› +//│ Expanding term Ref(x‹38›) +//│ ~> Ref(x‹38›) +//│ Optimization TailRecOpt left selected_debug unchanged +//│ Optimization WorkerWrapper left selected_debug unchanged +//│ Optimization BlockSimplifier 1 left selected_debug unchanged +//│ Optimization DeadParamElim left selected_debug unchanged +//│ Optimization TailRecOpt left selected_debug unchanged +//│ Optimization BlockSimplifier 2 left selected_debug unchanged diff --git a/hkmc2/shared/src/test/mlscript/codegen/DebugDirective.mls b/hkmc2/shared/src/test/mlscript/codegen/DebugDirective.mls new file mode 100644 index 0000000000..340093f7e8 --- /dev/null +++ b/hkmc2/shared/src/test/mlscript/codegen/DebugDirective.mls @@ -0,0 +1,42 @@ +:noCodeGen + + +// Shorthand annotation debugging is scoped to the annotated definition. +@dbg('elaboration) fun debug_me(x) = x +fun do_not_debug_me(x) = x +//│ Processing term definition App(Ident(debug_me),Tup(List(Ident(x)))) +//│ Elaborating ParamTree: ParamTree(‹›,Ident(x),None,None,Set()) +//│ Elab term Ident(x) +//│ | Elab subterm Ident(x) +//│ | ~> Ref(x‹38›) +//│ ~> Ref(x‹38›) + + +// Named flags can be explicitly disabled, and StdIO is the default destination. +#dbg(elaboration: false, out: StdIO) +fun named_debug_config(x) = x + + +// File destinations are accepted even when no phase is enabled. +#dbg(out: File("unused-debug.log")) +fun file_debug_config(x) = x + +#dbg(out: StdIO) + + +// A top-level directive persists into the following block. +#dbg(elaboration: true) +//│ Elab block List(App(Directive(Ident(dbg),Tup(List())),Tup(List(InfixApp(Ident(elaboration),Keywrd(keyword ':'),[...] LocalScope(block:6) +//│ ~> Blk(List(SetConfig(...)),Lit(UnitLit(false))) + +fun persistently_debugged(x) = x +//│ Elab block List(TermDef(Fun,App(Ident(persistently_debugged),Tup(List(Ident(x)))),Some(Ident(x)))) LocalScope(block:7) +//│ | Processing term definition App(Ident(persistently_debugged),Tup(List(Ident(x)))) +//│ | Elaborating ParamTree: ParamTree(‹›,Ident(x),None,None,Set()) +//│ | Elab term Ident(x) +//│ | | Elab subterm Ident(x) +//│ | | ~> Ref(x‹57›) +//│ | ~> Ref(x‹57›) +//│ ~> Blk(List(TermDefinition(Fun,member:persistently_debugged‹56›,term:persistently_debugged‹60›,List(ParamList(‹›,List(Param(‹›,x‹57›,None,Modulefulness(None))),None)),None,None,Some(Ref(x‹57›)),‹›,Modulefulness(None),List(),None)),Lit(UnitLit(false))) + +#dbg(elaboration: false) diff --git a/hkmc2/shared/src/test/mlscript/codegen/DebugParsingDirective.mls b/hkmc2/shared/src/test/mlscript/codegen/DebugParsingDirective.mls new file mode 100644 index 0000000000..73a651d05a --- /dev/null +++ b/hkmc2/shared/src/test/mlscript/codegen/DebugParsingDirective.mls @@ -0,0 +1,57 @@ +:noCodeGen + + +#dbg('parsing) +//│ ┊#┊dbg┊(⟨┊'parsing┊⟩)┊ +//│ @ blockOf(start of expression,allowNewlines = true) [at syntax/Parser.scala:408] +//│ │ ? blockOfImpl inspects ┊#┊dbg┊(⟨┊'parsing┊⟩)┊ [at syntax/Parser.scala:331] +//│ │ ! blockOfImpl consumes ┊#┊ [at syntax/Parser.scala:358] +//│ │ @ parseRule(39,'#' directive keyword ::= Expr('#' directive body ::= End()),allowNewlines = true) [at syntax/Parser.scala:371] +//│ │ │ @ simpleExpr(39) [at syntax/Parser.scala:420] +//│ │ │ │ ! simpleExprImpl consumes ┊dbg┊ [at syntax/Parser.scala:648] +//│ │ │ │ @ exprCont(39,`Ident(dbg)`,true) [at syntax/Parser.scala:663] +//│ │ │ │ │ ? exprContImpl inspects ┊(⟨┊'parsing┊⟩)┊ [at syntax/Parser.scala:877] +//│ │ │ │ │ @ exprJux(39,`Ident(dbg)`,false) [at syntax/Parser.scala:1149] +//│ │ │ │ │ │ ? exprJuxImpl inspects ┊(⟨┊'parsing┊⟩)┊ [at syntax/Parser.scala:1156] +//│ │ │ │ │ │ stops at BRACKETS(Round,List((IDENT('parsing,false),Loc(5,13,DebugParsingDirective.mls:+4)))) +//│ │ │ │ │ = Ident(dbg) +//│ │ │ │ = Ident(dbg) +//│ │ │ = Ident(dbg) +//│ │ │ @ parseRule(39,'#' directive body ::= End(),allowNewlines = true) [at syntax/Parser.scala:422] +//│ │ │ = Some(()) +//│ │ = Some(Directive(Ident(dbg),Tup(List()))) +//│ │ @ exprCont(1,`Directive(Ident(dbg),Tup(List()))`,true) [at syntax/Parser.scala:372] +//│ │ │ ? exprContImpl inspects ┊(⟨┊'parsing┊⟩)┊ [at syntax/Parser.scala:877] +//│ │ │ ! exprContImpl consumes ┊(⟨┊'parsing┊⟩)┊ [at syntax/Parser.scala:1081] +//│ │ │ > @ concludeWith(()) [at syntax/Parser.scala:1082] +//│ │ │ > │ @ blockOf(start of expression,allowNewlines = false) [at syntax/Parser.scala:301] +//│ │ │ > │ │ ? blockOfImpl inspects ┊'parsing┊ [at syntax/Parser.scala:331] +//│ │ │ > │ │ @ simpleExpr(1) [at syntax/Parser.scala:420] +//│ │ │ > │ │ │ ! simpleExprImpl consumes ┊'parsing┊ [at syntax/Parser.scala:648] +//│ │ │ > │ │ │ @ exprCont(1,`Ident('parsing)`,false) [at syntax/Parser.scala:663] +//│ │ │ > │ │ │ │ ? exprContImpl inspects ┊┊ [at syntax/Parser.scala:877] +//│ │ │ > │ │ │ │ @ exprJux(1,`Ident('parsing)`,false) [at syntax/Parser.scala:1149] +//│ │ │ > │ │ │ │ │ ? exprJuxImpl inspects ┊┊ [at syntax/Parser.scala:1156] +//│ │ │ > │ │ │ │ │ stops at the end of input +//│ │ │ > │ │ │ │ = Ident('parsing) +//│ │ │ > │ │ │ = Ident('parsing) +//│ │ │ > │ │ = Ident('parsing) +//│ │ │ > │ │ @ parseRule(1,expression ::= End(),allowNewlines = false) [at syntax/Parser.scala:422] +//│ │ │ > │ │ = Some(()) +//│ │ │ > │ │ ? blockOfImpl inspects ┊┊ [at syntax/Parser.scala:404] +//│ │ │ > │ = List(Ident('parsing)) +//│ │ │ > │ ? concludeWithImpl inspects ┊┊ [at syntax/Parser.scala:265] +//│ │ │ > │ Concluded with List(Ident('parsing)) +//│ │ │ > = List(Ident('parsing)) +//│ │ │ @ exprCont(1,`App(Directive(Ident(dbg),Tup(List())),Tup(List(Ident('parsing))))`,true) [at syntax/Parser.scala:1084] +//│ │ │ │ ? exprContImpl inspects ┊┊ [at syntax/Parser.scala:877] +//│ │ │ │ @ exprJux(1,`App(Directive(Ident(dbg),Tup(List())),Tup(List(Ident('parsing))))`,false) [at syntax/Parser.scala:1149] +//│ │ │ │ │ ? exprJuxImpl inspects ┊┊ [at syntax/Parser.scala:1156] +//│ │ │ │ │ stops at the end of input +//│ │ │ │ = App(Directive(Ident(dbg),Tup(List())),Tup(List(Ident('parsing)))) +//│ │ │ = App(Directive(Ident(dbg),Tup(List())),Tup(List(Ident('parsing)))) +//│ │ = App(Directive(Ident(dbg),Tup(List())),Tup(List(Ident('parsing)))) +//│ = List(App(Directive(Ident(dbg),Tup(List())),Tup(List(Ident('parsing))))) +//│ ? parseAll inspects ┊┊ [at syntax/Parser.scala:252] + +#dbg(parsing: false) diff --git a/hkmc2DiffTests/src/test/scala/hkmc2/JSBackendDiffMaker.scala b/hkmc2DiffTests/src/test/scala/hkmc2/JSBackendDiffMaker.scala index 12f7be7996..7198d27fe3 100644 --- a/hkmc2DiffTests/src/test/scala/hkmc2/JSBackendDiffMaker.scala +++ b/hkmc2DiffTests/src/test/scala/hkmc2/JSBackendDiffMaker.scala @@ -48,10 +48,14 @@ abstract class JSBackendDiffMaker extends MLsDiffMaker: override def doTrace = debugLowering.isSet || scope.exists: showUCS.get.getOrElse(Set.empty).contains override def emitDbg(str: String): Unit = output(str) + override protected[hkmc2] def emitDbg(str: Str, out: Config.DebugOutput): Unit = + debugOutputHandler.emit(out, str) val dtl = new TraceLogger: override def doTrace = debugOptimizations.isSet override def emitDbg(str: String): Unit = output(str) + override protected[hkmc2] def emitDbg(str: Str, out: Config.DebugOutput): Unit = + debugOutputHandler.emit(out, str) val replTL = new TraceLogger: override def doTrace = showRepl.isSet @@ -125,8 +129,12 @@ abstract class JSBackendDiffMaker extends MLsDiffMaker: new codegen.Lowering()(using summon[Config], ltl, summon[Raise], loweringState, curCtx, summon[SymbolPrinter]) with codegen.LoweringTraceLog(traceJS.isSet) - val lowered = ltl.givenIn: - low.program(blk, symbolsToPreserve = symbolsToPreserve) + val lowered = + def lower = ltl.givenIn: + low.program(blk, symbolsToPreserve = symbolsToPreserve) + if config.debug.lowering then + ltl.scopedDebug(enabled = true, config.debug.out)(lower) + else lower val optimized = ltl.givenIn: val customPipeline = new CompilationPipeline: @@ -166,7 +174,10 @@ abstract class JSBackendDiffMaker extends MLsDiffMaker: outputSeparator("Lowered IR") output(irStr) super.preOptimizeHook(prog) - customPipeline.run(lowered, print, symbolsToPreserve, dtl) + if config.debug.optimizations then + dtl.scopedDebug(enabled = true, config.debug.out): + customPipeline.run(lowered, print, symbolsToPreserve, dtl) + else customPipeline.run(lowered, print, symbolsToPreserve, dtl) if checkIR.isSet then BlockChecker().applyProgram(optimized) diff --git a/hkmc2DiffTests/src/test/scala/hkmc2/MLsDiffMaker.scala b/hkmc2DiffTests/src/test/scala/hkmc2/MLsDiffMaker.scala index 7143587f17..bcb7262f18 100644 --- a/hkmc2DiffTests/src/test/scala/hkmc2/MLsDiffMaker.scala +++ b/hkmc2DiffTests/src/test/scala/hkmc2/MLsDiffMaker.scala @@ -171,6 +171,7 @@ abstract class MLsDiffMaker extends DiffMaker: commentGeneratedCode = debug.isSet, noFreeze = noFreeze.isSet, noModuleCheck = noModuleCheck.isSet, + debug = Config.Debug.default, optimizer = Optimizer( deforest = Opt.when(deforest.isSet): val flags = parseFlags(deforest.get) @@ -224,11 +225,19 @@ abstract class MLsDiffMaker extends DiffMaker: ln.split(" ").iterator.map(x => "ucs:" + x.trim).toSet + private var activeDebug = Config.Debug.default + protected lazy val debugOutputHandler = DebugOutputHandler(cctx.fs, wd, str => output(str)) + given Elaborator.State = new Elaborator.State: - override def dbg: Bool = + override protected def doDbg: Bool = dbgParsing.isSet || dbgElab.isSet || dbgResolving.isSet + || activeDebug.parsing + || activeDebug.elaboration + || activeDebug.resolution + || activeDebug.lowering + || activeDebug.optimizations || debug.isSet @@ -263,16 +272,17 @@ abstract class MLsDiffMaker extends DiffMaker: override def doTrace = dbgElab.isSet || scope.exists: showUCS.get.getOrElse(Set.empty).contains override def emitDbg(str: String): Unit = output(str) + override protected[hkmc2] def emitDbg(str: Str, out: Config.DebugOutput): Unit = + debugOutputHandler.emit(out, str) override def trace[T](pre: => Str, post: T => Str = noPostTrace)(thunk: => T): T = - // * This override is for avoiding to increase the indentation when tracing if doTrace is false, - // * so that selectively-enabled tracing doesn't get strange indentation. - // * Perhaps this should be the default behavior of TraceLogger. - if doTrace then super.trace(pre, post)(thunk) - else thunk + // Avoid accumulating indentation outside selectively-enabled elaboration scopes. + if isTracing then super.trace(pre, post)(thunk) else thunk val rtl = new TraceLogger: override def doTrace = dbgResolving.isSet override def emitDbg(str: String): Unit = output(str) + override protected[hkmc2] def emitDbg(str: Str, out: Config.DebugOutput): Unit = + debugOutputHandler.emit(out, str) val ftl = new TraceLogger: override def doTrace = dbgFlow.isSet @@ -382,18 +392,28 @@ abstract class MLsDiffMaker extends DiffMaker: def processOrigin(origin: Origin)(using Raise): Unit = val oldCtx = curCtx - given Config = configModify(mkConfig) - - val lexer = new syntax.Lexer(origin, dbg = dbgParsing.isSet) - val tokens = lexer.bracketedTokens - - if showParse.isSet || dbgParsing.isSet then - output(syntax.Lexer.printTokens(tokens)) - - val rules = syntax.ParseRules() - val p = new syntax.Parser(origin, tokens, rules, raise, dbg = dbgParsing.isSet): - def doPrintDbg(msg: => Str): Unit = if dbg then output(msg) - val res = p.parseAll(p.block(allowNewlines = true)) + val baseConfig = configModify(mkConfig) + given Config = baseConfig + + def parse(dbg: Bool, out: Config.DebugOutput): Ls[syntax.Tree] = + val lexer = new syntax.Lexer(origin, dbg = dbg): + override protected def doPrintDbg(msg: => Str): Unit = debugOutputHandler.emit(out, msg) + val tokens = lexer.bracketedTokens + if showParse.isSet || dbg then + if dbg then debugOutputHandler.emit(out, syntax.Lexer.printTokens(tokens)) + else output(syntax.Lexer.printTokens(tokens)) + val rules = syntax.ParseRules() + val p = new syntax.Parser(origin, tokens, rules, raise, dbg = dbg): + def doPrintDbg(msg: => Str): Unit = if this.dbg then debugOutputHandler.emit(out, msg) + p.parseAll(p.block(allowNewlines = true)) + + val discoveryResult = parse(dbg = false, Config.DebugOutput.StdIO) + val phaseConfig = ConfigParser.discoverDebugFromTrees(discoveryResult) + activeDebug = phaseConfig.debug + val res = + if dbgParsing.isSet || phaseConfig.debug.parsing then + parse(dbg = true, phaseConfig.debug.out) + else discoveryResult // If parsed tree is displayed, don't show the string serialization. if (parseOnly.isSet || showParse.isSet) && !showParsedTree.isSet then @@ -419,6 +439,8 @@ abstract class MLsDiffMaker extends DiffMaker: private var blockNum = 0 def processTrees(trees: Ls[syntax.Tree])(using Config, Raise): Unit = + val phaseConfig = ConfigParser.discoverDebugFromTrees(trees) + activeDebug = phaseConfig.debug val elab = Elaborator(etl, file.up, prelude) // val blockSymbol = // semantics.TopLevelSymbol("block#"+blockNum) @@ -426,7 +448,10 @@ abstract class MLsDiffMaker extends DiffMaker: // given Elaborator.Ctx = curCtx.nest(S(blockSymbol)) given Elaborator.Ctx = curCtx.nestLocal(s"block:${blockNum}") val blk = new syntax.Tree.Block(trees) - val (e, newCtx) = elab.topLevel(blk) + val (e, newCtx) = + if phaseConfig.debug.elaboration then + etl.scopedDebug(enabled = true, phaseConfig.debug.out)(elab.topLevel(blk)) + else elab.topLevel(blk) curCtx = newCtx // Extract SetConfig statements and update persistent config @@ -450,11 +475,16 @@ abstract class MLsDiffMaker extends DiffMaker: def processTerm(trm: semantics.Term.Blk, inImport: Bool)(using Config, Raise): Unit = given Ctx = curCtx given Config = Config.extractConfigFromStats(trm) + activeDebug = config.debug if file.toString =/= runtimeSourceFile.toString && file.toString =/= preludeFile.toString then summon[Elaborator.State].initRuntimeSymbolsFromFile(runtimeSourceFile, prelude)( using summon[TL], summon[Raise], cctx) val resolver = Resolver(rtl) - curICtx = resolver.traverseBlock(trm)(using curICtx) + curICtx = + if config.debug.resolution then + rtl.scopedDebug(enabled = true, config.debug.out): + resolver.traverseBlock(trm)(using curICtx) + else resolver.traverseBlock(trm)(using curICtx) if showResolve.isSet then output(s"Resolved: ${trm.showDbg}") From 1cc0ec1be2cecc1399232e1eef84857aed590e24 Mon Sep 17 00:00:00 2001 From: OpenAI Codex Date: Tue, 11 Aug 2026 11:25:17 +0800 Subject: [PATCH 02/32] Reuse existing compiler debug trace paths --- DBG_DIRECTIVE_PLAN.md | 8 +++- .../src/main/scala/hkmc2/CompilerCtx.scala | 21 ++++++++- .../shared/src/main/scala/hkmc2/Config.scala | 6 +++ .../hkmc2/codegen/CompilationPipeline.scala | 36 -------------- .../scala/hkmc2/codegen/DeadParamElim.scala | 3 +- .../scala/hkmc2/codegen/EtaExpansion.scala | 2 +- .../main/scala/hkmc2/codegen/Lowering.scala | 9 +++- .../codegen/DebugDefinitionPhases.mls | 47 ++++++++++++++++--- .../codegen/DebugParsingDirective.mls | 2 +- .../test/scala/hkmc2/JSBackendDiffMaker.scala | 40 +++++++++------- .../src/test/scala/hkmc2/MLsDiffMaker.scala | 21 ++++----- 11 files changed, 114 insertions(+), 81 deletions(-) diff --git a/DBG_DIRECTIVE_PLAN.md b/DBG_DIRECTIVE_PLAN.md index 12c05fe846..0a76ad3b3f 100644 --- a/DBG_DIRECTIVE_PLAN.md +++ b/DBG_DIRECTIVE_PLAN.md @@ -9,6 +9,8 @@ - resolution (`:dr`), - lowering (`:dl`), and - optimizations (`:dopt`). +- Mirror the existing IR display commands with `showIR` (`:sir`) and + `showOptimizedIR` (`:soir`). - Accept both shorthand flags and named booleans, including: - `#dbg('parsing)`; - `#dbg(elaboration: true)`. @@ -25,9 +27,9 @@ 1. Add immutable debug settings and output-destination types to `Config`, with all phase flags disabled and `StdIO` selected by default. 2. Extend `ConfigParser` with one parser shared by `#dbg(...)` and `@dbg(...)`; support quoted phase names, named phase booleans, and `out`. 3. Elaborate `#dbg` into the existing persistent `SetConfig` representation and `@dbg` into a per-definition configuration annotation. -4. Add destination-aware, dynamically scoped tracing so a definition annotation can temporarily enable only its requested phase and destination. +4. Reuse the existing phase trace loggers, with destination-aware dynamic scoping only where a definition annotation must temporarily change their configuration. 5. Discover module/block-level parsing debug settings after an initial parse and, when requested, repeat parsing with lexer/parser tracing sent to the selected destination. -6. Drive elaboration, resolution, lowering, and optimization trace loggers from the effective `Config`; retain the legacy diff-test flags by combining them with the new settings. +6. Drive elaboration, resolution, lowering, optimization, and IR display from the effective `Config`; retain the legacy diff-test commands by sharing their existing code paths. 7. Add positive and negative diff tests covering shorthand/named syntax, persistence, destination selection, and definition-local elaboration output. 8. Run `ctest`, focused diff tests while iterating, and finally `hkmc2AllTests/test`; review and commit all intentional golden changes. @@ -39,3 +41,5 @@ - [x] Definition-local scoping implemented. - [x] Regression tests and golden outputs added. - [x] Full prescribed test suite passes. +- [x] Custom optimization reporting removed in favor of the existing `:dopt` trace path. +- [x] `showIR` and `showOptimizedIR` share the existing `:sir` and `:soir` display paths. diff --git a/hkmc2/shared/src/main/scala/hkmc2/CompilerCtx.scala b/hkmc2/shared/src/main/scala/hkmc2/CompilerCtx.scala index e94d32eb91..1b395329e1 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/CompilerCtx.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/CompilerCtx.scala @@ -159,12 +159,29 @@ class CompilerCtx( // and only fully-transformed definitions are valid to splice into another unit. def optimize(lowered: codegen.Program): codegen.Program = given Config = artifactConfig - val printer = (p: codegen.Program) => p.showAsTree // TODO: proper printing like in diff-tests + given ShowCfg = ShowCfg( + showExpansionMappings = false, + showFlowSymbols = true, + debug = false, + ) + val irPrintingScp = Scope.empty(Scope.Cfg.default.copy( + escapeChars = false, + useSuperscripts = false, + includeZero = false, + )) + val printer = (p: codegen.Program) => + codegen.Printer().worksheet(p)(using irPrintingScp).mkString(100) + def showDebugIR(title: Str, program: codegen.Program): Unit = + outputHandler.emit(artifactConfig.debug.out, s"${title}\n${printer(program)}") val pipeline = new codegen.CompilationPipeline: override def extraSymbolsToPreserve(prog: codegen.Program): Set[codegen.BoundSymbol] = collectCompilationUnitSymbols(prog).toSet - ltl.givenIn: + override def preOptimizeHook(prog: codegen.Program): Unit = + if artifactConfig.debug.showIR then showDebugIR("Lowered IR", prog) + val optimized = ltl.givenIn: pipeline.run(lowered, printer, exportedSymbol.toSet, dtl) + if artifactConfig.debug.showOptimizedIR then showDebugIR("Optimized IR", optimized) + optimized // Every compilation unit is lowered, so that its symbols carry the IR definitions an // importer's inliner may splice in — and so that the importer never has to lower it itself, diff --git a/hkmc2/shared/src/main/scala/hkmc2/Config.scala b/hkmc2/shared/src/main/scala/hkmc2/Config.scala index 7267b3f9c9..f48b982efd 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/Config.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/Config.scala @@ -140,6 +140,8 @@ object Config: resolution: Bool, lowering: Bool, optimizations: Bool, + showIR: Bool, + showOptimizedIR: Bool, out: DebugOutput, ) object Debug: @@ -149,6 +151,8 @@ object Config: resolution = false, lowering = false, optimizations = false, + showIR = false, + showOptimizedIR = false, out = DebugOutput.StdIO, ) @@ -520,6 +524,8 @@ object ConfigParser: case "resolution" => _.copy(resolution = value) case "lowering" => _.copy(lowering = value) case "optimizations" => _.copy(optimizations = value) + case "showIR" => _.copy(showIR = value) + case "showOptimizedIR" => _.copy(showOptimizedIR = value) case _ => raise(ErrorReport( msg"Unknown debugging aspect '${name}'" -> tree.toLoc :: Nil, diff --git a/hkmc2/shared/src/main/scala/hkmc2/codegen/CompilationPipeline.scala b/hkmc2/shared/src/main/scala/hkmc2/codegen/CompilationPipeline.scala index 8bc8948553..45a1c97498 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/codegen/CompilationPipeline.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/codegen/CompilationPipeline.scala @@ -25,48 +25,13 @@ class CompilationPipeline(using Config, Raise, State, Ctx, SymbolPrinter): private inline def blockPass(inline pass: Block => Block)(prog: Program): Program = val blk = pass(prog.main) if blk is prog.main then prog else Program(prog.imports, blk) - - private def collectDefinitions(block: Block): Ls[Defn] = block match - case Define(defn, rest) => - defn :: defn.subBlocks.flatMap(collectDefinitions) ::: collectDefinitions(rest) - case Match(_, arms, default, rest) => - arms.flatMap(arm => collectDefinitions(arm._2)) ::: - default.toList.flatMap(collectDefinitions) ::: collectDefinitions(rest) - case Label(_, _, body, rest) => collectDefinitions(body) ::: collectDefinitions(rest) - case Begin(sub, rest) => collectDefinitions(sub) ::: collectDefinitions(rest) - case TryBlock(sub, finallyDo, rest) => - collectDefinitions(sub) ::: collectDefinitions(finallyDo) ::: collectDefinitions(rest) - case Assign(_, _, rest) => collectDefinitions(rest) - case AssignField(_, _, _, rest) => collectDefinitions(rest) - case AssignDynField(_, _, _, _, rest) => collectDefinitions(rest) - case Scoped(_, body) => collectDefinitions(body) - case _: BlockTail => Nil - - /** Emit a compact per-pass delta for definitions carrying `@dbg('optimizations)`. - * This remains definition-local even though most optimization passes operate on a whole program. */ - private def reportDefinitionOptimizationDebug(passName: Str, before: Program, after: Program, otl: TL): Unit = - val afterBySymbol = collectDefinitions(after.main).map(defn => defn.sym -> defn).toMap - collectDefinitions(before.main).foreach: oldDefn => - oldDefn.configOverride.foreach: localConfig => - if localConfig.debug.optimizations then - otl.scopedDebug(enabled = true, localConfig.debug.out): - afterBySymbol.get(oldDefn.sym) match - case N => otl.log(s"Optimization $passName removed ${oldDefn.sym.nme}") - case S(newDefn) if newDefn == oldDefn => - otl.log(s"Optimization $passName left ${oldDefn.sym.nme} unchanged") - case S(newDefn) => - val details = newDefn match - case product: Product => product.showAsTree - otl.log(s"Optimization $passName changed ${oldDefn.sym.nme}:\n${details}") def run(prog: Program, printer: Program => Str, symbolsToPreserve: Set[BoundSymbol], otl: TL)(using TL): Program = var result = prog - var reportDefinitionDebug = false inline def runPass(passName: Str)(inline transform: Program => Program) = val before = result result = transform(before) passHook(passName, before, result) - if reportDefinitionDebug then reportDefinitionOptimizationDebug(passName, before, result, otl) runPass("LambdaRewriter")(LambdaRewriter.desugar) runPass("Deforest"): prog => @@ -98,7 +63,6 @@ class CompilationPipeline(using Config, Raise, State, Ctx, SymbolPrinter): runPass("ClassParamFlattener")(ClassParamFlattener.apply) runPass("ReflectionInstrumenter")(ReflectionInstrumenter(using summon).apply) preOptimizeHook(result) - reportDefinitionDebug = true // * We run this pass here first, before inlining so that the @tailrec/@tailcall annotations // * can be properly checked. diff --git a/hkmc2/shared/src/main/scala/hkmc2/codegen/DeadParamElim.scala b/hkmc2/shared/src/main/scala/hkmc2/codegen/DeadParamElim.scala index 19da739b34..5ff6f1f453 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/codegen/DeadParamElim.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/codegen/DeadParamElim.scala @@ -105,7 +105,7 @@ class DeadParamElimSolver(val constraintSolver: FlowConstraintSolver): case None => eliminableCallSiteArgsById(consFun.concreteId) = eliminable.toSet case S(existing) => assert(existing.toList.sorted === eliminable) - if tl.isTracing then + if tl.doTrace then def showRefSite(resultId: ResultId): Str = resultId.getReferredFun match case Some(fun) => s"${fun.nme}@$resultId" @@ -411,4 +411,3 @@ object DeadParamElim: else val rewrite = new Rewrite(deadParamElimSolver) rewrite() - diff --git a/hkmc2/shared/src/main/scala/hkmc2/codegen/EtaExpansion.scala b/hkmc2/shared/src/main/scala/hkmc2/codegen/EtaExpansion.scala index 14a9b0ccd9..7ecf86765b 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/codegen/EtaExpansion.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/codegen/EtaExpansion.scala @@ -126,7 +126,7 @@ class EtaExpansionSolver(val constraintSolver: FlowConstraintSolver): funShape.exists: case (id, shape) => checkIsEtaExpanded(id, shape) - if tl.isTracing then + if tl.doTrace then tl.log(">>> eta-expansion targets shapes >>>") for (id, shape) <- funShape diff --git a/hkmc2/shared/src/main/scala/hkmc2/codegen/Lowering.scala b/hkmc2/shared/src/main/scala/hkmc2/codegen/Lowering.scala index 47e195e2de..ec0cf99e26 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/codegen/Lowering.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/codegen/Lowering.scala @@ -88,6 +88,11 @@ class Lowering()(using Config, TL, Raise, State, Ctx, SymbolPrinter): private def definitionConfigOverride(annotations: Ls[Annot]): Opt[Config] = val modifiers = annotations.collect: case Annot.Config(modify) => modify + if modifiers.isEmpty then N + else S(modifiers.foldLeft(config)((cfg, modify) => modify(cfg))) + + private def definitionDebugConfig(annotations: Ls[Annot]): Opt[Config] = + val modifiers = annotations.collect: case Annot.Debug(modify) => modify if modifiers.isEmpty then N else S(modifiers.foldLeft(config)((cfg, modify) => modify(cfg))) @@ -266,7 +271,7 @@ class Lowering()(using Config, TL, Raise, State, Ctx, SymbolPrinter): case knd: syntax.Val => assert(td.params.isEmpty) val cfgOverride = definitionConfigOverride(td.extraAnnotations) - cfgOverride match + definitionDebugConfig(td.extraAnnotations) match case S(localConfig) => State.scopedDebug(localConfig.debug.lowering): tl.scopedDebug(localConfig.debug.lowering, localConfig.debug.out): @@ -280,7 +285,7 @@ class Lowering()(using Config, TL, Raise, State, Ctx, SymbolPrinter): blockImpl(stats, res)))(using LoweringCtx.nestFunc) case syntax.Fun => val cfgOverride = definitionConfigOverride(td.extraAnnotations) - val (paramLists, bodyBlock) = cfgOverride match + val (paramLists, bodyBlock) = definitionDebugConfig(td.extraAnnotations) match case S(localConfig) => State.scopedDebug(localConfig.debug.lowering): tl.scopedDebug(localConfig.debug.lowering, localConfig.debug.out): diff --git a/hkmc2/shared/src/test/mlscript/codegen/DebugDefinitionPhases.mls b/hkmc2/shared/src/test/mlscript/codegen/DebugDefinitionPhases.mls index 3b0cdc82ba..049c9d5fe1 100644 --- a/hkmc2/shared/src/test/mlscript/codegen/DebugDefinitionPhases.mls +++ b/hkmc2/shared/src/test/mlscript/codegen/DebugDefinitionPhases.mls @@ -1,4 +1,4 @@ -@dbg('resolution, 'lowering, 'optimizations) +@dbg('resolution, 'lowering) fun selected_debug(x) = x fun unrelated(x) = x //│ | Resolving definition: TermDefinition(Fun,member:selected_debug‹36›,term:selected_debug‹41›,List(ParamList(‹›,List(Param(‹›,x‹38›,None,Modulefulness(None))),None)),None,None,Some(Ref(x‹38›)),‹›,Modulefulness(None),List(Debug(...)),None) @@ -17,9 +17,42 @@ fun unrelated(x) = x //│ Lowering.term x⁰‹38› – symbol x‹38› //│ Expanding term Ref(x‹38›) //│ ~> Ref(x‹38›) -//│ Optimization TailRecOpt left selected_debug unchanged -//│ Optimization WorkerWrapper left selected_debug unchanged -//│ Optimization BlockSimplifier 1 left selected_debug unchanged -//│ Optimization DeadParamElim left selected_debug unchanged -//│ Optimization TailRecOpt left selected_debug unchanged -//│ Optimization BlockSimplifier 2 left selected_debug unchanged + + +#dbg('optimizations) +fun optimization_debug(x) = x +//│ ⬤ Simplif. iter. 1 +//│ Captured variables: HashSet() +//│ ⬤ Simplif. iter. 1 +//│ Captured variables: HashSet() + + +#dbg(optimizations: false) + + +:dopt +fun command_optimization_debug(x) = x +//│ ⬤ Simplif. iter. 1 +//│ Captured variables: HashSet() +//│ ⬤ Simplif. iter. 1 +//│ Captured variables: HashSet() + + +#dbg('showIR, 'showOptimizedIR) +fun shown_ir(x) = x +//│ ———————————————| Lowered IR |——————————————————————————————————————————————————————————————————————— +//│ define shown_ir⁰ as fun shown_ir¹(x) { return x }; end +//│ ——————————————| Optimized IR |—————————————————————————————————————————————————————————————————————— +//│ define shown_ir⁰ as fun shown_ir¹(x) { return x }; end + + +#dbg(showIR: false, showOptimizedIR: false) + + +:sir +:soir +fun command_shown_ir(x) = x +//│ ———————————————| Lowered IR |——————————————————————————————————————————————————————————————————————— +//│ define command_shown_ir⁰ as fun command_shown_ir¹(x) { return x }; end +//│ ——————————————| Optimized IR |—————————————————————————————————————————————————————————————————————— +//│ define command_shown_ir⁰ as fun command_shown_ir¹(x) { return x }; end diff --git a/hkmc2/shared/src/test/mlscript/codegen/DebugParsingDirective.mls b/hkmc2/shared/src/test/mlscript/codegen/DebugParsingDirective.mls index 73a651d05a..bd26e91e5c 100644 --- a/hkmc2/shared/src/test/mlscript/codegen/DebugParsingDirective.mls +++ b/hkmc2/shared/src/test/mlscript/codegen/DebugParsingDirective.mls @@ -3,7 +3,7 @@ #dbg('parsing) //│ ┊#┊dbg┊(⟨┊'parsing┊⟩)┊ -//│ @ blockOf(start of expression,allowNewlines = true) [at syntax/Parser.scala:408] +//│ @ blockOf(start of expression,allowNewlines = true) [at syntax/Parser.scala:412] //│ │ ? blockOfImpl inspects ┊#┊dbg┊(⟨┊'parsing┊⟩)┊ [at syntax/Parser.scala:331] //│ │ ! blockOfImpl consumes ┊#┊ [at syntax/Parser.scala:358] //│ │ @ parseRule(39,'#' directive keyword ::= Expr('#' directive body ::= End()),allowNewlines = true) [at syntax/Parser.scala:371] diff --git a/hkmc2DiffTests/src/test/scala/hkmc2/JSBackendDiffMaker.scala b/hkmc2DiffTests/src/test/scala/hkmc2/JSBackendDiffMaker.scala index 7198d27fe3..5062191b8c 100644 --- a/hkmc2DiffTests/src/test/scala/hkmc2/JSBackendDiffMaker.scala +++ b/hkmc2DiffTests/src/test/scala/hkmc2/JSBackendDiffMaker.scala @@ -45,14 +45,18 @@ abstract class JSBackendDiffMaker extends MLsDiffMaker: val prettyPrintNme = baseScp.allocateName(Elaborator.State.prettyPrintSymbol)(using throw _) val ltl = new TraceLogger: - override def doTrace = debugLowering.isSet || scope.exists: + override def doTrace = debugLowering.isSet || activeDebug.lowering || scope.exists: showUCS.get.getOrElse(Set.empty).contains + override protected def defaultDebugOutput: Config.DebugOutput = + if activeDebug.lowering then activeDebug.out else Config.DebugOutput.StdIO override def emitDbg(str: String): Unit = output(str) override protected[hkmc2] def emitDbg(str: Str, out: Config.DebugOutput): Unit = debugOutputHandler.emit(out, str) val dtl = new TraceLogger: - override def doTrace = debugOptimizations.isSet + override def doTrace = debugOptimizations.isSet || activeDebug.optimizations + override protected def defaultDebugOutput: Config.DebugOutput = + if activeDebug.optimizations then activeDebug.out else Config.DebugOutput.StdIO override def emitDbg(str: String): Unit = output(str) override protected[hkmc2] def emitDbg(str: Str, out: Config.DebugOutput): Unit = debugOutputHandler.emit(out, str) @@ -60,6 +64,12 @@ abstract class JSBackendDiffMaker extends MLsDiffMaker: val replTL = new TraceLogger: override def doTrace = showRepl.isSet override def emitDbg(str: String): Unit = output(str) + + private def outputDebugSection(title: Str, contents: Str, out: Config.DebugOutput): Unit = out match + case Config.DebugOutput.StdIO => + outputSeparator(title) + output(contents) + case _ => debugOutputHandler.emit(out, s"${title}\n${contents}") lazy val host = hostCreated = true @@ -129,12 +139,8 @@ abstract class JSBackendDiffMaker extends MLsDiffMaker: new codegen.Lowering()(using summon[Config], ltl, summon[Raise], loweringState, curCtx, summon[SymbolPrinter]) with codegen.LoweringTraceLog(traceJS.isSet) - val lowered = - def lower = ltl.givenIn: - low.program(blk, symbolsToPreserve = symbolsToPreserve) - if config.debug.lowering then - ltl.scopedDebug(enabled = true, config.debug.out)(lower) - else lower + val lowered = ltl.givenIn: + low.program(blk, symbolsToPreserve = symbolsToPreserve) val optimized = ltl.givenIn: val customPipeline = new CompilationPipeline: @@ -161,7 +167,7 @@ abstract class JSBackendDiffMaker extends MLsDiffMaker: if showLoweredTree.isSet then outputSeparator("Lowered IR Tree") output(prog.showAsTree) - if showIR.isSet || showIRLines.isSet then + if showIR.isSet || showIRLines.isSet || config.debug.showIR then given ShowCfg = ShowCfg( showExpansionMappings = false, showFlowSymbols = true, @@ -173,23 +179,25 @@ abstract class JSBackendDiffMaker extends MLsDiffMaker: if showIR.isSet then outputSeparator("Lowered IR") output(irStr) + else if config.debug.showIR then + outputDebugSection("Lowered IR", irStr, config.debug.out) super.preOptimizeHook(prog) - if config.debug.optimizations then - dtl.scopedDebug(enabled = true, config.debug.out): - customPipeline.run(lowered, print, symbolsToPreserve, dtl) - else customPipeline.run(lowered, print, symbolsToPreserve, dtl) + customPipeline.run(lowered, print, symbolsToPreserve, dtl) if checkIR.isSet then BlockChecker().applyProgram(optimized) - if showOptimizedIR.isSet then - outputSeparator("Optimized IR") + if showOptimizedIR.isSet || config.debug.showOptimizedIR then given ShowCfg = ShowCfg( showExpansionMappings = false, showFlowSymbols = true, debug = debug.isSet, ) - output(Printer().worksheet(optimized)(using irPrintingScp).mkString(output.ColWidth)) + val irStr = Printer().worksheet(optimized)(using irPrintingScp).mkString(output.ColWidth) + if showOptimizedIR.isSet then + outputSeparator("Optimized IR") + output(irStr) + else outputDebugSection("Optimized IR", irStr, config.debug.out) if showOptimizedTree.isSet then outputSeparator("Optimized IR Tree") output(optimized.showAsTree) diff --git a/hkmc2DiffTests/src/test/scala/hkmc2/MLsDiffMaker.scala b/hkmc2DiffTests/src/test/scala/hkmc2/MLsDiffMaker.scala index bcb7262f18..c56918634b 100644 --- a/hkmc2DiffTests/src/test/scala/hkmc2/MLsDiffMaker.scala +++ b/hkmc2DiffTests/src/test/scala/hkmc2/MLsDiffMaker.scala @@ -225,7 +225,7 @@ abstract class MLsDiffMaker extends DiffMaker: ln.split(" ").iterator.map(x => "ucs:" + x.trim).toSet - private var activeDebug = Config.Debug.default + protected var activeDebug = Config.Debug.default protected lazy val debugOutputHandler = DebugOutputHandler(cctx.fs, wd, str => output(str)) given Elaborator.State = new Elaborator.State: @@ -269,8 +269,10 @@ abstract class MLsDiffMaker extends DiffMaker: val etl = new TraceLogger: - override def doTrace = dbgElab.isSet || scope.exists: + override def doTrace = dbgElab.isSet || activeDebug.elaboration || scope.exists: showUCS.get.getOrElse(Set.empty).contains + override protected def defaultDebugOutput: Config.DebugOutput = + if activeDebug.elaboration then activeDebug.out else Config.DebugOutput.StdIO override def emitDbg(str: String): Unit = output(str) override protected[hkmc2] def emitDbg(str: Str, out: Config.DebugOutput): Unit = debugOutputHandler.emit(out, str) @@ -279,7 +281,9 @@ abstract class MLsDiffMaker extends DiffMaker: if isTracing then super.trace(pre, post)(thunk) else thunk val rtl = new TraceLogger: - override def doTrace = dbgResolving.isSet + override def doTrace = dbgResolving.isSet || activeDebug.resolution + override protected def defaultDebugOutput: Config.DebugOutput = + if activeDebug.resolution then activeDebug.out else Config.DebugOutput.StdIO override def emitDbg(str: String): Unit = output(str) override protected[hkmc2] def emitDbg(str: Str, out: Config.DebugOutput): Unit = debugOutputHandler.emit(out, str) @@ -448,10 +452,7 @@ abstract class MLsDiffMaker extends DiffMaker: // given Elaborator.Ctx = curCtx.nest(S(blockSymbol)) given Elaborator.Ctx = curCtx.nestLocal(s"block:${blockNum}") val blk = new syntax.Tree.Block(trees) - val (e, newCtx) = - if phaseConfig.debug.elaboration then - etl.scopedDebug(enabled = true, phaseConfig.debug.out)(elab.topLevel(blk)) - else elab.topLevel(blk) + val (e, newCtx) = elab.topLevel(blk) curCtx = newCtx // Extract SetConfig statements and update persistent config @@ -480,11 +481,7 @@ abstract class MLsDiffMaker extends DiffMaker: summon[Elaborator.State].initRuntimeSymbolsFromFile(runtimeSourceFile, prelude)( using summon[TL], summon[Raise], cctx) val resolver = Resolver(rtl) - curICtx = - if config.debug.resolution then - rtl.scopedDebug(enabled = true, config.debug.out): - resolver.traverseBlock(trm)(using curICtx) - else resolver.traverseBlock(trm)(using curICtx) + curICtx = resolver.traverseBlock(trm)(using curICtx) if showResolve.isSet then output(s"Resolved: ${trm.showDbg}") From f8672ef0949ac5205616f838efd376b1e737dfd3 Mon Sep 17 00:00:00 2001 From: OpenAI Codex Date: Tue, 11 Aug 2026 11:42:44 +0800 Subject: [PATCH 03/32] Restore definition-local debug IR output --- DBG_DIRECTIVE_PLAN.md | 2 + .../src/main/scala/hkmc2/CompilerCtx.scala | 24 +++++++++ .../main/scala/hkmc2/codegen/Lowering.scala | 1 + .../main/scala/hkmc2/codegen/Printer.scala | 6 ++- .../codegen/DebugDefinitionPhases.mls | 49 +++++++++++++------ .../test/scala/hkmc2/JSBackendDiffMaker.scala | 23 +++++++++ 6 files changed, 89 insertions(+), 16 deletions(-) diff --git a/DBG_DIRECTIVE_PLAN.md b/DBG_DIRECTIVE_PLAN.md index 0a76ad3b3f..47611b96f7 100644 --- a/DBG_DIRECTIVE_PLAN.md +++ b/DBG_DIRECTIVE_PLAN.md @@ -43,3 +43,5 @@ - [x] Full prescribed test suite passes. - [x] Custom optimization reporting removed in favor of the existing `:dopt` trace path. - [x] `showIR` and `showOptimizedIR` share the existing `:sir` and `:soir` display paths. +- [x] `@dbg` configuration overrides remain attached to lowered IR definitions, including methods. +- [x] Definition-local IR display prints only definitions selected by `@dbg`, through the existing IR printer. diff --git a/hkmc2/shared/src/main/scala/hkmc2/CompilerCtx.scala b/hkmc2/shared/src/main/scala/hkmc2/CompilerCtx.scala index 1b395329e1..2b806a2dd0 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/CompilerCtx.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/CompilerCtx.scala @@ -173,14 +173,38 @@ class CompilerCtx( codegen.Printer().worksheet(p)(using irPrintingScp).mkString(100) def showDebugIR(title: Str, program: codegen.Program): Unit = outputHandler.emit(artifactConfig.debug.out, s"${title}\n${printer(program)}") + def showDefinitionDebugIR( + title: Str, + program: codegen.Program, + isEnabled: Config.Debug => Bool, + ): Unit = + val blockPrinter = codegen.Printer() + def show(defn: codegen.Defn): Unit = + defn.configOverride.foreach: localConfig => + if isEnabled(localConfig.debug) then + val ir = blockPrinter.printDefinition(defn)(using irPrintingScp).mkString(100) + outputHandler.emit(localConfig.debug.out, s"${title}\n${ir}") + new codegen.BlockTraverser: + override def applyFunDefn(fun: codegen.FunDefn): Unit = + show(fun) + super.applyFunDefn(fun) + override def applyValDefn(defn: codegen.ValDefn): Unit = + show(defn) + super.applyValDefn(defn) + override def applyClsLikeDefn(defn: codegen.ClsLikeDefn): Unit = + show(defn) + super.applyClsLikeDefn(defn) + .applyProgram(program) val pipeline = new codegen.CompilationPipeline: override def extraSymbolsToPreserve(prog: codegen.Program): Set[codegen.BoundSymbol] = collectCompilationUnitSymbols(prog).toSet override def preOptimizeHook(prog: codegen.Program): Unit = if artifactConfig.debug.showIR then showDebugIR("Lowered IR", prog) + else showDefinitionDebugIR("Lowered IR", prog, _.showIR) val optimized = ltl.givenIn: pipeline.run(lowered, printer, exportedSymbol.toSet, dtl) if artifactConfig.debug.showOptimizedIR then showDebugIR("Optimized IR", optimized) + else showDefinitionDebugIR("Optimized IR", optimized, _.showOptimizedIR) optimized // Every compilation unit is lowered, so that its symbols carry the IR definitions an diff --git a/hkmc2/shared/src/main/scala/hkmc2/codegen/Lowering.scala b/hkmc2/shared/src/main/scala/hkmc2/codegen/Lowering.scala index ec0cf99e26..fd277c2d61 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/codegen/Lowering.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/codegen/Lowering.scala @@ -88,6 +88,7 @@ class Lowering()(using Config, TL, Raise, State, Ctx, SymbolPrinter): private def definitionConfigOverride(annotations: Ls[Annot]): Opt[Config] = val modifiers = annotations.collect: case Annot.Config(modify) => modify + case Annot.Debug(modify) => modify if modifiers.isEmpty then N else S(modifiers.foldLeft(config)((cfg, modify) => modify(cfg))) diff --git a/hkmc2/shared/src/main/scala/hkmc2/codegen/Printer.scala b/hkmc2/shared/src/main/scala/hkmc2/codegen/Printer.scala index 8efbb7e062..a417a1605d 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/codegen/Printer.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/codegen/Printer.scala @@ -65,7 +65,7 @@ class Printer(using Raise, ShowCfg, State, SymbolPrinter, Config): case AssignDynField(lhs, fld, arrayIdx, rhs, rest) => doc"set ${print(lhs)}${if arrayIdx then "." else "!"}${print(fld)} = ${print(rhs)}; # ${print(rest)}" case Define(defn, rest) => - doc"${printFlags(defn)}define ${print(defn.sym)} as ${print(defn)}; # ${print(rest)}" + doc"${printDefinition(defn)}; # ${print(rest)}" case Scoped(syms, body) => scope.nest.givenIn: import hkmc2.given_Ordering_Uid // Not sure why needed... @@ -86,6 +86,10 @@ class Printer(using Raise, ShowCfg, State, SymbolPrinter, Config): // if cfg.staged then doc"staged " else doc"" printAnnotations(defn.annotations, doc" # ") + /** Print one definition in the same form used when it occurs in a complete IR block. */ + def printDefinition(defn: Defn)(using Scope): Document = + doc"${printFlags(defn)}define ${print(defn.sym)} as ${print(defn)}" + def printAnnotations(annotations: Ls[Annot], trailing: Document)(using Scope): Document = if annotations.isEmpty then doc"" else annotations.map(_.show).mkDocument(doc" ") :: trailing diff --git a/hkmc2/shared/src/test/mlscript/codegen/DebugDefinitionPhases.mls b/hkmc2/shared/src/test/mlscript/codegen/DebugDefinitionPhases.mls index 049c9d5fe1..5ea9dc9058 100644 --- a/hkmc2/shared/src/test/mlscript/codegen/DebugDefinitionPhases.mls +++ b/hkmc2/shared/src/test/mlscript/codegen/DebugDefinitionPhases.mls @@ -1,22 +1,39 @@ +:js + + @dbg('resolution, 'lowering) fun selected_debug(x) = x fun unrelated(x) = x -//│ | Resolving definition: TermDefinition(Fun,member:selected_debug‹36›,term:selected_debug‹41›,List(ParamList(‹›,List(Param(‹›,x‹38›,None,Modulefulness(None))),None)),None,None,Some(Ref(x‹38›)),‹›,Modulefulness(None),List(Debug(...)),None) -//│ | | Resolving function definition TermDefinition(Fun,member:selected_debug‹36›,term:selected_debug‹41›,List(ParamList(‹›,List(Param(‹›,x‹38›,None,Modulefulness(None))),None)),None,None,Some(Ref(x‹38›)),‹›,Modulefulness(None),List(Debug(...)),None) -//│ | | Resolving parameter x‹38› -//│ | | Traversing term: Ref(x‹38›) (expect = NonModule) -//│ | | | Resolving resolvable term: Ref(x‹38›), (prefer = NonModule, inAppPrefix = false, inCtxPrefix = false, inTyPrefix = false) -//│ | | | | Resolving symbol for term: Ref(x‹38›) (prefer = NonModule) -//│ | | | | -> (sym = Some(x‹38›), typ = None) -//│ | | | | Resolving the type for term: Ref(x‹38›) (prefer = NonModule, sym = Some(x‹38›)) -//│ | | | | | Declaration: Some(Param(‹›,x‹38›,None,Modulefulness(None))) +//│ | Resolving definition: TermDefinition(Fun,member:selected_debug‹38›,term:selected_debug‹43›,List(ParamList(‹›,List(Param(‹›,x‹40›,None,Modulefulness(None))),None)),None,None,Some(Ref(x‹40›)),‹›,Modulefulness(None),List(Debug(...)),None) +//│ | | Resolving function definition TermDefinition(Fun,member:selected_debug‹38›,term:selected_debug‹43›,List(ParamList(‹›,List(Param(‹›,x‹40›,None,Modulefulness(None))),None)),None,None,Some(Ref(x‹40›)),‹›,Modulefulness(None),List(Debug(...)),None) +//│ | | Resolving parameter x‹40› +//│ | | Traversing term: Ref(x‹40›) (expect = NonModule) +//│ | | | Resolving resolvable term: Ref(x‹40›), (prefer = NonModule, inAppPrefix = false, inCtxPrefix = false, inTyPrefix = false) +//│ | | | | Resolving symbol for term: Ref(x‹40›) (prefer = NonModule) +//│ | | | | -> (sym = Some(x‹40›), typ = None) +//│ | | | | Resolving the type for term: Ref(x‹40›) (prefer = NonModule, sym = Some(x‹40›)) +//│ | | | | | Declaration: Some(Param(‹›,x‹40›,None,Modulefulness(None))) //│ | | | | -> typ = None -//│ | | | | Resolving resolvable term Ref(x‹38›) with sym = Some(x‹38›), typ = None -//│ | | | ~> Ref(x‹38›) (sym = Some(x‹38›), typ = None) -//│ | | | Checking Ref(x‹38›): expect NonModule, evalsToModule = false, evalsToStaticClass = false -//│ Lowering.term x⁰‹38› – symbol x‹38› -//│ Expanding term Ref(x‹38›) -//│ ~> Ref(x‹38›) +//│ | | | | Resolving resolvable term Ref(x‹40›) with sym = Some(x‹40›), typ = None +//│ | | | ~> Ref(x‹40›) (sym = Some(x‹40›), typ = None) +//│ | | | Checking Ref(x‹40›): expect NonModule, evalsToModule = false, evalsToStaticClass = false +//│ Lowering.term x⁰‹40› – symbol x‹40› +//│ Expanding term Ref(x‹40›) +//│ ~> Ref(x‹40›) + + +module Main with + // The IR of `foo1` should be shown + @dbg('showOptimizedIR) + fun foo1 = 2 + 2 + // The IR of `foo2` should NOT be shown + fun foo2 = 2 + 2 +//│ ——————————————| Optimized IR |—————————————————————————————————————————————————————————————————————— +//│ @dbg(...) define foo1⁰ as fun foo1¹ { return 4 } +//│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— + +Main.foo1 +//│ = 4 #dbg('optimizations) @@ -44,6 +61,7 @@ fun shown_ir(x) = x //│ define shown_ir⁰ as fun shown_ir¹(x) { return x }; end //│ ——————————————| Optimized IR |—————————————————————————————————————————————————————————————————————— //│ define shown_ir⁰ as fun shown_ir¹(x) { return x }; end +//│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— #dbg(showIR: false, showOptimizedIR: false) @@ -56,3 +74,4 @@ fun command_shown_ir(x) = x //│ define command_shown_ir⁰ as fun command_shown_ir¹(x) { return x }; end //│ ——————————————| Optimized IR |—————————————————————————————————————————————————————————————————————— //│ define command_shown_ir⁰ as fun command_shown_ir¹(x) { return x }; end +//│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— diff --git a/hkmc2DiffTests/src/test/scala/hkmc2/JSBackendDiffMaker.scala b/hkmc2DiffTests/src/test/scala/hkmc2/JSBackendDiffMaker.scala index 5062191b8c..24f1528773 100644 --- a/hkmc2DiffTests/src/test/scala/hkmc2/JSBackendDiffMaker.scala +++ b/hkmc2DiffTests/src/test/scala/hkmc2/JSBackendDiffMaker.scala @@ -126,6 +126,27 @@ abstract class JSBackendDiffMaker extends MLsDiffMaker: Printer() val print = (p: codegen.Program) => blockPrinter.worksheet(p)(using irPrintingScp).mkString(output.ColWidth) + def showDefinitionDebugIR( + title: Str, + program: codegen.Program, + isEnabled: Config.Debug => Bool, + ): Unit = + def show(defn: Defn): Unit = + defn.configOverride.foreach: localConfig => + if isEnabled(localConfig.debug) then + val ir = blockPrinter.printDefinition(defn)(using irPrintingScp).mkString(output.ColWidth) + outputDebugSection(title, ir, localConfig.debug.out) + new BlockTraverser: + override def applyFunDefn(fun: FunDefn): Unit = + show(fun) + super.applyFunDefn(fun) + override def applyValDefn(defn: ValDefn): Unit = + show(defn) + super.applyValDefn(defn) + override def applyClsLikeDefn(defn: ClsLikeDefn): Unit = + show(defn) + super.applyClsLikeDefn(defn) + .applyProgram(program) Config.extractConfigFromStats(blk).givenIn { val loweringState = summon[Elaborator.State] @@ -181,6 +202,7 @@ abstract class JSBackendDiffMaker extends MLsDiffMaker: output(irStr) else if config.debug.showIR then outputDebugSection("Lowered IR", irStr, config.debug.out) + else showDefinitionDebugIR("Lowered IR", prog, _.showIR) super.preOptimizeHook(prog) customPipeline.run(lowered, print, symbolsToPreserve, dtl) @@ -198,6 +220,7 @@ abstract class JSBackendDiffMaker extends MLsDiffMaker: outputSeparator("Optimized IR") output(irStr) else outputDebugSection("Optimized IR", irStr, config.debug.out) + else showDefinitionDebugIR("Optimized IR", optimized, _.showOptimizedIR) if showOptimizedTree.isSet then outputSeparator("Optimized IR Tree") output(optimized.showAsTree) From d674056965fc97e83891a6f7a63039864854fd9a Mon Sep 17 00:00:00 2001 From: OpenAI Codex Date: Tue, 11 Aug 2026 12:00:05 +0800 Subject: [PATCH 04/32] Add debug tree display directives --- DBG_DIRECTIVE_PLAN.md | 3 + .../src/main/scala/hkmc2/CompilerCtx.scala | 48 ++++++++--- .../shared/src/main/scala/hkmc2/Config.scala | 17 ++++ .../src/main/scala/hkmc2/MLsCompiler.scala | 21 ++++- .../main/scala/hkmc2/semantics/Resolver.scala | 4 + .../codegen/DebugDefinitionPhases.mls | 81 ++++++++++++++++++- .../codegen/DebugParsingDirective.mls | 2 +- .../test/scala/hkmc2/JSBackendDiffMaker.scala | 45 ++++++----- .../src/test/scala/hkmc2/MLsDiffMaker.scala | 37 ++++++++- 9 files changed, 221 insertions(+), 37 deletions(-) diff --git a/DBG_DIRECTIVE_PLAN.md b/DBG_DIRECTIVE_PLAN.md index 47611b96f7..40985b8eec 100644 --- a/DBG_DIRECTIVE_PLAN.md +++ b/DBG_DIRECTIVE_PLAN.md @@ -11,6 +11,8 @@ - optimizations (`:dopt`). - Mirror the existing IR display commands with `showIR` (`:sir`) and `showOptimizedIR` (`:soir`). +- Mirror the parsed, elaborated, and lowered tree displays with `showParsedTree` + (`:pt`), `showElaboratedTree` (`:elt`), and `showLoweredTree` (`:lot`). - Accept both shorthand flags and named booleans, including: - `#dbg('parsing)`; - `#dbg(elaboration: true)`. @@ -45,3 +47,4 @@ - [x] `showIR` and `showOptimizedIR` share the existing `:sir` and `:soir` display paths. - [x] `@dbg` configuration overrides remain attached to lowered IR definitions, including methods. - [x] Definition-local IR display prints only definitions selected by `@dbg`, through the existing IR printer. +- [x] Parsed, elaborated, and lowered tree displays support both `#dbg` and definition-local `@dbg`. diff --git a/hkmc2/shared/src/main/scala/hkmc2/CompilerCtx.scala b/hkmc2/shared/src/main/scala/hkmc2/CompilerCtx.scala index 2b806a2dd0..0ec9fef142 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/CompilerCtx.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/CompilerCtx.scala @@ -146,9 +146,19 @@ class CompilerCtx( val artifactConfig = Config.extractConfigFromStats(blk0) state.compilationUnitConfig = S(artifactConfig) + if artifactConfig.debug.showElaboratedTree then + outputHandler.emit(artifactConfig.debug.out, s"Elaborated tree\n${blk0.showAsTree}") artifactConfig.givenIn: given Elaborator.State = state - val resolver = Resolver(rtl) + val resolver = new Resolver(rtl): + override protected def preTraverseDefn(defn: Definition): Unit = + if !artifactConfig.debug.showElaboratedTree then + val modifiers = defn.annotations.collect: + case Annot.Debug(modify) => modify + if modifiers.nonEmpty then + val localConfig = modifiers.foldLeft(artifactConfig)((cfg, modify) => modify(cfg)) + if localConfig.debug.showElaboratedTree then + outputHandler.emit(localConfig.debug.out, s"Elaborated tree\n${defn.showAsTree}") resolver.traverseBlock(blk0)(using Resolver.ICtx.empty) // Runs the compilation pipeline on a freshly lowered compilation unit. @@ -173,32 +183,45 @@ class CompilerCtx( codegen.Printer().worksheet(p)(using irPrintingScp).mkString(100) def showDebugIR(title: Str, program: codegen.Program): Unit = outputHandler.emit(artifactConfig.debug.out, s"${title}\n${printer(program)}") - def showDefinitionDebugIR( - title: Str, + def forEachDefinitionDebug( program: codegen.Program, isEnabled: Config.Debug => Bool, - ): Unit = - val blockPrinter = codegen.Printer() - def show(defn: codegen.Defn): Unit = + )(display: (codegen.Defn, Config) => Unit): Unit = + def visit(defn: codegen.Defn): Unit = defn.configOverride.foreach: localConfig => - if isEnabled(localConfig.debug) then - val ir = blockPrinter.printDefinition(defn)(using irPrintingScp).mkString(100) - outputHandler.emit(localConfig.debug.out, s"${title}\n${ir}") + if isEnabled(localConfig.debug) then display(defn, localConfig) new codegen.BlockTraverser: override def applyFunDefn(fun: codegen.FunDefn): Unit = - show(fun) + visit(fun) super.applyFunDefn(fun) override def applyValDefn(defn: codegen.ValDefn): Unit = - show(defn) + visit(defn) super.applyValDefn(defn) override def applyClsLikeDefn(defn: codegen.ClsLikeDefn): Unit = - show(defn) + visit(defn) super.applyClsLikeDefn(defn) .applyProgram(program) + def showDefinitionDebugIR( + title: Str, + program: codegen.Program, + isEnabled: Config.Debug => Bool, + ): Unit = + val blockPrinter = codegen.Printer() + forEachDefinitionDebug(program, isEnabled): (defn, localConfig) => + val ir = blockPrinter.printDefinition(defn)(using irPrintingScp).mkString(100) + outputHandler.emit(localConfig.debug.out, s"${title}\n${ir}") + def showDefinitionDebugTree(title: Str, program: codegen.Program): Unit = + forEachDefinitionDebug(program, _.showLoweredTree): (defn, localConfig) => + val tree = defn match + case product: Product => product.showAsTree + outputHandler.emit(localConfig.debug.out, s"${title}\n${tree}") val pipeline = new codegen.CompilationPipeline: override def extraSymbolsToPreserve(prog: codegen.Program): Set[codegen.BoundSymbol] = collectCompilationUnitSymbols(prog).toSet override def preOptimizeHook(prog: codegen.Program): Unit = + if artifactConfig.debug.showLoweredTree then + outputHandler.emit(artifactConfig.debug.out, s"Lowered IR Tree\n${prog.showAsTree}") + else showDefinitionDebugTree("Lowered IR Tree", prog) if artifactConfig.debug.showIR then showDebugIR("Lowered IR", prog) else showDefinitionDebugIR("Lowered IR", prog, _.showIR) val optimized = ltl.givenIn: @@ -271,6 +294,7 @@ class CompilerCtx( given Elaborator.State = state given Config = rootConfig given CompilerCtx = this + given DebugPrinter = new DebugPrinter val outputHandler = DebugOutputHandler(fs, rootConfig.baseDir, println) val parse = ParserSetup(file, dbgParsing, outputHandler) val phaseConfig = parse.effectiveConfig diff --git a/hkmc2/shared/src/main/scala/hkmc2/Config.scala b/hkmc2/shared/src/main/scala/hkmc2/Config.scala index f48b982efd..087fba58d9 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/Config.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/Config.scala @@ -136,9 +136,12 @@ object Config: case class Debug( parsing: Bool, + showParsedTree: Bool, elaboration: Bool, + showElaboratedTree: Bool, resolution: Bool, lowering: Bool, + showLoweredTree: Bool, optimizations: Bool, showIR: Bool, showOptimizedIR: Bool, @@ -147,9 +150,12 @@ object Config: object Debug: val default = Debug( parsing = false, + showParsedTree = false, elaboration = false, + showElaboratedTree = false, resolution = false, lowering = false, + showLoweredTree = false, optimizations = false, showIR = false, showOptimizedIR = false, @@ -520,9 +526,12 @@ object ConfigParser: private def updateDebugFlag(name: Str, value: Bool, tree: Tree)(using Raise): Config.Debug => Config.Debug = name match case "parsing" => _.copy(parsing = value) + case "showParsedTree" => _.copy(showParsedTree = value) case "elaboration" => _.copy(elaboration = value) + case "showElaboratedTree" => _.copy(showElaboratedTree = value) case "resolution" => _.copy(resolution = value) case "lowering" => _.copy(lowering = value) + case "showLoweredTree" => _.copy(showLoweredTree = value) case "optimizations" => _.copy(optimizations = value) case "showIR" => _.copy(showIR = value) case "showOptimizedIR" => _.copy(showOptimizedIR = value) @@ -573,6 +582,14 @@ object ConfigParser: given Raise = _ => () extractDebugFromTrees(trees) + /** Apply `@dbg(...)` syntax annotations without reporting diagnostics during the discovery parse. */ + def discoverDebugFromAnnotations(annotations: Ls[Tree], base: Config): Opt[Config] = + given Raise = _ => () + val modifiers = annotations.collect: + case App(Ident("dbg"), Tup(args)) => parseDebugDirective(args) + if modifiers.isEmpty then N + else S(modifiers.foldLeft(base)((cfg, modify) => modify(cfg))) + /** Parse the `None`/`Some(...)` syntax for optional config fields. * Also accepts unwrapped values as a convenience (treated as `Some(value)`). */ private def parseOpt[A](tree: Tree)(parseInner: Tree => Opt[A])(using Raise): Opt[Opt[A]] = tree match diff --git a/hkmc2/shared/src/main/scala/hkmc2/MLsCompiler.scala b/hkmc2/shared/src/main/scala/hkmc2/MLsCompiler.scala index fc644bde2f..5cc1d70809 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/MLsCompiler.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/MLsCompiler.scala @@ -13,7 +13,7 @@ import semantics.Elaborator.{Ctx, State} class ParserSetup(file: io.Path, forceDebugParsing: Bool, outputHandler: DebugOutputHandler) -(using state: Elaborator.State, raise: Raise, cctx: CompilerCtx, config: Config): +(using state: Elaborator.State, raise: Raise, cctx: CompilerCtx, config: Config, debugPrinter: DebugPrinter): val block = cctx.fs.read(file) val fph = new FastParseHelpers(block) @@ -35,6 +35,25 @@ class ParserSetup(file: io.Path, forceDebugParsing: Bool, outputHandler: DebugOu if forceDebugParsing || effectiveConfig.debug.parsing then parse(dbg = true, effectiveConfig.debug.out) else discoveryResult + + private def showSelectedParsedTrees(trees: Ls[syntax.Tree]): Unit = + def visit(tree: syntax.Tree): Unit = tree match + case syntax.PossiblyAnnotated(annotations, target) => + ConfigParser.discoverDebugFromAnnotations(annotations, effectiveConfig).foreach: localConfig => + if localConfig.debug.showParsedTree then + outputHandler.emit(localConfig.debug.out, s"Parsed tree\n${tree.showAsTree}") + target.children.foreach: + case child: syntax.Tree => visit(child) + case _ => () + case _ => () + trees.foreach(visit) + + if effectiveConfig.debug.showParsedTree then + outputHandler.emit( + effectiveConfig.debug.out, + s"Parsed tree\n${result.map(_.showAsTree).mkString("\n")}", + ) + else showSelectedParsedTrees(result) val resultBlk = new syntax.Tree.Block(result) diff --git a/hkmc2/shared/src/main/scala/hkmc2/semantics/Resolver.scala b/hkmc2/shared/src/main/scala/hkmc2/semantics/Resolver.scala index 5e7fd3064e..d20f2740e3 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/semantics/Resolver.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/semantics/Resolver.scala @@ -355,8 +355,12 @@ class Resolver(tl: TraceLogger) :: expect.message)) end traverse + + /** Hook for consumers that need to observe each definition before resolution mutates its terms. */ + protected def preTraverseDefn(defn: Definition): Unit = () def traverseDefn(defn: Definition)(using ICtx): ICtx = + preTraverseDefn(defn) val modifiers = defn.annotations.collect: case Annot.Debug(modify) => modify if modifiers.isEmpty then traverseDefnImpl(defn) diff --git a/hkmc2/shared/src/test/mlscript/codegen/DebugDefinitionPhases.mls b/hkmc2/shared/src/test/mlscript/codegen/DebugDefinitionPhases.mls index 5ea9dc9058..27f33ad7fe 100644 --- a/hkmc2/shared/src/test/mlscript/codegen/DebugDefinitionPhases.mls +++ b/hkmc2/shared/src/test/mlscript/codegen/DebugDefinitionPhases.mls @@ -24,10 +24,48 @@ fun unrelated(x) = x module Main with // The IR of `foo1` should be shown - @dbg('showOptimizedIR) + @dbg('showParsedTree, 'showElaboratedTree, 'showLoweredTree, 'showOptimizedIR) fun foo1 = 2 + 2 // The IR of `foo2` should NOT be shown fun foo2 = 2 + 2 +//│ ———————————————| Parsed tree |—————————————————————————————————————————————————————————————————————— +//│ Annotated: +//│ annotation = App: +//│ lhs = Ident of "dbg" +//│ rhs = Tup of Ls of +//│ Ident of "'showParsedTree" +//│ Ident of "'showElaboratedTree" +//│ Ident of "'showLoweredTree" +//│ Ident of "'showOptimizedIR" +//│ target = TermDef: +//│ k = Fun +//│ head = Ident of "foo1" +//│ rhs = S of OpApp: +//│ lhs = IntLit of 2 +//│ op = Ident of "+" +//│ rhss = Ls of +//│ IntLit of 2 +//│ —————————————| Elaborated tree |———————————————————————————————————————————————————————————————————— +//│ TermDefinition: +//│ k = Fun +//│ sym = member:foo1⁰ +//│ tsym = term:Main⁰/foo1¹ +//│ body = S of App: +//│ lhs = Ref{sym=builtin:+⁰} of builtin:+⁰ +//│ rhs = Tup of Ls of +//│ Fld: +//│ term = Lit of IntLit of 2 +//│ Fld: +//│ term = Lit of IntLit of 2 +//│ modulefulness = Modulefulness of N +//│ annotations = Ls of +//│ Debug of ... +//│ —————————————| Lowered IR Tree |———————————————————————————————————————————————————————————————————— +//│ FunDefn: +//│ owner = S of module:Main⁰ +//│ sym = member:foo1⁰ +//│ dSym = term:Main⁰/foo1¹ +//│ body = Return of Lit of IntLit of 4 //│ ——————————————| Optimized IR |—————————————————————————————————————————————————————————————————————— //│ @dbg(...) define foo1⁰ as fun foo1¹ { return 4 } //│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— @@ -36,6 +74,47 @@ Main.foo1 //│ = 4 +#dbg('showParsedTree, 'showElaboratedTree, 'showLoweredTree) +fun shown_trees = 42 +//│ ———————————————| Parsed tree |—————————————————————————————————————————————————————————————————————— +//│ App: +//│ lhs = Directive: +//│ prefix = Ident of "dbg" +//│ body = Tup of Nil +//│ rhs = Tup of Ls of +//│ Ident of "'showParsedTree" +//│ Ident of "'showElaboratedTree" +//│ Ident of "'showLoweredTree" +//│ TermDef: +//│ k = Fun +//│ head = Ident of "shown_trees" +//│ rhs = S of IntLit of 42 +//│ —————————————| Elaborated tree |———————————————————————————————————————————————————————————————————— +//│ Blk: +//│ stats = Ls of +//│ SetConfig of ... +//│ TermDefinition: +//│ k = Fun +//│ sym = member:shown_trees⁰ +//│ tsym = term:shown_trees¹ +//│ body = S of Lit of IntLit of 42 +//│ modulefulness = Modulefulness of N +//│ res = Lit of UnitLit of false +//│ —————————————| Lowered IR Tree |———————————————————————————————————————————————————————————————————— +//│ Program: +//│ main = Define: +//│ defn = FunDefn: +//│ sym = member:shown_trees⁰ +//│ dSym = term:shown_trees¹ +//│ params = Ls of +//│ ParamList: ‹empty› +//│ body = Return of Lit of IntLit of 42 +//│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— + + +#dbg(showParsedTree: false, showElaboratedTree: false, showLoweredTree: false) + + #dbg('optimizations) fun optimization_debug(x) = x //│ ⬤ Simplif. iter. 1 diff --git a/hkmc2/shared/src/test/mlscript/codegen/DebugParsingDirective.mls b/hkmc2/shared/src/test/mlscript/codegen/DebugParsingDirective.mls index bd26e91e5c..a30daef4fc 100644 --- a/hkmc2/shared/src/test/mlscript/codegen/DebugParsingDirective.mls +++ b/hkmc2/shared/src/test/mlscript/codegen/DebugParsingDirective.mls @@ -3,7 +3,7 @@ #dbg('parsing) //│ ┊#┊dbg┊(⟨┊'parsing┊⟩)┊ -//│ @ blockOf(start of expression,allowNewlines = true) [at syntax/Parser.scala:412] +//│ @ blockOf(start of expression,allowNewlines = true) [at syntax/Parser.scala:418] //│ │ ? blockOfImpl inspects ┊#┊dbg┊(⟨┊'parsing┊⟩)┊ [at syntax/Parser.scala:331] //│ │ ! blockOfImpl consumes ┊#┊ [at syntax/Parser.scala:358] //│ │ @ parseRule(39,'#' directive keyword ::= Expr('#' directive body ::= End()),allowNewlines = true) [at syntax/Parser.scala:371] diff --git a/hkmc2DiffTests/src/test/scala/hkmc2/JSBackendDiffMaker.scala b/hkmc2DiffTests/src/test/scala/hkmc2/JSBackendDiffMaker.scala index 24f1528773..8db0802c0d 100644 --- a/hkmc2DiffTests/src/test/scala/hkmc2/JSBackendDiffMaker.scala +++ b/hkmc2DiffTests/src/test/scala/hkmc2/JSBackendDiffMaker.scala @@ -65,12 +65,6 @@ abstract class JSBackendDiffMaker extends MLsDiffMaker: override def doTrace = showRepl.isSet override def emitDbg(str: String): Unit = output(str) - private def outputDebugSection(title: Str, contents: Str, out: Config.DebugOutput): Unit = out match - case Config.DebugOutput.StdIO => - outputSeparator(title) - output(contents) - case _ => debugOutputHandler.emit(out, s"${title}\n${contents}") - lazy val host = hostCreated = true given TL = replTL @@ -126,27 +120,37 @@ abstract class JSBackendDiffMaker extends MLsDiffMaker: Printer() val print = (p: codegen.Program) => blockPrinter.worksheet(p)(using irPrintingScp).mkString(output.ColWidth) - def showDefinitionDebugIR( - title: Str, + def forEachDefinitionDebug( program: codegen.Program, isEnabled: Config.Debug => Bool, - ): Unit = - def show(defn: Defn): Unit = + )(display: (Defn, Config) => Unit): Unit = + def visit(defn: Defn): Unit = defn.configOverride.foreach: localConfig => - if isEnabled(localConfig.debug) then - val ir = blockPrinter.printDefinition(defn)(using irPrintingScp).mkString(output.ColWidth) - outputDebugSection(title, ir, localConfig.debug.out) + if isEnabled(localConfig.debug) then display(defn, localConfig) new BlockTraverser: override def applyFunDefn(fun: FunDefn): Unit = - show(fun) + visit(fun) super.applyFunDefn(fun) override def applyValDefn(defn: ValDefn): Unit = - show(defn) + visit(defn) super.applyValDefn(defn) override def applyClsLikeDefn(defn: ClsLikeDefn): Unit = - show(defn) + visit(defn) super.applyClsLikeDefn(defn) .applyProgram(program) + def showDefinitionDebugIR( + title: Str, + program: codegen.Program, + isEnabled: Config.Debug => Bool, + ): Unit = + forEachDefinitionDebug(program, isEnabled): (defn, localConfig) => + val ir = blockPrinter.printDefinition(defn)(using irPrintingScp).mkString(output.ColWidth) + outputDebugSection(title, ir, localConfig.debug.out) + def showDefinitionDebugTree(title: Str, program: codegen.Program): Unit = + forEachDefinitionDebug(program, _.showLoweredTree): (defn, localConfig) => + val tree = defn match + case product: Product => product.showAsTree + outputDebugSection(title, tree, localConfig.debug.out) Config.extractConfigFromStats(blk).givenIn { val loweringState = summon[Elaborator.State] @@ -185,9 +189,12 @@ abstract class JSBackendDiffMaker extends MLsDiffMaker: } rec(before.main, after.main) override def preOptimizeHook(prog: Program) = - if showLoweredTree.isSet then - outputSeparator("Lowered IR Tree") - output(prog.showAsTree) + if showLoweredTree.isSet || config.debug.showLoweredTree then + if showLoweredTree.isSet then + outputSeparator("Lowered IR Tree") + output(prog.showAsTree) + else outputDebugSection("Lowered IR Tree", prog.showAsTree, config.debug.out) + else showDefinitionDebugTree("Lowered IR Tree", prog) if showIR.isSet || showIRLines.isSet || config.debug.showIR then given ShowCfg = ShowCfg( showExpansionMappings = false, diff --git a/hkmc2DiffTests/src/test/scala/hkmc2/MLsDiffMaker.scala b/hkmc2DiffTests/src/test/scala/hkmc2/MLsDiffMaker.scala index c56918634b..1132f8dae4 100644 --- a/hkmc2DiffTests/src/test/scala/hkmc2/MLsDiffMaker.scala +++ b/hkmc2DiffTests/src/test/scala/hkmc2/MLsDiffMaker.scala @@ -228,6 +228,12 @@ abstract class MLsDiffMaker extends DiffMaker: protected var activeDebug = Config.Debug.default protected lazy val debugOutputHandler = DebugOutputHandler(cctx.fs, wd, str => output(str)) + protected def outputDebugSection(title: Str, contents: Str, out: Config.DebugOutput): Unit = out match + case Config.DebugOutput.StdIO => + outputSeparator(title) + output(contents) + case _ => debugOutputHandler.emit(out, s"${title}\n${contents}") + given Elaborator.State = new Elaborator.State: override protected def doDbg: Bool = dbgParsing.isSet @@ -418,14 +424,29 @@ abstract class MLsDiffMaker extends DiffMaker: if dbgParsing.isSet || phaseConfig.debug.parsing then parse(dbg = true, phaseConfig.debug.out) else discoveryResult + + def showSelectedParsedTrees(): Unit = + def visit(tree: syntax.Tree): Unit = tree match + case syntax.PossiblyAnnotated(annotations, target) => + ConfigParser.discoverDebugFromAnnotations(annotations, phaseConfig).foreach: localConfig => + if localConfig.debug.showParsedTree then + outputDebugSection("Parsed tree", tree.showAsTree, localConfig.debug.out) + target.children.foreach: + case child: syntax.Tree => visit(child) + case _ => () + case _ => () + res.foreach(visit) // If parsed tree is displayed, don't show the string serialization. - if (parseOnly.isSet || showParse.isSet) && !showParsedTree.isSet then + if (parseOnly.isSet || showParse.isSet) && !showParsedTree.isSet && !phaseConfig.debug.showParsedTree then output(s"Parsed:${res.map("\n\t"+_.showDbg).mkString}") if showParsedTree.isSet then outputSeparator(s"Parsed tree") res.foreach(t => output(t.showAsTree)) + else if phaseConfig.debug.showParsedTree then + outputDebugSection("Parsed tree", res.map(_.showAsTree).mkString("\n"), phaseConfig.debug.out) + else showSelectedParsedTrees() // if showParse.isSet then // output(s"AST: $res") @@ -463,11 +484,13 @@ abstract class MLsDiffMaker extends DiffMaker: case _ => () // If elaborated tree is displayed, don't show the string serialization. - if (showElab.isSet || debug.isSet) && !showElaboratedTree.isSet then + if (showElab.isSet || debug.isSet) && !showElaboratedTree.isSet && !phaseConfig.debug.showElaboratedTree then output(s"Elab: ${e.showDbg}") if showElaboratedTree.isSet then outputSeparator(s"Elaborated tree") output(e.showAsTree) + else if phaseConfig.debug.showElaboratedTree then + outputDebugSection("Elaborated tree", e.showAsTree, phaseConfig.debug.out) processTerm(e, inImport = false) @@ -480,7 +503,15 @@ abstract class MLsDiffMaker extends DiffMaker: if file.toString =/= runtimeSourceFile.toString && file.toString =/= preludeFile.toString then summon[Elaborator.State].initRuntimeSymbolsFromFile(runtimeSourceFile, prelude)( using summon[TL], summon[Raise], cctx) - val resolver = Resolver(rtl) + val resolver = new Resolver(rtl): + override protected def preTraverseDefn(defn: semantics.Definition): Unit = + if showElaboratedTree.isUnset && !config.debug.showElaboratedTree then + val modifiers = defn.annotations.collect: + case semantics.Annot.Debug(modify) => modify + if modifiers.nonEmpty then + val localConfig = modifiers.foldLeft(config)((cfg, modify) => modify(cfg)) + if localConfig.debug.showElaboratedTree then + outputDebugSection("Elaborated tree", defn.showAsTree, localConfig.debug.out) curICtx = resolver.traverseBlock(trm)(using curICtx) if showResolve.isSet then From 11d383e004b56fc49d4b3b6f15372bf561aac540 Mon Sep 17 00:00:00 2001 From: OpenAI Codex Date: Sat, 15 Aug 2026 14:47:35 +0800 Subject: [PATCH 05/32] Add debug flag to hide symbol UIDs --- .../src/main/scala/hkmc2/CompilerCtx.scala | 22 ++++++++++--- .../shared/src/main/scala/hkmc2/Config.scala | 3 ++ .../main/scala/hkmc2/codegen/Lowering.scala | 4 +-- .../scala/hkmc2/semantics/Elaborator.scala | 17 +++++----- .../main/scala/hkmc2/semantics/Resolver.scala | 2 +- .../codegen/DebugDefinitionPhases.mls | 31 ++++++++++--------- .../test/mlscript/codegen/DebugDirective.mls | 14 ++++----- .../codegen/DebugParsingDirective.mls | 2 +- .../src/test/scala/hkmc2/MLsDiffMaker.scala | 4 ++- 9 files changed, 60 insertions(+), 39 deletions(-) diff --git a/hkmc2/shared/src/main/scala/hkmc2/CompilerCtx.scala b/hkmc2/shared/src/main/scala/hkmc2/CompilerCtx.scala index 8dd0c6b06d..cae43719ef 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/CompilerCtx.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/CompilerCtx.scala @@ -76,8 +76,10 @@ class CompilerCtx( val dependencies = new CompilerCtx.DependencyRecorder val modulePath = (file.up / io.RelPath(file.baseName + ".mjs")).toString var stateDebug = false + var stateShowUids = true val state = new Elaborator.State: override protected def doDbg: Bool = stateDebug + override protected def doShowUids: Bool = stateShowUids given Elaborator.State = state given Config = rootConfig @@ -94,8 +96,8 @@ class CompilerCtx( given CompilerCtx = this ParserSetup(file, forceDebugParsing, outputHandler) val phaseConfig = parse.effectiveConfig - stateDebug = phaseConfig.debug.parsing || phaseConfig.debug.elaboration || - phaseConfig.debug.resolution || phaseConfig.debug.lowering || phaseConfig.debug.optimizations + stateDebug = CompilerCtx.debugTracingEnabled(phaseConfig.debug) + stateShowUids = phaseConfig.debug.showUids def traceLogger(enabled: Bool) = new TraceLogger: override def doTrace: Bool = enabled override protected def defaultDebugOutput: Config.DebugOutput = phaseConfig.debug.out @@ -108,7 +110,7 @@ class CompilerCtx( given Elaborator.Ctx = prelude val artifactCtx = derive(parse.origin.fileName, dependencies) - val elab = + val elab = phaseConfig.givenIn: given CompilerCtx = artifactCtx Elaborator(etl, file.up, prelude) @@ -297,7 +299,11 @@ class CompilerCtx( s"Cached prelude for $file was elaborated under a different root configuration") !forceDebugParsing && art.lastChangedTimestamp >= lastMod, create = - val state = new Elaborator.State + var stateDebug = false + var stateShowUids = true + val state = new Elaborator.State: + override protected def doDbg: Bool = stateDebug + override protected def doShowUids: Bool = stateShowUids given Elaborator.State = state given Config = rootConfig given CompilerCtx = this @@ -305,12 +311,15 @@ class CompilerCtx( val outputHandler = DebugOutputHandler(fs, rootConfig.baseDir, println) val parse = ParserSetup(file, forceDebugParsing, outputHandler) val phaseConfig = parse.effectiveConfig + stateDebug = CompilerCtx.debugTracingEnabled(phaseConfig.debug) + stateShowUids = phaseConfig.debug.showUids val etl = new TraceLogger: override def doTrace: Bool = phaseConfig.debug.elaboration override protected def defaultDebugOutput: Config.DebugOutput = phaseConfig.debug.out override protected[hkmc2] def emitDbg(str: Str, out: Config.DebugOutput): Unit = outputHandler.emit(out, str) - val elab = Elaborator(etl, file.up, Ctx.empty) + val elab = phaseConfig.givenIn: + Elaborator(etl, file.up, Ctx.empty) val initCtx = State.init.nestLocal("prelude") val (blk, ctx) = elab.importFrom(parse.resultBlk)(using initCtx) PreludeArtifact(parse.resultBlk, blk, ctx, state, rootConfig, lastMod), @@ -321,6 +330,9 @@ object CompilerCtx: inline def get(using cctx: CompilerCtx) = cctx + private def debugTracingEnabled(debug: Config.Debug): Bool = + debug.parsing || debug.elaboration || debug.resolution || debug.lowering || debug.optimizations + /** Collect import provenance after elaboration has assembled all user and synthetic imports. * diff --git a/hkmc2/shared/src/main/scala/hkmc2/Config.scala b/hkmc2/shared/src/main/scala/hkmc2/Config.scala index 087fba58d9..776aaa5c37 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/Config.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/Config.scala @@ -145,6 +145,7 @@ object Config: optimizations: Bool, showIR: Bool, showOptimizedIR: Bool, + showUids: Bool, out: DebugOutput, ) object Debug: @@ -159,6 +160,7 @@ object Config: optimizations = false, showIR = false, showOptimizedIR = false, + showUids = true, out = DebugOutput.StdIO, ) @@ -535,6 +537,7 @@ object ConfigParser: case "optimizations" => _.copy(optimizations = value) case "showIR" => _.copy(showIR = value) case "showOptimizedIR" => _.copy(showOptimizedIR = value) + case "showUids" => _.copy(showUids = value) case _ => raise(ErrorReport( msg"Unknown debugging aspect '${name}'" -> tree.toLoc :: Nil, diff --git a/hkmc2/shared/src/main/scala/hkmc2/codegen/Lowering.scala b/hkmc2/shared/src/main/scala/hkmc2/codegen/Lowering.scala index 497e97a7f5..a1c6591e9e 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/codegen/Lowering.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/codegen/Lowering.scala @@ -283,7 +283,7 @@ class Lowering()(using Config, TL, Raise, State, Ctx, SymbolPrinter): val cfgOverride = definitionConfigOverride(td.extraAnnotations) definitionDebugConfig(td.extraAnnotations) match case S(localConfig) => - State.scopedDebug(localConfig.debug.lowering): + State.scopedDebug(localConfig.debug.lowering, localConfig.debug.showUids): tl.scopedDebug(localConfig.debug.lowering, localConfig.debug.out): subTerm_nonTail(bod)(r => tl.inOuterDebugScope: @@ -297,7 +297,7 @@ class Lowering()(using Config, TL, Raise, State, Ctx, SymbolPrinter): val cfgOverride = definitionConfigOverride(td.extraAnnotations) val (paramLists, bodyBlock) = definitionDebugConfig(td.extraAnnotations) match case S(localConfig) => - State.scopedDebug(localConfig.debug.lowering): + State.scopedDebug(localConfig.debug.lowering, localConfig.debug.showUids): tl.scopedDebug(localConfig.debug.lowering, localConfig.debug.out): setupFunctionOrByNameDef(td.params, bod, S(td.sym.nme)) case N => setupFunctionOrByNameDef(td.params, bod, S(td.sym.nme)) diff --git a/hkmc2/shared/src/main/scala/hkmc2/semantics/Elaborator.scala b/hkmc2/shared/src/main/scala/hkmc2/semantics/Elaborator.scala index 9ca25c8505..9ce3a787c0 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/semantics/Elaborator.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/semantics/Elaborator.scala @@ -519,17 +519,20 @@ object Elaborator: "globalThis" -> globalThisSymbol, )) val superSymbol = builtinOpsMap("super") - private var debugOverrides = Ls.empty[Bool] + // Definition-local debug annotations may independently change whether tracing is active and + // whether symbol identities are useful in that trace, so both settings share the same scope. + private var debugOverrides = Ls.empty[(Bool, Bool)] protected def doDbg: Bool = false - final def dbg: Bool = debugOverrides.headOption.getOrElse(doDbg) - def scopedDebug[T](enabled: Bool)(thunk: => T): T = - debugOverrides ::= enabled + protected def doShowUids: Bool = true + final def dbg: Bool = debugOverrides.headOption.map(_._1).getOrElse(doDbg) + final def showUids: Bool = debugOverrides.headOption.map(_._2).getOrElse(doShowUids) + def scopedDebug[T](enabled: Bool, showUids: Bool)(thunk: => T): T = + debugOverrides ::= enabled -> showUids try thunk finally debugOverrides = debugOverrides.tail def dbgRefNum(num: Int): Str = if dbg then s"#$num" else "" def dbgUid(uid: Uid[Symbol]): Str = - if dbg then s"‹$uid›" else "" - // ^ we do not display the uid by default to avoid polluting diff-test outputs + if dbg && showUids then s"‹$uid›" else "" transparent inline def State(using state: State): State = state /** Extracts all parameter lists from a `constructor(...)...` declaration. @@ -571,7 +574,7 @@ extends Importer: if modifiers.isEmpty then thunk else val localConfig = modifiers.foldLeft(config)((cfg, modify) => modify(cfg)) - state.scopedDebug(localConfig.debug.elaboration): + state.scopedDebug(localConfig.debug.elaboration, localConfig.debug.showUids): tl.scopedDebug(localConfig.debug.elaboration, localConfig.debug.out)(thunk) lazy val illegalMemberNameTail = diff --git a/hkmc2/shared/src/main/scala/hkmc2/semantics/Resolver.scala b/hkmc2/shared/src/main/scala/hkmc2/semantics/Resolver.scala index d20f2740e3..45457319ee 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/semantics/Resolver.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/semantics/Resolver.scala @@ -366,7 +366,7 @@ class Resolver(tl: TraceLogger) if modifiers.isEmpty then traverseDefnImpl(defn) else val localConfig = modifiers.foldLeft(cfg)((config, modify) => modify(config)) - state.scopedDebug(localConfig.debug.resolution): + state.scopedDebug(localConfig.debug.resolution, localConfig.debug.showUids): tl.scopedDebug(localConfig.debug.resolution, localConfig.debug.out): traverseDefnImpl(defn) diff --git a/hkmc2/shared/src/test/mlscript/codegen/DebugDefinitionPhases.mls b/hkmc2/shared/src/test/mlscript/codegen/DebugDefinitionPhases.mls index b9d0716a2c..4dd7424586 100644 --- a/hkmc2/shared/src/test/mlscript/codegen/DebugDefinitionPhases.mls +++ b/hkmc2/shared/src/test/mlscript/codegen/DebugDefinitionPhases.mls @@ -1,25 +1,26 @@ :js +#dbg(showUids: false) @dbg('resolution, 'lowering) fun selected_debug(x) = x fun unrelated(x) = x -//│ | Resolving definition: TermDefinition(Fun,member:selected_debug‹40›,term:selected_debug‹45›,List(ParamList(‹›,List(Param(‹›,x‹42›,None,Modulefulness(None))),None)),None,None,Some(Ref(x‹42›)),‹›,Modulefulness(None),List(Debug(...)),None) -//│ | | Resolving function definition TermDefinition(Fun,member:selected_debug‹40›,term:selected_debug‹45›,List(ParamList(‹›,List(Param(‹›,x‹42›,None,Modulefulness(None))),None)),None,None,Some(Ref(x‹42›)),‹›,Modulefulness(None),List(Debug(...)),None) -//│ | | Resolving parameter x‹42› -//│ | | Traversing term: Ref(x‹42›) (expect = NonModule) -//│ | | | Resolving resolvable term: Ref(x‹42›), (prefer = NonModule, inAppPrefix = false, inCtxPrefix = false, inTyPrefix = false) -//│ | | | | Resolving symbol for term: Ref(x‹42›) (prefer = NonModule) -//│ | | | | -> (sym = Some(x‹42›), typ = None) -//│ | | | | Resolving the type for term: Ref(x‹42›) (prefer = NonModule, sym = Some(x‹42›)) -//│ | | | | | Declaration: Some(Param(‹›,x‹42›,None,Modulefulness(None))) +//│ | Resolving definition: TermDefinition(Fun,member:selected_debug,term:selected_debug,List(ParamList(‹›,List(Param(‹›,x,None,Modulefulness(None))),None)),None,None,Some(Ref(x)),‹›,Modulefulness(None),List(Debug(...)),None) +//│ | | Resolving function definition TermDefinition(Fun,member:selected_debug,term:selected_debug,List(ParamList(‹›,List(Param(‹›,x,None,Modulefulness(None))),None)),None,None,Some(Ref(x)),‹›,Modulefulness(None),List(Debug(...)),None) +//│ | | Resolving parameter x +//│ | | Traversing term: Ref(x) (expect = NonModule) +//│ | | | Resolving resolvable term: Ref(x), (prefer = NonModule, inAppPrefix = false, inCtxPrefix = false, inTyPrefix = false) +//│ | | | | Resolving symbol for term: Ref(x) (prefer = NonModule) +//│ | | | | -> (sym = Some(x), typ = None) +//│ | | | | Resolving the type for term: Ref(x) (prefer = NonModule, sym = Some(x)) +//│ | | | | | Declaration: Some(Param(‹›,x,None,Modulefulness(None))) //│ | | | | -> typ = None -//│ | | | | Resolving resolvable term Ref(x‹42›) with sym = Some(x‹42›), typ = None -//│ | | | ~> Ref(x‹42›) (sym = Some(x‹42›), typ = None) -//│ | | | Checking Ref(x‹42›): expect NonModule, evalsToModule = false, evalsToStaticClass = false -//│ Lowering.term x⁰‹42› – symbol x‹42› -//│ Expanding term Ref(x‹42›) -//│ ~> Ref(x‹42›) +//│ | | | | Resolving resolvable term Ref(x) with sym = Some(x), typ = None +//│ | | | ~> Ref(x) (sym = Some(x), typ = None) +//│ | | | Checking Ref(x): expect NonModule, evalsToModule = false, evalsToStaticClass = false +//│ Lowering.term x⁰ – symbol x +//│ Expanding term Ref(x) +//│ ~> Ref(x) module Main with diff --git a/hkmc2/shared/src/test/mlscript/codegen/DebugDirective.mls b/hkmc2/shared/src/test/mlscript/codegen/DebugDirective.mls index 876fd70faa..2984a2428e 100644 --- a/hkmc2/shared/src/test/mlscript/codegen/DebugDirective.mls +++ b/hkmc2/shared/src/test/mlscript/codegen/DebugDirective.mls @@ -2,14 +2,14 @@ // Shorthand annotation debugging is scoped to the annotated definition. -@dbg('elaboration) fun debug_me(x) = x +@dbg('elaboration, showUids: false) fun debug_me(x) = x fun do_not_debug_me(x) = x //│ Processing term definition App(Ident(debug_me),Tup(List(Ident(x)))) //│ Elaborating ParamTree: ParamTree(‹›,Ident(x),None,None,Set()) //│ Elab term Ident(x) //│ | Elab subterm Ident(x) -//│ | ~> Ref(x‹40›) -//│ ~> Ref(x‹40›) +//│ | ~> Ref(x) +//│ ~> Ref(x) // Named flags can be explicitly disabled, and StdIO is the default destination. @@ -25,7 +25,7 @@ fun file_debug_config(x) = x // A top-level directive persists into the following block. -#dbg(elaboration: true) +#dbg(elaboration: true, showUids: false) //│ Elab block List(App(Directive(Ident(dbg),Tup(List())),Tup(List(InfixApp(Ident(elaboration),Keywrd(keyword ':'),[...] LocalScope(block:6) //│ ~> Blk(List(SetConfig(...)),Lit(UnitLit(false))) @@ -35,8 +35,8 @@ fun persistently_debugged(x) = x //│ | Elaborating ParamTree: ParamTree(‹›,Ident(x),None,None,Set()) //│ | Elab term Ident(x) //│ | | Elab subterm Ident(x) -//│ | | ~> Ref(x‹59›) -//│ | ~> Ref(x‹59›) -//│ ~> Blk(List(TermDefinition(Fun,member:persistently_debugged‹58›,term:persistently_debugged‹62›,List(ParamList(‹›,List(Param(‹›,x‹59›,None,Modulefulness(None))),None)),None,None,Some(Ref(x‹59›)),‹›,Modulefulness(None),List(),None)),Lit(UnitLit(false))) +//│ | | ~> Ref(x) +//│ | ~> Ref(x) +//│ ~> Blk(List(TermDefinition(Fun,member:persistently_debugged,term:persistently_debugged,List(ParamList(‹›,List(Param(‹›,x,None,Modulefulness(None))),None)),None,None,Some(Ref(x)),‹›,Modulefulness(None),List(),None)),Lit(UnitLit(false))) #dbg(elaboration: false) diff --git a/hkmc2/shared/src/test/mlscript/codegen/DebugParsingDirective.mls b/hkmc2/shared/src/test/mlscript/codegen/DebugParsingDirective.mls index bd48489e80..e8d336b3da 100644 --- a/hkmc2/shared/src/test/mlscript/codegen/DebugParsingDirective.mls +++ b/hkmc2/shared/src/test/mlscript/codegen/DebugParsingDirective.mls @@ -3,7 +3,7 @@ #dbg('parsing) //│ ┊#┊dbg┊(⟨┊'parsing┊⟩)┊ -//│ @ blockOf(start of expression,allowNewlines = true) [at syntax/Parser.scala:418] +//│ @ blockOf(start of expression,allowNewlines = true) [at syntax/Parser.scala:419] //│ │ ? blockOfImpl inspects ┊#┊dbg┊(⟨┊'parsing┊⟩)┊ [at syntax/Parser.scala:333] //│ │ ! blockOfImpl consumes ┊#┊ [at syntax/Parser.scala:360] //│ │ @ parseRule(39,'#' directive keyword ::= Expr('#' directive body ::= End()),allowNewlines = true) [at syntax/Parser.scala:373] diff --git a/hkmc2DiffTests/src/test/scala/hkmc2/MLsDiffMaker.scala b/hkmc2DiffTests/src/test/scala/hkmc2/MLsDiffMaker.scala index bf37653069..7eb631e6be 100644 --- a/hkmc2DiffTests/src/test/scala/hkmc2/MLsDiffMaker.scala +++ b/hkmc2DiffTests/src/test/scala/hkmc2/MLsDiffMaker.scala @@ -245,6 +245,7 @@ abstract class MLsDiffMaker extends DiffMaker: || activeDebug.lowering || activeDebug.optimizations || debug.isSet + override protected def doShowUids: Bool = activeDebug.showUids protected lazy val dbgScp: utils.Scope = // for unique symbol debug-printing only @@ -466,7 +467,8 @@ abstract class MLsDiffMaker extends DiffMaker: def processTrees(trees: Ls[syntax.Tree])(using Config, Raise): Unit = val phaseConfig = ConfigParser.discoverDebugFromTrees(trees) activeDebug = phaseConfig.debug - val elab = Elaborator(etl, file.up, prelude) + val elab = phaseConfig.givenIn: + Elaborator(etl, file.up, prelude) // val blockSymbol = // semantics.TopLevelSymbol("block#"+blockNum) blockNum += 1 From 3084bd1976bd075fcbea686bf6ad97adfd21325b Mon Sep 17 00:00:00 2001 From: Lionel Parreaux Date: Fri, 16 Oct 2026 13:16:23 +0800 Subject: [PATCH 06/32] Minor --- hkmc2/shared/src/main/scala/hkmc2/codegen/Lowering.scala | 1 - 1 file changed, 1 deletion(-) diff --git a/hkmc2/shared/src/main/scala/hkmc2/codegen/Lowering.scala b/hkmc2/shared/src/main/scala/hkmc2/codegen/Lowering.scala index 925a7df958..71661952cd 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/codegen/Lowering.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/codegen/Lowering.scala @@ -108,7 +108,6 @@ class Lowering()(using Config, TL, Raise, State, Ctx, SymbolPrinter)(using Erasu case qual => save(qual)(p => k(sel.copy(qual = p)(sel.symbol, sel.toLoc)(sel.sanitize))) case _ => save(parent)(k) - private def definitionConfigOverride(annotations: Ls[Annot]): Opt[Config] = val modifiers = annotations.collect: case Annot.Config(modify) => modify From a5030db720bb6e05e6ff95e8cd011bde63c89659 Mon Sep 17 00:00:00 2001 From: Codex Date: Fri, 16 Oct 2026 13:40:05 +0800 Subject: [PATCH 07/32] Consolidate debug handling and fix definition trace scopes Share directive discovery, parsing, definition traversal, and trace scoping between the compiler and diff tests. Report parsing diagnostics once, honor method lowering traces, and inherit nested debug settings. Avoid reindenting the elaborator body and remove unused parsing overrides. Add diff tests for scoped and file output, plus explicit FIXME coverage for unsupported local parsing/optimization and class/module trace hooks. Include the latest golden output. Validation: ctest and hkmc2AllTests/test. --- .../src/main/scala/hkmc2/CompilerCtx.scala | 48 ++---- .../shared/src/main/scala/hkmc2/Config.scala | 22 +-- .../main/scala/hkmc2/DebugOutputHandler.scala | 80 +++++++++- .../src/main/scala/hkmc2/MLsCompiler.scala | 39 +---- .../main/scala/hkmc2/codegen/Lowering.scala | 22 +-- .../scala/hkmc2/semantics/Elaborator.scala | 145 +++++++++--------- .../main/scala/hkmc2/semantics/Resolver.scala | 9 +- .../src/main/scala/utils/TraceLogger.scala | 12 +- .../mlscript/codegen/DebugDestinations.mls | 63 ++++++++ .../codegen/DebugParsingDirective.mls | 2 +- .../test/mlscript/codegen/DebugScoping.mls | 31 ++++ .../codegen/DebugUnsupportedPhases.mls | 57 +++++++ .../test/scala/hkmc2/JSBackendDiffMaker.scala | 26 +--- .../src/test/scala/hkmc2/MLsDiffMaker.scala | 50 ++---- 14 files changed, 350 insertions(+), 256 deletions(-) create mode 100644 hkmc2/shared/src/test/mlscript/codegen/DebugDestinations.mls create mode 100644 hkmc2/shared/src/test/mlscript/codegen/DebugScoping.mls create mode 100644 hkmc2/shared/src/test/mlscript/codegen/DebugUnsupportedPhases.mls diff --git a/hkmc2/shared/src/main/scala/hkmc2/CompilerCtx.scala b/hkmc2/shared/src/main/scala/hkmc2/CompilerCtx.scala index 661960d577..f35d234056 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/CompilerCtx.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/CompilerCtx.scala @@ -63,12 +63,6 @@ class CompilerCtx( (file: io.Path, prelude: Ctx) (using TL, Raise) : Artifact = - getElaboratedBlock(file, prelude, forceDebugParsing = false) - - def getElaboratedBlock - (file: io.Path, prelude: Ctx, forceDebugParsing: Bool) - (using TL, Raise) - : Artifact = val lastMod = fs.getLastChangedTimestamp(file) @@ -95,9 +89,9 @@ class CompilerCtx( val parse = given CompilerCtx = this - ParserSetup(file, forceDebugParsing, outputHandler) + ParserSetup(file, outputHandler) val parsed = parse.resultBlk - val phaseConfig = parse.effectiveConfig.givenIn(Config.elaborationConfig(parsed)) + val phaseConfig = Config.elaborationConfig(parsed) stateDebug = CompilerCtx.debugTracingEnabled(phaseConfig.debug) stateShowUids = phaseConfig.debug.showUids def traceLogger(enabled: Bool) = new TraceLogger: @@ -191,38 +185,20 @@ class CompilerCtx( codegen.Printer().worksheet(p)(using irPrintingScp).mkString(100) def showDebugIR(title: Str, program: codegen.Program): Unit = outputHandler.emit(artifactConfig.debug.out, s"${title}\n${printer(program)}") - def forEachDefinitionDebug( - program: codegen.Program, - isEnabled: Config.Debug => Bool, - )(display: (codegen.Defn, Config) => Unit): Unit = - def visit(defn: codegen.Defn): Unit = - defn.configOverride.foreach: localConfig => - if isEnabled(localConfig.debug) then display(defn, localConfig) - new codegen.BlockTraverser: - override def applyFunDefn(fun: codegen.FunDefn): Unit = - visit(fun) - super.applyFunDefn(fun) - override def applyValDefn(defn: codegen.ValDefn): Unit = - visit(defn) - super.applyValDefn(defn) - override def applyClsLikeDefn(defn: codegen.ClsLikeDefn): Unit = - visit(defn) - super.applyClsLikeDefn(defn) - .applyProgram(program) def showDefinitionDebugIR( title: Str, program: codegen.Program, isEnabled: Config.Debug => Bool, ): Unit = val blockPrinter = codegen.Printer() - forEachDefinitionDebug(program, isEnabled): (defn, localConfig) => + DebugOutputHandler.forEachIRDefinition(program, isEnabled): (defn, out) => val ir = blockPrinter.printDefinition(defn)(using irPrintingScp).mkString(100) - outputHandler.emit(localConfig.debug.out, s"${title}\n${ir}") + outputHandler.emit(out, s"${title}\n${ir}") def showDefinitionDebugTree(title: Str, program: codegen.Program): Unit = - forEachDefinitionDebug(program, _.showLoweredTree): (defn, localConfig) => + DebugOutputHandler.forEachIRDefinition(program, _.showLoweredTree): (defn, out) => val tree = defn match case product: Product => product.showAsTree - outputHandler.emit(localConfig.debug.out, s"${title}\n${tree}") + outputHandler.emit(out, s"${title}\n${tree}") val pipeline = new codegen.CompilationPipeline: override def extraSymbolsToPreserveFrom(prog: codegen.Program): Set[codegen.ScopedSymbol] = collectCompilationUnitSymbols(prog) @@ -279,12 +255,6 @@ class CompilerCtx( (file: io.Path) (using tl: TL, raise: Raise) : PreludeArtifact = - getPrelude(file, forceDebugParsing = false) - - def getPrelude - (file: io.Path, forceDebugParsing: Bool) - (using tl: TL, raise: Raise) - : PreludeArtifact = // The prelude context is shared so every compilation unit sees the same prelude // symbols. Callers still elaborate their own files with a fresh State; the frozen // State remains the owner captured by the prelude symbols themselves. @@ -298,7 +268,7 @@ class CompilerCtx( // * See the corresponding assertion in `getElaboratedBlock` above. softAssert(art.config === rootConfig, s"Cached prelude for $file was elaborated under a different root configuration") - !forceDebugParsing && art.lastChangedTimestamp >= lastMod, + art.lastChangedTimestamp >= lastMod, create = var stateDebug = false var stateShowUids = true @@ -310,8 +280,8 @@ class CompilerCtx( given CompilerCtx = this given DebugPrinter = new DebugPrinter val outputHandler = DebugOutputHandler(fs, rootConfig.baseDir, println) - val parse = ParserSetup(file, forceDebugParsing, outputHandler) - val elaborationConfig = parse.effectiveConfig.givenIn(Config.elaborationConfig(parse.resultBlk)) + val parse = ParserSetup(file, outputHandler) + val elaborationConfig = Config.elaborationConfig(parse.resultBlk) stateDebug = CompilerCtx.debugTracingEnabled(elaborationConfig.debug) stateShowUids = elaborationConfig.debug.showUids val etl = new TraceLogger: diff --git a/hkmc2/shared/src/main/scala/hkmc2/Config.scala b/hkmc2/shared/src/main/scala/hkmc2/Config.scala index 9b5f7668eb..ed93749cfd 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/Config.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/Config.scala @@ -265,14 +265,19 @@ object Config: * Read them before constructing the elaborator so its resolver agrees with lowering. * Elaboration still records SetConfig statements and reports directive errors once. */ def elaborationConfig(prgm: syntax.Tree.Block)(using Config): Config = + elaborationConfig(prgm.desugStmts) + + /** The parser uses the raw trees to discover tracing before constructing a block's symbols. */ + def elaborationConfig(trees: Ls[syntax.Tree])(using Config): Config = import syntax.Tree.* given Raise = _ => () def modifier(tree: syntax.Tree): Config => Config = tree match case App(Directive(prefix, _), args) => modifier(Directive(prefix, args)) case Directive(Ident("config"), Tup(args)) => ConfigParser.parseOverrides(args) case Directive(Ident("lang"), Tup(args)) => ConfigParser.parseLanguageDirective(args) + case Directive(Ident("dbg"), Tup(args)) => ConfigParser.parseDebugDirective(args) case _ => identity - prgm.desugStmts.foldLeft(config)((cfg, tree) => modifier(tree)(cfg)) + trees.foldLeft(config)((cfg, tree) => modifier(tree)(cfg)) def extractConfigFromStats(prgm: semantics.Term.Blk)(using Config) = // Extract cumulative config modifications from SetConfig statements @@ -574,21 +579,6 @@ object ConfigParser: def apply(cfg: Config): Config = cfg.copy(debug = modifyDebug(cfg.debug)) override def toString: Str = "..." - /** Apply top-level debug directives before phase execution begins. - * - * Parsing itself uses this after a quiet discovery parse, then repeats the parse when requested. - * Other phases use it directly before elaborating the parsed block. */ - def extractDebugFromTrees(trees: Ls[Tree])(using Raise, Config): Config = - trees.foldLeft(config): - case (cfg, Directive(Ident("dbg"), Tup(args))) => parseDebugDirective(args)(cfg) - case (cfg, App(Directive(Ident("dbg"), _), Tup(args))) => parseDebugDirective(args)(cfg) - case (cfg, _) => cfg - - /** Discovery variant used before normal elaboration reports directive diagnostics. */ - def discoverDebugFromTrees(trees: Ls[Tree])(using Config): Config = - given Raise = _ => () - extractDebugFromTrees(trees) - /** Apply `@dbg(...)` syntax annotations without reporting diagnostics during the discovery parse. */ def discoverDebugFromAnnotations(annotations: Ls[Tree], base: Config): Opt[Config] = given Raise = _ => () diff --git a/hkmc2/shared/src/main/scala/hkmc2/DebugOutputHandler.scala b/hkmc2/shared/src/main/scala/hkmc2/DebugOutputHandler.scala index ccda0935a8..50406332b9 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/DebugOutputHandler.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/DebugOutputHandler.scala @@ -23,6 +23,63 @@ final class DebugOutputHandler(fs: io.FileSystem, baseDir: io.Path, stdIO: Str = object DebugOutputHandler: + def definitionConfig(annotations: Ls[semantics.Annot], base: Config): Opt[Config] = + val modifiers = annotations.collect: + case semantics.Annot.Debug(modify) => modify + if modifiers.isEmpty then N + else S(modifiers.foldLeft(base)((cfg, modify) => modify(cfg))) + + /** Keep trace routing and symbol printing in the same definition-local scope. */ + def traceDefinition[T](annotations: Ls[semantics.Annot], base: Config, enabled: Config.Debug => Bool) + (thunk: => T)(using state: semantics.Elaborator.State, tl: utils.TL): T = + definitionConfig(annotations, tl.debugConfig(base)) match + case S(localConfig) => + val debug = localConfig.debug + state.scopedDebug(enabled(debug), debug.showUids): + tl.scopedDebug(enabled(debug), debug)(thunk) + case N => thunk + + /** Discover directives without committing diagnostics from a parse that may be repeated. + * A traced parse reports its own diagnostics, in trace order; otherwise replay discovery's + * diagnostics exactly once. Both compiler entry points use this path. */ + def parse(origin: Origin, forceDebug: Bool, showTokens: Bool, output: (Config.DebugOutput, Str) => Unit) + (using semantics.Elaborator.State, Config, Raise): (Ls[syntax.Tree], Config) = + def run(dbg: Bool, out: Config.DebugOutput)(using Raise): Ls[syntax.Tree] = + val lexer = new syntax.Lexer(origin, dbg): + override protected def doPrintDbg(msg: => Str): Unit = output(out, msg) + val tokens = lexer.bracketedTokens + if dbg then output(out, syntax.Lexer.printTokens(tokens)) + val parser = new syntax.Parser(origin, tokens, syntax.ParseRules(), summon[Raise], dbg): + def doPrintDbg(msg: => Str): Unit = if this.dbg then output(out, msg) + parser.parseAll(parser.block(allowNewlines = true)) + + val diagnostics = mutable.ListBuffer.empty[Diagnostic] + val discovery = run(false, Config.DebugOutput.StdIO)(using diagnostics += _) + val effectiveConfig = Config.elaborationConfig(discovery) + val result = + if forceDebug || effectiveConfig.debug.parsing then + run(true, effectiveConfig.debug.out) + else + diagnostics.foreach(summon[Raise]) + // Token display is a legacy diff-test option. Re-lexing does not report diagnostics twice. + if showTokens then + val lexer = new syntax.Lexer(origin, false)(using _ => ()) + output(Config.DebugOutput.StdIO, syntax.Lexer.printTokens(lexer.bracketedTokens)) + discovery + result -> effectiveConfig + + def showParsedDefinitions(trees: Ls[syntax.Tree], config: Config) + (display: (syntax.Tree, Config.DebugOutput) => Unit): Unit = + def visit(tree: syntax.Tree): Unit = tree match + case syntax.PossiblyAnnotated(annotations, target) => + ConfigParser.discoverDebugFromAnnotations(annotations, config).foreach: localConfig => + if localConfig.debug.showParsedTree then display(tree, localConfig.debug.out) + target.children.foreach: + case child: syntax.Tree => visit(child) + case _ => () + case _ => () + trees.foreach(visit) + /** Inspect the elaborated tree directly: new resolution runs during elaboration and does not * traverse the legacy resolver, but annotated definitions still need their tree displayed. */ def showElaboratedDefinitions(block: semantics.Term.Blk, config: Config) @@ -30,11 +87,26 @@ object DebugOutputHandler: def visit(statement: semantics.Statement): Unit = statement match case defn: semantics.Definition => - val modifiers = defn.annotations.collect: - case semantics.Annot.Debug(modify) => modify - if modifiers.nonEmpty then - val localConfig = modifiers.foldLeft(config)((cfg, modify) => modify(cfg)) + definitionConfig(defn.annotations, config).foreach: localConfig => if localConfig.debug.showElaboratedTree then display(defn, localConfig.debug.out) case _ => () statement.subStatements.foreach(visit) visit(block) + + /** Methods are visited directly by BlockTraverser, without passing through applyDefn. */ + def forEachIRDefinition(program: codegen.Program, isEnabled: Config.Debug => Bool) + (display: (codegen.Defn, Config.DebugOutput) => Unit): Unit = + def visit(defn: codegen.Defn): Unit = + defn.configOverride.foreach: localConfig => + if isEnabled(localConfig.debug) then display(defn, localConfig.debug.out) + new codegen.BlockTraverser: + override def applyFunDefn(fun: codegen.FunDefn): Unit = + visit(fun) + super.applyFunDefn(fun) + override def applyValDefn(defn: codegen.ValDefn): Unit = + visit(defn) + super.applyValDefn(defn) + override def applyClsLikeDefn(defn: codegen.ClsLikeDefn): Unit = + visit(defn) + super.applyClsLikeDefn(defn) + .applyProgram(program) diff --git a/hkmc2/shared/src/main/scala/hkmc2/MLsCompiler.scala b/hkmc2/shared/src/main/scala/hkmc2/MLsCompiler.scala index 145697af41..2b64020f1a 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/MLsCompiler.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/MLsCompiler.scala @@ -7,50 +7,23 @@ import utils.* import hkmc2.semantics.* -class ParserSetup(file: io.Path, forceDebugParsing: Bool, outputHandler: DebugOutputHandler) -(using state: Elaborator.State, raise: Raise, cctx: CompilerCtx, config: Config, debugPrinter: DebugPrinter): +class ParserSetup(file: io.Path, outputHandler: DebugOutputHandler) +(using Elaborator.State, Raise, CompilerCtx, Config, DebugPrinter): val block = CompilerCtx.get.fs.read(file) val fph = new FastParseHelpers(block) val origin = Origin(file, 0, fph) - private def parse(dbg: Bool, out: Config.DebugOutput): Ls[syntax.Tree] = - val lexer = new syntax.Lexer(origin, dbg = dbg): - override protected def doPrintDbg(msg: => Str): Unit = outputHandler.emit(out, msg) - val tokens = lexer.bracketedTokens - - if dbg then outputHandler.emit(out, syntax.Lexer.printTokens(tokens)) - val rules = syntax.ParseRules() - val parser = new syntax.Parser(origin, tokens, rules, raise, dbg = dbg): - def doPrintDbg(msg: => Str): Unit = if this.dbg then outputHandler.emit(out, msg) - - parser.parseAll(parser.block(allowNewlines = true)) - - private val discoveryResult = parse(dbg = false, Config.DebugOutput.StdIO) - val effectiveConfig = ConfigParser.discoverDebugFromTrees(discoveryResult) - val result = - if forceDebugParsing || effectiveConfig.debug.parsing then - parse(dbg = true, effectiveConfig.debug.out) - else discoveryResult - - private def showSelectedParsedTrees(trees: Ls[syntax.Tree]): Unit = - def visit(tree: syntax.Tree): Unit = tree match - case syntax.PossiblyAnnotated(annotations, target) => - ConfigParser.discoverDebugFromAnnotations(annotations, effectiveConfig).foreach: localConfig => - if localConfig.debug.showParsedTree then - outputHandler.emit(localConfig.debug.out, s"Parsed tree\n${tree.showAsTree}") - target.children.foreach: - case child: syntax.Tree => visit(child) - case _ => () - case _ => () - trees.foreach(visit) + val (result, effectiveConfig) = DebugOutputHandler.parse( + origin, forceDebug = false, showTokens = false, outputHandler.emit) if effectiveConfig.debug.showParsedTree then outputHandler.emit( effectiveConfig.debug.out, s"Parsed tree\n${result.map(_.showAsTree).mkString("\n")}", ) - else showSelectedParsedTrees(result) + else DebugOutputHandler.showParsedDefinitions(result, effectiveConfig): (tree, out) => + outputHandler.emit(out, s"Parsed tree\n${tree.showAsTree}") val resultBlk = new syntax.Tree.Block(result) diff --git a/hkmc2/shared/src/main/scala/hkmc2/codegen/Lowering.scala b/hkmc2/shared/src/main/scala/hkmc2/codegen/Lowering.scala index 71661952cd..9623cca46e 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/codegen/Lowering.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/codegen/Lowering.scala @@ -115,11 +115,8 @@ class Lowering()(using Config, TL, Raise, State, Ctx, SymbolPrinter)(using Erasu if modifiers.isEmpty then N else S(modifiers.foldLeft(config)((cfg, modify) => modify(cfg))) - private def definitionDebugConfig(annotations: Ls[Annot]): Opt[Config] = - val modifiers = annotations.collect: - case Annot.Debug(modify) => modify - if modifiers.isEmpty then N - else S(modifiers.foldLeft(config)((cfg, modify) => modify(cfg))) + private def debugLowering[T](annotations: Ls[Annot])(thunk: => T): T = + DebugOutputHandler.traceDefinition(annotations, config, _.lowering)(thunk) extension (t: Term) def instantiated = t match @@ -322,10 +319,10 @@ class Lowering()(using Config, TL, Raise, State, Ctx, SymbolPrinter)(using Erasu assert(td.params.isEmpty) val cfgOverride = definitionConfigOverride(td.extraAnnotations) def lowerValue[R <: Result](lower: (R => Block) => Block)(k: R => Block): Block = - definitionDebugConfig(td.extraAnnotations) match + DebugOutputHandler.definitionConfig(td.extraAnnotations, tl.debugConfig(config)) match case S(localConfig) => State.scopedDebug(localConfig.debug.lowering, localConfig.debug.showUids): - tl.scopedDebug(localConfig.debug.lowering, localConfig.debug.out): + tl.scopedDebug(localConfig.debug.lowering, localConfig.debug): // The continuation lowers sibling statements, outside this value's debug scope. lower: r => State.inOuterDebugScope: @@ -343,12 +340,8 @@ class Lowering()(using Config, TL, Raise, State, Ctx, SymbolPrinter)(using Erasu ) case syntax.Fun => val cfgOverride = definitionConfigOverride(td.extraAnnotations) - val (paramLists, bodyBlock) = definitionDebugConfig(td.extraAnnotations) match - case S(localConfig) => - State.scopedDebug(localConfig.debug.lowering, localConfig.debug.showUids): - tl.scopedDebug(localConfig.debug.lowering, localConfig.debug.out): - setupFunctionOrByNameDef(td.params, bod, S(td.sym.nme), td.tsym.declaredResultType) - case N => setupFunctionOrByNameDef(td.params, bod, S(td.sym.nme), td.tsym.declaredResultType) + val (paramLists, bodyBlock) = debugLowering(td.extraAnnotations): + setupFunctionOrByNameDef(td.params, bod, S(td.sym.nme), td.tsym.declaredResultType) Define(FunDefn(td.owner, td.sym, td.tsym, paramLists, bodyBlock)(cfgOverride, td.annotations), blockImpl(stats, res)) case syntax.LetBind | syntax.HandlerBind | syntax.RecordField => fail: @@ -1683,7 +1676,8 @@ class Lowering()(using Config, TL, Raise, State, Ctx, SymbolPrinter)(using Erasu val mtds = clsBody.methods .flatMap: td => td.body.map: bod => - val (paramLists, bodyBlock) = setupFunctionDef(td.params, bod, S(td.sym.nme), td.tsym.declaredResultType) + val (paramLists, bodyBlock) = debugLowering(td.extraAnnotations): + setupFunctionDef(td.params, bod, S(td.sym.nme), td.tsym.declaredResultType) reportAnnotations(td, td.extraAnnotations) val cfgOverride = definitionConfigOverride(td.extraAnnotations) FunDefn(td.owner, td.sym, td.tsym, paramLists, bodyBlock)(cfgOverride, td.annotations) diff --git a/hkmc2/shared/src/main/scala/hkmc2/semantics/Elaborator.scala b/hkmc2/shared/src/main/scala/hkmc2/semantics/Elaborator.scala index ba8afdc94d..37ffb3a314 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/semantics/Elaborator.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/semantics/Elaborator.scala @@ -784,14 +784,7 @@ extends Importer: val newResolution: Bool = config.language.useNewResolution private def debugElaboration[T](annotations: Ls[Annot])(thunk: => T): T = - val modifiers = annotations.collect: - case Annot.Debug(modify) => modify - if modifiers.isEmpty then thunk - else - val localConfig = modifiers.foldLeft(config)((cfg, modify) => modify(cfg)) - val enabled = localConfig.debug.traceElaboration(localConfig.language) - state.scopedDebug(enabled, localConfig.debug.showUids): - tl.scopedDebug(enabled, localConfig.debug.out)(thunk) + DebugOutputHandler.traceDefinition(annotations, config, _.traceElaboration(config.language))(thunk)(using state, tl) lazy val illegalMemberNameTail = msg"Member names must start with a letter or underscore, followed by letters, digits, or underscores." -> N @@ -2358,74 +2351,76 @@ extends Importer: // Recorded before the definition's own type parameters enter scope. rctx.state.lexicalTypeBinders(tsym) = ctx.typeBinders - val tdf = debugElaboration(annotations): + val elaborateDefinition: Ctx ?=> TermDefinition = newCtx ?=> + log(s"Processing term definition $nme") - ctx.nest(OuterCtx.NonReturnContext(S(tsym))).givenIn: newCtx ?=> - - // * Add type parameters to context - val (tps, newCtx1) = td.typeParams match - case S(t) => - val (tps, ctx) = typeParams(t) - (S(tps), ctx) - case N => (N, ctx) - if newResolution then tps.foreach(ps => registerTypeParameters(tsym, ps.map(_.sym))) - // * Add parameters to context - var newCtx = newCtx1 - val pss = td.paramLists.map: ps => - val (res, newCtx2) = funParams(ps)(using newCtx) - newCtx = newCtx2 - res - - // * Elaborate signature - val st = td.annotatedResultType.orElse(newSignatureTrees.get(id.name)) // FIXME: may elaborate external sig twice!! - val s = st.map: - // unwrap possible module modifier - // e.g, `fun f: module M` - // ^^^^^^ - case TypeDef(Mod, st, N) => term(st, Tpe)(using newCtx) - case st => term(st, Tpe)(using newCtx) - - if newResolution && td.annotatedResultType.isEmpty then - s.foreach(registerParameterSignature(pss, _)) - - val body: Opt[Term] = rhs match - case N => N - case _ if ctx.mode is Mode.Light => S(Term.Missing) - case S(rhs) => S: - val nonLocalRetHandler = TempSymbol(N, erasedType = N, s"nonLocalRetHandler$$${id.name}") - val hasGeneratorAnnotation = annotations.exists(_.isInstanceOf[Annot.Generator]) - val hasAsyncAnnotation = annotations.exists(_.isInstanceOf[Annot.Async]) - if pss.isEmpty && hasGeneratorAnnotation then - raise(ErrorReport(msg"Generators are not supported on functions without a parameter list" -> statementLoc :: Nil)) - newCtx.nest(OuterCtx.Function(nonLocalRetHandler, S(tsym))(pss.nonEmpty && hasGeneratorAnnotation, hasAsyncAnnotation)).givenIn: newCtx ?=> - val b = term(rhs, Trm)(using newCtx) - if nonLocalRetHandler.directRefs.isEmpty then b else - mkEffectHandleAbortive( - nonLocalRetHandler, - "‹non-local return effect›", - EffectHandlerMethodSpec("ret", S("value"), requireEffectMethodValue("ret", _)) :: Nil, - b, - ) - val r = FlowSymbol(s"‹result of ${sym}›") - - val mfn = st match - // st.isModified(Mod) indicates if the function marks - // its result as "module". e.g, `fun f: module M` - // ^^^^^^ - case S(st) if st.isModified(Mod) => - Modulefulness.ofSign(s)(true) - case _ => - Modulefulness.none - - // Retain the source signature's meaning; Erasure decides which arrows are physical parameters. - if newResolution then s.foreach(registerSignature) - val tdf = TermDefinition(k, sym, tsym, pss, tps, s, body, - TermDefFlags.empty.copy(isMethod = isMethod, hasResultAnnotation = td.annotatedResultType.isDefined), mfn, annotations, N)(statementLoc) - sym.tsym = S(tsym) - tsym.defn = S(tdf) - if newResolution then checkDeclaredResult(tdf) - - tdf + // * Add type parameters to context + val (tps, newCtx1) = td.typeParams match + case S(t) => + val (tps, ctx) = typeParams(t) + (S(tps), ctx) + case N => (N, ctx) + if newResolution then tps.foreach(ps => registerTypeParameters(tsym, ps.map(_.sym))) + // * Add parameters to context + var newCtx = newCtx1 + val pss = td.paramLists.map: ps => + val (res, newCtx2) = funParams(ps)(using newCtx) + newCtx = newCtx2 + res + + // * Elaborate signature + val st = td.annotatedResultType.orElse(newSignatureTrees.get(id.name)) // FIXME: may elaborate external sig twice!! + val s = st.map: + // unwrap possible module modifier + // e.g, `fun f: module M` + // ^^^^^^ + case TypeDef(Mod, st, N) => term(st, Tpe)(using newCtx) + case st => term(st, Tpe)(using newCtx) + + if newResolution && td.annotatedResultType.isEmpty then + s.foreach(registerParameterSignature(pss, _)) + + val body: Opt[Term] = rhs match + case N => N + case _ if ctx.mode is Mode.Light => S(Term.Missing) + case S(rhs) => S: + val nonLocalRetHandler = TempSymbol(N, erasedType = N, s"nonLocalRetHandler$$${id.name}") + val hasGeneratorAnnotation = annotations.exists(_.isInstanceOf[Annot.Generator]) + val hasAsyncAnnotation = annotations.exists(_.isInstanceOf[Annot.Async]) + if pss.isEmpty && hasGeneratorAnnotation then + raise(ErrorReport(msg"Generators are not supported on functions without a parameter list" -> statementLoc :: Nil)) + newCtx.nest(OuterCtx.Function(nonLocalRetHandler, S(tsym))(pss.nonEmpty && hasGeneratorAnnotation, hasAsyncAnnotation)).givenIn: newCtx ?=> + val b = term(rhs, Trm)(using newCtx) + if nonLocalRetHandler.directRefs.isEmpty then b else + mkEffectHandleAbortive( + nonLocalRetHandler, + "‹non-local return effect›", + EffectHandlerMethodSpec("ret", S("value"), requireEffectMethodValue("ret", _)) :: Nil, + b, + ) + val r = FlowSymbol(s"‹result of ${sym}›") + + val mfn = st match + // st.isModified(Mod) indicates if the function marks + // its result as "module". e.g, `fun f: module M` + // ^^^^^^ + case S(st) if st.isModified(Mod) => + Modulefulness.ofSign(s)(true) + case _ => + Modulefulness.none + + // Retain the source signature's meaning; Erasure decides which arrows are physical parameters. + if newResolution then s.foreach(registerSignature) + val tdf = TermDefinition(k, sym, tsym, pss, tps, s, body, + TermDefFlags.empty.copy(isMethod = isMethod, hasResultAnnotation = td.annotatedResultType.isDefined), mfn, annotations, N)(statementLoc) + sym.tsym = S(tsym) + tsym.defn = S(tdf) + if newResolution then checkDeclaredResult(tdf) + + tdf + + val tdf = debugElaboration(annotations): + ctx.nest(OuterCtx.NonReturnContext(S(tsym))).givenIn(elaborateDefinition) go(sts, Nil, tdf :: acc)(N) case L(d) => diff --git a/hkmc2/shared/src/main/scala/hkmc2/semantics/Resolver.scala b/hkmc2/shared/src/main/scala/hkmc2/semantics/Resolver.scala index 5829bec6a3..51e3ac66ff 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/semantics/Resolver.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/semantics/Resolver.scala @@ -357,14 +357,7 @@ class Resolver(tl: TraceLogger) end traverse def traverseDefn(defn: Definition)(using ICtx): ICtx = - val modifiers = defn.annotations.collect: - case Annot.Debug(modify) => modify - if modifiers.isEmpty then traverseDefnImpl(defn) - else - val localConfig = modifiers.foldLeft(cfg)((config, modify) => modify(config)) - state.scopedDebug(localConfig.debug.resolution, localConfig.debug.showUids): - tl.scopedDebug(localConfig.debug.resolution, localConfig.debug.out): - traverseDefnImpl(defn) + DebugOutputHandler.traceDefinition(defn.annotations, cfg, _.resolution)(traverseDefnImpl(defn))(using state, tl) private def traverseDefnImpl(defn: Definition)(using ICtx): ICtx = trace(s"Resolving definition: $defn"): diff --git a/hkmc2/shared/src/main/scala/utils/TraceLogger.scala b/hkmc2/shared/src/main/scala/utils/TraceLogger.scala index e49e8a4442..ddeb9864d3 100644 --- a/hkmc2/shared/src/main/scala/utils/TraceLogger.scala +++ b/hkmc2/shared/src/main/scala/utils/TraceLogger.scala @@ -12,10 +12,14 @@ abstract class TraceLogger(using val debugPrinter: DebugPrinter): def doTrace: Bool = true protected def defaultDebugOutput: Config.DebugOutput = Config.DebugOutput.StdIO - private var debugOverrides: Ls[(Bool, Config.DebugOutput)] = Nil + private var debugOverrides: Ls[(Bool, Config.Debug)] = Nil /** Whether tracing is enabled after applying the innermost definition-local override. */ final def isTracing: Bool = debugOverrides.headOption.fold(doTrace)(_._1) + + /** An inner annotation changes only the settings it names in the enclosing debug scope. */ + final def debugConfig(base: Config): Config = + debugOverrides.headOption.fold(base)(entry => base.copy(debug = entry._2)) protected val noPostTrace: Any => Str = _ => "" @@ -44,13 +48,13 @@ abstract class TraceLogger(using val debugPrinter: DebugPrinter): if isTracing then emitDbg( if noIndent then msg.toString else "| " * indent + msg.toString.indentNewLines("| " * indent + "> "), - debugOverrides.headOption.fold(defaultDebugOutput)(_._2), + debugOverrides.headOption.fold(defaultDebugOutput)(_._2.out), ) /** Temporarily select both tracing enablement and its destination. * Definition annotations use this to avoid leaking debug settings to sibling definitions. */ - def scopedDebug[T](enabled: Bool, out: Config.DebugOutput)(thunk: => T): T = - debugOverrides ::= enabled -> out + def scopedDebug[T](enabled: Bool, debug: Config.Debug)(thunk: => T): T = + debugOverrides ::= enabled -> debug try thunk finally debugOverrides = debugOverrides.tail /** Run a continuation under the debug scope that surrounded the current local scope. */ diff --git a/hkmc2/shared/src/test/mlscript/codegen/DebugDestinations.mls b/hkmc2/shared/src/test/mlscript/codegen/DebugDestinations.mls new file mode 100644 index 0000000000..8be708c876 --- /dev/null +++ b/hkmc2/shared/src/test/mlscript/codegen/DebugDestinations.mls @@ -0,0 +1,63 @@ +:js + + +:silent +val debugFile = "hkmc2/shared/src/test/mlscript/codegen/DebugDestinations.log" +fs.rmSync(debugFile, {force: true}) + + +// Nested annotations inherit the enclosing phase and destination. Siblings do not. +@dbg('lowering, showUids: false, out: File("DebugDestinations.log")) +fun selected_file() = + @dbg(showUids: false) fun nested_file() = 42 + @dbg(lowering: false) fun muted_file() = 43 + nested_file() +fun unrelated_file() = 44 + + +:silent +:expect true +let trace = fs.readFileSync(debugFile, "utf8") +trace.includes("Lowering.term 42\n") and + not(trace.includes("Lowering.term 43\n")) and + not(trace.includes("Lowering.term 44\n")) + + +// Methods route to the same output handler as free functions. +module FileMethods with + @dbg('lowering, showUids: false, out: File("DebugDestinations.log")) + fun selected() = 45 + fun unrelated() = 46 + +:silent +:expect true +let trace = fs.readFileSync(debugFile, "utf8") +trace.includes("Lowering.term 42\n") and + trace.includes("Lowering.term 45\n") and + not(trace.includes("Lowering.term 46\n")) + + +:silent +fs.rmSync(debugFile, {force: true}) + + +// A discovery parse and its traced replay must report each syntax error only once. +:noCodeGen +:pe +#dbg('parsing, out: File("DebugDestinationsParse.log")) +fun broken(x) = ) +//│ ╔══[PARSE ERROR] Unexpected closing parenthesis +//│ ║ l.48: fun broken(x) = ) +//│ ╙── ^ +//│ ╔══[PARSE ERROR] Expected expression or block after 'fun' binding equals sign; found end of input instead +//│ ║ l.48: fun broken(x) = ) +//│ ╙── ^ + +#dbg(parsing: false, out: StdIO) + +:silent +:expect true +fs.readFileSync("hkmc2/shared/src/test/mlscript/codegen/DebugDestinationsParse.log", "utf8").includes("blockOf") + +:silent +fs.rmSync("hkmc2/shared/src/test/mlscript/codegen/DebugDestinationsParse.log", {force: true}) diff --git a/hkmc2/shared/src/test/mlscript/codegen/DebugParsingDirective.mls b/hkmc2/shared/src/test/mlscript/codegen/DebugParsingDirective.mls index ca3277970c..260d5f3fb5 100644 --- a/hkmc2/shared/src/test/mlscript/codegen/DebugParsingDirective.mls +++ b/hkmc2/shared/src/test/mlscript/codegen/DebugParsingDirective.mls @@ -3,7 +3,7 @@ #dbg('parsing) //│ ┊#┊dbg┊(⟨┊'parsing┊⟩)┊ -//│ @ blockOf(start of expression,allowNewlines = true) [at syntax/Parser.scala:455] +//│ @ blockOf(start of expression,allowNewlines = true) [at syntax/Parser.scala:54] //│ │ ? blockOfImpl inspects ┊#┊dbg┊(⟨┊'parsing┊⟩)┊ [at syntax/Parser.scala:340] //│ │ ! blockOfImpl consumes ┊#┊ [at syntax/Parser.scala:367] //│ │ @ parseRule(39,'#' directive keyword ::= Expr('#' directive body ::= End()),allowNewlines = true) [at syntax/Parser.scala:380] diff --git a/hkmc2/shared/src/test/mlscript/codegen/DebugScoping.mls b/hkmc2/shared/src/test/mlscript/codegen/DebugScoping.mls new file mode 100644 index 0000000000..59fe8743ae --- /dev/null +++ b/hkmc2/shared/src/test/mlscript/codegen/DebugScoping.mls @@ -0,0 +1,31 @@ +:js + + +// A method must use the same definition-local lowering trace as a free function. +#dbg(showUids: false) +module DebugMethods with + @dbg('lowering) fun selected(x) = x + fun sibling(x) = x +//│ Lowering.term x⁰ – symbol x +//│ Expanding term Ref(x) +//│ ~> Ref(x) + +:expect 7 +DebugMethods.selected(7) +//│ = 7 + + +// Changing only UID display should retain the outer definition's trace flag. +@dbg('lowering, showUids: false) fun debug_outer() = + @dbg(showUids: false) fun debug_inner() = 42 + debug_inner() +//│ Lowering.term { fun member:debug_inner() = 42; member:debug_inner⁰() } +//│ Lowering.term 42 +//│ Lowering.term member:debug_inner⁰() +//│ Expanding term App(Ref(member:debug_inner),Tup(List())) +//│ ~> App(Ref(member:debug_inner),Tup(List())) +//│ Expanding term Ref(member:debug_inner) +//│ ~> Resolved(Ref(member:debug_inner),term:debug_inner) +//│ Lowering.term member:debug_inner⁰‹term:debug_inner› – symbol term:debug_inner +//│ Expanding term Resolved(Ref(member:debug_inner),term:debug_inner) +//│ ~> Resolved(Ref(member:debug_inner),term:debug_inner) diff --git a/hkmc2/shared/src/test/mlscript/codegen/DebugUnsupportedPhases.mls b/hkmc2/shared/src/test/mlscript/codegen/DebugUnsupportedPhases.mls new file mode 100644 index 0000000000..14d6a89285 --- /dev/null +++ b/hkmc2/shared/src/test/mlscript/codegen/DebugUnsupportedPhases.mls @@ -0,0 +1,57 @@ +:js + + +:silent +val debugDir = "hkmc2/shared/src/test/mlscript/codegen/" +fs.rmSync(debugDir + "DebugLocalParsing.log", {force: true}) +fs.rmSync(debugDir + "DebugLocalOptimization.log", {force: true}) +fs.rmSync(debugDir + "DebugModuleElaboration.log", {force: true}) +fs.rmSync(debugDir + "DebugClassLowering.log", {force: true}) + + +// FIXME: parsing traces currently inspect only #dbg, ignoring definition annotations. +:fixme +:expect true +@dbg('parsing, out: File("DebugLocalParsing.log")) fun parse_selected(x) = x +fs.existsSync(debugDir + "DebugLocalParsing.log") +//│ ═══[RUNTIME ERROR] Expected: 'true', got: 'false' +//│ = false + + +// FIXME: optimization trace loggers never consult definition-local debug settings. +:fixme +:expect true +@dbg('optimizations, out: File("DebugLocalOptimization.log")) +fun optimize_selected(x) = ((y) => y)(x) +fs.existsSync(debugDir + "DebugLocalOptimization.log") +//│ ═══[RUNTIME ERROR] Expected: 'true', got: 'false' +//│ = false + + +// FIXME: type definitions bypass the elaborator's definition-local trace scope. +:fixme +:expect true +@dbg('elaboration, out: File("DebugModuleElaboration.log")) +module ElaborationSelected with + fun member(x) = x +fs.existsSync(debugDir + "DebugModuleElaboration.log") +//│ ═══[RUNTIME ERROR] Expected: 'true', got: 'false' +//│ = false + + +// FIXME: lowering a class bypasses its definition-local trace scope. +:fixme +:expect true +@dbg('lowering, out: File("DebugClassLowering.log")) +class LoweringSelected(x) with + fun member() = x +fs.existsSync(debugDir + "DebugClassLowering.log") +//│ ═══[RUNTIME ERROR] Expected: 'true', got: 'false' +//│ = false + + +:silent +fs.rmSync(debugDir + "DebugLocalParsing.log", {force: true}) +fs.rmSync(debugDir + "DebugLocalOptimization.log", {force: true}) +fs.rmSync(debugDir + "DebugModuleElaboration.log", {force: true}) +fs.rmSync(debugDir + "DebugClassLowering.log", {force: true}) diff --git a/hkmc2DiffTests/src/test/scala/hkmc2/JSBackendDiffMaker.scala b/hkmc2DiffTests/src/test/scala/hkmc2/JSBackendDiffMaker.scala index 1511c1915f..0c42929aaf 100644 --- a/hkmc2DiffTests/src/test/scala/hkmc2/JSBackendDiffMaker.scala +++ b/hkmc2DiffTests/src/test/scala/hkmc2/JSBackendDiffMaker.scala @@ -125,37 +125,19 @@ abstract class JSBackendDiffMaker extends MLsDiffMaker: Printer() val print = (p: codegen.Program) => blockPrinter.worksheet(p)(using irPrintingScp).mkString(output.ColWidth) - def forEachDefinitionDebug( - program: codegen.Program, - isEnabled: Config.Debug => Bool, - )(display: (Defn, Config) => Unit): Unit = - def visit(defn: Defn): Unit = - defn.configOverride.foreach: localConfig => - if isEnabled(localConfig.debug) then display(defn, localConfig) - new BlockTraverser: - override def applyFunDefn(fun: FunDefn): Unit = - visit(fun) - super.applyFunDefn(fun) - override def applyValDefn(defn: ValDefn): Unit = - visit(defn) - super.applyValDefn(defn) - override def applyClsLikeDefn(defn: ClsLikeDefn): Unit = - visit(defn) - super.applyClsLikeDefn(defn) - .applyProgram(program) def showDefinitionDebugIR( title: Str, program: codegen.Program, isEnabled: Config.Debug => Bool, ): Unit = - forEachDefinitionDebug(program, isEnabled): (defn, localConfig) => + DebugOutputHandler.forEachIRDefinition(program, isEnabled): (defn, out) => val ir = blockPrinter.printDefinition(defn)(using irPrintingScp).mkString(output.ColWidth) - outputDebugSection(title, ir, localConfig.debug.out) + outputDebugSection(title, ir, out) def showDefinitionDebugTree(title: Str, program: codegen.Program): Unit = - forEachDefinitionDebug(program, _.showLoweredTree): (defn, localConfig) => + DebugOutputHandler.forEachIRDefinition(program, _.showLoweredTree): (defn, out) => val tree = defn match case product: Product => product.showAsTree - outputDebugSection(title, tree, localConfig.debug.out) + outputDebugSection(title, tree, out) Config.extractConfigFromStats(blk).givenIn { val loweringState = summon[Elaborator.State] diff --git a/hkmc2DiffTests/src/test/scala/hkmc2/MLsDiffMaker.scala b/hkmc2DiffTests/src/test/scala/hkmc2/MLsDiffMaker.scala index 8372bc8f50..8684b4c79b 100644 --- a/hkmc2DiffTests/src/test/scala/hkmc2/MLsDiffMaker.scala +++ b/hkmc2DiffTests/src/test/scala/hkmc2/MLsDiffMaker.scala @@ -440,39 +440,9 @@ abstract class MLsDiffMaker extends DiffMaker: val baseConfig = configModify(mkConfig) given Config = baseConfig - def parse(dbg: Bool, out: Config.DebugOutput): Ls[syntax.Tree] = - val lexer = new syntax.Lexer(origin, dbg = dbg): - override protected def doPrintDbg(msg: => Str): Unit = debugOutputHandler.emit(out, msg) - val tokens = lexer.bracketedTokens - - if showParse.isSet || dbg then - if dbg then debugOutputHandler.emit(out, syntax.Lexer.printTokens(tokens)) - else output(syntax.Lexer.printTokens(tokens)) - - val rules = syntax.ParseRules() - val p = new syntax.Parser(origin, tokens, rules, raise, dbg = dbg): - def doPrintDbg(msg: => Str): Unit = if this.dbg then debugOutputHandler.emit(out, msg) - p.parseAll(p.block(allowNewlines = true)) - - val discoveryResult = parse(dbg = false, Config.DebugOutput.StdIO) - val phaseConfig = ConfigParser.discoverDebugFromTrees(discoveryResult) + val (res, phaseConfig) = DebugOutputHandler.parse( + origin, dbgParsing.isSet, showParse.isSet, debugOutputHandler.emit) activeDebug = phaseConfig.debug - val res = - if dbgParsing.isSet || phaseConfig.debug.parsing then - parse(dbg = true, phaseConfig.debug.out) - else discoveryResult - - def showSelectedParsedTrees(): Unit = - def visit(tree: syntax.Tree): Unit = tree match - case syntax.PossiblyAnnotated(annotations, target) => - ConfigParser.discoverDebugFromAnnotations(annotations, phaseConfig).foreach: localConfig => - if localConfig.debug.showParsedTree then - outputDebugSection("Parsed tree", tree.showAsTree, localConfig.debug.out) - target.children.foreach: - case child: syntax.Tree => visit(child) - case _ => () - case _ => () - res.foreach(visit) // If parsed tree is displayed, don't show the string serialization. if (parseOnly.isSet || showParse.isSet) && !showParsedTree.isSet && !phaseConfig.debug.showParsedTree then @@ -483,7 +453,8 @@ abstract class MLsDiffMaker extends DiffMaker: res.foreach(t => output(t.showAsTree)) else if phaseConfig.debug.showParsedTree then outputDebugSection("Parsed tree", res.map(_.showAsTree).mkString("\n"), phaseConfig.debug.out) - else showSelectedParsedTrees() + else DebugOutputHandler.showParsedDefinitions(res, phaseConfig): (tree, out) => + outputDebugSection("Parsed tree", tree.showAsTree, out) // if showParse.isSet then // output(s"AST: $res") @@ -509,10 +480,9 @@ abstract class MLsDiffMaker extends DiffMaker: case _ => () def processTrees(trees: Ls[syntax.Tree])(using Config, Raise): Unit = - val phaseConfig = ConfigParser.discoverDebugFromTrees(trees) - activeDebug = phaseConfig.debug val blk = new syntax.Tree.Block(trees) - val elaborationConfig = phaseConfig.givenIn(Config.elaborationConfig(blk)) + val elaborationConfig = Config.elaborationConfig(blk) + activeDebug = elaborationConfig.debug // val blockSymbol = // semantics.TopLevelSymbol("block#"+blockNum) blockNum += 1 @@ -526,20 +496,20 @@ abstract class MLsDiffMaker extends DiffMaker: Elaborator(etl, file.up, prelude) val (e, newCtx) = elaborationConfig.givenIn: if elaborationConfig.language.useNewResolution && elaborationConfig.debug.resolution then - etl.scopedDebug(true, elaborationConfig.debug.out)(elab.topLevel(blk)) + etl.scopedDebug(true, elaborationConfig.debug)(elab.topLevel(blk)) else elab.topLevel(blk) curCtx = newCtx extractConfig(e.stats) // If elaborated tree is displayed, don't show the string serialization. - if (showElab.isSet || debug.isSet) && !showElaboratedTree.isSet && !phaseConfig.debug.showElaboratedTree then + if (showElab.isSet || debug.isSet) && !showElaboratedTree.isSet && !elaborationConfig.debug.showElaboratedTree then output(s"Elab: ${e.showDbg}") if showElaboratedTree.isSet then outputSeparator(s"Elaborated tree") output(e.showAsTree) - else if phaseConfig.debug.showElaboratedTree then - outputDebugSection("Elaborated tree", e.showAsTree, phaseConfig.debug.out) + else if elaborationConfig.debug.showElaboratedTree then + outputDebugSection("Elaborated tree", e.showAsTree, elaborationConfig.debug.out) else DebugOutputHandler.showElaboratedDefinitions(e, elaborationConfig): (defn, out) => outputDebugSection("Elaborated tree", defn.showAsTree, out) From 4bbd01a4bf6cec3e9395a58bf434c31a340b3c40 Mon Sep 17 00:00:00 2001 From: Codex Date: Fri, 16 Oct 2026 14:07:17 +0800 Subject: [PATCH 08/32] Fix debug phase warnings, class tracing, and file output Warn when local debug annotations request parsing or optimization traces. Reuse definition scopes for class and module elaboration and lowering. Append file output through the filesystem abstraction and share one handler across imported units. Keep concurrent debugging writes best effort. Add diff regressions, a linear I/O check, and in-memory append coverage. Validate with ctest and hkmc2AllTests/test; include updated golden output. --- DBG_DIRECTIVE_PLAN.md | 6 +- .../scala/hkmc2/io/InMemoryFileSystem.scala | 21 +++--- .../scala/hkmc2/io/node/NodeFileSystem.scala | 3 + .../main/scala/hkmc2/io/node/package.scala | 1 + .../scala/hkmc2/io/VirtualPathTests.scala | 13 ++++ .../scala/hkmc2/io/PlatformFileSystem.scala | 2 + .../test/scala/hkmc2/CompilerCacheTest.scala | 1 + .../scala/hkmc2/DebugOutputHandlerTest.scala | 34 ++++++++++ .../src/main/scala/hkmc2/CompilerCtx.scala | 5 +- .../shared/src/main/scala/hkmc2/Config.scala | 22 ++++--- .../main/scala/hkmc2/DebugOutputHandler.scala | 15 +++-- .../main/scala/hkmc2/codegen/Lowering.scala | 12 ++-- .../src/main/scala/hkmc2/io/FileSystem.scala | 3 + .../scala/hkmc2/semantics/Elaborator.scala | 10 +-- .../test/mlscript/codegen/DebugImports.mls | 26 ++++++++ .../codegen/DebugParsingDirective.mls | 2 +- .../src/test/mlscript/codegen/DebugTypes.mls | 66 +++++++++++++++++++ .../codegen/DebugUnsupportedPhases.mls | 55 +++++++++++----- 18 files changed, 245 insertions(+), 52 deletions(-) create mode 100644 hkmc2/jvm/src/test/scala/hkmc2/DebugOutputHandlerTest.scala create mode 100644 hkmc2/shared/src/test/mlscript/codegen/DebugImports.mls create mode 100644 hkmc2/shared/src/test/mlscript/codegen/DebugTypes.mls diff --git a/DBG_DIRECTIVE_PLAN.md b/DBG_DIRECTIVE_PLAN.md index 40985b8eec..03ede1b1cd 100644 --- a/DBG_DIRECTIVE_PLAN.md +++ b/DBG_DIRECTIVE_PLAN.md @@ -20,7 +20,11 @@ - `StdIO`, which is the default and may be omitted; - `File("out.mls")`, resolved relative to the active configuration's base directory. - Support the annotation form on individual definitions, for example - `@dbg('elaboration) fun foo() = ...`, without enabling the same phase for unrelated definitions. + `@dbg('elaboration) fun foo() = ...`, without requiring file-wide tracing. + Parsing and optimization traces require `#dbg`; requesting them through `@dbg` warns. + Class lowering traces may also include companion definitions and the continuation of superclass evaluation. +- File output clears each destination on first use in a compiler session, then appends across + compilation units. Concurrent debug writes are best effort. - Keep the feature available in diff-test files as well as through `MLsCompiler`. - Diagnose unknown flags, malformed boolean values, malformed destinations, and unsupported arguments. diff --git a/hkmc2/js/src/main/scala/hkmc2/io/InMemoryFileSystem.scala b/hkmc2/js/src/main/scala/hkmc2/io/InMemoryFileSystem.scala index 668c76ac2d..355924c30d 100644 --- a/hkmc2/js/src/main/scala/hkmc2/io/InMemoryFileSystem.scala +++ b/hkmc2/js/src/main/scala/hkmc2/io/InMemoryFileSystem.scala @@ -7,17 +7,22 @@ import scala.scalajs.js.annotation.JSExportTopLevel /** * In-memory file system for testing and web compiler. Stores files as a map - * from normalized path strings to content strings. + * from normalized path strings to buffers, so repeated appends do not copy the whole file. */ class InMemoryFileSystem(initialFiles: Map[String, String]) extends FileSystem: - private val files: MutMap[String, (Int, String)] = MutMap.from: - initialFiles.map { case (k, v) => (Path(k).toString, (0, v)) } + private val files: MutMap[String, (Int, StringBuilder)] = MutMap.from: + initialFiles.map { case (k, v) => (Path(k).toString, (0, new StringBuilder(v))) } def read(path: Path): String = read(path.toString) def write(path: Path, content: String): Unit = write(path.toString, content) + def append(path: Path, content: String): Unit = + files.updateWith(path.toString): + case Some((ts, buffer)) => Some((ts + 1, buffer.append(content))) + case None => Some((0, new StringBuilder(content))) + def exists(path: Path): Bool = files.contains(path.toString) def getLastChangedTimestamp(path: Path): Long = @@ -28,20 +33,20 @@ class InMemoryFileSystem(initialFiles: Map[String, String]) extends FileSystem: @JSExport("write") def write(path: Str, content: Str): Unit = files.updateWith(Path(path).toString): - case Some((ts, _)) => Some((ts + 1, content)) - case None => Some((0, content)) + case Some((ts, _)) => Some((ts + 1, new StringBuilder(content))) + case None => Some((0, new StringBuilder(content))) @JSExport("read") def read(path: Str): Str = val normalizedPath = Path(path) files.getOrElse(normalizedPath.toString, - throw new FileSystem.FileNotFoundException(normalizedPath))._2 + throw new FileSystem.FileNotFoundException(normalizedPath))._2.result() @JSExport("list") - def list: js.Array[Str] = allFiles.keys.toJSArray + def list: js.Array[Str] = files.keys.toJSArray /** Get all files (for debugging) */ - def allFiles: Map[String, String] = files.view.mapValues(_._2).toMap + def allFiles: Map[String, String] = files.view.mapValues(_._2.result()).toMap object InMemoryFileSystem: /** Create an empty in-memory file system. */ diff --git a/hkmc2/js/src/main/scala/hkmc2/io/node/NodeFileSystem.scala b/hkmc2/js/src/main/scala/hkmc2/io/node/NodeFileSystem.scala index 862157b1ca..cf38594a02 100644 --- a/hkmc2/js/src/main/scala/hkmc2/io/node/NodeFileSystem.scala +++ b/hkmc2/js/src/main/scala/hkmc2/io/node/NodeFileSystem.scala @@ -17,6 +17,9 @@ private[io] class NodeFileSystem extends FileSystem: def write(path: Path, content: String): Unit = fs.writeFileSync(path.toString, content) + def append(path: Path, content: String): Unit = + fs.appendFileSync(path.toString, content) + def exists(path: Path): Bool = fs.existsSync(path.toString) diff --git a/hkmc2/js/src/main/scala/hkmc2/io/node/package.scala b/hkmc2/js/src/main/scala/hkmc2/io/node/package.scala index e62dadc9ad..0397d2f554 100644 --- a/hkmc2/js/src/main/scala/hkmc2/io/node/package.scala +++ b/hkmc2/js/src/main/scala/hkmc2/io/node/package.scala @@ -19,6 +19,7 @@ package object node: object fs extends js.Object: def readFileSync(path: Str, encoding: Str): Str = js.native def writeFileSync(path: Str, data: Str): Unit = js.native + def appendFileSync(path: Str, data: Str): Unit = js.native def readdirSync(path: Str): js.Array[Str] = js.native def existsSync(path: Str): Bool = js.native def lstatSync(path: Str): Stats = js.native diff --git a/hkmc2/js/src/test/scala/hkmc2/io/VirtualPathTests.scala b/hkmc2/js/src/test/scala/hkmc2/io/VirtualPathTests.scala index 3015a2fa58..3043058630 100644 --- a/hkmc2/js/src/test/scala/hkmc2/io/VirtualPathTests.scala +++ b/hkmc2/js/src/test/scala/hkmc2/io/VirtualPathTests.scala @@ -170,3 +170,16 @@ class VirtualPathTests extends AnyFunSuite: assert(fs.read("/a/b.mls") == "initial") fs.write("/a/c/../b.mls", "updated") assert(fs.read("/a/./b.mls") == "updated") + + test("in-memory append preserves snapshots and updates normalized paths"): + val fs = new InMemoryFileSystem(Map.empty) + val path = Path("/a/b.log") + fs.append(path, "first") + val snapshot = fs.allFiles + val timestamp = fs.getLastChangedTimestamp(path) + fs.append(Path("/a/c/../b.log"), " second") + assert(fs.read("/a/b.log") == "first second") + assert(fs.getLastChangedTimestamp(path) == timestamp + 1) + assert(snapshot("/a/b.log") == "first", "Previously read contents must remain immutable") + fs.write(path, "replacement") + assert(fs.read(path) == "replacement") diff --git a/hkmc2/jvm/src/main/scala/hkmc2/io/PlatformFileSystem.scala b/hkmc2/jvm/src/main/scala/hkmc2/io/PlatformFileSystem.scala index c711e78395..c0f6bc548f 100644 --- a/hkmc2/jvm/src/main/scala/hkmc2/io/PlatformFileSystem.scala +++ b/hkmc2/jvm/src/main/scala/hkmc2/io/PlatformFileSystem.scala @@ -8,6 +8,8 @@ private[io] class JavaFileSystem extends FileSystem: def write(path: Path, content: String): Unit = os.write.over(unwrap(path), content) + def append(path: Path, content: String): Unit = os.write.append(unwrap(path), content) + def exists(path: Path): Bool = os.exists(unwrap(path)) def getLastChangedTimestamp(path: Path): Long = os.mtime(unwrap(path)) diff --git a/hkmc2/jvm/src/test/scala/hkmc2/CompilerCacheTest.scala b/hkmc2/jvm/src/test/scala/hkmc2/CompilerCacheTest.scala index 6fabb90896..e86319ffcf 100644 --- a/hkmc2/jvm/src/test/scala/hkmc2/CompilerCacheTest.scala +++ b/hkmc2/jvm/src/test/scala/hkmc2/CompilerCacheTest.scala @@ -138,6 +138,7 @@ class CompilerCacheTest extends AnyFunSuite: underlyingFs.read(path) def write(path: io.Path, content: String): Unit = underlyingFs.write(path, content) + def append(path: io.Path, content: String): Unit = underlyingFs.append(path, content) def exists(path: io.Path): Boolean = underlyingFs.exists(path) def getLastChangedTimestamp(path: io.Path): Long = underlyingFs.getLastChangedTimestamp(path) diff --git a/hkmc2/jvm/src/test/scala/hkmc2/DebugOutputHandlerTest.scala b/hkmc2/jvm/src/test/scala/hkmc2/DebugOutputHandlerTest.scala new file mode 100644 index 0000000000..7afd93e4b6 --- /dev/null +++ b/hkmc2/jvm/src/test/scala/hkmc2/DebugOutputHandlerTest.scala @@ -0,0 +1,34 @@ +package hkmc2 + +import org.scalatest.funsuite.AnyFunSuite + +import io.PlatformPath.given + + +class DebugOutputHandlerTest extends AnyFunSuite: + // Diff tests cover the emitted traces. This measures total I/O, which a golden file cannot + // distinguish from repeatedly rewriting the same growing log. + test("file debug output writes a linear amount of data"): + val dir = os.temp.dir(prefix = "debug-output-") + val underlying = io.FileSystem.default + var writtenChars = 0L + val fs = new io.FileSystem: + def read(path: io.Path): String = underlying.read(path) + def exists(path: io.Path): Boolean = underlying.exists(path) + def getLastChangedTimestamp(path: io.Path): Long = underlying.getLastChangedTimestamp(path) + def write(path: io.Path, content: String): Unit = + writtenChars += content.length + underlying.write(path, content) + def append(path: io.Path, content: String): Unit = + writtenChars += content.length + underlying.append(path, content) + try + os.write(dir / "trace.log", "old session contents") + val output = DebugOutputHandler(fs, dir, _ => fail("Unexpected standard output")) + val lines = (1 to 2000).map(i => s"debug line $i") + lines.foreach(output.emit(Config.DebugOutput.File("trace.log"), _)) + val expected = lines.mkString("", "\n", "\n") + assert(os.read(dir / "trace.log") == expected) + assert(writtenChars == expected.length, + "Only new output should be written, without copying accumulated contents") + finally os.remove.all(dir) diff --git a/hkmc2/shared/src/main/scala/hkmc2/CompilerCtx.scala b/hkmc2/shared/src/main/scala/hkmc2/CompilerCtx.scala index f35d234056..05b766edae 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/CompilerCtx.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/CompilerCtx.scala @@ -31,6 +31,9 @@ class CompilerCtx( * symbols they define — must not depend on which of them is compiled or imported first. */ val rootConfig: Config, ): + private val outputHandler: DebugOutputHandler = importing match + case S((_, parent)) => parent.outputHandler + case N => DebugOutputHandler(fs, rootConfig.baseDir, println) def allFilesBeingImported: Ls[io.Path] = importing match @@ -85,7 +88,6 @@ class CompilerCtx( includeZero = true, )) ) - val outputHandler = DebugOutputHandler(fs, rootConfig.baseDir, println) val parse = given CompilerCtx = this @@ -279,7 +281,6 @@ class CompilerCtx( given Config = rootConfig given CompilerCtx = this given DebugPrinter = new DebugPrinter - val outputHandler = DebugOutputHandler(fs, rootConfig.baseDir, println) val parse = ParserSetup(file, outputHandler) val elaborationConfig = Config.elaborationConfig(parse.resultBlk) stateDebug = CompilerCtx.debugTracingEnabled(elaborationConfig.debug) diff --git a/hkmc2/shared/src/main/scala/hkmc2/Config.scala b/hkmc2/shared/src/main/scala/hkmc2/Config.scala index ed93749cfd..96e277fc35 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/Config.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/Config.scala @@ -275,7 +275,7 @@ object Config: case App(Directive(prefix, _), args) => modifier(Directive(prefix, args)) case Directive(Ident("config"), Tup(args)) => ConfigParser.parseOverrides(args) case Directive(Ident("lang"), Tup(args)) => ConfigParser.parseLanguageDirective(args) - case Directive(Ident("dbg"), Tup(args)) => ConfigParser.parseDebugDirective(args) + case Directive(Ident("dbg"), Tup(args)) => ConfigParser.parseDebugDirective(args, definitionLocal = false) case _ => identity trees.foldLeft(config)((cfg, tree) => modifier(tree)(cfg)) @@ -534,7 +534,11 @@ object ConfigParser: expect("StdIO or File(\"path\")")(tree) N - private def updateDebugFlag(name: Str, value: Bool, tree: Tree)(using Raise): Config.Debug => Config.Debug = + private def updateDebugFlag(name: Str, value: Bool, tree: Tree, definitionLocal: Bool)(using Raise): Config.Debug => Config.Debug = + if definitionLocal && (name === "parsing" || name === "optimizations") then + raise(WarningReport( + msg"Debugging aspect '${name}' is not supported in @dbg; use #dbg(${name}: ${value.toString}) instead." -> tree.toLoc :: Nil, + source = Diagnostic.Source.Compilation)) name match case "parsing" => _.copy(parsing = value) case "showParsedTree" => _.copy(showParsedTree = value) @@ -553,27 +557,27 @@ object ConfigParser: source = Diagnostic.Source.Compilation)) identity - private def parseDebugArg(arg: Tree)(using Raise): Config.Debug => Config.Debug = arg match + private def parseDebugArg(arg: Tree, definitionLocal: Bool)(using Raise): Config.Debug => Config.Debug = arg match case Quoted(Ident(name)) => - updateDebugFlag(name, true, arg) + updateDebugFlag(name, true, arg, definitionLocal) case Ident(name) if name.startsWith("'") => - updateDebugFlag(name.drop(1), true, arg) + updateDebugFlag(name.drop(1), true, arg, definitionLocal) case NamedArg("out", value) => parseDebugOutput(value) match case S(out) => _.copy(out = out) case N => identity case NamedArg(name, value) => parseBool(value) match - case S(enabled) => updateDebugFlag(name, enabled, arg) + case S(enabled) => updateDebugFlag(name, enabled, arg, definitionLocal) case N => identity case _ => unsupported("debugging", arg) identity /** Parse `#dbg(...)` and `@dbg(...)` arguments into a cumulative debug configuration. */ - def parseDebugDirective(args: Ls[Tree])(using Raise): Config => Config = + def parseDebugDirective(args: Ls[Tree], definitionLocal: Bool)(using Raise): Config => Config = val modifyDebug = args.foldLeft(identity[Config.Debug]): (acc, arg) => - val modify = parseDebugArg(arg) + val modify = parseDebugArg(arg, definitionLocal) debug => modify(acc(debug)) new (Config => Config): def apply(cfg: Config): Config = cfg.copy(debug = modifyDebug(cfg.debug)) @@ -583,7 +587,7 @@ object ConfigParser: def discoverDebugFromAnnotations(annotations: Ls[Tree], base: Config): Opt[Config] = given Raise = _ => () val modifiers = annotations.collect: - case App(Ident("dbg"), Tup(args)) => parseDebugDirective(args) + case App(Ident("dbg"), Tup(args)) => parseDebugDirective(args, definitionLocal = true) if modifiers.isEmpty then N else S(modifiers.foldLeft(base)((cfg, modify) => modify(cfg))) diff --git a/hkmc2/shared/src/main/scala/hkmc2/DebugOutputHandler.scala b/hkmc2/shared/src/main/scala/hkmc2/DebugOutputHandler.scala index 50406332b9..dc496bec52 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/DebugOutputHandler.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/DebugOutputHandler.scala @@ -7,19 +7,20 @@ import hkmc2.utils.*, shorthands.* /** Routes compiler debugging output without putting callbacks or mutable streams in [[Config]]. * - * File output is accumulated per handler and rewritten through the compiler file-system - * abstraction. This gives every compilation session truncate-on-first-use semantics and also - * works for the Scala.js in-memory file system, which has no append operation. */ + * A session shares one handler across compilation units: each destination is cleared on first + * use, then appended to without retaining or rewriting previous output. Concurrent debug writes + * are best effort; this handler does not synchronize them. */ final class DebugOutputHandler(fs: io.FileSystem, baseDir: io.Path, stdIO: Str => Unit): - private val fileContents = mutable.Map.empty[io.Path, StringBuilder] + private val writtenFiles = mutable.Set.empty[io.Path] def emit(out: Config.DebugOutput, line: Str): Unit = out match case Config.DebugOutput.StdIO => stdIO(line) case Config.DebugOutput.File(path) => val file = baseDir / io.RelPath(path) - val contents = fileContents.getOrElseUpdate(file, new StringBuilder) - contents.append(line).append('\n') - fs.write(file, contents.result()) + if writtenFiles(file) then fs.append(file, line + '\n') + else + fs.write(file, line + '\n') + writtenFiles += file object DebugOutputHandler: diff --git a/hkmc2/shared/src/main/scala/hkmc2/codegen/Lowering.scala b/hkmc2/shared/src/main/scala/hkmc2/codegen/Lowering.scala index 9623cca46e..aede9d211d 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/codegen/Lowering.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/codegen/Lowering.scala @@ -410,7 +410,9 @@ class Lowering()(using Config, TL, Raise, State, Ctx, SymbolPrinter)(using Erasu def defineClass(parent: Opt[Path], parentParams: Ls[ParamList]): Block = val base = parent.getOrElse( Select(State.globalThisSymbol.asThis, Tree.Ident("Object"))(S(ctx.builtins.Object), defn.toLoc)(false)) - val clsDef = LoweringCtx.withSuperBase(defn.sym, base).givenIn: + // Class and module companions share one lowered definition, so class tracing may + // include its companion. Explicit module debug settings still override that scope. + val clsDef = debugLowering(defn.extraAnnotations)(LoweringCtx.withSuperBase(defn.sym, base).givenIn { val (mtds, publicFlds, privateFlds, ctor) = defn match case pd: PatternDef => // Compile the pattern definition into `unapply` and `unapplyStringPrefix` @@ -440,7 +442,7 @@ class Lowering()(using Config, TL, Raise, State, Ctx, SymbolPrinter)(using Erasu ) case N => val (mtds, publicFlds, privateFlds, ctor) = - gatherMembers(mod.body) + debugLowering(mod.extraAnnotations)(gatherMembers(mod.body)) S(ClsLikeBody(mod.sym, mtds, privateFlds, publicFlds, ctor, mod.annotations)) case _ => N case _ => N @@ -449,13 +451,15 @@ class Lowering()(using Config, TL, Raise, State, Ctx, SymbolPrinter)(using Erasu val cfgOverride = definitionConfigOverride(defn.extraAnnotations) ClsLikeDefn(defn.owner, defn.sym, defn.bsym, defn.ctorSym, defn.kind, defn.paramsOpt, defn.auxParams, parent, mtds, privateFlds, publicFlds, pctor, ctor, mod, bufferable)(cfgOverride, defn.annotations) + }) Define(clsDef, blockImpl(stats, res)) defn.ext match case N => defineClass(N, Nil) case S(ext) => assert(k isnt syntax.Mod) // modules can't extend things and can't have super calls - classOf(ext.cls, ext): (clsp, params) => - withClassParent(clsp)(parent => defineClass(S(parent), params)) + debugLowering(defn.extraAnnotations): + classOf(ext.cls, ext): (clsp, params) => + withClassParent(clsp)(parent => defineClass(S(parent), params)) case td: TypeDef => // * Type definitions are erased blockImpl(stats, res) diff --git a/hkmc2/shared/src/main/scala/hkmc2/io/FileSystem.scala b/hkmc2/shared/src/main/scala/hkmc2/io/FileSystem.scala index 17368b1678..da1cc73c43 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/io/FileSystem.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/io/FileSystem.scala @@ -18,6 +18,9 @@ trait FileSystem: /** Write string to file, overwriting if exists. */ def write(path: Path, content: String): Unit + /** Append string to file, creating it if missing. */ + def append(path: Path, content: String): Unit + /** Check if a file exists at the given path. */ def exists(path: Path): Bool diff --git a/hkmc2/shared/src/main/scala/hkmc2/semantics/Elaborator.scala b/hkmc2/shared/src/main/scala/hkmc2/semantics/Elaborator.scala index 37ffb3a314..62f3d2023b 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/semantics/Elaborator.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/semantics/Elaborator.scala @@ -825,7 +825,7 @@ extends Importer: val modify = ConfigParser.parseOverrides(args) S(Annot.Config(modify)(tree.toLoc)) case App(Ident("dbg"), Tup(args)) => - val modify = ConfigParser.parseDebugDirective(args) + val modify = ConfigParser.parseDebugDirective(args, definitionLocal = true) S(Annot.Debug(modify)(tree.toLoc)) case App(Ident("affine"), Tup(IntLit(whichParamList) :: Nil)) => S(Annot.Affine(whichParamList.toInt)(tree.toLoc)) @@ -2490,7 +2490,7 @@ extends Importer: case Annot.Modifier(Keyword.`data`) => true case _ => false - val pss = td.paramLists.map: ps => + val pss = debugElaboration(annotations)(td.paramLists.map { ps => val (res, newCtx2) = given Ctx = newCtx params(ps, isDataClass, k is Pat) @@ -2500,6 +2500,7 @@ extends Importer: raise(ErrorReport( msg"Spread parameters are not supported in class parameters." -> rp.toLoc :: Nil)) res.copy(restParam = N)(res.toLoc) + }) def withFields(extraParams: Ls[ParamList])(using Ctx)(fn: (Ctx) ?=> (Term.Blk, Ctx)): (Term.Blk, Ctx) = softAssert(pss.sizeCompare(td.clsParams) === 0, @@ -2606,7 +2607,7 @@ extends Importer: msg"Illegal body of ${k.desc} definition (should be a block; found ${t.describe})." -> t.toLoc :: Nil)) (new Blk(Nil, Term.Lit(UnitLit(false))), ctx) - val defn = k match + val defn = debugElaboration(annotations)(k match case Als => val alsSym = td.symbol.asInstanceOf[TypeAliasSymbol] // TODO improve `asInstanceOf` // newCtx.nest(S(alsSym)).givenIn: @@ -2747,6 +2748,7 @@ extends Importer: clsSym.defn = S(cd) cd case Trt | Mxn => lastWords(s"Unexpected type definition kind here: $k") + ) go(sts, Nil, defn :: acc)(N) case Annotated(annotation, target) :: sts => go(target :: sts, annotations ++ annot(annotation), acc)(statementLoc) @@ -2764,7 +2766,7 @@ extends Importer: go(sts, Nil, SetConfig(modify)(statementLoc) :: acc)(N) case Directive(Ident("dbg"), Tup(args)) :: sts => reportUnusedAnnotations - val modify = ConfigParser.parseDebugDirective(args) + val modify = ConfigParser.parseDebugDirective(args, definitionLocal = false) go(sts, Nil, SetConfig(modify)(statementLoc) :: acc)(N) case Directive(Ident(name), _) :: sts => raise(ErrorReport( diff --git a/hkmc2/shared/src/test/mlscript/codegen/DebugImports.mls b/hkmc2/shared/src/test/mlscript/codegen/DebugImports.mls new file mode 100644 index 0000000000..3f8deacfeb --- /dev/null +++ b/hkmc2/shared/src/test/mlscript/codegen/DebugImports.mls @@ -0,0 +1,26 @@ +:js + + +// Generate imported units after test discovery; they are fixtures, not standalone diff tests. +:silent +val debugImportDir = "hkmc2/shared/src/test/mlscript/codegen/" +fs.writeFileSync(debugImportDir + "DebugImports.log", "old session contents") +fs.writeFileSync(debugImportDir + "DebugImportA.mls", "@dbg('lowering, showUids: false, out: File(\"mlscript/codegen/DebugImports.log\"))\nmodule DebugImportA with\n fun value() = 2101\n") +fs.writeFileSync(debugImportDir + "DebugImportB.mls", "import \"./DebugImportA.mls\"\n@dbg('lowering, showUids: false, out: File(\"mlscript/codegen/DebugImports.log\"))\nmodule DebugImportB with\n fun value() = 2102\n") + + +// Importing B also compiles A. Both must contribute to the same log in this session. +:noCodeGen +import "./DebugImportB.mls" + + +:silent +:expect true +val importLog = fs.readFileSync(debugImportDir + "DebugImports.log", "utf8") +importLog.includes("Lowering.term 2101") && importLog.includes("Lowering.term 2102") && not(importLog.includes("old session contents")) + + +:silent +fs.rmSync(debugImportDir + "DebugImportA.mls", {force: true}) +fs.rmSync(debugImportDir + "DebugImportB.mls", {force: true}) +fs.rmSync(debugImportDir + "DebugImports.log", {force: true}) diff --git a/hkmc2/shared/src/test/mlscript/codegen/DebugParsingDirective.mls b/hkmc2/shared/src/test/mlscript/codegen/DebugParsingDirective.mls index 260d5f3fb5..1264ef3cb8 100644 --- a/hkmc2/shared/src/test/mlscript/codegen/DebugParsingDirective.mls +++ b/hkmc2/shared/src/test/mlscript/codegen/DebugParsingDirective.mls @@ -3,7 +3,7 @@ #dbg('parsing) //│ ┊#┊dbg┊(⟨┊'parsing┊⟩)┊ -//│ @ blockOf(start of expression,allowNewlines = true) [at syntax/Parser.scala:54] +//│ @ blockOf(start of expression,allowNewlines = true) [at syntax/Parser.scala:55] //│ │ ? blockOfImpl inspects ┊#┊dbg┊(⟨┊'parsing┊⟩)┊ [at syntax/Parser.scala:340] //│ │ ! blockOfImpl consumes ┊#┊ [at syntax/Parser.scala:367] //│ │ @ parseRule(39,'#' directive keyword ::= Expr('#' directive body ::= End()),allowNewlines = true) [at syntax/Parser.scala:380] diff --git a/hkmc2/shared/src/test/mlscript/codegen/DebugTypes.mls b/hkmc2/shared/src/test/mlscript/codegen/DebugTypes.mls new file mode 100644 index 0000000000..aa60ee27d8 --- /dev/null +++ b/hkmc2/shared/src/test/mlscript/codegen/DebugTypes.mls @@ -0,0 +1,66 @@ +:js + + +:silent +val debugTypesDir = "hkmc2/shared/src/test/mlscript/codegen/" +fs.rmSync(debugTypesDir + "DebugTypesElaboration.log", {force: true}) +fs.rmSync(debugTypesDir + "DebugTypesLowering.log", {force: true}) + + +// Type definitions trace their parameters and bodies, including nested methods. +:silent +@dbg('elaboration, showUids: false, out: File("DebugTypesElaboration.log")) +class ElaborationClass(value: Int) with + fun answer() = 1101 +@dbg('elaboration, showUids: false, out: File("DebugTypesElaboration.log")) +module ElaborationModule with + fun answer() = 1102 + + +:silent +:expect true +val elaborationLog = fs.readFileSync(debugTypesDir + "DebugTypesElaboration.log", "utf8") +elaborationLog.includes("Elaborating ParamTree") && elaborationLog.includes("IntLit(1101)") && elaborationLog.includes("IntLit(1102)") + + +// A class is lowered at its companion's definition. Both annotations must take effect. +:silent +@dbg('lowering, showUids: false, out: File("DebugTypesLowering.log")) +class LoweringCompanion(value) with + fun answer() = 1201 +@dbg('lowering, showUids: false, out: File("DebugTypesLowering.log")) +module LoweringCompanion with + fun answer() = 1202 +@dbg('lowering, showUids: false, out: File("DebugTypesLowering.log")) +module LoweringModule with + fun answer() = 1203 + + +:silent +:expect true +val loweringLog = fs.readFileSync(debugTypesDir + "DebugTypesLowering.log", "utf8") +loweringLog.includes("Lowering.term 1201") && loweringLog.includes("Lowering.term 1202") && loweringLog.includes("Lowering.term 1203") + + +// New resolution runs during elaboration; class tracing includes superclass lowering too. +#lang(0.3.x) + +:silent +class DebugBase(value: Int) +@dbg('resolution, showUids: false, out: File("DebugTypesElaboration.log")) +module ResolutionModule with + fun answer() = 1301 +@dbg('resolution, 'lowering, showUids: false, out: File("DebugTypesLowering.log")) +class ResolutionClass extends DebugBase(1302) with + fun answer() = 1303 + + +:silent +:expect true +val newResolutionLog = fs.readFileSync(debugTypesDir + "DebugTypesLowering.log", "utf8") +fs.readFileSync(debugTypesDir + "DebugTypesElaboration.log", "utf8").includes("IntLit(1301)") && newResolutionLog.includes("IntLit(1303)") && newResolutionLog.includes("Lowering.term 1302") && newResolutionLog.includes("Lowering.term 1303") + + +:silent +fs.rmSync(debugTypesDir + "DebugTypesElaboration.log", {force: true}) +fs.rmSync(debugTypesDir + "DebugTypesLowering.log", {force: true}) diff --git a/hkmc2/shared/src/test/mlscript/codegen/DebugUnsupportedPhases.mls b/hkmc2/shared/src/test/mlscript/codegen/DebugUnsupportedPhases.mls index 14d6a89285..4fe5cfd7ee 100644 --- a/hkmc2/shared/src/test/mlscript/codegen/DebugUnsupportedPhases.mls +++ b/hkmc2/shared/src/test/mlscript/codegen/DebugUnsupportedPhases.mls @@ -9,45 +9,45 @@ fs.rmSync(debugDir + "DebugModuleElaboration.log", {force: true}) fs.rmSync(debugDir + "DebugClassLowering.log", {force: true}) -// FIXME: parsing traces currently inspect only #dbg, ignoring definition annotations. -:fixme -:expect true +// Parsing needs a file-level directive; the annotation warns and does not create a log. +:w +:expect false @dbg('parsing, out: File("DebugLocalParsing.log")) fun parse_selected(x) = x fs.existsSync(debugDir + "DebugLocalParsing.log") -//│ ═══[RUNTIME ERROR] Expected: 'true', got: 'false' +//│ ╔══[WARNING] Debugging aspect 'parsing' is not supported in @dbg; use #dbg(parsing: true) instead. +//│ ║ l.15: @dbg('parsing, out: File("DebugLocalParsing.log")) fun parse_selected(x) = x +//│ ╙── ^^^^^^^^ //│ = false -// FIXME: optimization trace loggers never consult definition-local debug settings. -:fixme -:expect true +// Optimization passes do not support definition-local tracing either. +:w +:expect false @dbg('optimizations, out: File("DebugLocalOptimization.log")) fun optimize_selected(x) = ((y) => y)(x) fs.existsSync(debugDir + "DebugLocalOptimization.log") -//│ ═══[RUNTIME ERROR] Expected: 'true', got: 'false' +//│ ╔══[WARNING] Debugging aspect 'optimizations' is not supported in @dbg; use #dbg(optimizations: true) instead. +//│ ║ l.26: @dbg('optimizations, out: File("DebugLocalOptimization.log")) +//│ ╙── ^^^^^^^^^^^^^^ //│ = false -// FIXME: type definitions bypass the elaborator's definition-local trace scope. -:fixme +// Module elaboration honors definition-local tracing. :expect true @dbg('elaboration, out: File("DebugModuleElaboration.log")) module ElaborationSelected with fun member(x) = x fs.existsSync(debugDir + "DebugModuleElaboration.log") -//│ ═══[RUNTIME ERROR] Expected: 'true', got: 'false' -//│ = false +//│ = true -// FIXME: lowering a class bypasses its definition-local trace scope. -:fixme +// Class lowering honors definition-local tracing. :expect true @dbg('lowering, out: File("DebugClassLowering.log")) class LoweringSelected(x) with fun member() = x fs.existsSync(debugDir + "DebugClassLowering.log") -//│ ═══[RUNTIME ERROR] Expected: 'true', got: 'false' -//│ = false +//│ = true :silent @@ -55,3 +55,26 @@ fs.rmSync(debugDir + "DebugLocalParsing.log", {force: true}) fs.rmSync(debugDir + "DebugLocalOptimization.log", {force: true}) fs.rmSync(debugDir + "DebugModuleElaboration.log", {force: true}) fs.rmSync(debugDir + "DebugClassLowering.log", {force: true}) + + +// Unsupported local settings also warn when explicitly disabled. +:w +@dbg(parsing: false, optimizations: false) fun disabled_local(x) = x +//│ ╔══[WARNING] Debugging aspect 'parsing' is not supported in @dbg; use #dbg(parsing: false) instead. +//│ ║ l.62: @dbg(parsing: false, optimizations: false) fun disabled_local(x) = x +//│ ╙── ^^^^^^^^^^^^^^ +//│ ╔══[WARNING] Debugging aspect 'optimizations' is not supported in @dbg; use #dbg(optimizations: false) instead. +//│ ║ l.62: @dbg(parsing: false, optimizations: false) fun disabled_local(x) = x +//│ ╙── ^^^^^^^^^^^^^^^^^^^^ + + +// A warning about one phase does not prevent a supported phase from tracing. +:w +:expect 57 +@dbg('parsing, 'lowering, showUids: false) fun mixed_local() = 57 +mixed_local() +//│ ╔══[WARNING] Debugging aspect 'parsing' is not supported in @dbg; use #dbg(parsing: true) instead. +//│ ║ l.74: @dbg('parsing, 'lowering, showUids: false) fun mixed_local() = 57 +//│ ╙── ^^^^^^^^ +//│ Lowering.term 57 +//│ = 57 From 0b1850e771ad1b4e8c198b42be3a49c8265ee844 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 16 Oct 2026 16:24:08 +0800 Subject: [PATCH 09/32] Add regression tests for debug directive bugs found in review Reproduce, with their current wrong output: - diff-test commands (`:dl`, `:de`, `:dr`) muted inside definitions whose `@dbg` annotation does not enable the same phase; - lowering traces of a class with a superclass leaking into the statements that follow the class; - `:dp` parse traces redirected to a `#dbg(out: File(...))` destination; - absolute `File(...)` destinations crashing the compiler; - value definitions with parameter lists crashing lowering. Co-Authored-By: Claude Opus 5.5 --- .../src/test/mlscript/basics/BadDefs.mls | 12 +++++ .../mlscript/codegen/DebugDestinations.mls | 43 ++++++++++++++++ .../test/mlscript/codegen/DebugScoping.mls | 50 +++++++++++++++++++ 3 files changed, 105 insertions(+) diff --git a/hkmc2/shared/src/test/mlscript/basics/BadDefs.mls b/hkmc2/shared/src/test/mlscript/basics/BadDefs.mls index 196190f34a..74d6e05b3c 100644 --- a/hkmc2/shared/src/test/mlscript/basics/BadDefs.mls +++ b/hkmc2/shared/src/test/mlscript/basics/BadDefs.mls @@ -85,3 +85,15 @@ trait F //│ /!!!\ Uncaught error: scala.NotImplementedError: an implementation is missing +// Value definitions do not take parameter lists. +:fixme +val badValue(x) = x +//│ /!!!\ Uncaught error: java.lang.AssertionError: assertion failed + +:fixme +fun localBadValue() = + val badLocal(x) = x + badLocal +//│ /!!!\ Uncaught error: java.lang.AssertionError: assertion failed + + diff --git a/hkmc2/shared/src/test/mlscript/codegen/DebugDestinations.mls b/hkmc2/shared/src/test/mlscript/codegen/DebugDestinations.mls index 8be708c876..89e6afc504 100644 --- a/hkmc2/shared/src/test/mlscript/codegen/DebugDestinations.mls +++ b/hkmc2/shared/src/test/mlscript/codegen/DebugDestinations.mls @@ -61,3 +61,46 @@ fs.readFileSync("hkmc2/shared/src/test/mlscript/codegen/DebugDestinationsParse.l :silent fs.rmSync("hkmc2/shared/src/test/mlscript/codegen/DebugDestinationsParse.log", {force: true}) + + +// Diff-test commands write to the test output, even when #dbg selects a file destination. +:silent +fs.rmSync("hkmc2/shared/src/test/mlscript/codegen/DebugDestinationsCommand.log", {force: true}) + +:noCodeGen +#dbg(out: File("DebugDestinationsCommand.log")) + +:noCodeGen +:dp +1 + +#dbg(out: StdIO) + +:fixme +:expect false +fs.existsSync("hkmc2/shared/src/test/mlscript/codegen/DebugDestinationsCommand.log") +//│ ═══[RUNTIME ERROR] Expected: 'false', got: 'true' +//│ = true + +:silent +fs.rmSync("hkmc2/shared/src/test/mlscript/codegen/DebugDestinationsCommand.log", {force: true}) + + +// Absolute file destinations are not resolved against the base directory, as for imports. +:silent +val absoluteDebugFile = "/tmp/mlscript-DebugDestinationsAbsolute.log" +fs.rmSync(absoluteDebugFile, {force: true}) + +:fixme +@dbg('lowering, showUids: false, out: File("/tmp/mlscript-DebugDestinationsAbsolute.log")) +fun absolute_file() = 1801 +//│ /!!!\ Uncaught error: java.lang.IllegalArgumentException: requirement failed: /tmp/mlscript-DebugDestinationsAbsolute.log is not a relative path + +:fixme +:expect true +fs.existsSync(absoluteDebugFile) and fs.readFileSync(absoluteDebugFile, "utf8").includes("Lowering.term 1801\n") +//│ ═══[RUNTIME ERROR] Expected: 'true', got: 'false' +//│ = false + +:silent +fs.rmSync(absoluteDebugFile, {force: true}) diff --git a/hkmc2/shared/src/test/mlscript/codegen/DebugScoping.mls b/hkmc2/shared/src/test/mlscript/codegen/DebugScoping.mls index 59fe8743ae..1b3f61e5d7 100644 --- a/hkmc2/shared/src/test/mlscript/codegen/DebugScoping.mls +++ b/hkmc2/shared/src/test/mlscript/codegen/DebugScoping.mls @@ -29,3 +29,53 @@ DebugMethods.selected(7) //│ Lowering.term member:debug_inner⁰‹term:debug_inner› – symbol term:debug_inner //│ Expanding term Resolved(Ref(member:debug_inner),term:debug_inner) //│ ~> Resolved(Ref(member:debug_inner),term:debug_inner) + + +// Diff-test debug commands trace the whole block. A definition's @dbg annotation that does not +// enable the corresponding phase must not mute them inside that definition. +:dl +@dbg(showUids: false) fun command_lowering() = 1601 +//│ Lowering.term undefined +//│ avoidNames Scoped(Set(member:command_lowering),Define(FunDefn(None,member:command_lowering,term:command_lowerin... +//│ | avoidNames Define(FunDefn(None,member:command_lowering,term:command_lowering,List(ParamList(‹›,List(),None)),Re... +//│ | | avoidNames Return(Lit(IntLit(1601)))... +//│ | | avoidNames End()... + +:noCodeGen +:de +@dbg('lowering) fun command_elaboration(x) = x +//│ Elab block List(Annotated(App(Ident(dbg),Tup(List(Ident('lowering)))),TermDef(Fun,App(Ident(command_elaboration[...] LocalScope(block:6) +//│ ~> { fun member:command_elaboration(x¹) = x¹; } + +:noCodeGen +:dr +@dbg('lowering) fun command_resolution(x) = x +//│ Traversing block: Blk(List(TermDefinition(Fun,member:command_resolution,term:command_resolution,List(ParamList(‹›,List(Param(‹›,x,None,Modulefulness(None))),None)),None,None,Some(Ref(x)),‹›,Modulefulness(None),List(Debug(...)),None)),Lit(UnitLit(false))) +//│ | Traversing statement: TermDefinition(Fun,member:command_resolution,term:command_resolution,List(ParamList(‹›,List(Param(‹›,x,None,Modulefulness(None))),None)),None,None,Some(Ref(x)),‹›,Modulefulness(None),List(Debug(...)),None) +//│ | Traversing term: Lit(UnitLit(false)) (expect = Any) +//│ | | Checking Lit(UnitLit(false)): expect Any, evalsToModule = false, evalsToStaticClass = false + + +// A class's lowering trace covers the evaluation of its superclass, +// but not the statements that follow the class definition. +class DebugParent +@dbg('lowering, showUids: false) class DebugChild extends DebugParent +print(1701) +//│ Lowering.term member:DebugParent⁰ – symbol class:DebugParent +//│ Expanding term Ref(member:DebugParent) +//│ ~> Resolved(Ref(member:DebugParent),class:DebugParent) +//│ Lowering.term { } (in stmt) +//│ Lowering.term undefined (in stmt) +//│ Lowering.term (member:Predef⁰.)print‹member:print›(1701) +//│ Expanding term App(SynthSel(Ref(member:Predef),Ident(print)),Tup(List(Fld(‹›,Lit(IntLit(1701)),None)))) +//│ ~> App(SynthSel(Ref(member:Predef),Ident(print)),Tup(List(Fld(‹›,Lit(IntLit(1701)),None)))) +//│ Expanding term SynthSel(Ref(member:Predef),Ident(print)) +//│ ~> Resolved(SynthSel(Ref(member:Predef),Ident(print)),term:Predef/print) +//│ Lowering.term (member:Predef⁰.)print‹term:Predef/print› – symbol term:Predef/print +//│ Expanding term Resolved(SynthSel(Ref(member:Predef),Ident(print)),term:Predef/print) +//│ ~> Resolved(SynthSel(Ref(member:Predef),Ident(print)),term:Predef/print) +//│ Lowering.term member:Predef⁰ – symbol module:Predef +//│ Expanding term Ref(member:Predef) +//│ ~> Resolved(Ref(member:Predef),module:Predef) +//│ Lowering.term 1701 +//│ > 1701 From 8a9099c419b58646623108bfd634c1ee3213089d Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 16 Oct 2026 16:25:54 +0800 Subject: [PATCH 10/32] Keep diff-test debug commands active inside @dbg definitions A definition-local `@dbg` override replaced the trace logger's and the elaboration state's enablement wholesale, so an annotation that did not enable a phase (e.g. `@dbg(showUids: false)` under `:dl`) silently muted the block-wide tracing requested by `:dl`, `:de`, `:dr`, `:d`, or UCS scopes inside that definition. Separate the debugging that diff-test commands force (`doForceTrace`, `doForceDbg`) from the configurable part: overrides can still enable a phase, and disable a phase enabled by `#dbg`, but forced debugging stays on and keeps writing to standard output. Co-Authored-By: Claude Opus 5.5 --- .../scala/hkmc2/semantics/Elaborator.scala | 8 +++++++- .../src/main/scala/utils/TraceLogger.scala | 17 ++++++++++++++-- .../test/mlscript/codegen/DebugScoping.mls | 20 +++++++++++++++++++ .../test/scala/hkmc2/JSBackendDiffMaker.scala | 3 ++- .../src/test/scala/hkmc2/MLsDiffMaker.scala | 14 ++++++++----- 5 files changed, 53 insertions(+), 9 deletions(-) diff --git a/hkmc2/shared/src/main/scala/hkmc2/semantics/Elaborator.scala b/hkmc2/shared/src/main/scala/hkmc2/semantics/Elaborator.scala index 62f3d2023b..d8a13ed018 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/semantics/Elaborator.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/semantics/Elaborator.scala @@ -724,8 +724,14 @@ object Elaborator: // whether symbol identities are useful in that trace, so both settings share the same scope. private var debugOverrides = Ls.empty[(Bool, Bool)] protected def doDbg: Bool = false + /** Debugging requested independently of the [[Config.Debug]] settings, such as by diff-test + * commands; as for `TraceLogger.doForceTrace`, definition-local overrides cannot disable it. + * Implementations should include it in [[doDbg]]. */ + protected def doForceDbg: Bool = false protected def doShowUids: Bool = true - final def dbg: Bool = debugOverrides.headOption.map(_._1).getOrElse(doDbg) + final def dbg: Bool = debugOverrides match + case (enabled, _) :: _ => enabled || doForceDbg + case Nil => doDbg final def showUids: Bool = debugOverrides.headOption.map(_._2).getOrElse(doShowUids) def scopedDebug[T](enabled: Bool, showUids: Bool)(thunk: => T): T = debugOverrides ::= enabled -> showUids diff --git a/hkmc2/shared/src/main/scala/utils/TraceLogger.scala b/hkmc2/shared/src/main/scala/utils/TraceLogger.scala index ddeb9864d3..bd2b9478c8 100644 --- a/hkmc2/shared/src/main/scala/utils/TraceLogger.scala +++ b/hkmc2/shared/src/main/scala/utils/TraceLogger.scala @@ -10,12 +10,25 @@ def tl(using TL): TL = summon abstract class TraceLogger(using val debugPrinter: DebugPrinter): def doTrace: Bool = true + /** Tracing requested independently of the [[Config.Debug]] settings, such as by diff-test + * commands, which trace a whole phase. Definition-local `@dbg` annotations can neither disable + * nor redirect it: their overrides only decide whether the phase is traced *in addition* to it. + * Implementations should include it in [[doTrace]], which also governs code outside annotations. */ + protected def doForceTrace: Bool = false protected def defaultDebugOutput: Config.DebugOutput = Config.DebugOutput.StdIO private var debugOverrides: Ls[(Bool, Config.Debug)] = Nil /** Whether tracing is enabled after applying the innermost definition-local override. */ - final def isTracing: Bool = debugOverrides.headOption.fold(doTrace)(_._1) + final def isTracing: Bool = debugOverrides match + case (enabled, _) :: _ => enabled || doForceTrace + case Nil => doTrace + + /** An override that does not enable the phase leaves only forced tracing, which goes to `StdIO`. */ + private def debugOutput: Config.DebugOutput = debugOverrides match + case (true, debug) :: _ => debug.out + case (false, _) :: _ => Config.DebugOutput.StdIO + case Nil => defaultDebugOutput /** An inner annotation changes only the settings it names in the enclosing debug scope. */ final def debugConfig(base: Config): Config = @@ -48,7 +61,7 @@ abstract class TraceLogger(using val debugPrinter: DebugPrinter): if isTracing then emitDbg( if noIndent then msg.toString else "| " * indent + msg.toString.indentNewLines("| " * indent + "> "), - debugOverrides.headOption.fold(defaultDebugOutput)(_._2.out), + debugOutput, ) /** Temporarily select both tracing enablement and its destination. diff --git a/hkmc2/shared/src/test/mlscript/codegen/DebugScoping.mls b/hkmc2/shared/src/test/mlscript/codegen/DebugScoping.mls index 1b3f61e5d7..9a1db259c1 100644 --- a/hkmc2/shared/src/test/mlscript/codegen/DebugScoping.mls +++ b/hkmc2/shared/src/test/mlscript/codegen/DebugScoping.mls @@ -35,6 +35,7 @@ DebugMethods.selected(7) // enable the corresponding phase must not mute them inside that definition. :dl @dbg(showUids: false) fun command_lowering() = 1601 +//│ Lowering.term 1601 //│ Lowering.term undefined //│ avoidNames Scoped(Set(member:command_lowering),Define(FunDefn(None,member:command_lowering,term:command_lowerin... //│ | avoidNames Define(FunDefn(None,member:command_lowering,term:command_lowering,List(ParamList(‹›,List(),None)),Re... @@ -45,6 +46,12 @@ DebugMethods.selected(7) :de @dbg('lowering) fun command_elaboration(x) = x //│ Elab block List(Annotated(App(Ident(dbg),Tup(List(Ident('lowering)))),TermDef(Fun,App(Ident(command_elaboration[...] LocalScope(block:6) +//│ | Processing term definition App(Ident(command_elaboration),Tup(List(Ident(x)))) +//│ | Elaborating ParamTree: ParamTree(‹›,Ident(x),None,None,Set()) +//│ | Elab term Ident(x) +//│ | | Elab subterm Ident(x) +//│ | | ~> x¹ +//│ | ~> x¹ //│ ~> { fun member:command_elaboration(x¹) = x¹; } :noCodeGen @@ -52,6 +59,19 @@ DebugMethods.selected(7) @dbg('lowering) fun command_resolution(x) = x //│ Traversing block: Blk(List(TermDefinition(Fun,member:command_resolution,term:command_resolution,List(ParamList(‹›,List(Param(‹›,x,None,Modulefulness(None))),None)),None,None,Some(Ref(x)),‹›,Modulefulness(None),List(Debug(...)),None)),Lit(UnitLit(false))) //│ | Traversing statement: TermDefinition(Fun,member:command_resolution,term:command_resolution,List(ParamList(‹›,List(Param(‹›,x,None,Modulefulness(None))),None)),None,None,Some(Ref(x)),‹›,Modulefulness(None),List(Debug(...)),None) +//│ | Resolving definition: TermDefinition(Fun,member:command_resolution,term:command_resolution,List(ParamList(‹›,List(Param(‹›,x,None,Modulefulness(None))),None)),None,None,Some(Ref(x)),‹›,Modulefulness(None),List(Debug(...)),None) +//│ | | Resolving function definition TermDefinition(Fun,member:command_resolution,term:command_resolution,List(ParamList(‹›,List(Param(‹›,x,None,Modulefulness(None))),None)),None,None,Some(Ref(x)),‹›,Modulefulness(None),List(Debug(...)),None) +//│ | | Resolving parameter x² +//│ | | Traversing term: Ref(x) (expect = NonModule) +//│ | | | Resolving resolvable term: Ref(x), (prefer = NonModule, inAppPrefix = false, inCtxPrefix = false, inTyPrefix = false) +//│ | | | | Resolving symbol for term: Ref(x) (prefer = NonModule) +//│ | | | | -> (sym = Some(x), typ = None) +//│ | | | | Resolving the type for term: Ref(x) (prefer = NonModule, sym = Some(x)) +//│ | | | | | Declaration: Some(Param(‹›,x,None,Modulefulness(None))) +//│ | | | | -> typ = None +//│ | | | | Resolving resolvable term Ref(x) with sym = Some(x), typ = None +//│ | | | ~> Ref(x) (sym = Some(x), typ = None) +//│ | | | Checking Ref(x): expect NonModule, evalsToModule = false, evalsToStaticClass = false //│ | Traversing term: Lit(UnitLit(false)) (expect = Any) //│ | | Checking Lit(UnitLit(false)): expect Any, evalsToModule = false, evalsToStaticClass = false diff --git a/hkmc2DiffTests/src/test/scala/hkmc2/JSBackendDiffMaker.scala b/hkmc2DiffTests/src/test/scala/hkmc2/JSBackendDiffMaker.scala index 0c42929aaf..fe78d4a402 100644 --- a/hkmc2DiffTests/src/test/scala/hkmc2/JSBackendDiffMaker.scala +++ b/hkmc2DiffTests/src/test/scala/hkmc2/JSBackendDiffMaker.scala @@ -46,8 +46,9 @@ abstract class JSBackendDiffMaker extends MLsDiffMaker: val prettyPrintNme = baseScp.allocateName(Elaborator.State.prettyPrintSymbol)(using throw _) val ltl = new TraceLogger: - override def doTrace = debugLowering.isSet || activeDebug.lowering || scope.exists: + override protected def doForceTrace = debugLowering.isSet || scope.exists: showUCS.get.getOrElse(Set.empty).contains + override def doTrace = doForceTrace || activeDebug.lowering override protected def defaultDebugOutput: Config.DebugOutput = if activeDebug.lowering then activeDebug.out else Config.DebugOutput.StdIO override def emitDbg(str: String): Unit = output(str) diff --git a/hkmc2DiffTests/src/test/scala/hkmc2/MLsDiffMaker.scala b/hkmc2DiffTests/src/test/scala/hkmc2/MLsDiffMaker.scala index 8684b4c79b..34d95affb8 100644 --- a/hkmc2DiffTests/src/test/scala/hkmc2/MLsDiffMaker.scala +++ b/hkmc2DiffTests/src/test/scala/hkmc2/MLsDiffMaker.scala @@ -245,14 +245,16 @@ abstract class MLsDiffMaker extends DiffMaker: given Elaborator.State = new Elaborator.State: override protected def doDbg: Bool = - dbgParsing.isSet - // || dbgElab.isSet - || dbgResolving.isSet + doForceDbg || activeDebug.parsing || activeDebug.elaboration || activeDebug.resolution || activeDebug.lowering || activeDebug.optimizations + override protected def doForceDbg: Bool = + dbgParsing.isSet + // || dbgElab.isSet + || dbgResolving.isSet || debug.isSet override protected def doShowUids: Bool = activeDebug.showUids @@ -285,8 +287,9 @@ abstract class MLsDiffMaker extends DiffMaker: val etl = new TraceLogger: - override def doTrace = dbgElab.isSet || activeDebug.elaboration || scope.exists: + override protected def doForceTrace = dbgElab.isSet || scope.exists: showUCS.get.getOrElse(Set.empty).contains + override def doTrace = doForceTrace || activeDebug.elaboration override protected def defaultDebugOutput: Config.DebugOutput = if activeDebug.elaboration then activeDebug.out else Config.DebugOutput.StdIO override def emitDbg(str: String): Unit = output(str) @@ -297,7 +300,8 @@ abstract class MLsDiffMaker extends DiffMaker: if isTracing then super.trace(pre, post)(thunk) else thunk val rtl = new TraceLogger: - override def doTrace = dbgResolving.isSet || activeDebug.resolution + override protected def doForceTrace = dbgResolving.isSet + override def doTrace = doForceTrace || activeDebug.resolution override protected def defaultDebugOutput: Config.DebugOutput = if activeDebug.resolution then activeDebug.out else Config.DebugOutput.StdIO override def emitDbg(str: String): Unit = output(str) From 207f6bdb7006e1cc254dd9397882c365d3c641b0 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 16 Oct 2026 16:27:17 +0800 Subject: [PATCH 11/32] Lower statements after a class with a superclass outside its debug scope For a class with an `extends` clause, the `@dbg` lowering scope wrapped the continuation-passing evaluation of the superclass, whose continuation also lowers every statement following the class, so those siblings were traced with the class's settings. Share the continuation-aware scoping that local values already used as `DebugOutputHandler.traceDefinitionThen`, and use it for both: the superclass and the class definition stay in the class's debug scope, while the remaining statements are lowered in the enclosing one. Co-Authored-By: Claude Opus 5.5 --- DBG_DIRECTIVE_PLAN.md | 2 +- .../main/scala/hkmc2/DebugOutputHandler.scala | 17 ++++++++-- .../main/scala/hkmc2/codegen/Lowering.scala | 32 ++++++++----------- .../codegen/DebugParsingDirective.mls | 2 +- .../test/mlscript/codegen/DebugScoping.mls | 12 ------- 5 files changed, 30 insertions(+), 35 deletions(-) diff --git a/DBG_DIRECTIVE_PLAN.md b/DBG_DIRECTIVE_PLAN.md index 03ede1b1cd..634ca94c8f 100644 --- a/DBG_DIRECTIVE_PLAN.md +++ b/DBG_DIRECTIVE_PLAN.md @@ -22,7 +22,7 @@ - Support the annotation form on individual definitions, for example `@dbg('elaboration) fun foo() = ...`, without requiring file-wide tracing. Parsing and optimization traces require `#dbg`; requesting them through `@dbg` warns. - Class lowering traces may also include companion definitions and the continuation of superclass evaluation. + Class lowering traces may also include companion definitions and superclass evaluation, but not the statements after the class. - File output clears each destination on first use in a compiler session, then appends across compilation units. Concurrent debug writes are best effort. - Keep the feature available in diff-test files as well as through `MLsCompiler`. diff --git a/hkmc2/shared/src/main/scala/hkmc2/DebugOutputHandler.scala b/hkmc2/shared/src/main/scala/hkmc2/DebugOutputHandler.scala index dc496bec52..3a776d0ca9 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/DebugOutputHandler.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/DebugOutputHandler.scala @@ -32,13 +32,24 @@ object DebugOutputHandler: /** Keep trace routing and symbol printing in the same definition-local scope. */ def traceDefinition[T](annotations: Ls[semantics.Annot], base: Config, enabled: Config.Debug => Bool) - (thunk: => T)(using state: semantics.Elaborator.State, tl: utils.TL): T = + (thunk: => T)(using semantics.Elaborator.State, utils.TL): T = + traceDefinitionThen[T, T](annotations, base, enabled)(k => k(thunk))(identity) + + /** Like [[traceDefinition]], for a definition processed in continuation-passing style, where the + * continuation `k` processes the statements that follow the definition: `process` runs in the + * definition's debug scope, but `k` runs back in the enclosing one, so that the definition's + * settings do not leak to its siblings. */ + def traceDefinitionThen[R, T](annotations: Ls[semantics.Annot], base: Config, enabled: Config.Debug => Bool) + (process: (R => T) => T)(k: R => T)(using state: semantics.Elaborator.State, tl: utils.TL): T = definitionConfig(annotations, tl.debugConfig(base)) match case S(localConfig) => val debug = localConfig.debug state.scopedDebug(enabled(debug), debug.showUids): - tl.scopedDebug(enabled(debug), debug)(thunk) - case N => thunk + tl.scopedDebug(enabled(debug), debug): + process: r => + state.inOuterDebugScope: + tl.inOuterDebugScope(k(r)) + case N => process(k) /** Discover directives without committing diagnostics from a parse that may be repeated. * A traced parse reports its own diagnostics, in trace order; otherwise replay discovery's diff --git a/hkmc2/shared/src/main/scala/hkmc2/codegen/Lowering.scala b/hkmc2/shared/src/main/scala/hkmc2/codegen/Lowering.scala index aede9d211d..827e022f29 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/codegen/Lowering.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/codegen/Lowering.scala @@ -117,6 +117,10 @@ class Lowering()(using Config, TL, Raise, State, Ctx, SymbolPrinter)(using Erasu private def debugLowering[T](annotations: Ls[Annot])(thunk: => T): T = DebugOutputHandler.traceDefinition(annotations, config, _.lowering)(thunk) + + /** Trace a definition whose lowering continues, through `k`, with the statements that follow it. */ + private def debugLoweringThen[R](annotations: Ls[Annot])(lower: (R => Block) => Block)(k: R => Block): Block = + DebugOutputHandler.traceDefinitionThen(annotations, config, _.lowering)(lower)(k) extension (t: Term) def instantiated = t match @@ -318,22 +322,12 @@ class Lowering()(using Config, TL, Raise, State, Ctx, SymbolPrinter)(using Erasu case knd: syntax.Val => assert(td.params.isEmpty) val cfgOverride = definitionConfigOverride(td.extraAnnotations) - def lowerValue[R <: Result](lower: (R => Block) => Block)(k: R => Block): Block = - DebugOutputHandler.definitionConfig(td.extraAnnotations, tl.debugConfig(config)) match - case S(localConfig) => - State.scopedDebug(localConfig.debug.lowering, localConfig.debug.showUids): - tl.scopedDebug(localConfig.debug.lowering, localConfig.debug): - // The continuation lowers sibling statements, outside this value's debug scope. - lower: r => - State.inOuterDebugScope: - tl.inOuterDebugScope(k(r)) - case N => lower(k) localVal match case S(sym) => - lowerValue[Result](term_nonTail(bod)(_)): + debugLoweringThen[Result](td.extraAnnotations)(term_nonTail(bod)(_)): r => defineSymbol(sym, r, blockImpl(stats, res)) case N => - lowerValue[Path](k => subTerm_nonTail(bod)(k)(using LoweringCtx.nestFunc(N))): r => + debugLoweringThen[Path](td.extraAnnotations)(k => subTerm_nonTail(bod)(k)(using LoweringCtx.nestFunc(N))): r => Define( ValDefn(td.tsym, td.sym, castTo(r, td.tsym.erasedType, bod.toLoc))(cfgOverride, td.annotations), blockImpl(stats, res), @@ -407,12 +401,12 @@ class Lowering()(using Config, TL, Raise, State, Ctx, SymbolPrinter)(using Erasu msg"Buffered classes must not have a main parameter list; use `constructor(...)` syntax instead." -> defn.toLoc :: Nil, source = Diagnostic.Source.Compilation )) - def defineClass(parent: Opt[Path], parentParams: Ls[ParamList]): Block = + def lowerClass(parent: Opt[Path], parentParams: Ls[ParamList]): ClsLikeDefn = val base = parent.getOrElse( Select(State.globalThisSymbol.asThis, Tree.Ident("Object"))(S(ctx.builtins.Object), defn.toLoc)(false)) // Class and module companions share one lowered definition, so class tracing may // include its companion. Explicit module debug settings still override that scope. - val clsDef = debugLowering(defn.extraAnnotations)(LoweringCtx.withSuperBase(defn.sym, base).givenIn { + debugLowering(defn.extraAnnotations)(LoweringCtx.withSuperBase(defn.sym, base).givenIn { val (mtds, publicFlds, privateFlds, ctor) = defn match case pd: PatternDef => // Compile the pattern definition into `unapply` and `unapplyStringPrefix` @@ -452,14 +446,16 @@ class Lowering()(using Config, TL, Raise, State, Ctx, SymbolPrinter)(using Erasu ClsLikeDefn(defn.owner, defn.sym, defn.bsym, defn.ctorSym, defn.kind, defn.paramsOpt, defn.auxParams, parent, mtds, privateFlds, publicFlds, pctor, ctor, mod, bufferable)(cfgOverride, defn.annotations) }) - Define(clsDef, blockImpl(stats, res)) defn.ext match - case N => defineClass(N, Nil) + case N => Define(lowerClass(N, Nil), blockImpl(stats, res)) case S(ext) => assert(k isnt syntax.Mod) // modules can't extend things and can't have super calls - debugLowering(defn.extraAnnotations): + // Evaluating the superclass is part of the class definition, so it is traced with it, + // but the statements following the class are not. + debugLoweringThen[ClsLikeDefn](defn.extraAnnotations)(define => classOf(ext.cls, ext): (clsp, params) => - withClassParent(clsp)(parent => defineClass(S(parent), params)) + withClassParent(clsp)(parent => define(lowerClass(S(parent), params))) + )(Define(_, blockImpl(stats, res))) case td: TypeDef => // * Type definitions are erased blockImpl(stats, res) diff --git a/hkmc2/shared/src/test/mlscript/codegen/DebugParsingDirective.mls b/hkmc2/shared/src/test/mlscript/codegen/DebugParsingDirective.mls index 1264ef3cb8..4a23e1441b 100644 --- a/hkmc2/shared/src/test/mlscript/codegen/DebugParsingDirective.mls +++ b/hkmc2/shared/src/test/mlscript/codegen/DebugParsingDirective.mls @@ -3,7 +3,7 @@ #dbg('parsing) //│ ┊#┊dbg┊(⟨┊'parsing┊⟩)┊ -//│ @ blockOf(start of expression,allowNewlines = true) [at syntax/Parser.scala:55] +//│ @ blockOf(start of expression,allowNewlines = true) [at syntax/Parser.scala:66] //│ │ ? blockOfImpl inspects ┊#┊dbg┊(⟨┊'parsing┊⟩)┊ [at syntax/Parser.scala:340] //│ │ ! blockOfImpl consumes ┊#┊ [at syntax/Parser.scala:367] //│ │ @ parseRule(39,'#' directive keyword ::= Expr('#' directive body ::= End()),allowNewlines = true) [at syntax/Parser.scala:380] diff --git a/hkmc2/shared/src/test/mlscript/codegen/DebugScoping.mls b/hkmc2/shared/src/test/mlscript/codegen/DebugScoping.mls index 9a1db259c1..11585a3c45 100644 --- a/hkmc2/shared/src/test/mlscript/codegen/DebugScoping.mls +++ b/hkmc2/shared/src/test/mlscript/codegen/DebugScoping.mls @@ -86,16 +86,4 @@ print(1701) //│ ~> Resolved(Ref(member:DebugParent),class:DebugParent) //│ Lowering.term { } (in stmt) //│ Lowering.term undefined (in stmt) -//│ Lowering.term (member:Predef⁰.)print‹member:print›(1701) -//│ Expanding term App(SynthSel(Ref(member:Predef),Ident(print)),Tup(List(Fld(‹›,Lit(IntLit(1701)),None)))) -//│ ~> App(SynthSel(Ref(member:Predef),Ident(print)),Tup(List(Fld(‹›,Lit(IntLit(1701)),None)))) -//│ Expanding term SynthSel(Ref(member:Predef),Ident(print)) -//│ ~> Resolved(SynthSel(Ref(member:Predef),Ident(print)),term:Predef/print) -//│ Lowering.term (member:Predef⁰.)print‹term:Predef/print› – symbol term:Predef/print -//│ Expanding term Resolved(SynthSel(Ref(member:Predef),Ident(print)),term:Predef/print) -//│ ~> Resolved(SynthSel(Ref(member:Predef),Ident(print)),term:Predef/print) -//│ Lowering.term member:Predef⁰ – symbol module:Predef -//│ Expanding term Ref(member:Predef) -//│ ~> Resolved(Ref(member:Predef),module:Predef) -//│ Lowering.term 1701 //│ > 1701 From 589856d8bf1f70887b94796d8b74a423150074f9 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 16 Oct 2026 16:27:59 +0800 Subject: [PATCH 12/32] Keep :dp parse traces in the diff-test output `DebugOutputHandler.parse` sent every traced parse to the destination selected by `#dbg(out: ...)`, even when only the `:dp` diff-test command requested the trace, so a file destination swallowed `:dp` output. Like the other forced traces, use the selected destination only when `#dbg` itself enables parsing, and standard output otherwise. Co-Authored-By: Claude Opus 5.5 --- .../main/scala/hkmc2/DebugOutputHandler.scala | 5 +++-- .../mlscript/codegen/DebugDestinations.mls | 21 ++++++++++++++++--- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/hkmc2/shared/src/main/scala/hkmc2/DebugOutputHandler.scala b/hkmc2/shared/src/main/scala/hkmc2/DebugOutputHandler.scala index 3a776d0ca9..68942ff620 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/DebugOutputHandler.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/DebugOutputHandler.scala @@ -69,8 +69,9 @@ object DebugOutputHandler: val discovery = run(false, Config.DebugOutput.StdIO)(using diagnostics += _) val effectiveConfig = Config.elaborationConfig(discovery) val result = - if forceDebug || effectiveConfig.debug.parsing then - run(true, effectiveConfig.debug.out) + // As for `TraceLogger.doForceTrace`, a parse traced only because of `forceDebug` uses `StdIO`. + if effectiveConfig.debug.parsing then run(true, effectiveConfig.debug.out) + else if forceDebug then run(true, Config.DebugOutput.StdIO) else diagnostics.foreach(summon[Raise]) // Token display is a legacy diff-test option. Re-lexing does not report diagnostics twice. diff --git a/hkmc2/shared/src/test/mlscript/codegen/DebugDestinations.mls b/hkmc2/shared/src/test/mlscript/codegen/DebugDestinations.mls index 89e6afc504..ccd3b78c47 100644 --- a/hkmc2/shared/src/test/mlscript/codegen/DebugDestinations.mls +++ b/hkmc2/shared/src/test/mlscript/codegen/DebugDestinations.mls @@ -73,14 +73,29 @@ fs.rmSync("hkmc2/shared/src/test/mlscript/codegen/DebugDestinationsCommand.log", :noCodeGen :dp 1 +//│ ┊1┊ +//│ @ blockOf(start of expression,allowNewlines = true) [at syntax/Parser.scala:66] +//│ │ ? blockOfImpl inspects ┊1┊ [at syntax/Parser.scala:340] +//│ │ @ simpleExpr(1) [at syntax/Parser.scala:429] +//│ │ │ ! simpleExprImpl consumes ┊1┊ [at syntax/Parser.scala:674] +//│ │ │ @ exprCont(1,`IntLit(1)`,true) [at syntax/Parser.scala:675] +//│ │ │ │ ? exprContImpl inspects ┊┊ [at syntax/Parser.scala:931] +//│ │ │ │ @ exprJux(1,`IntLit(1)`,false) [at syntax/Parser.scala:1203] +//│ │ │ │ │ ? exprJuxImpl inspects ┊┊ [at syntax/Parser.scala:1210] +//│ │ │ │ │ stops at the end of input +//│ │ │ │ = IntLit(1) +//│ │ │ = IntLit(1) +//│ │ = IntLit(1) +//│ │ @ parseRule(1,expression ::= End(),allowNewlines = true) [at syntax/Parser.scala:431] +//│ │ = Some(()) +//│ = List(IntLit(1)) +//│ ? parseAll inspects ┊┊ [at syntax/Parser.scala:261] #dbg(out: StdIO) -:fixme :expect false fs.existsSync("hkmc2/shared/src/test/mlscript/codegen/DebugDestinationsCommand.log") -//│ ═══[RUNTIME ERROR] Expected: 'false', got: 'true' -//│ = true +//│ = false :silent fs.rmSync("hkmc2/shared/src/test/mlscript/codegen/DebugDestinationsCommand.log", {force: true}) From c4f0a689ae9e0b7c94c42af9ea2f3bd9322daa2e Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 16 Oct 2026 16:28:33 +0800 Subject: [PATCH 13/32] Support absolute debug output paths `DebugOutputHandler.emit` always built `baseDir / io.RelPath(path)`, and `io.RelPath` rejects absolute paths with an exception, so a destination such as `#dbg(out: File("/tmp/trace.log"))` crashed the compiler on its first trace line. Resolve destinations like import paths instead: keep absolute paths as they are, and resolve others against the base directory, through a shared `io.Path.resolve`. Co-Authored-By: Claude Opus 5.5 --- DBG_DIRECTIVE_PLAN.md | 2 +- hkmc2/shared/src/main/scala/hkmc2/DebugOutputHandler.scala | 2 +- hkmc2/shared/src/main/scala/hkmc2/io/Path.scala | 5 +++++ hkmc2/shared/src/main/scala/hkmc2/semantics/Importer.scala | 5 +---- .../shared/src/test/mlscript/codegen/DebugDestinations.mls | 6 +----- 5 files changed, 9 insertions(+), 11 deletions(-) diff --git a/DBG_DIRECTIVE_PLAN.md b/DBG_DIRECTIVE_PLAN.md index 634ca94c8f..78b16ed701 100644 --- a/DBG_DIRECTIVE_PLAN.md +++ b/DBG_DIRECTIVE_PLAN.md @@ -18,7 +18,7 @@ - `#dbg(elaboration: true)`. - Support a selectable debug destination: - `StdIO`, which is the default and may be omitted; - - `File("out.mls")`, resolved relative to the active configuration's base directory. + - `File("out.mls")`, resolved relative to the active configuration's base directory unless absolute, as for imports. - Support the annotation form on individual definitions, for example `@dbg('elaboration) fun foo() = ...`, without requiring file-wide tracing. Parsing and optimization traces require `#dbg`; requesting them through `@dbg` warns. diff --git a/hkmc2/shared/src/main/scala/hkmc2/DebugOutputHandler.scala b/hkmc2/shared/src/main/scala/hkmc2/DebugOutputHandler.scala index 68942ff620..5e63311544 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/DebugOutputHandler.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/DebugOutputHandler.scala @@ -16,7 +16,7 @@ final class DebugOutputHandler(fs: io.FileSystem, baseDir: io.Path, stdIO: Str = def emit(out: Config.DebugOutput, line: Str): Unit = out match case Config.DebugOutput.StdIO => stdIO(line) case Config.DebugOutput.File(path) => - val file = baseDir / io.RelPath(path) + val file = io.Path.resolve(baseDir, path) if writtenFiles(file) then fs.append(file, line + '\n') else fs.write(file, line + '\n') diff --git a/hkmc2/shared/src/main/scala/hkmc2/io/Path.scala b/hkmc2/shared/src/main/scala/hkmc2/io/Path.scala index a582392afd..79e902100b 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/io/Path.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/io/Path.scala @@ -42,6 +42,11 @@ object Path: /** Create path from string - delegates to platform-specific implementation */ def apply(str: String): Path = PathFactory.fromString(str) + /** Resolve a path written in source code, such as an import path or a debug output file: + * an absolute path is used as is, and any other path is relative to `base`. */ + def resolve(base: Path, str: String): Path = + if str.startsWith("/") then Path(str) else base / RelPath(str) + /** Platform-specific path separator */ def separator: String = PathFactory.separator diff --git a/hkmc2/shared/src/main/scala/hkmc2/semantics/Importer.scala b/hkmc2/shared/src/main/scala/hkmc2/semantics/Importer.scala index abaec6fc38..8a1b5046da 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/semantics/Importer.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/semantics/Importer.scala @@ -23,10 +23,7 @@ class Importer extends NewResolver: // log(s"pwd: ${os.pwd}") // log(s"wd: ${wd}") - val file = - if path.startsWith("/") - then io.Path(path) - else wd / io.RelPath(path) + val file = io.Path.resolve(wd, path) val nme = file.baseName val id = alias.getOrElse(new syntax.Tree.Ident(nme)) // TODO loc diff --git a/hkmc2/shared/src/test/mlscript/codegen/DebugDestinations.mls b/hkmc2/shared/src/test/mlscript/codegen/DebugDestinations.mls index ccd3b78c47..1a9e0715f9 100644 --- a/hkmc2/shared/src/test/mlscript/codegen/DebugDestinations.mls +++ b/hkmc2/shared/src/test/mlscript/codegen/DebugDestinations.mls @@ -106,16 +106,12 @@ fs.rmSync("hkmc2/shared/src/test/mlscript/codegen/DebugDestinationsCommand.log", val absoluteDebugFile = "/tmp/mlscript-DebugDestinationsAbsolute.log" fs.rmSync(absoluteDebugFile, {force: true}) -:fixme @dbg('lowering, showUids: false, out: File("/tmp/mlscript-DebugDestinationsAbsolute.log")) fun absolute_file() = 1801 -//│ /!!!\ Uncaught error: java.lang.IllegalArgumentException: requirement failed: /tmp/mlscript-DebugDestinationsAbsolute.log is not a relative path -:fixme :expect true fs.existsSync(absoluteDebugFile) and fs.readFileSync(absoluteDebugFile, "utf8").includes("Lowering.term 1801\n") -//│ ═══[RUNTIME ERROR] Expected: 'true', got: 'false' -//│ = false +//│ = true :silent fs.rmSync(absoluteDebugFile, {force: true}) From d0425a524466621a23454c3e3bbb72d4b56e2063 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 16 Oct 2026 16:29:53 +0800 Subject: [PATCH 14/32] Reject value definitions with parameter lists The elaborator accepted `val f(x) = ...`, producing a value definition with parameters, which lowering cannot represent: top-level values hit an assertion in lowering, and local values used to lose their parameters silently and fail later in code generation. The debug-scoping refactor of value lowering extended the assertion to local values as well. Report a compilation error instead, and elaborate the value without its parameter lists, so uses of the parameters in the body are reported too. Co-Authored-By: Claude Opus 5.5 --- .../scala/hkmc2/semantics/Elaborator.scala | 12 ++++++- .../src/test/mlscript/basics/BadDefs.mls | 34 +++++++++++++++---- 2 files changed, 39 insertions(+), 7 deletions(-) diff --git a/hkmc2/shared/src/main/scala/hkmc2/semantics/Elaborator.scala b/hkmc2/shared/src/main/scala/hkmc2/semantics/Elaborator.scala index d8a13ed018..6a58e2309e 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/semantics/Elaborator.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/semantics/Elaborator.scala @@ -2351,6 +2351,16 @@ extends Importer: msg"Illegal ${k.desc} member name: '${id.name}'" -> nme.toLoc :: illegalMemberNameTail return go(sts, Nil, acc)(N) + // A value is the result of evaluating its body once, so it cannot take parameters; + // lowering relies on this. Drop them, so that uses of them in the body are reported. + val paramLists = td.paramLists match + case pss @ (_ :: _) if k.isInstanceOf[Val] => + raise: + ErrorReport: + msg"${k.desc.capitalize} definitions cannot have parameter lists" -> Loc(pss) + :: Nil + Nil + case pss => pss val isMethod = owner.exists(_.isInstanceOf[ClassSymbol]) val tsym = TermSymbol(k, owner, id) // TODO? @@ -2369,7 +2379,7 @@ extends Importer: if newResolution then tps.foreach(ps => registerTypeParameters(tsym, ps.map(_.sym))) // * Add parameters to context var newCtx = newCtx1 - val pss = td.paramLists.map: ps => + val pss = paramLists.map: ps => val (res, newCtx2) = funParams(ps)(using newCtx) newCtx = newCtx2 res diff --git a/hkmc2/shared/src/test/mlscript/basics/BadDefs.mls b/hkmc2/shared/src/test/mlscript/basics/BadDefs.mls index 74d6e05b3c..a262fa1319 100644 --- a/hkmc2/shared/src/test/mlscript/basics/BadDefs.mls +++ b/hkmc2/shared/src/test/mlscript/basics/BadDefs.mls @@ -86,14 +86,36 @@ trait F // Value definitions do not take parameter lists. -:fixme -val badValue(x) = x -//│ /!!!\ Uncaught error: java.lang.AssertionError: assertion failed +:e +val badValue(arg) = arg +//│ ╔══[COMPILATION ERROR] Value definitions cannot have parameter lists +//│ ║ l.90: val badValue(arg) = arg +//│ ╙── ^^^^^ +//│ ╔══[COMPILATION ERROR] Name not found: arg +//│ ║ l.90: val badValue(arg) = arg +//│ ╙── ^^^ +//│ ═══[RUNTIME ERROR] This code cannot be run as its compilation yielded an error. +//│ badValue = undefined -:fixme +:e fun localBadValue() = - val badLocal(x) = x + val badLocal(arg) = arg badLocal -//│ /!!!\ Uncaught error: java.lang.AssertionError: assertion failed +//│ ╔══[COMPILATION ERROR] Value definitions cannot have parameter lists +//│ ║ l.102: val badLocal(arg) = arg +//│ ╙── ^^^^^ +//│ ╔══[COMPILATION ERROR] Name not found: arg +//│ ║ l.102: val badLocal(arg) = arg +//│ ╙── ^^^ + +:e +class BadValueMember with + mut val badMember(arg)(arg2) = arg +//│ ╔══[COMPILATION ERROR] Mutable value definitions cannot have parameter lists +//│ ║ l.113: mut val badMember(arg)(arg2) = arg +//│ ╙── ^^^^^^^^^^^ +//│ ╔══[COMPILATION ERROR] Name not found: arg +//│ ║ l.113: mut val badMember(arg)(arg2) = arg +//│ ╙── ^^^ From a08db539eb8e04bee61eddf0ee81e177639a9ea7 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 16 Oct 2026 16:45:21 +0800 Subject: [PATCH 15/32] Document compiler debugging in the language reference Add a "Compiler Debugging" section describing the `#dbg` directive and the `@dbg` annotation: the available aspects, their syntax, output destinations, scoping rules, and current limitations. Remove the working notes in DBG_DIRECTIVE_PLAN.md, which this section supersedes. Co-Authored-By: Claude Opus 5.5 --- DBG_DIRECTIVE_PLAN.md | 54 ------------------- doc/reference.md | 121 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 121 insertions(+), 54 deletions(-) delete mode 100644 DBG_DIRECTIVE_PLAN.md diff --git a/DBG_DIRECTIVE_PLAN.md b/DBG_DIRECTIVE_PLAN.md deleted file mode 100644 index 78b16ed701..0000000000 --- a/DBG_DIRECTIVE_PLAN.md +++ /dev/null @@ -1,54 +0,0 @@ -# `#dbg` directive progress - -## Requirements - -- Add a `#dbg` directive whose settings are stored in `Config`, analogously to `#lang`. -- Support the compiler phases represented by the existing diff-test debug commands: - - parsing (`:dp`), - - elaboration (`:de`), - - resolution (`:dr`), - - lowering (`:dl`), and - - optimizations (`:dopt`). -- Mirror the existing IR display commands with `showIR` (`:sir`) and - `showOptimizedIR` (`:soir`). -- Mirror the parsed, elaborated, and lowered tree displays with `showParsedTree` - (`:pt`), `showElaboratedTree` (`:elt`), and `showLoweredTree` (`:lot`). -- Accept both shorthand flags and named booleans, including: - - `#dbg('parsing)`; - - `#dbg(elaboration: true)`. -- Support a selectable debug destination: - - `StdIO`, which is the default and may be omitted; - - `File("out.mls")`, resolved relative to the active configuration's base directory unless absolute, as for imports. -- Support the annotation form on individual definitions, for example - `@dbg('elaboration) fun foo() = ...`, without requiring file-wide tracing. - Parsing and optimization traces require `#dbg`; requesting them through `@dbg` warns. - Class lowering traces may also include companion definitions and superclass evaluation, but not the statements after the class. -- File output clears each destination on first use in a compiler session, then appends across - compilation units. Concurrent debug writes are best effort. -- Keep the feature available in diff-test files as well as through `MLsCompiler`. -- Diagnose unknown flags, malformed boolean values, malformed destinations, and unsupported arguments. - -## Implementation plan - -1. Add immutable debug settings and output-destination types to `Config`, with all phase flags disabled and `StdIO` selected by default. -2. Extend `ConfigParser` with one parser shared by `#dbg(...)` and `@dbg(...)`; support quoted phase names, named phase booleans, and `out`. -3. Elaborate `#dbg` into the existing persistent `SetConfig` representation and `@dbg` into a per-definition configuration annotation. -4. Reuse the existing phase trace loggers, with destination-aware dynamic scoping only where a definition annotation must temporarily change their configuration. -5. Discover module/block-level parsing debug settings after an initial parse and, when requested, repeat parsing with lexer/parser tracing sent to the selected destination. -6. Drive elaboration, resolution, lowering, optimization, and IR display from the effective `Config`; retain the legacy diff-test commands by sharing their existing code paths. -7. Add positive and negative diff tests covering shorthand/named syntax, persistence, destination selection, and definition-local elaboration output. -8. Run `ctest`, focused diff tests while iterating, and finally `hkmc2AllTests/test`; review and commit all intentional golden changes. - -## Progress - -- [x] Requirements summarized and implementation plan recorded. -- [x] Configuration model and parser implemented. -- [x] Compiler and diff-test tracing integrated. -- [x] Definition-local scoping implemented. -- [x] Regression tests and golden outputs added. -- [x] Full prescribed test suite passes. -- [x] Custom optimization reporting removed in favor of the existing `:dopt` trace path. -- [x] `showIR` and `showOptimizedIR` share the existing `:sir` and `:soir` display paths. -- [x] `@dbg` configuration overrides remain attached to lowered IR definitions, including methods. -- [x] Definition-local IR display prints only definitions selected by `@dbg`, through the existing IR printer. -- [x] Parsed, elaborated, and lowered tree displays support both `#dbg` and definition-local `@dbg`. diff --git a/doc/reference.md b/doc/reference.md index c401322e00..0649b899f9 100644 --- a/doc/reference.md +++ b/doc/reference.md @@ -27,6 +27,7 @@ which is an evolving language that has not stabilized just yet. 17. [Indentation and Block Syntax](#17-indentation-and-block-syntax) 18. [Miscellaneous](#18-miscellaneous) 19. [Built-in Types and Prelude](#19-built-in-types-and-prelude) +20. [Compiler Debugging](#20-compiler-debugging) --- @@ -1976,6 +1977,126 @@ parenthesizedIf(x, cond) // wrap in parens if cond --- +## 20. Compiler Debugging + +The compiler can report what it does in each of its phases, and show the intermediate programs it produces. +This is configured in the source code, either for a whole file with the `#dbg` directive, +or for individual definitions with the `@dbg` annotation. +The output is meant for compiler developers: its format is not stable and may change at any time. + +### Debugging Aspects + +Each argument of `#dbg` or `@dbg` enables or disables one *aspect*. +The following aspects trace a phase of the compiler: + +| Aspect | Traces | +|---|---| +| `parsing` | lexing and parsing | +| `elaboration` | elaboration of syntax trees into terms | +| `resolution` | name resolution | +| `lowering` | lowering of terms into the intermediate representation (IR) | +| `optimizations` | the optimization passes run on the IR | + +With the new name resolution (`#lang(0.3.x)`), names are resolved during elaboration, +so enabling `resolution` traces elaboration as well. + +The following aspects display a program representation, without tracing: + +| Aspect | Displays | +|---|---| +| `showParsedTree` | the syntax tree produced by the parser | +| `showElaboratedTree` | the elaborated term tree | +| `showLoweredTree` | the IR produced by lowering, as a tree | +| `showIR` | the IR produced by lowering, in readable form | +| `showOptimizedIR` | the IR after optimizations, in readable form | + +Finally, `showUids` (enabled by default) controls whether traces show the unique identifiers of symbols. +Disabling it makes traces easier to compare across compiler runs. + +### Syntax + +An aspect is enabled by writing its name as a quoted identifier, +or enabled or disabled by giving it a Boolean value: + +```mlscript +#dbg('lowering) // enable an aspect +#dbg(elaboration: true, showIR: true) // enable aspects explicitly +#dbg(showUids: false) // disable an aspect +``` + +Unknown aspects and non-Boolean values are reported as errors. + +### Output Destination + +Debugging output is written to the standard output by default, which `out: StdIO` selects explicitly. +The `out: File(...)` argument writes it to a file instead: + +```mlscript +#dbg('lowering, out: File("lowering.log")) +``` + +A relative `File` path is resolved against the compiler's base directory, +and an absolute path is used as is, as for imports. +The first time the compiler writes to a file during a compilation session, it replaces the file's previous contents; +it then appends all later output, including the output of other files compiled in the same session, +such as imported files. +Output from concurrent compilations to the same file may be interleaved. + +Any other destination is reported as an error. + +### The `#dbg` Directive + +The `#dbg` directive configures debugging for the file in which it appears, and must appear at its top level: + +```mlscript +#dbg('elaboration, 'showOptimizedIR, out: File("debug.log")) + +fun f(x) = x + 1 +``` + +Several `#dbg` directives may set different aspects. +Currently, debugging settings apply to the whole file regardless of where the directive appears, +so if several directives set the same aspect, the last one takes effect. + +### The `@dbg` Annotation + +The `@dbg` annotation takes the same arguments as `#dbg`, +but only applies them to the definition it annotates. +It can annotate functions, methods, values, classes, modules, and objects: + +```mlscript +@dbg('elaboration, 'showIR, showUids: false) +fun selected(x) = x + 1 + +fun unrelated(x) = x // not traced, not displayed +``` + +Annotations combine with the file's settings: +for example, `@dbg(lowering: false)` stops a lowering trace enabled by `#dbg` for the annotated definition. +They also apply to the definitions nested in the annotated definition, +which can override them with their own `@dbg` annotations: + +```mlscript +@dbg('lowering, out: File("lowering.log")) +fun traced() = + fun helper() = 1 // traced to lowering.log + @dbg(lowering: false) fun muted() = 2 // not traced + helper() + muted() +``` + +The statements that follow an annotated definition are not affected by its annotation. +The lowering trace of an annotated class also covers the evaluation of its superclass, if any, +and its companion module, if any, which is compiled together with the class. + +The `parsing` and `optimizations` aspects cannot be selected per definition, +as these phases process whole files. +Setting them in `@dbg` produces a warning and has no effect; use `#dbg` instead. + +Currently, the destination selected by an enclosing `@dbg` annotation only applies to traces: +the displays requested by the annotation of a nested definition use the file's destination. + +--- + ## Appendix: Grammar Summary ``` From 872086f930a9ed1357a115ebc4a2cbd9611380b7 Mon Sep 17 00:00:00 2001 From: Codex Date: Fri, 16 Oct 2026 16:49:20 +0800 Subject: [PATCH 16/32] Clarify forced trace output routing Document that explicit phase configuration selects the output destination even when a diff-test command also forces tracing. The implementation already behaves this way. Reviewed the six follow-up fixes through d0425a524. Validation: ctest, focused debug and invalid-definition regressions, and hkmc2AllTests/test. --- hkmc2/shared/src/main/scala/utils/TraceLogger.scala | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/hkmc2/shared/src/main/scala/utils/TraceLogger.scala b/hkmc2/shared/src/main/scala/utils/TraceLogger.scala index bd2b9478c8..28e0caf31e 100644 --- a/hkmc2/shared/src/main/scala/utils/TraceLogger.scala +++ b/hkmc2/shared/src/main/scala/utils/TraceLogger.scala @@ -11,8 +11,9 @@ def tl(using TL): TL = summon abstract class TraceLogger(using val debugPrinter: DebugPrinter): def doTrace: Bool = true /** Tracing requested independently of the [[Config.Debug]] settings, such as by diff-test - * commands, which trace a whole phase. Definition-local `@dbg` annotations can neither disable - * nor redirect it: their overrides only decide whether the phase is traced *in addition* to it. + * commands, which trace a whole phase. Definition-local `@dbg` annotations cannot disable it. + * If the phase is also enabled in [[Config.Debug]], its selected destination is used; + * tracing enabled only by this flag goes to `StdIO`. * Implementations should include it in [[doTrace]], which also governs code outside annotations. */ protected def doForceTrace: Bool = false protected def defaultDebugOutput: Config.DebugOutput = Config.DebugOutput.StdIO From 3d65322e0b04cd9a87ac722d1c890059aa7465a6 Mon Sep 17 00:00:00 2001 From: Codex Date: Sat, 17 Oct 2026 11:55:09 +0800 Subject: [PATCH 17/32] Simplify debug directives and consolidate coverage Keep parser tracing in the diff-test :dp command and remove source-directed discovery and replay. Share IR display selection and formatting between the compiler and diff tests. Consolidate nine debug diff tests into four, retain scope and routing regressions, and write generated fixtures and logs under target without rmSync cleanup. Update the language reference and include regenerated goldens. Validation: ctest, dtest Debug, and hkmc2AllTests/test all pass. --- doc/reference.md | 9 +- .../src/main/scala/hkmc2/CompilerCtx.scala | 33 +-- .../shared/src/main/scala/hkmc2/Config.scala | 9 +- .../main/scala/hkmc2/DebugOutputHandler.scala | 51 ++--- .../src/main/scala/hkmc2/MLsCompiler.scala | 11 +- .../mlscript/codegen/BadDebugDirective.mls | 23 +- .../codegen/DebugDefinitionPhases.mls | 200 ------------------ .../mlscript/codegen/DebugDestinations.mls | 123 +++-------- .../test/mlscript/codegen/DebugDirective.mls | 103 ++++++++- .../test/mlscript/codegen/DebugImports.mls | 26 --- .../codegen/DebugParsingDirective.mls | 57 ----- .../test/mlscript/codegen/DebugScoping.mls | 72 ++++++- .../src/test/mlscript/codegen/DebugTypes.mls | 66 ------ .../codegen/DebugUnsupportedPhases.mls | 80 ------- .../test/scala/hkmc2/JSBackendDiffMaker.scala | 52 +---- .../src/test/scala/hkmc2/MLsDiffMaker.scala | 14 +- 16 files changed, 282 insertions(+), 647 deletions(-) delete mode 100644 hkmc2/shared/src/test/mlscript/codegen/DebugDefinitionPhases.mls delete mode 100644 hkmc2/shared/src/test/mlscript/codegen/DebugImports.mls delete mode 100644 hkmc2/shared/src/test/mlscript/codegen/DebugParsingDirective.mls delete mode 100644 hkmc2/shared/src/test/mlscript/codegen/DebugTypes.mls delete mode 100644 hkmc2/shared/src/test/mlscript/codegen/DebugUnsupportedPhases.mls diff --git a/doc/reference.md b/doc/reference.md index 0649b899f9..b86e41d676 100644 --- a/doc/reference.md +++ b/doc/reference.md @@ -1991,7 +1991,6 @@ The following aspects trace a phase of the compiler: | Aspect | Traces | |---|---| -| `parsing` | lexing and parsing | | `elaboration` | elaboration of syntax trees into terms | | `resolution` | name resolution | | `lowering` | lowering of terms into the intermediate representation (IR) | @@ -2000,6 +1999,9 @@ The following aspects trace a phase of the compiler: With the new name resolution (`#lang(0.3.x)`), names are resolved during elaboration, so enabling `resolution` traces elaboration as well. +Parser tracing is available through the diff-test command `:dp`. +Source directives are read after parsing and cannot enable parser tracing. + The following aspects display a program representation, without tracing: | Aspect | Displays | @@ -2088,9 +2090,8 @@ The statements that follow an annotated definition are not affected by its annot The lowering trace of an annotated class also covers the evaluation of its superclass, if any, and its companion module, if any, which is compiled together with the class. -The `parsing` and `optimizations` aspects cannot be selected per definition, -as these phases process whole files. -Setting them in `@dbg` produces a warning and has no effect; use `#dbg` instead. +The `optimizations` aspect cannot be selected per definition, as optimization passes process whole files. +Setting it in `@dbg` produces a warning and has no effect; use `#dbg` instead. Currently, the destination selected by an enclosing `@dbg` annotation only applies to traces: the displays requested by the annotation of a nested definition use the file's destination. diff --git a/hkmc2/shared/src/main/scala/hkmc2/CompilerCtx.scala b/hkmc2/shared/src/main/scala/hkmc2/CompilerCtx.scala index 05b766edae..16a919756c 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/CompilerCtx.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/CompilerCtx.scala @@ -183,37 +183,20 @@ class CompilerCtx( useSuperscripts = false, includeZero = false, )) + val blockPrinter = codegen.Printer() val printer = (p: codegen.Program) => - codegen.Printer().worksheet(p)(using irPrintingScp).mkString(100) - def showDebugIR(title: Str, program: codegen.Program): Unit = - outputHandler.emit(artifactConfig.debug.out, s"${title}\n${printer(program)}") - def showDefinitionDebugIR( - title: Str, - program: codegen.Program, - isEnabled: Config.Debug => Bool, - ): Unit = - val blockPrinter = codegen.Printer() - DebugOutputHandler.forEachIRDefinition(program, isEnabled): (defn, out) => - val ir = blockPrinter.printDefinition(defn)(using irPrintingScp).mkString(100) - outputHandler.emit(out, s"${title}\n${ir}") - def showDefinitionDebugTree(title: Str, program: codegen.Program): Unit = - DebugOutputHandler.forEachIRDefinition(program, _.showLoweredTree): (defn, out) => - val tree = defn match - case product: Product => product.showAsTree - outputHandler.emit(out, s"${title}\n${tree}") + blockPrinter.worksheet(p)(using irPrintingScp).mkString(100) + def display(title: Str, contents: Str, out: Config.DebugOutput): Unit = + outputHandler.emit(out, s"${title}\n${contents}") val pipeline = new codegen.CompilationPipeline: override def extraSymbolsToPreserveFrom(prog: codegen.Program): Set[codegen.ScopedSymbol] = collectCompilationUnitSymbols(prog) override def preOptimizeHook(prog: codegen.Program): Unit = - if artifactConfig.debug.showLoweredTree then - outputHandler.emit(artifactConfig.debug.out, s"Lowered IR Tree\n${prog.showAsTree}") - else showDefinitionDebugTree("Lowered IR Tree", prog) - if artifactConfig.debug.showIR then showDebugIR("Lowered IR", prog) - else showDefinitionDebugIR("Lowered IR", prog, _.showIR) + DebugOutputHandler.showLoweredTree(prog)(display) + DebugOutputHandler.showIR(prog, false, blockPrinter, 100)(display)(using artifactConfig, irPrintingScp) val optimized = ltl.givenIn: pipeline.run(lowered, printer, exportedSymbol.toSet, dtl) - if artifactConfig.debug.showOptimizedIR then showDebugIR("Optimized IR", optimized) - else showDefinitionDebugIR("Optimized IR", optimized, _.showOptimizedIR) + DebugOutputHandler.showIR(optimized, true, blockPrinter, 100)(display)(using artifactConfig, irPrintingScp) optimized // Every compilation unit is lowered, so that its symbols carry the IR definitions an @@ -311,7 +294,7 @@ object CompilerCtx: inline def get(using cctx: CompilerCtx) = cctx private def debugTracingEnabled(debug: Config.Debug): Bool = - debug.parsing || debug.elaboration || debug.resolution || debug.lowering || debug.optimizations + debug.elaboration || debug.resolution || debug.lowering || debug.optimizations /** Collect import provenance after elaboration has assembled all user and synthetic imports. diff --git a/hkmc2/shared/src/main/scala/hkmc2/Config.scala b/hkmc2/shared/src/main/scala/hkmc2/Config.scala index eef7deaf98..f75bb024bf 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/Config.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/Config.scala @@ -147,7 +147,6 @@ object Config: case File(path: Str) case class Debug( - parsing: Bool, showParsedTree: Bool, elaboration: Bool, showElaboratedTree: Bool, @@ -164,7 +163,6 @@ object Config: def traceElaboration(language: Language): Bool = elaboration || (language.useNewResolution && resolution) object Debug: val default = Debug( - parsing = false, showParsedTree = false, elaboration = false, showElaboratedTree = false, @@ -272,7 +270,7 @@ object Config: def elaborationConfig(prgm: syntax.Tree.Block)(using Config): Config = elaborationConfig(prgm.desugStmts) - /** The parser uses the raw trees to discover tracing before constructing a block's symbols. */ + /** Read directives from parsed trees before elaboration or tree display. */ def elaborationConfig(trees: Ls[syntax.Tree])(using Config): Config = import syntax.Tree.* given Raise = _ => () @@ -540,12 +538,11 @@ object ConfigParser: N private def updateDebugFlag(name: Str, value: Bool, tree: Tree, definitionLocal: Bool)(using Raise): Config.Debug => Config.Debug = - if definitionLocal && (name === "parsing" || name === "optimizations") then + if definitionLocal && name === "optimizations" then raise(WarningReport( msg"Debugging aspect '${name}' is not supported in @dbg; use #dbg(${name}: ${value.toString}) instead." -> tree.toLoc :: Nil, source = Diagnostic.Source.Compilation)) name match - case "parsing" => _.copy(parsing = value) case "showParsedTree" => _.copy(showParsedTree = value) case "elaboration" => _.copy(elaboration = value) case "showElaboratedTree" => _.copy(showElaboratedTree = value) @@ -588,7 +585,7 @@ object ConfigParser: def apply(cfg: Config): Config = cfg.copy(debug = modifyDebug(cfg.debug)) override def toString: Str = "..." - /** Apply `@dbg(...)` syntax annotations without reporting diagnostics during the discovery parse. */ + /** Select parsed-tree displays without duplicating diagnostics reported by elaboration. */ def discoverDebugFromAnnotations(annotations: Ls[Tree], base: Config): Opt[Config] = given Raise = _ => () val modifiers = annotations.collect: diff --git a/hkmc2/shared/src/main/scala/hkmc2/DebugOutputHandler.scala b/hkmc2/shared/src/main/scala/hkmc2/DebugOutputHandler.scala index 5e63311544..82fb065269 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/DebugOutputHandler.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/DebugOutputHandler.scala @@ -51,36 +51,6 @@ object DebugOutputHandler: tl.inOuterDebugScope(k(r)) case N => process(k) - /** Discover directives without committing diagnostics from a parse that may be repeated. - * A traced parse reports its own diagnostics, in trace order; otherwise replay discovery's - * diagnostics exactly once. Both compiler entry points use this path. */ - def parse(origin: Origin, forceDebug: Bool, showTokens: Bool, output: (Config.DebugOutput, Str) => Unit) - (using semantics.Elaborator.State, Config, Raise): (Ls[syntax.Tree], Config) = - def run(dbg: Bool, out: Config.DebugOutput)(using Raise): Ls[syntax.Tree] = - val lexer = new syntax.Lexer(origin, dbg): - override protected def doPrintDbg(msg: => Str): Unit = output(out, msg) - val tokens = lexer.bracketedTokens - if dbg then output(out, syntax.Lexer.printTokens(tokens)) - val parser = new syntax.Parser(origin, tokens, syntax.ParseRules(), summon[Raise], dbg): - def doPrintDbg(msg: => Str): Unit = if this.dbg then output(out, msg) - parser.parseAll(parser.block(allowNewlines = true)) - - val diagnostics = mutable.ListBuffer.empty[Diagnostic] - val discovery = run(false, Config.DebugOutput.StdIO)(using diagnostics += _) - val effectiveConfig = Config.elaborationConfig(discovery) - val result = - // As for `TraceLogger.doForceTrace`, a parse traced only because of `forceDebug` uses `StdIO`. - if effectiveConfig.debug.parsing then run(true, effectiveConfig.debug.out) - else if forceDebug then run(true, Config.DebugOutput.StdIO) - else - diagnostics.foreach(summon[Raise]) - // Token display is a legacy diff-test option. Re-lexing does not report diagnostics twice. - if showTokens then - val lexer = new syntax.Lexer(origin, false)(using _ => ()) - output(Config.DebugOutput.StdIO, syntax.Lexer.printTokens(lexer.bracketedTokens)) - discovery - result -> effectiveConfig - def showParsedDefinitions(trees: Ls[syntax.Tree], config: Config) (display: (syntax.Tree, Config.DebugOutput) => Unit): Unit = def visit(tree: syntax.Tree): Unit = tree match @@ -106,8 +76,27 @@ object DebugOutputHandler: statement.subStatements.foreach(visit) visit(block) + /** Use the same selection and formatting in the compiler and diff-test pipelines. */ + def showIR(program: codegen.Program, optimized: Bool, printer: codegen.Printer, width: Int) + (display: (Str, Str, Config.DebugOutput) => Unit)(using Config, utils.Scope): Unit = + val title = if optimized then "Optimized IR" else "Lowered IR" + val isEnabled: Config.Debug => Bool = if optimized then _.showOptimizedIR else _.showIR + if isEnabled(config.debug) then + display(title, printer.worksheet(program).mkString(width), config.debug.out) + else forEachIRDefinition(program, isEnabled): (defn, out) => + display(title, printer.printDefinition(defn).mkString(width), out) + + def showLoweredTree(program: codegen.Program)(display: (Str, Str, Config.DebugOutput) => Unit) + (using Config, DebugPrinter): Unit = + if config.debug.showLoweredTree then + display("Lowered IR Tree", program.showAsTree, config.debug.out) + else forEachIRDefinition(program, _.showLoweredTree): (defn, out) => + val tree = defn match + case product: Product => product.showAsTree + display("Lowered IR Tree", tree, out) + /** Methods are visited directly by BlockTraverser, without passing through applyDefn. */ - def forEachIRDefinition(program: codegen.Program, isEnabled: Config.Debug => Bool) + private def forEachIRDefinition(program: codegen.Program, isEnabled: Config.Debug => Bool) (display: (codegen.Defn, Config.DebugOutput) => Unit): Unit = def visit(defn: codegen.Defn): Unit = defn.configOverride.foreach: localConfig => diff --git a/hkmc2/shared/src/main/scala/hkmc2/MLsCompiler.scala b/hkmc2/shared/src/main/scala/hkmc2/MLsCompiler.scala index 408802c330..654d8bbea3 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/MLsCompiler.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/MLsCompiler.scala @@ -14,8 +14,15 @@ class ParserSetup(file: io.Path, outputHandler: DebugOutputHandler) val fph = new FastParseHelpers(block) val origin = Origin(file, 0, fph) - val (result, effectiveConfig) = DebugOutputHandler.parse( - origin, forceDebug = false, showTokens = false, outputHandler.emit) + val lexer = new syntax.Lexer(origin, dbg = false) + val tokens = lexer.bracketedTokens + + val rules = syntax.ParseRules() + val parser = new syntax.Parser(origin, tokens, rules, raise, dbg = false): + def doPrintDbg(msg: => Str): Unit = () + + val result = parser.parseAll(parser.block(allowNewlines = true)) + val effectiveConfig = Config.elaborationConfig(result) if effectiveConfig.debug.showParsedTree then outputHandler.emit( diff --git a/hkmc2/shared/src/test/mlscript/codegen/BadDebugDirective.mls b/hkmc2/shared/src/test/mlscript/codegen/BadDebugDirective.mls index 305a6bd932..2d14220040 100644 --- a/hkmc2/shared/src/test/mlscript/codegen/BadDebugDirective.mls +++ b/hkmc2/shared/src/test/mlscript/codegen/BadDebugDirective.mls @@ -1,4 +1,4 @@ -:noCodeGen +:js :e @@ -22,8 +22,21 @@ //│ ╙── ^^^^^^^ +// Parsing happens before directives can be read. :e -#dbg(out: Somewhere) -//│ ╔══[COMPILATION ERROR] Expected StdIO or File("path") -//│ ║ l.26: #dbg(out: Somewhere) -//│ ╙── ^^^^^^^^^ +#dbg('parsing) +//│ ╔══[COMPILATION ERROR] Unknown debugging aspect 'parsing' +//│ ║ l.27: #dbg('parsing) +//│ ╙── ^^^^^^^^ + + +// Unsupported local phases warn without suppressing supported ones. +:w +:expect 57 +@dbg('optimizations, 'lowering, showUids: false) fun mixed_local() = 57 +mixed_local() +//│ ╔══[WARNING] Debugging aspect 'optimizations' is not supported in @dbg; use #dbg(optimizations: true) instead. +//│ ║ l.36: @dbg('optimizations, 'lowering, showUids: false) fun mixed_local() = 57 +//│ ╙── ^^^^^^^^^^^^^^ +//│ Lowering.term 57 +//│ = 57 diff --git a/hkmc2/shared/src/test/mlscript/codegen/DebugDefinitionPhases.mls b/hkmc2/shared/src/test/mlscript/codegen/DebugDefinitionPhases.mls deleted file mode 100644 index d483e534bb..0000000000 --- a/hkmc2/shared/src/test/mlscript/codegen/DebugDefinitionPhases.mls +++ /dev/null @@ -1,200 +0,0 @@ -:js - - -#dbg(showUids: false) -@dbg('resolution, 'lowering) -fun selected_debug(x) = x -fun unrelated(x) = x -//│ | Resolving definition: TermDefinition(Fun,member:selected_debug,term:selected_debug,List(ParamList(‹›,List(Param(‹›,x,None,Modulefulness(None))),None)),None,None,Some(Ref(x)),‹›,Modulefulness(None),List(Debug(...)),None) -//│ | | Resolving function definition TermDefinition(Fun,member:selected_debug,term:selected_debug,List(ParamList(‹›,List(Param(‹›,x,None,Modulefulness(None))),None)),None,None,Some(Ref(x)),‹›,Modulefulness(None),List(Debug(...)),None) -//│ | | Resolving parameter x⁰ -//│ | | Traversing term: Ref(x) (expect = NonModule) -//│ | | | Resolving resolvable term: Ref(x), (prefer = NonModule, inAppPrefix = false, inCtxPrefix = false, inTyPrefix = false) -//│ | | | | Resolving symbol for term: Ref(x) (prefer = NonModule) -//│ | | | | -> (sym = Some(x), typ = None) -//│ | | | | Resolving the type for term: Ref(x) (prefer = NonModule, sym = Some(x)) -//│ | | | | | Declaration: Some(Param(‹›,x,None,Modulefulness(None))) -//│ | | | | -> typ = None -//│ | | | | Resolving resolvable term Ref(x) with sym = Some(x), typ = None -//│ | | | ~> Ref(x) (sym = Some(x), typ = None) -//│ | | | Checking Ref(x): expect NonModule, evalsToModule = false, evalsToStaticClass = false -//│ Lowering.term x⁰ – symbol x -//│ Expanding term Ref(x) -//│ ~> Ref(x) - - -module Main with - // The IR of `foo1` should be shown - @dbg('showParsedTree, 'showElaboratedTree, 'showLoweredTree, 'showOptimizedIR) - fun foo1 = 2 + 2 - // The IR of `foo2` should NOT be shown - fun foo2 = 2 + 2 -//│ ———————————————| Parsed tree |—————————————————————————————————————————————————————————————————————— -//│ Annotated: -//│ annotation = App: -//│ lhs = Ident of "dbg" -//│ rhs = Tup of Ls of -//│ Ident of "'showParsedTree" -//│ Ident of "'showElaboratedTree" -//│ Ident of "'showLoweredTree" -//│ Ident of "'showOptimizedIR" -//│ target = TermDef: -//│ k = Fun -//│ head = Ident of "foo1" -//│ rhs = S of OpApp: -//│ lhs = IntLit of 2 -//│ op = Ident of "+" -//│ rhss = Ls of -//│ IntLit of 2 -//│ —————————————| Elaborated tree |———————————————————————————————————————————————————————————————————— -//│ TermDefinition: -//│ k = Fun -//│ sym = member:foo1⁰ -//│ tsym = term:Main⁰/foo1¹ -//│ body = S of App: -//│ lhs = Ref{sym=builtin:+⁰} of builtin:+⁰ -//│ rhs = Tup of Ls of -//│ Fld: -//│ term = Lit of IntLit of 2 -//│ Fld: -//│ term = Lit of IntLit of 2 -//│ modulefulness = Modulefulness of N -//│ annotations = Ls of -//│ Debug of ... -//│ —————————————| Lowered IR Tree |———————————————————————————————————————————————————————————————————— -//│ FunDefn: -//│ owner = S of module:Main⁰ -//│ sym = member:foo1⁰ -//│ dSym = term:Main⁰/foo1¹ -//│ body = Return of Lit of IntLit of 4 -//│ ——————————————| Optimized IR |—————————————————————————————————————————————————————————————————————— -//│ @dbg(...) define foo1⁰ as fun foo1¹ { return 4 } -//│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— - -Main.foo1 -//│ = 4 - - -#dbg('showParsedTree, 'showElaboratedTree, 'showLoweredTree) -fun shown_trees = 42 -//│ ———————————————| Parsed tree |—————————————————————————————————————————————————————————————————————— -//│ App: -//│ lhs = Directive: -//│ prefix = Ident of "dbg" -//│ body = Tup of Nil -//│ rhs = Tup of Ls of -//│ Ident of "'showParsedTree" -//│ Ident of "'showElaboratedTree" -//│ Ident of "'showLoweredTree" -//│ TermDef: -//│ k = Fun -//│ head = Ident of "shown_trees" -//│ rhs = S of IntLit of 42 -//│ —————————————| Elaborated tree |———————————————————————————————————————————————————————————————————— -//│ Blk: -//│ stats = Ls of -//│ SetConfig of ... -//│ TermDefinition: -//│ k = Fun -//│ sym = member:shown_trees⁰ -//│ tsym = term:shown_trees¹ -//│ body = S of Lit of IntLit of 42 -//│ modulefulness = Modulefulness of N -//│ res = Lit of UnitLit of false -//│ —————————————| Lowered IR Tree |———————————————————————————————————————————————————————————————————— -//│ Program: -//│ main = Define: -//│ defn = FunDefn: -//│ sym = member:shown_trees⁰ -//│ dSym = term:shown_trees¹ -//│ params = Ls of -//│ ParamList: ‹empty› -//│ body = Return of Lit of IntLit of 42 -//│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— - - -#dbg(showParsedTree: false, showElaboratedTree: false, showLoweredTree: false) - - -#dbg('optimizations) -fun optimization_debug(x) = x -//│ ⬤ Simplif. iter. 1 -//│ Captured variables: HashSet() -//│ ⬤ Simplif. iter. 1 -//│ Captured variables: HashSet() - - -#dbg(optimizations: false) - - -:dopt -fun command_optimization_debug(x) = x -//│ ⬤ Simplif. iter. 1 -//│ Captured variables: HashSet() -//│ ⬤ Simplif. iter. 1 -//│ Captured variables: HashSet() - - -#dbg('showIR, 'showOptimizedIR) -fun shown_ir(x) = x -//│ ———————————————| Lowered IR |——————————————————————————————————————————————————————————————————————— -//│ define shown_ir⁰ as fun shown_ir¹(x) { return x }; end -//│ ——————————————| Optimized IR |—————————————————————————————————————————————————————————————————————— -//│ define shown_ir⁰ as fun shown_ir¹(x) { return x }; end -//│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— - - -#dbg(showIR: false, showOptimizedIR: false) - - -:sir -:soir -fun command_shown_ir(x) = x -//│ ———————————————| Lowered IR |——————————————————————————————————————————————————————————————————————— -//│ define command_shown_ir⁰ as fun command_shown_ir¹(x) { return x }; end -//│ ——————————————| Optimized IR |—————————————————————————————————————————————————————————————————————— -//│ define command_shown_ir⁰ as fun command_shown_ir¹(x) { return x }; end -//│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— - - -// Local values lower to assignments; their debug scope must end before the next value. -#lang(0.3.x) - -:expect 3 -fun debug_local_values() = - @dbg('lowering, showUids: false) val selected_local = 1 - val unrelated_local = 2 - selected_local + unrelated_local -debug_local_values() -//│ Lowering.term 1 -//│ = 3 - - -// New resolution shares elaboration tracing and skips the legacy resolver's tree traversal. -:expect 3 -@dbg('resolution, 'showElaboratedTree, showUids: false) fun selected_new(x) = x -fun unrelated_new(x) = x -selected_new(3) -//│ Processing term definition App(Ident(selected_new),Tup(List(Ident(x)))) -//│ Elaborating ParamTree: ParamTree(‹›,Ident(x),None,None,Set()) -//│ Elab term Ident(x) -//│ | Elab subterm Ident(x) -//│ | ~> x¹ -//│ ~> x¹ -//│ —————————————| Elaborated tree |———————————————————————————————————————————————————————————————————— -//│ TermDefinition: -//│ k = Fun -//│ sym = member:selected_new⁰ -//│ tsym = term:selected_new¹ -//│ params = Ls of -//│ ParamList: -//│ params = Ls of -//│ Param: -//│ sym = x¹ -//│ modulefulness = Modulefulness of N -//│ body = S of SimpleRef of x¹ -//│ modulefulness = Modulefulness of N -//│ annotations = Ls of -//│ Debug of ... -//│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— -//│ = 3 diff --git a/hkmc2/shared/src/test/mlscript/codegen/DebugDestinations.mls b/hkmc2/shared/src/test/mlscript/codegen/DebugDestinations.mls index 1a9e0715f9..00bf6aa5e8 100644 --- a/hkmc2/shared/src/test/mlscript/codegen/DebugDestinations.mls +++ b/hkmc2/shared/src/test/mlscript/codegen/DebugDestinations.mls @@ -2,116 +2,59 @@ :silent -val debugFile = "hkmc2/shared/src/test/mlscript/codegen/DebugDestinations.log" -fs.rmSync(debugFile, {force: true}) +val debugFile = "target/DebugDestinations.log" +fs.writeFileSync(debugFile, "old session contents") -// Nested annotations inherit the enclosing phase and destination. Siblings do not. -@dbg('lowering, showUids: false, out: File("DebugDestinations.log")) -fun selected_file() = - @dbg(showUids: false) fun nested_file() = 42 - @dbg(lowering: false) fun muted_file() = 43 - nested_file() -fun unrelated_file() = 44 +@dbg('lowering, showUids: false, out: File("../../../../../../target/DebugDestinations.log")) +fun selected_file() = 42 +fun unrelated_file() = 43 -:silent -:expect true -let trace = fs.readFileSync(debugFile, "utf8") -trace.includes("Lowering.term 42\n") and - not(trace.includes("Lowering.term 43\n")) and - not(trace.includes("Lowering.term 44\n")) - - -// Methods route to the same output handler as free functions. +// Later definitions append to the same file, including methods. module FileMethods with - @dbg('lowering, showUids: false, out: File("DebugDestinations.log")) + @dbg('lowering, showUids: false, out: File("../../../../../../target/DebugDestinations.log")) fun selected() = 45 fun unrelated() = 46 -:silent :expect true -let trace = fs.readFileSync(debugFile, "utf8") -trace.includes("Lowering.term 42\n") and - trace.includes("Lowering.term 45\n") and - not(trace.includes("Lowering.term 46\n")) +fs.readFileSync(debugFile, "utf8") == "Lowering.term 42\nLowering.term 45\n" +//│ = true -:silent -fs.rmSync(debugFile, {force: true}) +// A destination alone must not redirect tracing forced by diff-test commands. +#dbg(out: File("../../../../../../target/DebugDestinations.log")) +:dl +@dbg(showUids: false) fun forced_output() = 47 +//│ Lowering.term 47 +//│ Lowering.term undefined +//│ avoidNames Scoped(Set(member:forced_output),Define(FunDefn(None,member:forced_output,term:forced_output,List(Pa... +//│ | avoidNames Define(FunDefn(None,member:forced_output,term:forced_output,List(ParamList(‹›,List(),None)),Return(L... +//│ | | avoidNames Return(Lit(IntLit(47)))... +//│ | | avoidNames End()... -// A discovery parse and its traced replay must report each syntax error only once. -:noCodeGen -:pe -#dbg('parsing, out: File("DebugDestinationsParse.log")) -fun broken(x) = ) -//│ ╔══[PARSE ERROR] Unexpected closing parenthesis -//│ ║ l.48: fun broken(x) = ) -//│ ╙── ^ -//│ ╔══[PARSE ERROR] Expected expression or block after 'fun' binding equals sign; found end of input instead -//│ ║ l.48: fun broken(x) = ) -//│ ╙── ^ - -#dbg(parsing: false, out: StdIO) +#dbg(out: StdIO) -:silent :expect true -fs.readFileSync("hkmc2/shared/src/test/mlscript/codegen/DebugDestinationsParse.log", "utf8").includes("blockOf") - -:silent -fs.rmSync("hkmc2/shared/src/test/mlscript/codegen/DebugDestinationsParse.log", {force: true}) +fs.readFileSync(debugFile, "utf8") == "Lowering.term 42\nLowering.term 45\n" +//│ = true -// Diff-test commands write to the test output, even when #dbg selects a file destination. +// Imported units share a handler. Relative and absolute destinations name the same log. +// Generate fixtures under target/, outside test discovery, just like the emitted logs. :silent -fs.rmSync("hkmc2/shared/src/test/mlscript/codegen/DebugDestinationsCommand.log", {force: true}) +val importLog = "target/DebugImports.log" +val absoluteLog = JSON.stringify(fs.realpathSync("target") + "/DebugImports.log") +fs.writeFileSync(importLog, "old session contents") +fs.writeFileSync("target/DebugImportA.mls", "#lang(0.3.x)\n@dbg('lowering, showUids: false, out: File(\"../../../../target/DebugImports.log\"))\nmodule DebugImportA with\n fun value() = 2101\n") +fs.writeFileSync("target/DebugImportB.mls", "#lang(0.3.x)\nimport \"./DebugImportA.mls\"\n@dbg('lowering, showUids: false, out: File(" + absoluteLog + "))\nmodule DebugImportB with\n fun value() = 2102\n") :noCodeGen -#dbg(out: File("DebugDestinationsCommand.log")) - -:noCodeGen -:dp -1 -//│ ┊1┊ -//│ @ blockOf(start of expression,allowNewlines = true) [at syntax/Parser.scala:66] -//│ │ ? blockOfImpl inspects ┊1┊ [at syntax/Parser.scala:340] -//│ │ @ simpleExpr(1) [at syntax/Parser.scala:429] -//│ │ │ ! simpleExprImpl consumes ┊1┊ [at syntax/Parser.scala:674] -//│ │ │ @ exprCont(1,`IntLit(1)`,true) [at syntax/Parser.scala:675] -//│ │ │ │ ? exprContImpl inspects ┊┊ [at syntax/Parser.scala:931] -//│ │ │ │ @ exprJux(1,`IntLit(1)`,false) [at syntax/Parser.scala:1203] -//│ │ │ │ │ ? exprJuxImpl inspects ┊┊ [at syntax/Parser.scala:1210] -//│ │ │ │ │ stops at the end of input -//│ │ │ │ = IntLit(1) -//│ │ │ = IntLit(1) -//│ │ = IntLit(1) -//│ │ @ parseRule(1,expression ::= End(),allowNewlines = true) [at syntax/Parser.scala:431] -//│ │ = Some(()) -//│ = List(IntLit(1)) -//│ ? parseAll inspects ┊┊ [at syntax/Parser.scala:261] - -#dbg(out: StdIO) - -:expect false -fs.existsSync("hkmc2/shared/src/test/mlscript/codegen/DebugDestinationsCommand.log") -//│ = false - -:silent -fs.rmSync("hkmc2/shared/src/test/mlscript/codegen/DebugDestinationsCommand.log", {force: true}) - - -// Absolute file destinations are not resolved against the base directory, as for imports. -:silent -val absoluteDebugFile = "/tmp/mlscript-DebugDestinationsAbsolute.log" -fs.rmSync(absoluteDebugFile, {force: true}) - -@dbg('lowering, showUids: false, out: File("/tmp/mlscript-DebugDestinationsAbsolute.log")) -fun absolute_file() = 1801 +import "../../../../../../target/DebugImportB.mls" :expect true -fs.existsSync(absoluteDebugFile) and fs.readFileSync(absoluteDebugFile, "utf8").includes("Lowering.term 1801\n") +let trace = fs.readFileSync(importLog, "utf8") +trace.includes("Lowering.term 2101") && trace.includes("Lowering.term 2102") && not(trace.includes("old session contents")) //│ = true - -:silent -fs.rmSync(absoluteDebugFile, {force: true}) +//│ trace = "Lowering.term 2101\nLowering.term { } (in stmt)\nLowering.term undefined (in stmt)\nLowering.term 2102\nLowering.term { } (in stmt)\nLowering.term undefined (in stmt)\n" diff --git a/hkmc2/shared/src/test/mlscript/codegen/DebugDirective.mls b/hkmc2/shared/src/test/mlscript/codegen/DebugDirective.mls index 4cb36f3aef..9d628d5909 100644 --- a/hkmc2/shared/src/test/mlscript/codegen/DebugDirective.mls +++ b/hkmc2/shared/src/test/mlscript/codegen/DebugDirective.mls @@ -1,4 +1,4 @@ -:noCodeGen +:js // Shorthand annotation debugging is scoped to the annotated definition. @@ -17,20 +17,13 @@ fun do_not_debug_me(x) = x fun named_debug_config(x) = x -// File destinations are accepted even when no phase is enabled. -#dbg(out: File("unused-debug.log")) -fun file_debug_config(x) = x - -#dbg(out: StdIO) - - // A top-level directive persists into the following block. #dbg(elaboration: true, showUids: false) -//│ Elab block List(App(Directive(Ident(dbg),Tup(List())),Tup(List(InfixApp(Ident(elaboration),Keywrd(keyword ':'),[...] LocalScope(block:6) +//│ Elab block List(App(Directive(Ident(dbg),Tup(List())),Tup(List(InfixApp(Ident(elaboration),Keywrd(keyword ':'),[...] LocalScope(block:4) //│ ~> { #config(...); } fun persistently_debugged(x) = x -//│ Elab block List(TermDef(Fun,App(Ident(persistently_debugged),Tup(List(Ident(x)))),Some(Ident(x)))) LocalScope(block:7) +//│ Elab block List(TermDef(Fun,App(Ident(persistently_debugged),Tup(List(Ident(x)))),Some(Ident(x)))) LocalScope(block:5) //│ | Processing term definition App(Ident(persistently_debugged),Tup(List(Ident(x)))) //│ | Elaborating ParamTree: ParamTree(‹›,Ident(x),None,None,Set()) //│ | Elab term Ident(x) @@ -40,3 +33,93 @@ fun persistently_debugged(x) = x //│ ~> { fun member:persistently_debugged(x¹) = x¹; } #dbg(elaboration: false) + + +module Main with + // The IR of `foo1` should be shown + @dbg('showParsedTree, 'showElaboratedTree, 'showLoweredTree, 'showOptimizedIR) + fun foo1 = 2 + 2 + // The IR of `foo2` should NOT be shown + fun foo2 = 2 + 2 +//│ ———————————————| Parsed tree |—————————————————————————————————————————————————————————————————————— +//│ Annotated: +//│ annotation = App: +//│ lhs = Ident of "dbg" +//│ rhs = Tup of Ls of +//│ Ident of "'showParsedTree" +//│ Ident of "'showElaboratedTree" +//│ Ident of "'showLoweredTree" +//│ Ident of "'showOptimizedIR" +//│ target = TermDef: +//│ k = Fun +//│ head = Ident of "foo1" +//│ rhs = S of OpApp: +//│ lhs = IntLit of 2 +//│ op = Ident of "+" +//│ rhss = Ls of +//│ IntLit of 2 +//│ —————————————| Elaborated tree |———————————————————————————————————————————————————————————————————— +//│ TermDefinition: +//│ k = Fun +//│ sym = member:foo1⁰ +//│ tsym = term:Main⁰/foo1¹ +//│ body = S of App: +//│ lhs = Ref{sym=builtin:+⁰} of builtin:+⁰ +//│ rhs = Tup of Ls of +//│ Fld: +//│ term = Lit of IntLit of 2 +//│ Fld: +//│ term = Lit of IntLit of 2 +//│ modulefulness = Modulefulness of N +//│ annotations = Ls of +//│ Debug of ... +//│ —————————————| Lowered IR Tree |———————————————————————————————————————————————————————————————————— +//│ FunDefn: +//│ owner = S of module:Main⁰ +//│ sym = member:foo1⁰ +//│ dSym = term:Main⁰/foo1¹ +//│ body = Return of Lit of IntLit of 4 +//│ ——————————————| Optimized IR |—————————————————————————————————————————————————————————————————————— +//│ @dbg(...) define foo1⁰ as fun foo1¹ { return 4 } +//│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— + +Main.foo1 +//│ = 4 + + +// Whole-program displays and optimization tracing use the same settings. +#dbg('showParsedTree, 'showElaboratedTree, 'showLoweredTree, 'showIR, 'showOptimizedIR, 'optimizations) +42 +//│ ———————————————| Parsed tree |—————————————————————————————————————————————————————————————————————— +//│ App: +//│ lhs = Directive: +//│ prefix = Ident of "dbg" +//│ body = Tup of Nil +//│ rhs = Tup of Ls of +//│ Ident of "'showParsedTree" +//│ Ident of "'showElaboratedTree" +//│ Ident of "'showLoweredTree" +//│ Ident of "'showIR" +//│ Ident of "'showOptimizedIR" +//│ Ident of "'optimizations" +//│ IntLit of 42 +//│ —————————————| Elaborated tree |———————————————————————————————————————————————————————————————————— +//│ Blk: +//│ stats = Ls of +//│ SetConfig of ... +//│ res = Lit of IntLit of 42 +//│ —————————————| Lowered IR Tree |———————————————————————————————————————————————————————————————————— +//│ Program: +//│ main = Return of Lit of IntLit of 42 +//│ ———————————————| Lowered IR |——————————————————————————————————————————————————————————————————————— +//│ return 42 +//│ ⬤ Simplif. iter. 1 +//│ Captured variables: HashSet() +//│ ⬤ Simplif. iter. 1 +//│ Captured variables: HashSet() +//│ ——————————————| Optimized IR |—————————————————————————————————————————————————————————————————————— +//│ return 42 +//│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— +//│ = 42 + +#dbg(showParsedTree: false, showElaboratedTree: false, showLoweredTree: false, showIR: false, showOptimizedIR: false, optimizations: false) diff --git a/hkmc2/shared/src/test/mlscript/codegen/DebugImports.mls b/hkmc2/shared/src/test/mlscript/codegen/DebugImports.mls deleted file mode 100644 index 3f8deacfeb..0000000000 --- a/hkmc2/shared/src/test/mlscript/codegen/DebugImports.mls +++ /dev/null @@ -1,26 +0,0 @@ -:js - - -// Generate imported units after test discovery; they are fixtures, not standalone diff tests. -:silent -val debugImportDir = "hkmc2/shared/src/test/mlscript/codegen/" -fs.writeFileSync(debugImportDir + "DebugImports.log", "old session contents") -fs.writeFileSync(debugImportDir + "DebugImportA.mls", "@dbg('lowering, showUids: false, out: File(\"mlscript/codegen/DebugImports.log\"))\nmodule DebugImportA with\n fun value() = 2101\n") -fs.writeFileSync(debugImportDir + "DebugImportB.mls", "import \"./DebugImportA.mls\"\n@dbg('lowering, showUids: false, out: File(\"mlscript/codegen/DebugImports.log\"))\nmodule DebugImportB with\n fun value() = 2102\n") - - -// Importing B also compiles A. Both must contribute to the same log in this session. -:noCodeGen -import "./DebugImportB.mls" - - -:silent -:expect true -val importLog = fs.readFileSync(debugImportDir + "DebugImports.log", "utf8") -importLog.includes("Lowering.term 2101") && importLog.includes("Lowering.term 2102") && not(importLog.includes("old session contents")) - - -:silent -fs.rmSync(debugImportDir + "DebugImportA.mls", {force: true}) -fs.rmSync(debugImportDir + "DebugImportB.mls", {force: true}) -fs.rmSync(debugImportDir + "DebugImports.log", {force: true}) diff --git a/hkmc2/shared/src/test/mlscript/codegen/DebugParsingDirective.mls b/hkmc2/shared/src/test/mlscript/codegen/DebugParsingDirective.mls deleted file mode 100644 index 4a23e1441b..0000000000 --- a/hkmc2/shared/src/test/mlscript/codegen/DebugParsingDirective.mls +++ /dev/null @@ -1,57 +0,0 @@ -:noCodeGen - - -#dbg('parsing) -//│ ┊#┊dbg┊(⟨┊'parsing┊⟩)┊ -//│ @ blockOf(start of expression,allowNewlines = true) [at syntax/Parser.scala:66] -//│ │ ? blockOfImpl inspects ┊#┊dbg┊(⟨┊'parsing┊⟩)┊ [at syntax/Parser.scala:340] -//│ │ ! blockOfImpl consumes ┊#┊ [at syntax/Parser.scala:367] -//│ │ @ parseRule(39,'#' directive keyword ::= Expr('#' directive body ::= End()),allowNewlines = true) [at syntax/Parser.scala:380] -//│ │ │ @ simpleExpr(39) [at syntax/Parser.scala:429] -//│ │ │ │ ! simpleExprImpl consumes ┊dbg┊ [at syntax/Parser.scala:657] -//│ │ │ │ @ exprCont(39,`Ident(dbg)`,true) [at syntax/Parser.scala:672] -//│ │ │ │ │ ? exprContImpl inspects ┊(⟨┊'parsing┊⟩)┊ [at syntax/Parser.scala:931] -//│ │ │ │ │ @ exprJux(39,`Ident(dbg)`,false) [at syntax/Parser.scala:1203] -//│ │ │ │ │ │ ? exprJuxImpl inspects ┊(⟨┊'parsing┊⟩)┊ [at syntax/Parser.scala:1210] -//│ │ │ │ │ │ stops at BRACKETS(Round,List((IDENT('parsing,false),Loc(5,13,DebugParsingDirective.mls:+4)))) -//│ │ │ │ │ = Ident(dbg) -//│ │ │ │ = Ident(dbg) -//│ │ │ = Ident(dbg) -//│ │ │ @ parseRule(39,'#' directive body ::= End(),allowNewlines = true) [at syntax/Parser.scala:431] -//│ │ │ = Some(()) -//│ │ = Some(Directive(Ident(dbg),Tup(List()))) -//│ │ @ exprCont(1,`Directive(Ident(dbg),Tup(List()))`,true) [at syntax/Parser.scala:381] -//│ │ │ ? exprContImpl inspects ┊(⟨┊'parsing┊⟩)┊ [at syntax/Parser.scala:931] -//│ │ │ ! exprContImpl consumes ┊(⟨┊'parsing┊⟩)┊ [at syntax/Parser.scala:1133] -//│ │ │ > @ concludeWith(()) [at syntax/Parser.scala:1134] -//│ │ │ > │ @ blockOf(start of expression,allowNewlines = false) [at syntax/Parser.scala:310] -//│ │ │ > │ │ ? blockOfImpl inspects ┊'parsing┊ [at syntax/Parser.scala:340] -//│ │ │ > │ │ @ simpleExpr(1) [at syntax/Parser.scala:429] -//│ │ │ > │ │ │ ! simpleExprImpl consumes ┊'parsing┊ [at syntax/Parser.scala:657] -//│ │ │ > │ │ │ @ exprCont(1,`Ident('parsing)`,false) [at syntax/Parser.scala:672] -//│ │ │ > │ │ │ │ ? exprContImpl inspects ┊┊ [at syntax/Parser.scala:931] -//│ │ │ > │ │ │ │ @ exprJux(1,`Ident('parsing)`,false) [at syntax/Parser.scala:1203] -//│ │ │ > │ │ │ │ │ ? exprJuxImpl inspects ┊┊ [at syntax/Parser.scala:1210] -//│ │ │ > │ │ │ │ │ stops at the end of input -//│ │ │ > │ │ │ │ = Ident('parsing) -//│ │ │ > │ │ │ = Ident('parsing) -//│ │ │ > │ │ = Ident('parsing) -//│ │ │ > │ │ @ parseRule(1,expression ::= End(),allowNewlines = false) [at syntax/Parser.scala:431] -//│ │ │ > │ │ = Some(()) -//│ │ │ > │ │ ? blockOfImpl inspects ┊┊ [at syntax/Parser.scala:413] -//│ │ │ > │ = List(Ident('parsing)) -//│ │ │ > │ ? concludeWithImpl inspects ┊┊ [at syntax/Parser.scala:274] -//│ │ │ > │ Concluded with List(Ident('parsing)) -//│ │ │ > = List(Ident('parsing)) -//│ │ │ @ exprCont(1,`App(Directive(Ident(dbg),Tup(List())),Tup(List(Ident('parsing))))`,true) [at syntax/Parser.scala:1136] -//│ │ │ │ ? exprContImpl inspects ┊┊ [at syntax/Parser.scala:931] -//│ │ │ │ @ exprJux(1,`App(Directive(Ident(dbg),Tup(List())),Tup(List(Ident('parsing))))`,false) [at syntax/Parser.scala:1203] -//│ │ │ │ │ ? exprJuxImpl inspects ┊┊ [at syntax/Parser.scala:1210] -//│ │ │ │ │ stops at the end of input -//│ │ │ │ = App(Directive(Ident(dbg),Tup(List())),Tup(List(Ident('parsing)))) -//│ │ │ = App(Directive(Ident(dbg),Tup(List())),Tup(List(Ident('parsing)))) -//│ │ = App(Directive(Ident(dbg),Tup(List())),Tup(List(Ident('parsing)))) -//│ = List(App(Directive(Ident(dbg),Tup(List())),Tup(List(Ident('parsing))))) -//│ ? parseAll inspects ┊┊ [at syntax/Parser.scala:261] - -#dbg(parsing: false) diff --git a/hkmc2/shared/src/test/mlscript/codegen/DebugScoping.mls b/hkmc2/shared/src/test/mlscript/codegen/DebugScoping.mls index 11585a3c45..ecb7e99bd3 100644 --- a/hkmc2/shared/src/test/mlscript/codegen/DebugScoping.mls +++ b/hkmc2/shared/src/test/mlscript/codegen/DebugScoping.mls @@ -18,8 +18,9 @@ DebugMethods.selected(7) // Changing only UID display should retain the outer definition's trace flag. @dbg('lowering, showUids: false) fun debug_outer() = @dbg(showUids: false) fun debug_inner() = 42 + @dbg(lowering: false) fun debug_muted() = 43 debug_inner() -//│ Lowering.term { fun member:debug_inner() = 42; member:debug_inner⁰() } +//│ Lowering.term { fun member:debug_inner() = 42; fun member:debug_muted() = 43; member:debug_inner⁰() } //│ Lowering.term 42 //│ Lowering.term member:debug_inner⁰() //│ Expanding term App(Ref(member:debug_inner),Tup(List())) @@ -87,3 +88,72 @@ print(1701) //│ Lowering.term { } (in stmt) //│ Lowering.term undefined (in stmt) //│ > 1701 + + +// Local values lower to assignments; their debug scope must end before the next value. +#lang(0.3.x) + +:expect 3 +fun debug_local_values() = + @dbg('lowering, showUids: false) val selected_local = 1 + val unrelated_local = 2 + selected_local + unrelated_local +debug_local_values() +//│ Lowering.term 1 +//│ = 3 + + +// New resolution traces through elaboration, without visiting the legacy resolver. +@dbg('resolution, showUids: false) fun selected_new() = 3 +fun unrelated_new() = 4 +//│ Processing term definition App(Ident(selected_new),Tup(List())) +//│ Elab term IntLit(3) +//│ | Elab subterm IntLit(3) +//│ | ~> 3 +//│ ~> 3 + + +// Classes and their companions share lowering, while each elaboration annotation is scoped. +@dbg('elaboration, 'lowering, showUids: false) +class DebugCompanion(value: Int) with + fun answer() = 1101 +@dbg('elaboration, showUids: false) +module DebugCompanion with + fun answer() = 1102 +//│ Elaborating ParamTree: ParamTree(‹›,Ident(value),Some(Ident(Int)),None,Set()) +//│ Elab term Ident(Int) +//│ | Elab subterm Ident(Int) +//│ | | listen: trm = member:Int⁰ +//│ | ~> member:Int⁰^class:DebugCompanion⁰ +//│ ~> member:Int⁰^class:DebugCompanion⁰ +//│ transportType type of member:Int⁰ (previous = List(), marks = List(↘⟨class:DebugCompanion⁰⟩ϵ)) +//│ | .exit InstanceShape(missing) (↘⟨class:DebugCompanion⁰⟩ϵ) +//│ | | .exit InstanceShape(missing) (ϵ) +//│ | | = InstanceShape(missing) +//│ | = MarkedShape(InstanceShape(missing), ↘⟨class:DebugCompanion⁰⟩ϵ) +//│ Processing class definition Ident(DebugCompanion) +//│ | Companion: Some(module:DebugCompanion) +//│ | Elab block List(TermDef(Fun,App(Ident(answer),Tup(List())),Some(IntLit(1101)))) InnerScope(class:DebugCompanion) +//│ | | Processing term definition App(Ident(answer),Tup(List())) +//│ | | Elab term IntLit(1101) +//│ | | | Elab subterm IntLit(1101) +//│ | | | ~> 1101 +//│ | | ~> 1101 +//│ | ~> { method fun member:answer() = 1101; } +//│ Processing module/object definition Ident(DebugCompanion) +//│ | Companion: Some(class:DebugCompanion) +//│ | Elab block List(TermDef(Fun,App(Ident(answer),Tup(List())),Some(IntLit(1102)))) InnerScope(module:DebugCompanion) +//│ | | Processing term definition App(Ident(answer),Tup(List())) +//│ | | Elab term IntLit(1102) +//│ | | | Elab subterm IntLit(1102) +//│ | | | ~> 1102 +//│ | | ~> 1102 +//│ | ~> { fun member:answer() = 1102; } +//│ Lowering.term 1101 +//│ Lowering.term { val member:value = value⁰^term:DebugCompanion⁰/value¹; } (in stmt) +//│ Lowering.term value⁰^term:DebugCompanion⁰/value¹ – symbol value +//│ Lowering.term value⁰ – symbol value +//│ Lowering.term undefined (in stmt) +//│ Lowering.term 1102 +//│ Lowering.term { } (in stmt) +//│ Lowering.term undefined (in stmt) diff --git a/hkmc2/shared/src/test/mlscript/codegen/DebugTypes.mls b/hkmc2/shared/src/test/mlscript/codegen/DebugTypes.mls deleted file mode 100644 index aa60ee27d8..0000000000 --- a/hkmc2/shared/src/test/mlscript/codegen/DebugTypes.mls +++ /dev/null @@ -1,66 +0,0 @@ -:js - - -:silent -val debugTypesDir = "hkmc2/shared/src/test/mlscript/codegen/" -fs.rmSync(debugTypesDir + "DebugTypesElaboration.log", {force: true}) -fs.rmSync(debugTypesDir + "DebugTypesLowering.log", {force: true}) - - -// Type definitions trace their parameters and bodies, including nested methods. -:silent -@dbg('elaboration, showUids: false, out: File("DebugTypesElaboration.log")) -class ElaborationClass(value: Int) with - fun answer() = 1101 -@dbg('elaboration, showUids: false, out: File("DebugTypesElaboration.log")) -module ElaborationModule with - fun answer() = 1102 - - -:silent -:expect true -val elaborationLog = fs.readFileSync(debugTypesDir + "DebugTypesElaboration.log", "utf8") -elaborationLog.includes("Elaborating ParamTree") && elaborationLog.includes("IntLit(1101)") && elaborationLog.includes("IntLit(1102)") - - -// A class is lowered at its companion's definition. Both annotations must take effect. -:silent -@dbg('lowering, showUids: false, out: File("DebugTypesLowering.log")) -class LoweringCompanion(value) with - fun answer() = 1201 -@dbg('lowering, showUids: false, out: File("DebugTypesLowering.log")) -module LoweringCompanion with - fun answer() = 1202 -@dbg('lowering, showUids: false, out: File("DebugTypesLowering.log")) -module LoweringModule with - fun answer() = 1203 - - -:silent -:expect true -val loweringLog = fs.readFileSync(debugTypesDir + "DebugTypesLowering.log", "utf8") -loweringLog.includes("Lowering.term 1201") && loweringLog.includes("Lowering.term 1202") && loweringLog.includes("Lowering.term 1203") - - -// New resolution runs during elaboration; class tracing includes superclass lowering too. -#lang(0.3.x) - -:silent -class DebugBase(value: Int) -@dbg('resolution, showUids: false, out: File("DebugTypesElaboration.log")) -module ResolutionModule with - fun answer() = 1301 -@dbg('resolution, 'lowering, showUids: false, out: File("DebugTypesLowering.log")) -class ResolutionClass extends DebugBase(1302) with - fun answer() = 1303 - - -:silent -:expect true -val newResolutionLog = fs.readFileSync(debugTypesDir + "DebugTypesLowering.log", "utf8") -fs.readFileSync(debugTypesDir + "DebugTypesElaboration.log", "utf8").includes("IntLit(1301)") && newResolutionLog.includes("IntLit(1303)") && newResolutionLog.includes("Lowering.term 1302") && newResolutionLog.includes("Lowering.term 1303") - - -:silent -fs.rmSync(debugTypesDir + "DebugTypesElaboration.log", {force: true}) -fs.rmSync(debugTypesDir + "DebugTypesLowering.log", {force: true}) diff --git a/hkmc2/shared/src/test/mlscript/codegen/DebugUnsupportedPhases.mls b/hkmc2/shared/src/test/mlscript/codegen/DebugUnsupportedPhases.mls deleted file mode 100644 index 4fe5cfd7ee..0000000000 --- a/hkmc2/shared/src/test/mlscript/codegen/DebugUnsupportedPhases.mls +++ /dev/null @@ -1,80 +0,0 @@ -:js - - -:silent -val debugDir = "hkmc2/shared/src/test/mlscript/codegen/" -fs.rmSync(debugDir + "DebugLocalParsing.log", {force: true}) -fs.rmSync(debugDir + "DebugLocalOptimization.log", {force: true}) -fs.rmSync(debugDir + "DebugModuleElaboration.log", {force: true}) -fs.rmSync(debugDir + "DebugClassLowering.log", {force: true}) - - -// Parsing needs a file-level directive; the annotation warns and does not create a log. -:w -:expect false -@dbg('parsing, out: File("DebugLocalParsing.log")) fun parse_selected(x) = x -fs.existsSync(debugDir + "DebugLocalParsing.log") -//│ ╔══[WARNING] Debugging aspect 'parsing' is not supported in @dbg; use #dbg(parsing: true) instead. -//│ ║ l.15: @dbg('parsing, out: File("DebugLocalParsing.log")) fun parse_selected(x) = x -//│ ╙── ^^^^^^^^ -//│ = false - - -// Optimization passes do not support definition-local tracing either. -:w -:expect false -@dbg('optimizations, out: File("DebugLocalOptimization.log")) -fun optimize_selected(x) = ((y) => y)(x) -fs.existsSync(debugDir + "DebugLocalOptimization.log") -//│ ╔══[WARNING] Debugging aspect 'optimizations' is not supported in @dbg; use #dbg(optimizations: true) instead. -//│ ║ l.26: @dbg('optimizations, out: File("DebugLocalOptimization.log")) -//│ ╙── ^^^^^^^^^^^^^^ -//│ = false - - -// Module elaboration honors definition-local tracing. -:expect true -@dbg('elaboration, out: File("DebugModuleElaboration.log")) -module ElaborationSelected with - fun member(x) = x -fs.existsSync(debugDir + "DebugModuleElaboration.log") -//│ = true - - -// Class lowering honors definition-local tracing. -:expect true -@dbg('lowering, out: File("DebugClassLowering.log")) -class LoweringSelected(x) with - fun member() = x -fs.existsSync(debugDir + "DebugClassLowering.log") -//│ = true - - -:silent -fs.rmSync(debugDir + "DebugLocalParsing.log", {force: true}) -fs.rmSync(debugDir + "DebugLocalOptimization.log", {force: true}) -fs.rmSync(debugDir + "DebugModuleElaboration.log", {force: true}) -fs.rmSync(debugDir + "DebugClassLowering.log", {force: true}) - - -// Unsupported local settings also warn when explicitly disabled. -:w -@dbg(parsing: false, optimizations: false) fun disabled_local(x) = x -//│ ╔══[WARNING] Debugging aspect 'parsing' is not supported in @dbg; use #dbg(parsing: false) instead. -//│ ║ l.62: @dbg(parsing: false, optimizations: false) fun disabled_local(x) = x -//│ ╙── ^^^^^^^^^^^^^^ -//│ ╔══[WARNING] Debugging aspect 'optimizations' is not supported in @dbg; use #dbg(optimizations: false) instead. -//│ ║ l.62: @dbg(parsing: false, optimizations: false) fun disabled_local(x) = x -//│ ╙── ^^^^^^^^^^^^^^^^^^^^ - - -// A warning about one phase does not prevent a supported phase from tracing. -:w -:expect 57 -@dbg('parsing, 'lowering, showUids: false) fun mixed_local() = 57 -mixed_local() -//│ ╔══[WARNING] Debugging aspect 'parsing' is not supported in @dbg; use #dbg(parsing: true) instead. -//│ ║ l.74: @dbg('parsing, 'lowering, showUids: false) fun mixed_local() = 57 -//│ ╙── ^^^^^^^^ -//│ Lowering.term 57 -//│ = 57 diff --git a/hkmc2DiffTests/src/test/scala/hkmc2/JSBackendDiffMaker.scala b/hkmc2DiffTests/src/test/scala/hkmc2/JSBackendDiffMaker.scala index fe78d4a402..039b60dc60 100644 --- a/hkmc2DiffTests/src/test/scala/hkmc2/JSBackendDiffMaker.scala +++ b/hkmc2DiffTests/src/test/scala/hkmc2/JSBackendDiffMaker.scala @@ -126,19 +126,6 @@ abstract class JSBackendDiffMaker extends MLsDiffMaker: Printer() val print = (p: codegen.Program) => blockPrinter.worksheet(p)(using irPrintingScp).mkString(output.ColWidth) - def showDefinitionDebugIR( - title: Str, - program: codegen.Program, - isEnabled: Config.Debug => Bool, - ): Unit = - DebugOutputHandler.forEachIRDefinition(program, isEnabled): (defn, out) => - val ir = blockPrinter.printDefinition(defn)(using irPrintingScp).mkString(output.ColWidth) - outputDebugSection(title, ir, out) - def showDefinitionDebugTree(title: Str, program: codegen.Program): Unit = - DebugOutputHandler.forEachIRDefinition(program, _.showLoweredTree): (defn, out) => - val tree = defn match - case product: Product => product.showAsTree - outputDebugSection(title, tree, out) Config.extractConfigFromStats(blk).givenIn { val loweringState = summon[Elaborator.State] @@ -192,20 +179,12 @@ abstract class JSBackendDiffMaker extends MLsDiffMaker: passIdx += 1 showPipelineStage(s"pipeline #${passIdx} ${passName}", after) override def preOptimizeHook(prog: Program) = - if showLoweredTree.isSet || config.debug.showLoweredTree then - if showLoweredTree.isSet then - outputSeparator("Lowered IR Tree") - output(prog.showAsTree) - else outputDebugSection("Lowered IR Tree", prog.showAsTree, config.debug.out) - else showDefinitionDebugTree("Lowered IR Tree", prog) - if showIR.isSet || showIRLines.isSet || config.debug.showIR then - given ShowCfg = ShowCfg( - showErasedTypes = showIRErasedTypes.isSet, - showExpansionMappings = false, - showFlowSymbols = true, - debug = debug.isSet, - ) - val irStr = Printer().worksheet(prog)(using irPrintingScp).mkString(output.ColWidth) + if showLoweredTree.isSet then + outputSeparator("Lowered IR Tree") + output(prog.showAsTree) + else DebugOutputHandler.showLoweredTree(prog)(outputDebugSection) + if showIR.isSet || showIRLines.isSet then + val irStr = print(prog) val sloc = irStr.count(_ == '\n') + 1 if showIRLines.isSet then output(s"Lines of IR: ${sloc}") if showIR.isSet then @@ -213,26 +192,17 @@ abstract class JSBackendDiffMaker extends MLsDiffMaker: output(irStr) else if config.debug.showIR then outputDebugSection("Lowered IR", irStr, config.debug.out) - else showDefinitionDebugIR("Lowered IR", prog, _.showIR) + else DebugOutputHandler.showIR(prog, false, blockPrinter, output.ColWidth)(outputDebugSection)(using config, irPrintingScp) super.preOptimizeHook(prog) customPipeline.run(lowered, print, symbolsToPreserve, dtl) if checkIR.isSet then BlockChecker().applyProgram(optimized) - if showOptimizedIR.isSet || config.debug.showOptimizedIR then - given ShowCfg = ShowCfg( - showErasedTypes = showIRErasedTypes.isSet, - showExpansionMappings = false, - showFlowSymbols = true, - debug = debug.isSet, - ) - val irStr = Printer().worksheet(optimized)(using irPrintingScp).mkString(output.ColWidth) - if showOptimizedIR.isSet then - outputSeparator("Optimized IR") - output(irStr) - else outputDebugSection("Optimized IR", irStr, config.debug.out) - else showDefinitionDebugIR("Optimized IR", optimized, _.showOptimizedIR) + if showOptimizedIR.isSet then + outputSeparator("Optimized IR") + output(print(optimized)) + else DebugOutputHandler.showIR(optimized, true, blockPrinter, output.ColWidth)(outputDebugSection)(using config, irPrintingScp) if showOptimizedTree.isSet then outputSeparator("Optimized IR Tree") output(optimized.showAsTree) diff --git a/hkmc2DiffTests/src/test/scala/hkmc2/MLsDiffMaker.scala b/hkmc2DiffTests/src/test/scala/hkmc2/MLsDiffMaker.scala index 1bee64452b..b9b6a7cd2c 100644 --- a/hkmc2DiffTests/src/test/scala/hkmc2/MLsDiffMaker.scala +++ b/hkmc2DiffTests/src/test/scala/hkmc2/MLsDiffMaker.scala @@ -247,7 +247,6 @@ abstract class MLsDiffMaker extends DiffMaker: given Elaborator.State = new Elaborator.State: override protected def doDbg: Bool = doForceDbg - || activeDebug.parsing || activeDebug.elaboration || activeDebug.resolution || activeDebug.lowering @@ -445,8 +444,17 @@ abstract class MLsDiffMaker extends DiffMaker: val baseConfig = configModify(mkConfig) given Config = baseConfig - val (res, phaseConfig) = DebugOutputHandler.parse( - origin, dbgParsing.isSet, showParse.isSet, debugOutputHandler.emit) + val lexer = new syntax.Lexer(origin, dbg = dbgParsing.isSet): + override protected def doPrintDbg(msg: => Str): Unit = output(msg) + val tokens = lexer.bracketedTokens + + if showParse.isSet || dbgParsing.isSet then + output(syntax.Lexer.printTokens(tokens)) + + val parser = new syntax.Parser(origin, tokens, syntax.ParseRules(), raise, dbg = dbgParsing.isSet): + def doPrintDbg(msg: => Str): Unit = if dbg then output(msg) + val res = parser.parseAll(parser.block(allowNewlines = true)) + val phaseConfig = Config.elaborationConfig(res) activeDebug = phaseConfig.debug // If parsed tree is displayed, don't show the string serialization. From dd03ba888afbfff74485ce0e3798ce7e03e4edc8 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 18 Oct 2026 00:52:54 +0800 Subject: [PATCH 18/32] Address review: add showElaboratedTerm, drop the resolution aspect - Add the `showElaboratedTerm` debugging aspect, which prints the elaborated term in readable form with the settings of the `:sr` diff-test command, as `showIR` does for the IR. It shares the whole block/annotated definition selection with `showElaboratedTree`. - Remove the `resolution` aspect: it only traced the legacy resolver. New resolution happens during elaboration, so `elaboration` traces it. The `:dr` diff-test command still traces the legacy resolver. - Remove DebugOutputHandlerTest, which did not pay for its maintenance. - Restore the original indentation of local value lowering, a commented-out reference to `Assign`, and the original comment on the diff-test elaboration logger's `trace` override. Co-Authored-By: Claude Opus 5.5 --- doc/reference.md | 6 ++-- .../scala/hkmc2/DebugOutputHandlerTest.scala | 34 ------------------- .../src/main/scala/hkmc2/CompilerCtx.scala | 23 +++++++------ .../shared/src/main/scala/hkmc2/Config.scala | 10 +++--- .../main/scala/hkmc2/DebugOutputHandler.scala | 34 +++++++++++++++---- .../main/scala/hkmc2/codegen/Lowering.scala | 20 ++++++----- .../scala/hkmc2/semantics/Elaborator.scala | 2 +- .../main/scala/hkmc2/semantics/Resolver.scala | 3 -- .../test/mlscript/codegen/DebugDirective.mls | 22 ++++++++++-- .../test/mlscript/codegen/DebugScoping.mls | 31 ----------------- .../src/test/scala/hkmc2/MLsDiffMaker.scala | 26 +++++--------- 11 files changed, 87 insertions(+), 124 deletions(-) delete mode 100644 hkmc2/jvm/src/test/scala/hkmc2/DebugOutputHandlerTest.scala diff --git a/doc/reference.md b/doc/reference.md index 96b0c69aa1..71c0b764eb 100644 --- a/doc/reference.md +++ b/doc/reference.md @@ -2051,12 +2051,11 @@ The following aspects trace a phase of the compiler: | Aspect | Traces | |---|---| | `elaboration` | elaboration of syntax trees into terms | -| `resolution` | name resolution | | `lowering` | lowering of terms into the intermediate representation (IR) | | `optimizations` | the optimization passes run on the IR | With the new name resolution (`#lang(0.3.x)`), names are resolved during elaboration, -so enabling `resolution` traces elaboration as well. +so `elaboration` traces name resolution as well. Parser tracing is available through the diff-test command `:dp`. Source directives are read after parsing and cannot enable parser tracing. @@ -2066,7 +2065,8 @@ The following aspects display a program representation, without tracing: | Aspect | Displays | |---|---| | `showParsedTree` | the syntax tree produced by the parser | -| `showElaboratedTree` | the elaborated term tree | +| `showElaboratedTree` | the elaborated term, as a tree | +| `showElaboratedTerm` | the elaborated term, in readable form | | `showLoweredTree` | the IR produced by lowering, as a tree | | `showIR` | the IR produced by lowering, in readable form | | `showOptimizedIR` | the IR after optimizations, in readable form | diff --git a/hkmc2/jvm/src/test/scala/hkmc2/DebugOutputHandlerTest.scala b/hkmc2/jvm/src/test/scala/hkmc2/DebugOutputHandlerTest.scala deleted file mode 100644 index 7afd93e4b6..0000000000 --- a/hkmc2/jvm/src/test/scala/hkmc2/DebugOutputHandlerTest.scala +++ /dev/null @@ -1,34 +0,0 @@ -package hkmc2 - -import org.scalatest.funsuite.AnyFunSuite - -import io.PlatformPath.given - - -class DebugOutputHandlerTest extends AnyFunSuite: - // Diff tests cover the emitted traces. This measures total I/O, which a golden file cannot - // distinguish from repeatedly rewriting the same growing log. - test("file debug output writes a linear amount of data"): - val dir = os.temp.dir(prefix = "debug-output-") - val underlying = io.FileSystem.default - var writtenChars = 0L - val fs = new io.FileSystem: - def read(path: io.Path): String = underlying.read(path) - def exists(path: io.Path): Boolean = underlying.exists(path) - def getLastChangedTimestamp(path: io.Path): Long = underlying.getLastChangedTimestamp(path) - def write(path: io.Path, content: String): Unit = - writtenChars += content.length - underlying.write(path, content) - def append(path: io.Path, content: String): Unit = - writtenChars += content.length - underlying.append(path, content) - try - os.write(dir / "trace.log", "old session contents") - val output = DebugOutputHandler(fs, dir, _ => fail("Unexpected standard output")) - val lines = (1 to 2000).map(i => s"debug line $i") - lines.foreach(output.emit(Config.DebugOutput.File("trace.log"), _)) - val expected = lines.mkString("", "\n", "\n") - assert(os.read(dir / "trace.log") == expected) - assert(writtenChars == expected.length, - "Only new output should be written, without copying accumulated contents") - finally os.remove.all(dir) diff --git a/hkmc2/shared/src/main/scala/hkmc2/CompilerCtx.scala b/hkmc2/shared/src/main/scala/hkmc2/CompilerCtx.scala index 16a919756c..e11de1d048 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/CompilerCtx.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/CompilerCtx.scala @@ -88,6 +88,8 @@ class CompilerCtx( includeZero = true, )) ) + val backendTL = new TraceLogger: + override def doTrace: Bool = false val parse = given CompilerCtx = this @@ -101,10 +103,9 @@ class CompilerCtx( override protected def defaultDebugOutput: Config.DebugOutput = phaseConfig.debug.out override protected[hkmc2] def emitDbg(str: Str, out: Config.DebugOutput): Unit = outputHandler.emit(out, str) - val etl = traceLogger(phaseConfig.debug.traceElaboration(phaseConfig.language)) + val etl = traceLogger(phaseConfig.debug.elaboration) val ltl = traceLogger(phaseConfig.debug.lowering) val dtl = traceLogger(phaseConfig.debug.optimizations) - val rtl = traceLogger(phaseConfig.debug.resolution) given Elaborator.Ctx = prelude val artifactCtx = derive(parse.origin.fileName, dependencies) @@ -133,13 +134,15 @@ class CompilerCtx( state.initRuntimeSymbolsFromBlock(blk0) val artifactConfig = Config.extractConfigFromStats(blk0) - if artifactConfig.debug.showElaboratedTree then - outputHandler.emit(artifactConfig.debug.out, s"Elaborated tree\n${blk0.showAsTree}") - else DebugOutputHandler.showElaboratedDefinitions(blk0, artifactConfig): (defn, out) => - outputHandler.emit(out, s"Elaborated tree\n${defn.showAsTree}") + def display(title: Str, contents: Str, out: Config.DebugOutput): Unit = + outputHandler.emit(out, s"${title}\n${contents}") + DebugOutputHandler.showElaboratedTree(blk0, artifactConfig)(display) + DebugOutputHandler.showElaboratedTerm(blk0, artifactConfig, 100)(display)(using + Scope.empty(Scope.Cfg.default.copy(escapeChars = false, useSuperscripts = true, includeZero = true)), + summon[Raise]) if !artifactConfig.language.useNewResolution then artifactConfig.givenIn: given Elaborator.State = state - val resolver = Resolver(rtl) + val resolver = Resolver(backendTL) resolver.traverseBlock(blk0)(using Resolver.ICtx.empty) def findQuote(t: semantics.Statement): Bool = t match case Term.Quoted(_) | Term.Unquoted(_) => true @@ -186,8 +189,6 @@ class CompilerCtx( val blockPrinter = codegen.Printer() val printer = (p: codegen.Program) => blockPrinter.worksheet(p)(using irPrintingScp).mkString(100) - def display(title: Str, contents: Str, out: Config.DebugOutput): Unit = - outputHandler.emit(out, s"${title}\n${contents}") val pipeline = new codegen.CompilationPipeline: override def extraSymbolsToPreserveFrom(prog: codegen.Program): Set[codegen.ScopedSymbol] = collectCompilationUnitSymbols(prog) @@ -269,7 +270,7 @@ class CompilerCtx( stateDebug = CompilerCtx.debugTracingEnabled(elaborationConfig.debug) stateShowUids = elaborationConfig.debug.showUids val etl = new TraceLogger: - override def doTrace: Bool = elaborationConfig.debug.traceElaboration(elaborationConfig.language) + override def doTrace: Bool = elaborationConfig.debug.elaboration override protected def defaultDebugOutput: Config.DebugOutput = elaborationConfig.debug.out override protected[hkmc2] def emitDbg(str: Str, out: Config.DebugOutput): Unit = outputHandler.emit(out, str) @@ -294,7 +295,7 @@ object CompilerCtx: inline def get(using cctx: CompilerCtx) = cctx private def debugTracingEnabled(debug: Config.Debug): Bool = - debug.elaboration || debug.resolution || debug.lowering || debug.optimizations + debug.elaboration || debug.lowering || debug.optimizations /** Collect import provenance after elaboration has assembled all user and synthetic imports. diff --git a/hkmc2/shared/src/main/scala/hkmc2/Config.scala b/hkmc2/shared/src/main/scala/hkmc2/Config.scala index 14fe24a5cf..5ad67106bb 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/Config.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/Config.scala @@ -203,7 +203,7 @@ object Config: showParsedTree: Bool, elaboration: Bool, showElaboratedTree: Bool, - resolution: Bool, + showElaboratedTerm: Bool, lowering: Bool, showLoweredTree: Bool, optimizations: Bool, @@ -211,15 +211,13 @@ object Config: showOptimizedIR: Bool, showUids: Bool, out: DebugOutput, - ): - // New resolution is interleaved with elaboration and shares its trace logger. - def traceElaboration(language: Language): Bool = elaboration || (language.useNewResolution && resolution) + ) object Debug: val default = Debug( showParsedTree = false, elaboration = false, showElaboratedTree = false, - resolution = false, + showElaboratedTerm = false, lowering = false, showLoweredTree = false, optimizations = false, @@ -599,7 +597,7 @@ object ConfigParser: case "showParsedTree" => _.copy(showParsedTree = value) case "elaboration" => _.copy(elaboration = value) case "showElaboratedTree" => _.copy(showElaboratedTree = value) - case "resolution" => _.copy(resolution = value) + case "showElaboratedTerm" => _.copy(showElaboratedTerm = value) case "lowering" => _.copy(lowering = value) case "showLoweredTree" => _.copy(showLoweredTree = value) case "optimizations" => _.copy(optimizations = value) diff --git a/hkmc2/shared/src/main/scala/hkmc2/DebugOutputHandler.scala b/hkmc2/shared/src/main/scala/hkmc2/DebugOutputHandler.scala index 82fb065269..3fede87dc6 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/DebugOutputHandler.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/DebugOutputHandler.scala @@ -63,18 +63,40 @@ object DebugOutputHandler: case _ => () trees.foreach(visit) - /** Inspect the elaborated tree directly: new resolution runs during elaboration and does not - * traverse the legacy resolver, but annotated definitions still need their tree displayed. */ - def showElaboratedDefinitions(block: semantics.Term.Blk, config: Config) - (display: (semantics.Definition, Config.DebugOutput) => Unit): Unit = + def showElaboratedTree(block: semantics.Term.Blk, config: Config) + (display: (Str, Str, Config.DebugOutput) => Unit)(using DebugPrinter): Unit = + showElaborated(block, config, _.showElaboratedTree, "Elaborated tree", _.showAsTree, _.showAsTree)(display) + + /** Print elaborated terms in readable form, with the settings of the `:sr` diff-test command. + * `showFlowSymbols` also disambiguates symbol names, which a readable form needs; before flow + * analysis, it shows selections as unresolved (`ˀˀˀ`). Erased types are not computed yet. */ + def showElaboratedTerm(block: semantics.Term.Blk, config: Config, width: Int) + (display: (Str, Str, Config.DebugOutput) => Unit)(using utils.Scope, Raise): Unit = + given semantics.ShowCfg = semantics.ShowCfg( + showErasedTypes = false, + showExpansionMappings = true, + showFlowSymbols = true, + debug = false, + ) + showElaborated(block, config, _.showElaboratedTerm, "Elaborated term", + _.showTopLevel.mkString(width), _.show.mkString(width))(display) + + /** Display the whole block if the file's settings select it, and otherwise each annotated + * definition that selects it, including definitions nested in other definitions. + * Inspect the elaborated tree directly: new resolution runs during elaboration and does not + * traverse the legacy resolver, but annotated definitions still need to be displayed. */ + private def showElaborated(block: semantics.Term.Blk, config: Config, isEnabled: Config.Debug => Bool, title: Str, + showBlock: semantics.Term.Blk => Str, showDefinition: semantics.Definition => Str) + (display: (Str, Str, Config.DebugOutput) => Unit): Unit = def visit(statement: semantics.Statement): Unit = statement match case defn: semantics.Definition => definitionConfig(defn.annotations, config).foreach: localConfig => - if localConfig.debug.showElaboratedTree then display(defn, localConfig.debug.out) + if isEnabled(localConfig.debug) then display(title, showDefinition(defn), localConfig.debug.out) case _ => () statement.subStatements.foreach(visit) - visit(block) + if isEnabled(config.debug) then display(title, showBlock(block), config.debug.out) + else visit(block) /** Use the same selection and formatting in the compiler and diff-test pipelines. */ def showIR(program: codegen.Program, optimized: Bool, printer: codegen.Printer, width: Int) diff --git a/hkmc2/shared/src/main/scala/hkmc2/codegen/Lowering.scala b/hkmc2/shared/src/main/scala/hkmc2/codegen/Lowering.scala index 023f41a1ae..a9c95b4af8 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/codegen/Lowering.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/codegen/Lowering.scala @@ -332,15 +332,17 @@ class Lowering()(using Config, TL, Raise, State, Ctx, SymbolPrinter)(using Erasu assert(td.params.isEmpty) val cfgOverride = definitionConfigOverride(td.extraAnnotations) localVal match - case S(sym) => - debugLoweringThen[Result](td.extraAnnotations)(term_nonTail(bod)(_)): - r => defineSymbol(sym, r, blockImpl(stats, res)) - case N => - debugLoweringThen[Path](td.extraAnnotations)(k => subTerm_nonTail(bod)(k)(using LoweringCtx.nestFunc(N))): r => - Define( - ValDefn(td.tsym, td.sym, castTo(r, td.tsym.erasedType, bod.toLoc))(cfgOverride, td.annotations), - blockImpl(stats, res), - ) + case S(sym) => + debugLoweringThen[Result](td.extraAnnotations)(term_nonTail(bod)(_)): r => + defineSymbol(sym, r, blockImpl(stats, res)) + case N => + debugLoweringThen[Path](td.extraAnnotations)(k => subTerm_nonTail(bod)(k)(using LoweringCtx.nestFunc(N))): r => + // Assign(td.sym, r, + // term(st.Blk(stats, res))(k))) + Define( + ValDefn(td.tsym, td.sym, castTo(r, td.tsym.erasedType, bod.toLoc))(cfgOverride, td.annotations), + blockImpl(stats, res), + ) case syntax.Fun => val cfgOverride = definitionConfigOverride(td.extraAnnotations) val (paramLists, bodyBlock) = debugLowering(td.extraAnnotations): diff --git a/hkmc2/shared/src/main/scala/hkmc2/semantics/Elaborator.scala b/hkmc2/shared/src/main/scala/hkmc2/semantics/Elaborator.scala index 7c533149b9..0d7f506c29 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/semantics/Elaborator.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/semantics/Elaborator.scala @@ -813,7 +813,7 @@ extends Importer: val newResolution: Bool = lang.useNewResolution private def debugElaboration[T](annotations: Ls[Annot])(thunk: => T): T = - DebugOutputHandler.traceDefinition(annotations, config, _.traceElaboration(config.language))(thunk)(using state, tl) + DebugOutputHandler.traceDefinition(annotations, config, _.elaboration)(thunk)(using state, tl) lazy val illegalMemberNameTail = msg"Member names must start with a letter or underscore, followed by letters, digits, or underscores." -> N diff --git a/hkmc2/shared/src/main/scala/hkmc2/semantics/Resolver.scala b/hkmc2/shared/src/main/scala/hkmc2/semantics/Resolver.scala index 51e3ac66ff..4fcf7ffaba 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/semantics/Resolver.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/semantics/Resolver.scala @@ -357,9 +357,6 @@ class Resolver(tl: TraceLogger) end traverse def traverseDefn(defn: Definition)(using ICtx): ICtx = - DebugOutputHandler.traceDefinition(defn.annotations, cfg, _.resolution)(traverseDefnImpl(defn))(using state, tl) - - private def traverseDefnImpl(defn: Definition)(using ICtx): ICtx = trace(s"Resolving definition: $defn"): def traverseTermDef(tdf: TermDefinition) = val TermDefinition(_k, _sym, _tsym, diff --git a/hkmc2/shared/src/test/mlscript/codegen/DebugDirective.mls b/hkmc2/shared/src/test/mlscript/codegen/DebugDirective.mls index 9d628d5909..1eb3a373cd 100644 --- a/hkmc2/shared/src/test/mlscript/codegen/DebugDirective.mls +++ b/hkmc2/shared/src/test/mlscript/codegen/DebugDirective.mls @@ -37,7 +37,7 @@ fun persistently_debugged(x) = x module Main with // The IR of `foo1` should be shown - @dbg('showParsedTree, 'showElaboratedTree, 'showLoweredTree, 'showOptimizedIR) + @dbg('showParsedTree, 'showElaboratedTree, 'showElaboratedTerm, 'showLoweredTree, 'showOptimizedIR) fun foo1 = 2 + 2 // The IR of `foo2` should NOT be shown fun foo2 = 2 + 2 @@ -48,6 +48,7 @@ module Main with //│ rhs = Tup of Ls of //│ Ident of "'showParsedTree" //│ Ident of "'showElaboratedTree" +//│ Ident of "'showElaboratedTerm" //│ Ident of "'showLoweredTree" //│ Ident of "'showOptimizedIR" //│ target = TermDef: @@ -73,6 +74,8 @@ module Main with //│ modulefulness = Modulefulness of N //│ annotations = Ls of //│ Debug of ... +//│ —————————————| Elaborated term |———————————————————————————————————————————————————————————————————— +//│ @dbg(...) fun foo1⁰::foo1¹ ‹flow:foo1⁰› = +(2, 2)‹app⁰› //│ —————————————| Lowered IR Tree |———————————————————————————————————————————————————————————————————— //│ FunDefn: //│ owner = S of module:Main⁰ @@ -87,8 +90,18 @@ Main.foo1 //│ = 4 +// The readable elaborated term distinguishes symbols that share a name. +@dbg('showElaboratedTerm) +fun shadowing(x) = + let x = x + 1 + x +//│ —————————————| Elaborated term |———————————————————————————————————————————————————————————————————— +//│ @dbg(...) fun shadowing⁰::shadowing¹(x⁰) ‹flow:shadowing⁰› = { let x¹, x¹ = +(x⁰, 1)‹app¹›, x¹ } +//│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— + + // Whole-program displays and optimization tracing use the same settings. -#dbg('showParsedTree, 'showElaboratedTree, 'showLoweredTree, 'showIR, 'showOptimizedIR, 'optimizations) +#dbg('showParsedTree, 'showElaboratedTree, 'showElaboratedTerm, 'showLoweredTree, 'showIR, 'showOptimizedIR, 'optimizations) 42 //│ ———————————————| Parsed tree |—————————————————————————————————————————————————————————————————————— //│ App: @@ -98,6 +111,7 @@ Main.foo1 //│ rhs = Tup of Ls of //│ Ident of "'showParsedTree" //│ Ident of "'showElaboratedTree" +//│ Ident of "'showElaboratedTerm" //│ Ident of "'showLoweredTree" //│ Ident of "'showIR" //│ Ident of "'showOptimizedIR" @@ -108,6 +122,8 @@ Main.foo1 //│ stats = Ls of //│ SetConfig of ... //│ res = Lit of IntLit of 42 +//│ —————————————| Elaborated term |———————————————————————————————————————————————————————————————————— +//│ #config(...), 42 //│ —————————————| Lowered IR Tree |———————————————————————————————————————————————————————————————————— //│ Program: //│ main = Return of Lit of IntLit of 42 @@ -122,4 +138,4 @@ Main.foo1 //│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— //│ = 42 -#dbg(showParsedTree: false, showElaboratedTree: false, showLoweredTree: false, showIR: false, showOptimizedIR: false, optimizations: false) +#dbg(showParsedTree: false, showElaboratedTree: false, showElaboratedTerm: false, showLoweredTree: false, showIR: false, showOptimizedIR: false, optimizations: false) diff --git a/hkmc2/shared/src/test/mlscript/codegen/DebugScoping.mls b/hkmc2/shared/src/test/mlscript/codegen/DebugScoping.mls index ecb7e99bd3..59b222520e 100644 --- a/hkmc2/shared/src/test/mlscript/codegen/DebugScoping.mls +++ b/hkmc2/shared/src/test/mlscript/codegen/DebugScoping.mls @@ -55,27 +55,6 @@ DebugMethods.selected(7) //│ | ~> x¹ //│ ~> { fun member:command_elaboration(x¹) = x¹; } -:noCodeGen -:dr -@dbg('lowering) fun command_resolution(x) = x -//│ Traversing block: Blk(List(TermDefinition(Fun,member:command_resolution,term:command_resolution,List(ParamList(‹›,List(Param(‹›,x,None,Modulefulness(None))),None)),None,None,Some(Ref(x)),‹›,Modulefulness(None),List(Debug(...)),None)),Lit(UnitLit(false))) -//│ | Traversing statement: TermDefinition(Fun,member:command_resolution,term:command_resolution,List(ParamList(‹›,List(Param(‹›,x,None,Modulefulness(None))),None)),None,None,Some(Ref(x)),‹›,Modulefulness(None),List(Debug(...)),None) -//│ | Resolving definition: TermDefinition(Fun,member:command_resolution,term:command_resolution,List(ParamList(‹›,List(Param(‹›,x,None,Modulefulness(None))),None)),None,None,Some(Ref(x)),‹›,Modulefulness(None),List(Debug(...)),None) -//│ | | Resolving function definition TermDefinition(Fun,member:command_resolution,term:command_resolution,List(ParamList(‹›,List(Param(‹›,x,None,Modulefulness(None))),None)),None,None,Some(Ref(x)),‹›,Modulefulness(None),List(Debug(...)),None) -//│ | | Resolving parameter x² -//│ | | Traversing term: Ref(x) (expect = NonModule) -//│ | | | Resolving resolvable term: Ref(x), (prefer = NonModule, inAppPrefix = false, inCtxPrefix = false, inTyPrefix = false) -//│ | | | | Resolving symbol for term: Ref(x) (prefer = NonModule) -//│ | | | | -> (sym = Some(x), typ = None) -//│ | | | | Resolving the type for term: Ref(x) (prefer = NonModule, sym = Some(x)) -//│ | | | | | Declaration: Some(Param(‹›,x,None,Modulefulness(None))) -//│ | | | | -> typ = None -//│ | | | | Resolving resolvable term Ref(x) with sym = Some(x), typ = None -//│ | | | ~> Ref(x) (sym = Some(x), typ = None) -//│ | | | Checking Ref(x): expect NonModule, evalsToModule = false, evalsToStaticClass = false -//│ | Traversing term: Lit(UnitLit(false)) (expect = Any) -//│ | | Checking Lit(UnitLit(false)): expect Any, evalsToModule = false, evalsToStaticClass = false - // A class's lowering trace covers the evaluation of its superclass, // but not the statements that follow the class definition. @@ -103,16 +82,6 @@ debug_local_values() //│ = 3 -// New resolution traces through elaboration, without visiting the legacy resolver. -@dbg('resolution, showUids: false) fun selected_new() = 3 -fun unrelated_new() = 4 -//│ Processing term definition App(Ident(selected_new),Tup(List())) -//│ Elab term IntLit(3) -//│ | Elab subterm IntLit(3) -//│ | ~> 3 -//│ ~> 3 - - // Classes and their companions share lowering, while each elaboration annotation is scoped. @dbg('elaboration, 'lowering, showUids: false) class DebugCompanion(value: Int) with diff --git a/hkmc2DiffTests/src/test/scala/hkmc2/MLsDiffMaker.scala b/hkmc2DiffTests/src/test/scala/hkmc2/MLsDiffMaker.scala index b9b6a7cd2c..83bd69cb91 100644 --- a/hkmc2DiffTests/src/test/scala/hkmc2/MLsDiffMaker.scala +++ b/hkmc2DiffTests/src/test/scala/hkmc2/MLsDiffMaker.scala @@ -248,7 +248,6 @@ abstract class MLsDiffMaker extends DiffMaker: override protected def doDbg: Bool = doForceDbg || activeDebug.elaboration - || activeDebug.resolution || activeDebug.lowering || activeDebug.optimizations override protected def doForceDbg: Bool = @@ -296,17 +295,15 @@ abstract class MLsDiffMaker extends DiffMaker: override protected[hkmc2] def emitDbg(str: Str, out: Config.DebugOutput): Unit = debugOutputHandler.emit(out, str) override def trace[T](pre: => Str, post: T => Str = noPostTrace)(thunk: => T): T = - // Avoid accumulating indentation outside selectively-enabled elaboration scopes. - if isTracing then super.trace(pre, post)(thunk) else thunk + // * This override is for avoiding to increase the indentation when tracing if isTracing is false, + // * so that selectively-enabled tracing doesn't get strange indentation. + // * Perhaps this should be the default behavior of TraceLogger. + if isTracing then super.trace(pre, post)(thunk) + else thunk val rtl = new TraceLogger: - override protected def doForceTrace = dbgResolving.isSet - override def doTrace = doForceTrace || activeDebug.resolution - override protected def defaultDebugOutput: Config.DebugOutput = - if activeDebug.resolution then activeDebug.out else Config.DebugOutput.StdIO + override def doTrace = dbgResolving.isSet override def emitDbg(str: String): Unit = output(str) - override protected[hkmc2] def emitDbg(str: Str, out: Config.DebugOutput): Unit = - debugOutputHandler.emit(out, str) val ftl = new TraceLogger: override def doTrace = dbgFlow.isSet @@ -507,10 +504,7 @@ abstract class MLsDiffMaker extends DiffMaker: val elab = given Config = elaborationConfig Elaborator(etl, file.up, prelude) - val (e, newCtx) = elaborationConfig.givenIn: - if elaborationConfig.language.useNewResolution && elaborationConfig.debug.resolution then - etl.scopedDebug(true, elaborationConfig.debug)(elab.topLevel(blk)) - else elab.topLevel(blk) + val (e, newCtx) = elaborationConfig.givenIn(elab.topLevel(blk)) curCtx = newCtx extractConfig(e.stats) @@ -521,10 +515,8 @@ abstract class MLsDiffMaker extends DiffMaker: if showElaboratedTree.isSet then outputSeparator(s"Elaborated tree") output(e.showAsTree) - else if elaborationConfig.debug.showElaboratedTree then - outputDebugSection("Elaborated tree", e.showAsTree, elaborationConfig.debug.out) - else DebugOutputHandler.showElaboratedDefinitions(e, elaborationConfig): (defn, out) => - outputDebugSection("Elaborated tree", defn.showAsTree, out) + else DebugOutputHandler.showElaboratedTree(e, elaborationConfig)(outputDebugSection) + DebugOutputHandler.showElaboratedTerm(e, elaborationConfig, output.ColWidth)(outputDebugSection)(using flowScp) if showResl.isSet then import semantics.ShowCfg From 70f241e53f035197cd5d1837ff49593a3a0bce19 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 18 Oct 2026 14:25:15 +0800 Subject: [PATCH 19/32] Parse any keyword followed by a colon as a key Record fields and named arguments such as `#dbg(out: ...)` may need names that are keywords. Instead of a dedicated parse rule for `out:`, parse any alphanumeric keyword followed by a colon as an identifier, in statement, rule, and simple-expression positions. No keyword-introduced syntax continues with a colon, and expressions are ascribed with `as`, so this is unambiguous; `true: e` is a key too. Selections such as `r.class` already worked, since the lexer makes them selection tokens. Co-Authored-By: Claude Opus 5.5 --- .../main/scala/hkmc2/syntax/ParseRule.scala | 16 +------ .../src/main/scala/hkmc2/syntax/Parser.scala | 23 +++++++--- .../test/mlscript/invalml/InvalMLSyntax.mls | 2 +- .../src/test/mlscript/parser/KeywordKeys.mls | 43 +++++++++++++++++++ 4 files changed, 63 insertions(+), 21 deletions(-) create mode 100644 hkmc2/shared/src/test/mlscript/parser/KeywordKeys.mls diff --git a/hkmc2/shared/src/main/scala/hkmc2/syntax/ParseRule.scala b/hkmc2/shared/src/main/scala/hkmc2/syntax/ParseRule.scala index d4ea0248a5..f969fe4b67 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/syntax/ParseRule.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/syntax/ParseRule.scala @@ -419,21 +419,7 @@ class ParseRules(using State): modified(`data`), modified(`public`), modified(`private`), - Kw(`out`)( - ParseRule("'out' keyword")( - Kw(`:`)( - ParseRule("named 'out' argument colon")( - Expr(ParseRule("named 'out' argument value")(end(()))): - case (value, ()) => value - ) - ) { case (colon, value) => S(colon) -> value }, - Expr(ParseRule("'out' modifier body")(end(()))): - case (body, ()) => N -> body - ) - ): - case (outKw, (S(colon), value)) => - InfixApp(Ident("out").withLocOf(outKw), colon, value).withLocOf(outKw) - case (outKw, (N, body)) => Tree.Modified(outKw, body), + modified(`out`), modified(`staged`), singleKw(`true`)(BoolLit(true)), singleKw(`false`)(BoolLit(false)), diff --git a/hkmc2/shared/src/main/scala/hkmc2/syntax/Parser.scala b/hkmc2/shared/src/main/scala/hkmc2/syntax/Parser.scala index ebe4ead03b..d2624c5eb8 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/syntax/Parser.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/syntax/Parser.scala @@ -116,6 +116,17 @@ object Parser: def unapply(ts: Ls[TokLoc]): S[Ls[TokLoc]] = S(get(ts)) + /** Whether `id`, followed by the tokens `rest`, is a keyword used as a key, as in `class: e` or `true: e`. + * An alphanumeric keyword followed by a colon is parsed as an identifier, so that record fields and + * named arguments can have any name. This does not make anything ambiguous: no keyword-introduced + * syntax continues with a colon, and expressions are ascribed with `as` rather than with a colon. */ + private def isKeywordKey(id: IDENT, rest: Ls[TokLoc]): Bool = + def colonFollows(ts: Ls[TokLoc]): Bool = ts match + case (SPACE, _) :: ts => colonFollows(ts) + case (IDENT(":", _), _) :: _ => true + case _ => false + !id.symbolic && Keyword.all.contains(id.name) && colonFollows(rest) + extension (loc: Loc) def showStart: String = loc.origin.fph.getLineColAt(loc.spanStart) match @@ -361,7 +372,7 @@ abstract class Parser( errExpr :: Nil case _ => blockOf(rule, a :: annotations, allowNewlines = allowNewlines) - case (tok @ (id: IDENT), loc) :: _ if id.name =/= ":" => + case (tok @ (id: IDENT), loc) :: rest if id.name =/= ":" && !isKeywordKey(id, rest) => Keyword.all.get(id.name) match case S(kw) => consume @@ -460,12 +471,11 @@ abstract class Parser( // case N => // err((msg"Expected ${rule.whatComesAfter} ${rule.mkAfterStr}; found end of phrase instead" -> S(loc.left) :: Nil) // N + case (tok @ (id: IDENT), loc) :: rest if isKeywordKey(id, rest) => + tryParseExp(prec, tok, loc, rule, allowNewlines = allowNewlines) case (tok @ (id: IDENT), loc) :: _ => Keyword.all.get(id.name) match - case S(Keyword.`:`) if rule.getKwAlt(Keyword.`:`, S(loc)).isEmpty => - // encountering `:` should lead to parsing an expr (likely a pun) - tryParseExp(prec, tok, loc, rule, allowNewlines = allowNewlines) - case N => + case S(Keyword.`:`) | N => // encountering `:` should lead to parsing an expr (likely a pun) tryParseExp(prec, tok, loc, rule, allowNewlines = allowNewlines) case S(kw) => @@ -649,6 +659,9 @@ abstract class Parser( consume val id = Tree.Ident(name).withLoc(S(loc)) exprCont(id, prec, allowNewlines = true) + case (tok @ IDENT(nme, _), loc) :: rest if isKeywordKey(tok, rest) => + consume + exprCont(Tree.Ident(nme).withLoc(S(loc)), prec, allowNewlines = allowNewlines) case (IDENT(nme, sym), loc) :: _ => Keyword.all.get(nme) match case S(kw) => // * Expressions starting with keywords should be handled in parseRule diff --git a/hkmc2/shared/src/test/mlscript/invalml/InvalMLSyntax.mls b/hkmc2/shared/src/test/mlscript/invalml/InvalMLSyntax.mls index ab47957bb5..756cbeae94 100644 --- a/hkmc2/shared/src/test/mlscript/invalml/InvalMLSyntax.mls +++ b/hkmc2/shared/src/test/mlscript/invalml/InvalMLSyntax.mls @@ -67,7 +67,7 @@ true and true false: Bool //│ Parsed: -//│ InfixApp(BoolLit(false),Keywrd(keyword ':'),Ident(Bool)) +//│ InfixApp(Ident(false),Keywrd(keyword ':'),Ident(Bool)) f of false //│ Parsed: diff --git a/hkmc2/shared/src/test/mlscript/parser/KeywordKeys.mls b/hkmc2/shared/src/test/mlscript/parser/KeywordKeys.mls new file mode 100644 index 0000000000..647106f66d --- /dev/null +++ b/hkmc2/shared/src/test/mlscript/parser/KeywordKeys.mls @@ -0,0 +1,43 @@ +:js + + +// A keyword followed by a colon is a key, like any other identifier. +:pt +(class: 1, true: 2, out : 3) +//│ ———————————————| Parsed tree |—————————————————————————————————————————————————————————————————————— +//│ Bra: +//│ k = Round +//│ inner = Block of Ls of +//│ InfixApp: +//│ lhs = Ident of "class" +//│ kw = Keywrd of keyword ':' +//│ rhs = IntLit of 1 +//│ InfixApp: +//│ lhs = Ident of "true" +//│ kw = Keywrd of keyword ':' +//│ rhs = IntLit of 2 +//│ InfixApp: +//│ lhs = Ident of "out" +//│ kw = Keywrd of keyword ':' +//│ rhs = IntLit of 3 +//│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— +//│ = {class: 1, true: 2, out: 3} + +let r = (class: 1, true: 2, if: 3) +r.class + r.true + r.if +//│ = 6 +//│ r = {class: 1, true: 2, if: 3} + +fun f(x) = x +f(class: 1) +//│ = {class: 1} + +{fun: 4} +//│ = {fun: 4} + +// Keywords keep their meaning in other positions. +:expect true +true +//│ = true + +class C(val x: Int) From 858fe7bab4b5d8f4b58ac2584554155ef7aef900 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 18 Oct 2026 14:25:15 +0800 Subject: [PATCH 20/32] Warn about @dbg annotations on statements other than definitions `@dbg` on a `let` binding was silently accepted and had no effect. Co-Authored-By: Claude Opus 5.5 --- .../src/main/scala/hkmc2/codegen/Lowering.scala | 4 +++- .../test/mlscript/codegen/BadDebugDirective.mls | 17 +++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/hkmc2/shared/src/main/scala/hkmc2/codegen/Lowering.scala b/hkmc2/shared/src/main/scala/hkmc2/codegen/Lowering.scala index a9c95b4af8..6002be725c 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/codegen/Lowering.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/codegen/Lowering.scala @@ -1899,7 +1899,9 @@ class Lowering()(using Config, TL, Raise, State, Ctx, SymbolPrinter)(using Erasu case TermDefinition(k = syntax.Fun) => () case _ => warn(a) case _: Annot.Config => () // Config annotations are handled during FunDefn creation - case _: Annot.Debug => () // Debug annotations are handled during definition lowering + case a: Annot.Debug => target match + case _: Definition => () // Debug annotations are handled during definition elaboration and lowering + case _ => warn(a) case annot => warn(annot) def reportAnnotations(receiver: Term, annotations: Ls[Annot]): Unit = diff --git a/hkmc2/shared/src/test/mlscript/codegen/BadDebugDirective.mls b/hkmc2/shared/src/test/mlscript/codegen/BadDebugDirective.mls index 2d14220040..71b7549a13 100644 --- a/hkmc2/shared/src/test/mlscript/codegen/BadDebugDirective.mls +++ b/hkmc2/shared/src/test/mlscript/codegen/BadDebugDirective.mls @@ -40,3 +40,20 @@ mixed_local() //│ ╙── ^^^^^^^^^^^^^^ //│ Lowering.term 57 //│ = 57 + + +// Annotations on statements other than definitions have no effect. +:w +@dbg('lowering) let not_a_definition = 1 +//│ ╔══[WARNING] This annotation has no effect. +//│ ║ l.47: @dbg('lowering) let not_a_definition = 1 +//│ ╙── ^^^^^^^^^^^^^^^ +//│ not_a_definition = 1 + +:w +fun local_let() = + @dbg('lowering) let not_a_definition = 2 + not_a_definition +//│ ╔══[WARNING] This annotation has no effect. +//│ ║ l.55: @dbg('lowering) let not_a_definition = 2 +//│ ╙── ^^^^^^^^^^^^^^^ From 27d781ef844f0b5baa87eaea780dd8f7b3262391 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 18 Oct 2026 14:25:15 +0800 Subject: [PATCH 21/32] Resolve debug output files against the declaring file's directory Relative `File(...)` destinations were resolved against the compiler's root base directory, so the same directive named different files depending on how a unit was compiled; in diff tests, the test file and the units it imports used different bases. Resolve them like imports, against the directory of the file containing the directive, when the directive is parsed: `DebugOutput.File` now holds the resolved path, and `DebugOutputHandler` no longer needs a base directory. Co-Authored-By: Claude Opus 5.5 --- doc/reference.md | 4 +-- .../src/main/scala/hkmc2/CompilerCtx.scala | 6 ++-- .../shared/src/main/scala/hkmc2/Config.scala | 34 +++++++++++-------- .../main/scala/hkmc2/DebugOutputHandler.scala | 9 +++-- .../src/main/scala/hkmc2/MLsCompiler.scala | 4 +-- .../scala/hkmc2/semantics/Elaborator.scala | 4 +-- .../mlscript/codegen/DebugDestinations.mls | 5 +-- .../src/test/scala/hkmc2/MLsDiffMaker.scala | 8 ++--- 8 files changed, 39 insertions(+), 35 deletions(-) diff --git a/doc/reference.md b/doc/reference.md index 71c0b764eb..5352929518 100644 --- a/doc/reference.md +++ b/doc/reference.md @@ -2096,8 +2096,8 @@ The `out: File(...)` argument writes it to a file instead: #dbg('lowering, out: File("lowering.log")) ``` -A relative `File` path is resolved against the compiler's base directory, -and an absolute path is used as is, as for imports. +As for imports, a relative `File` path is resolved against the directory of the file +in which the `#dbg` directive or `@dbg` annotation is written, and an absolute path is used as is. The first time the compiler writes to a file during a compilation session, it replaces the file's previous contents; it then appends all later output, including the output of other files compiled in the same session, such as imported files. diff --git a/hkmc2/shared/src/main/scala/hkmc2/CompilerCtx.scala b/hkmc2/shared/src/main/scala/hkmc2/CompilerCtx.scala index e11de1d048..2ae803610f 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/CompilerCtx.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/CompilerCtx.scala @@ -33,7 +33,7 @@ class CompilerCtx( ): private val outputHandler: DebugOutputHandler = importing match case S((_, parent)) => parent.outputHandler - case N => DebugOutputHandler(fs, rootConfig.baseDir, println) + case N => DebugOutputHandler(fs, println) def allFilesBeingImported: Ls[io.Path] = importing match @@ -95,7 +95,7 @@ class CompilerCtx( given CompilerCtx = this ParserSetup(file, outputHandler) val parsed = parse.resultBlk - val phaseConfig = Config.elaborationConfig(parsed) + val phaseConfig = Config.elaborationConfig(parsed, file.up) stateDebug = CompilerCtx.debugTracingEnabled(phaseConfig.debug) stateShowUids = phaseConfig.debug.showUids def traceLogger(enabled: Bool) = new TraceLogger: @@ -266,7 +266,7 @@ class CompilerCtx( given CompilerCtx = this given DebugPrinter = new DebugPrinter val parse = ParserSetup(file, outputHandler) - val elaborationConfig = Config.elaborationConfig(parse.resultBlk) + val elaborationConfig = Config.elaborationConfig(parse.resultBlk, file.up) stateDebug = CompilerCtx.debugTracingEnabled(elaborationConfig.debug) stateShowUids = elaborationConfig.debug.showUids val etl = new TraceLogger: diff --git a/hkmc2/shared/src/main/scala/hkmc2/Config.scala b/hkmc2/shared/src/main/scala/hkmc2/Config.scala index 5ad67106bb..f05867d244 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/Config.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/Config.scala @@ -197,7 +197,9 @@ object Config: enum DebugOutput: case StdIO - case File(path: Str) + /** A relative path written in a directive is resolved, when the directive is parsed, + * against the directory of the file in which it is written, as for imports. */ + case File(path: io.Path) case class Debug( showParsedTree: Bool, @@ -318,18 +320,19 @@ object Config: /** File directives apply to the whole compilation unit, including elaboration. * Read them before constructing the elaborator so its resolver agrees with lowering. * Elaboration still records SetConfig statements and reports directive errors once. */ - def elaborationConfig(prgm: syntax.Tree.Block)(using Config): Config = - elaborationConfig(prgm.desugStmts) + def elaborationConfig(prgm: syntax.Tree.Block, sourceDir: io.Path)(using Config): Config = + elaborationConfig(prgm.desugStmts, sourceDir) - /** Read directives from parsed trees before elaboration or tree display. */ - def elaborationConfig(trees: Ls[syntax.Tree])(using Config): Config = + /** Read directives from parsed trees before elaboration or tree display. + * `sourceDir` is the directory of the file containing the trees. */ + def elaborationConfig(trees: Ls[syntax.Tree], sourceDir: io.Path)(using Config): Config = import syntax.Tree.* given Raise = _ => () def modifier(tree: syntax.Tree): Config => Config = tree match case App(Directive(prefix, _), args) => modifier(Directive(prefix, args)) case Directive(Ident("config"), Tup(args)) => ConfigParser.parseOverrides(args) case Directive(Ident("lang"), Tup(args)) => ConfigParser.parseLanguageDirective(args) - case Directive(Ident("dbg"), Tup(args)) => ConfigParser.parseDebugDirective(args, definitionLocal = false) + case Directive(Ident("dbg"), Tup(args)) => ConfigParser.parseDebugDirective(args, definitionLocal = false, sourceDir) case _ => identity trees.foldLeft(config)((cfg, tree) => modifier(tree)(cfg)) @@ -579,11 +582,11 @@ object ConfigParser: cfg => cfg.copy(language = modify(cfg.language)) case N => identity - private def parseDebugOutput(tree: Tree)(using Raise): Opt[Config.DebugOutput] = tree match + private def parseDebugOutput(tree: Tree, sourceDir: io.Path)(using Raise): Opt[Config.DebugOutput] = tree match case Ident("StdIO") | Call("StdIO", Nil) => S(Config.DebugOutput.StdIO) case Call("File", StrLit(path) :: Nil) => - S(Config.DebugOutput.File(path)) + S(Config.DebugOutput.File(io.Path.resolve(sourceDir, path))) case _ => expect("StdIO or File(\"path\")")(tree) N @@ -610,13 +613,13 @@ object ConfigParser: source = Diagnostic.Source.Compilation)) identity - private def parseDebugArg(arg: Tree, definitionLocal: Bool)(using Raise): Config.Debug => Config.Debug = arg match + private def parseDebugArg(arg: Tree, definitionLocal: Bool, sourceDir: io.Path)(using Raise): Config.Debug => Config.Debug = arg match case Quoted(Ident(name)) => updateDebugFlag(name, true, arg, definitionLocal) case Ident(name) if name.startsWith("'") => updateDebugFlag(name.drop(1), true, arg, definitionLocal) case NamedArg("out", value) => - parseDebugOutput(value) match + parseDebugOutput(value, sourceDir) match case S(out) => _.copy(out = out) case N => identity case NamedArg(name, value) => @@ -627,20 +630,21 @@ object ConfigParser: unsupported("debugging", arg) identity - /** Parse `#dbg(...)` and `@dbg(...)` arguments into a cumulative debug configuration. */ - def parseDebugDirective(args: Ls[Tree], definitionLocal: Bool)(using Raise): Config => Config = + /** Parse `#dbg(...)` and `@dbg(...)` arguments into a cumulative debug configuration. + * `sourceDir` is the directory of the file containing them. */ + def parseDebugDirective(args: Ls[Tree], definitionLocal: Bool, sourceDir: io.Path)(using Raise): Config => Config = val modifyDebug = args.foldLeft(identity[Config.Debug]): (acc, arg) => - val modify = parseDebugArg(arg, definitionLocal) + val modify = parseDebugArg(arg, definitionLocal, sourceDir) debug => modify(acc(debug)) new (Config => Config): def apply(cfg: Config): Config = cfg.copy(debug = modifyDebug(cfg.debug)) override def toString: Str = "..." /** Select parsed-tree displays without duplicating diagnostics reported by elaboration. */ - def discoverDebugFromAnnotations(annotations: Ls[Tree], base: Config): Opt[Config] = + def discoverDebugFromAnnotations(annotations: Ls[Tree], base: Config, sourceDir: io.Path): Opt[Config] = given Raise = _ => () val modifiers = annotations.collect: - case App(Ident("dbg"), Tup(args)) => parseDebugDirective(args, definitionLocal = true) + case App(Ident("dbg"), Tup(args)) => parseDebugDirective(args, definitionLocal = true, sourceDir) if modifiers.isEmpty then N else S(modifiers.foldLeft(base)((cfg, modify) => modify(cfg))) diff --git a/hkmc2/shared/src/main/scala/hkmc2/DebugOutputHandler.scala b/hkmc2/shared/src/main/scala/hkmc2/DebugOutputHandler.scala index 3fede87dc6..532bfc9ee3 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/DebugOutputHandler.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/DebugOutputHandler.scala @@ -10,13 +10,12 @@ import hkmc2.utils.*, shorthands.* * A session shares one handler across compilation units: each destination is cleared on first * use, then appended to without retaining or rewriting previous output. Concurrent debug writes * are best effort; this handler does not synchronize them. */ -final class DebugOutputHandler(fs: io.FileSystem, baseDir: io.Path, stdIO: Str => Unit): +final class DebugOutputHandler(fs: io.FileSystem, stdIO: Str => Unit): private val writtenFiles = mutable.Set.empty[io.Path] def emit(out: Config.DebugOutput, line: Str): Unit = out match case Config.DebugOutput.StdIO => stdIO(line) - case Config.DebugOutput.File(path) => - val file = io.Path.resolve(baseDir, path) + case Config.DebugOutput.File(file) => if writtenFiles(file) then fs.append(file, line + '\n') else fs.write(file, line + '\n') @@ -51,11 +50,11 @@ object DebugOutputHandler: tl.inOuterDebugScope(k(r)) case N => process(k) - def showParsedDefinitions(trees: Ls[syntax.Tree], config: Config) + def showParsedDefinitions(trees: Ls[syntax.Tree], config: Config, sourceDir: io.Path) (display: (syntax.Tree, Config.DebugOutput) => Unit): Unit = def visit(tree: syntax.Tree): Unit = tree match case syntax.PossiblyAnnotated(annotations, target) => - ConfigParser.discoverDebugFromAnnotations(annotations, config).foreach: localConfig => + ConfigParser.discoverDebugFromAnnotations(annotations, config, sourceDir).foreach: localConfig => if localConfig.debug.showParsedTree then display(tree, localConfig.debug.out) target.children.foreach: case child: syntax.Tree => visit(child) diff --git a/hkmc2/shared/src/main/scala/hkmc2/MLsCompiler.scala b/hkmc2/shared/src/main/scala/hkmc2/MLsCompiler.scala index 345034f81c..1112c30a03 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/MLsCompiler.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/MLsCompiler.scala @@ -22,14 +22,14 @@ class ParserSetup(file: io.Path, outputHandler: DebugOutputHandler) def doPrintDbg(msg: => Str): Unit = () val result = parser.parseAll(parser.block(allowNewlines = true)) - val effectiveConfig = Config.elaborationConfig(result) + val effectiveConfig = Config.elaborationConfig(result, file.up) if effectiveConfig.debug.showParsedTree then outputHandler.emit( effectiveConfig.debug.out, s"Parsed tree\n${result.map(_.showAsTree).mkString("\n")}", ) - else DebugOutputHandler.showParsedDefinitions(result, effectiveConfig): (tree, out) => + else DebugOutputHandler.showParsedDefinitions(result, effectiveConfig, file.up): (tree, out) => outputHandler.emit(out, s"Parsed tree\n${tree.showAsTree}") val resultBlk = new syntax.Tree.Block(result) diff --git a/hkmc2/shared/src/main/scala/hkmc2/semantics/Elaborator.scala b/hkmc2/shared/src/main/scala/hkmc2/semantics/Elaborator.scala index 0d7f506c29..e87400ff2c 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/semantics/Elaborator.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/semantics/Elaborator.scala @@ -854,7 +854,7 @@ extends Importer: val modify = ConfigParser.parseOverrides(args) S(Annot.Config(modify)(tree.toLoc)) case App(Ident("dbg"), Tup(args)) => - val modify = ConfigParser.parseDebugDirective(args, definitionLocal = true) + val modify = ConfigParser.parseDebugDirective(args, definitionLocal = true, wd) S(Annot.Debug(modify)(tree.toLoc)) case App(Ident("affine"), Tup(IntLit(whichParamList) :: Nil)) => S(Annot.Affine(whichParamList.toInt)(tree.toLoc)) @@ -2899,7 +2899,7 @@ extends Importer: go(sts, Nil, SetConfig(modify)(statementLoc) :: acc)(N) case Directive(Ident("dbg"), Tup(args)) :: sts => reportUnusedAnnotations - val modify = ConfigParser.parseDebugDirective(args, definitionLocal = false) + val modify = ConfigParser.parseDebugDirective(args, definitionLocal = false, wd) go(sts, Nil, SetConfig(modify)(statementLoc) :: acc)(N) case Directive(Ident(name), _) :: sts => raise(ErrorReport( diff --git a/hkmc2/shared/src/test/mlscript/codegen/DebugDestinations.mls b/hkmc2/shared/src/test/mlscript/codegen/DebugDestinations.mls index 00bf6aa5e8..af1288d8d3 100644 --- a/hkmc2/shared/src/test/mlscript/codegen/DebugDestinations.mls +++ b/hkmc2/shared/src/test/mlscript/codegen/DebugDestinations.mls @@ -41,13 +41,14 @@ fs.readFileSync(debugFile, "utf8") == "Lowering.term 42\nLowering.term 45\n" //│ = true -// Imported units share a handler. Relative and absolute destinations name the same log. +// Imported units share a handler. Relative destinations are resolved against the directory of +// the file that declares them, so relative and absolute destinations name the same log here. // Generate fixtures under target/, outside test discovery, just like the emitted logs. :silent val importLog = "target/DebugImports.log" val absoluteLog = JSON.stringify(fs.realpathSync("target") + "/DebugImports.log") fs.writeFileSync(importLog, "old session contents") -fs.writeFileSync("target/DebugImportA.mls", "#lang(0.3.x)\n@dbg('lowering, showUids: false, out: File(\"../../../../target/DebugImports.log\"))\nmodule DebugImportA with\n fun value() = 2101\n") +fs.writeFileSync("target/DebugImportA.mls", "#lang(0.3.x)\n@dbg('lowering, showUids: false, out: File(\"DebugImports.log\"))\nmodule DebugImportA with\n fun value() = 2101\n") fs.writeFileSync("target/DebugImportB.mls", "#lang(0.3.x)\nimport \"./DebugImportA.mls\"\n@dbg('lowering, showUids: false, out: File(" + absoluteLog + "))\nmodule DebugImportB with\n fun value() = 2102\n") :noCodeGen diff --git a/hkmc2DiffTests/src/test/scala/hkmc2/MLsDiffMaker.scala b/hkmc2DiffTests/src/test/scala/hkmc2/MLsDiffMaker.scala index 83bd69cb91..fea7b77e3b 100644 --- a/hkmc2DiffTests/src/test/scala/hkmc2/MLsDiffMaker.scala +++ b/hkmc2DiffTests/src/test/scala/hkmc2/MLsDiffMaker.scala @@ -236,7 +236,7 @@ abstract class MLsDiffMaker extends DiffMaker: protected var activeDebug = Config.Debug.default - protected lazy val debugOutputHandler = DebugOutputHandler(cctx.fs, wd, str => output(str)) + protected lazy val debugOutputHandler = DebugOutputHandler(cctx.fs, str => output(str)) protected def outputDebugSection(title: Str, contents: Str, out: Config.DebugOutput): Unit = out match case Config.DebugOutput.StdIO => @@ -451,7 +451,7 @@ abstract class MLsDiffMaker extends DiffMaker: val parser = new syntax.Parser(origin, tokens, syntax.ParseRules(), raise, dbg = dbgParsing.isSet): def doPrintDbg(msg: => Str): Unit = if dbg then output(msg) val res = parser.parseAll(parser.block(allowNewlines = true)) - val phaseConfig = Config.elaborationConfig(res) + val phaseConfig = Config.elaborationConfig(res, file.up) activeDebug = phaseConfig.debug // If parsed tree is displayed, don't show the string serialization. @@ -463,7 +463,7 @@ abstract class MLsDiffMaker extends DiffMaker: res.foreach(t => output(t.showAsTree)) else if phaseConfig.debug.showParsedTree then outputDebugSection("Parsed tree", res.map(_.showAsTree).mkString("\n"), phaseConfig.debug.out) - else DebugOutputHandler.showParsedDefinitions(res, phaseConfig): (tree, out) => + else DebugOutputHandler.showParsedDefinitions(res, phaseConfig, file.up): (tree, out) => outputDebugSection("Parsed tree", tree.showAsTree, out) // if showParse.isSet then @@ -491,7 +491,7 @@ abstract class MLsDiffMaker extends DiffMaker: def processTrees(trees: Ls[syntax.Tree])(using Config, Raise): Unit = val blk = new syntax.Tree.Block(trees) - val elaborationConfig = Config.elaborationConfig(blk) + val elaborationConfig = Config.elaborationConfig(blk, file.up) activeDebug = elaborationConfig.debug // val blockSymbol = // semantics.TopLevelSymbol("block#"+blockNum) From da6497a73511142303a8e6feba4569d5b0f65899 Mon Sep 17 00:00:00 2001 From: Lionel Parreaux Date: Tue, 20 Oct 2026 22:39:26 +0800 Subject: [PATCH 22/32] Make small cleanup pass --- .../main/scala/hkmc2/codegen/Lowering.scala | 24 +++++++++---------- .../main/scala/hkmc2/codegen/Printer.scala | 1 - .../scala/hkmc2/semantics/Elaborator.scala | 24 +++++++++---------- .../src/main/scala/hkmc2/syntax/Parser.scala | 18 +++++--------- 4 files changed, 28 insertions(+), 39 deletions(-) diff --git a/hkmc2/shared/src/main/scala/hkmc2/codegen/Lowering.scala b/hkmc2/shared/src/main/scala/hkmc2/codegen/Lowering.scala index 6002be725c..c7188caa88 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/codegen/Lowering.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/codegen/Lowering.scala @@ -94,7 +94,7 @@ class Lowering()(using Config, TL, Raise, State, Ctx, SymbolPrinter)(using Erasu private def erroneousRef(ref: NewResolvable): Bool = ref.isErroneous || ref.resolvedTargets.exists(_.defn.exists(_.isErroneous)) - /** Lower the parent before the class body and share its value with every super reference. + /** Lower the parent reference before the class body. * Definition references are stable; classOf already saves computed class values in temporaries. * Other paths (notably a class selected through a mutable receiver) must be read here, once, * so changing that receiver after the class definition cannot redirect its super calls. @@ -104,16 +104,16 @@ class Lowering()(using Config, TL, Raise, State, Ctx, SymbolPrinter)(using Erasu val saved = loweringCtx.registerTempSymbol(N, erasedType = N, "superBase") Assign(saved, path, k(saved.asSimpleRef)) parent match - case Value.MemberRef(_, _: ClassSymbol) | Value.SimpleRef(_: TempSymbol) => k(parent) - case sel: Select if sel.symbol.exists(_.isInstanceOf[ClassSymbol]) => - // The selected class declaration is immutable, but its receiver might be a variable. - // Save that receiver while retaining the class symbol for nominal IR analyses. - sel.qual match - case _: Value.This | Value.MemberRef(_, _: ClassSymbol | _: ModuleOrObjectSymbol) | - Value.SimpleRef(_: TempSymbol) => k(sel) - case qual => save(qual)(p => k(sel.copy(qual = p)(sel.symbol, sel.toLoc)(sel.sanitize))) - case _ => save(parent)(k) - + case Value.MemberRef(_, _: ClassSymbol) | Value.SimpleRef(_: TempSymbol) => k(parent) + case sel: Select if sel.symbol.exists(_.isInstanceOf[ClassSymbol]) => + // The selected class declaration is immutable, but its receiver might be a variable. + // Save that receiver while retaining the class symbol for nominal IR analyses. + sel.qual match + case _: Value.This | Value.MemberRef(_, _: ClassSymbol | _: ModuleOrObjectSymbol) + | Value.SimpleRef(_: TempSymbol) => k(sel) + case qual => save(qual)(p => k(sel.copy(qual = p)(sel.symbol, sel.toLoc)(sel.sanitize))) + case _ => save(parent)(k) + private def definitionConfigOverride(annotations: Ls[Annot]): Opt[Config] = val modifiers = annotations.collect: case Annot.Config(modify) => modify @@ -462,8 +462,6 @@ class Lowering()(using Config, TL, Raise, State, Ctx, SymbolPrinter)(using Erasu case N => Define(lowerClass(N, Nil), blockImpl(stats, res)) case S(ext) => assert(k isnt syntax.Mod) // modules can't extend things and can't have super calls - // Evaluating the superclass is part of the class definition, so it is traced with it, - // but the statements following the class are not. debugLoweringThen[ClsLikeDefn](defn.extraAnnotations)(define => classOf(ext.cls, ext): (clsp, params) => withClassParent(clsp)(parent => define(lowerClass(S(parent), params))) diff --git a/hkmc2/shared/src/main/scala/hkmc2/codegen/Printer.scala b/hkmc2/shared/src/main/scala/hkmc2/codegen/Printer.scala index aaae454874..2a7b3d3e6a 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/codegen/Printer.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/codegen/Printer.scala @@ -118,7 +118,6 @@ class Printer(using Config, Ctx, Raise, ShowCfg, State, SymbolPrinter): // if cfg.staged then doc"staged " else doc"" printAnnotations(defn.annotations, doc" # ") - /** Print one definition in the same form used when it occurs in a complete IR block. */ def printDefinition(defn: Defn)(using Scope): Document = doc"${printFlags(defn)}define ${print(defn.sym)} as ${print(defn)}" diff --git a/hkmc2/shared/src/main/scala/hkmc2/semantics/Elaborator.scala b/hkmc2/shared/src/main/scala/hkmc2/semantics/Elaborator.scala index e87400ff2c..5123a6b118 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/semantics/Elaborator.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/semantics/Elaborator.scala @@ -2464,8 +2464,6 @@ extends Importer: msg"Illegal ${k.desc} member name: '${id.name}'" -> nme.toLoc :: illegalMemberNameTail return go(sts, Nil, acc)(N) - // A value is the result of evaluating its body once, so it cannot take parameters; - // lowering relies on this. Drop them, so that uses of them in the body are reported. val paramLists = td.paramLists match case pss @ (_ :: _) if k.isInstanceOf[Val] => raise: @@ -2619,17 +2617,17 @@ extends Importer: case Annot.Modifier(Keyword.`data`) => true case _ => false - val pss = debugElaboration(annotations)(td.paramLists.map { ps => - val (res, newCtx2) = - given Ctx = newCtx - params(ps, isDataClass, k is Pat) - newCtx = newCtx2 - // Spread parameters are not supported in class parameters. - res.restParam.foreach: rp => - raise(ErrorReport( - msg"Spread parameters are not supported in class parameters." -> rp.toLoc :: Nil)) - res.copy(restParam = N)(res.toLoc) - }) + val pss = debugElaboration(annotations): + td.paramLists.map: ps => + val (res, newCtx2) = + given Ctx = newCtx + params(ps, isDataClass, k is Pat) + newCtx = newCtx2 + // Spread parameters are not supported in class parameters. + res.restParam.foreach: rp => + raise(ErrorReport( + msg"Spread parameters are not supported in class parameters." -> rp.toLoc :: Nil)) + res.copy(restParam = N)(res.toLoc) def withFields(extraParams: Ls[ParamList])(using Ctx)(fn: (Ctx) ?=> (Term.Blk, Ctx)): (Term.Blk, Ctx) = softAssert(pss.sizeCompare(td.clsParams) === 0, diff --git a/hkmc2/shared/src/main/scala/hkmc2/syntax/Parser.scala b/hkmc2/shared/src/main/scala/hkmc2/syntax/Parser.scala index d2624c5eb8..4833fdc2e9 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/syntax/Parser.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/syntax/Parser.scala @@ -116,16 +116,10 @@ object Parser: def unapply(ts: Ls[TokLoc]): S[Ls[TokLoc]] = S(get(ts)) - /** Whether `id`, followed by the tokens `rest`, is a keyword used as a key, as in `class: e` or `true: e`. - * An alphanumeric keyword followed by a colon is parsed as an identifier, so that record fields and - * named arguments can have any name. This does not make anything ambiguous: no keyword-introduced - * syntax continues with a colon, and expressions are ascribed with `as` rather than with a colon. */ - private def isKeywordKey(id: IDENT, rest: Ls[TokLoc]): Bool = - def colonFollows(ts: Ls[TokLoc]): Bool = ts match - case (SPACE, _) :: ts => colonFollows(ts) - case (IDENT(":", _), _) :: _ => true + private def isKey(id: IDENT, rest: Ls[TokLoc]): Bool = + !id.symbolic && rest.match + case NOISE(IDENT(":", _) -> _ :: _) => true case _ => false - !id.symbolic && Keyword.all.contains(id.name) && colonFollows(rest) extension (loc: Loc) def showStart: String = @@ -372,7 +366,7 @@ abstract class Parser( errExpr :: Nil case _ => blockOf(rule, a :: annotations, allowNewlines = allowNewlines) - case (tok @ (id: IDENT), loc) :: rest if id.name =/= ":" && !isKeywordKey(id, rest) => + case (tok @ (id: IDENT), loc) :: rest if id.name =/= ":" && !isKey(id, rest) => Keyword.all.get(id.name) match case S(kw) => consume @@ -471,7 +465,7 @@ abstract class Parser( // case N => // err((msg"Expected ${rule.whatComesAfter} ${rule.mkAfterStr}; found end of phrase instead" -> S(loc.left) :: Nil) // N - case (tok @ (id: IDENT), loc) :: rest if isKeywordKey(id, rest) => + case (tok @ (id: IDENT), loc) :: rest if isKey(id, rest) => tryParseExp(prec, tok, loc, rule, allowNewlines = allowNewlines) case (tok @ (id: IDENT), loc) :: _ => Keyword.all.get(id.name) match @@ -659,7 +653,7 @@ abstract class Parser( consume val id = Tree.Ident(name).withLoc(S(loc)) exprCont(id, prec, allowNewlines = true) - case (tok @ IDENT(nme, _), loc) :: rest if isKeywordKey(tok, rest) => + case (tok @ IDENT(nme, _), loc) :: rest if isKey(tok, rest) => consume exprCont(Tree.Ident(nme).withLoc(S(loc)), prec, allowNewlines = allowNewlines) case (IDENT(nme, sym), loc) :: _ => From c68d4b80e78a5e270e4e93defe3f319018f18449 Mon Sep 17 00:00:00 2001 From: OpenAI Codex Date: Tue, 20 Oct 2026 23:05:35 +0800 Subject: [PATCH 23/32] Remove redundant and misleading debug comments --- .../main/scala/hkmc2/DebugOutputHandler.scala | 17 ++++------------- .../src/main/scala/hkmc2/codegen/Lowering.scala | 1 - .../src/main/scala/utils/TraceLogger.scala | 1 - .../test/mlscript/codegen/DebugDirective.mls | 2 +- .../src/test/mlscript/codegen/DebugScoping.mls | 2 +- 5 files changed, 6 insertions(+), 17 deletions(-) diff --git a/hkmc2/shared/src/main/scala/hkmc2/DebugOutputHandler.scala b/hkmc2/shared/src/main/scala/hkmc2/DebugOutputHandler.scala index 532bfc9ee3..8337f80af2 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/DebugOutputHandler.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/DebugOutputHandler.scala @@ -5,11 +5,8 @@ import scala.collection.mutable import hkmc2.utils.*, shorthands.* -/** Routes compiler debugging output without putting callbacks or mutable streams in [[Config]]. - * - * A session shares one handler across compilation units: each destination is cleared on first - * use, then appended to without retaining or rewriting previous output. Concurrent debug writes - * are best effort; this handler does not synchronize them. */ +/** A compilation session shares one handler across units: each output file is overwritten on first + * use, then appended to. Concurrent debug writes are not synchronized. */ final class DebugOutputHandler(fs: io.FileSystem, stdIO: Str => Unit): private val writtenFiles = mutable.Set.empty[io.Path] @@ -66,24 +63,19 @@ object DebugOutputHandler: (display: (Str, Str, Config.DebugOutput) => Unit)(using DebugPrinter): Unit = showElaborated(block, config, _.showElaboratedTree, "Elaborated tree", _.showAsTree, _.showAsTree)(display) - /** Print elaborated terms in readable form, with the settings of the `:sr` diff-test command. - * `showFlowSymbols` also disambiguates symbol names, which a readable form needs; before flow - * analysis, it shows selections as unresolved (`ˀˀˀ`). Erased types are not computed yet. */ def showElaboratedTerm(block: semantics.Term.Blk, config: Config, width: Int) (display: (Str, Str, Config.DebugOutput) => Unit)(using utils.Scope, Raise): Unit = given semantics.ShowCfg = semantics.ShowCfg( showErasedTypes = false, showExpansionMappings = true, - showFlowSymbols = true, + showFlowSymbols = true, // Also disambiguates symbols that share a name. debug = false, ) showElaborated(block, config, _.showElaboratedTerm, "Elaborated term", _.showTopLevel.mkString(width), _.show.mkString(width))(display) /** Display the whole block if the file's settings select it, and otherwise each annotated - * definition that selects it, including definitions nested in other definitions. - * Inspect the elaborated tree directly: new resolution runs during elaboration and does not - * traverse the legacy resolver, but annotated definitions still need to be displayed. */ + * definition that selects it, including definitions nested in other definitions. */ private def showElaborated(block: semantics.Term.Blk, config: Config, isEnabled: Config.Debug => Bool, title: Str, showBlock: semantics.Term.Blk => Str, showDefinition: semantics.Definition => Str) (display: (Str, Str, Config.DebugOutput) => Unit): Unit = @@ -97,7 +89,6 @@ object DebugOutputHandler: if isEnabled(config.debug) then display(title, showBlock(block), config.debug.out) else visit(block) - /** Use the same selection and formatting in the compiler and diff-test pipelines. */ def showIR(program: codegen.Program, optimized: Bool, printer: codegen.Printer, width: Int) (display: (Str, Str, Config.DebugOutput) => Unit)(using Config, utils.Scope): Unit = val title = if optimized then "Optimized IR" else "Lowered IR" diff --git a/hkmc2/shared/src/main/scala/hkmc2/codegen/Lowering.scala b/hkmc2/shared/src/main/scala/hkmc2/codegen/Lowering.scala index c7188caa88..2aeaef0a30 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/codegen/Lowering.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/codegen/Lowering.scala @@ -124,7 +124,6 @@ class Lowering()(using Config, TL, Raise, State, Ctx, SymbolPrinter)(using Erasu private def debugLowering[T](annotations: Ls[Annot])(thunk: => T): T = DebugOutputHandler.traceDefinition(annotations, config, _.lowering)(thunk) - /** Trace a definition whose lowering continues, through `k`, with the statements that follow it. */ private def debugLoweringThen[R](annotations: Ls[Annot])(lower: (R => Block) => Block)(k: R => Block): Block = DebugOutputHandler.traceDefinitionThen(annotations, config, _.lowering)(lower)(k) diff --git a/hkmc2/shared/src/main/scala/utils/TraceLogger.scala b/hkmc2/shared/src/main/scala/utils/TraceLogger.scala index 28e0caf31e..6e73e8fa28 100644 --- a/hkmc2/shared/src/main/scala/utils/TraceLogger.scala +++ b/hkmc2/shared/src/main/scala/utils/TraceLogger.scala @@ -31,7 +31,6 @@ abstract class TraceLogger(using val debugPrinter: DebugPrinter): case (false, _) :: _ => Config.DebugOutput.StdIO case Nil => defaultDebugOutput - /** An inner annotation changes only the settings it names in the enclosing debug scope. */ final def debugConfig(base: Config): Config = debugOverrides.headOption.fold(base)(entry => base.copy(debug = entry._2)) diff --git a/hkmc2/shared/src/test/mlscript/codegen/DebugDirective.mls b/hkmc2/shared/src/test/mlscript/codegen/DebugDirective.mls index 1eb3a373cd..27197741c5 100644 --- a/hkmc2/shared/src/test/mlscript/codegen/DebugDirective.mls +++ b/hkmc2/shared/src/test/mlscript/codegen/DebugDirective.mls @@ -12,7 +12,7 @@ fun do_not_debug_me(x) = x //│ ~> x⁰ -// Named flags can be explicitly disabled, and StdIO is the default destination. +// Named flags can be explicitly disabled. #dbg(elaboration: false, out: StdIO) fun named_debug_config(x) = x diff --git a/hkmc2/shared/src/test/mlscript/codegen/DebugScoping.mls b/hkmc2/shared/src/test/mlscript/codegen/DebugScoping.mls index 59b222520e..1e4d4d7285 100644 --- a/hkmc2/shared/src/test/mlscript/codegen/DebugScoping.mls +++ b/hkmc2/shared/src/test/mlscript/codegen/DebugScoping.mls @@ -56,7 +56,7 @@ DebugMethods.selected(7) //│ ~> { fun member:command_elaboration(x¹) = x¹; } -// A class's lowering trace covers the evaluation of its superclass, +// A class's lowering trace includes its superclass reference, // but not the statements that follow the class definition. class DebugParent @dbg('lowering, showUids: false) class DebugChild extends DebugParent From 2e42a5607d0e022c7e366984859a30a053ada35f Mon Sep 17 00:00:00 2001 From: OpenAI Codex Date: Tue, 20 Oct 2026 23:38:24 +0800 Subject: [PATCH 24/32] Centralize debug scoping and compose trace loggers --- .../src/test/scala/hkmc2/CompilerTest.scala | 18 ++--- .../jvm/src/test/scala/hkmc2/MarksTest.scala | 3 +- .../scala/hkmc2/SubstitutionGrowthTest.scala | 6 +- .../test/scala/hkmc2/TypeRelationTest.scala | 3 +- .../src/main/scala/hkmc2/CompilerCtx.scala | 45 +++-------- .../src/main/scala/hkmc2/DebugContext.scala | 42 ++++++++++ .../main/scala/hkmc2/DebugOutputHandler.scala | 21 ----- .../src/main/scala/hkmc2/MLsCompiler.scala | 3 +- .../hkmc2/codegen/DeadConstructorElim.scala | 2 +- .../scala/hkmc2/codegen/DeadParamElim.scala | 2 +- .../scala/hkmc2/codegen/EtaExpansion.scala | 2 +- .../main/scala/hkmc2/codegen/Lowering.scala | 4 +- .../codegen/flowAnalysis/FlowAnalysis.scala | 10 +-- .../scala/hkmc2/semantics/Elaborator.scala | 30 +------ .../src/main/scala/utils/TraceLogger.scala | 81 ++++++++----------- .../test/scala/hkmc2/JSBackendDiffMaker.scala | 23 +----- .../src/test/scala/hkmc2/MLsDiffMaker.scala | 57 ++++--------- 17 files changed, 133 insertions(+), 219 deletions(-) create mode 100644 hkmc2/shared/src/main/scala/hkmc2/DebugContext.scala diff --git a/hkmc2/js/src/test/scala/hkmc2/CompilerTest.scala b/hkmc2/js/src/test/scala/hkmc2/CompilerTest.scala index 024a6a2d4e..8ff8c08733 100644 --- a/hkmc2/js/src/test/scala/hkmc2/CompilerTest.scala +++ b/hkmc2/js/src/test/scala/hkmc2/CompilerTest.scala @@ -252,8 +252,7 @@ class CompilerTest extends AnyFunSuite: compiler.compile("/B.mls") given DebugPrinter = new DebugPrinter - given TL = new TraceLogger: - override def doTrace = false + given TL = TraceLogger.silent given Raise = diagnostic => fail(diagnostic.toString) val prelude = cctx.getPrelude(paths.preludeFile).ctx val artifact = cctx.getElaboratedBlock(Path("/A.mls"), prelude) @@ -275,8 +274,7 @@ class CompilerTest extends AnyFunSuite: val fs = new InMemoryFileSystem(loadStandardLibrary()) given cctx: CompilerCtx = CompilerCtx.fresh(fs, paths, Config.default(io.Path("/"))) given DebugPrinter = new DebugPrinter - given TL = new TraceLogger: - override def doTrace = false + given TL = TraceLogger.silent given Raise = diagnostic => fail(diagnostic.toString) val prelude = cctx.getPrelude(paths.preludeFile).ctx val map = prelude.builtins.Array.defn.get.body.members("map").asTrm.get.defn.get @@ -316,8 +314,7 @@ class CompilerTest extends AnyFunSuite: val fs = new InMemoryFileSystem(loadStandardLibrary()) given cctx: CompilerCtx = CompilerCtx.fresh(fs, paths, Config.default(io.Path("/"))) given DebugPrinter = new DebugPrinter - given TL = new TraceLogger: - override def doTrace = false + given TL = TraceLogger.silent given Raise = diagnostic => fail(diagnostic.toString) fs.write("/Generic.mls", """#lang(0.3.x, strictResolution: true) |module Generic with @@ -353,8 +350,7 @@ class CompilerTest extends AnyFunSuite: val fs = new InMemoryFileSystem(loadStandardLibrary()) given cctx: CompilerCtx = CompilerCtx.fresh(fs, paths, Config.default(io.Path("/"))) given DebugPrinter = new DebugPrinter - given TL = new TraceLogger: - override def doTrace = false + given TL = TraceLogger.silent given Raise = diagnostic => fail(diagnostic.toString) fs.write("/Types.mls", """#lang(0.3.x, strictResolution: true) |module Types with @@ -391,8 +387,7 @@ class CompilerTest extends AnyFunSuite: val fs = new InMemoryFileSystem(loadStandardLibrary()) given cctx: CompilerCtx = CompilerCtx.fresh(fs, paths, Config.default(io.Path("/"))) given DebugPrinter = new DebugPrinter - given TL = new TraceLogger: - override def doTrace = false + given TL = TraceLogger.silent given Raise = diagnostic => fail(diagnostic.toString) val prelude = cctx.getPrelude(paths.preludeFile).ctx val parameter = prelude.builtins.Array.defn.get.tparams.head.sym @@ -425,8 +420,7 @@ class CompilerTest extends AnyFunSuite: val fs = new InMemoryFileSystem(loadStandardLibrary()) given cctx: CompilerCtx = CompilerCtx.fresh(fs, paths, Config.default(io.Path("/"))) given DebugPrinter = new DebugPrinter - given TL = new TraceLogger: - override def doTrace = false + given TL = TraceLogger.silent val errors = scala.collection.mutable.ArrayBuffer.empty[Diagnostic] given Raise = errors += _ fs.write("/Unsafe.mls", """#lang(0.3.x, strictResolution: false) diff --git a/hkmc2/jvm/src/test/scala/hkmc2/MarksTest.scala b/hkmc2/jvm/src/test/scala/hkmc2/MarksTest.scala index 3b2b4dcb31..0b400ae555 100644 --- a/hkmc2/jvm/src/test/scala/hkmc2/MarksTest.scala +++ b/hkmc2/jvm/src/test/scala/hkmc2/MarksTest.scala @@ -20,8 +20,7 @@ class MarksTest extends AnyFunSuite: given ResolutionContext = owner.newResolverState.context given Raise = diagnostic => fail(diagnostic.theMsg) given DebugPrinter = new DebugPrinter - given TraceLogger = new TraceLogger: - override def doTrace: Boolean = false + given TraceLogger = TraceLogger.silent // Path normalization needs a value that retains scope exits. Dynamic values // omit those exits because they carry no references into their source scope. val resolution = new TypeResolution(Term.UnitVal(), _ => fail("Unexpected type error")) diff --git a/hkmc2/jvm/src/test/scala/hkmc2/SubstitutionGrowthTest.scala b/hkmc2/jvm/src/test/scala/hkmc2/SubstitutionGrowthTest.scala index 4981b5ceb8..450b5d6eed 100644 --- a/hkmc2/jvm/src/test/scala/hkmc2/SubstitutionGrowthTest.scala +++ b/hkmc2/jvm/src/test/scala/hkmc2/SubstitutionGrowthTest.scala @@ -48,8 +48,7 @@ class SubstitutionGrowthTest extends AnyFunSuite: private val cctx = TestFolders.compilerCtx(os.pwd) private given DebugPrinter = new DebugPrinter - private val tl = new TraceLogger: - override def doTrace: Bool = false + private val tl = TraceLogger.silent /** Resolves a generated unit through ordinary elaboration, in a fresh state. */ private def measure(name: Str, source: Str): Measurement = @@ -129,8 +128,7 @@ class SubstitutionGrowthTest extends AnyFunSuite: given owner: Elaborator.State = new Elaborator.State given state: ResolutionContext = owner.newResolverState.context given Raise = diagnostic => fail(diagnostic.theMsg) - given trace: TraceLogger = new TraceLogger: - override def doTrace: Boolean = false + given trace: TraceLogger = TraceLogger.silent given Config = Config.default(TestFolders.mainTestDir(os.pwd)) given CompilerCtx = CompilerCtx.fresh(io.FileSystem.default, TestFolders.compilerPaths(os.pwd), summon[Config]) val resolver = Elaborator(trace, TestFolders.mainTestDir(os.pwd), Elaborator.Ctx.empty) diff --git a/hkmc2/jvm/src/test/scala/hkmc2/TypeRelationTest.scala b/hkmc2/jvm/src/test/scala/hkmc2/TypeRelationTest.scala index 600f59df16..4be17b0a2c 100644 --- a/hkmc2/jvm/src/test/scala/hkmc2/TypeRelationTest.scala +++ b/hkmc2/jvm/src/test/scala/hkmc2/TypeRelationTest.scala @@ -14,8 +14,7 @@ class TypeRelationTest extends AnyFunSuite: given state: ResolutionContext = owner.newResolverState.context given Raise = diagnostic => fail(diagnostic.theMsg) given DebugPrinter = new DebugPrinter - given trace: TraceLogger = new TraceLogger: - override def doTrace: Boolean = false + given trace: TraceLogger = TraceLogger.silent given Config = Config.default(TestFolders.mainTestDir(os.pwd)) given CompilerCtx = CompilerCtx.fresh(io.FileSystem.default, TestFolders.compilerPaths(os.pwd), summon[Config]) val resolver = Elaborator(trace, TestFolders.mainTestDir(os.pwd), Elaborator.Ctx.empty) diff --git a/hkmc2/shared/src/main/scala/hkmc2/CompilerCtx.scala b/hkmc2/shared/src/main/scala/hkmc2/CompilerCtx.scala index 2ae803610f..9cd2a70f87 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/CompilerCtx.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/CompilerCtx.scala @@ -72,11 +72,8 @@ class CompilerCtx( def mk = val dependencies = new CompilerCtx.DependencyRecorder val modulePath = (file.up / io.RelPath(file.baseName + ".mjs")).toString - var stateDebug = false - var stateShowUids = true - val state = new Elaborator.State: - override protected def doDbg: Bool = stateDebug - override protected def doShowUids: Bool = stateShowUids + val debug = new DebugContext + val state = new Elaborator.State(debug) given Elaborator.State = state given Config = rootConfig @@ -88,24 +85,19 @@ class CompilerCtx( includeZero = true, )) ) - val backendTL = new TraceLogger: - override def doTrace: Bool = false + val backendTL = TraceLogger.silent val parse = given CompilerCtx = this ParserSetup(file, outputHandler) val parsed = parse.resultBlk val phaseConfig = Config.elaborationConfig(parsed, file.up) - stateDebug = CompilerCtx.debugTracingEnabled(phaseConfig.debug) - stateShowUids = phaseConfig.debug.showUids - def traceLogger(enabled: Bool) = new TraceLogger: - override def doTrace: Bool = enabled - override protected def defaultDebugOutput: Config.DebugOutput = phaseConfig.debug.out - override protected[hkmc2] def emitDbg(str: Str, out: Config.DebugOutput): Unit = - outputHandler.emit(out, str) - val etl = traceLogger(phaseConfig.debug.elaboration) - val ltl = traceLogger(phaseConfig.debug.lowering) - val dtl = traceLogger(phaseConfig.debug.optimizations) + debug.configure(phaseConfig.debug) + def traceLogger(phase: Config.Debug => Bool) = + new TraceLogger(debug, phase, _ => false, outputHandler.emit, indentWhenDisabled = true) + val etl = traceLogger(_.elaboration) + val ltl = traceLogger(_.lowering) + val dtl = traceLogger(_.optimizations) given Elaborator.Ctx = prelude val artifactCtx = derive(parse.origin.fileName, dependencies) @@ -256,24 +248,16 @@ class CompilerCtx( s"Cached prelude for $file was elaborated under a different root configuration") art.lastChangedTimestamp >= lastMod, create = - var stateDebug = false - var stateShowUids = true - val state = new Elaborator.State: - override protected def doDbg: Bool = stateDebug - override protected def doShowUids: Bool = stateShowUids + val debug = new DebugContext + val state = new Elaborator.State(debug) given Elaborator.State = state given Config = rootConfig given CompilerCtx = this given DebugPrinter = new DebugPrinter val parse = ParserSetup(file, outputHandler) val elaborationConfig = Config.elaborationConfig(parse.resultBlk, file.up) - stateDebug = CompilerCtx.debugTracingEnabled(elaborationConfig.debug) - stateShowUids = elaborationConfig.debug.showUids - val etl = new TraceLogger: - override def doTrace: Bool = elaborationConfig.debug.elaboration - override protected def defaultDebugOutput: Config.DebugOutput = elaborationConfig.debug.out - override protected[hkmc2] def emitDbg(str: Str, out: Config.DebugOutput): Unit = - outputHandler.emit(out, str) + debug.configure(elaborationConfig.debug) + val etl = new TraceLogger(debug, _.elaboration, _ => false, outputHandler.emit, indentWhenDisabled = true) // The prelude defines the builtins consulted during its own resolution. // Seed lookup with the block's original symbols, as block elaboration does, // so those lookups share the declarations that are being elaborated. @@ -294,9 +278,6 @@ object CompilerCtx: inline def get(using cctx: CompilerCtx) = cctx - private def debugTracingEnabled(debug: Config.Debug): Bool = - debug.elaboration || debug.lowering || debug.optimizations - /** Collect import provenance after elaboration has assembled all user and synthetic imports. * diff --git a/hkmc2/shared/src/main/scala/hkmc2/DebugContext.scala b/hkmc2/shared/src/main/scala/hkmc2/DebugContext.scala new file mode 100644 index 0000000000..07dde672c7 --- /dev/null +++ b/hkmc2/shared/src/main/scala/hkmc2/DebugContext.scala @@ -0,0 +1,42 @@ +package hkmc2 + +import hkmc2.utils.*, shorthands.* + + +/** Debug settings shared by a compilation unit's symbol printing and phase loggers. + * Imported units have their own context; only their output handler is shared. + * Compilation is synchronous within a unit, so scoped changes are restored on the call stack. */ +final class DebugContext(forceUids: () => Bool): + def this() = this(() => false) + + private case class Settings(debug: Config.Debug, uidDebug: Bool) + private var current = Settings(Config.Debug.default, false) + + def config: Config.Debug = current.debug + def showUids: Bool = (current.uidDebug || forceUids()) && config.showUids + + /** File directives are discovered after parsing; diff-test blocks also replace their base settings. */ + def configure(debug: Config.Debug): Unit = + current = Settings(debug, debug.elaboration || debug.lowering || debug.optimizations) + + private def within[T](settings: Settings)(body: => T): T = + val saved = current + current = settings + try body finally current = saved + + def traceDefinition[T](annotations: Ls[semantics.Annot], base: Config, enabled: Config.Debug => Bool) + (body: => T): T = + traceDefinitionThen[T, T](annotations, base, enabled)(k => k(body))(identity) + + /** A lowering continuation processes following statements in the enclosing settings. + * Capture those settings at entry, independently of scopes entered while processing the definition. */ + def traceDefinitionThen[R, T](annotations: Ls[semantics.Annot], base: Config, enabled: Config.Debug => Bool) + (process: (R => T) => T)(k: R => T): T = + DebugOutputHandler.definitionConfig(annotations, base.copy(debug = config)) match + case S(localConfig) => + val outer = current + within(Settings(localConfig.debug, enabled(localConfig.debug))): + process(r => within(outer)(k(r))) + case N => process(k) + +end DebugContext diff --git a/hkmc2/shared/src/main/scala/hkmc2/DebugOutputHandler.scala b/hkmc2/shared/src/main/scala/hkmc2/DebugOutputHandler.scala index 8337f80af2..9203aa76ba 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/DebugOutputHandler.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/DebugOutputHandler.scala @@ -26,27 +26,6 @@ object DebugOutputHandler: if modifiers.isEmpty then N else S(modifiers.foldLeft(base)((cfg, modify) => modify(cfg))) - /** Keep trace routing and symbol printing in the same definition-local scope. */ - def traceDefinition[T](annotations: Ls[semantics.Annot], base: Config, enabled: Config.Debug => Bool) - (thunk: => T)(using semantics.Elaborator.State, utils.TL): T = - traceDefinitionThen[T, T](annotations, base, enabled)(k => k(thunk))(identity) - - /** Like [[traceDefinition]], for a definition processed in continuation-passing style, where the - * continuation `k` processes the statements that follow the definition: `process` runs in the - * definition's debug scope, but `k` runs back in the enclosing one, so that the definition's - * settings do not leak to its siblings. */ - def traceDefinitionThen[R, T](annotations: Ls[semantics.Annot], base: Config, enabled: Config.Debug => Bool) - (process: (R => T) => T)(k: R => T)(using state: semantics.Elaborator.State, tl: utils.TL): T = - definitionConfig(annotations, tl.debugConfig(base)) match - case S(localConfig) => - val debug = localConfig.debug - state.scopedDebug(enabled(debug), debug.showUids): - tl.scopedDebug(enabled(debug), debug): - process: r => - state.inOuterDebugScope: - tl.inOuterDebugScope(k(r)) - case N => process(k) - def showParsedDefinitions(trees: Ls[syntax.Tree], config: Config, sourceDir: io.Path) (display: (syntax.Tree, Config.DebugOutput) => Unit): Unit = def visit(tree: syntax.Tree): Unit = tree match diff --git a/hkmc2/shared/src/main/scala/hkmc2/MLsCompiler.scala b/hkmc2/shared/src/main/scala/hkmc2/MLsCompiler.scala index 1112c30a03..7b09d29fe6 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/MLsCompiler.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/MLsCompiler.scala @@ -62,8 +62,7 @@ class MLsCompiler given Raise = mkRaise(file) given DebugPrinter = new DebugPrinter - val compilerTL = new TraceLogger: - override def doTrace: Bool = false + val compilerTL = TraceLogger.silent val preludeCtx = cctx.getPrelude(preludeFile)(using compilerTL, summon[Raise]).ctx val artifact = cctx.getElaboratedBlock(file, preludeCtx)(using compilerTL) diff --git a/hkmc2/shared/src/main/scala/hkmc2/codegen/DeadConstructorElim.scala b/hkmc2/shared/src/main/scala/hkmc2/codegen/DeadConstructorElim.scala index 2c2634d564..04b0a8a8e2 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/codegen/DeadConstructorElim.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/codegen/DeadConstructorElim.scala @@ -74,7 +74,7 @@ class DeadConstructorElimSolver(val constraintSolver: FlowConstraintSolver, trac .filterNot(liveCtorConcreteIds) end deadCtors - if tl.doTrace then + if tl.isTracing then tl.log(">>> dead-constructor-elim results >>>") for ctorStr <- deadCtors.toSeq.map(_.exprId.getResult.showDbg).sorted do tl.log(s"eliminable: $ctorStr") diff --git a/hkmc2/shared/src/main/scala/hkmc2/codegen/DeadParamElim.scala b/hkmc2/shared/src/main/scala/hkmc2/codegen/DeadParamElim.scala index fb241111dd..5d45c2187c 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/codegen/DeadParamElim.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/codegen/DeadParamElim.scala @@ -115,7 +115,7 @@ class DeadParamElimSolver(val constraintSolver: FlowConstraintSolver, traceLogge case None => eliminableCallSiteArgsById(consFun.concreteId) = eliminable.toSet case S(existing) => assert(existing.toList.sorted === eliminable) - if tl.doTrace then + if tl.isTracing then given ShowCfg = ShowCfg.internal assert(eliminableCallSiteArgsById.nonEmpty === eliminableParamsById.nonEmpty) diff --git a/hkmc2/shared/src/main/scala/hkmc2/codegen/EtaExpansion.scala b/hkmc2/shared/src/main/scala/hkmc2/codegen/EtaExpansion.scala index cad92575d6..fc1f77b3e5 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/codegen/EtaExpansion.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/codegen/EtaExpansion.scala @@ -140,7 +140,7 @@ class EtaExpansionSolver(val constraintSolver: FlowConstraintSolver, tl: TraceLo case (_: TermSymbol, 0) => addFunShapeIfChanged() case _ => () - if tl.doTrace then + if tl.isTracing then given ShowCfg = ShowCfg.internal tl.log(">>> eta-expansion targets shapes >>>") diff --git a/hkmc2/shared/src/main/scala/hkmc2/codegen/Lowering.scala b/hkmc2/shared/src/main/scala/hkmc2/codegen/Lowering.scala index 2aeaef0a30..1317b13007 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/codegen/Lowering.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/codegen/Lowering.scala @@ -122,10 +122,10 @@ class Lowering()(using Config, TL, Raise, State, Ctx, SymbolPrinter)(using Erasu else S(modifiers.foldLeft(config)((cfg, modify) => modify(cfg))) private def debugLowering[T](annotations: Ls[Annot])(thunk: => T): T = - DebugOutputHandler.traceDefinition(annotations, config, _.lowering)(thunk) + tl.traceDefinition(annotations, config)(thunk) private def debugLoweringThen[R](annotations: Ls[Annot])(lower: (R => Block) => Block)(k: R => Block): Block = - DebugOutputHandler.traceDefinitionThen(annotations, config, _.lowering)(lower)(k) + tl.traceDefinitionThen(annotations, config)(lower)(k) extension (t: Term) def instantiated = t match diff --git a/hkmc2/shared/src/main/scala/hkmc2/codegen/flowAnalysis/FlowAnalysis.scala b/hkmc2/shared/src/main/scala/hkmc2/codegen/flowAnalysis/FlowAnalysis.scala index 220404f5a8..c153524d61 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/codegen/flowAnalysis/FlowAnalysis.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/codegen/flowAnalysis/FlowAnalysis.scala @@ -89,14 +89,14 @@ object FlowAnalysis: prefix: Str, outerTl: TL, ): TraceLogger = - new TraceLogger(using outerTl.debugPrinter): - override def doTrace: Bool = scope match + TraceLogger( + { case S(TraceScope.NonAffineSyms) => cfg.logNonAffine case S(TraceScope.AccumulatorSym) => cfg.logAccumulator case _ => cfg.debug - - override def emitDbg(str: Str): Unit = - outerTl.emitDbg(s"$prefix$str") + }, + str => outerTl.emitDbg(s"$prefix$str"), + )(using outerTl.debugPrinter) end FlowAnalysis diff --git a/hkmc2/shared/src/main/scala/hkmc2/semantics/Elaborator.scala b/hkmc2/shared/src/main/scala/hkmc2/semantics/Elaborator.scala index 5123a6b118..482deaacf6 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/semantics/Elaborator.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/semantics/Elaborator.scala @@ -631,7 +631,8 @@ object Elaborator: case Default(sym: ImportSymbol, modulePath: Str) case Private(sym: BlockMemberSymbol, modulePath: Str) - class State: + class State(val debugContext: DebugContext): + def this() = this(new DebugContext) val newResolverState: NewResolverState = new NewResolverState(this) val suid = new Uid.Symbol.State given State = this @@ -727,31 +728,8 @@ object Elaborator: "globalThis" -> globalThisSymbol, )) val superSymbol = builtinOpsMap("super") - // Definition-local debug annotations may independently change whether tracing is active and - // whether symbol identities are useful in that trace, so both settings share the same scope. - private var debugOverrides = Ls.empty[(Bool, Bool)] - protected def doDbg: Bool = false - /** Debugging requested independently of the [[Config.Debug]] settings, such as by diff-test - * commands; as for `TraceLogger.doForceTrace`, definition-local overrides cannot disable it. - * Implementations should include it in [[doDbg]]. */ - protected def doForceDbg: Bool = false - protected def doShowUids: Bool = true - final def dbg: Bool = debugOverrides match - case (enabled, _) :: _ => enabled || doForceDbg - case Nil => doDbg - final def showUids: Bool = debugOverrides.headOption.map(_._2).getOrElse(doShowUids) - def scopedDebug[T](enabled: Bool, showUids: Bool)(thunk: => T): T = - debugOverrides ::= enabled -> showUids - try thunk finally debugOverrides = debugOverrides.tail - /** Restore symbol printing for the continuation outside a definition-local debug annotation. */ - def inOuterDebugScope[T](thunk: => T): T = debugOverrides match - case _ :: outer => - val current = debugOverrides - debugOverrides = outer - try thunk finally debugOverrides = current - case Nil => thunk def dbgUid(uid: Uid[Symbol]): Str = - if dbg && showUids then s"‹$uid›" else "" + if debugContext.showUids then s"‹$uid›" else "" transparent inline def State(using state: State): State = state /** Extracts all parameter lists from a `constructor(...)...` declaration. @@ -813,7 +791,7 @@ extends Importer: val newResolution: Bool = lang.useNewResolution private def debugElaboration[T](annotations: Ls[Annot])(thunk: => T): T = - DebugOutputHandler.traceDefinition(annotations, config, _.elaboration)(thunk)(using state, tl) + tl.traceDefinition(annotations, config)(thunk) lazy val illegalMemberNameTail = msg"Member names must start with a letter or underscore, followed by letters, digits, or underscores." -> N diff --git a/hkmc2/shared/src/main/scala/utils/TraceLogger.scala b/hkmc2/shared/src/main/scala/utils/TraceLogger.scala index 6e73e8fa28..2f815e9a23 100644 --- a/hkmc2/shared/src/main/scala/utils/TraceLogger.scala +++ b/hkmc2/shared/src/main/scala/utils/TraceLogger.scala @@ -8,39 +8,28 @@ type TL = TraceLogger def tl(using TL): TL = summon -abstract class TraceLogger(using val debugPrinter: DebugPrinter): - def doTrace: Bool = true - /** Tracing requested independently of the [[Config.Debug]] settings, such as by diff-test - * commands, which trace a whole phase. Definition-local `@dbg` annotations cannot disable it. - * If the phase is also enabled in [[Config.Debug]], its selected destination is used; - * tracing enabled only by this flag goes to `StdIO`. - * Implementations should include it in [[doTrace]], which also governs code outside annotations. */ - protected def doForceTrace: Bool = false - protected def defaultDebugOutput: Config.DebugOutput = Config.DebugOutput.StdIO - - private var debugOverrides: Ls[(Bool, Config.Debug)] = Nil - - /** Whether tracing is enabled after applying the innermost definition-local override. */ - final def isTracing: Bool = debugOverrides match - case (enabled, _) :: _ => enabled || doForceTrace - case Nil => doTrace - - /** An override that does not enable the phase leaves only forced tracing, which goes to `StdIO`. */ - private def debugOutput: Config.DebugOutput = debugOverrides match - case (true, debug) :: _ => debug.out - case (false, _) :: _ => Config.DebugOutput.StdIO - case Nil => defaultDebugOutput - - final def debugConfig(base: Config): Config = - debugOverrides.headOption.fold(base)(entry => base.copy(debug = entry._2)) +final class TraceLogger( + val debug: DebugContext, + phase: Config.Debug => Bool, + force: Opt[Str] => Bool, + output: (Config.DebugOutput, Str) => Unit, + indentWhenDisabled: Bool, +)(using val debugPrinter: DebugPrinter): + def isTracing: Bool = phase(debug.config) || force(scope) + + /** Commands can force a trace without enabling the phase in source settings. + * Only source settings that enable the phase select a destination other than StdIO. */ + private def debugOutput: Config.DebugOutput = + if phase(debug.config) then debug.config.out else Config.DebugOutput.StdIO - protected val noPostTrace: Any => Str = _ => "" + private val noPostTrace: Any => Str = _ => "" - protected var indent = 0 + private var indent = 0 def trace[T](pre: => Str, post: T => Str = noPostTrace)(thunk: => T): T = log(pre) - enter() - val res = try thunk finally exit() + val indentTrace = indentWhenDisabled || isTracing + if indentTrace then enter() + val res = try thunk finally if indentTrace then exit() if post isnt noPostTrace then log(post(res)) res inline def traceNot[T](pre: => Str, post: T => Str = noPostTrace)(thunk: => T): T = @@ -49,8 +38,7 @@ abstract class TraceLogger(using val debugPrinter: DebugPrinter): inline def enter() = indent += 1 inline def exit() = indent -= 1 - protected[hkmc2] def emitDbg(str: Str): Unit = scala.Predef.println(str) - protected[hkmc2] def emitDbg(str: Str, out: Config.DebugOutput): Unit = emitDbg(str) + private[hkmc2] def emitDbg(str: Str): Unit = output(Config.DebugOutput.StdIO, str) inline def log(msg: => Any): Unit = log(msg, noIndent = false) @@ -58,31 +46,30 @@ abstract class TraceLogger(using val debugPrinter: DebugPrinter): if isTracing then msgs.foreach(log(_)) def log(msg: => Any, noIndent: Bool = false): Unit = - if isTracing then emitDbg( + if isTracing then output( + debugOutput, if noIndent then msg.toString else "| " * indent + msg.toString.indentNewLines("| " * indent + "> "), - debugOutput, ) - /** Temporarily select both tracing enablement and its destination. - * Definition annotations use this to avoid leaking debug settings to sibling definitions. */ - def scopedDebug[T](enabled: Bool, debug: Config.Debug)(thunk: => T): T = - debugOverrides ::= enabled -> debug - try thunk finally debugOverrides = debugOverrides.tail + def traceDefinition[T](annotations: Ls[semantics.Annot], base: Config)(body: => T): T = + debug.traceDefinition(annotations, base, phase)(body) - /** Run a continuation under the debug scope that surrounded the current local scope. */ - def inOuterDebugScope[T](thunk: => T): T = debugOverrides match - case _ :: outer => - val current = debugOverrides - debugOverrides = outer - try thunk finally debugOverrides = current - case Nil => thunk + def traceDefinitionThen[R, T](annotations: Ls[semantics.Annot], base: Config) + (process: (R => T) => T)(k: R => T): T = + debug.traceDefinitionThen(annotations, base, phase)(process)(k) - protected var scope: Opt[Str] = N + private var scope: Opt[Str] = N def scoped[T](flag: Str)(thunk: => T): T = - var oldScope = scope + val oldScope = scope scope = S(flag) try thunk finally scope = oldScope +object TraceLogger: + /** A logger controlled independently of source directives, such as a diff-test command or a pass flag. */ + def apply(enabled: Opt[Str] => Bool, output: Str => Unit)(using DebugPrinter): TraceLogger = + new TraceLogger(new DebugContext, _ => false, enabled, (_, line) => output(line), indentWhenDisabled = true) + + def silent(using DebugPrinter): TraceLogger = apply(_ => false, line => scala.Predef.println(line)) diff --git a/hkmc2DiffTests/src/test/scala/hkmc2/JSBackendDiffMaker.scala b/hkmc2DiffTests/src/test/scala/hkmc2/JSBackendDiffMaker.scala index 039b60dc60..649735bbd0 100644 --- a/hkmc2DiffTests/src/test/scala/hkmc2/JSBackendDiffMaker.scala +++ b/hkmc2DiffTests/src/test/scala/hkmc2/JSBackendDiffMaker.scala @@ -45,27 +45,12 @@ abstract class JSBackendDiffMaker extends MLsDiffMaker: val definitionMetadataNme = baseScp.allocateName(Elaborator.State.definitionMetadataSymbol)(using throw _) val prettyPrintNme = baseScp.allocateName(Elaborator.State.prettyPrintSymbol)(using throw _) - val ltl = new TraceLogger: - override protected def doForceTrace = debugLowering.isSet || scope.exists: - showUCS.get.getOrElse(Set.empty).contains - override def doTrace = doForceTrace || activeDebug.lowering - override protected def defaultDebugOutput: Config.DebugOutput = - if activeDebug.lowering then activeDebug.out else Config.DebugOutput.StdIO - override def emitDbg(str: String): Unit = output(str) - override protected[hkmc2] def emitDbg(str: Str, out: Config.DebugOutput): Unit = - debugOutputHandler.emit(out, str) + val ltl = phaseLogger(_.lowering, + scope => debugLowering.isSet || scope.exists(showUCS.get.getOrElse(Set.empty).contains), indentWhenDisabled = true) - val dtl = new TraceLogger: - override def doTrace = debugOptimizations.isSet || activeDebug.optimizations - override protected def defaultDebugOutput: Config.DebugOutput = - if activeDebug.optimizations then activeDebug.out else Config.DebugOutput.StdIO - override def emitDbg(str: String): Unit = output(str) - override protected[hkmc2] def emitDbg(str: Str, out: Config.DebugOutput): Unit = - debugOutputHandler.emit(out, str) + val dtl = phaseLogger(_.optimizations, _ => debugOptimizations.isSet, indentWhenDisabled = true) - val replTL = new TraceLogger: - override def doTrace = showRepl.isSet - override def emitDbg(str: String): Unit = output(str) + val replTL = TraceLogger(_ => showRepl.isSet, str => output(str)) lazy val host = hostCreated = true diff --git a/hkmc2DiffTests/src/test/scala/hkmc2/MLsDiffMaker.scala b/hkmc2DiffTests/src/test/scala/hkmc2/MLsDiffMaker.scala index fea7b77e3b..36ebcb20cf 100644 --- a/hkmc2DiffTests/src/test/scala/hkmc2/MLsDiffMaker.scala +++ b/hkmc2DiffTests/src/test/scala/hkmc2/MLsDiffMaker.scala @@ -235,7 +235,7 @@ abstract class MLsDiffMaker extends DiffMaker: ln.split(" ").iterator.map(x => "ucs:" + x.trim).toSet - protected var activeDebug = Config.Debug.default + protected lazy val debugContext = new DebugContext(() => dbgParsing.isSet || dbgResolving.isSet || debug.isSet) protected lazy val debugOutputHandler = DebugOutputHandler(cctx.fs, str => output(str)) protected def outputDebugSection(title: Str, contents: Str, out: Config.DebugOutput): Unit = out match @@ -244,18 +244,7 @@ abstract class MLsDiffMaker extends DiffMaker: output(contents) case _ => debugOutputHandler.emit(out, s"${title}\n${contents}") - given Elaborator.State = new Elaborator.State: - override protected def doDbg: Bool = - doForceDbg - || activeDebug.elaboration - || activeDebug.lowering - || activeDebug.optimizations - override protected def doForceDbg: Bool = - dbgParsing.isSet - // || dbgElab.isSet - || dbgResolving.isSet - || debug.isSet - override protected def doShowUids: Bool = activeDebug.showUids + given Elaborator.State = new Elaborator.State(debugContext) protected lazy val dbgScp: utils.Scope = // for unique symbol debug-printing only @@ -285,29 +274,15 @@ abstract class MLsDiffMaker extends DiffMaker: ) - val etl = new TraceLogger: - override protected def doForceTrace = dbgElab.isSet || scope.exists: - showUCS.get.getOrElse(Set.empty).contains - override def doTrace = doForceTrace || activeDebug.elaboration - override protected def defaultDebugOutput: Config.DebugOutput = - if activeDebug.elaboration then activeDebug.out else Config.DebugOutput.StdIO - override def emitDbg(str: String): Unit = output(str) - override protected[hkmc2] def emitDbg(str: Str, out: Config.DebugOutput): Unit = - debugOutputHandler.emit(out, str) - override def trace[T](pre: => Str, post: T => Str = noPostTrace)(thunk: => T): T = - // * This override is for avoiding to increase the indentation when tracing if isTracing is false, - // * so that selectively-enabled tracing doesn't get strange indentation. - // * Perhaps this should be the default behavior of TraceLogger. - if isTracing then super.trace(pre, post)(thunk) - else thunk - - val rtl = new TraceLogger: - override def doTrace = dbgResolving.isSet - override def emitDbg(str: String): Unit = output(str) - - val ftl = new TraceLogger: - override def doTrace = dbgFlow.isSet - override def emitDbg(str: String): Unit = output(str) + protected def phaseLogger(phase: Config.Debug => Bool, force: Opt[Str] => Bool, indentWhenDisabled: Bool): TraceLogger = + new TraceLogger(debugContext, phase, force, (out, line) => debugOutputHandler.emit(out, line), indentWhenDisabled) + + val etl = phaseLogger(_.elaboration, + scope => dbgElab.isSet || scope.exists(showUCS.get.getOrElse(Set.empty).contains), indentWhenDisabled = false) + + val rtl = TraceLogger(_ => dbgResolving.isSet, str => output(str)) + + val ftl = TraceLogger(_ => dbgFlow.isSet, str => output(str)) var curCtx = Elaborator.State.init var curICtx = Resolver.ICtx.empty @@ -430,9 +405,7 @@ abstract class MLsDiffMaker extends DiffMaker: case err: Throwable => uncaught(err) - given tl: TraceLogger with - override def doTrace = debug.isSet - override def emitDbg(str: String): Unit = output(str) + given tl: TraceLogger = TraceLogger(_ => debug.isSet, str => output(str)) def processOrigin(origin: Origin)(using Raise): Unit = @@ -452,7 +425,7 @@ abstract class MLsDiffMaker extends DiffMaker: def doPrintDbg(msg: => Str): Unit = if dbg then output(msg) val res = parser.parseAll(parser.block(allowNewlines = true)) val phaseConfig = Config.elaborationConfig(res, file.up) - activeDebug = phaseConfig.debug + debugContext.configure(phaseConfig.debug) // If parsed tree is displayed, don't show the string serialization. if (parseOnly.isSet || showParse.isSet) && !showParsedTree.isSet && !phaseConfig.debug.showParsedTree then @@ -492,7 +465,7 @@ abstract class MLsDiffMaker extends DiffMaker: def processTrees(trees: Ls[syntax.Tree])(using Config, Raise): Unit = val blk = new syntax.Tree.Block(trees) val elaborationConfig = Config.elaborationConfig(blk, file.up) - activeDebug = elaborationConfig.debug + debugContext.configure(elaborationConfig.debug) // val blockSymbol = // semantics.TopLevelSymbol("block#"+blockNum) blockNum += 1 @@ -538,7 +511,7 @@ abstract class MLsDiffMaker extends DiffMaker: def processTerm(trm: semantics.Term.Blk, inImport: Bool)(using Config, Raise): Unit = given Ctx = curCtx given Config = Config.extractConfigFromStats(trm) - activeDebug = config.debug + debugContext.configure(config.debug) if file.toString =/= runtimeSourceFile.toString && file.toString =/= preludeFile.toString then summon[Elaborator.State].initRuntimeSymbolsFromFile(runtimeSourceFile, prelude)( using summon[TL], summon[Raise], cctx) From 42e92c685679cf5c9f46264cae3163cabbebef38 Mon Sep 17 00:00:00 2001 From: OpenAI Codex Date: Wed, 21 Oct 2026 00:03:28 +0800 Subject: [PATCH 25/32] Move debug rendering out of symbol state and toString Render trace messages with an explicit printer policy, including symbols nested in products and collections. Keep numeric UIDs local to the printer compilation unit, and use disambiguated names for imported symbols. Cover UID scoping and error recovery with diff tests and update the golden outputs. --- doc/reference.md | 1 + .../src/main/scala/hkmc2/CompilerCtx.scala | 6 +- .../src/main/scala/hkmc2/DebugContext.scala | 2 +- .../scala/hkmc2/codegen/BlockSimplifier.scala | 22 +++--- .../hkmc2/codegen/DeadConstructorElim.scala | 2 +- .../scala/hkmc2/codegen/DeadParamElim.scala | 2 +- .../scala/hkmc2/codegen/EtaExpansion.scala | 2 +- .../scala/hkmc2/codegen/HandlerLowering.scala | 2 +- .../main/scala/hkmc2/codegen/Lowering.scala | 10 +-- .../scala/hkmc2/codegen/WorkerWrapper.scala | 2 +- .../hkmc2/codegen/deforest/Deforest.scala | 8 +-- .../scala/hkmc2/codegen/js/JSBuilder.scala | 2 +- .../hkmc2/invalml/ConstraintSolver.scala | 12 ++-- .../main/scala/hkmc2/invalml/InvalML.scala | 8 +-- .../main/scala/hkmc2/invalml/NormalForm.scala | 6 +- .../scala/hkmc2/invalml/TypeSimplifier.scala | 23 ++++--- .../src/main/scala/hkmc2/invalml/types.scala | 4 +- .../scala/hkmc2/semantics/Elaborator.scala | 49 +++++++------ .../main/scala/hkmc2/semantics/Importer.scala | 4 +- .../scala/hkmc2/semantics/NewResolver.scala | 38 +++++------ .../main/scala/hkmc2/semantics/Resolver.scala | 50 +++++++------- .../main/scala/hkmc2/semantics/Shape.scala | 6 +- .../main/scala/hkmc2/semantics/Symbol.scala | 6 +- .../scala/hkmc2/semantics/SymbolPrinter.scala | 14 ++-- .../src/main/scala/hkmc2/semantics/Term.scala | 4 +- .../main/scala/hkmc2/semantics/TermImpl.scala | 35 +++++----- .../hkmc2/semantics/flow/FlowAnalysis.scala | 64 ++++++++--------- .../hkmc2/semantics/ucs/Normalization.scala | 54 +++++++-------- .../scala/hkmc2/semantics/ups/Compiler.scala | 18 ++--- .../semantics/ups/FixedPointCompiler.scala | 6 +- .../hkmc2/semantics/ups/Instantiator.scala | 4 +- .../hkmc2/semantics/ups/SplitCompiler.scala | 8 +-- .../src/main/scala/hkmc2/utils/ReplHost.scala | 6 +- .../src/main/scala/hkmc2/utils/utils.scala | 50 ++++++++++++-- .../src/main/scala/utils/TraceLogger.scala | 34 +++++----- .../mlscript/codegen/DebugDestinations.mls | 4 +- .../test/mlscript/codegen/DebugDirective.mls | 6 +- .../test/mlscript/codegen/DebugScoping.mls | 68 ++++++++++++------- .../src/test/mlscript/codegen/DebugUids.mls | 45 ++++++++++++ .../src/test/mlscript/newres/Functions.mls | 10 +-- .../src/test/mlscript/opt/ConstantFolding.mls | 8 +-- .../src/test/mlscript/parser/Handler.mls | 6 +- .../normalization/ExcessiveDeduplication.mls | 6 +- .../ucs/normalization/ScrutineeIdentity.mls | 4 +- .../mlscript/ups/parametric/EtaConversion.mls | 4 +- .../test/scala/hkmc2/JSBackendDiffMaker.scala | 2 +- .../src/test/scala/hkmc2/MLsDiffMaker.scala | 30 ++++---- 47 files changed, 433 insertions(+), 324 deletions(-) create mode 100644 hkmc2/shared/src/test/mlscript/codegen/DebugUids.mls diff --git a/doc/reference.md b/doc/reference.md index 5352929518..2878e79531 100644 --- a/doc/reference.md +++ b/doc/reference.md @@ -2073,6 +2073,7 @@ The following aspects display a program representation, without tracing: Finally, `showUids` (enabled by default) controls whether traces show the unique identifiers of symbols. Disabling it makes traces easier to compare across compiler runs. +UIDs are local to a compilation unit, so imported symbols use disambiguated names without numeric UIDs. ### Syntax diff --git a/hkmc2/shared/src/main/scala/hkmc2/CompilerCtx.scala b/hkmc2/shared/src/main/scala/hkmc2/CompilerCtx.scala index 9cd2a70f87..2e7a947970 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/CompilerCtx.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/CompilerCtx.scala @@ -73,7 +73,7 @@ class CompilerCtx( val dependencies = new CompilerCtx.DependencyRecorder val modulePath = (file.up / io.RelPath(file.baseName + ".mjs")).toString val debug = new DebugContext - val state = new Elaborator.State(debug) + val state = new Elaborator.State given Elaborator.State = state given Config = rootConfig @@ -249,11 +249,11 @@ class CompilerCtx( art.lastChangedTimestamp >= lastMod, create = val debug = new DebugContext - val state = new Elaborator.State(debug) + val state = new Elaborator.State given Elaborator.State = state given Config = rootConfig given CompilerCtx = this - given DebugPrinter = new DebugPrinter + given DebugPrinter = new DebugPrinter(state) val parse = ParserSetup(file, outputHandler) val elaborationConfig = Config.elaborationConfig(parse.resultBlk, file.up) debug.configure(elaborationConfig.debug) diff --git a/hkmc2/shared/src/main/scala/hkmc2/DebugContext.scala b/hkmc2/shared/src/main/scala/hkmc2/DebugContext.scala index 07dde672c7..8ab286c142 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/DebugContext.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/DebugContext.scala @@ -3,7 +3,7 @@ package hkmc2 import hkmc2.utils.*, shorthands.* -/** Debug settings shared by a compilation unit's symbol printing and phase loggers. +/** Source debug settings shared by a compilation unit's phase loggers. * Imported units have their own context; only their output handler is shared. * Compilation is synchronous within a unit, so scoped changes are restored on the call stack. */ final class DebugContext(forceUids: () => Bool): diff --git a/hkmc2/shared/src/main/scala/hkmc2/codegen/BlockSimplifier.scala b/hkmc2/shared/src/main/scala/hkmc2/codegen/BlockSimplifier.scala index 2b98f54420..25389d7c2c 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/codegen/BlockSimplifier.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/codegen/BlockSimplifier.scala @@ -60,10 +60,10 @@ class BlockSimplifier iteration += 1 if iteration > MaxIterations then - log(s"⬤ Reached maximum number of iterations ($MaxIterations), stopping simplifications") + log(dbg"⬤ Reached maximum number of iterations ($MaxIterations), stopping simplifications") return res - log(s"⬤ Simplif. iter. $iteration") + log(dbg"⬤ Simplif. iter. $iteration") if summon[Config].optimizer.deadCodeElim then // * Running DCE once sometimes produces more DCE opportunities; @@ -113,7 +113,7 @@ class BlockSimplifier var changed = false def registerChange(dbg: => Str)(using Line) = - log(s"!! Change triggered { ${dbg} }") + log(dbg"!! Change triggered { ${dbg} }") // * For debugging: // log(s"!! Change triggered { ${dbg} } at ${summon[FileName].value}:${summon[Line].value}") changed = true @@ -345,7 +345,7 @@ class BlockSimplifier // * Remove useless break case Break(label) if tailLabels.contains(label) => - log(s"Break ${label} is eliminated: current tail label list is ${tailLabels}") + log(dbg"Break ${label} is eliminated: current tail label list is ${tailLabels}") registerChange(s"rm tail-position break ${label.showDbg}") End() @@ -487,7 +487,7 @@ class BlockSimplifier end new - log(s"Captured variables: ${capturedVars}") + log(dbg"Captured variables: ${capturedVars}") cur = applyProgram(prog) @@ -805,7 +805,7 @@ class BlockSimplifier private def showMap: Str = assignedResults .iterator.map: (k, v) => - s"${k.showDbg} -> ${v.toString}" + s"${k.showDbg} -> ${v.showAsPlain}" .mkString("{", ", ", "}") private def canCollapseImmediateCallPrefix( @@ -962,7 +962,7 @@ class BlockSimplifier // * might end up being live even though local flow analysis would think they are not. sub.definedVars.foreach: case sym: LocalVar => - log(s"Variable ${sym.showDbg} is written in a `finally` block; marking it as imprecise tracked") + log(dbg"Variable ${sym.showDbg} is written in a `finally` block; marking it as imprecise tracked") impreciselyTrackedVars += sym case _ => applyBlock(finallyDo) @@ -1031,7 +1031,7 @@ class BlockSimplifier var shapes = if deadBranchRemoval then getShapes(scrut2) else giveUp // TODO: if analysis gave up, make the shapes the set of cases of the patmat, to rm redundant arms - if !gaveUp then log(s"Initial shapes: ${shapes}") + if !gaveUp then log(dbg"Initial shapes: ${shapes}") val oldAssigned = assignedResults var branchAssigneds = List.empty[AssignedResults] @@ -1078,7 +1078,7 @@ class BlockSimplifier shapes.contains(sym) && { shapes -= sym; true } || regChange("Impossible instanceof") case _ => true - if !gaveUp then log(s"Filtered arms: ${arms2.map(_._1)}") + if !gaveUp then log(dbg"Filtered arms: ${arms2.map(_._1)}") val newArms = arms2.mapConserve: case arm @ (cse, body) => @@ -1865,7 +1865,7 @@ class BlockSimplifier override def applyBlock(blk: Block) = blk match case Define(defn: FunDefn, rest) if m.get(defn.dSym).exists(_.canBeInlineEliminated) => - log(s"Inline elimination: ${defn.dSym}") + log(dbg"Inline elimination: ${defn.dSym}") registerChange(s"rm inline-eliminated function ${defn.dSym.showDbg}") applyBlock(rest) case _ => super.applyBlock(blk) @@ -1914,7 +1914,7 @@ class BlockSimplifier super.applyResult(call)(k) case S(matchedArgs) => registerChange(s"inline call ${callee.showDbg}") - log(s"Inline call for ${callee}, with args ${argss}") + log(dbg"Inline call for ${callee}, with args ${argss}") val extraArgss = argss.drop(info.defn.params.length) def go(acc: Block => Block, args: List[(VarSymbol, Result)], mapping: Map[Symbol, Symbol]): Block = args match diff --git a/hkmc2/shared/src/main/scala/hkmc2/codegen/DeadConstructorElim.scala b/hkmc2/shared/src/main/scala/hkmc2/codegen/DeadConstructorElim.scala index 04b0a8a8e2..6982c55934 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/codegen/DeadConstructorElim.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/codegen/DeadConstructorElim.scala @@ -77,7 +77,7 @@ class DeadConstructorElimSolver(val constraintSolver: FlowConstraintSolver, trac if tl.isTracing then tl.log(">>> dead-constructor-elim results >>>") for ctorStr <- deadCtors.toSeq.map(_.exprId.getResult.showDbg).sorted do - tl.log(s"eliminable: $ctorStr") + tl.log(dbg"eliminable: $ctorStr") tl.log("<<< dead-constructor-elim results <<<") end if end DeadConstructorElimSolver diff --git a/hkmc2/shared/src/main/scala/hkmc2/codegen/DeadParamElim.scala b/hkmc2/shared/src/main/scala/hkmc2/codegen/DeadParamElim.scala index 5d45c2187c..3c9e3d2b08 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/codegen/DeadParamElim.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/codegen/DeadParamElim.scala @@ -121,7 +121,7 @@ class DeadParamElimSolver(val constraintSolver: FlowConstraintSolver, traceLogge assert(eliminableCallSiteArgsById.nonEmpty === eliminableParamsById.nonEmpty) tl.log(">>> dead-param-elim results >>>") for (id, elim) <- eliminableParamsById do - tl.log(s"${id.showConcreteFunId} -> eliminable: {${elim.toSeq.sorted.mkString(", ")}}") + tl.log(dbg"${id.showConcreteFunId} -> eliminable: {${elim.toSeq.sorted.mkString(", ")}}") tl.log("<<< dead-param-elim results <<<") end if end DeadParamElimSolver diff --git a/hkmc2/shared/src/main/scala/hkmc2/codegen/EtaExpansion.scala b/hkmc2/shared/src/main/scala/hkmc2/codegen/EtaExpansion.scala index fc1f77b3e5..9ca175614c 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/codegen/EtaExpansion.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/codegen/EtaExpansion.scala @@ -145,7 +145,7 @@ class EtaExpansionSolver(val constraintSolver: FlowConstraintSolver, tl: TraceLo tl.log(">>> eta-expansion targets shapes >>>") for (id, shape) <- etaExpandedFunShape do - tl.log(s"${id.showConcreteFunId}: ${shape.map(_.ppParamInfo).mkString("[", ", ", "]")}") + tl.log(dbg"${id.showConcreteFunId}: ${shape.map(_.ppParamInfo).mkString("[", ", ", "]")}") tl.log("<<< eta-expansion targets shapes <<<") end if end EtaExpansionSolver diff --git a/hkmc2/shared/src/main/scala/hkmc2/codegen/HandlerLowering.scala b/hkmc2/shared/src/main/scala/hkmc2/codegen/HandlerLowering.scala index e3e3cb5044..8162618db5 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/codegen/HandlerLowering.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/codegen/HandlerLowering.scala @@ -588,7 +588,7 @@ class HandlerLowering(paths: HandlerPaths, opt: Opt[EffectHandlers])(using TL, R // without many special handling. val newCtor = if opt.fold(true)(_.doNotInstrumentTopLevelModCtor) && !h.currentBlockIsTrulyNested then bod.ctor else translateCtorLike(bod.ctor, bod.isym.asThis, true) - tl.log(s"companion name: ${bod.isym.nme}") + tl.log(dbg"companion name: ${bod.isym.nme}") if (bod.methods is newMtds) && (bod.ctor is newCtor) then bod else diff --git a/hkmc2/shared/src/main/scala/hkmc2/codegen/Lowering.scala b/hkmc2/shared/src/main/scala/hkmc2/codegen/Lowering.scala index 1317b13007..c167b39926 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/codegen/Lowering.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/codegen/Lowering.scala @@ -130,7 +130,7 @@ class Lowering()(using Config, TL, Raise, State, Ctx, SymbolPrinter)(using Erasu extension (t: Term) def instantiated = t match case r: Resolvable => - tl.trace[Term](s"Expanding term ${r}", post = t => s"~> ${t}"): + tl.trace[Term](dbg"Expanding term ${r}", post = t => dbg"~> ${t}"): r.expanded case t => t @@ -878,7 +878,7 @@ class Lowering()(using Config, TL, Raise, State, Ctx, SymbolPrinter)(using Erasu FlowSymbol(sym.nme) ).resolve val (paramLists, bodyBlock) = setupFunctionDef(ps :: Nil, bod, S(sym.nme), N) - tl.log(s"Ref builtin $sym") + tl.log(dbg"Ref builtin $sym") assert(paramLists.length === 1) return k(Lambda(paramLists.head, bodyBlock)(Nil, ref.toLoc)) if sym.unary then @@ -893,7 +893,7 @@ class Lowering()(using Config, TL, Raise, State, Ctx, SymbolPrinter)(using Erasu FlowSymbol(sym.nme) ).resolve val (paramLists, bodyBlock) = setupFunctionDef(ps :: Nil, bod, S(sym.nme), N) - tl.log(s"Ref builtin $sym") + tl.log(dbg"Ref builtin $sym") assert(paramLists.length === 1) return k(Lambda(paramLists.head, bodyBlock)(Nil, ref.toLoc)) case bs: BlockMemberSymbol => @@ -948,9 +948,9 @@ class Lowering()(using Config, TL, Raise, State, Ctx, SymbolPrinter)(using Erasu @tailrec final def term(t: st, inStmtPos: Bool = false)(k0: Result => Block)(using LoweringCtx): Block = - tl.log(s"Lowering.term ${t.showDbg.truncate(100, "[...]")}${ + tl.log(dbg"Lowering.term ${t.showDbg.truncate(100, "[...]")}${ if inStmtPos then " (in stmt)" else ""}${ - t.resolvedSym.fold("")(" – symbol " + _)}") + t.resolvedSym.fold("")(sym => " – symbol " + sym.showDbg)}") def warnStmt = if inStmtPos then warnPureExprInStmtPos(t.toLoc, S(t)) diff --git a/hkmc2/shared/src/main/scala/hkmc2/codegen/WorkerWrapper.scala b/hkmc2/shared/src/main/scala/hkmc2/codegen/WorkerWrapper.scala index d753a369e5..256a14715d 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/codegen/WorkerWrapper.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/codegen/WorkerWrapper.scala @@ -88,7 +88,7 @@ class WorkerWrapper case Annot.NoInline() | Annot.Generator() => false case _ => true )) - log(s"▶ Worker-wrapper: ${fun.dSym.showDbg} -> ${worker.dSym.showDbg}") + log(dbg"▶ Worker-wrapper: ${fun.dSym.showDbg} -> ${worker.dSym.showDbg}") Scoped(Set.single(worker.sym), Define(wrapper, Define(worker, rest))) private def mkInlineOnly(fun: FunDefn, body: Block): FunDefn = diff --git a/hkmc2/shared/src/main/scala/hkmc2/codegen/deforest/Deforest.scala b/hkmc2/shared/src/main/scala/hkmc2/codegen/deforest/Deforest.scala index 502c013be7..86077c27db 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/codegen/deforest/Deforest.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/codegen/deforest/Deforest.scala @@ -125,13 +125,13 @@ class DeforestFusionSolver(val constraintSolver: FlowConstraintSolver)(using Con tl.log(">>> fusing >>>") for case (c, dest) <- finalCtorDests do - tl.log(s"${pp(c)} ->") + tl.log(dbg"${pp(c)} ->") dest match case FinalDestMatch(dtor, sels) => - tl.log(s"\tmatch: ${pp(dtor)}") - for s <- sels.toSeq.sortBy(_.exprId.uid) do tl.log(s"\tfields: ${pp(s)}") + tl.log(dbg"\tmatch: ${pp(dtor)}") + for s <- sels.toSeq.sortBy(_.exprId.uid) do tl.log(dbg"\tfields: ${pp(s)}") case FinalDestSel(dtors, field) => - tl.log(s"\tselect: ${pp(field)}") + tl.log(dbg"\tselect: ${pp(field)}") tl.log("<<< fusing <<<") end DeforestFusionSolver diff --git a/hkmc2/shared/src/main/scala/hkmc2/codegen/js/JSBuilder.scala b/hkmc2/shared/src/main/scala/hkmc2/codegen/js/JSBuilder.scala index def68a1bad..8ef8616207 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/codegen/js/JSBuilder.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/codegen/js/JSBuilder.scala @@ -944,7 +944,7 @@ class JSBuilder(using Config, TL, State, Ctx) extends CodeBuilder: * but the result has the same semantics. * */ def reserveNames(p: Program)(using Scope, Raise): Unit = - def go(blk: Block): Unit = tl.trace(s"avoidNames ${blk.toString.take(100)}..."): + def go(blk: Block): Unit = tl.trace(dbg"avoidNames ${blk.showAsPlain.take(100)}..."): blk match case Define(defn, rest) => defn match diff --git a/hkmc2/shared/src/main/scala/hkmc2/invalml/ConstraintSolver.scala b/hkmc2/shared/src/main/scala/hkmc2/invalml/ConstraintSolver.scala index 16a4c0452b..8275e9d9f8 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/invalml/ConstraintSolver.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/invalml/ConstraintSolver.scala @@ -41,7 +41,7 @@ class ConstraintSolver(infVarState: InfVarUid.State, elState: Elaborator.State, InfVar(lvl, infVarState.nextUid, new VarState(), false)(InstSymbol(sym)(using elState), hint) def extrude(ty: Type)(using lvl: Int, pol: Bool, cache: ExtrudeCache, invalctx: InvalCtx, cctx: CCtx, tl: TL): Type = - trace[Type](s"Extruding[${printPol(pol)}] ${ty.showDbg}", r => s"~> ${r.showDbg}"): + trace[Type](dbg"Extruding[${printPol(pol)}] ${ty.showDbg}", r => s"~> ${r.showDbg}"): if ty.lvl <= lvl then ty else ty.toBasic/*TODO improve extrude directly*/ match case ClassLikeType(sym, targs) => ClassLikeType(sym, targs.map { @@ -78,7 +78,7 @@ class ConstraintSolver(infVarState: InfVarUid.State, elState: Elaborator.State, case NegType(ty) => Type.mkNegType(extrude(ty)(using lvl, !pol)) case Top | Bot => ty - private def constrainConj(conj: Conj)(using InvalCtx, CCtx, TL): Unit = trace(s"Constraining ${conj.showDbg}"): + private def constrainConj(conj: Conj)(using InvalCtx, CCtx, TL): Unit = trace(dbg"Constraining ${conj.showDbg}"): conj match case Conj(i, u, (v, pol) :: tail) => var rest = Conj(i, u, tail) @@ -87,13 +87,13 @@ class ConstraintSolver(infVarState: InfVarUid.State, elState: Elaborator.State, val bd = if v.lvl >= rest.lvl then rest else extrude(rest)(using v.lvl, true, mutable.HashMap.empty) if pol then val nc = Type.mkNegType(bd).toDnf // always cache the normal form to avoid unexpected cache misses - log(s"New bound: ${v.showDbg} <: ${nc.showDbg}") + log(dbg"New bound: ${v.showDbg} <: ${nc.showDbg}") cctx.nest(v.toDnf -> nc) givenIn: v.state.upperBounds ::= nc v.state.lowerBounds.foreach(lb => constrainImpl(lb, nc)) else val c = bd.toDnf // always cache the normal form to avoid unexpected cache misses - log(s"New bound: ${v.showDbg} :> ${c.showDbg}") + log(dbg"New bound: ${v.showDbg} :> ${c.showDbg}") cctx.nest(c -> v.toDnf) givenIn: v.state.lowerBounds ::= c v.state.upperBounds.foreach(ub => constrainImpl(c, ub)) @@ -138,8 +138,8 @@ class ConstraintSolver(infVarState: InfVarUid.State, elState: Elaborator.State, private def constrainImpl(lhs: Type, rhs: Type)(using InvalCtx, CCtx, TL): Unit = val p = lhs.toDnf -> rhs.toDnf - if cctx.cache(p) then log(s"Cached!") - else trace(s"CONSTRAINT ${lhs.showDbg} <: ${rhs.showDbg}"): + if cctx.cache(p) then log(dbg"Cached!") + else trace(dbg"CONSTRAINT ${lhs.showDbg} <: ${rhs.showDbg}"): cctx.nest(p) givenIn: val ty = dnf(inlineSkolemBounds(lhs & rhs.!, true)(using Set.empty)) constrainDNF(ty) diff --git a/hkmc2/shared/src/main/scala/hkmc2/invalml/InvalML.scala b/hkmc2/shared/src/main/scala/hkmc2/invalml/InvalML.scala index 4614e2518f..5aaba70cb4 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/invalml/InvalML.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/invalml/InvalML.scala @@ -124,13 +124,13 @@ class InvalTyper(using codegen.Erasure)(using elState: Elaborator.State, tl: TL) private def typeAndSubstType (ty: Term, pol: Bool)(using map: Map[Uid[Symbol], TypeArg])(using ctx: InvalCtx, cctx: CCtx) : GeneralType = - trace[GeneralType](s"${ctx.lvl}. Typing type ${ty.showDbg}", r => s"~> ${r.showDbg}"): + trace[GeneralType](dbg"${ctx.lvl}. Typing type ${ty.showDbg}", r => s"~> ${r.showDbg}"): def mono(ty: Term, pol: Bool): Type = monoOrErr(typeAndSubstType(ty, pol), ty) ty match case Capture(base, _) => typeAndSubstType(base, pol) case Ref(sym: LocalSymbol) => - log(s"Type lookup: ${sym.nme} ${sym.uid} ${map.keySet}") + log(dbg"Type lookup: ${sym.nme} ${sym.uid} ${map.keySet}") map.get(sym.uid) match case Some(Wildcard(in, out)) => if pol then out else in case Some(ty: Type) => ty @@ -492,7 +492,7 @@ class InvalTyper(using codegen.Erasure)(using elState: Elaborator.State, tl: TL) // * Note: currently, the returned type is not used or useful, but it could be in the future private def ascribe(lhs: Term, rhs: GeneralType)(using ctx: InvalCtx, scope: Scope): (GeneralType, Type) = - trace[(GeneralType, Type)](s"${ctx.lvl}. Ascribing ${lhs.showDbg} : ${rhs.showDbg}", res => s"! ${res._2.showDbg}"): + trace[(GeneralType, Type)](dbg"${ctx.lvl}. Ascribing ${lhs.showDbg} : ${rhs.showDbg}", res => s"! ${res._2.showDbg}"): given CCtx = CCtx.init(lhs, S(rhs)) (lhs, rhs) match case (Term.Lam(PlainParamList(params), body), ft @ PolyFunType(args, ret, eff)) => // * annoted functions @@ -619,7 +619,7 @@ class InvalTyper(using codegen.Erasure)(using elState: Elaborator.State, tl: TL) }, typeAndSubstType(resTy, true)(using map.toMap), Bot)) private def typeCheck(t: Term)(using ctx: InvalCtx, scope: Scope): (GeneralType, Type) = - trace[(GeneralType, Type)](s"${ctx.lvl}. Typing ${t.showDbg}", res => s": (${res._1.showDbg}, ${res._2.showDbg})"): + trace[(GeneralType, Type)](dbg"${ctx.lvl}. Typing ${t.showDbg}", res => s": (${res._1.showDbg}, ${res._2.showDbg})"): given CCtx = CCtx.init(t, N) t match case Term.Annotated(Annot.Untyped(), _) => (Bot, Bot) diff --git a/hkmc2/shared/src/main/scala/hkmc2/invalml/NormalForm.scala b/hkmc2/shared/src/main/scala/hkmc2/invalml/NormalForm.scala index ae2bb5b3c0..adc7e223f1 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/invalml/NormalForm.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/invalml/NormalForm.scala @@ -150,7 +150,7 @@ sealed abstract class NormalForm extends TypeExt: object NormalForm: def inter(lhs: Disj, rhs: Disj)(using TL): Disj = - tl.traceNot[Disj](s"inter ${lhs.showDbg} and ${rhs.showDbg}", r => s"= ${r.showDbg}"): + tl.traceNot[Disj](dbg"inter ${lhs.showDbg} and ${rhs.showDbg}", r => s"= ${r.showDbg}"): if lhs.isBot || rhs.isBot then Disj.bot else Disj(lhs.conjs.flatMap(lhs => rhs.conjs.flatMap(rhs => lhs.merge(rhs) match { case S(conj) => conj :: Nil @@ -160,7 +160,7 @@ object NormalForm: def union(lhs: Disj, rhs: Disj): Disj = Disj(lhs.conjs ++ rhs.conjs) def neg(ty: Type)(using TL): Disj = - tl.traceNot[Disj](s"~DNF ${ty.showDbg} ${ty.getClass} ${ty.toBasic.showDbg}", r => s"= ${r.showDbg}"): + tl.traceNot[Disj](dbg"~DNF ${ty.showDbg} ${ty.getClass} ${ty.toBasic.showDbg}", r => s"= ${r.showDbg}"): ty match case u: Union => Disj(Conj(Inter(N), u, Nil) :: Nil) case _ => ty.toBasic match @@ -174,7 +174,7 @@ object NormalForm: case NegType(ty) => dnf(ty) def dnf(ty: Type)(using TL): Disj = - tl.traceNot[Disj](s"DNF ${ty.showDbg} ${ty.getClass}", r => s"= ${r.showDbg}"): + tl.traceNot[Disj](dbg"DNF ${ty.showDbg} ${ty.getClass}", r => s"= ${r.showDbg}"): ty match case d: Disj => d case c: Conj => Disj(c :: Nil) diff --git a/hkmc2/shared/src/main/scala/hkmc2/invalml/TypeSimplifier.scala b/hkmc2/shared/src/main/scala/hkmc2/invalml/TypeSimplifier.scala index 40b046bdb9..16fd354183 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/invalml/TypeSimplifier.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/invalml/TypeSimplifier.scala @@ -5,6 +5,7 @@ import scala.collection.immutable.{SortedMap, SortedSet} import scala.util.boundary, boundary.break import scala.util.chaining._ +import hkmc2.dbg import hkmc2.utils.*, shorthands.* import hkmc2.utils.* @@ -29,7 +30,7 @@ class TypeSimplifier(tl: TraceLogger): tv.state.upperBounds = tv.state.upperBounds.map(goType) tv def goType(ty: Type): Type = - trace[Type](s"simplifyStructure(${ty.showDbg})", r => s"= ${r.showDbg}"): + trace[Type](dbg"simplifyStructure(${ty.showDbg})", r => s"= ${r.showDbg}"): ty match case tv: InfVar => goTV(tv) case _ => @@ -75,19 +76,19 @@ class TypeSimplifier(tl: TraceLogger): } override def apply(pol: Bool)(ty: GeneralType): Unit = boundary: - trace(s"Analyse[${printPol(pol)}] ${ty.showDbg} [${curPath.reverseIterator.mkString(" ~> ")}]"): + trace(dbg"Analyse[${printPol(pol)}] ${ty.showDbg} [${curPath.reverseIterator.map(_.showDbg).mkString(" ~> ")}]"): ty match case ty if ty.lvl <= lvl => - log(s"Level is < $lvl") + log(dbg"Level is < $lvl") case tv: IV if { occsNum(tv) = occsNum.getOrElse(tv, 0) + 1; false } => case tv: IV => - if varSubst.contains(tv) then break(log(s"Already subst'd")) // * If the IV was set to be substituted, it means it's been found recursive and we don't need to traverse it again + if varSubst.contains(tv) then break(log(dbg"Already subst'd")) // * If the IV was set to be substituted, it means it's been found recursive and we don't need to traverse it again var continue = true // if (!traversedTVs.contains(tv)) { if curPath.exists(_ is tv) then // TODO opt traversedTVs += tv val recPrefix = curPath.takeWhile(_ isnt tv) - log(s"UNIFYING ${tv.showDbg} with ${recPrefix.mkString(", ")}") + log(dbg"UNIFYING ${tv.showDbg} with ${recPrefix.map(_.showDbg).mkString(", ")}") recPrefix.foreach: tv2 => if tv2 isnt tv then traversedTVs += tv2 @@ -102,9 +103,9 @@ class TypeSimplifier(tl: TraceLogger): else if tvRepr.lvl > lvl && !varSubst.contains(tvRepr) then varSubst += tvRepr -> tv continue = false // TODO else?? - if traversedTVs.contains(tv) then log(s"Now already traversed ${tv.showDbg}") + if traversedTVs.contains(tv) then log(dbg"Now already traversed ${tv.showDbg}") else if pastPathsSet.contains(tv) then - log(s"REC ${tv.showDbg}") + log(dbg"REC ${tv.showDbg}") recVars += tv continue = false if continue then @@ -155,7 +156,7 @@ class TypeSimplifier(tl: TraceLogger): pastPathsSet --= oldPath () - trace(s"Simplifying type ${ty.showDbg}"): + trace(dbg"Simplifying type ${ty.showDbg}"): Analysis(pol)(ty) log("Unif-pre: " + Analysis.varSubst) @@ -172,15 +173,15 @@ class TypeSimplifier(tl: TraceLogger): val traversed: MutSet[IV] = MutSet.empty val transformed: MutMap[IV, Type] = MutMap.empty - def subst(ty: GeneralType): GeneralType = trace[GeneralType](s"subst(${ty.showDbg})", r => s"= ${r.showDbg}"): + def subst(ty: GeneralType): GeneralType = trace[GeneralType](dbg"subst(${ty.showDbg})", r => s"= ${r.showDbg}"): ty match case ty if ty.lvl <= lvl => ty // TODO NOPE case _tv: IV => val tv = Analysis.getRepr(_tv) - log(s"Repr: ${tv.showDbg}") + log(dbg"Repr: ${tv.showDbg}") transformed.getOrElseUpdate(tv, { if Analysis.recVars.contains(tv) then - log(s"It's recursive!") + log(dbg"It's recursive!") transformed += tv -> tv else transformed += tv -> // TypeBounds(TopType, BotType)(noProv) // TODO improve? creates lots of junk... diff --git a/hkmc2/shared/src/main/scala/hkmc2/invalml/types.scala b/hkmc2/shared/src/main/scala/hkmc2/invalml/types.scala index 7004f877de..9b8f5ad29c 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/invalml/types.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/invalml/types.scala @@ -325,7 +325,7 @@ case class PolyType(tvs: Ls[InfVar], outer: Opt[InfVar], body: GeneralType) exte // * `outer` is already skolemized when it is declared val map = tvs.map(v => val sk = InfVar(lvl, nextUid, new VarState(), true)(v.sym, v.hint) - tl.log(s"skolemize ${v.showDbg} ~> ${sk.showDbg}") + tl.log(dbg"skolemize ${v.showDbg} ~> ${sk.showDbg}") v.uid -> sk ).toMap substAndGetBody(using map) @@ -333,7 +333,7 @@ case class PolyType(tvs: Ls[InfVar], outer: Opt[InfVar], body: GeneralType) exte def instantiate(nextUid: => Uid[InfVar], env: InfVar, lvl: Int)(tl: TL)(using State): GeneralType = val map = (outer.map(_.uid -> env).toList ++ tvs.map(v => val nv = InfVar(lvl, nextUid, new VarState(), false)(new InstSymbol(v.sym), v.hint) - tl.log(s"instantiate ${v.showDbg} ~> ${nv.showDbg}") + tl.log(dbg"instantiate ${v.showDbg} ~> ${nv.showDbg}") v.uid -> nv )).toMap substAndGetBody(using map) diff --git a/hkmc2/shared/src/main/scala/hkmc2/semantics/Elaborator.scala b/hkmc2/shared/src/main/scala/hkmc2/semantics/Elaborator.scala index 482deaacf6..9298094cba 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/semantics/Elaborator.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/semantics/Elaborator.scala @@ -631,8 +631,7 @@ object Elaborator: case Default(sym: ImportSymbol, modulePath: Str) case Private(sym: BlockMemberSymbol, modulePath: Str) - class State(val debugContext: DebugContext): - def this() = this(new DebugContext) + class State: val newResolverState: NewResolverState = new NewResolverState(this) val suid = new Uid.Symbol.State given State = this @@ -728,8 +727,6 @@ object Elaborator: "globalThis" -> globalThisSymbol, )) val superSymbol = builtinOpsMap("super") - def dbgUid(uid: Uid[Symbol]): Str = - if debugContext.showUids then s"‹$uid›" else "" transparent inline def State(using state: State): State = state /** Extracts all parameter lists from a `constructor(...)...` declaration. @@ -1024,7 +1021,7 @@ extends Importer: Head.Let(binding, term) protected def mkMatch(scrutinee: Term, pattern: Pattern, consequent: SimpleSplit) = - log(s"mkMatch: scrutinee = ${scrutinee.showDbg}, pattern = ${pattern.showDbg}, consequent = ${consequent.showDbg}") + log(dbg"mkMatch: scrutinee = ${scrutinee.showDbg}, pattern = ${pattern.showDbg}, consequent = ${consequent.showDbg}") matchScrutPat(scrutinee, pattern) // Captures carry the scope crossings needed to resolve extracted bindings, // but matching the value at runtime only needs the underlying reference. @@ -1257,7 +1254,7 @@ extends Importer: // * In the new syntax, only some positions do (see `delimitedTerm`), and placeholders elsewhere are illegal. def term(tree: Tree, interp: Interpretation): Ctxl[Term] = if lang.newUnderscoreAndApplicationSyntax then - trace[Term](s"Elab term ${tree.showDbg}", r => s"~> ${r.showDbg}"): + trace[Term](dbg"Elab term ${tree.showDbg}", r => s"~> ${r.showDbg}"): given UnderCtx = UnderCtx.none subterm(tree, interp) else delimitedTerm(tree, interp) @@ -1265,7 +1262,7 @@ extends Importer: /** Elaborate `tree` as a term that delimits an implicit lambda, whose parameters are the `_` placeholders * of `tree` that are not delimited by one of its subterms, as in the body of `=> _ + 1`. */ def delimitedTerm(tree: Tree, interp: Interpretation): Ctxl[Term] = - trace[Term](s"Elab term ${tree.showDbg}", r => s"~> ${r.showDbg}"): + trace[Term](dbg"Elab term ${tree.showDbg}", r => s"~> ${r.showDbg}"): val unders = mutable.ArrayBuffer.empty[VarSymbol] given UnderCtx = UnderCtx(S(unders)) val st = subterm(tree, interp) @@ -1397,7 +1394,7 @@ extends Importer: // Most recursive operands are terms. Transparent wrappers must explicitly // forward their interpretation; class, pattern, and type operands specify theirs. def subterm(tree: Tree, interp: Interpretation = Trm): Ctxl[UnderCtx ?=> Term] = - trace[Term](s"Elab subterm ${tree.showDbg}", r => s"~> ${r.showDbg}"): + trace[Term](dbg"Elab subterm ${tree.showDbg}", r => s"~> ${r.showDbg}"): def error = Term.Error().withLocOf(tree) @@ -1530,7 +1527,7 @@ extends Importer: case (hd @ Hndl(id: Ident, c, Block(sts_), S(bod))) => ctx.nest(OuterCtx.LambdaOrHandlerBlock).givenIn: val sym = VarSymbol(id, erasedType = N) - log(s"Processing `handle` statement $id (${sym}) ${ctx.outer}") + log(dbg"Processing `handle` statement $id (${sym}) ${ctx.outer}") val derivedClsSym = ClassSymbol(Tree.DummyTypeDef(syntax.Cls), Tree.Ident(s"Handler$$${id.name}$$")) recordDefinitionScope(derivedClsSym) @@ -1913,7 +1910,7 @@ extends Importer: case PrefixApp(kw @ Keywrd(Keyword.`return`), body) => ctx.getRetHandler match case ReturnHandler.Required(sym) => - log(s"Non-local return: $sym") + log(dbg"Non-local return: $sym") if !ctx.potentiallyInstrumented then raise: ErrorReport(msg"Non-local return statements are only supported with effect handlers enabled." -> tree.toLoc :: Nil) @@ -2127,7 +2124,7 @@ extends Importer: def blockOrRcd(blk: Block, hasResult: Bool, resultInterp: Interpretation)(using UnderCtx) : Ctxl[(Blk | Rcd, Ctx)] = trace[(Blk | Rcd, Ctx)]( - pre = s"Elab block ${blk.desugStmts.toString.truncate(100, "[...]")} ${ctx.outer}", r => s"~> ${r._1.showDbg}" + pre = dbg"Elab block ${blk.desugStmts.showAsPlain.truncate(100, "[...]")} ${ctx.outer}", r => s"~> ${r._1.showDbg}" ): val members = blk.definedSymbols.toMap @@ -2162,7 +2159,7 @@ extends Importer: raise(ErrorReport(msg"Multiple definitions of symbol '$name'" -> N :: group.map(msg"defined here" -> _.toLoc))) val mainDefn = group.head // * Safe since these `groupMapReduce` groups cannot be empty - log(s"Processing overloadings for '$name'") + log(dbg"Processing overloadings for '$name'") defns.iterator.foreach: defn => if defn.k > k then // Only function definitions with parameters create a fresh companion object. @@ -2376,7 +2373,7 @@ extends Importer: if tups.isEmpty || id.name.headOption.exists(_.isLower) => val sym = fieldOrVarSym(LetBind, id) - log(s"Processing `let` statement $id (${sym}) ${ctx.outer}") + log(dbg"Processing `let` statement $id (${sym}) ${ctx.outer}") members.get(id.name).foreach: s => raise(ErrorReport(msg"Name '${id.name}' is already used" -> hd.toLoc :: msg"by a member declared in the same block" -> s.toLoc :: Nil)) @@ -2458,7 +2455,7 @@ extends Importer: val elaborateDefinition: Ctx ?=> TermDefinition = newCtx ?=> - log(s"Processing term definition $nme") + log(dbg"Processing term definition $nme") // * Add type parameters to context val (tps, newCtx1) = td.typeParams match case S(t) => @@ -2749,8 +2746,8 @@ extends Importer: // 2. extraction bindings, e.g., `value` in `pattern Middle(value) = ...`; and // 3. the rest are reported as invalid parameters. val (patternParams, extractionParams) = allParams.partition(_.flags.pat) - log(s"`${patSym.nme}`'s pattern parameters: ${patternParams.mkString("[", ", ", "]")}") - log(s"`${patSym.nme}`'s extraction parameters: ${extractionParams.mkString("[", ", ", "]")}") + log(dbg"`${patSym.nme}`'s pattern parameters: ${patternParams.map(_.showDbg).mkString("[", ", ", "]")}") + log(dbg"`${patSym.nme}`'s extraction parameters: ${extractionParams.map(_.showDbg).mkString("[", ", ", "]")}") // Empty pattern body is considered as wildcard patterns. val rhs = td.rhs.getOrElse: raise(ErrorReport(msg"Pattern definitions must have a body." -> statementLoc :: Nil)) @@ -2772,8 +2769,8 @@ extends Importer: case N if !guardedNames.contains(name) => raise(WarningReport(msg"Unused pattern binding: $name." -> aliases.head.toLoc :: Nil)) case _ => () - scoped("ucs:ups")(log(s"elaborated pattern body: ${pat.showDbg}")) - scoped("ucs:ups:tree")(log(s"elaborated pattern body: ${pat.showAsTree}")) + scoped("ucs:ups")(log(dbg"elaborated pattern body: ${pat.showDbg}")) + scoped("ucs:ups:tree")(log(dbg"elaborated pattern body: ${pat.showAsTree}")) // `paramsOpt` is set to `N` because we don't want parameters to // appear in the generated class's constructor. val pd = PatternDef(owner, patSym, sym, tps, allParams, @@ -2784,13 +2781,13 @@ extends Importer: case k: (Mod.type | Obj.type) => val modSym = td.symbol.asInstanceOf[ModuleOrObjectSymbol] // TODO: improve `asInstanceOf` newCtx.givenIn: - trace(s"Processing module/object definition $nme"): + trace(dbg"Processing module/object definition $nme"): val comp = sym.asCls match case comp @ S(_) => assert(sym.asAls.isEmpty) comp case N => sym.asAls - log(s"Companion: ${comp}") + log(dbg"Companion: ${comp}") val md = val (bod, c) = mkBody(Nil) ModuleOrObjectDef(owner, modSym, sym, @@ -2823,9 +2820,9 @@ extends Importer: msg"Spread parameters are not supported in class parameters." -> rp.toLoc :: Nil)) res.copy(restParam = N)(res.toLoc) newCtx.givenIn: - trace(s"Processing class definition $nme"): + trace(dbg"Processing class definition $nme"): val comp = sym.asMod - log(s"Companion: ${comp}") + log(dbg"Companion: ${comp}") val allCtorPss = pss ::: auxCtorPss val tsym = if allCtorPss.nonEmpty then val ctsym = ClassCtorSymbol(Fun, owner, clsSym) @@ -2986,7 +2983,7 @@ extends Importer: def param(t: Tree, inUsing: Bool, inDataClass: Bool): Ctxl[Diagnostic \/ (Param, Opt[SpreadKind], Ls[Str])] = t.desugared.asParam(inUsing).map: case pt @ ParamTree(flags, id, sign, spd, modifiers) => - log(s"Elaborating ParamTree: ${pt}") + log(dbg"Elaborating ParamTree: ${pt}") val flg = flags.copy(isVal = flags.isVal || inDataClass) val (canonicalId, aliases) = symbolicSuffixBase(id.name) match case S(base) => @@ -3104,7 +3101,7 @@ extends Importer: * FIXME: This routine is insufficient to look up definitions defined * later in the program. */ def ident(id: Ident)(using Ctx): Ctxl[Opt[Term]] = scoped("ucs:pattern:resolution"): - log(s"resolve ${id}") + log(dbg"resolve ${id}") ctx.get(id.name) match case S(elem) => log("has elem!") @@ -3194,7 +3191,7 @@ extends Importer: def arg(t: Tree): Ctxl[Pattern \/ Pattern] = t match case TypeDef(syntax.Pat, body, N) => L(go(body)) case _ => R(go(t)) - def go(t: Tree): Ctxl[Pattern] = trace[Pattern](s"Elab pattern ${t.showDbg}", r => s"~> ${r.showDbg}"): + def go(t: Tree): Ctxl[Pattern] = trace[Pattern](dbg"Elab pattern ${t.showDbg}", r => s"~> ${r.showDbg}"): t match // Annotated patterns like `@compile P`. case Tree.Annotated(annotation, target) => @@ -3385,7 +3382,7 @@ extends Importer: TODO(sym->sym.uid) // case S(sym) => ??? case N => - log(s"No symbol found $lhs ${lhs.symbol}") + log(dbg"No symbol found $lhs ${lhs.symbol}") // ??? () // TODO case Term.Ref(sym: VarSymbol) => diff --git a/hkmc2/shared/src/main/scala/hkmc2/semantics/Importer.scala b/hkmc2/shared/src/main/scala/hkmc2/semantics/Importer.scala index 8a1b5046da..f2ded6d33d 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/semantics/Importer.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/semantics/Importer.scala @@ -31,7 +31,7 @@ class Importer extends NewResolver: lazy val sym = VarSymbol(id, erasedType = N) if path.startsWith(".") || path.startsWith("/") then // leave alone imports like "fs" - log(s"importing $file") + log(dbg"importing $file") val nme = file.baseName val id = new syntax.Tree.Ident(nme) // TODO loc @@ -54,7 +54,7 @@ class Importer extends NewResolver: reportCycle(cctx.allFilesBeingImported :+ file) else cctx.withActiveDependency(file)(reportCycle): - val importedSym = tl.trace(s">>> Importing $file"): + val importedSym = tl.trace(dbg">>> Importing $file"): given TL = tl val artifact = cctx.getElaboratedBlock(file, prelude) artifact.compilationUnit.defaultExport.getOrElse: diff --git a/hkmc2/shared/src/main/scala/hkmc2/semantics/NewResolver.scala b/hkmc2/shared/src/main/scala/hkmc2/semantics/NewResolver.scala index 7a3f4db078..0e86315ace 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/semantics/NewResolver.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/semantics/NewResolver.scala @@ -1624,7 +1624,7 @@ class NewResolver: // consumes a candidate's entry site and the new entry supplies its own. // Deferred references use exactly the same transport as ordinary values. // The nested trace shows the carrier standing in for `base` (see markCarrier). - trace[DeclaredType](s"transportType ${base.resolution.showDbg} (previous = ${previous.map(_.showDbg)}, marks = ${marks.map(_.showDbg)})"): + trace[DeclaredType](dbg"transportType ${base.resolution.showDbg} (previous = ${previous.map(_.showDbg)}, marks = ${marks.map(_.showDbg)})"): markCarrier.exit(previous).exit(marks) match case Marked(_, NoMarks) => base case Marked(_, path: SomeMarks) => @@ -2956,16 +2956,16 @@ class NewResolver: return case _ => () val sh = appShapes.getOrElseUpdate((ShapeIdentity.key(lhs), res.resSym), { - log(s"appShape: lhs = ${lhs.shwDbg}, args = ${args.showDbg}, res = ${res.showDbg}") + log(dbg"appShape: lhs = ${lhs.shwDbg}, args = ${args.showDbg}, res = ${res.showDbg}") new AppShape(lhs, args, res) }) lhs.unappliedParams match case Nil => () case (ps, mss) :: pss => zipArgsIn(mss, ps.params, ps.restParam, args, res, lhs, instances) - log(s"appShape isSaturated? ${sh.isSaturated}; head? ${sh.applicationHead._1.shwDbg}, ${sh.applicationHead._2.map(_.showDbg)}") + log(dbg"appShape isSaturated? ${sh.isSaturated}; head? ${sh.applicationHead._1.shwDbg}, ${sh.applicationHead._2.map(_.showDbg)}") def register(using ResolutionContext) = publish(sh) - log(s"lhs ${lhs.isSaturated} ${lhs.unappliedParams.map(_.mapFirst(_.showDbg).mapSecond(_.map(_.showDbg)))}") + log(dbg"lhs ${lhs.isSaturated} ${lhs.unappliedParams.map(_.mapFirst(_.showDbg).mapSecond(_.map(_.showDbg)))}") if lhs.isSaturated && !rctx.hasError(res) then rctx.markError(res) // Marks transport a value across scopes; they do not apply arguments. @@ -3000,7 +3000,7 @@ class NewResolver: // TODO: handle `mss` register case _ => - log(s"appShape: td.body = ${td.body}") + log(dbg"appShape: td.body = ${td.body}") listenDefinitionResult(td, instances): result => transportShape(result, mss) match case value: TermShape => publish(value) @@ -3248,13 +3248,13 @@ class NewResolver: /** `scope` is where `sel` is written, which determines the private members it can select. */ def newSel(sel: NewSel, scope: SrcScope)(using ResolutionContext): Unit = - log(s"newSel? sel = ${sel.showDbg}") + log(dbg"newSel? sel = ${sel.showDbg}") def visible(info: MemberLookup): MemberLookup = info.visibleFrom(scope.canAccessPrivatesOf, reportInaccessible = true) def member(info: MemberLookup, description: Message, diagnostic: Message => Ls[(Message, Opt[Loc])], instances: TypeSubstitution)(using ResolutionContext): Unit = info match case MemberLookup.Contextual(source, substitution) => member(source, description, diagnostic, instances.withOverrides(substitution)) case MemberLookup.Found(bms, marks) if rctx.canResolve(sel) || sel.resolvedMembers.contains(bms.memberSymbol) => - log(s"newSel member: bms = ${bms.memberSymbol.showDbg}, mss = ${marks.map(_.showDbg)}") + log(dbg"newSel member: bms = ${bms.memberSymbol.showDbg}, mss = ${marks.map(_.showDbg)}") rctx.recordResolution(sel, sel.resolvedMembers.contains(bms.memberSymbol))(sel.resolvedMembers ::= bms.memberSymbol) publishMember(sel, bms, sel.resSym, marks, instances) case MemberLookup.Declared(member, bindings, marks, annotation, positive) if rctx.canResolve(sel) || sel.resolvedMembers.contains(member) => @@ -3298,7 +3298,7 @@ class NewResolver: , shape => member(visible(shape.getMember(sel.id.name)), msg"${shape.describe.capitalize}", shape.diagnostic, TypeSubstitution.empty)) case N => listenReceiver(sel.prefix): shape => - log(s"newSel: sel = ${sel.showDbg}, shape = ${shape.shwDbg}") + log(dbg"newSel: sel = ${sel.showDbg}, shape = ${shape.shwDbg}") member(visible(shape.getMember(sel.id.name)), msg"${shape.describe.capitalize}", shape.diagnostic, TypeSubstitution.empty) case S(cls) => classSelection(cls)(ref => @@ -3452,7 +3452,7 @@ class NewResolver: case N => listenResult(body)(publishActivated(host, _)) def pipeTerm(from: Term, to: ShapeHost)(using ResolutionContext): Unit = - log(s"pipeTerm: from = ${from.showDbg}, to = ${to.showDbg}; ${to.currentShapes}") + log(dbg"pipeTerm: from = ${from.showDbg}, to = ${to.showDbg}; ${to.currentShapes}") listenTerm(from): sh => publishActivated(to, sh) @@ -3576,9 +3576,9 @@ class NewResolver: private def fromBMSAt(bms: BlockMemberSymbol, resSym: FlowSymbol, markss: Ls[Marks], listener: Listener, trm: Term, selected: ShapeListener[DefinitionSymbol[?]], receiver: Bool, specialization: Opt[(FlowSymbol, Ls[DeclaredType])])(using ResolutionContext): Unit = - log(s"listenBMS: bms = ${bms.describe}") + log(dbg"listenBMS: bms = ${bms.describe}") bms.onComplete: () => - log(s"listenedBMS: bms = ${bms.describe}") + log(dbg"listenedBMS: bms = ${bms.describe}") valueTarget(bms, receiver) match case S(sym: (ModuleOrObjectSymbol | TermSymbol | ClassSymbol | PatternSymbol)) => // Selection is independent of the selected value's shape. In particular, @@ -3586,7 +3586,7 @@ class NewResolver: // Pattern resolution supplies its own interpretation of the selected head. selected(sym) val wrappedListener: Listener = sh => - log(s"fromBMS: bms = ${bms.showDbg}, sh = ${sh.shwDbg}, flow = ${resSym.showDbg}, markss = ${markss.map(_.showDbg)}") + log(dbg"fromBMS: bms = ${bms.showDbg}, sh = ${sh.shwDbg}, flow = ${resSym.showDbg}, markss = ${markss.map(_.showDbg)}") val sh0 = sh // Modules, objects, and runtime pattern objects introduce no value // boundary. Pattern bodies have separate matching scopes; passing the @@ -3599,15 +3599,15 @@ class NewResolver: case value: TermShape => transportShape(value, markss) case NoShape => NoShape) match case NoShape => - log(s"FILTER OUT ${sh.shwDbg} for ${sym.showDbg} % ${resSym.showDbg}") + log(dbg"FILTER OUT ${sh.shwDbg} for ${sym.showDbg} % ${resSym.showDbg}") case sh: TermShape => if sh0.isInstanceOf[MarkedShape[?]] - then log(s"MATCH ${sh.shwDbg} for ${sym.showDbg} % ${resSym.showDbg}") - else log(s"PUSH ${sh.shwDbg}") + then log(dbg"MATCH ${sh.shwDbg} for ${sym.showDbg} % ${resSym.showDbg}") + else log(dbg"PUSH ${sh.shwDbg}") listener(sh) sym.defn match case S(td: TermDefinition) if td.params.isEmpty => - log(s"listenTerm: td.body = ${td.body.fold("N")(_.showDbg)}") + log(dbg"listenTerm: td.body = ${td.body.fold("N")(_.showDbg)}") val callerInstances = rctx.instances val instances = if isByName(td) then instantiateByName(td, resSym, ExitMark(ResolutionBoundary(td.tsym), S(resSym), NoMarks) :: markss, specialization, Map.empty) @@ -3808,7 +3808,7 @@ class NewResolver: flow.exits.getOrElse(N, Vector.empty).foreach(value => listenTerm(value)(listener)) def listenTerm(trm: Term)(listener: Listener)(using ResolutionContext): Unit = - log(s"listenTerm: trm = ${trm.showDbg}") + log(dbg"listenTerm: trm = ${trm.showDbg}") listenValue(trm, false)(listener) def listenReceiver(trm: Term)(listener: Listener)(using ResolutionContext): Unit = @@ -3871,7 +3871,7 @@ class NewResolver: private def listen(trm: Term, discardMarks: Bool)(receive: ShapeListener[Shape])(using ResolutionContext): Unit = val listener = activationListener(receive) - log(s"listen: trm = ${trm.showDbg}") + log(dbg"listen: trm = ${trm.showDbg}") trm.addShapeListener(listener) trm match case _: SynthSel => @@ -4067,7 +4067,7 @@ class NewResolver: case _ => N def publish(parent: Opt[NominalInstanceView])(using ResolutionContext): Unit = listener(introShapes.getOrElseUpdate(new Identity(intro), { - log(s"introShape: intro = $intro") + log(dbg"introShape: intro = $intro") IntroShape(intro, parent) })) primitive match diff --git a/hkmc2/shared/src/main/scala/hkmc2/semantics/Resolver.scala b/hkmc2/shared/src/main/scala/hkmc2/semantics/Resolver.scala index 4fcf7ffaba..ddc1fe3b8f 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/semantics/Resolver.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/semantics/Resolver.scala @@ -259,7 +259,7 @@ class Resolver(tl: TraceLogger) * usually the entry point for the resolver. */ def traverseBlock(blk: Term.Blk)(using ICtx): ICtx = - trace(s"Traversing block: $blk"): + trace(dbg"Traversing block: $blk"): traverseStmts(blk.stats).givenIn: traverse(blk.res, expect = Any) ictx @@ -268,7 +268,7 @@ class Resolver(tl: TraceLogger) final def traverseStmts(stmts: Ls[Statement])(using ICtx): ICtx = stmts match case Nil => ictx case stmt :: rest => - log(s"Traversing statement: $stmt") + log(dbg"Traversing statement: $stmt") val newICtx = stmt match case tdf: Definition => traverseDefn(tdf) @@ -289,7 +289,7 @@ class Resolver(tl: TraceLogger) * check the modulefulness of the term. */ def traverse(t: Term, expect: Expect)(using ictx: ICtx): Unit = - trace(s"Traversing term: $t (expect = ${expect})"): + trace(dbg"Traversing term: $t (expect = ${expect})"): t match case blk: Term.Blk => @@ -341,7 +341,7 @@ class Resolver(tl: TraceLogger) if !cfg.noModuleCheck then val evalsToModule = ModuleChecker.evalsToModule(t, prefer = expect) val evalsToStaticClass = ModuleChecker.isStaticClass(t) - log(s"Checking ${t}: expect ${expect}, evalsToModule = ${evalsToModule}, evalsToStaticClass = ${evalsToStaticClass}") + log(dbg"Checking ${t}: expect ${expect}, evalsToModule = ${evalsToModule}, evalsToStaticClass = ${evalsToStaticClass}") if expect.`module` && !evalsToModule then raise(ErrorReport(msg"Expected a module; found non-moduleful ${t.describe}." -> t.toLoc @@ -357,7 +357,7 @@ class Resolver(tl: TraceLogger) end traverse def traverseDefn(defn: Definition)(using ICtx): ICtx = - trace(s"Resolving definition: $defn"): + trace(dbg"Resolving definition: $defn"): def traverseTermDef(tdf: TermDefinition) = val TermDefinition(_k, _sym, _tsym, pss, tps, sign, body, TermDefFlags(isMethod, _), modulefulness, annotations, comp @@ -425,7 +425,7 @@ class Resolver(tl: TraceLogger) // Case: instance definition. Add the instance to the context. case defn @ TermDefinition(k = Ins, sym = sym, tsym = tsym, flags = TermDefFlags(isMethod, _), sign = sign) => softAssert(defn.owner.isEmpty) - log(s"Resolving instance definition ${defn.showDbg}") + log(dbg"Resolving instance definition ${defn.showDbg}") traverseTermDef(defn) sign match case N => @@ -436,7 +436,7 @@ class Resolver(tl: TraceLogger) // Case: Fun/Val definition. case defn @ TermDefinition(k = Fun | ImmutVal | MutVal) => - log(s"Resolving ${defn.k.desc} definition $defn") + log(dbg"Resolving ${defn.k.desc} definition $defn") traverseTermDef(defn) ictx @@ -444,7 +444,7 @@ class Resolver(tl: TraceLogger) // context and use the context to traverse through the body. // Traverse through other subterms with original context. case defn: ClassLikeDef => - log(s"Resolving ${defn.kind.desc} definition $defn") + log(dbg"Resolving ${defn.kind.desc} definition $defn") defn match case defn: PatternDef => @@ -590,7 +590,7 @@ class Resolver(tl: TraceLogger) (N, ictx) t.expandedResolvableIn: t => - log(s"Resolving resolvable term ${t} with sym = ${t.legacyResolvedSym}, typ = ${t.resolvedTyp}") + log(dbg"Resolving resolvable term ${t} with sym = ${t.legacyResolvedSym}, typ = ${t.resolvedTyp}") // Fill the context with possibly the type arguments information. val newICtx2 = newICtx1.givenIn: @@ -609,11 +609,11 @@ class Resolver(tl: TraceLogger) case (ictx, (tparam, targ)) => val sym = tparam.sym val typ = resolveSign(targ, expect = Any) - log(s"Resolving App with type arg ${sym} = $typ") + log(dbg"Resolving App with type arg ${sym} = $typ") ictx.withTypeArg(sym, typ) case (S(tparams), N) => tparams.foldLeft(ictx): case (ictx, tparam) => - log(s"Resolving App with type arg ${tparam.sym} unspecified") + log(dbg"Resolving App with type arg ${tparam.sym} unspecified") ictx case (N, _) => ictx newICtx @@ -868,15 +868,15 @@ class Resolver(tl: TraceLogger) case _ if t.symbol.exists(_.isInstanceOf[ErrorSymbol]) => () case t @ Term.Ref(bsym: BlockMemberSymbol) => - log(s"Resolving symbol for reference ${t} (bsym = ${bsym})") + log(dbg"Resolving symbol for reference ${t} (bsym = ${bsym})") val sym = disambSym(prefer, sign = sign)(bsym) sym.foreach: sym => t.expand(S((t.duplicate.resolved(sym)))) resolveType(t, prefer = prefer) - log(s"Resolved symbol for ${t}: ${sym}") + log(dbg"Resolved symbol for ${t}: ${sym}") case t @ AnySel(lhs: Resolvable, id, cls) => lhs.expandedResolvableIn: lhs => - log(s"Resolving symbol for selection ${t}, defn = ${lhs.defn}") + log(dbg"Resolving symbol for selection ${t}, defn = ${lhs.defn}") val clsDefn = cls.flatMap: sym => sym.legacyResolvedSym.flatMap(_.asCls).map: clsSym => @@ -896,15 +896,15 @@ class Resolver(tl: TraceLogger) val fsym = defn.body.members.get(id.name) fsym match case S(bms: BlockMemberSymbol) => - log(s"Resolving symbol for ${t}, defn = ${lhs.defn}") + log(dbg"Resolving symbol for ${t}, defn = ${lhs.defn}") disambSym(prefer, sign)(bms) match case S(ds) => checkPrivateAccess(t, ds) t.expand(S(t.withSym(bms).resolved(ds))) case N => - log(s"Unable to disambiguate ${bms}") + log(dbg"Unable to disambiguate ${bms}") t.expand(S(t.withSym(bms))) - log(s"Resolved symbol for ${t}: ${bms}") + log(dbg"Resolved symbol for ${t}: ${bms}") case N => defn match case defn: ModuleOrObjectDef => @@ -919,7 +919,7 @@ class Resolver(tl: TraceLogger) // - MLscript programs selecting JS members without properly defining them. // - Inherited members. case defn => - log(s"Unsupported selection from definition: ${defn}") + log(dbg"Unsupported selection from definition: ${defn}") case t @ Term.New(cls, _, N) => // cls is already resolved, so the symbol should be present; @@ -966,14 +966,14 @@ class Resolver(tl: TraceLogger) val decl = base.legacyResolvedSym match case S(bms: BlockMemberSymbol) => val disambBms = disambSym(bms) - log(s"Disambiguate ${bms} (${bms.asTrm}) into ${disambBms} (defn = ${disambBms.map(_.defn)}), preferring ${prefer}") + log(dbg"Disambiguate ${bms} (${bms.asTrm}) into ${disambBms} (defn = ${disambBms.map(_.defn)}), preferring ${prefer}") disambBms match case S(disambBms) => disambBms.defn case N => bms.asPrincipal.flatMap(_.defn) case S(bls: LocalVarSymbol) => bls.decl case S(ds: DefinitionSymbol[?]) => ds.defn case _ => N - log(s"Declaration: ${decl}") + log(dbg"Declaration: ${decl}") decl match case S(defn: TermDefinition) if defn.params.length === ass.length => val ty = defn.sign.map(resolveSign(_, expect = Expect.fromModulefulness(defn.modulefulness))) @@ -994,15 +994,15 @@ class Resolver(tl: TraceLogger) end resolveType def resolveArg(p: Param)(lhs: Term)(using ictx: ICtx): Elem = - log(s"Resolving implicit argument, expecting a ${p.sign}") + log(dbg"Resolving implicit argument, expecting a ${p.sign}") p.sign match case S(sign) => // FIXME[Harry]: Don't re-resolve signatures for every argument! val ty = resolveSign(sign, expect = if p.modulefulness.modified then Module(N) else NonModule(N)) - log(s"Resolving implicit argument, expecting a ${ty.show}") + log(dbg"Resolving implicit argument, expecting a ${ty.show}") ictx.query(ty) match case R(i) => - log(s"Resolved ${p.sign} with instance ${i}") + log(dbg"Resolved ${p.sign} with instance ${i}") val ref = i.sym.ref() traverse(ref, // note: we accept regular arguments for module parameters @@ -1022,7 +1022,7 @@ class Resolver(tl: TraceLogger) lastWords(s"No type signature for contextual parameter ${p.showDbg} at ${p.toLoc}") def traverseParam(p: Param)(using ictx: ICtx): Unit = - log(s"Resolving parameter ${p.showDbg}") + log(dbg"Resolving parameter ${p.showDbg}") val ty = p.sign.map(sign => resolveSign(sign, expect = if p.modulefulness.modified then Module(N) else NonModule(N))) p.signType = ty @@ -1048,7 +1048,7 @@ class Resolver(tl: TraceLogger) * details. */ def traverseSign(t: Term, expect: Expect, inAppPrefix: Bool = false)(using ictx: ICtx): Unit = boundary: - trace(s"Traversing type ${t}, expecting ${expect}"): + trace(dbg"Traversing type ${t}, expecting ${expect}"): // * Traverse through the sub-terms. t match diff --git a/hkmc2/shared/src/main/scala/hkmc2/semantics/Shape.scala b/hkmc2/shared/src/main/scala/hkmc2/semantics/Shape.scala index 18d44066bd..09752f393c 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/semantics/Shape.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/semantics/Shape.scala @@ -301,7 +301,7 @@ object MarkedShape: case sh: NonMarkedShape => MarkedShape(sh, EntryMark(boundary, id, NoMarks)) case MarkedShape(sh, marks) => MarkedShape(sh, EntryMark(boundary, id, marks)) def exit(sh: TermShape, boundary: ResolutionBoundary, id: Opt[FlowSymbol])(using TL): TermShape | NoShape = - tl.trace[TermShape | NoShape](s".exit MarkedShape (${sh.shwDbg}, ${boundary.showDbg}, ${id.fold("")(_.showDbg)})", res => s"= ${res.shwDbg}"): + tl.trace[TermShape | NoShape](dbg".exit MarkedShape (${sh.shwDbg}, ${boundary.showDbg}, ${id.fold("")(_.showDbg)})", res => s"= ${res.shwDbg}"): sh match // Exits are only ever prepended to a path's trailing exits, never compared // against them; they locate what a value refers to. A self-contained value @@ -392,7 +392,7 @@ sealed trait TermShape extends Shape, ShapeLike: case _ => unappliedParamLists.map(_ -> applicationMarks) def exit(marks: Marks)(using TL): TermShape | NoShape = - tl.trace[TermShape | NoShape](s".exit $shwDbg (${marks.showDbg})", res => s"= ${res.shwDbg}"): + tl.trace[TermShape | NoShape](dbg".exit $shwDbg (${marks.showDbg})", res => s"= ${res.shwDbg}"): marks match case NoMarks => this case EntryMark(sym, id, rest) => @@ -410,7 +410,7 @@ sealed trait TermShape extends Shape, ShapeLike: exit(mark).exit(rest) def enter(marks: Marks)(using tl: TL): TermShape | NoShape = - tl.trace[TermShape | NoShape](s".enter $shwDbg (${marks.showDbg})", res => s"= ${res.shwDbg}"): + tl.trace[TermShape | NoShape](dbg".enter $shwDbg (${marks.showDbg})", res => s"= ${res.shwDbg}"): marks match case NoMarks => this case EntryMark(sym, id, rest) => diff --git a/hkmc2/shared/src/main/scala/hkmc2/semantics/Symbol.scala b/hkmc2/shared/src/main/scala/hkmc2/semantics/Symbol.scala index a079481b1c..e5c9909efd 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/semantics/Symbol.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/semantics/Symbol.scala @@ -47,10 +47,12 @@ abstract class Symbol(using ownerState: State) extends MaybeSymbol with Located: def showPrefix(using Scope, ShowCfg, Raise): Str = prefix def showFullName(using Scope, ShowCfg, Raise): Str = - showPrefix + showName + State.dbgUid(uid) + showPrefix + showName + // Generic string conversion is independent of the symbol's owner and the current trace. + // DebugPrinter adds disambiguated names and optional UIDs when explicitly rendering a symbol. override def toString: Str = - prefix + nme + State.dbgUid(uid) + prefix + nme private[semantics] val directRefs: mutable.Buffer[Term.Ref] = mutable.Buffer.empty def ref(id: Tree.Ident = diff --git a/hkmc2/shared/src/main/scala/hkmc2/semantics/SymbolPrinter.scala b/hkmc2/shared/src/main/scala/hkmc2/semantics/SymbolPrinter.scala index 7a710b2182..373ca5d0d5 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/semantics/SymbolPrinter.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/semantics/SymbolPrinter.scala @@ -6,13 +6,15 @@ import syntax.* import hkmc2.utils.* -class SymbolPrinter(val dbgScp: Scope) extends DebugPrinter: +class SymbolPrinter( + val dbgScp: Scope, + preprocess: Product => Product, + postprocess: Product => Str, +)(using owner: Elaborator.State) extends DebugPrinter(false, S(owner), + sym => sym.showFullName(using dbgScp, semantics.ShowCfg.internal, throw _), + preprocess, postprocess): + def this(dbgScp: Scope)(using Elaborator.State) = this(dbgScp, identity, _ => "") - override def printPlain(v: Any): Str = v match - case sym: Symbol => - sym.showFullName(using dbgScp, semantics.ShowCfg.internal, throw _) - case _ => super.printPlain(v) - def printSymbol(sym: Symbol)(using Raise, ShowCfg): Str = sym.showName(using dbgScp, summon) diff --git a/hkmc2/shared/src/main/scala/hkmc2/semantics/Term.scala b/hkmc2/shared/src/main/scala/hkmc2/semantics/Term.scala index 42edf4f328..3d840ef6be 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/semantics/Term.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/semantics/Term.scala @@ -1254,13 +1254,13 @@ final case class TyParam(flags: FldFlags, vce: Opt[Bool], sym: VarSymbol)(val to var isCovariant: Bool = true var isContravariant: Bool = true - def showDbg: Str = + def showDbg(using DebugPrinter): Str = // (if isContravariant then "in " else "") + // (if isCovariant then "out " else "") + (if isCovariant then if isContravariant then "" else "out " else if isContravariant then "in " else "in out ") + - "‹" + flags.show + "›" + sym + "‹" + flags.show + "›" + sym.showDbg object Param: diff --git a/hkmc2/shared/src/main/scala/hkmc2/semantics/TermImpl.scala b/hkmc2/shared/src/main/scala/hkmc2/semantics/TermImpl.scala index 028c4cbdc5..bdcce84824 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/semantics/TermImpl.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/semantics/TermImpl.scala @@ -183,7 +183,7 @@ trait StatementImpl extends Located, ProductWithExtraInfo, Describable: r.legacyResolvedSym.map(s => s"sym=${s.showAsPlain}") :: r.resolvedTyp.map(s => s"typ=${s.showDbg}") :: Nil ).flatten.mkString(",") - case r: SelProj => r.symbol.mkString + case r: SelProj => r.symbol.map(_.showDbg).mkString case _ => "" def subStatements: Vector[Statement] = this match @@ -463,10 +463,10 @@ trait StatementImpl extends Located, ProductWithExtraInfo, Describable: def showDbg(using DebugPrinter): Str = this match case r: Ref => r.sym.showAsPlain case r: Resolved => - s"${r.showPlain}‹${r.sym}›" + s"${r.showPlain}‹${r.sym.showDbg}›" case trm: Term => // s"$showPlain‹${trm.symbol.getOrElse("")}›" - s"$showPlain${trm.symbol.fold("")("‹"+_+"›")}" + s"$showPlain${trm.symbol.fold("")(sym => "‹"+sym.showDbg+"›")}" case _ => showPlain @@ -496,10 +496,11 @@ trait StatementImpl extends Located, ProductWithExtraInfo, Describable: eff.map(e => s"{${e.showDbg}}").getOrElse("")} ${rhs.showDbg}" case FunTy(lhs, rhs, eff) => s"(...${lhs.showDbg}) ->${eff.map(e => s"{${e.showDbg}}").getOrElse("")} ${rhs.showDbg}" - case TyApp(lhs, targs) => s"${lhs.showDbg}[${targs.mkString(", ")}]" - case Forall(tvs, outer, body) => s"forall ${tvs.mkString(", ")}${outer.map(v => s", outer $v").mkString}: ${body.toString}" + case TyApp(lhs, targs) => s"${lhs.showDbg}[${targs.map(_.showDbg).mkString(", ")}]" + case Forall(tvs, outer, body) => + s"forall ${tvs.map(_.showAsPlain).mkString(", ")}${outer.map(v => s", outer ${v.showDbg}").mkString}: ${body.showDbg}" case Constrained(constraints, body) => s"[${constraints.map(_.showDbg).mkString(", ")}] => ${body.showDbg}" - case WildcardTy(in, out) => s"in ${in.map(_.toString).getOrElse("⊥")} out ${out.map(_.toString).getOrElse("⊤")}" + case WildcardTy(in, out) => s"in ${in.map(_.showDbg).getOrElse("⊥")} out ${out.map(_.showDbg).getOrElse("⊤")}" case Sel(pre, nme) => s"${pre.showDbg}.${nme.name}" case SynthSel(pre, nme) => s"(${pre.showDbg}.)${nme.name}" case Super(_, owner, id) => s"super[${owner.showDbg}].${id.name}" @@ -522,17 +523,17 @@ trait StatementImpl extends Located, ProductWithExtraInfo, Describable: case DynNew(cls, args) => s"new! ${cls.showDbg}${args.map(_.showDbgAsParams).mkString}" case SelProj(pre, cls, proj) => s"${pre.showDbg}.${cls.showDbg}#${proj.name}" - case Asc(term, ty) => s"${term.toString}: ${ty.toString}" - case LetDecl(sym, _) => s"let ${sym}" - case DefineVar(sym, rhs) => s"${sym} = ${rhs.showDbg}" + case Asc(term, ty) => s"${term.showDbg}: ${ty.showDbg}" + case LetDecl(sym, _) => s"let ${sym.showDbg}" + case DefineVar(sym, rhs) => s"${sym.showDbg} = ${rhs.showDbg}" case Handle(lhs, rhs, args, derivedClsSym, defs, bod) => - s"handle ${lhs} = ${rhs}(${args.mkString(", ")}) ${defs} in ${bod}" + s"handle ${lhs.showDbg} = ${rhs.showDbg}(${args.map(_.showDbg).mkString(", ")}) ${defs.showAsPlain} in ${bod.showDbg}" case Region(name, body) => s"region ${name.nme} in ${body.showDbg}" case RegRef(reg, value) => s"(${reg.showDbg}).ref ${value.showDbg}" case Assgn(lhs, rhs) => s"${lhs.showDbg} := ${rhs.showDbg}" case SetRef(lhs, rhs) => s"${lhs.showDbg} := ${rhs.showDbg}" - case Drop(term) => s"drop $term" - case Deref(term) => s"!$term" + case Drop(term) => s"drop ${term.showDbg}" + case Deref(term) => s"!${term.showDbg}" case Neg(ty) => s"~${ty.showDbg}" case CompType(lhs, rhs, pol) => s"${lhs.showDbg} ${if pol then "|" else "&"} ${rhs.showDbg}" case Error() => "" @@ -540,7 +541,7 @@ trait StatementImpl extends Located, ProductWithExtraInfo, Describable: case Mut(und) => s"mut ${und.showDbg}" case CtxTup(fields) => fields.map(_.showDbg).mkString("‹using›[", ", ", "]") case TermDefinition(k, sym, tsym, pss, tps, sign, body, flags, _, _, _) => - s"${flags.showDbg}${k.str} ${sym}${ + s"${flags.showDbg}${k.str} ${sym.showDbg}${ tps.map(_.map(_.showDbg)).mkStringOr(", ", "[", "]") }${ pss.map(_.showDbg).mkString("") @@ -550,11 +551,11 @@ trait StatementImpl extends Located, ProductWithExtraInfo, Describable: case N => "" }" case cls: ClassLikeDef => - s"${cls.kind} ${cls.sym.nme}${ + s"${cls.kind} ${cls.sym.showDbg}${ cls.tparams.map(_.showDbg).mkStringOr(", ", "[", "]")}${ - cls.paramsOpt.fold("")(_.toString)} ${cls.body}" + cls.paramsOpt.fold("")(_.showDbg)} ${cls.body.blk.showDbg}" case Import(sym, str, file) => s"import $str from ${file}" - case Annotated(ann, target) => s"@${ann} ${target.showDbg}" + case Annotated(ann, target) => s"@${ann.showAsPlain} ${target.showDbg}" case Throw(res) => s"throw ${res.showDbg}" case Label(label, _, body, _) => s"do ${label.nme}: ${body.showDbg}" case Break(label, _, value) => s"${label.nme}.break${value.fold("")(v => s" ${v.showDbg}")}" @@ -562,7 +563,7 @@ trait StatementImpl extends Located, ProductWithExtraInfo, Describable: case Try(body, finallyDo) => s"try ${body.showDbg} finally ${finallyDo.showDbg}" case Ret(res) => s"return ${res.showDbg}" case TypeDef(sym, _, tparams, rhs, _, _) => - s"type ${sym}${tparams.mkStringOr(", ", "[", "]")} = ${rhs.fold("")(x => x.showDbg)}" + s"type ${sym.showDbg}${tparams.map(_.showDbg).mkStringOr(", ", "[", "]")} = ${rhs.fold("")(x => x.showDbg)}" case Missing => "missing" case LeadingDotSel(nme) => s"_?_.${nme.name}" case SetConfig(_) => "#config(...)" diff --git a/hkmc2/shared/src/main/scala/hkmc2/semantics/flow/FlowAnalysis.scala b/hkmc2/shared/src/main/scala/hkmc2/semantics/flow/FlowAnalysis.scala index 72da6b44ac..7133a02075 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/semantics/flow/FlowAnalysis.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/semantics/flow/FlowAnalysis.scala @@ -62,7 +62,7 @@ class FlowAnalysis(using tl: TraceLogger)(using Raise, State, Ctx): typeProdImpl(t.expanded, insideSelAppChain) def typeProdImpl(t: Term, insideSelAppChain: Boolean): Producer = - trace[P](s"Typing producer: ${t.showDbg}", post = res => s": ${res.showDbg}"): + trace[P](dbg"Typing producer: ${t.showDbg}", post = res => s": ${res.showDbg}"): def constrain(lhs: P, rhs: C): Unit = collectedConstraints += ((src = t, c = Constraint(lhs, rhs))) @@ -70,7 +70,7 @@ class FlowAnalysis(using tl: TraceLogger)(using Raise, State, Ctx): t.ldsRoot match case S(lds) if !insideSelAppChain => val sym = FlowSymbol("bind") - log(s"Constraining leading dot selection ${lds.showDbg} at the top level") + log(dbg"Constraining leading dot selection ${lds.showDbg} at the top level") constrain(P.LeadingDotSel(lds), C.Flow(sym)) constrain(res(typeProd(sub, insideSelAppChain = true)), C.Flow(sym)) P.Flow(sym) @@ -100,7 +100,7 @@ class FlowAnalysis(using tl: TraceLogger)(using Raise, State, Ctx): msg"Cannot use non-term member ${bms.nme} in term position" -> t.toLoc :: Nil getFlowSymOrType(bms) case _: Symbol => - log(s"/!\\ Unhandled symbol type: ${sym} (${sym.getClass.getSimpleName}) /!\\") + log(dbg"/!\\ Unhandled symbol type: ${sym} (${sym.getClass.getSimpleName}) /!\\") P.Unknown(t) case Blk(stats, res) => @@ -142,20 +142,20 @@ class FlowAnalysis(using tl: TraceLogger)(using Raise, State, Ctx): ) case N => P.Unknown(cd) - log(s"Class member type: ${prod.showDbg}") + log(dbg"Class member type: ${prod.showDbg}") constrain(prod, C.Flow(cd.bsym.flow)) case md: ModuleOrObjectDef => // TODO - log(s"Module: ${md.path}") + log(dbg"Module: ${md.path}") typeBody(md.body) case _: Import => // TODO? case stt => - log(s"/!\\ Unhandled statement: ${stt} /!\\") + log(dbg"/!\\ Unhandled statement: ${stt} /!\\") P.Unknown(stt) typeProd(res) @@ -165,21 +165,21 @@ class FlowAnalysis(using tl: TraceLogger)(using Raise, State, Ctx): case sel @ LeadingDotSel(nme) => leadingDotSelsToExpand += sel - log(s"Leading dot selection ${sel.showDbg}") + log(dbg"Leading dot selection ${sel.showDbg}") val sym = sel.resSym constrain(P.LeadingDotSel(sel), C.Flow(sym)) P.Flow(sym) case sel @ AnySel(pre, nme, cls) => - log(s"Selection ${sel.showDbg} ${sel.typ}") + log(dbg"Selection ${sel.showDbg} ${sel.typ}") checkLDS(pre): pre_t => sel.legacyResolvedSym match case S(sym: BlockMemberSymbol) => - log(s"RES ${sym.nme} in ${sel.showDbg}") + log(dbg"RES ${sym.nme} in ${sel.showDbg}") getFlowSymOrType(sym) case S(sym) => selsToExpand += sel - log(s"Unhandled symbol reference ${sym.nme} in ${sel.showDbg}") + log(dbg"Unhandled symbol reference ${sym.nme} in ${sel.showDbg}") P.Unknown(sel) case N => selsToExpand += sel @@ -192,7 +192,7 @@ class FlowAnalysis(using tl: TraceLogger)(using Raise, State, Ctx): case N => cls.legacyResolvedSym.flatMap(_.asCls) match case N => - log(s"Unresolved or invalid class symbol in ${cls.showDbg}") + log(dbg"Unresolved or invalid class symbol in ${cls.showDbg}") P.Unknown(nw) case S(sym) => sym match @@ -239,7 +239,7 @@ class FlowAnalysis(using tl: TraceLogger)(using Raise, State, Ctx): */ def typeParam(p: Param): C = - trace[C](s"Typing param: ${p.showDbg}", post = res => s": ${res.showDbg}"): + trace[C](dbg"Typing param: ${p.showDbg}", post = res => s": ${res.showDbg}"): p.signType match case S(typ) => p.flow.producers += ConcreteProd(Vector.empty, P.Typ(typ)) @@ -253,7 +253,7 @@ class FlowAnalysis(using tl: TraceLogger)(using Raise, State, Ctx): ps.params.map(typeParam) def typeType(t: Term): Type = - trace[Type](s"Typing consumer: ${t.showDbg}", post = res => s": ${res.showDbg}"): + trace[Type](dbg"Typing consumer: ${t.showDbg}", post = res => s": ${res.showDbg}"): t match case Capture(base, _) => typeType(base) case Ref(sym: VarSymbol) => Type.Ref(sym, Nil) // unparameterized type variable @@ -277,7 +277,7 @@ class FlowAnalysis(using tl: TraceLogger)(using Raise, State, Ctx): def expandTerms() = import SelectionTarget.* selsToExpand.foreach: sel => - log(s"Resolved targets for ${sel.showDbg}: ${sel.resolvedTargets.mkString(", ")}") + log(dbg"Resolved targets for ${sel.showDbg}: ${sel.resolvedTargets.map(_.showAsPlain).mkString(", ")}") assert(sel.expansion.isEmpty) sel.resolvedTargets match case ObjectMember(sym) :: Nil => @@ -286,7 +286,7 @@ class FlowAnalysis(using tl: TraceLogger)(using Raise, State, Ctx): case CompanionMember(comp, sym) :: Nil => val base = Sel(comp, Tree.Ident(sym.nme))(S(sym), FlowSymbol.sel(sym.nme), N, N) val app = App(base, Tup(sel.prefix :: Nil)(Tree.DummyTup))(Tree.DummyApp, N, FlowSymbol.app()) - log(s"Expansion: ${app.showDbg}") + log(dbg"Expansion: ${app.showDbg}") sel.expansion = S(S(app)) case Nil => // FIXME: actually allow that in dead code (use floodfill constraints from exported members to detect) @@ -301,12 +301,12 @@ class FlowAnalysis(using tl: TraceLogger)(using Raise, State, Ctx): case ObjectMember(sym) => msg"object member ${sym.nme}" -> sym.toLoc case CompanionMember(_, sym) => msg"companion member ${sym.nme}" -> sym.toLoc leadingDotSelsToExpand.foreach: sel => - log(s"Resolved targets for ${sel.showDbg}: ${sel.resolvedTargets.mkString(", ")}") + log(dbg"Resolved targets for ${sel.showDbg}: ${sel.resolvedTargets.map(_.showAsPlain).mkString(", ")}") assert(sel.expansion.isEmpty) sel.resolvedTargets match case CompanionMember(comp, sym) :: Nil => val base = Sel(comp, Tree.Ident(sym.nme))(S(sym), FlowSymbol.sel(sym.nme), N, N) - log(s"Leading dot expansion: ${base.showDbg}") + log(dbg"Leading dot expansion: ${base.showDbg}") sel.expansion = S(S(base)) case Nil => // FIXME: actually allow that in dead code (use floodfill constraints from exported members to detect) @@ -365,7 +365,7 @@ class FlowAnalysis(using tl: TraceLogger)(using Raise, State, Ctx): val (trm, cc) = collectedConstraints.pop() toSolve.push(cc) - trace(s"Handling constraint: ${cc.showDbg} (from ${trm.showDbg})"): + trace(dbg"Handling constraint: ${cc.showDbg} (from ${trm.showDbg})"): while fuel > 0 && toSolve.nonEmpty do @@ -374,27 +374,27 @@ class FlowAnalysis(using tl: TraceLogger)(using Raise, State, Ctx): def dig(lhs: P, rhs: C, path: Path): Unit = - log(s"Solving: ${lhs.showDbg} <: ${rhs.showDbg} (${lhs.getClass.getSimpleName}, ${rhs.getClass.getSimpleName})") + log(dbg"Solving: ${lhs.showDbg} <: ${rhs.showDbg} (${lhs.getClass.getSimpleName}, ${rhs.getClass.getSimpleName})") (lhs, rhs) match case (P.Flow(sym), rhs) if inCache.contains(sym -> rhs) - => log(s"In (in) cache!") + => log(dbg"In (in) cache!") case (lhs, C.Flow(sym)) if outCache.contains(lhs -> sym) - => log(s"In (out) cache!") + => log(dbg"In (out) cache!") case (P.Flow(sym), C.Flow(sym2)) => - log(s"New flow $sym ~> $sym2") + log(dbg"New flow $sym ~> $sym2") sym.outFlows += sym2 sym.producers.foreach(cp => dig(cp.ctor, rhs, cp.path ++ path)) case (lhs: ProdCtor, C.Flow(sym)) => - log(s"New flow $lhs ~> $sym") + log(dbg"New flow $lhs ~> $sym") sym.producers += ConcreteProd(path, lhs) sym.consumers.foreach: c => dig(lhs, c, path) case (P.Flow(sym), rhs) => - log(s"New flow $sym ~> $rhs") + log(dbg"New flow $sym ~> $rhs") sym.consumers += rhs sym.producers.foreach: cp => dig(cp.ctor, rhs, cp.path ++ path) @@ -431,14 +431,14 @@ class FlowAnalysis(using tl: TraceLogger)(using Raise, State, Ctx): zip(args, ini, rst, path) case (sel @ P.LeadingDotSel(trm), rhs) => rhs match case C.Typ(Type.Ref(sym, _)) => - log(s"Examining ${sym} for leading dot selection resolution") + log(dbg"Examining ${sym} for leading dot selection resolution") getCompanionMember(trm.nme.name, trm.originalCtx, sym) match case S((path, memb)) => sel.trm.resolvedTargets ::= SelectionTarget.CompanionMember(path, memb) - log(s"Found member ${memb}") + log(dbg"Found member ${memb}") toSolve.push(Constraint(getFlowSymOrType(memb), C.Flow(trm.resSym))) case _ => - log(s"Could not find member ${trm.nme.name} in ${sym}") + log(dbg"Could not find member ${trm.nme.name} in ${sym}") case _ => log("Unhandled RHS for leading dot selections") case (lhs, sel: C.Sel) => lhs match @@ -450,7 +450,7 @@ class FlowAnalysis(using tl: TraceLogger)(using Raise, State, Ctx): d.body.members.get(sel.nme.name) match case S(memb: BlockMemberSymbol) => sel.trm.resolvedTargets ::= SelectionTarget.ObjectMember(memb) - log(s"Found immediate member ${memb}") + log(dbg"Found immediate member ${memb}") toSolve.push(Constraint(getFlowSymOrType(memb), sel.res)) case N => d.moduleCompanion match @@ -458,11 +458,11 @@ class FlowAnalysis(using tl: TraceLogger)(using Raise, State, Ctx): val cd = comp.defn.getOrElse(die) cd.body.members.get(sel.nme.name) match case S(memb) => - log(s"Found companion member ${memb}") + log(dbg"Found companion member ${memb}") sel.trm.originalCtx match case S(oc) => val patho = findAccessPath(oc, cd.path, comp) - log(s"Access path: ${patho}") + log(dbg"Access path: ${patho}") patho match case S(path) => sel.trm.resolvedTargets ::= SelectionTarget.CompanionMember(path, memb) @@ -488,7 +488,7 @@ class FlowAnalysis(using tl: TraceLogger)(using Raise, State, Ctx): :: msg"Type `${lhs.showDbg}` does not contain member '${sel.nme.name}'" -> lhs.toLoc :: Nil) case _ => - log(s"/!\\ Unhandled constraint /!\\") + log(dbg"/!\\ Unhandled constraint /!\\") end dig dig(c.lhs, c.rhs, Vector.empty) @@ -498,7 +498,7 @@ class FlowAnalysis(using tl: TraceLogger)(using Raise, State, Ctx): msg"Could not solve all constraints within $MAX_FUEL iterations." -> N :: Nil)) def findAccessPath(src: SrcScope, dst: SrcScope, moduleSym: ModuleOrObjectSymbol): Opt[Term] = - log(s"outermostAcessibleBase ${dst.outermostAcessibleBase}") + log(dbg"outermostAcessibleBase ${dst.outermostAcessibleBase}") val (outermostBase, outermostPath) = dst.outermostAcessibleBase var cur = src while cur isnt outermostBase do diff --git a/hkmc2/shared/src/main/scala/hkmc2/semantics/ucs/Normalization.scala b/hkmc2/shared/src/main/scala/hkmc2/semantics/ucs/Normalization.scala index feecca37bc..2d472115b5 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/semantics/ucs/Normalization.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/semantics/ucs/Normalization.scala @@ -132,7 +132,7 @@ class Normalization(lowering: Lowering)(using tl: TL)(using Raise, Ctx, State, C * `freeSplitSyms` of the recursive result. */ private def normalizeImpl(split: Split)(using vs: VarSet): Split = split match case Split.Cons(Branch(scrutinee, pattern, consequent), alternative) => - log(s"MATCH: ${scrutinee.showDbg} is ${pattern.showDbg}") + log(dbg"MATCH: ${scrutinee.showDbg} is ${pattern.showDbg}") val specializedConsequent = consequent.specialized(+, scrutinee, pattern) if specializedConsequent.isFull then // The positive specialization of the consequent ends with `Else` or @@ -157,13 +157,13 @@ class Normalization(lowering: Lowering)(using tl: TL)(using Raise, Ctx, State, C case S(threshold) => normalizedAlternative.size * 2 > threshold case N => false if shouldShare then - log(s"SHARE: let-split ${splitSymbol.nme}, body size ${normalizedAlternative.size}") + log(dbg"SHARE: let-split ${splitSymbol.nme}, body size ${normalizedAlternative.size}") Split.LetSplit(splitSymbol, Branch(scrutinee, pattern, whenTrue) ~: useSplit) else - log(s"INLINE: body size ${normalizedAlternative.size} below threshold") + log(dbg"INLINE: body size ${normalizedAlternative.size} below threshold") Branch(scrutinee, pattern, inlineUseSplit(whenTrue, splitSymbol, normalizedAlternative)) ~: normalizedAlternative else - log(s"ABSORB: positive consequent absorbed $$${splitSymbol.nme}, no sharing") + log(dbg"ABSORB: positive consequent absorbed $$${splitSymbol.nme}, no sharing") Branch(scrutinee, pattern, whenTrue) ~: normalizedAlternative case (S(positiveAlternative), S(negativeAlternative)) => log("DUP: pos≠, neg≠") @@ -175,13 +175,13 @@ class Normalization(lowering: Lowering)(using tl: TL)(using Raise, Ctx, State, C log("DUP: pos=, neg≠") Branch(scrutinee, pattern, normalize(specializedConsequent ++ alternative.duplicate)) ~: normalize(negativeAlternative) case Split.Let(v, _, tail) if vs has v => - log(s"LET: SKIP already declared scrutinee $v") + log(dbg"LET: SKIP already declared scrutinee $v") normalizeImpl(tail) case Split.Let(v, rhs, tail) => - log(s"LET: $v") + log(dbg"LET: $v") Split.Let(v, rhs, normalizeImpl(tail)(using vs + v)) case split @ Split.Else(default) => - log(s"DFLT: ${default.showDbg}") + log(dbg"DFLT: ${default.showDbg}") split case Split.End => Split.End case Split.LetSplit(sym, tail) => Split.LetSplit(sym, normalizeImpl(tail)) @@ -227,23 +227,23 @@ class Normalization(lowering: Lowering)(using tl: TL)(using Raise, Ctx, State, C case Split.End => log("CASE Nil"); N case Split.Else(_) => log("CASE Else"); N case split @ Split.Let(sym, _, tail) => - log(s"CASE Let ${sym}") + log(dbg"CASE Let ${sym}") rec(tail).map(newTail => split.copy(tail = newTail)) case split @ Split.Cons(head @ Branch(thatScrutinee, thatPattern, continuation), tail) => - log(s"CASE Cons ${head.showDbg}") + log(dbg"CASE Cons ${head.showDbg}") if scrutinee.sym is thatScrutinee.sym then mode match case + => - log(s"Case 1.1: $scrutinee === $thatScrutinee") + log(dbg"Case 1.1: $scrutinee === $thatScrutinee") if thatPattern =:= pattern then - log(s"Case 1.1.1: $pattern =:= $thatPattern") + log(dbg"Case 1.1.1: $pattern =:= $thatPattern") S(aliasBindings(pattern, thatPattern)(rec(continuation).getOrElse(continuation) ++ rec(tail).getOrElse(tail))) else if thatPattern <:< pattern then - log(s"Case 1.1.2: $pattern <:< $thatPattern") + log(dbg"Case 1.1.2: $pattern <:< $thatPattern") pattern.markAsRefined rec(tail).map(newTail => split.copy(tail = newTail)) else thatPattern match case thatPattern: FlatPattern.Record => - log(s"Case 1.1.4: $thatPattern is a record") + log(dbg"Case 1.1.4: $thatPattern is a record") // we can use information if pattern is itself a record, or if it is a constructor with arguments val simplifiedRecord = thatPattern assuming pattern if simplifiedRecord.entries.isEmpty then @@ -264,40 +264,40 @@ class Normalization(lowering: Lowering)(using tl: TL)(using Raise, Ctx, State, C // case Pattern.Class(cls, _, _) => cls.toLoc // case _ => thatPattern.toLoc // })) - log(s"case 1.1.5: $pattern <:< $thatPattern") + log(dbg"case 1.1.5: $pattern <:< $thatPattern") N else if areProvablyDisjoint(pattern, thatPattern) then - log(s"Case 1.1.6: $pattern and $thatPattern are provably disjoint") + log(dbg"Case 1.1.6: $pattern and $thatPattern are provably disjoint") S(rec(tail).getOrElse(tail)) else // When patterns are not provably disjoint, we cannot assume // the scrutinee can't match both (e.g., conjunction patterns // like `A & B`). Keep the branch. - log(s"Case 1.1.6: $pattern and $thatPattern are not provably disjoint") + log(dbg"Case 1.1.6: $pattern and $thatPattern are not provably disjoint") (rec(continuation), rec(tail)) match case (N, N) => N case (optCont, optTail) => S(head.copy(continuation = optCont.getOrElse(continuation)) ~: optTail.getOrElse(tail)) case - => - log(s"Case 1.2: $scrutinee === $thatScrutinee") + log(dbg"Case 1.2: $scrutinee === $thatScrutinee") if thatPattern =:= pattern || thatPattern <:< pattern then - log(s"Case 1.2.1: $pattern =:= (or <:<) $thatPattern") + log(dbg"Case 1.2.1: $pattern =:= (or <:<) $thatPattern") S(rec(tail).getOrElse(tail)) else - log(s"Case 1.2.2: $pattern are unrelated to $thatPattern") + log(dbg"Case 1.2.2: $pattern are unrelated to $thatPattern") rec(tail).map(newTail => split.copy(tail = newTail)) else - log(s"Case 2: $scrutinee =/= $thatScrutinee") + log(dbg"Case 2: $scrutinee =/= $thatScrutinee") (rec(continuation), rec(tail)) match case (N, N) => N case (optCont, optTail) => S(head.copy(continuation = optCont.getOrElse(continuation)) ~: optTail.getOrElse(tail)) case split @ Split.LetSplit(sym, tail) => - log(s"CASE LetSplit ${sym.nme}") + log(dbg"CASE LetSplit ${sym.nme}") rec(tail).map(newTail => split.copy(tail = newTail)) case split @ Split.UseSplit(sym) => - log(s"CASE UseSplit ${sym.nme}") + log(dbg"CASE UseSplit ${sym.nme}") // UseSplit references a shared body. If the body mentions the current // scrutinee, inline it and specialize; otherwise keep the reference. // When rec returns N (body unchanged), the UseSplit is preserved to @@ -329,7 +329,7 @@ class Normalization(lowering: Lowering)(using tl: TL)(using Raise, Ctx, State, C Assign(sym, r, lowerSplit(tl, cont)) case Split.Cons(Branch(scrut, pat, tail), restSplit) => subTerm_nonTail(scrut): sr => - tl.log(s"Binding scrut $scrut to $sr (${summon[LoweringCtx].localVals})") + tl.log(dbg"Binding scrut $scrut to $sr (${summon[LoweringCtx].localVals})") def mkMatch(cse: Case -> Block) = Match(sr, cse :: Nil, S(lowerSplit(restSplit, cont)), End() @@ -449,8 +449,8 @@ class Normalization(lowering: Lowering)(using tl: TL)(using Raise, Ctx, State, C def apply(t: Term.IfLike)(k: Result => Block)(using config: Config)(using LoweringCtx): Block = val newSplit = t.split.getExpandedSplit scoped("ucs:desugared"): - log(s"Split with nested patterns:\n${t.split.prettyPrint(t.kw)}") - log(s"Expanded split with flattened patterns:\n${newSplit.prettyPrint}") + log(dbg"Split with nested patterns:\n${t.split.prettyPrint(t.kw)}") + log(dbg"Expanded split with flattened patterns:\n${newSplit.prettyPrint}") this(newSplit, t.form, S(t), k) def apply(t: Term.SynthIf)(k: Result => Block)(using Config, LoweringCtx): Block = @@ -497,7 +497,7 @@ class Normalization(lowering: Lowering)(using tl: TL)(using Raise, Ctx, State, C val normalized = tl.scoped("ucs:normalize"): normalize(inputSplit)(using VarSet()) tl.scoped("ucs:normalized"): - tl.log(s"Normalized:\n${normalized.prettyPrint}") + tl.log(dbg"Normalized:\n${normalized.prettyPrint}") lazy val assignResult = (r: Result) => form match case IfLikeForm.ReturningIf => @@ -574,7 +574,7 @@ class Normalization(lowering: Lowering)(using tl: TL)(using Raise, Ctx, State, C else Begin(body, rest) scoped("ucs:lowered"): - log(s"Lowered:\n${block.showAsTree}") + log(dbg"Lowered:\n${block.showAsTree}") block end Normalization diff --git a/hkmc2/shared/src/main/scala/hkmc2/semantics/ups/Compiler.scala b/hkmc2/shared/src/main/scala/hkmc2/semantics/ups/Compiler.scala index 710ee0b06a..b276459122 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/semantics/ups/Compiler.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/semantics/ups/Compiler.scala @@ -95,13 +95,13 @@ class Compiler(using Context)(using codegen.Erasure)(using tl: TL)(using Ctx, St extension (patterns: Ls[(Label, ExPat)]) /** Specialize a set of patterns. Also display them in the debug log. */ def specializePatterns(head: Opt[Head]): Ls[(Label, SpPat)] = - log(s"Specialized patterns of ${head.fold("the case without tags")(_.showDbg)}:") + log(dbg"Specialized patterns of ${head.fold("the case without tags")(_.showDbg)}:") patterns.map: (label, pattern) => val spec = pattern.specialize(head) val simp = spec.simplify - log(s"• Label $label") - log(s" ‣ Expanded: ${spec.showDbg}") - log(s" ‣ Simplified: ${simp.showDbg}") + log(dbg"• Label $label") + log(dbg" ‣ Expanded: ${spec.showDbg}") + log(dbg" ‣ Simplified: ${simp.showDbg}") (label, simp) private val labelMap: MutMap[Pat, Label] = MutMap() @@ -179,7 +179,7 @@ class Compiler(using Context)(using codegen.Erasure)(using tl: TL)(using Ctx, St // Make a split that tries all branches in order. val topmostSplit = branches.foldLeft(default: Split)((tail, branch) => branch ~: tail) val bodyTerm = SynthIf(topmostSplit) - log(s"Multi-matcher body:\n${topmostSplit.prettyPrint}") + log(dbg"Multi-matcher body:\n${topmostSplit.prettyPrint}") (paramList(param(scrutinee)), bodyTerm) def multiMatcherBranch( @@ -193,7 +193,7 @@ class Compiler(using Context)(using codegen.Erasure)(using tl: TL)(using Ctx, St .mkString("{", ", ", "}")}" ): val fields = patterns.iterator.flatMap((_, p) => p.fields).distinct.toList - log(s"fields: ${fields.iterator.map(_.showDbg).mkString("{", ", ", "}")}") + log(dbg"fields: ${fields.iterator.map(_.showDbg).mkString("{", ", ", "}")}") val subPatternsByField = fields.map: field => field -> patterns.iterator.flatMap((_, p) => p.collectSubPatterns(field)).toList val subScrutinees = Map.from(subPatternsByField.iterator.map: (field, subPatterns) => @@ -202,7 +202,7 @@ class Compiler(using Context)(using codegen.Erasure)(using tl: TL)(using Ctx, St val bindings = subPatternsByField.foldLeft(Ls.empty[Statement]): (statements, entry) => val (field, subPatterns) = entry val subScrutinee = subScrutinees(field) - log(s"subPattern for field ${field.showDbg}: ${ + log(dbg"subPattern for field ${field.showDbg}: ${ subPatterns.iterator.map(_.showDbg).mkString("{", ", ", "}")}") val subMatcherSymbol = buildMultiMatcher(subPatterns.iterator) /** Shorthands for the result of the matcher branch. */ @@ -311,7 +311,7 @@ class Compiler(using Context)(using codegen.Erasure)(using tl: TL)(using Ctx, St val makeMakeSplit = fields.iterator.foldRight( (fields: Ls[(Ident, Term)], bindingsSymbols: Ls[TempSymbol]) => ((makeConsequent, alternative) => - log(s"current bindings: " + aliases.map(_.name).mkString("{", ", ", "}")) + log(dbg"current bindings: " + aliases.map(_.name).mkString("{", ", ", "}")) val currentBindings = aliases.map: alias => RcdField(str(alias.name), scrutinee.safeRef) val bindings = bindingsSymbols match @@ -387,7 +387,7 @@ class Compiler(using Context)(using codegen.Erasure)(using tl: TL)(using Ctx, St val makeMakeSplit = fields.iterator.foldRight( (fields: Ls[RcdField], bindingsSymbols: Ls[TempSymbol]) => ((makeConsequent, alternative) => - log(s"current bindings: " + aliases.map(_.name).mkString("{", ", ", "}")) + log(dbg"current bindings: " + aliases.map(_.name).mkString("{", ", ", "}")) // The current pattern's bindings. val currentBindings = aliases.map: alias => RcdField(str(alias.name), scrutinee.safeRef) diff --git a/hkmc2/shared/src/main/scala/hkmc2/semantics/ups/FixedPointCompiler.scala b/hkmc2/shared/src/main/scala/hkmc2/semantics/ups/FixedPointCompiler.scala index eac76e5137..03ec87f1ec 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/semantics/ups/FixedPointCompiler.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/semantics/ups/FixedPointCompiler.scala @@ -560,10 +560,10 @@ class FixedPointCompiler(using codegen.Erasure)(using tl: TL)(using State, Ctx, // The step pattern is not built from a recursive context. The fixed // point degenerates to a flat contraction loop: keep applying the // step pattern to its own output until it fails. - log(s"No recursive context; compiling a flat contraction loop.") + log(dbg"No recursive context; compiling a flat contraction loop.") S(assemble(entry, Nil, post, requireProgress)) case S((ctxInst, body)) => - log(s"Recursive context: ${ctxInst.showDbg}") + log(dbg"Recursive context: ${ctxInst.showDbg}") val alternatives = body match case Or(alternatives) => alternatives case pattern => pattern :: Nil @@ -800,7 +800,7 @@ class FixedPointCompiler(using codegen.Erasure)(using tl: TL)(using State, Ctx, val (postMatcher, postImpls) = postCompiler.buildMatcher(postPattern, ResultMode.Full) (postMatcher, postImpls) - log(s"Classes: ${classes.map(cls => s"${cls.symbol.nme}(${cls.alts})").mkString(", ")}") + log(dbg"Classes: ${classes.map(cls => s"${cls.symbol.nme}(${cls.alts})").mkString(", ")}") // ---- Machine state ---- val inputSymbol = VarSymbol(Ident("input"), erasedType = N) diff --git a/hkmc2/shared/src/main/scala/hkmc2/semantics/ups/Instantiator.scala b/hkmc2/shared/src/main/scala/hkmc2/semantics/ups/Instantiator.scala index e757cb8695..1c19d55848 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/semantics/ups/Instantiator.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/semantics/ups/Instantiator.scala @@ -49,7 +49,7 @@ class Instantiator(using codegen.Erasure)(using tl: TL)(using Ctx, State, Raise) val subst = defn.patternParams.iterator.map(_.sym).zip(instantiation.arguments).toMap log("Instantiating " + instantiation.showDbg) val instantiated = instantiate(defn.pattern)(using subst) - log(s"Instantiated ${instantiation.showDbg}\n" + + log(dbg"Instantiated ${instantiation.showDbg}\n" + s"arity = ${instantiation.symbol.defn.get.patternParams.size}\n" + s"arguments = ${instantiation.arguments.iterator.map(_.showDbg).mkString(", ")}\n" + s"instantiated = ${instantiated.showDbg}") @@ -68,7 +68,7 @@ class Instantiator(using codegen.Erasure)(using tl: TL)(using Ctx, State, Raise) progress += (instantiation -> N) worklist.enqueue(instantiation) else - log(s"Already instantiated ${instantiation.showDbg}") + log(dbg"Already instantiated ${instantiation.showDbg}") progress(instantiation) instantiation diff --git a/hkmc2/shared/src/main/scala/hkmc2/semantics/ups/SplitCompiler.scala b/hkmc2/shared/src/main/scala/hkmc2/semantics/ups/SplitCompiler.scala index 5fecd56947..5e97b53948 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/semantics/ups/SplitCompiler.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/semantics/ups/SplitCompiler.scala @@ -1339,7 +1339,7 @@ class SplitCompiler(using codegen.Erasure)(using tl: TL)(using State, Ctx, Raise alternative = failure ) pd.extractionParams.map(_.sym) - log(s"Translated `unapply`: ${topmost.prettyPrint}") + log(dbg"Translated `unapply`: ${topmost.prettyPrint}") makeMethod("unapply", pd.patternParams, inputSymbol, topmost) // TODO: Use `pd.extractionParams`. val unapplyStringPrefix = scoped("ucs:cp"): @@ -1350,7 +1350,7 @@ class SplitCompiler(using codegen.Erasure)(using tl: TL)(using State, Ctx, Raise val topmost = makeStringPrefixMatchSplit(inputSymbol.toScrut, pd.pattern) ((consumedOutput, remainingOutput, bindings) => Split.Else: makeMatchSuccess(tup(fld(consumedOutput()), fld(remainingOutput()))), failure) - log(s"Translated `unapplyStringPrefix`: ${topmost.prettyPrint}") + log(dbg"Translated `unapplyStringPrefix`: ${topmost.prettyPrint}") makeMethod("unapplyStringPrefix", pd.patternParams, inputSymbol, topmost) unapply :: unapplyStringPrefix :: Nil @@ -1386,7 +1386,7 @@ class SplitCompiler(using codegen.Erasure)(using tl: TL)(using State, Ctx, Raise val inputSymbol = VarSymbol(Ident("input"), erasedType = N) val topmost = makeMatchSplit(inputSymbol.toScrut, pattern, true) ((output, bindings) => Split.Else(makeMatchSuccess(output())), failure) - log(s"Translated `unapply`: ${topmost.prettyPrint}") + log(dbg"Translated `unapply`: ${topmost.prettyPrint}") makeUnapplyRecordStatements("unapply", patternParams, inputSymbol, topmost) val unapplyStringPrefix = scoped("ucs:cp"): // We don't report errors here because they have been already reported in @@ -1396,6 +1396,6 @@ class SplitCompiler(using codegen.Erasure)(using tl: TL)(using State, Ctx, Raise val topmost = makeStringPrefixMatchSplit(inputSymbol.toScrut, pattern) ((consumedOutput, remainingOutput, bindings) => Split.Else: makeMatchSuccess(tup(fld(consumedOutput()), fld(remainingOutput()))), failure) - log(s"Translated `unapplyStringPrefix`: ${topmost.prettyPrint}") + log(dbg"Translated `unapplyStringPrefix`: ${topmost.prettyPrint}") makeUnapplyRecordStatements("unapplyStringPrefix", patternParams, inputSymbol, topmost) Term.Rcd(false, unapply ::: unapplyStringPrefix) diff --git a/hkmc2/shared/src/main/scala/hkmc2/utils/ReplHost.scala b/hkmc2/shared/src/main/scala/hkmc2/utils/ReplHost.scala index fa006ea449..74e66ca9cc 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/utils/ReplHost.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/utils/ReplHost.scala @@ -52,7 +52,7 @@ class ReplHost(rootPath: Str)(using TL): case Some(syntaxErrorLine) => val message = syntaxErrorLine.substring(ReplHost.syntaxErrorHead.length) ReplHost.Error(true, message, reply.take(reply.indexOf(syntaxErrorLine)).trim()) - tl.log(s"REPL> Collected:\n${res}") + tl.log(dbg"REPL> Collected:\n${res}") res private def consumeStderr(): String = @@ -83,7 +83,7 @@ class ReplHost(rootPath: Str)(using TL): ReplHost.Error(false, reply.substring(begin + 1, end), reply.takeWhile(_ != 0x200b).trim()) else reply case error: ReplHost.Error => error - tl.log(s"REPL> Parsed:\n${parsed}") + tl.log(dbg"REPL> Parsed:\n${parsed}") parsed /** @@ -92,7 +92,7 @@ class ReplHost(rootPath: Str)(using TL): * @param code the code to execute */ private def send(code: Str): Unit = - tl.log(s"REPL> Sending: ${code}") + tl.log(dbg"REPL> Sending: ${code}") stdin.write(if code.endsWith("\n") then code else code + "\n") stdin.flush() diff --git a/hkmc2/shared/src/main/scala/hkmc2/utils/utils.scala b/hkmc2/shared/src/main/scala/hkmc2/utils/utils.scala index 52d6da3a07..e763c56bdd 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/utils/utils.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/utils/utils.scala @@ -52,13 +52,55 @@ extension (t: Any) ts.printPlain(t) -class DebugPrinter: +/** Interpolate compiler values through the selected debug printer, including symbols nested in + * collections and products. Ordinary string interpolation cannot carry a printing policy. */ +extension (context: StringContext) + def dbg(values: Any*)(using printer: DebugPrinter): Str = + context.s(values.map(printer.printPlain)*) + + +class DebugPrinter( + val showUids: Bool, + uidOwner: Opt[semantics.Elaborator.State], + symbolName: semantics.Symbol => Str, + val preProcess: Product => Product, + val postProcess: Product => Str, +): + def this() = this(false, N, sym => sym.prefix + sym.nme, identity, _ => "") + def this(owner: semantics.Elaborator.State) = this(false, S(owner), sym => sym.prefix + sym.nme, identity, _ => "") + given DebugPrinter = this - def preProcess(t: Product): Product = t - def postProcess(t: Product): Str = "" + /** A printing operation captures an immutable policy; changing a trace scope cannot change + * a printer already passed to a renderer. Name allocation and tree hooks are preserved. */ + def withUids(enabled: Bool): DebugPrinter = + new DebugPrinter(enabled, uidOwner, symbolName, preProcess, postProcess) - def printPlain(v: Any): Str = v.toString + def printPlain(v: Any): Str = v match + case null => "null" + case sym: semantics.Symbol => + // Numeric UIDs are unique only within their allocating unit. Foreign symbols must not + // acquire a local-looking identity just because the importing unit enables UID printing. + val showUid = showUids && uidOwner.exists(_ is sym.getState) + symbolName(sym) + (if showUid then s"‹${sym.uid}›" else "") + // Shapes contain inference graphs. Their explicit summaries avoid traversing those graphs. + case shape: semantics.Shape => shape.shwDbg + case marks: semantics.Marks => marks.showDbg + case flags: TermDefFlags => flags.toString + case flags: FldFlags => flags.toString + case flags: ParamListFlags => flags.toString + case path: io.Path => path.toString + case path: io.RelPath => path.toString + case xs: List[?] => xs.iterator.map(printPlain).mkString("List(", ", ", ")") + case xs: Vector[?] => xs.iterator.map(printPlain).mkString("Vector(", ", ", ")") + case xs: Array[?] => xs.iterator.map(printPlain).mkString("Array(", ", ", ")") + case xs: collection.Map[?, ?] => + xs.iterator.map((k, v) => s"${printPlain(k)} -> ${printPlain(v)}").mkString("Map(", ", ", ")") + case xs: collection.Set[?] => xs.iterator.map(printPlain).mkString("Set(", ", ", ")") + case tuple: Tuple => tuple.productIterator.map(printPlain).mkString("[", ", ", "]") + case p: Product if p.productArity > 0 => + p.productIterator.map(printPlain).mkString(p.productPrefix + "(", ",", ")") + case _ => v.toString def printProduct(inTailPos: Bool, t: Product): Str = diff --git a/hkmc2/shared/src/main/scala/utils/TraceLogger.scala b/hkmc2/shared/src/main/scala/utils/TraceLogger.scala index 2f815e9a23..93bd688da1 100644 --- a/hkmc2/shared/src/main/scala/utils/TraceLogger.scala +++ b/hkmc2/shared/src/main/scala/utils/TraceLogger.scala @@ -14,7 +14,9 @@ final class TraceLogger( force: Opt[Str] => Bool, output: (Config.DebugOutput, Str) => Unit, indentWhenDisabled: Bool, -)(using val debugPrinter: DebugPrinter): +)(using printer: DebugPrinter): + def debugPrinter: DebugPrinter = printer.withUids(debug.showUids) + def isTracing: Bool = phase(debug.config) || force(scope) /** Commands can force a trace without enabling the phase in source settings. @@ -22,17 +24,17 @@ final class TraceLogger( private def debugOutput: Config.DebugOutput = if phase(debug.config) then debug.config.out else Config.DebugOutput.StdIO - private val noPostTrace: Any => Str = _ => "" + private val noPostTrace: Any => DebugPrinter ?=> Str = _ => "" private var indent = 0 - def trace[T](pre: => Str, post: T => Str = noPostTrace)(thunk: => T): T = + def trace[T](pre: DebugPrinter ?=> Str, post: T => DebugPrinter ?=> Str = noPostTrace)(thunk: => T): T = log(pre) val indentTrace = indentWhenDisabled || isTracing if indentTrace then enter() val res = try thunk finally if indentTrace then exit() if post isnt noPostTrace then log(post(res)) res - inline def traceNot[T](pre: => Str, post: T => Str = noPostTrace)(thunk: => T): T = + inline def traceNot[T](pre: DebugPrinter ?=> Str, post: T => DebugPrinter ?=> Str = noPostTrace)(thunk: => T): T = thunk inline def enter() = indent += 1 @@ -40,17 +42,17 @@ final class TraceLogger( private[hkmc2] def emitDbg(str: Str): Unit = output(Config.DebugOutput.StdIO, str) - inline def log(msg: => Any): Unit = log(msg, noIndent = false) - - def logs(msgs: => Any*): Unit = - if isTracing then msgs.foreach(log(_)) + def logs(msgs: (DebugPrinter ?=> Any)*): Unit = + if isTracing then msgs.foreach(msg => log(msg)) - def log(msg: => Any, noIndent: Bool = false): Unit = - if isTracing then output( - debugOutput, - if noIndent then msg.toString - else "| " * indent + msg.toString.indentNewLines("| " * indent + "> "), - ) + def log(msg: DebugPrinter ?=> Any, noIndent: Bool = false): Unit = + if isTracing then + val printer = debugPrinter + val rendered = printer.printPlain(msg(using printer)) + output(debugOutput, + if noIndent then rendered + else "| " * indent + rendered.indentNewLines("| " * indent + "> "), + ) def traceDefinition[T](annotations: Ls[semantics.Annot], base: Config)(body: => T): T = debug.traceDefinition(annotations, base, phase)(body) @@ -69,7 +71,7 @@ final class TraceLogger( object TraceLogger: /** A logger controlled independently of source directives, such as a diff-test command or a pass flag. */ - def apply(enabled: Opt[Str] => Bool, output: Str => Unit)(using DebugPrinter): TraceLogger = - new TraceLogger(new DebugContext, _ => false, enabled, (_, line) => output(line), indentWhenDisabled = true) + def apply(enabled: Opt[Str] => Bool, output: Str => Unit)(using printer: DebugPrinter): TraceLogger = + new TraceLogger(new DebugContext(() => printer.showUids), _ => false, enabled, (_, line) => output(line), indentWhenDisabled = true) def silent(using DebugPrinter): TraceLogger = apply(_ => false, line => scala.Predef.println(line)) diff --git a/hkmc2/shared/src/test/mlscript/codegen/DebugDestinations.mls b/hkmc2/shared/src/test/mlscript/codegen/DebugDestinations.mls index af1288d8d3..95ffa3e540 100644 --- a/hkmc2/shared/src/test/mlscript/codegen/DebugDestinations.mls +++ b/hkmc2/shared/src/test/mlscript/codegen/DebugDestinations.mls @@ -29,8 +29,8 @@ fs.readFileSync(debugFile, "utf8") == "Lowering.term 42\nLowering.term 45\n" @dbg(showUids: false) fun forced_output() = 47 //│ Lowering.term 47 //│ Lowering.term undefined -//│ avoidNames Scoped(Set(member:forced_output),Define(FunDefn(None,member:forced_output,term:forced_output,List(Pa... -//│ | avoidNames Define(FunDefn(None,member:forced_output,term:forced_output,List(ParamList(‹›,List(),None)),Return(L... +//│ avoidNames Scoped(Set(member:forced_output⁰),Define(FunDefn(None,member:forced_output⁰,term:forced_output¹,List... +//│ | avoidNames Define(FunDefn(None,member:forced_output⁰,term:forced_output¹,List(ParamList(‹›,List(),None)),Return... //│ | | avoidNames Return(Lit(IntLit(47)))... //│ | | avoidNames End()... diff --git a/hkmc2/shared/src/test/mlscript/codegen/DebugDirective.mls b/hkmc2/shared/src/test/mlscript/codegen/DebugDirective.mls index 27197741c5..3943f9b339 100644 --- a/hkmc2/shared/src/test/mlscript/codegen/DebugDirective.mls +++ b/hkmc2/shared/src/test/mlscript/codegen/DebugDirective.mls @@ -30,7 +30,7 @@ fun persistently_debugged(x) = x //│ | | Elab subterm Ident(x) //│ | | ~> x¹ //│ | ~> x¹ -//│ ~> { fun member:persistently_debugged(x¹) = x¹; } +//│ ~> { fun member:persistently_debugged⁰(x¹) = x¹; } #dbg(elaboration: false) @@ -130,9 +130,9 @@ fun shadowing(x) = //│ ———————————————| Lowered IR |——————————————————————————————————————————————————————————————————————— //│ return 42 //│ ⬤ Simplif. iter. 1 -//│ Captured variables: HashSet() +//│ Captured variables: Set() //│ ⬤ Simplif. iter. 1 -//│ Captured variables: HashSet() +//│ Captured variables: Set() //│ ——————————————| Optimized IR |—————————————————————————————————————————————————————————————————————— //│ return 42 //│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— diff --git a/hkmc2/shared/src/test/mlscript/codegen/DebugScoping.mls b/hkmc2/shared/src/test/mlscript/codegen/DebugScoping.mls index 1e4d4d7285..9bc54ecb7a 100644 --- a/hkmc2/shared/src/test/mlscript/codegen/DebugScoping.mls +++ b/hkmc2/shared/src/test/mlscript/codegen/DebugScoping.mls @@ -6,9 +6,9 @@ module DebugMethods with @dbg('lowering) fun selected(x) = x fun sibling(x) = x -//│ Lowering.term x⁰ – symbol x -//│ Expanding term Ref(x) -//│ ~> Ref(x) +//│ Lowering.term x⁰ – symbol x⁰ +//│ Expanding term Ref(x⁰) +//│ ~> Ref(x⁰) :expect 7 DebugMethods.selected(7) @@ -20,16 +20,16 @@ DebugMethods.selected(7) @dbg(showUids: false) fun debug_inner() = 42 @dbg(lowering: false) fun debug_muted() = 43 debug_inner() -//│ Lowering.term { fun member:debug_inner() = 42; fun member:debug_muted() = 43; member:debug_inner⁰() } +//│ Lowering.term { fun member:debug_inner⁰() = 42; fun member:debug_muted⁰() = 43; member:debug_inner⁰() } //│ Lowering.term 42 //│ Lowering.term member:debug_inner⁰() -//│ Expanding term App(Ref(member:debug_inner),Tup(List())) -//│ ~> App(Ref(member:debug_inner),Tup(List())) -//│ Expanding term Ref(member:debug_inner) -//│ ~> Resolved(Ref(member:debug_inner),term:debug_inner) -//│ Lowering.term member:debug_inner⁰‹term:debug_inner› – symbol term:debug_inner -//│ Expanding term Resolved(Ref(member:debug_inner),term:debug_inner) -//│ ~> Resolved(Ref(member:debug_inner),term:debug_inner) +//│ Expanding term App(Ref(member:debug_inner⁰),Tup(List())) +//│ ~> App(Ref(member:debug_inner⁰),Tup(List())) +//│ Expanding term Ref(member:debug_inner⁰) +//│ ~> Resolved(Ref(member:debug_inner⁰),term:debug_inner¹) +//│ Lowering.term member:debug_inner⁰‹term:debug_inner¹› – symbol term:debug_inner¹ +//│ Expanding term Resolved(Ref(member:debug_inner⁰),term:debug_inner¹) +//│ ~> Resolved(Ref(member:debug_inner⁰),term:debug_inner¹) // Diff-test debug commands trace the whole block. A definition's @dbg annotation that does not @@ -38,8 +38,8 @@ DebugMethods.selected(7) @dbg(showUids: false) fun command_lowering() = 1601 //│ Lowering.term 1601 //│ Lowering.term undefined -//│ avoidNames Scoped(Set(member:command_lowering),Define(FunDefn(None,member:command_lowering,term:command_lowerin... -//│ | avoidNames Define(FunDefn(None,member:command_lowering,term:command_lowering,List(ParamList(‹›,List(),None)),Re... +//│ avoidNames Scoped(Set(member:command_lowering⁰),Define(FunDefn(None,member:command_lowering⁰,term:command_lower... +//│ | avoidNames Define(FunDefn(None,member:command_lowering⁰,term:command_lowering¹,List(ParamList(‹›,List(),None)),... //│ | | avoidNames Return(Lit(IntLit(1601)))... //│ | | avoidNames End()... @@ -53,7 +53,7 @@ DebugMethods.selected(7) //│ | | Elab subterm Ident(x) //│ | | ~> x¹ //│ | ~> x¹ -//│ ~> { fun member:command_elaboration(x¹) = x¹; } +//│ ~> { fun member:command_elaboration⁰(x¹) = x¹; } // A class's lowering trace includes its superclass reference, @@ -61,14 +61,30 @@ DebugMethods.selected(7) class DebugParent @dbg('lowering, showUids: false) class DebugChild extends DebugParent print(1701) -//│ Lowering.term member:DebugParent⁰ – symbol class:DebugParent -//│ Expanding term Ref(member:DebugParent) -//│ ~> Resolved(Ref(member:DebugParent),class:DebugParent) +//│ Lowering.term member:DebugParent⁰ – symbol class:DebugParent¹ +//│ Expanding term Ref(member:DebugParent⁰) +//│ ~> Resolved(Ref(member:DebugParent⁰),class:DebugParent¹) //│ Lowering.term { } (in stmt) //│ Lowering.term undefined (in stmt) //│ > 1701 +// An error in an annotated definition must not enable tracing for its following sibling. +:noCodeGen +:e +@dbg('elaboration, showUids: false) +fun debug_error() = missing_debug_name +fun after_debug_error(x) = x +//│ Processing term definition App(Ident(debug_error),Tup(List())) +//│ Elab term Ident(missing_debug_name) +//│ | Elab subterm Ident(missing_debug_name) +//│ ╔══[COMPILATION ERROR] Name not found: missing_debug_name +//│ ║ l.76: fun debug_error() = missing_debug_name +//│ ╙── ^^^^^^^^^^^^^^^^^^ +//│ | ~> +//│ ~> + + // Local values lower to assignments; their debug scope must end before the next value. #lang(0.3.x) @@ -101,27 +117,27 @@ module DebugCompanion with //│ | | = InstanceShape(missing) //│ | = MarkedShape(InstanceShape(missing), ↘⟨class:DebugCompanion⁰⟩ϵ) //│ Processing class definition Ident(DebugCompanion) -//│ | Companion: Some(module:DebugCompanion) -//│ | Elab block List(TermDef(Fun,App(Ident(answer),Tup(List())),Some(IntLit(1101)))) InnerScope(class:DebugCompanion) +//│ | Companion: Some(module:DebugCompanion¹) +//│ | Elab block List(TermDef(Fun,App(Ident(answer),Tup(List())),Some(IntLit(1101)))) InnerScope(class:DebugCompanion⁰) //│ | | Processing term definition App(Ident(answer),Tup(List())) //│ | | Elab term IntLit(1101) //│ | | | Elab subterm IntLit(1101) //│ | | | ~> 1101 //│ | | ~> 1101 -//│ | ~> { method fun member:answer() = 1101; } +//│ | ~> { method fun member:answer⁰() = 1101; } //│ Processing module/object definition Ident(DebugCompanion) -//│ | Companion: Some(class:DebugCompanion) -//│ | Elab block List(TermDef(Fun,App(Ident(answer),Tup(List())),Some(IntLit(1102)))) InnerScope(module:DebugCompanion) +//│ | Companion: Some(class:DebugCompanion⁰) +//│ | Elab block List(TermDef(Fun,App(Ident(answer),Tup(List())),Some(IntLit(1102)))) InnerScope(module:DebugCompanion¹) //│ | | Processing term definition App(Ident(answer),Tup(List())) //│ | | Elab term IntLit(1102) //│ | | | Elab subterm IntLit(1102) //│ | | | ~> 1102 //│ | | ~> 1102 -//│ | ~> { fun member:answer() = 1102; } +//│ | ~> { fun member:answer¹() = 1102; } //│ Lowering.term 1101 -//│ Lowering.term { val member:value = value⁰^term:DebugCompanion⁰/value¹; } (in stmt) -//│ Lowering.term value⁰^term:DebugCompanion⁰/value¹ – symbol value -//│ Lowering.term value⁰ – symbol value +//│ Lowering.term { val member:value⁰ = value¹^term:DebugCompanion⁰/value²; } (in stmt) +//│ Lowering.term value¹^term:DebugCompanion⁰/value² – symbol value¹ +//│ Lowering.term value¹ – symbol value¹ //│ Lowering.term undefined (in stmt) //│ Lowering.term 1102 //│ Lowering.term { } (in stmt) diff --git a/hkmc2/shared/src/test/mlscript/codegen/DebugUids.mls b/hkmc2/shared/src/test/mlscript/codegen/DebugUids.mls new file mode 100644 index 0000000000..6a73265a2b --- /dev/null +++ b/hkmc2/shared/src/test/mlscript/codegen/DebugUids.mls @@ -0,0 +1,45 @@ +:js + + +// Imported symbols retain disambiguated names, but only the current unit's symbols get numeric UIDs. +:silent +fs.writeFileSync("target/DebugUidLibrary.mls", "#lang(0.3.x)\nmodule DebugUidLibrary with\n fun value() = 23\n") + +:noCodeGen +import "../../../../../../target/DebugUidLibrary.mls" + + +@dbg('lowering, showUids: true, out: File("../../../../../../target/DebugUids.log")) +fun with_uids(x) = DebugUidLibrary.value() + x +@dbg('lowering, showUids: false, out: File("../../../../../../target/DebugNoUids.log")) +fun without_uids(x) = DebugUidLibrary.value() + x + +:expect false +new RegExp("DebugUidLibrary[⁰-⁹]*‹[0-9]+›").test(fs.readFileSync("target/DebugUids.log", "utf8")) +//│ = false + +:expect true +:silent +let trace = fs.readFileSync("target/DebugUids.log", "utf8") +trace.includes("DebugUidLibrary") && new RegExp("x[⁰-⁹]*‹[0-9]+›").test(trace) + +:expect false +new RegExp("‹[0-9]+›").test(fs.readFileSync("target/DebugNoUids.log", "utf8")) +//│ = false + + +// A nested printer override must end before the next definition is lowered. +@dbg('lowering, showUids: false, out: File("../../../../../../target/DebugNestedUids.log")) +fun nested_uids(x) = + @dbg(showUids: true, out: File("../../../../../../target/DebugInnerUids.log")) + fun inner(y) = y + fun sibling(z) = z + inner(x) + sibling(x) + +:expect true +new RegExp("‹[0-9]+›").test(fs.readFileSync("target/DebugInnerUids.log", "utf8")) +//│ = true + +:expect false +new RegExp("‹[0-9]+›").test(fs.readFileSync("target/DebugNestedUids.log", "utf8")) +//│ = false diff --git a/hkmc2/shared/src/test/mlscript/newres/Functions.mls b/hkmc2/shared/src/test/mlscript/newres/Functions.mls index 078790cfc6..e1db36f35c 100644 --- a/hkmc2/shared/src/test/mlscript/newres/Functions.mls +++ b/hkmc2/shared/src/test/mlscript/newres/Functions.mls @@ -176,8 +176,8 @@ callid1(Predef) //│ | | = MarkedShape(IntroShape(1), ↘⟨term:Predef¹/id¹⟩%⟨⋅id⁰⟩ϵ) //│ | = MarkedShape(IntroShape(1), ↘⟨term:Predef¹/id¹⟩%⟨⋅id⁰⟩ϵ) //│ | appShape isSaturated? true; head? member:id⁰, List(↘⟨term:callid1²⟩%⟨callid1¹⟩↗⟨term:Predef¹/id¹⟩%⟨⋅id⁰⟩ϵ) -//│ | lhs false List(((x⁰),List(↘⟨term:callid1²⟩%⟨callid1¹⟩↗⟨term:Predef¹/id¹⟩%⟨⋅id⁰⟩ϵ))) -//│ | appShape: td.body = Some(SimpleRef(x)) +//│ | lhs false List([(x⁰), List(↘⟨term:callid1²⟩%⟨callid1¹⟩↗⟨term:Predef¹/id¹⟩%⟨⋅id⁰⟩ϵ)]) +//│ | appShape: td.body = Some(SimpleRef(x⁰)) //│ | listenTerm: trm = x⁰ //│ | listen: trm = x⁰ //│ | .exit MarkedShape(UnknownValueShape(x⁰), ↘⟨term:Predef¹/id¹⟩%⟨exposed⁰⟩ϵ) (↘⟨term:callid1²⟩%⟨callid1¹⟩↗⟨term:Predef¹/id¹⟩%⟨⋅id⁰⟩ϵ) @@ -197,8 +197,8 @@ callid1(Predef) //│ | | = IntroShape(1) //│ | = MarkedShape(IntroShape(1), ↘⟨term:callid1²⟩%⟨callid1¹⟩ϵ) //│ | appShape isSaturated? true; head? member:callid1⁰, List(↗⟨term:callid1²⟩%⟨callid1¹⟩ϵ) -//│ | lhs false List(((p⁰),List(↗⟨term:callid1²⟩%⟨callid1¹⟩ϵ))) -//│ | appShape: td.body = Some(App(NewSel(SimpleRef(p),Ident(id),None),Tup(List(Fld(‹›,Lit(IntLit(1)),None))))) +//│ | lhs false List([(p⁰), List(↗⟨term:callid1²⟩%⟨callid1¹⟩ϵ)]) +//│ | appShape: td.body = Some(App(NewSel(SimpleRef(p⁰),Ident(id),None),Tup(List(Fld(‹›,Lit(IntLit(1)),None))))) //│ | listenTerm: trm = p⁰.id(1) //│ | listen: trm = p⁰.id(1) //│ | .exit MarkedShape(IntroShape(1), ↘⟨term:callid1²⟩%⟨callid1¹⟩ϵ) (↗⟨term:callid1²⟩%⟨callid1¹⟩ϵ) @@ -207,7 +207,7 @@ callid1(Predef) //│ | | .exit MarkedShape (MarkedShape(IntroShape(1), ↘⟨term:callid1²⟩%⟨callid1¹⟩ϵ), term:callid1², callid1¹) //│ | | = IntroShape(1) //│ | = IntroShape(1) -//│ ~> { fun member:callid1(p⁰) = p⁰.id(1); member:callid1⁰(member:Predef⁰) } +//│ ~> { fun member:callid1⁰(p⁰) = p⁰.id(1); member:callid1⁰(member:Predef⁰) } //│ listenTerm: trm = member:callid1⁰(member:Predef⁰) //│ listen: trm = member:callid1⁰(member:Predef⁰) //│ ————————————————| Resolved |———————————————————————————————————————————————————————————————————————— diff --git a/hkmc2/shared/src/test/mlscript/opt/ConstantFolding.mls b/hkmc2/shared/src/test/mlscript/opt/ConstantFolding.mls index 3d0ce0fad6..c95806fb56 100644 --- a/hkmc2/shared/src/test/mlscript/opt/ConstantFolding.mls +++ b/hkmc2/shared/src/test/mlscript/opt/ConstantFolding.mls @@ -27,7 +27,7 @@ in set y += 1 print(x + y) //│ ⬤ Simplif. iter. 1 -//│ Captured variables: HashSet() +//│ Captured variables: Set() //│ !! Change triggered { x⁰ ~> 2 } //│ !! Change triggered { y⁰ ~> 2 } //│ !! Change triggered { tmp:tmp⁰ ~> 3 } @@ -52,11 +52,11 @@ in //│ !! Change triggered { rm tmp:tmp¹ = 6 } //│ ▶ DCE: //│ > return Predef⁰.print⁰(6) -//│ Captured variables: HashSet() +//│ Captured variables: Set() //│ ⬤ Simplif. iter. 3 -//│ Captured variables: HashSet() +//│ Captured variables: Set() //│ ⬤ Simplif. iter. 1 -//│ Captured variables: HashSet() +//│ Captured variables: Set() //│ ——————————————| Optimized IR |—————————————————————————————————————————————————————————————————————— //│ return Predef⁰.print⁰(6) //│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— diff --git a/hkmc2/shared/src/test/mlscript/parser/Handler.mls b/hkmc2/shared/src/test/mlscript/parser/Handler.mls index e424c7aa11..69cb8603ef 100644 --- a/hkmc2/shared/src/test/mlscript/parser/Handler.mls +++ b/hkmc2/shared/src/test/mlscript/parser/Handler.mls @@ -51,7 +51,7 @@ handle h = Eff with fun f()(r) = r(0) in foo(h) -//│ Elab: { handle h = Ref(member:Eff)() List(HandlerTermDefinition(r,TermDefinition(Fun,member:f,term:Handler$h$/f,List(ParamList(‹›,List(),None)),None,None,Some(App(Ref(r),Tup(List(Fld(‹›,Lit(IntLit(0)),None))))),‹method ›,Modulefulness(None),List(),None))) in App(Ref(member:foo),Tup(List(Fld(‹›,Ref(h),None)))) } +//│ Elab: { handle h⁰ = member:Eff⁰() List(HandlerTermDefinition(r⁰,TermDefinition(Fun,member:f⁰,term:Handler$h$⁰/f¹,List(ParamList(‹›,List(),None)),None,None,Some(App(Ref(r⁰),Tup(List(Fld(‹›,Lit(IntLit(0)),None))))),‹method ›,Modulefulness(None),List(),None))) in member:foo⁰(h⁰) } :e ( @@ -75,7 +75,7 @@ handle h = Mod.Eff(3) with fun f()(r) = r(0) fun g(a)()()(r) = r(1) foo(h) -//│ Elab: { handle h = Sel(Ref(member:Mod),Ident(Eff))(Lit(IntLit(3))) List(HandlerTermDefinition(r,TermDefinition(Fun,member:f,term:Handler$h$/f,List(ParamList(‹›,List(),None)),None,None,Some(App(Ref(r),Tup(List(Fld(‹›,Lit(IntLit(0)),None))))),‹method ›,Modulefulness(None),List(),None)), HandlerTermDefinition(r,TermDefinition(Fun,member:g,term:Handler$h$/g,List(ParamList(‹›,List(Param(‹›,a,None,Modulefulness(None))),None), ParamList(‹›,List(),None), ParamList(‹›,List(),None)),None,None,Some(App(Ref(r),Tup(List(Fld(‹›,Lit(IntLit(1)),None))))),‹method ›,Modulefulness(None),List(),None))) in App(Ref(member:foo),Tup(List(Fld(‹›,Ref(h),None)))) } +//│ Elab: { handle h¹ = member:Mod⁰.Eff‹member:Eff¹›(3) List(HandlerTermDefinition(r¹,TermDefinition(Fun,member:f²,term:Handler$h$¹/f³,List(ParamList(‹›,List(),None)),None,None,Some(App(Ref(r¹),Tup(List(Fld(‹›,Lit(IntLit(0)),None))))),‹method ›,Modulefulness(None),List(),None)), HandlerTermDefinition(r²,TermDefinition(Fun,member:g⁰,term:Handler$h$¹/g¹,List(ParamList(‹›,List(Param(‹›,a⁰,None,Modulefulness(None))),None), ParamList(‹›,List(),None), ParamList(‹›,List(),None)),None,None,Some(App(Ref(r²),Tup(List(Fld(‹›,Lit(IntLit(1)),None))))),‹method ›,Modulefulness(None),List(),None))) in member:foo⁰(h¹) } :e handle h = Eff with @@ -131,4 +131,4 @@ foo(h) //│ ╔══[WARNING] Terms in handler block do nothing //│ ║ l.129: 12345 //│ ╙── ^^^^^ -//│ Elab: { handle h = Ref(member:Eff)() List(HandlerTermDefinition(r,TermDefinition(Fun,member:f,term:Handler$h$/f,List(ParamList(‹›,List(),None)),None,None,Some(App(Ref(r),Tup(List(Fld(‹›,Lit(IntLit(0)),None))))),‹method ›,Modulefulness(None),List(),None)), HandlerTermDefinition(r,TermDefinition(Fun,member:g,term:Handler$h$/g,List(ParamList(‹›,List(Param(‹›,a,None,Modulefulness(None))),None)),None,None,Some(App(Ref(r),Tup(List(Fld(‹›,Lit(IntLit(1)),None))))),‹method ›,Modulefulness(None),List(),None))) in App(Ref(member:foo),Tup(List(Fld(‹›,Ref(h),None)))) } +//│ Elab: { handle h² = member:Eff⁰() List(HandlerTermDefinition(r³,TermDefinition(Fun,member:f⁴,term:Handler$h$²/f⁵,List(ParamList(‹›,List(),None)),None,None,Some(App(Ref(r³),Tup(List(Fld(‹›,Lit(IntLit(0)),None))))),‹method ›,Modulefulness(None),List(),None)), HandlerTermDefinition(r⁴,TermDefinition(Fun,member:g²,term:Handler$h$²/g³,List(ParamList(‹›,List(Param(‹›,a¹,None,Modulefulness(None))),None)),None,None,Some(App(Ref(r⁴),Tup(List(Fld(‹›,Lit(IntLit(1)),None))))),‹method ›,Modulefulness(None),List(),None))) in member:foo⁰(h²) } diff --git a/hkmc2/shared/src/test/mlscript/ucs/normalization/ExcessiveDeduplication.mls b/hkmc2/shared/src/test/mlscript/ucs/normalization/ExcessiveDeduplication.mls index c7cd8af007..678223a1be 100644 --- a/hkmc2/shared/src/test/mlscript/ucs/normalization/ExcessiveDeduplication.mls +++ b/hkmc2/shared/src/test/mlscript/ucs/normalization/ExcessiveDeduplication.mls @@ -54,11 +54,11 @@ if //│ Normalized: //│ > ‹if|while› //│ > let-split σ = y is []=1 and -//│ > let tmp:element0$ = ((tmp:runtime⁰.)Tuple‹module:Tuple›.)get‹term:Tuple/get›(y⁰, 0) +//│ > let tmp:element0$ = ((tmp:runtime⁰.)Tuple‹module:Tuple⁰›.)get‹term:Tuple⁰/get⁰›(y⁰, 0) //│ > let b = tmp:element0$⁰ //│ > else -//│ > let x -//│ > x = 1 +//│ > let x⁰ +//│ > x⁰ = 1 //│ > builtin:+⁰(x⁰, b⁰) //│ > x is true and //│ > z is true then 1 diff --git a/hkmc2/shared/src/test/mlscript/ucs/normalization/ScrutineeIdentity.mls b/hkmc2/shared/src/test/mlscript/ucs/normalization/ScrutineeIdentity.mls index 646ae77a3c..148824cf13 100644 --- a/hkmc2/shared/src/test/mlscript/ucs/normalization/ScrutineeIdentity.mls +++ b/hkmc2/shared/src/test/mlscript/ucs/normalization/ScrutineeIdentity.mls @@ -32,8 +32,8 @@ fun tuple(x) = if //│ Normalized: //│ > ‹if|while› //│ > x is []=2 and -//│ > let tmp:element0$ = ((tmp:runtime⁰.)Tuple‹module:Tuple›.)get‹term:Tuple/get›(x⁰, 0) -//│ > let tmp:element1$ = ((tmp:runtime⁰.)Tuple‹module:Tuple›.)get‹term:Tuple/get›(x⁰, 1) +//│ > let tmp:element0$ = ((tmp:runtime⁰.)Tuple‹module:Tuple⁰›.)get‹term:Tuple⁰/get⁰›(x⁰, 0) +//│ > let tmp:element1$ = ((tmp:runtime⁰.)Tuple‹module:Tuple⁰›.)get‹term:Tuple⁰/get⁰›(x⁰, 1) //│ > tmp:element0$ is 0 and //│ > let y = tmp:element1$⁰ //│ > tmp:element1$ is 1 then y⁰ diff --git a/hkmc2/shared/src/test/mlscript/ups/parametric/EtaConversion.mls b/hkmc2/shared/src/test/mlscript/ups/parametric/EtaConversion.mls index 019303aeb4..e4f5798707 100644 --- a/hkmc2/shared/src/test/mlscript/ups/parametric/EtaConversion.mls +++ b/hkmc2/shared/src/test/mlscript/ups/parametric/EtaConversion.mls @@ -14,7 +14,7 @@ pattern Zero = 0 //│ > let tmp:scrut = 0 //│ > let tmp:patternArgument0$ = member:Zero⁰ //│ > let tmp:unapplyResult = (member:Nullable⁰.)unapply(tmp:patternArgument0$⁰, tmp:scrut⁰) -//│ > tmp:unapplyResult is (tmp:runtime⁰.)MatchSuccess‹class:MatchSuccess›(output, bindings) then true +//│ > tmp:unapplyResult is (tmp:runtime⁰.)MatchSuccess‹class:MatchSuccess⁰›(output, bindings) then true //│ > else false //│ = true @@ -26,7 +26,7 @@ fun foo(x) = x is Nullable(Char.Letter) //│ > ‹if|while› //│ > let tmp:patternArgument0$ = member:Char⁰^term:foo⁰.Letter //│ > let tmp:unapplyResult = (member:Nullable⁰^term:foo⁰.)unapply(tmp:patternArgument0$¹, x⁰) -//│ > tmp:unapplyResult is (tmp:runtime⁰.)MatchSuccess‹class:MatchSuccess›(output, bindings) then true +//│ > tmp:unapplyResult is (tmp:runtime⁰.)MatchSuccess‹class:MatchSuccess⁰›(output, bindings) then true //│ > else false foo of null diff --git a/hkmc2DiffTests/src/test/scala/hkmc2/JSBackendDiffMaker.scala b/hkmc2DiffTests/src/test/scala/hkmc2/JSBackendDiffMaker.scala index 649735bbd0..1c87ecf57b 100644 --- a/hkmc2DiffTests/src/test/scala/hkmc2/JSBackendDiffMaker.scala +++ b/hkmc2DiffTests/src/test/scala/hkmc2/JSBackendDiffMaker.scala @@ -50,7 +50,7 @@ abstract class JSBackendDiffMaker extends MLsDiffMaker: val dtl = phaseLogger(_.optimizations, _ => debugOptimizations.isSet, indentWhenDisabled = true) - val replTL = TraceLogger(_ => showRepl.isSet, str => output(str)) + val replTL = phaseLogger(_ => false, _ => showRepl.isSet, indentWhenDisabled = true) lazy val host = hostCreated = true diff --git a/hkmc2DiffTests/src/test/scala/hkmc2/MLsDiffMaker.scala b/hkmc2DiffTests/src/test/scala/hkmc2/MLsDiffMaker.scala index 36ebcb20cf..07025baea7 100644 --- a/hkmc2DiffTests/src/test/scala/hkmc2/MLsDiffMaker.scala +++ b/hkmc2DiffTests/src/test/scala/hkmc2/MLsDiffMaker.scala @@ -244,7 +244,7 @@ abstract class MLsDiffMaker extends DiffMaker: output(contents) case _ => debugOutputHandler.emit(out, s"${title}\n${contents}") - given Elaborator.State = new Elaborator.State(debugContext) + given Elaborator.State = new Elaborator.State protected lazy val dbgScp: utils.Scope = // for unique symbol debug-printing only @@ -255,23 +255,23 @@ abstract class MLsDiffMaker extends DiffMaker: )) - val dbgPrinter: SymbolPrinter = new SymbolPrinter(dbgScp): - override def preProcess(t: Product): Product = super.preProcess: + val dbgPrinter: SymbolPrinter = new SymbolPrinter(dbgScp, + t => case class Unexpanded(origin: Resolvable) t match - case t: Resolvable if t.hasExpansion => t.expanded - case t: Resolvable if dbgResolving.isSet => Unexpanded(t.duplicate.resolve) - case t => t - override def postProcess(t: Product): Str = - super.postProcess(t) + ( - if showLocations.isSet then - t match + case t: Resolvable if t.hasExpansion => t.expanded + case t: Resolvable if dbgResolving.isSet => Unexpanded(t.duplicate.resolve) + case t => t, + t => + if showLocations.isSet then + t match case t: Located => t.toLoc.fold(" loc[none]"): loc => val (sl, _, sc) = loc.origin.fph.getLineColAt(loc.spanStart) val (el, _, ec) = loc.origin.fph.getLineColAt(loc.spanEnd) s" loc[$sl:$sc-$el:$ec]" - else "" - ) + case _ => "" + else "", + ) protected def phaseLogger(phase: Config.Debug => Bool, force: Opt[Str] => Bool, indentWhenDisabled: Bool): TraceLogger = @@ -280,9 +280,9 @@ abstract class MLsDiffMaker extends DiffMaker: val etl = phaseLogger(_.elaboration, scope => dbgElab.isSet || scope.exists(showUCS.get.getOrElse(Set.empty).contains), indentWhenDisabled = false) - val rtl = TraceLogger(_ => dbgResolving.isSet, str => output(str)) + val rtl = phaseLogger(_ => false, _ => dbgResolving.isSet, indentWhenDisabled = true) - val ftl = TraceLogger(_ => dbgFlow.isSet, str => output(str)) + val ftl = phaseLogger(_ => false, _ => dbgFlow.isSet, indentWhenDisabled = true) var curCtx = Elaborator.State.init var curICtx = Resolver.ICtx.empty @@ -405,7 +405,7 @@ abstract class MLsDiffMaker extends DiffMaker: case err: Throwable => uncaught(err) - given tl: TraceLogger = TraceLogger(_ => debug.isSet, str => output(str)) + given tl: TraceLogger = phaseLogger(_ => false, _ => debug.isSet, indentWhenDisabled = true) def processOrigin(origin: Origin)(using Raise): Unit = From e294ae2350c078ece763c4d3ad9b810d240b6e80 Mon Sep 17 00:00:00 2001 From: OpenAI Codex Date: Wed, 21 Oct 2026 12:26:34 +0800 Subject: [PATCH 26/32] Use abstract methods for trace logger policies --- .../src/main/scala/hkmc2/CompilerCtx.scala | 19 ++++--- .../src/main/scala/hkmc2/DebugContext.scala | 15 ++---- .../codegen/flowAnalysis/FlowAnalysis.scala | 10 ++-- .../src/main/scala/utils/TraceLogger.scala | 50 +++++++++++-------- .../test/scala/hkmc2/JSBackendDiffMaker.scala | 12 +++-- .../src/test/scala/hkmc2/MLsDiffMaker.scala | 22 +++++--- 6 files changed, 73 insertions(+), 55 deletions(-) diff --git a/hkmc2/shared/src/main/scala/hkmc2/CompilerCtx.scala b/hkmc2/shared/src/main/scala/hkmc2/CompilerCtx.scala index 2e7a947970..c74f84da23 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/CompilerCtx.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/CompilerCtx.scala @@ -34,6 +34,11 @@ class CompilerCtx( private val outputHandler: DebugOutputHandler = importing match case S((_, parent)) => parent.outputHandler case N => DebugOutputHandler(fs, println) + + private abstract class PhaseTraceLogger(context: DebugContext)(using DebugPrinter) + extends TraceLogger(context, indentWhenDisabled = true): + protected final def forceTrace(scope: Opt[Str]): Bool = false + protected final def output(out: Config.DebugOutput, line: Str): Unit = outputHandler.emit(out, line) def allFilesBeingImported: Ls[io.Path] = importing match @@ -93,11 +98,12 @@ class CompilerCtx( val parsed = parse.resultBlk val phaseConfig = Config.elaborationConfig(parsed, file.up) debug.configure(phaseConfig.debug) - def traceLogger(phase: Config.Debug => Bool) = - new TraceLogger(debug, phase, _ => false, outputHandler.emit, indentWhenDisabled = true) - val etl = traceLogger(_.elaboration) - val ltl = traceLogger(_.lowering) - val dtl = traceLogger(_.optimizations) + val etl = new PhaseTraceLogger(debug): + protected def phaseEnabled(config: Config.Debug): Bool = config.elaboration + val ltl = new PhaseTraceLogger(debug): + protected def phaseEnabled(config: Config.Debug): Bool = config.lowering + val dtl = new PhaseTraceLogger(debug): + protected def phaseEnabled(config: Config.Debug): Bool = config.optimizations given Elaborator.Ctx = prelude val artifactCtx = derive(parse.origin.fileName, dependencies) @@ -257,7 +263,8 @@ class CompilerCtx( val parse = ParserSetup(file, outputHandler) val elaborationConfig = Config.elaborationConfig(parse.resultBlk, file.up) debug.configure(elaborationConfig.debug) - val etl = new TraceLogger(debug, _.elaboration, _ => false, outputHandler.emit, indentWhenDisabled = true) + val etl = new PhaseTraceLogger(debug): + protected def phaseEnabled(config: Config.Debug): Bool = config.elaboration // The prelude defines the builtins consulted during its own resolution. // Seed lookup with the block's original symbols, as block elaboration does, // so those lookups share the declarations that are being elaborated. diff --git a/hkmc2/shared/src/main/scala/hkmc2/DebugContext.scala b/hkmc2/shared/src/main/scala/hkmc2/DebugContext.scala index 8ab286c142..075596ac23 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/DebugContext.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/DebugContext.scala @@ -24,19 +24,12 @@ final class DebugContext(forceUids: () => Bool): current = settings try body finally current = saved - def traceDefinition[T](annotations: Ls[semantics.Annot], base: Config, enabled: Config.Debug => Bool) - (body: => T): T = - traceDefinitionThen[T, T](annotations, base, enabled)(k => k(body))(identity) - /** A lowering continuation processes following statements in the enclosing settings. * Capture those settings at entry, independently of scopes entered while processing the definition. */ - def traceDefinitionThen[R, T](annotations: Ls[semantics.Annot], base: Config, enabled: Config.Debug => Bool) + def scopedThen[R, T](config: Config.Debug, uidDebug: Bool) (process: (R => T) => T)(k: R => T): T = - DebugOutputHandler.definitionConfig(annotations, base.copy(debug = config)) match - case S(localConfig) => - val outer = current - within(Settings(localConfig.debug, enabled(localConfig.debug))): - process(r => within(outer)(k(r))) - case N => process(k) + val outer = current + within(Settings(config, uidDebug)): + process(r => within(outer)(k(r))) end DebugContext diff --git a/hkmc2/shared/src/main/scala/hkmc2/codegen/flowAnalysis/FlowAnalysis.scala b/hkmc2/shared/src/main/scala/hkmc2/codegen/flowAnalysis/FlowAnalysis.scala index c153524d61..328c5b4d21 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/codegen/flowAnalysis/FlowAnalysis.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/codegen/flowAnalysis/FlowAnalysis.scala @@ -89,14 +89,14 @@ object FlowAnalysis: prefix: Str, outerTl: TL, ): TraceLogger = - TraceLogger( - { + given printer: DebugPrinter = outerTl.debugPrinter + new TraceLogger(new DebugContext(() => printer.showUids), indentWhenDisabled = true): + protected def phaseEnabled(config: Config.Debug): Bool = false + protected def forceTrace(scope: Opt[Str]): Bool = scope match case S(TraceScope.NonAffineSyms) => cfg.logNonAffine case S(TraceScope.AccumulatorSym) => cfg.logAccumulator case _ => cfg.debug - }, - str => outerTl.emitDbg(s"$prefix$str"), - )(using outerTl.debugPrinter) + protected def output(out: Config.DebugOutput, line: Str): Unit = outerTl.emitDbg(s"$prefix$line") end FlowAnalysis diff --git a/hkmc2/shared/src/main/scala/utils/TraceLogger.scala b/hkmc2/shared/src/main/scala/utils/TraceLogger.scala index 93bd688da1..b0220a8cd8 100644 --- a/hkmc2/shared/src/main/scala/utils/TraceLogger.scala +++ b/hkmc2/shared/src/main/scala/utils/TraceLogger.scala @@ -8,26 +8,27 @@ type TL = TraceLogger def tl(using TL): TL = summon -final class TraceLogger( - val debug: DebugContext, - phase: Config.Debug => Bool, - force: Opt[Str] => Bool, - output: (Config.DebugOutput, Str) => Unit, +abstract class TraceLogger( + final val debug: DebugContext, indentWhenDisabled: Bool, )(using printer: DebugPrinter): - def debugPrinter: DebugPrinter = printer.withUids(debug.showUids) + protected def phaseEnabled(config: Config.Debug): Bool + protected def forceTrace(scope: Opt[Str]): Bool + protected def output(out: Config.DebugOutput, line: Str): Unit - def isTracing: Bool = phase(debug.config) || force(scope) + final def debugPrinter: DebugPrinter = printer.withUids(debug.showUids) + + final def isTracing: Bool = phaseEnabled(debug.config) || forceTrace(scope) /** Commands can force a trace without enabling the phase in source settings. * Only source settings that enable the phase select a destination other than StdIO. */ private def debugOutput: Config.DebugOutput = - if phase(debug.config) then debug.config.out else Config.DebugOutput.StdIO + if phaseEnabled(debug.config) then debug.config.out else Config.DebugOutput.StdIO private val noPostTrace: Any => DebugPrinter ?=> Str = _ => "" private var indent = 0 - def trace[T](pre: DebugPrinter ?=> Str, post: T => DebugPrinter ?=> Str = noPostTrace)(thunk: => T): T = + final def trace[T](pre: DebugPrinter ?=> Str, post: T => DebugPrinter ?=> Str = noPostTrace)(thunk: => T): T = log(pre) val indentTrace = indentWhenDisabled || isTracing if indentTrace then enter() @@ -40,12 +41,12 @@ final class TraceLogger( inline def enter() = indent += 1 inline def exit() = indent -= 1 - private[hkmc2] def emitDbg(str: Str): Unit = output(Config.DebugOutput.StdIO, str) + private[hkmc2] final def emitDbg(str: Str): Unit = output(Config.DebugOutput.StdIO, str) - def logs(msgs: (DebugPrinter ?=> Any)*): Unit = + final def logs(msgs: (DebugPrinter ?=> Any)*): Unit = if isTracing then msgs.foreach(msg => log(msg)) - def log(msg: DebugPrinter ?=> Any, noIndent: Bool = false): Unit = + final def log(msg: DebugPrinter ?=> Any, noIndent: Bool = false): Unit = if isTracing then val printer = debugPrinter val rendered = printer.printPlain(msg(using printer)) @@ -54,24 +55,29 @@ final class TraceLogger( else "| " * indent + rendered.indentNewLines("| " * indent + "> "), ) - def traceDefinition[T](annotations: Ls[semantics.Annot], base: Config)(body: => T): T = - debug.traceDefinition(annotations, base, phase)(body) + final def traceDefinition[T](annotations: Ls[semantics.Annot], base: Config)(body: => T): T = + traceDefinitionThen[T, T](annotations, base)(k => k(body))(identity) - def traceDefinitionThen[R, T](annotations: Ls[semantics.Annot], base: Config) + final def traceDefinitionThen[R, T](annotations: Ls[semantics.Annot], base: Config) (process: (R => T) => T)(k: R => T): T = - debug.traceDefinitionThen(annotations, base, phase)(process)(k) + DebugOutputHandler.definitionConfig(annotations, base.copy(debug = debug.config)) match + case S(localConfig) => + debug.scopedThen(localConfig.debug, phaseEnabled(localConfig.debug))(process)(k) + case N => process(k) private var scope: Opt[Str] = N - def scoped[T](flag: Str)(thunk: => T): T = + final def scoped[T](flag: Str)(thunk: => T): T = val oldScope = scope scope = S(flag) try thunk finally scope = oldScope object TraceLogger: - /** A logger controlled independently of source directives, such as a diff-test command or a pass flag. */ - def apply(enabled: Opt[Str] => Bool, output: Str => Unit)(using printer: DebugPrinter): TraceLogger = - new TraceLogger(new DebugContext(() => printer.showUids), _ => false, enabled, (_, line) => output(line), indentWhenDisabled = true) - - def silent(using DebugPrinter): TraceLogger = apply(_ => false, line => scala.Predef.println(line)) + def silent(using printer: DebugPrinter): TraceLogger = + new TraceLogger(new DebugContext(() => printer.showUids), indentWhenDisabled = true): + protected def phaseEnabled(config: Config.Debug): Bool = false + protected def forceTrace(scope: Opt[Str]): Bool = false + // FlowAnalysis.mkTraceLogger uses emitDbg to print traces enabled by its own flags, + // so disabling this logger's traces must not discard that forwarded output. + protected def output(out: Config.DebugOutput, line: Str): Unit = scala.Predef.println(line) diff --git a/hkmc2DiffTests/src/test/scala/hkmc2/JSBackendDiffMaker.scala b/hkmc2DiffTests/src/test/scala/hkmc2/JSBackendDiffMaker.scala index 1c87ecf57b..0f02d46e4a 100644 --- a/hkmc2DiffTests/src/test/scala/hkmc2/JSBackendDiffMaker.scala +++ b/hkmc2DiffTests/src/test/scala/hkmc2/JSBackendDiffMaker.scala @@ -45,12 +45,16 @@ abstract class JSBackendDiffMaker extends MLsDiffMaker: val definitionMetadataNme = baseScp.allocateName(Elaborator.State.definitionMetadataSymbol)(using throw _) val prettyPrintNme = baseScp.allocateName(Elaborator.State.prettyPrintSymbol)(using throw _) - val ltl = phaseLogger(_.lowering, - scope => debugLowering.isSet || scope.exists(showUCS.get.getOrElse(Set.empty).contains), indentWhenDisabled = true) + val ltl = new DiffTraceLogger(indentWhenDisabled = true): + protected def phaseEnabled(config: Config.Debug): Bool = config.lowering + protected def forceTrace(scope: Opt[Str]): Bool = + debugLowering.isSet || scope.exists(showUCS.get.getOrElse(Set.empty).contains) - val dtl = phaseLogger(_.optimizations, _ => debugOptimizations.isSet, indentWhenDisabled = true) + val dtl = new DiffTraceLogger(indentWhenDisabled = true): + protected def phaseEnabled(config: Config.Debug): Bool = config.optimizations + protected def forceTrace(scope: Opt[Str]): Bool = debugOptimizations.isSet - val replTL = phaseLogger(_ => false, _ => showRepl.isSet, indentWhenDisabled = true) + val replTL = commandLogger(showRepl) lazy val host = hostCreated = true diff --git a/hkmc2DiffTests/src/test/scala/hkmc2/MLsDiffMaker.scala b/hkmc2DiffTests/src/test/scala/hkmc2/MLsDiffMaker.scala index 07025baea7..59b80a66ea 100644 --- a/hkmc2DiffTests/src/test/scala/hkmc2/MLsDiffMaker.scala +++ b/hkmc2DiffTests/src/test/scala/hkmc2/MLsDiffMaker.scala @@ -274,15 +274,23 @@ abstract class MLsDiffMaker extends DiffMaker: ) - protected def phaseLogger(phase: Config.Debug => Bool, force: Opt[Str] => Bool, indentWhenDisabled: Bool): TraceLogger = - new TraceLogger(debugContext, phase, force, (out, line) => debugOutputHandler.emit(out, line), indentWhenDisabled) + protected abstract class DiffTraceLogger(indentWhenDisabled: Bool) + extends TraceLogger(debugContext, indentWhenDisabled)(using dbgPrinter): + protected final def output(out: Config.DebugOutput, line: Str): Unit = debugOutputHandler.emit(out, line) - val etl = phaseLogger(_.elaboration, - scope => dbgElab.isSet || scope.exists(showUCS.get.getOrElse(Set.empty).contains), indentWhenDisabled = false) + protected def commandLogger(command: Command[?]): TraceLogger = + new DiffTraceLogger(indentWhenDisabled = true): + protected def phaseEnabled(config: Config.Debug): Bool = false + protected def forceTrace(scope: Opt[Str]): Bool = command.isSet + + val etl = new DiffTraceLogger(indentWhenDisabled = false): + protected def phaseEnabled(config: Config.Debug): Bool = config.elaboration + protected def forceTrace(scope: Opt[Str]): Bool = + dbgElab.isSet || scope.exists(showUCS.get.getOrElse(Set.empty).contains) - val rtl = phaseLogger(_ => false, _ => dbgResolving.isSet, indentWhenDisabled = true) + val rtl = commandLogger(dbgResolving) - val ftl = phaseLogger(_ => false, _ => dbgFlow.isSet, indentWhenDisabled = true) + val ftl = commandLogger(dbgFlow) var curCtx = Elaborator.State.init var curICtx = Resolver.ICtx.empty @@ -405,7 +413,7 @@ abstract class MLsDiffMaker extends DiffMaker: case err: Throwable => uncaught(err) - given tl: TraceLogger = phaseLogger(_ => false, _ => debug.isSet, indentWhenDisabled = true) + given tl: TraceLogger = commandLogger(debug) def processOrigin(origin: Origin)(using Raise): Unit = From ce39a4b025e9d210dd1a6be229ea0e8e034a5833 Mon Sep 17 00:00:00 2001 From: Lionel Parreaux Date: Wed, 21 Oct 2026 12:29:46 +0800 Subject: [PATCH 27/32] Minor --- AGENTS.md | 4 ++++ hkmc2/shared/src/main/scala/hkmc2/semantics/Symbol.scala | 2 -- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index ff1f3c52ba..9441469766 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -94,6 +94,10 @@ avoid vague claims about where a "lifecycle" or "responsibility" belongs. If his or a rejected alternative matters, include enough context to explain the tradeoff after commits are squashed. +Do NOT include comments that simply describe what the code is doing +without adding any additional insight, or comments that talk about +parts of the system that are not directly related to the code being commented on. + ## User-Facing Diagnostics diff --git a/hkmc2/shared/src/main/scala/hkmc2/semantics/Symbol.scala b/hkmc2/shared/src/main/scala/hkmc2/semantics/Symbol.scala index e5c9909efd..421ed43b0a 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/semantics/Symbol.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/semantics/Symbol.scala @@ -49,8 +49,6 @@ abstract class Symbol(using ownerState: State) extends MaybeSymbol with Located: def showFullName(using Scope, ShowCfg, Raise): Str = showPrefix + showName - // Generic string conversion is independent of the symbol's owner and the current trace. - // DebugPrinter adds disambiguated names and optional UIDs when explicitly rendering a symbol. override def toString: Str = prefix + nme From 40a0ca9fabdc1f1b40b0bd41c0a89de31368ccb5 Mon Sep 17 00:00:00 2001 From: OpenAI Codex Date: Wed, 21 Oct 2026 12:38:38 +0800 Subject: [PATCH 28/32] Separate trace output sinks from logger policies --- .../src/main/scala/hkmc2/CompilerCtx.scala | 3 +-- .../main/scala/hkmc2/DebugOutputHandler.scala | 2 +- .../shared/src/main/scala/hkmc2/DebugSink.scala | 12 ++++++++++++ .../hkmc2/codegen/CompilationPipeline.scala | 4 ++-- .../hkmc2/codegen/FlowAnalysisBasedRewrite.scala | 2 +- .../codegen/flowAnalysis/FlowAnalysis.scala | 8 +++----- .../src/main/scala/utils/TraceLogger.scala | 16 ++++++---------- .../test/mlscript/codegen/DebugDestinations.mls | 11 +++++++++++ .../src/test/scala/hkmc2/MLsDiffMaker.scala | 3 +-- 9 files changed, 38 insertions(+), 23 deletions(-) create mode 100644 hkmc2/shared/src/main/scala/hkmc2/DebugSink.scala diff --git a/hkmc2/shared/src/main/scala/hkmc2/CompilerCtx.scala b/hkmc2/shared/src/main/scala/hkmc2/CompilerCtx.scala index c74f84da23..882919ad54 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/CompilerCtx.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/CompilerCtx.scala @@ -36,9 +36,8 @@ class CompilerCtx( case N => DebugOutputHandler(fs, println) private abstract class PhaseTraceLogger(context: DebugContext)(using DebugPrinter) - extends TraceLogger(context, indentWhenDisabled = true): + extends TraceLogger(context, outputHandler, indentWhenDisabled = true, prefix = ""): protected final def forceTrace(scope: Opt[Str]): Bool = false - protected final def output(out: Config.DebugOutput, line: Str): Unit = outputHandler.emit(out, line) def allFilesBeingImported: Ls[io.Path] = importing match diff --git a/hkmc2/shared/src/main/scala/hkmc2/DebugOutputHandler.scala b/hkmc2/shared/src/main/scala/hkmc2/DebugOutputHandler.scala index 9203aa76ba..6853cc97a7 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/DebugOutputHandler.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/DebugOutputHandler.scala @@ -7,7 +7,7 @@ import hkmc2.utils.*, shorthands.* /** A compilation session shares one handler across units: each output file is overwritten on first * use, then appended to. Concurrent debug writes are not synchronized. */ -final class DebugOutputHandler(fs: io.FileSystem, stdIO: Str => Unit): +final class DebugOutputHandler(fs: io.FileSystem, stdIO: Str => Unit) extends DebugSink: private val writtenFiles = mutable.Set.empty[io.Path] def emit(out: Config.DebugOutput, line: Str): Unit = out match diff --git a/hkmc2/shared/src/main/scala/hkmc2/DebugSink.scala b/hkmc2/shared/src/main/scala/hkmc2/DebugSink.scala new file mode 100644 index 0000000000..c46dd40367 --- /dev/null +++ b/hkmc2/shared/src/main/scala/hkmc2/DebugSink.scala @@ -0,0 +1,12 @@ +package hkmc2 + +import hkmc2.utils.*, shorthands.* + + +/** Sharing a sink shares output delivery; each logger decides which messages to emit and how to format them. */ +trait DebugSink: + def emit(out: Config.DebugOutput, line: Str): Unit + +object DebugSink: + object discard extends DebugSink: + def emit(out: Config.DebugOutput, line: Str): Unit = () diff --git a/hkmc2/shared/src/main/scala/hkmc2/codegen/CompilationPipeline.scala b/hkmc2/shared/src/main/scala/hkmc2/codegen/CompilationPipeline.scala index 74f1b7a53f..34fe1b3a72 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/codegen/CompilationPipeline.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/codegen/CompilationPipeline.scala @@ -46,11 +46,11 @@ class CompilationPipeline(using Config, Raise, State, Ctx, SymbolPrinter): blockPass(Lifter(_).transform)(prog) else prog runPass("Deforest"): prog => - val outterTl = tl + val outerTl = tl config.deforest match case None => prog case Some(dCfg) => - flowAnalysis.FlowAnalysis.mkTraceLogger(dCfg.config, "deforest > ", outterTl).givenIn: + flowAnalysis.FlowAnalysis.mkTraceLogger(dCfg.config, "deforest > ", outerTl.sink)(using outerTl.debugPrinter).givenIn: deforest.Deforest(prog, additionalTopLevelSymbols = symbolsToPreserve.collect{case s: semantics.LocalVarSymbol => s}) runPass("HandlerLowering"): prog => diff --git a/hkmc2/shared/src/main/scala/hkmc2/codegen/FlowAnalysisBasedRewrite.scala b/hkmc2/shared/src/main/scala/hkmc2/codegen/FlowAnalysisBasedRewrite.scala index 8f92f74930..2c66104cd4 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/codegen/FlowAnalysisBasedRewrite.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/codegen/FlowAnalysisBasedRewrite.scala @@ -293,7 +293,7 @@ object FlowAnalysisBasedRewrite: symbolPrinter: SymbolPrinter, ): Program = def mkTl(prefix: Str, debug: Bool) = - FlowAnalysis.mkTraceLogger(optCfg.config.copy(debug = debug), prefix, tl) + FlowAnalysis.mkTraceLogger(optCfg.config.copy(debug = debug), prefix, tl.sink)(using tl.debugPrinter) val flowAnalysisRes = mkTl("flow-analysis > ", optCfg.debug).givenIn: FlowAnalysis( p, diff --git a/hkmc2/shared/src/main/scala/hkmc2/codegen/flowAnalysis/FlowAnalysis.scala b/hkmc2/shared/src/main/scala/hkmc2/codegen/flowAnalysis/FlowAnalysis.scala index 328c5b4d21..869e505cc4 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/codegen/flowAnalysis/FlowAnalysis.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/codegen/flowAnalysis/FlowAnalysis.scala @@ -87,16 +87,14 @@ object FlowAnalysis: def mkTraceLogger( cfg: Config.FlowAnalysisConfig, prefix: Str, - outerTl: TL, - ): TraceLogger = - given printer: DebugPrinter = outerTl.debugPrinter - new TraceLogger(new DebugContext(() => printer.showUids), indentWhenDisabled = true): + sink: DebugSink, + )(using printer: DebugPrinter): TraceLogger = + new TraceLogger(new DebugContext(() => printer.showUids), sink, indentWhenDisabled = true, prefix = prefix): protected def phaseEnabled(config: Config.Debug): Bool = false protected def forceTrace(scope: Opt[Str]): Bool = scope match case S(TraceScope.NonAffineSyms) => cfg.logNonAffine case S(TraceScope.AccumulatorSym) => cfg.logAccumulator case _ => cfg.debug - protected def output(out: Config.DebugOutput, line: Str): Unit = outerTl.emitDbg(s"$prefix$line") end FlowAnalysis diff --git a/hkmc2/shared/src/main/scala/utils/TraceLogger.scala b/hkmc2/shared/src/main/scala/utils/TraceLogger.scala index b0220a8cd8..c7b68a70ba 100644 --- a/hkmc2/shared/src/main/scala/utils/TraceLogger.scala +++ b/hkmc2/shared/src/main/scala/utils/TraceLogger.scala @@ -10,11 +10,12 @@ def tl(using TL): TL = summon abstract class TraceLogger( final val debug: DebugContext, + final val sink: DebugSink, indentWhenDisabled: Bool, + prefix: Str, )(using printer: DebugPrinter): protected def phaseEnabled(config: Config.Debug): Bool protected def forceTrace(scope: Opt[Str]): Bool - protected def output(out: Config.DebugOutput, line: Str): Unit final def debugPrinter: DebugPrinter = printer.withUids(debug.showUids) @@ -41,8 +42,6 @@ abstract class TraceLogger( inline def enter() = indent += 1 inline def exit() = indent -= 1 - private[hkmc2] final def emitDbg(str: Str): Unit = output(Config.DebugOutput.StdIO, str) - final def logs(msgs: (DebugPrinter ?=> Any)*): Unit = if isTracing then msgs.foreach(msg => log(msg)) @@ -50,10 +49,10 @@ abstract class TraceLogger( if isTracing then val printer = debugPrinter val rendered = printer.printPlain(msg(using printer)) - output(debugOutput, + sink.emit(debugOutput, prefix + ( if noIndent then rendered - else "| " * indent + rendered.indentNewLines("| " * indent + "> "), - ) + else "| " * indent + rendered.indentNewLines("| " * indent + "> ") + )) final def traceDefinition[T](annotations: Ls[semantics.Annot], base: Config)(body: => T): T = traceDefinitionThen[T, T](annotations, base)(k => k(body))(identity) @@ -75,9 +74,6 @@ abstract class TraceLogger( object TraceLogger: def silent(using printer: DebugPrinter): TraceLogger = - new TraceLogger(new DebugContext(() => printer.showUids), indentWhenDisabled = true): + new TraceLogger(new DebugContext(() => printer.showUids), DebugSink.discard, indentWhenDisabled = true, prefix = ""): protected def phaseEnabled(config: Config.Debug): Bool = false protected def forceTrace(scope: Opt[Str]): Bool = false - // FlowAnalysis.mkTraceLogger uses emitDbg to print traces enabled by its own flags, - // so disabling this logger's traces must not discard that forwarded output. - protected def output(out: Config.DebugOutput, line: Str): Unit = scala.Predef.println(line) diff --git a/hkmc2/shared/src/test/mlscript/codegen/DebugDestinations.mls b/hkmc2/shared/src/test/mlscript/codegen/DebugDestinations.mls index 95ffa3e540..ecd6fffe3e 100644 --- a/hkmc2/shared/src/test/mlscript/codegen/DebugDestinations.mls +++ b/hkmc2/shared/src/test/mlscript/codegen/DebugDestinations.mls @@ -34,6 +34,17 @@ fs.readFileSync(debugFile, "utf8") == "Lowering.term 42\nLowering.term 45\n" //│ | | avoidNames Return(Lit(IntLit(47)))... //│ | | avoidNames End()... +// Analysis flags share the captured output sink without enabling optimization-phase traces +// or redirecting their output to the file selected above. +:flowBasedOpt debugDpe +:noInline +private fun forced_analysis(unused) = 48 +forced_analysis(0) +//│ dead-param-elim > >>> dead-param-elim results >>> +//│ dead-param-elim > forced_analysis⁰#0 @ -> eliminable: {0} +//│ dead-param-elim > <<< dead-param-elim results <<< +//│ = 48 + #dbg(out: StdIO) :expect true diff --git a/hkmc2DiffTests/src/test/scala/hkmc2/MLsDiffMaker.scala b/hkmc2DiffTests/src/test/scala/hkmc2/MLsDiffMaker.scala index 59b80a66ea..fe66bc368a 100644 --- a/hkmc2DiffTests/src/test/scala/hkmc2/MLsDiffMaker.scala +++ b/hkmc2DiffTests/src/test/scala/hkmc2/MLsDiffMaker.scala @@ -275,8 +275,7 @@ abstract class MLsDiffMaker extends DiffMaker: protected abstract class DiffTraceLogger(indentWhenDisabled: Bool) - extends TraceLogger(debugContext, indentWhenDisabled)(using dbgPrinter): - protected final def output(out: Config.DebugOutput, line: Str): Unit = debugOutputHandler.emit(out, line) + extends TraceLogger(debugContext, debugOutputHandler, indentWhenDisabled, prefix = "")(using dbgPrinter) protected def commandLogger(command: Command[?]): TraceLogger = new DiffTraceLogger(indentWhenDisabled = true): From 906825bb89c0d88c66b84fe4a68913c0be7fe977 Mon Sep 17 00:00:00 2001 From: Lionel Parreaux Date: Wed, 21 Oct 2026 13:17:40 +0800 Subject: [PATCH 29/32] Minor --- hkmc2/shared/src/main/scala/hkmc2/MLsCompiler.scala | 9 ++++++--- hkmc2/shared/src/main/scala/hkmc2/syntax/Lexer.scala | 2 +- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/hkmc2/shared/src/main/scala/hkmc2/MLsCompiler.scala b/hkmc2/shared/src/main/scala/hkmc2/MLsCompiler.scala index 7b09d29fe6..ff028f2fc7 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/MLsCompiler.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/MLsCompiler.scala @@ -8,7 +8,7 @@ import hkmc2.semantics.* class ParserSetup(file: io.Path, outputHandler: DebugOutputHandler) -(using Elaborator.State, Raise, CompilerCtx, Config, DebugPrinter): + (using Elaborator.State, Raise, CompilerCtx, Config, DebugPrinter): val block = CompilerCtx.get.fs.read(file) val fph = new FastParseHelpers(block) @@ -19,12 +19,15 @@ class ParserSetup(file: io.Path, outputHandler: DebugOutputHandler) val rules = syntax.ParseRules() val parser = new syntax.Parser(origin, tokens, rules, raise, dbg = false): + // * Tracing is not supported for compiled files, + // * as debug flags to enable traces are only available after parsing. def doPrintDbg(msg: => Str): Unit = () val result = parser.parseAll(parser.block(allowNewlines = true)) val effectiveConfig = Config.elaborationConfig(result, file.up) - - if effectiveConfig.debug.showParsedTree then + + if effectiveConfig.debug.showParsedTree + then outputHandler.emit( effectiveConfig.debug.out, s"Parsed tree\n${result.map(_.showAsTree).mkString("\n")}", diff --git a/hkmc2/shared/src/main/scala/hkmc2/syntax/Lexer.scala b/hkmc2/shared/src/main/scala/hkmc2/syntax/Lexer.scala index c47d1b63a5..125bc116c7 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/syntax/Lexer.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/syntax/Lexer.scala @@ -12,7 +12,7 @@ import Tree.{IntLit, DecLit, StrLit} class Lexer(origin: Origin, dbg: Bool)(using raise: Raise): - protected def doPrintDbg(msg: => Str): Unit = println(msg) + inline protected def doPrintDbg(msg: Str): Unit = println(msg) val bytes: Array[Char] = origin.fph.blockStr.toArray private val length = bytes.length From b53cf9147ee02e8bee889dc40e5c70129473add5 Mon Sep 17 00:00:00 2001 From: OpenAI Codex Date: Wed, 21 Oct 2026 13:27:11 +0800 Subject: [PATCH 30/32] Replace debug printer callbacks with overridable methods --- .../scala/hkmc2/semantics/SymbolPrinter.scala | 11 ++---- .../src/main/scala/hkmc2/syntax/Lexer.scala | 2 +- .../src/main/scala/hkmc2/utils/utils.scala | 36 ++++++++++++------- .../src/test/mlscript/meta/LocDebugging.mls | 15 ++++++++ .../src/test/scala/hkmc2/MLsDiffMaker.scala | 13 ++++--- 5 files changed, 48 insertions(+), 29 deletions(-) diff --git a/hkmc2/shared/src/main/scala/hkmc2/semantics/SymbolPrinter.scala b/hkmc2/shared/src/main/scala/hkmc2/semantics/SymbolPrinter.scala index 373ca5d0d5..01c9ec4e46 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/semantics/SymbolPrinter.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/semantics/SymbolPrinter.scala @@ -6,14 +6,9 @@ import syntax.* import hkmc2.utils.* -class SymbolPrinter( - val dbgScp: Scope, - preprocess: Product => Product, - postprocess: Product => Str, -)(using owner: Elaborator.State) extends DebugPrinter(false, S(owner), - sym => sym.showFullName(using dbgScp, semantics.ShowCfg.internal, throw _), - preprocess, postprocess): - def this(dbgScp: Scope)(using Elaborator.State) = this(dbgScp, identity, _ => "") +class SymbolPrinter(val dbgScp: Scope)(using owner: Elaborator.State) extends DebugPrinter(owner): + override protected def symbolName(sym: Symbol): Str = + sym.showFullName(using dbgScp, ShowCfg.internal, throw _) def printSymbol(sym: Symbol)(using Raise, ShowCfg): Str = sym.showName(using dbgScp, summon) diff --git a/hkmc2/shared/src/main/scala/hkmc2/syntax/Lexer.scala b/hkmc2/shared/src/main/scala/hkmc2/syntax/Lexer.scala index 125bc116c7..4ceb69141a 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/syntax/Lexer.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/syntax/Lexer.scala @@ -12,7 +12,7 @@ import Tree.{IntLit, DecLit, StrLit} class Lexer(origin: Origin, dbg: Bool)(using raise: Raise): - inline protected def doPrintDbg(msg: Str): Unit = println(msg) + protected def doPrintDbg(msg: Str): Unit = println(msg) val bytes: Array[Char] = origin.fph.blockStr.toArray private val length = bytes.length diff --git a/hkmc2/shared/src/main/scala/hkmc2/utils/utils.scala b/hkmc2/shared/src/main/scala/hkmc2/utils/utils.scala index e763c56bdd..feb6abdcf9 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/utils/utils.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/utils/utils.scala @@ -60,23 +60,27 @@ extension (context: StringContext) class DebugPrinter( - val showUids: Bool, - uidOwner: Opt[semantics.Elaborator.State], - symbolName: semantics.Symbol => Str, - val preProcess: Product => Product, - val postProcess: Product => Str, + final val showUids: Bool, + private val uidOwner: Opt[semantics.Elaborator.State], ): - def this() = this(false, N, sym => sym.prefix + sym.nme, identity, _ => "") - def this(owner: semantics.Elaborator.State) = this(false, S(owner), sym => sym.prefix + sym.nme, identity, _ => "") + def this() = this(false, N) + def this(owner: semantics.Elaborator.State) = this(false, S(owner)) + + protected def symbolName(sym: semantics.Symbol): Str = sym.prefix + sym.nme + protected def preProcess(t: Product): Product = t + protected def postProcess(t: Product): Str = "" given DebugPrinter = this - /** A printing operation captures an immutable policy; changing a trace scope cannot change - * a printer already passed to a renderer. Name allocation and tree hooks are preserved. */ - def withUids(enabled: Bool): DebugPrinter = - new DebugPrinter(enabled, uidOwner, symbolName, preProcess, postProcess) + /** Capture UID visibility without replacing the symbol-naming or tree-processing hooks. + * A view points directly at the original printer, even after repeated visibility changes. */ + final def withUids(enabled: Bool): DebugPrinter = + if enabled === showUids then this + else this match + case view: DebugPrinter.UidView => view.underlying + case _ => new DebugPrinter.UidView(this, enabled) - def printPlain(v: Any): Str = v match + final def printPlain(v: Any): Str = v match case null => "null" case sym: semantics.Symbol => // Numeric UIDs are unique only within their allocating unit. Foreign symbols must not @@ -102,7 +106,7 @@ class DebugPrinter( p.productIterator.map(printPlain).mkString(p.productPrefix + "(", ",", ")") case _ => v.toString - def printProduct(inTailPos: Bool, t: Product): Str = + final def printProduct(inTailPos: Bool, t: Product): Str = def aux(v: Any, inTailPos: Bool = false): Str = v match @@ -169,6 +173,12 @@ end DebugPrinter object DebugPrinter: + private final class UidView(val underlying: DebugPrinter, enabled: Bool) + extends DebugPrinter(enabled, underlying.uidOwner): + override protected def symbolName(sym: semantics.Symbol): Str = underlying.symbolName(sym) + override protected def preProcess(t: Product): Product = underlying.preProcess(t) + override protected def postProcess(t: Product): Str = underlying.postProcess(t) + val emptyValues: Set[Any] = Set( None, Nil, Vector.empty, semantics.PlainParamList(Nil)(N), diff --git a/hkmc2/shared/src/test/mlscript/meta/LocDebugging.mls b/hkmc2/shared/src/test/mlscript/meta/LocDebugging.mls index 8267ab6678..e6f0c821ef 100644 --- a/hkmc2/shared/src/test/mlscript/meta/LocDebugging.mls +++ b/hkmc2/shared/src/test/mlscript/meta/LocDebugging.mls @@ -26,3 +26,18 @@ //│ = 333 +// UID-enabled traces retain the resolution and location hooks. +:loc +:dr +:ucs ups:tree +pattern IdentityWithUids = x => x +//│ elaborated pattern body: Transform loc[1:28-1:34]: +//│ > pattern = Alias loc[1:28-1:29]: +//│ > pattern = Wildcard loc[none] +//│ > id = Ident loc[1:28-1:29] of "x" +//│ > parameters = Ls of +//│ > Tuple2: +//│ > _1 = x⁰‹51› +//│ > _2 = x¹‹52› +//│ > transform = SimpleRef loc[1:33-1:34] of x¹‹52› + diff --git a/hkmc2DiffTests/src/test/scala/hkmc2/MLsDiffMaker.scala b/hkmc2DiffTests/src/test/scala/hkmc2/MLsDiffMaker.scala index fe66bc368a..b64563c3e9 100644 --- a/hkmc2DiffTests/src/test/scala/hkmc2/MLsDiffMaker.scala +++ b/hkmc2DiffTests/src/test/scala/hkmc2/MLsDiffMaker.scala @@ -255,14 +255,14 @@ abstract class MLsDiffMaker extends DiffMaker: )) - val dbgPrinter: SymbolPrinter = new SymbolPrinter(dbgScp, - t => + val dbgPrinter: SymbolPrinter = new SymbolPrinter(dbgScp): + override protected def preProcess(t: Product): Product = case class Unexpanded(origin: Resolvable) t match case t: Resolvable if t.hasExpansion => t.expanded case t: Resolvable if dbgResolving.isSet => Unexpanded(t.duplicate.resolve) - case t => t, - t => + case t => t + override protected def postProcess(t: Product): Str = if showLocations.isSet then t match case t: Located => t.toLoc.fold(" loc[none]"): loc => @@ -270,8 +270,7 @@ abstract class MLsDiffMaker extends DiffMaker: val (el, _, ec) = loc.origin.fph.getLineColAt(loc.spanEnd) s" loc[$sl:$sc-$el:$ec]" case _ => "" - else "", - ) + else "" protected abstract class DiffTraceLogger(indentWhenDisabled: Bool) @@ -422,7 +421,7 @@ abstract class MLsDiffMaker extends DiffMaker: given Config = baseConfig val lexer = new syntax.Lexer(origin, dbg = dbgParsing.isSet): - override protected def doPrintDbg(msg: => Str): Unit = output(msg) + override protected def doPrintDbg(msg: Str): Unit = output(msg) val tokens = lexer.bracketedTokens if showParse.isSet || dbgParsing.isSet then From 54fc97bde4daf9bfcb07b89010a68f8ab67b9262 Mon Sep 17 00:00:00 2001 From: OpenAI Codex Date: Wed, 21 Oct 2026 13:29:40 +0800 Subject: [PATCH 31/32] Replace debug context callback with an overridable method --- .../shared/src/main/scala/hkmc2/DebugContext.scala | 12 ++++++------ .../hkmc2/codegen/flowAnalysis/FlowAnalysis.scala | 4 +++- .../shared/src/main/scala/utils/TraceLogger.scala | 4 +++- .../shared/src/test/mlscript/meta/LocDebugging.mls | 14 ++++++++++++++ .../src/test/scala/hkmc2/MLsDiffMaker.scala | 3 ++- 5 files changed, 28 insertions(+), 9 deletions(-) diff --git a/hkmc2/shared/src/main/scala/hkmc2/DebugContext.scala b/hkmc2/shared/src/main/scala/hkmc2/DebugContext.scala index 075596ac23..a366584031 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/DebugContext.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/DebugContext.scala @@ -6,17 +6,17 @@ import hkmc2.utils.*, shorthands.* /** Source debug settings shared by a compilation unit's phase loggers. * Imported units have their own context; only their output handler is shared. * Compilation is synchronous within a unit, so scoped changes are restored on the call stack. */ -final class DebugContext(forceUids: () => Bool): - def this() = this(() => false) +class DebugContext: + protected def forceUids: Bool = false private case class Settings(debug: Config.Debug, uidDebug: Bool) private var current = Settings(Config.Debug.default, false) - def config: Config.Debug = current.debug - def showUids: Bool = (current.uidDebug || forceUids()) && config.showUids + final def config: Config.Debug = current.debug + final def showUids: Bool = (current.uidDebug || forceUids) && config.showUids /** File directives are discovered after parsing; diff-test blocks also replace their base settings. */ - def configure(debug: Config.Debug): Unit = + final def configure(debug: Config.Debug): Unit = current = Settings(debug, debug.elaboration || debug.lowering || debug.optimizations) private def within[T](settings: Settings)(body: => T): T = @@ -26,7 +26,7 @@ final class DebugContext(forceUids: () => Bool): /** A lowering continuation processes following statements in the enclosing settings. * Capture those settings at entry, independently of scopes entered while processing the definition. */ - def scopedThen[R, T](config: Config.Debug, uidDebug: Bool) + final def scopedThen[R, T](config: Config.Debug, uidDebug: Bool) (process: (R => T) => T)(k: R => T): T = val outer = current within(Settings(config, uidDebug)): diff --git a/hkmc2/shared/src/main/scala/hkmc2/codegen/flowAnalysis/FlowAnalysis.scala b/hkmc2/shared/src/main/scala/hkmc2/codegen/flowAnalysis/FlowAnalysis.scala index 869e505cc4..5123393983 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/codegen/flowAnalysis/FlowAnalysis.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/codegen/flowAnalysis/FlowAnalysis.scala @@ -89,7 +89,9 @@ object FlowAnalysis: prefix: Str, sink: DebugSink, )(using printer: DebugPrinter): TraceLogger = - new TraceLogger(new DebugContext(() => printer.showUids), sink, indentWhenDisabled = true, prefix = prefix): + val debug = new DebugContext: + override protected def forceUids: Bool = printer.showUids + new TraceLogger(debug, sink, indentWhenDisabled = true, prefix = prefix): protected def phaseEnabled(config: Config.Debug): Bool = false protected def forceTrace(scope: Opt[Str]): Bool = scope match case S(TraceScope.NonAffineSyms) => cfg.logNonAffine diff --git a/hkmc2/shared/src/main/scala/utils/TraceLogger.scala b/hkmc2/shared/src/main/scala/utils/TraceLogger.scala index c7b68a70ba..45ae7d0718 100644 --- a/hkmc2/shared/src/main/scala/utils/TraceLogger.scala +++ b/hkmc2/shared/src/main/scala/utils/TraceLogger.scala @@ -74,6 +74,8 @@ abstract class TraceLogger( object TraceLogger: def silent(using printer: DebugPrinter): TraceLogger = - new TraceLogger(new DebugContext(() => printer.showUids), DebugSink.discard, indentWhenDisabled = true, prefix = ""): + val debug = new DebugContext: + override protected def forceUids: Bool = printer.showUids + new TraceLogger(debug, DebugSink.discard, indentWhenDisabled = true, prefix = ""): protected def phaseEnabled(config: Config.Debug): Bool = false protected def forceTrace(scope: Opt[Str]): Bool = false diff --git a/hkmc2/shared/src/test/mlscript/meta/LocDebugging.mls b/hkmc2/shared/src/test/mlscript/meta/LocDebugging.mls index e6f0c821ef..f6e54eef98 100644 --- a/hkmc2/shared/src/test/mlscript/meta/LocDebugging.mls +++ b/hkmc2/shared/src/test/mlscript/meta/LocDebugging.mls @@ -41,3 +41,17 @@ pattern IdentityWithUids = x => x //│ > _2 = x¹‹52› //│ > transform = SimpleRef loc[1:33-1:34] of x¹‹52› +// Resetting per-block commands also resets UID printing. +:loc +:ucs ups:tree +pattern IdentityWithoutUids = y => y +//│ elaborated pattern body: Transform loc[1:31-1:37]: +//│ > pattern = Alias loc[1:31-1:32]: +//│ > pattern = Wildcard loc[none] +//│ > id = Ident loc[1:31-1:32] of "y" +//│ > parameters = Ls of +//│ > Tuple2: +//│ > _1 = y⁰ +//│ > _2 = y¹ +//│ > transform = SimpleRef loc[1:36-1:37] of y¹ + diff --git a/hkmc2DiffTests/src/test/scala/hkmc2/MLsDiffMaker.scala b/hkmc2DiffTests/src/test/scala/hkmc2/MLsDiffMaker.scala index b64563c3e9..6d08512348 100644 --- a/hkmc2DiffTests/src/test/scala/hkmc2/MLsDiffMaker.scala +++ b/hkmc2DiffTests/src/test/scala/hkmc2/MLsDiffMaker.scala @@ -235,7 +235,8 @@ abstract class MLsDiffMaker extends DiffMaker: ln.split(" ").iterator.map(x => "ucs:" + x.trim).toSet - protected lazy val debugContext = new DebugContext(() => dbgParsing.isSet || dbgResolving.isSet || debug.isSet) + protected lazy val debugContext = new DebugContext: + override protected def forceUids: Bool = dbgParsing.isSet || dbgResolving.isSet || debug.isSet protected lazy val debugOutputHandler = DebugOutputHandler(cctx.fs, str => output(str)) protected def outputDebugSection(title: Str, contents: Str, out: Config.DebugOutput): Unit = out match From 942fb0fe5007fc6f21626c81bf966a2a69830644 Mon Sep 17 00:00:00 2001 From: Lionel Parreaux Date: Wed, 21 Oct 2026 13:40:47 +0800 Subject: [PATCH 32/32] Minor --- AGENTS.md | 1 + .../src/test/scala/hkmc2/SubstitutionGrowthTest.scala | 2 +- hkmc2/jvm/src/test/scala/hkmc2/TypeRelationTest.scala | 2 +- hkmc2/shared/src/main/scala/hkmc2/CompilerCtx.scala | 2 +- hkmc2/shared/src/main/scala/hkmc2/MLsCompiler.scala | 2 +- .../src/main/scala/hkmc2/semantics/SymbolPrinter.scala | 2 +- hkmc2/shared/src/main/scala/hkmc2/utils/utils.scala | 9 ++++----- 7 files changed, 10 insertions(+), 10 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 9441469766..07d0cf536a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -74,6 +74,7 @@ exactly where the mutable state is supposed to be defined and used. Never use default arguments in core business logic. Default arguments should be reserved for user-facing APIs. +Do NOT encode default arguments through overloading to skirt this rule. Keep it DRY: if you find yourself copying and pasting code, consider refactoring it into a reusable function or class. diff --git a/hkmc2/jvm/src/test/scala/hkmc2/SubstitutionGrowthTest.scala b/hkmc2/jvm/src/test/scala/hkmc2/SubstitutionGrowthTest.scala index 9d0b3e8f0c..c915671164 100644 --- a/hkmc2/jvm/src/test/scala/hkmc2/SubstitutionGrowthTest.scala +++ b/hkmc2/jvm/src/test/scala/hkmc2/SubstitutionGrowthTest.scala @@ -47,7 +47,7 @@ class SubstitutionGrowthTest extends AnyFunSuite: private final case class Measurement(views: Int, largestSubstitution: Int, activations: Int, typeInstances: Int) private val cctx = TestFolders.compilerCtx(os.pwd) - private given DebugPrinter = new DebugPrinter + private given DebugPrinter = new DebugPrinter() private val tl = TraceLogger.silent /** Resolves a generated unit through ordinary elaboration, in a fresh state. */ diff --git a/hkmc2/jvm/src/test/scala/hkmc2/TypeRelationTest.scala b/hkmc2/jvm/src/test/scala/hkmc2/TypeRelationTest.scala index 4e2b4efb99..ead3d9cd48 100644 --- a/hkmc2/jvm/src/test/scala/hkmc2/TypeRelationTest.scala +++ b/hkmc2/jvm/src/test/scala/hkmc2/TypeRelationTest.scala @@ -13,7 +13,7 @@ class TypeRelationTest extends AnyFunSuite: given owner: Elaborator.State = new Elaborator.State given state: ResolutionContext = owner.newResolverState.context given Raise = diagnostic => fail(diagnostic.theMsg) - given DebugPrinter = new DebugPrinter + given DebugPrinter = new DebugPrinter() given trace: TraceLogger = TraceLogger.silent given Config = Config.default(TestFolders.mainTestDir(os.pwd)) given CompilerCtx = CompilerCtx.fresh(io.FileSystem.default, TestFolders.compilerPaths(os.pwd), summon[Config]) diff --git a/hkmc2/shared/src/main/scala/hkmc2/CompilerCtx.scala b/hkmc2/shared/src/main/scala/hkmc2/CompilerCtx.scala index 882919ad54..d76fc05a7a 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/CompilerCtx.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/CompilerCtx.scala @@ -258,7 +258,7 @@ class CompilerCtx( given Elaborator.State = state given Config = rootConfig given CompilerCtx = this - given DebugPrinter = new DebugPrinter(state) + given DebugPrinter = new DebugPrinter(S(state)) val parse = ParserSetup(file, outputHandler) val elaborationConfig = Config.elaborationConfig(parse.resultBlk, file.up) debug.configure(elaborationConfig.debug) diff --git a/hkmc2/shared/src/main/scala/hkmc2/MLsCompiler.scala b/hkmc2/shared/src/main/scala/hkmc2/MLsCompiler.scala index ff028f2fc7..ccd9929e8f 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/MLsCompiler.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/MLsCompiler.scala @@ -63,7 +63,7 @@ class MLsCompiler def compileModule(file: io.Path): Unit = given Raise = mkRaise(file) - given DebugPrinter = new DebugPrinter + given DebugPrinter = new DebugPrinter(N) val compilerTL = TraceLogger.silent diff --git a/hkmc2/shared/src/main/scala/hkmc2/semantics/SymbolPrinter.scala b/hkmc2/shared/src/main/scala/hkmc2/semantics/SymbolPrinter.scala index 01c9ec4e46..5d24979fdd 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/semantics/SymbolPrinter.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/semantics/SymbolPrinter.scala @@ -6,7 +6,7 @@ import syntax.* import hkmc2.utils.* -class SymbolPrinter(val dbgScp: Scope)(using owner: Elaborator.State) extends DebugPrinter(owner): +class SymbolPrinter(val dbgScp: Scope)(using owner: Elaborator.State) extends DebugPrinter(S(owner)): override protected def symbolName(sym: Symbol): Str = sym.showFullName(using dbgScp, ShowCfg.internal, throw _) diff --git a/hkmc2/shared/src/main/scala/hkmc2/utils/utils.scala b/hkmc2/shared/src/main/scala/hkmc2/utils/utils.scala index feb6abdcf9..a29402f9c2 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/utils/utils.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/utils/utils.scala @@ -60,11 +60,10 @@ extension (context: StringContext) class DebugPrinter( - final val showUids: Bool, - private val uidOwner: Opt[semantics.Elaborator.State], + private val uidOwner: Opt[semantics.Elaborator.State] = N, + final val showUids: Bool = false, ): - def this() = this(false, N) - def this(owner: semantics.Elaborator.State) = this(false, S(owner)) + require(!showUids || uidOwner.isDefined) protected def symbolName(sym: semantics.Symbol): Str = sym.prefix + sym.nme protected def preProcess(t: Product): Product = t @@ -174,7 +173,7 @@ end DebugPrinter object DebugPrinter: private final class UidView(val underlying: DebugPrinter, enabled: Bool) - extends DebugPrinter(enabled, underlying.uidOwner): + extends DebugPrinter(underlying.uidOwner, enabled): override protected def symbolName(sym: semantics.Symbol): Str = underlying.symbolName(sym) override protected def preProcess(t: Product): Product = underlying.preProcess(t) override protected def postProcess(t: Product): Str = underlying.postProcess(t)