Elvelt

EMPL IR

EMPLValidator — IR structural validation

EMPLValidator — IR structural validation

EMPLValidator walks an IR tree and produces a structured ValidationResult of errors and warnings. It is declared in MPL_Compiler/core/ir/EMPLValidator.h (27 lines) and implemented in EMPLValidator.cpp.

// MPL_Compiler/core/ir/EMPLValidator.h:17
class EMPLValidator {
public:
    static ValidationResult validate(const std::shared_ptr<ModuleNode>& module);
    static ValidationResult validate(const std::shared_ptr<ProgramNode>& program);

private:
    static void validateNode(const EMPLNodePtr& node,
                             ValidationResult& result,
                             bool strictUnsupported);
};

ValidationResult

// MPL_Compiler/core/ir/EMPLValidator.h:11
struct ValidationResult {
    bool ok = true;
    std::vector<std::string> errors;
    std::vector<std::string> warnings;
};
Field Type Meaning
ok bool true if errors.empty(). warnings do NOT affect ok.
errors vector<string> Human-readable error messages (one per failure). An error means the IR is malformed.
warnings vector<string> Human-readable warnings (e.g. legacy PrintCall use). The IR is correct, but the caller may want to upgrade.

API

Method Source Purpose
validate(module) EMPLValidator.h:19 Validate a single module. Strict on Unsupported.
validate(program) EMPLValidator.h:20 Validate a program. Walks every module.
validateNode(node, result, strictUnsupported) (private) EMPLValidator.h:23 Per-node recursive check.

validateNode is private; users only call the two public overloads. Internally both public methods dispatch to validateNode with strictUnsupported = true.

Usage

#include "core/ir/EMPLValidator.h"

auto result = mpl::ir::EMPLValidator::validate(module);
if (!result.ok) {
    for (const auto& err : result.errors) {
        std::cerr << "IR error: " << err << "\n";
    }
    return 1;
}
for (const auto& warn : result.warnings) {
    std::cerr << "IR warning: " << warn << "\n";
}

Validation rules

The validator checks the following categories. Each rule produces one or more error messages with the offending node's SourceLocation prefix (<file>:<line>:<col> when available).

Rule V1 — Required child slots

Each node kind has a set of required child slots. Missing required children is an error.

Node Required slots
ModuleNode name (non-empty string)
FunctionDeclNode name (non-empty); body may be nullptr only if the function is abstract / extern (no flag for this currently — nullptr body is allowed for forward declarations)
IfNode condition, thenBranch
ForNode body; init / condition / increment MAY be nullptr (empty parts of for (;;))
WhileNode condition, body
DoWhileNode body, condition
SwitchNode expr, cases[] (default branch may be nullptr)
TryCatchNode tryBlock
BinaryOpNode left, right, op
UnaryOpNode operand, op
FunctionCallNode name, arguments[]
MethodCallNode object, methodName
MemberAccessNode object, memberName
IndexAccessNode object, index
LiteralNode value (always present by construction)
IdentifierNode name
TernaryNode condition, whenTrue, whenFalse
AwaitNode expression
YieldNode expression
LambdaNode parameters[], body or expressionBody (at least one)
ImportNode module (non-empty)
ExportNode either bindings[] non-empty, value != nullptr, or isReexport = true
CastNode targetType (non-empty), expression
ReturnNode / ThrowNode value MAY be nullptr

Rule V2 — Operator allow-list (BinaryOpNode, UnaryOpNode)

For BinaryOpNode::op, the validator accepts the following strings (others produce an error):

Category Allowed op strings
Arithmetic +, -, *, /, %, **
Comparison ==, !=, ===, !==, <, <=, >, >=
Logical &&, `
Bitwise &, |, ^, <<, >>, >>>
String / collection in, instanceof

For UnaryOpNode::op:

Category Allowed op strings
Logical !
Arithmetic +, -
Increment / decrement ++, --
Type queries typeof, void, delete
Bitwise ~

Note. === / !== are accepted as IR-level operators (their semantics are language-defined; backends MAY lower them to == / != when the language lacks strict equality).

Rule V3 — Assignment operator allow-list

AssignmentNode::op accepts exactly:

op Meaning
= Plain assignment
+= Add-assign
-= Subtract-assign
*= Multiply-assign
/= Divide-assign
%= Modulo-assign
**= Power-assign (where supported)
<<=, >>=, >>>=, &=, |=, ^= Bitwise compound assigns
&&=, ||=, ??= Logical / nullish compound assigns

Anything else is an error.

Rule V4 — Standard-library module/function names

StandardLibraryCallNode::moduleName and functionName are checked against the canonical MPL_Standard/Specification.json module list. Unknown combinations produce an error. The canonical module set is:

Module Functions (canonical)
Console WriteLine, Write, ErrorWriteLine, ErrorWrite, ReadLine
Environment GetVariable, SetVariable, GetCurrentDirectory, SetCurrentDirectory, GetPlatform, GetNewLine
IO ReadFileText, WriteFileText, FileExists, DeleteFile, CreateDirectory, DeleteDirectory, GetDirectoryEntries
Path Join, GetDirectoryName, GetFileName, GetExtension, GetFullPath
Process Exit, GetCommandLineArgs, GetRuntimeVersion
Math Abs, Floor, Ceil, Round, Sqrt, Pow, Max, Min, Pi, E
String Trim, ToLower, ToUpper, StartsWith, EndsWith, Split, Join
DateTime NowUnixMs, NowUnixSeconds

This list is duplicated from MPL_Standard/README.md lines 49-56 and MPL_Standard/CppImpl.h lines 32-332.

Rule V5 — Unsupported nodes (strict mode)

When strictUnsupported is true (the default for both public methods), the validator produces an error for every UnsupportedNode. When false, an UnsupportedNode produces a warning instead.

This is the recommended gate for release builds: run EMPLValidator::validate after the frontend parses and before the canonicalizer runs. If the result has any UnsupportedNode, the build fails.

Rule V6 — Legacy nodes produce warnings

The following legacy NodeKind values always produce a warning (not an error) so that older frontends can keep emitting them while still passing validation:

Legacy kind Warning message
PrintCall "PrintCall is deprecated; emit StandardLibraryCall(Console, WriteLine|Write, args) instead"
FuncDecl "FuncDecl is a retired alias; use FunctionDecl"
MethodNode alias (only at the C++ type level — no NodeKind)

The validator does NOT downgrade these to errors, so legacy frontends continue to work; new code SHOULD upgrade.

Rule V7 — Identifier name validity

IdentifierNode::name must be a non-empty string and must not contain control characters (\0\x1F) or whitespace.

Rule V8 — Variable declaration invariants

VariableDeclNode invariants:

  • If name is non-empty AND pattern is non-null, the validator reports an error: pick one or the other.
  • If both name and pattern are empty, the validator reports an error.

Rule V9 — Class declaration invariants

ClassDeclNode invariants:

  • name must be non-empty.
  • methods[] may contain any number of FunctionDeclNode entries. A method without a body (abstract) MUST have isStatic = false (abstract static methods are not modeled).
  • If baseClass is non-null, its kind MUST be Identifier or MemberAccess (a class identifier expression).

Rule V10 — Import / export invariants

ImportNode invariants:

  • module must be non-empty.
  • If isSideEffectOnly = true, bindings MUST be empty.
  • If bindings is non-empty, every ImportBinding::localName must be non-empty.

ExportNode invariants:

  • Exactly one of: isDefault = true, isReexport = true with reexportModule non-empty, OR bindings non-empty OR value != nullptr.

Failure modes

If validation finds errors, the compiler is expected to fail fast with a non-zero exit code. There is no auto-correction step; canonicalization does not repair invalid IR.

See also