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:
- syntax or semantic acceptance
- a fact or payload model
- a lowering component
- 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
- Creating Methods And Chain Features - semantic ownership for methods and chains
- Emit Walkthrough - the backend-side version of package ownership