Add top-level fence marker for unwrapped, compile-only Scala blocks#2
Merged
Conversation
Every Scala block is normally wrapped in `object MarklitWrapper: def
run(): Unit = …`, so user code is a method body. Constructs that are
only legal — or only warning-free — at the top level of a source file
can't be demonstrated that way:
- `opaque type` and `@main def` fail to compile inside the wrapper.
- Matching a parameterized enum/ADT case against a non-local type
warns "the type test for X cannot be checked at runtime because
it's a local class", because an enum declared inside `def run()` is
a local class. This is the warning that forced `show-warnings=false`
across the conduit docs.
The `top-level` modifier compiles a block verbatim as its own
compilation unit (no wrapper). Such blocks are compile-only: the code
and any diagnostics render, but they never execute. A top-level
definition can be hoisted into a running example by giving it an `id=`
and `extends=`-ing it from a normal block — marklit emits the definition
at file scope while the example runs inside the wrapper, so it compiles
cleanly and shows output.
Details:
- Modifier.TopLevel + CodeBlock.isTopLevel/modifierConflicts; top-level
is strict (only scope options, a version selector, and show-warnings
may accompany it).
- ScopeContext carries topLevel + topLevelPriorCode; ScalaCompiler
bypasses the wrapper/marker for top-level blocks and hoists inherited
top-level definitions above the wrapper for normal consumers.
- ScopeManager tags scopes by kind, partitions inherited code into
hoist/body buckets, and guards cross-kind extends/append/reuse. A
normal block may extend a top-level scope; the reverse is rejected.
- Version selectors compose: top-level blocks honor scala=<major> and
scala=<version>, and a consumer must agree on the version.
- BlockCacheKey includes topLevel + topLevelPriorCode (key prefix v3).
Adds README docs (feature matrix rows + a section, with an mdoc
comparison), a worked example (examples/.../top-level.md, indexed), and
positive/negative/pathological tests across the model, parser, compiler,
cache, scope, processor, and end-to-end layers.
Adds .github/workflows/ci.yml: on push to main and on every PR, runs `sbt "scalafmtCheckAll;test"` — verifies formatting, then compiles and runs the full test suite in a single sbt invocation. Reformats the repo with scalafmt so the new format gate passes. The build.sbt and MarkdownRenderer.scala changes are pre-existing formatting drift unrelated to the top-level feature; the rest are the feature's own files. All changes are whitespace/wrapping only.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Adds a
top-levelfence modifier that compiles a Scala block verbatim as its own compilation unit — noobject MarklitWrapper: def run()wrapper. Such blocks are compile-only (code + diagnostics render, never executed). A top-level definition can be hoisted into a running example viaid=/extends=.Why
Wrapping every block in
def run()makes user code a method body, which breaks or warns on top-level-only constructs (empirically confirmed against the bundled shim):enum/ADT case against a non-local scrutinee emitsthe type test for X cannot be checked at runtime because it's a local class— because anenuminsidedef run()is a local class. This is the warning that forcedshow-warnings=falseacross the conduit docs. Hoisting the enum to file scope removes it.opaque typeand@main deffail outright inside the wrapper.vs. mdoc
Confirmed from mdoc's own docs: mdoc wraps blocks in a class, and its only escape (
reset-object) wraps in an object and resets the scope. It has no top-level modifier and can't share a definition forward. marklit'stop-levelcompiles at genuine file scope and hoists forward.How
Modifier.TopLevel+CodeBlock.isTopLevel/modifierConflicts—top-levelis strict (only scope options, a version selector, andshow-warningsmay accompany it).ScopeContextcarriestopLevel+topLevelPriorCode;ScalaCompilerbypasses the wrapper/marker for top-level blocks and hoists inherited definitions above the wrapper for normal consumers.ScopeManagertags scopes by kind, splits inherited code into hoist/body buckets, and guards cross-kindextends/append/id-reuse. A normal block may extend a top-level scope; the reverse is rejected.top-level,scala=3.7.3compiles against that version, and a consumer must agree.BlockCacheKeyincludes the new inputs (key prefix bumped tov3).Docs & example
examples/base/src/main/markdown/top-level.md(added to the index), demonstrating the warning contrast,opaque type,@main, and a cross-versiongiven/extension. Verified by rendering it through the example's sbt build.Tests
Positive / negative / pathological coverage across model, parser, compiler, cache, scope, processor, and end-to-end (
MarklitSpec, both isolated and--page-scope) layers, including a regression test pinning the local-class warning behavior and cross-version cases. Full suite green (261 core + 63 compiler + Java).