Skip to content

Commit

Permalink
Structured errors from compiler (#342)
Browse files Browse the repository at this point in the history
Instead of errors as one monolithic string from the compiler, it's now
structured with separate path, kind, name, and detail components.

Also adds `edgir.local_path_to_str_list` utility function.

Updates proto version to 4.
  • Loading branch information
ducky64 authored Apr 24, 2024
1 parent dda3378 commit f7edaae
Show file tree
Hide file tree
Showing 11 changed files with 219 additions and 34 deletions.
2 changes: 1 addition & 1 deletion compiler/src/main/scala/edg/compiler/Compiler.scala
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ class AssignNamer() {
}

object Compiler {
final val kExpectedProtoVersion = 3
final val kExpectedProtoVersion = 4
}

/** Compiler for a particular design, with an associated library to elaborate references from.
Expand Down
136 changes: 130 additions & 6 deletions compiler/src/main/scala/edg/compiler/CompilerError.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4,45 +4,142 @@ import edg.EdgirUtils.SimpleLibraryPath
import edg.wir.{DesignPath, IndirectDesignPath}
import edgir.expr.expr
import edgir.ref.ref
import edgrpc.compiler.{compiler => edgcompiler}

class ElaboratingException(msg: String, wrapped: Exception) extends Exception(f"$msg:\n$wrapped")

sealed trait CompilerError
sealed trait CompilerError {
def toIr: edgcompiler.ErrorRecord
}

object CompilerError {
case class Unelaborated(elaborateRecord: ElaborateRecord, missing: Set[ElaborateRecord]) extends CompilerError {
case class Unelaborated(record: ElaborateRecord, missing: Set[ElaborateRecord]) extends CompilerError {
// These errors may be redundant with below, but provides dependency data
override def toString: String = s"Unelaborated missing dependencies $elaborateRecord:\n" +
override def toString: String = s"Unelaborated missing dependencies $record:\n" +
s"${missing.map(x => s"- $x").mkString("\n")}"

override def toIr: edgcompiler.ErrorRecord = {
val missingStr = "missing " + missing.map(_.toString).mkString(", ")
val (kind, path) = record match {
case ElaborateRecord.ExpandBlock(path, _) =>
(f"Uncompiled block, $missingStr", Some(path.asIndirect.toLocalPath))
case ElaborateRecord.Block(path) => (f"Uncompiled block, $missingStr", Some(path.asIndirect.toLocalPath))
case ElaborateRecord.Link(path) => (f"Uncompiled link, $missingStr", Some(path.asIndirect.toLocalPath))
case ElaborateRecord.LinkArray(path) =>
(f"Uncompiled link array, $missingStr", Some(path.asIndirect.toLocalPath))
case ElaborateRecord.Parameter(containerPath, _, postfix, _) =>
(f"Uncompiled parameter, $missingStr", Some((containerPath.asIndirect ++ postfix).toLocalPath))
case _ => (f"Uncompiled internal element, $missingStr", None)
}
edgcompiler.ErrorRecord(
path = path,
kind = kind,
name = "",
details = ""
)
}
}
case class LibraryElement(path: DesignPath, target: ref.LibraryPath) extends CompilerError {
override def toString: String = s"Unelaborated library element ${target.toSimpleString} @ $path"

override def toIr: edgcompiler.ErrorRecord = {
edgcompiler.ErrorRecord(
path = Some(path.asIndirect.toLocalPath),
kind = "Uncompiled library element",
name = "",
details = f"class ${target.toSimpleString}"
)
}
}
case class UndefinedPortArray(path: DesignPath, portType: ref.LibraryPath) extends CompilerError {
override def toString: String = s"Undefined port array ${portType.toSimpleString} @ $path"

override def toIr: edgcompiler.ErrorRecord = {
edgcompiler.ErrorRecord(
path = Some(path.asIndirect.toLocalPath),
kind = "Undefined port array",
name = "",
details = f"port type ${portType.toSimpleString}"
)
}
}

// TODO partly redundant w/ LibraryElement error, which is a compiler-level error
case class LibraryError(path: DesignPath, target: ref.LibraryPath, err: String) extends CompilerError {
override def toString: String = s"Library error ${target.toSimpleString} @ $path: $err"

override def toIr: edgcompiler.ErrorRecord = {
edgcompiler.ErrorRecord(
path = Some(path.asIndirect.toLocalPath),
kind = "Library error",
name = "",
details = f"${target.toSimpleString}: err"
)
}
}
case class GeneratorError(path: DesignPath, target: ref.LibraryPath, err: String) extends CompilerError {
override def toString: String = s"Generator error ${target.toSimpleString} @ $path: $err"

override def toIr: edgcompiler.ErrorRecord = {
edgcompiler.ErrorRecord(
path = Some(path.asIndirect.toLocalPath),
kind = "Generator error",
name = "",
details = f"${target.toSimpleString}: err"
)
}
}
case class RefinementSubclassError(path: DesignPath, refinedLibrary: ref.LibraryPath, designLibrary: ref.LibraryPath)
extends CompilerError {
override def toString: String =
s"Invalid refinement ${refinedLibrary.toSimpleString} <- ${designLibrary.toSimpleString} @ $path"
s"Invalid refinement ${refinedLibrary.toSimpleString} -> ${designLibrary.toSimpleString} @ $path"

override def toIr: edgcompiler.ErrorRecord = {
edgcompiler.ErrorRecord(
path = Some(path.asIndirect.toLocalPath),
kind = "Invalid refinement",
name = "",
details = f"${refinedLibrary.toSimpleString} -> ${designLibrary.toSimpleString}"
)
}
}

case class OverAssign(target: IndirectDesignPath, causes: Seq[OverAssignCause]) extends CompilerError {
override def toString: String = s"Overassign to $target:\n" +
s"${causes.map(x => s"- $x").mkString("\n")}"

override def toIr: edgcompiler.ErrorRecord = {
edgcompiler.ErrorRecord(
path = Some(target.toLocalPath),
kind = "Overassign",
name = "",
details = causes.map(_.toString).mkString(", ")
)
}
}

case class BadRef(path: DesignPath, ref: IndirectDesignPath) extends CompilerError
case class BadRef(path: DesignPath, ref: IndirectDesignPath) extends CompilerError {
override def toIr: edgcompiler.ErrorRecord = {
edgcompiler.ErrorRecord(
path = Some(path.asIndirect.toLocalPath),
kind = "Bad reference",
name = "",
details = ref.toLocalPath.toString
)
}
}

case class AbstractBlock(path: DesignPath, blockType: ref.LibraryPath) extends CompilerError {
override def toString: String = s"Abstract block: $path (of type ${blockType.toSimpleString})"

override def toIr: edgcompiler.ErrorRecord = {
edgcompiler.ErrorRecord(
path = Some(path.asIndirect.toLocalPath),
kind = "Abstract block",
name = "",
details = f"block type ${blockType.toSimpleString}"
)
}
}

sealed trait AssertionError extends CompilerError
Expand All @@ -55,6 +152,15 @@ object CompilerError {
) extends AssertionError {
override def toString: String =
s"Failed assertion: $root.$constrName, ${ExprToString.apply(value)} => $result"

override def toIr: edgcompiler.ErrorRecord = {
edgcompiler.ErrorRecord(
path = Some(root.asIndirect.toLocalPath),
kind = "Failed assertion",
name = constrName,
details = s"${ExprToString.apply(value)} => $result"
)
}
}
case class MissingAssertion(
root: DesignPath,
Expand All @@ -63,7 +169,16 @@ object CompilerError {
missing: Set[IndirectDesignPath]
) extends AssertionError {
override def toString: String =
s"Unevaluated assertion: $root.$constrName (${ExprToString.apply(value)}), missing ${missing.mkString(", ")}"
s"Unevaluated assertion: $root.$constrName: missing ${missing.mkString(", ")} in ${ExprToString.apply(value)}"

override def toIr: edgcompiler.ErrorRecord = {
edgcompiler.ErrorRecord(
path = Some(root.asIndirect.toLocalPath),
kind = "Unevaluated assertion",
name = constrName,
details = s"Missing ${missing.mkString(", ")} in ${ExprToString.apply(value)}"
)
}
}

case class InconsistentLinkArrayElements(
Expand All @@ -75,6 +190,15 @@ object CompilerError {
) extends CompilerError {
override def toString: String =
s"Inconsistent link array elements: $linkPath ($linkElements), $blockPortPath ($blockPortElements)"

override def toIr: edgcompiler.ErrorRecord = {
edgcompiler.ErrorRecord(
path = Some(linkPath.toLocalPath),
kind = "Inconsistent link array elements",
name = "",
details = s"$linkElements (link-side), $blockPortElements (block-side)"
)
}
}

sealed trait OverAssignCause
Expand Down
11 changes: 9 additions & 2 deletions compiler/src/main/scala/edg/compiler/CompilerServerMain.scala
Original file line number Diff line number Diff line change
Expand Up @@ -74,15 +74,22 @@ object CompilerServerMain {
new DesignStructuralValidate().map(compiled) ++ new DesignRefsValidate().validate(compiled)
val result = edgcompiler.CompilerResult(
design = Some(compiled),
error = errors.mkString("\n"),
errors = errors.map(_.toIr),
solvedValues = constPropToSolved(compiler.getAllSolved)
)
result
} catch {
case e: Throwable =>
val sw = new StringWriter()
e.printStackTrace(new PrintWriter(sw))
edgcompiler.CompilerResult(error = sw.toString)
edgcompiler.CompilerResult(errors =
Seq(edgcompiler.ErrorRecord(
path = Some(DesignPath().asIndirect.toLocalPath),
kind = "Internal error",
name = "",
details = sw.toString
))
)
}
}

Expand Down
4 changes: 2 additions & 2 deletions edg/BoardCompiler.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,9 @@ def compile_board(design: Type[Block], target_dir_name: Optional[Tuple[str, str]
with open(design_filename, 'wb') as raw_file:
raw_file.write(compiled.design.SerializeToString())

if compiled.error:
if compiled.errors:
import edg_core
raise edg_core.ScalaCompilerInterface.CompilerCheckError(f"error during compilation: \n{compiled.error}")
raise edg_core.ScalaCompilerInterface.CompilerCheckError(f"error during compilation:\n{compiled.errors_str()}")

netlist_all = NetlistBackend().run(compiled)
netlist_refdes = NetlistBackend().run(compiled, {'RefdesMode': 'refdes'})
Expand Down
24 changes: 17 additions & 7 deletions edg_core/ScalaCompilerInterface.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,20 +22,29 @@ class CompiledDesign:
def from_compiler_result(result: edgrpc.CompilerResult) -> 'CompiledDesign':
values = {value.path.SerializeToString(): edgir.valuelit_to_lit(value.value)
for value in result.solvedValues}
return CompiledDesign(result.design, values, result.error)
return CompiledDesign(result.design, values, list(result.errors))

@staticmethod
def from_request(design: edgir.Design, values: Iterable[edgrpc.ExprValue]) -> 'CompiledDesign':
values_dict = {value.path.SerializeToString(): edgir.valuelit_to_lit(value.value)
for value in values}
return CompiledDesign(design, values_dict, "")
return CompiledDesign(design, values_dict, [])

def __init__(self, design: edgir.Design, values: Dict[bytes, edgir.LitTypes], error: str):
def __init__(self, design: edgir.Design, values: Dict[bytes, edgir.LitTypes], errors: List[edgrpc.ErrorRecord]):
self.design = design
self.contents = design.contents # convenience accessor
self.error = error
self.errors = errors
self._values = values

def errors_str(self) -> str:
err_strs = []
for error in self.errors:
error_pathname = edgir.local_path_to_str(error.path)
if error.name:
error_pathname += ':' + error.name
err_strs.append(f"{error.kind} @ {error_pathname}: {error.details}")
return '\n'.join([f'- {err_str}' for err_str in err_strs])

# Reserved.V is a string because it doesn't load properly at runtime
# Serialized strings are used since proto objects are mutable and unhashable
def get_value(self, path: Union[edgir.LocalPath, Iterable[Union[str, 'edgir.Reserved.V']]]) ->\
Expand Down Expand Up @@ -112,9 +121,10 @@ def compile(self, block: Type[Block], refinements: Refinements = Refinements(),

assert result is not None
assert result.HasField('design')
if result.error and not ignore_errors:
raise CompilerCheckError(f"error during compilation: \n{result.error}")
return CompiledDesign.from_compiler_result(result)
design = CompiledDesign.from_compiler_result(result)
if result.errors and not ignore_errors:
raise CompilerCheckError(f"error during compilation:\n{design.errors_str()}")
return design

def close(self):
assert self.process is not None
Expand Down
Binary file modified edg_core/resources/edg-compiler-precompiled.jar
Binary file not shown.
2 changes: 1 addition & 1 deletion edg_hdl_server/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from edg_core.Core import NonLibraryProperty


EDG_PROTO_VERSION = 3
EDG_PROTO_VERSION = 4


class LibraryElementIndexer:
Expand Down
9 changes: 6 additions & 3 deletions edgir/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ def valuelit_to_lit(expr: ValueLit) -> LitTypes:
elif expr.HasField('integer'):
return expr.integer.val
elif expr.HasField('range') and \
expr.range.minimum.HasField('floating') and expr.range.maximum.HasField('floating'):
expr.range.minimum.HasField('floating') and expr.range.maximum.HasField('floating'):
from edg_core.Range import Range # TODO fix me, this prevents a circular import
return Range(expr.range.minimum.floating.val, expr.range.maximum.floating.val)
elif expr.HasField('text'):
Expand Down Expand Up @@ -169,7 +169,8 @@ def AssignRef(dst: Iterable[str], src: Iterable[str]) -> ValueExpr:
return pb


def local_path_to_str(path: LocalPath) -> str:
def local_path_to_str_list(path: LocalPath) -> List[str]:
"""Convert a LocalPath to a list of its components. Reserved params are presented as strings."""
def step_to_str(step: LocalStep) -> str:
if step.HasField('name'):
return step.name
Expand All @@ -180,8 +181,10 @@ def step_to_str(step: LocalStep) -> str:
}[step.reserved_param]
else:
raise ValueError(f"unknown step {step}")
return [step_to_str(step) for step in path.steps]

return '.'.join([step_to_str(step) for step in path.steps])
def local_path_to_str(path: LocalPath) -> str:
return '.'.join(local_path_to_str_list(path))

@overload
def add_pair(pb: RepeatedCompositeFieldContainer[NamedPortLike], name: str) -> PortLike: ...
Expand Down
16 changes: 9 additions & 7 deletions edgrpc/compiler_pb2.py

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit f7edaae

Please sign in to comment.