Skip to content

Commit 356ccac

Browse files
authored
Diagnostics for main function (#1100)
Resolves #1093
1 parent c1832e6 commit 356ccac

22 files changed

+197
-51
lines changed

effekt/jvm/src/main/scala/effekt/Repl.scala

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -121,11 +121,11 @@ class Repl(driver: Driver) extends REPL[Tree, EffektConfig, EffektError] {
121121
def typecheck(source: Source, config: EffektConfig): Unit =
122122
parse(source) match {
123123
case Success(e: Term, _) =>
124-
runFrontend(source, module.make(source, e), config) { mod =>
125-
// TODO this is a bit ad-hoc
126-
val mainSym = mod.exports.terms("main").head
127-
val mainTpe = context.functionTypeOf(mainSym)
128-
output.emitln(pp"${mainTpe.result}")
124+
// TODO this is a bit ad-hoc!!!
125+
runFrontend(source, module.make(source, e, "repl"), config) { mod =>
126+
val replSym = mod.exports.terms("repl").head
127+
val replTpe = context.functionTypeOf(replSym)
128+
output.emitln(pp"${replTpe.result}")
129129
}
130130

131131
case Success(other, _) =>
@@ -306,15 +306,22 @@ class Repl(driver: Driver) extends REPL[Tree, EffektConfig, EffektError] {
306306
def contains(im: Include) = includes.exists { other => im.path == other.path }
307307

308308
/**
309-
* Create a module declaration using the given expression as body of main
309+
* Create a module declaration using the given expression as body of `defName`
310+
*
311+
* def <defName>() = {
312+
* return <expr>
313+
* }
314+
*
315+
* By default `defName` is "main", however, when type checking a main function the result must be unit.
316+
* When querying the type of an expression using :t, this is not desirable.
317+
* Hence, the caller can choose some different name such that these restrictions don't exist.
310318
*/
311-
def make(source: Source, expr: Term): ModuleDecl = {
312-
319+
def make(source: Source, expr: Term, defName: String = "main"): ModuleDecl = {
313320
val body = Return(expr, expr.span.synthesized)
314321
val fakeSpan = Span(source, 0, 0, origin = Origin.Synthesized)
315322
val fullSpan = Span(source, 0, source.content.length, origin = Origin.Synthesized)
316323
ModuleDecl("interactive", includes,
317-
definitions :+ FunDef(IdDef("main", fakeSpan), Many.empty(fakeSpan), Many.empty(fakeSpan), Many.empty(fakeSpan), Maybe.None(fakeSpan),
324+
definitions :+ FunDef(IdDef(defName, fakeSpan), Many.empty(fakeSpan), Many.empty(fakeSpan), Many.empty(fakeSpan), Maybe.None(fakeSpan),
318325
body, Info.empty(fakeSpan), fullSpan), None, fullSpan)
319326
}
320327

effekt/jvm/src/test/scala/effekt/LSPTests.scala

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,106 @@ class LSPTests extends FunSuite {
122122
}
123123
}
124124

125+
// Diagnostics
126+
//
127+
//
128+
129+
test("main function return type") {
130+
withClientAndServer { (client, server) =>
131+
val (textDoc, range) = raw"""
132+
|def main() = 1
133+
|↑ ↑
134+
|""".stripMargin.textDocumentAndRange
135+
136+
val didOpenParams = new DidOpenTextDocumentParams()
137+
didOpenParams.setTextDocument(textDoc)
138+
server.getTextDocumentService().didOpen(didOpenParams)
139+
140+
val diagnostic = new Diagnostic()
141+
diagnostic.setRange(range)
142+
diagnostic.setSeverity(DiagnosticSeverity.Error)
143+
diagnostic.setSource("effekt")
144+
diagnostic.setMessage("Main must return Unit, please use `exit(n)` to return an error code.")
145+
146+
val diagnosticsWithError = new util.ArrayList[Diagnostic]()
147+
diagnosticsWithError.add(diagnostic)
148+
149+
val expected = List(
150+
new PublishDiagnosticsParams("file://test.effekt", new util.ArrayList[Diagnostic]()),
151+
new PublishDiagnosticsParams("file://test.effekt", diagnosticsWithError)
152+
)
153+
154+
val diagnostics = client.receivedDiagnostics()
155+
assertEquals(diagnostics, expected)
156+
}
157+
}
158+
159+
test("exactly one main function") {
160+
withClientAndServer { (client, server) =>
161+
val (textDoc, range) = raw"""
162+
|def main() = println("hello")
163+
|↑
164+
|def main() = 42
165+
| ↑
166+
|""".stripMargin.textDocumentAndRange
167+
168+
val didOpenParams = new DidOpenTextDocumentParams()
169+
didOpenParams.setTextDocument(textDoc)
170+
server.getTextDocumentService().didOpen(didOpenParams)
171+
172+
val diagnostic = new Diagnostic()
173+
diagnostic.setRange(range)
174+
diagnostic.setSeverity(DiagnosticSeverity.Error)
175+
diagnostic.setSource("effekt")
176+
diagnostic.setMessage("Multiple main functions defined: test::main, test::main")
177+
178+
val diagnosticsWithError = new util.ArrayList[Diagnostic]()
179+
diagnosticsWithError.add(diagnostic)
180+
181+
val expected = List(
182+
new PublishDiagnosticsParams("file://test.effekt", new util.ArrayList[Diagnostic]()),
183+
new PublishDiagnosticsParams("file://test.effekt", diagnosticsWithError)
184+
)
185+
186+
val diagnostics = client.receivedDiagnostics()
187+
assertEquals(diagnostics, expected)
188+
}
189+
}
190+
191+
test("no unhandled effects in main function") {
192+
withClientAndServer { (client, server) =>
193+
val (textDoc, range) = raw"""
194+
|effect Eff(): Unit
195+
|def main() = {
196+
|↑
197+
| do Eff()
198+
|}
199+
|↑
200+
|""".stripMargin.textDocumentAndRange
201+
202+
val didOpenParams = new DidOpenTextDocumentParams()
203+
didOpenParams.setTextDocument(textDoc)
204+
server.getTextDocumentService().didOpen(didOpenParams)
205+
206+
val diagnostic = new Diagnostic()
207+
diagnostic.setRange(range)
208+
diagnostic.setSeverity(DiagnosticSeverity.Error)
209+
diagnostic.setSource("effekt")
210+
diagnostic.setMessage("Main cannot have effects, but includes effects: { Eff }")
211+
212+
val diagnosticsWithError = new util.ArrayList[Diagnostic]()
213+
diagnosticsWithError.add(diagnostic)
214+
215+
val expected = List(
216+
new PublishDiagnosticsParams("file://test.effekt", new util.ArrayList[Diagnostic]()),
217+
new PublishDiagnosticsParams("file://test.effekt", diagnosticsWithError)
218+
)
219+
220+
val diagnostics = client.receivedDiagnostics()
221+
assertEquals(diagnostics, expected)
222+
}
223+
}
224+
125225
test("setTrace is implemented") {
126226
withClientAndServer { (client, server) =>
127227
val didOpenParams = new DidOpenTextDocumentParams()

effekt/shared/src/main/scala/effekt/Compiler.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -306,7 +306,7 @@ trait Compiler[Executable] {
306306

307307
lazy val Machine = Phase("machine") {
308308
case CoreTransformed(source, tree, mod, core) =>
309-
val main = Context.checkMain(mod)
309+
val main = Context.ensureMainExists(mod)
310310
val program = machine.Transformer.transform(main, core)
311311
(mod, main, program)
312312
}

effekt/shared/src/main/scala/effekt/Typer.scala

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,9 +79,47 @@ object Typer extends Phase[NameResolved, Typechecked] {
7979
// Store the backtrackable annotations into the global DB
8080
// This is done regardless of errors, since
8181
Context.commitTypeAnnotations()
82+
checkMain(input.mod)
8283
}
8384
}
8485

86+
/**
87+
* Ensures there are <= 1 main functions, that the potential main function has no unhandled effects and returns Unit.
88+
*/
89+
def checkMain(mod: Module)(using C: Context) = {
90+
val mains = Context.findMain(mod)
91+
if (mains.size > 1) {
92+
val names = mains.toList.map(sym => pp"${sym.name}").mkString(", ")
93+
C.abort(pp"Multiple main functions defined: ${names}")
94+
}
95+
mains.headOption.foreach { main =>
96+
val mainFn = main.asUserFunction
97+
Context.at(mainFn.decl) {
98+
// If type checking failed before reaching main, there is no type
99+
C.functionTypeOption(mainFn).foreach {
100+
case FunctionType(tparams, cparams, vparams, bparams, result, effects) =>
101+
if (vparams.nonEmpty || bparams.nonEmpty) {
102+
C.abort("Main does not take arguments")
103+
}
104+
105+
if (effects.nonEmpty) {
106+
C.abort(pp"Main cannot have effects, but includes effects: ${effects}")
107+
}
108+
109+
result match {
110+
case symbols.builtins.TInt =>
111+
C.abort(pp"Main must return Unit, please use `exit(n)` to return an error code.")
112+
case symbols.builtins.TUnit =>
113+
()
114+
case tpe =>
115+
// def main() = <>
116+
// has return type Nothing which should still be permissible since Nothing <: Unit
117+
matchExpected(tpe, symbols.builtins.TUnit)
118+
}
119+
}
120+
}
121+
}
122+
}
85123

86124
//<editor-fold desc="expressions">
87125

effekt/shared/src/main/scala/effekt/context/ModuleDB.scala

Lines changed: 15 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -53,16 +53,22 @@ trait ModuleDB { self: Context =>
5353
mod <- compiler.runFrontend(source)(using this)
5454
} yield mod
5555

56-
/**
57-
* Util to check whether main exists on the given module
56+
/**
57+
* Ad-hoc method of finding all functions called `main` in a module.
58+
*
59+
* Should be replaced by explicit @main annotation
5860
*/
59-
def checkMain(mod: Module)(implicit C: Context): TermSymbol = C.at(mod.decl) {
60-
61-
// deep discovery of main: should be replaced by explicit @main annotation
62-
def findMain(b: Bindings): Set[TermSymbol] =
63-
b.terms.getOrElse("main", Set()) ++ b.namespaces.flatMap { case (_, b) => findMain(b) }
61+
def findMain(mod: Module): Set[TermSymbol] = {
62+
def go(b: Bindings): Set[TermSymbol] =
63+
b.terms.getOrElse("main", Set()) ++ b.namespaces.flatMap { case (_, b) => go(b) }
64+
go(mod.exports)
65+
}
6466

65-
val mains = findMain(mod.exports)
67+
/**
68+
* Util to check whether exactly one main function exists on the given module
69+
*/
70+
def ensureMainExists(mod: Module)(implicit C: Context): TermSymbol = C.at(mod.decl) {
71+
val mains = findMain(mod)
6672

6773
if (mains.isEmpty) {
6874
C.abort("No main function defined")
@@ -73,31 +79,6 @@ trait ModuleDB { self: Context =>
7379
C.abort(pp"Multiple main functions defined: ${names}")
7480
}
7581

76-
val main = mains.head.asUserFunction
77-
78-
Context.at(main.decl) {
79-
val mainValueParams = C.functionTypeOf(main).vparams
80-
val mainBlockParams = C.functionTypeOf(main).bparams
81-
if (mainValueParams.nonEmpty || mainBlockParams.nonEmpty) {
82-
C.abort("Main does not take arguments")
83-
}
84-
85-
val tpe = C.functionTypeOf(main)
86-
val controlEffects = tpe.effects
87-
if (controlEffects.nonEmpty) {
88-
C.abort(pp"Main cannot have effects, but includes effects: ${controlEffects}")
89-
}
90-
91-
tpe.result match {
92-
case symbols.builtins.TInt =>
93-
C.abort(pp"Main must return Unit, please use `exit(n)` to return an error code.")
94-
case symbols.builtins.TUnit =>
95-
()
96-
case other =>
97-
C.abort(pp"Main must return Unit, but returns ${other}.")
98-
}
99-
100-
main
101-
}
82+
mains.head.asUserFunction
10283
}
10384
}

effekt/shared/src/main/scala/effekt/core/optimizer/DropBindings.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ object DropBindings extends Phase[CoreTransformed, CoreTransformed] {
2525
def run(input: CoreTransformed)(using C: Context): Option[CoreTransformed] =
2626
input match {
2727
case CoreTransformed(source, tree, mod, core) =>
28-
val main = C.checkMain(mod)
28+
val main = C.ensureMainExists(mod)
2929
Some(CoreTransformed(source, tree, mod, apply(Set(main), core)))
3030
}
3131

effekt/shared/src/main/scala/effekt/core/optimizer/Optimizer.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ object Optimizer extends Phase[CoreTransformed, CoreTransformed] {
1414
def run(input: CoreTransformed)(using Context): Option[CoreTransformed] =
1515
input match {
1616
case CoreTransformed(source, tree, mod, core) =>
17-
val term = Context.checkMain(mod)
17+
val term = Context.ensureMainExists(mod)
1818
val optimized = Context.timed("optimize", source.name) { optimize(source, term, core) }
1919
Some(CoreTransformed(source, tree, mod, optimized))
2020
}

effekt/shared/src/main/scala/effekt/generator/chez/ChezScheme.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ trait ChezScheme extends Compiler[String] {
6363

6464
lazy val Chez = Phase("chez") {
6565
case CoreTransformed(source, tree, mod, core) =>
66-
val mainSymbol = Context.checkMain(mod)
66+
val mainSymbol = Context.ensureMainExists(mod)
6767
val mainFile = path(mod)
6868
mainFile -> chez.Let(Nil, compilationUnit(mainSymbol, mod, core))
6969
}

effekt/shared/src/main/scala/effekt/generator/js/JavaScript.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ class JavaScript(additionalFeatureFlags: List[String] = Nil) extends Compiler[St
4444

4545
lazy val Optimized = allToCore(Core) andThen Aggregate andThen Optimizer andThen DropBindings map {
4646
case input @ CoreTransformed(source, tree, mod, core) =>
47-
val mainSymbol = Context.checkMain(mod)
47+
val mainSymbol = Context.ensureMainExists(mod)
4848
val mainFile = path(mod)
4949
(mainSymbol, mainFile, core)
5050
}

effekt/shared/src/main/scala/effekt/generator/vm/VM.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ class VM extends Compiler[(Id, symbols.Module, ModuleDecl)] {
3838

3939
lazy val Optimized = allToCore(Core) andThen Aggregate andThen core.optimizer.Optimizer map {
4040
case input @ CoreTransformed(source, tree, mod, core) =>
41-
val mainSymbol = Context.checkMain(mod)
41+
val mainSymbol = Context.ensureMainExists(mod)
4242
(mainSymbol, mod, core)
4343
}
4444
}

0 commit comments

Comments
 (0)