Skip to main content

Semantic And Lowering Walkthrough

Package features usually need at least two real layers:

  • semantic ownership
  • lowering ownership

The repo already shows this pattern clearly in the Test package.


Why This Split Exists

Semantic logic answers:

  • is the feature valid here?
  • what types and facts does it produce?

Lowering logic answers:

  • how does that feature become IR or runtime calls?

If a package owns the feature, it should own both layers.


A Real Lowering Example

From morphs/Test/host/TestHostMorphs.cpp:

class TestDeclarationLowering final : public HostDeclarationLowering {
public:
bool matches(const ASTNode &node) const override {
return test_morph::matchesTestExtension(node);
}

bool lower(NIRBuilder &builder, ASTNode *node,
std::string *outError) override {
auto *payload = test_morph::asTestBlockPayload(node);
if (payload == nullptr) {
return true;
}
if (builder.compilationMode() != morph::CompilationMode::Test) {
return true;
}
if (payload->compileExpectation) {
return true;
}

HarnessState &harness = ensureHarness(builder);
const int caseIndex = harness.nextCaseIndex++;
if (!matchesRequestedFilters(*payload, caseIndex)) {
return true;
}
const std::string caseName = "__MorphTestCase_" + std::to_string(caseIndex);
const std::string displayName = testNameOrDefault(*payload, caseIndex);

builder.beginSyntheticFunction(caseName, NType::makeInt());
Instruction *scopeBegin =
builder.createInst(InstKind::Call, NType::makeVoid(), "");
scopeBegin->addOperand(makeStringLiteral(kScopeBeginSymbol));
scopeBegin->addOperand(makeStringLiteral(displayName));
scopeBegin->addOperand(makeStringLiteral(payload->tag));
scopeBegin->addOperand(makeStringLiteral(payload->expectedId));
scopeBegin->addOperand(makeStringLiteral(node->location.file));
scopeBegin->addOperand(makeIntegerLiteral(node->location.line));
...
}
};

MORPHLANG_MORPH_DEFINE_DECLARATION_LOWERING_FACTORY(test_declaration_lowering,
TestDeclarationLowering)

What This Lowering Does

This package-owned lowering:

  • matches a package extension node
  • checks compilation mode
  • creates synthetic test functions
  • emits calls into package-owned runtime symbols
  • appends work into a generated harness

That is real feature lowering, but it still lives under the package.


Managed Statement Lowering

The same file also shows how a package can lower internal behavior statement by statement:

void lowerManagedStep(NIRBuilder &builder, ASTNode *statement, int stepIndex) {
Instruction *stepBegin =
builder.createInst(InstKind::Call, NType::makeVoid(), "");
stepBegin->addOperand(makeStringLiteral(kStepBeginSymbol));
stepBegin->addOperand(makeIntegerLiteral(stepIndex));
...

if (statement->type == ASTNodeType::ExtensionStmt) {
bool handled = false;
std::string error;
if (!morph::nir::morph::MorphLoweringRuntime::tryLowerStatement(
builder.toolRoot(), &builder, statement, &handled, &error)) {
builder.reportError(statement->location, error);
} else if (!handled) {
builder.reportError(statement->location,
"statement lowering is Morph-owned but no component claimed this extension statement");
}
} else {
builder.buildExpression(statement);
}
...
}

This is important because it shows package lowering can:

  • call back into generic lowering runtime
  • lower package-owned extension statements
  • recover from package-owned failures

What To Learn From This

A real package feature often needs:

  1. syntax or semantic acceptance
  2. a fact or payload model
  3. a lowering component
  4. runtime or route integration

If you only write the semantic side, the feature is incomplete.


Framework Widening Rule

If the lowerer cannot express the behavior you need:

  • widen the generic lowering API
  • keep the actual feature lowering in the package

Do not move the feature into core lowering ownership.


Next Steps