From d28916578bb62257613cbdafdfe7cd399c6fe5aa Mon Sep 17 00:00:00 2001 From: bQUARKz Date: Fri, 27 Mar 2026 17:24:36 +0000 Subject: [PATCH] implements PLN-0007 --- discussion/index.ndjson | 2 +- ...s-ignored-values-warning-implementation.md | 4 +- .../pbs/12. Diagnostics Specification.md | 7 +- .../13. Lowering IRBackend Specification.md | 6 ++ .../pbs/4. Static Semantics Specification.md | 3 + .../lowering/PbsExecutableBodyLowerer.java | 102 ++++++++++++++++++ .../PbsExecutableCallsiteEmitter.java | 23 ++++ .../semantics/PbsFlowStatementAnalyzer.java | 10 +- .../pbs/semantics/PbsFlowTypeOps.java | 10 ++ .../pbs/semantics/PbsSemanticsErrors.java | 1 + .../pbs/PbsDiagnosticsContractTest.java | 23 ++++ .../compiler/pbs/PbsFrontendCompilerTest.java | 68 +++++++++++- .../PbsIgnoredValueSemanticsTest.java | 48 +++++++++ .../services/PBSFrontendPhaseServiceTest.java | 62 ++++++++++- 14 files changed, 357 insertions(+), 12 deletions(-) create mode 100644 prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/semantics/PbsIgnoredValueSemanticsTest.java diff --git a/discussion/index.ndjson b/discussion/index.ndjson index bc92b81a..47eb56dc 100644 --- a/discussion/index.ndjson +++ b/discussion/index.ndjson @@ -4,6 +4,6 @@ {"type":"discussion","id":"DSC-0003","status":"done","ticket":"packer-docs-import","title":"Import docs/packer into discussion-framework artifacts","created_at":"2026-03-26","updated_at":"2026-03-26","tags":["packer","migration","discussion-framework","docs-import"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0009","file":"discussion/lessons/DSC-0003-packer-docs-import/LSN-0009-mental-model-packer-legacy.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"},{"id":"LSN-0010","file":"discussion/lessons/DSC-0003-packer-docs-import/LSN-0010-asset-identity-and-runtime-contract-legacy.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"},{"id":"LSN-0011","file":"discussion/lessons/DSC-0003-packer-docs-import/LSN-0011-foundations-workspace-runtime-and-build-legacy.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"},{"id":"LSN-0012","file":"discussion/lessons/DSC-0003-packer-docs-import/LSN-0012-runtime-ownership-and-studio-boundary-legacy.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"},{"id":"LSN-0013","file":"discussion/lessons/DSC-0003-packer-docs-import/LSN-0013-metadata-convergence-and-runtime-sink-legacy.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"},{"id":"LSN-0014","file":"discussion/lessons/DSC-0003-packer-docs-import/LSN-0014-pack-wizard-summary-validation-and-pack-execution-legacy.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"},{"id":"LSN-0015","file":"discussion/lessons/DSC-0003-packer-docs-import/LSN-0015-tile-bank-packing-contract-legacy.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"},{"id":"LSN-0017","file":"discussion/lessons/DSC-0003-packer-docs-import/LSN-0017-packer-docs-import-pattern.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"}]} {"type":"discussion","id":"DSC-0004","status":"open","ticket":"tilemap-and-metatile-runtime-binary-layout","title":"Tilemap and Metatile Runtime Binary Layout","created_at":"2026-03-26","updated_at":"2026-03-26","tags":["packer","legacy-import","tilemap","metatile","runtime-layout"],"agendas":[{"id":"AGD-0004","file":"AGD-0004-tilemap-and-metatile-runtime-binary-layout.md","status":"open","created_at":"2026-03-26","updated_at":"2026-03-26"}],"decisions":[],"plans":[],"lessons":[]} {"type":"discussion","id":"DSC-0005","status":"open","ticket":"variable-tile-bank-palette-serialization","title":"Variable Tile Bank Palette Serialization","created_at":"2026-03-26","updated_at":"2026-03-26","tags":["packer","legacy-import","tile-bank","palette-serialization","versioning"],"agendas":[{"id":"AGD-0005","file":"AGD-0005-variable-tile-bank-palette-serialization.md","status":"open","created_at":"2026-03-26","updated_at":"2026-03-26"}],"decisions":[],"plans":[],"lessons":[]} -{"type":"discussion","id":"DSC-0006","status":"open","ticket":"pbs-game-facing-asset-refs-and-call-result-discard","title":"PBS Game-Facing Asset References and Ignored Call Result Lowering","created_at":"2026-03-27","updated_at":"2026-03-27","tags":["compiler","pbs","ergonomics","lowering","runtime","asset-identity","expression-statements"],"agendas":[{"id":"AGD-0006","file":"AGD-0006-pbs-game-facing-asset-refs-and-call-result-discard.md","status":"accepted","created_at":"2026-03-27","updated_at":"2026-03-27"}],"decisions":[{"id":"DEC-0005","file":"DEC-0005-pbs-asset-address-surface-and-be-lowering.md","status":"accepted","created_at":"2026-03-27","updated_at":"2026-03-27","ref_agenda":"AGD-0006"},{"id":"DEC-0006","file":"DEC-0006-pbs-ignored-values-lowering-and-warning.md","status":"accepted","created_at":"2026-03-27","updated_at":"2026-03-27","ref_agenda":"AGD-0006"}],"plans":[{"id":"PLN-0005","file":"PLN-0005-pbs-asset-address-surface-spec-propagation.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27","ref_decisions":["DEC-0005"]},{"id":"PLN-0006","file":"PLN-0006-pbs-asset-address-surface-implementation.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27","ref_decisions":["DEC-0005"]},{"id":"PLN-0007","file":"PLN-0007-pbs-ignored-values-warning-implementation.md","status":"review","created_at":"2026-03-27","updated_at":"2026-03-27","ref_decisions":["DEC-0006"]}],"lessons":[]} +{"type":"discussion","id":"DSC-0006","status":"open","ticket":"pbs-game-facing-asset-refs-and-call-result-discard","title":"PBS Game-Facing Asset References and Ignored Call Result Lowering","created_at":"2026-03-27","updated_at":"2026-03-27","tags":["compiler","pbs","ergonomics","lowering","runtime","asset-identity","expression-statements"],"agendas":[{"id":"AGD-0006","file":"AGD-0006-pbs-game-facing-asset-refs-and-call-result-discard.md","status":"accepted","created_at":"2026-03-27","updated_at":"2026-03-27"}],"decisions":[{"id":"DEC-0005","file":"DEC-0005-pbs-asset-address-surface-and-be-lowering.md","status":"accepted","created_at":"2026-03-27","updated_at":"2026-03-27","ref_agenda":"AGD-0006"},{"id":"DEC-0006","file":"DEC-0006-pbs-ignored-values-lowering-and-warning.md","status":"accepted","created_at":"2026-03-27","updated_at":"2026-03-27","ref_agenda":"AGD-0006"}],"plans":[{"id":"PLN-0005","file":"PLN-0005-pbs-asset-address-surface-spec-propagation.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27","ref_decisions":["DEC-0005"]},{"id":"PLN-0006","file":"PLN-0006-pbs-asset-address-surface-implementation.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27","ref_decisions":["DEC-0005"]},{"id":"PLN-0007","file":"PLN-0007-pbs-ignored-values-warning-implementation.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27","ref_decisions":["DEC-0006"]}],"lessons":[]} {"type":"discussion","id":"DSC-0007","status":"done","ticket":"pbs-learn-to-discussion-lessons-migration","title":"Migrate PBS Learn Documents into Discussion Lessons","created_at":"2026-03-27","updated_at":"2026-03-27","tags":["compiler","pbs","migration","discussion-framework","lessons","learn-prune"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0018","file":"discussion/lessons/DSC-0007-pbs-learn-to-discussion-lessons-migration/LSN-0018-pbs-ast-and-parser-contract-legacy.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0019","file":"discussion/lessons/DSC-0007-pbs-learn-to-discussion-lessons-migration/LSN-0019-pbs-name-resolution-and-linking-legacy.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0020","file":"discussion/lessons/DSC-0007-pbs-learn-to-discussion-lessons-migration/LSN-0020-pbs-runtime-values-identity-memory-boundaries-legacy.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0021","file":"discussion/lessons/DSC-0007-pbs-learn-to-discussion-lessons-migration/LSN-0021-pbs-diagnostics-and-conformance-governance-legacy.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0022","file":"discussion/lessons/DSC-0007-pbs-learn-to-discussion-lessons-migration/LSN-0022-pbs-globals-lifecycle-and-published-entrypoint-legacy.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"}]} {"type":"discussion","id":"DSC-0008","status":"done","ticket":"pbs-low-level-asset-manager-surface","title":"PBS Low-Level Asset Manager Surface for Runtime AssetManager","created_at":"2026-03-27","updated_at":"2026-03-27","tags":["compiler","pbs","runtime","asset-manager","host-abi","stdlib","asset"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0023","file":"discussion/lessons/DSC-0008-pbs-low-level-asset-manager-surface/LSN-0023-lowassets-runtime-aligned-sdk-surface.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"}]} diff --git a/discussion/workflow/plans/PLN-0007-pbs-ignored-values-warning-implementation.md b/discussion/workflow/plans/PLN-0007-pbs-ignored-values-warning-implementation.md index 12d2aa85..d8735526 100644 --- a/discussion/workflow/plans/PLN-0007-pbs-ignored-values-warning-implementation.md +++ b/discussion/workflow/plans/PLN-0007-pbs-ignored-values-warning-implementation.md @@ -2,9 +2,9 @@ id: PLN-0007 ticket: pbs-game-facing-asset-refs-and-call-result-discard title: Implement DEC-0006 Ignored Values Lowering and Warning -status: review +status: done created: 2026-03-27 -completed: +completed: 2026-03-27 tags: [compiler, pbs, implementation, diagnostics, lowering, expression-statements, warnings] --- diff --git a/docs/specs/compiler-languages/pbs/12. Diagnostics Specification.md b/docs/specs/compiler-languages/pbs/12. Diagnostics Specification.md index 3815be05..2bd67633 100644 --- a/docs/specs/compiler-languages/pbs/12. Diagnostics Specification.md +++ b/docs/specs/compiler-languages/pbs/12. Diagnostics Specification.md @@ -203,6 +203,9 @@ At minimum, the PBS diagnostics baseline must cover: - unresolved terminal asset reference, - namespace-versus-terminal asset misuse, - structurally invalid symbolic asset path admitted by source syntax but rejected by semantic surface rules. +10. ignored-value diagnostics for expression statements, including: + - a stable warning when a materialized value is produced and then ignored, + - and no warning for unit-like expression statements. At minimum, host-admission diagnostics must cover missing or malformed host capability metadata and unknown or undeclared capability names. @@ -229,7 +232,9 @@ When failures exist, implementations must still preserve deterministic diagnosti ## 11. Cost Diagnostics and Warning Policy -Warnings are supported and recommended in v1, but they are not part of the minimum mandatory conformance baseline. +Warnings are supported in v1, and one warning is mandatory for PBS core: ignored materialized values in expression statements. + +The mandatory warning identity for this rule is `W_SEM_IGNORED_VALUE`. Tooling may surface qualitative cost facts as warnings or notes, especially for: diff --git a/docs/specs/compiler-languages/pbs/13. Lowering IRBackend Specification.md b/docs/specs/compiler-languages/pbs/13. Lowering IRBackend Specification.md index dde0224f..aaee5221 100644 --- a/docs/specs/compiler-languages/pbs/13. Lowering IRBackend Specification.md +++ b/docs/specs/compiler-languages/pbs/13. Lowering IRBackend Specification.md @@ -188,6 +188,12 @@ When a host-backed callsite also depends on backend-owned symbolic asset lowerin - the preserved symbolic operand MUST remain attributable to a backend-provided `Addressable` identity; - backend stages MUST be able to rewrite that symbolic operand into runtime-facing `asset_id` before the final low-level host path is emitted. +When an expression statement leaves a materialized runtime value on the stack: + +- `IRBackend` lowering MUST emit explicit discard behavior rather than relying on implicit backend stack cleanup; +- for single-slot materialized values this discard is represented by `POP`; +- and the discard path MUST preserve stack-accounting validity for subsequent backend lowering stages. + ### 12.4 VM-owned metadata obligations VM-owned intrinsic callsites admitted at this boundary must preserve canonical intrinsic identity: diff --git a/docs/specs/compiler-languages/pbs/4. Static Semantics Specification.md b/docs/specs/compiler-languages/pbs/4. Static Semantics Specification.md index e3250f8f..6999550b 100644 --- a/docs/specs/compiler-languages/pbs/4. Static Semantics Specification.md +++ b/docs/specs/compiler-languages/pbs/4. Static Semantics Specification.md @@ -238,6 +238,9 @@ Rules: - Symbolic asset references admitted by the frontend MUST resolve against the backend-provided `Addressable` surface rather than by direct packer queries. - A valid symbolic asset reference MUST correspond to a terminal `Addressable` entry in the backend-provided surface. - Intermediate `assets...` namespace nodes MUST NOT type-check as `Addressable` values. +- An expression statement MAY evaluate an expression that has a materialized value type. +- When an expression statement ignores a materialized value, PBS v1 MUST emit the ignored-value warning defined by the diagnostics specification. +- Unit-like expression statements MUST NOT emit the ignored-value warning. ### 3.4 Constant expressions diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/lowering/PbsExecutableBodyLowerer.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/lowering/PbsExecutableBodyLowerer.java index 7fbb9aa6..d6ae2c03 100644 --- a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/lowering/PbsExecutableBodyLowerer.java +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/lowering/PbsExecutableBodyLowerer.java @@ -143,6 +143,7 @@ final class PbsExecutableBodyLowerer { case PbsAst.WhileStatement whileStatement -> lowerWhileStatement(whileStatement, context); case PbsAst.ExpressionStatement expressionStatement -> { lowerExpression(expressionStatement.expression(), context); + discardIgnoredValueSlots(expressionStatement.expression(), context); yield false; } case PbsAst.BreakStatement breakStatement -> lowerBreakStatement(breakStatement, context); @@ -426,6 +427,21 @@ final class PbsExecutableBodyLowerer { } } + private void discardIgnoredValueSlots( + final PbsAst.Expression expression, + final PbsExecutableLoweringContext context) { + final var slots = materializedValueSlots(expression, context); + for (int i = 0; i < slots; i++) { + context.instructions().add(new IRBackendExecutableFunction.Instruction( + IRBackendExecutableFunction.InstructionKind.POP, + "", + null, + null, + null, + expression == null ? p.studio.compiler.source.Span.none() : expression.span())); + } + } + private void lowerMemberExpression( final PbsAst.MemberExpr memberExpr, final PbsExecutableLoweringContext context) { @@ -508,6 +524,92 @@ final class PbsExecutableBodyLowerer { return receiverPath + "." + memberExpr.memberName(); } + private int materializedValueSlots( + final PbsAst.Expression expression, + final PbsExecutableLoweringContext context) { + if (expression == null) { + return 0; + } + return switch (expression) { + case PbsAst.CallExpr callExpr -> callsiteEmitter.returnSlotsOf(callExpr, context); + case PbsAst.ApplyExpr ignored -> 0; + case PbsAst.BinaryExpr ignored -> 1; + case PbsAst.UnaryExpr ignored -> 1; + case PbsAst.ElseExpr elseExpr -> + Math.max( + materializedValueSlots(elseExpr.optionalExpression(), context), + materializedValueSlots(elseExpr.fallbackExpression(), context)); + case PbsAst.IfExpr ifExpr -> + Math.max( + blockValueSlots(ifExpr.thenBlock(), context), + materializedValueSlots(ifExpr.elseExpression(), context)); + case PbsAst.SwitchExpr ignored -> 0; + case PbsAst.HandleExpr ignored -> 0; + case PbsAst.AsExpr asExpr -> materializedValueSlots(asExpr.expression(), context); + case PbsAst.MemberExpr memberExpr -> { + final var assetReference = flattenAssetReference(memberExpr); + if (assetReference != null && context.resolveAssetId(assetReference) != null) { + yield 1; + } + yield materializedValueSlots(memberExpr.receiver(), context); + } + case PbsAst.PropagateExpr propagateExpr -> materializedValueSlots(propagateExpr.expression(), context); + case PbsAst.GroupExpr groupExpr -> materializedValueSlots(groupExpr.expression(), context); + case PbsAst.NewExpr newExpr -> expressionListValueSlots(newExpr.arguments(), context); + case PbsAst.BindExpr bindExpr -> materializedValueSlots(bindExpr.contextExpression(), context); + case PbsAst.SomeExpr someExpr -> materializedValueSlots(someExpr.value(), context); + case PbsAst.OkExpr okExpr -> materializedValueSlots(okExpr.value(), context); + case PbsAst.TupleExpr tupleExpr -> { + var slots = 0; + for (final var item : tupleExpr.items()) { + slots += materializedValueSlots(item.expression(), context); + } + yield slots; + } + case PbsAst.BlockExpr blockExpr -> blockValueSlots(blockExpr.block(), context); + case PbsAst.IdentifierExpr identifierExpr -> producesIdentifierValue(identifierExpr, context) ? 1 : 0; + case PbsAst.IntLiteralExpr ignored -> 1; + case PbsAst.StringLiteralExpr ignoredString -> 1; + case PbsAst.BoolLiteralExpr ignoredBool -> 1; + case PbsAst.FloatLiteralExpr ignoredFloat -> 0; + case PbsAst.BoundedLiteralExpr ignoredBounded -> 0; + case PbsAst.ThisExpr ignoredThis -> 0; + case PbsAst.NoneExpr ignoredNone -> 0; + case PbsAst.ErrExpr ignoredErr -> 0; + case PbsAst.UnitExpr ignoredUnit -> 0; + }; + } + + private int blockValueSlots( + final PbsAst.Block block, + final PbsExecutableLoweringContext context) { + if (block == null || block.tailExpression() == null) { + return 0; + } + return materializedValueSlots(block.tailExpression(), context); + } + + private int expressionListValueSlots( + final ReadOnlyList expressions, + final PbsExecutableLoweringContext context) { + var slots = 0; + for (final var expression : expressions) { + slots += materializedValueSlots(expression, context); + } + return slots; + } + + private boolean producesIdentifierValue( + final PbsAst.IdentifierExpr identifierExpr, + final PbsExecutableLoweringContext context) { + if ("assets".equals(identifierExpr.name())) { + return false; + } + return context.resolveLocalSlot(identifierExpr.name()) != null + || context.resolveGlobalSlot(identifierExpr.name()) != null + || context.resolveConstDecl(identifierExpr.name()) != null; + } + private void lowerConstExpression( final PbsAst.Expression expression, final PbsExecutableLoweringContext context) { diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/lowering/PbsExecutableCallsiteEmitter.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/lowering/PbsExecutableCallsiteEmitter.java index f2882cc7..9ac7d2fc 100644 --- a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/lowering/PbsExecutableCallsiteEmitter.java +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/lowering/PbsExecutableCallsiteEmitter.java @@ -16,6 +16,29 @@ import java.util.Map; import static p.studio.compiler.pbs.lowering.PbsExecutableLoweringModels.*; final class PbsExecutableCallsiteEmitter { + int returnSlotsOf( + final PbsAst.CallExpr callExpr, + final PbsExecutableLoweringContext context) { + final var calleeIdentity = resolveCalleeIdentity(callExpr.callee(), context.nameTable()); + if (calleeIdentity == null) { + return 0; + } + final var candidates = resolveCallsiteCandidates(callExpr, calleeIdentity, context); + final var resolvedCategory = resolveCallsiteCategory(candidates, callExpr, context); + if (resolvedCategory == null) { + return 0; + } + return switch (resolvedCategory) { + case HOST -> candidates.hostCandidates().getFirst().retSlots(); + case INTRINSIC -> candidates.intrinsicCandidates().getFirst().retSlots(); + case CALLABLE -> { + if (candidates.callableCandidates().size() != 1) { + yield 0; + } + yield context.returnSlotsByCallableId().getOrDefault(candidates.callableCandidates().getFirst(), 0); + } + }; + } void emitCallsite( final PbsAst.CallExpr callExpr, diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsFlowStatementAnalyzer.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsFlowStatementAnalyzer.java index 45f0774a..8b88693d 100644 --- a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsFlowStatementAnalyzer.java +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsFlowStatementAnalyzer.java @@ -172,7 +172,7 @@ final class PbsFlowStatementAnalyzer { return; } if (statement instanceof PbsAst.ExpressionStatement expressionStatement) { - expressionAnalyzer.analyzeExpression( + final var expressionType = expressionAnalyzer.analyzeExpression( expressionStatement.expression(), scope, null, @@ -183,7 +183,13 @@ final class PbsFlowStatementAnalyzer { diagnostics, ExprUse.VALUE, false, - this::analyzeNestedBlock); + this::analyzeNestedBlock).type(); + if (typeOps.isMaterializedValue(expressionType)) { + Diagnostics.warning(diagnostics, + PbsSemanticsErrors.W_SEM_IGNORED_VALUE.name(), + "Ignored materialized value in expression statement", + expressionStatement.span()); + } return; } if (statement instanceof PbsAst.AssignStatement assignStatement) { diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsFlowTypeOps.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsFlowTypeOps.java index 40619f60..394cfae4 100644 --- a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsFlowTypeOps.java +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsFlowTypeOps.java @@ -139,6 +139,16 @@ final class PbsFlowTypeOps { return type.kind() == Kind.STR || type.kind() == Kind.UNKNOWN; } + boolean isMaterializedValue(final TypeView type) { + if (type == null) { + return false; + } + return switch (type.kind()) { + case UNKNOWN, UNIT, ASSET_NAMESPACE -> false; + default -> true; + }; + } + private boolean tupleCompatible(final TypeView actual, final TypeView expected) { if (actual.tupleFields().size() != expected.tupleFields().size()) { return false; diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsSemanticsErrors.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsSemanticsErrors.java index 8393c07c..67655139 100644 --- a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsSemanticsErrors.java +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsSemanticsErrors.java @@ -69,4 +69,5 @@ public enum PbsSemanticsErrors { E_SEM_HANDLE_ERROR_MISMATCH, E_SEM_HANDLE_ARM_TERMINAL_INVALID, E_SEM_EXEC_LOWERING_UNRESOLVED_CALLEE, + W_SEM_IGNORED_VALUE, } diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/PbsDiagnosticsContractTest.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/PbsDiagnosticsContractTest.java index a3f645af..d7c8629f 100644 --- a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/PbsDiagnosticsContractTest.java +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/PbsDiagnosticsContractTest.java @@ -10,6 +10,7 @@ import p.studio.compiler.pbs.parser.PbsParser; import p.studio.compiler.pbs.semantics.PbsSemanticsErrors; import p.studio.compiler.source.diagnostics.DiagnosticPhase; import p.studio.compiler.source.diagnostics.DiagnosticSink; +import p.studio.compiler.source.diagnostics.Severity; import p.studio.compiler.source.identifiers.FileId; import p.studio.utilities.structures.ReadOnlyList; @@ -52,6 +53,28 @@ class PbsDiagnosticsContractTest { assertEquals(DiagnosticPhase.STATIC_SEMANTICS, diagnostic.getPhase()); } + @Test + void shouldTagIgnoredValueWarningsWithStaticSemanticsPhase() { + final var source = """ + fn pure() -> int { return 1; } + fn frame() -> void { + pure(); + return; + } + """; + final var diagnostics = DiagnosticSink.empty(); + + new PbsFrontendCompiler().compileFile(new FileId(11), source, diagnostics); + + final var diagnostic = diagnostics.stream() + .filter(d -> d.getCode().equals(PbsSemanticsErrors.W_SEM_IGNORED_VALUE.name())) + .findFirst() + .orElseThrow(); + assertEquals(DiagnosticPhase.STATIC_SEMANTICS, diagnostic.getPhase()); + assertEquals(Severity.Warning, diagnostic.getSeverity()); + assertEquals(diagnostic.getCode(), diagnostic.getTemplateId()); + } + @Test void shouldTagLinkingDiagnosticsAndExposeRelatedSpanForDuplicateBarrelEntry() { final var diagnostics = DiagnosticSink.empty(); diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/PbsFrontendCompilerTest.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/PbsFrontendCompilerTest.java index 7a22f6fe..8b045f4c 100644 --- a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/PbsFrontendCompilerTest.java +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/PbsFrontendCompilerTest.java @@ -288,13 +288,17 @@ class PbsFrontendCompilerTest { final var compiler = new PbsFrontendCompiler(); final var fileBackend = compiler.compileFile(new FileId(99), source, diagnostics); - assertTrue(diagnostics.isEmpty(), "Valid program should not report diagnostics"); + assertEquals(0, diagnostics.errorCount(), "Valid program should not report errors"); + assertEquals(2, diagnostics.warningCount()); final var executableA = fileBackend.executableFunctions().stream() .filter(fn -> fn.callableName().equals("a")) .findFirst() .orElseThrow(); - assertEquals("b", executableA.instructions().get(0).calleeCallableName()); - assertEquals("c", executableA.instructions().get(1).calleeCallableName()); + final var callNames = executableA.instructions().stream() + .filter(i -> i.kind() == p.studio.compiler.models.IRBackendExecutableFunction.InstructionKind.CALL_FUNC) + .map(p.studio.compiler.models.IRBackendExecutableFunction.Instruction::calleeCallableName) + .toList(); + assertEquals(java.util.List.of("b", "c"), callNames); } @Test @@ -312,7 +316,8 @@ class PbsFrontendCompilerTest { final var compiler = new PbsFrontendCompiler(); final var fileBackend = compiler.compileFile(new FileId(100), source, diagnostics); - assertTrue(diagnostics.isEmpty(), "Valid program should not report diagnostics"); + assertEquals(0, diagnostics.errorCount(), "Valid program should not report errors"); + assertEquals(1, diagnostics.warningCount()); final var executableMain = fileBackend.executableFunctions().stream() .filter(fn -> fn.callableName().equals("main")) .findFirst() @@ -512,6 +517,61 @@ class PbsFrontendCompilerTest { assertEquals(0, fileBackend.executableFunctions().size()); } + @Test + void shouldWarnAndDiscardIgnoredMaterializedExpressionStatements() { + final var source = """ + fn pure() -> int { return 1; } + + fn frame() -> void { + pure(); + 1 + 2; + return; + } + """; + + final var diagnostics = DiagnosticSink.empty(); + final var compiler = new PbsFrontendCompiler(); + final var fileBackend = compiler.compileFile(new FileId(14), source, diagnostics); + + assertEquals(2, diagnostics.warningCount()); + assertTrue(diagnostics.stream().anyMatch(d -> + d.getCode().equals(PbsSemanticsErrors.W_SEM_IGNORED_VALUE.name()))); + final var executableFrame = fileBackend.executableFunctions().stream() + .filter(fn -> fn.callableName().equals("frame")) + .findFirst() + .orElseThrow(); + assertTrue(executableFrame.instructions().stream().anyMatch(i -> + i.kind() == p.studio.compiler.models.IRBackendExecutableFunction.InstructionKind.CALL_FUNC)); + assertEquals(2, executableFrame.instructions().stream() + .filter(i -> i.kind() == p.studio.compiler.models.IRBackendExecutableFunction.InstructionKind.POP) + .count()); + } + + @Test + void shouldNotWarnOrDiscardUnitExpressionStatements() { + final var source = """ + fn log() -> void { return; } + + fn frame() -> void { + log(); + return; + } + """; + + final var diagnostics = DiagnosticSink.empty(); + final var compiler = new PbsFrontendCompiler(); + final var fileBackend = compiler.compileFile(new FileId(15), source, diagnostics); + + assertEquals(0, diagnostics.warningCount()); + final var executableFrame = fileBackend.executableFunctions().stream() + .filter(fn -> fn.callableName().equals("frame")) + .findFirst() + .orElseThrow(); + assertEquals(0, executableFrame.instructions().stream() + .filter(i -> i.kind() == p.studio.compiler.models.IRBackendExecutableFunction.InstructionKind.POP) + .count()); + } + @Test void shouldRejectHostBindingWithoutCapabilityAtHostAdmission() { final var source = """ diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/semantics/PbsIgnoredValueSemanticsTest.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/semantics/PbsIgnoredValueSemanticsTest.java new file mode 100644 index 00000000..86b73117 --- /dev/null +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/semantics/PbsIgnoredValueSemanticsTest.java @@ -0,0 +1,48 @@ +package p.studio.compiler.pbs.semantics; + +import org.junit.jupiter.api.Test; +import p.studio.compiler.pbs.PbsFrontendCompiler; +import p.studio.compiler.source.diagnostics.DiagnosticSink; +import p.studio.compiler.source.identifiers.FileId; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class PbsIgnoredValueSemanticsTest { + + @Test + void shouldWarnWhenExpressionStatementIgnoresMaterializedValue() { + final var source = """ + fn pure() -> int { return 1; } + + fn frame() -> void { + pure(); + return; + } + """; + final var diagnostics = DiagnosticSink.empty(); + + new PbsFrontendCompiler().compileFile(new FileId(0), source, diagnostics); + + assertEquals(1, diagnostics.warningCount()); + assertTrue(diagnostics.stream().anyMatch(d -> + d.getCode().equals(PbsSemanticsErrors.W_SEM_IGNORED_VALUE.name()))); + } + + @Test + void shouldNotWarnWhenExpressionStatementIsUnitLike() { + final var source = """ + fn log() -> void { return; } + + fn frame() -> void { + log(); + return; + } + """; + final var diagnostics = DiagnosticSink.empty(); + + new PbsFrontendCompiler().compileFile(new FileId(1), source, diagnostics); + + assertEquals(0, diagnostics.warningCount()); + } +} diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/services/PBSFrontendPhaseServiceTest.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/services/PBSFrontendPhaseServiceTest.java index ba426dfd..3edacf72 100644 --- a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/services/PBSFrontendPhaseServiceTest.java +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/services/PBSFrontendPhaseServiceTest.java @@ -87,7 +87,8 @@ class PBSFrontendPhaseServiceTest { LogAggregator.empty(), BuildingIssueSink.empty()); - assertTrue(diagnostics.isEmpty(), diagnostics.stream().map(d -> d.getCode() + ":" + d.getMessage()).toList().toString()); + assertEquals(0, diagnostics.errorCount(), diagnostics.stream().map(d -> d.getCode() + ":" + d.getMessage()).toList().toString()); + assertEquals(0, diagnostics.warningCount()); final var frameFn = irBackend.getExecutableFunctions().stream() .filter(function -> "frame".equals(function.callableName())) .findFirst() @@ -97,6 +98,62 @@ class PBSFrontendPhaseServiceTest { && "91".equals(instruction.label()))); } + @Test + void shouldKeepWarningsNonBlockingWhileLoweringExplicitDiscard() throws IOException { + final var projectRoot = tempDir.resolve("project-ignored-values"); + final var sourceRoot = projectRoot.resolve("src"); + final var modulePath = sourceRoot.resolve("main"); + Files.createDirectories(modulePath); + + final var sourceFile = modulePath.resolve("source.pbs"); + final var modBarrel = modulePath.resolve("mod.barrel"); + Files.writeString(sourceFile, """ + fn pure() -> int { return 1; } + + [Frame] + fn frame() -> void { + pure(); + return; + } + """); + Files.writeString(modBarrel, "pub fn frame() -> void;"); + + final var projectTable = new ProjectTable(); + final var fileTable = new FileTable(1); + final var projectId = projectTable.register(ProjectDescriptor.builder() + .rootPath(projectRoot) + .name("core") + .version("1.0.0") + .sourceRoots(ReadOnlyList.wrap(List.of(sourceRoot))) + .build()); + + registerFile(projectId, projectRoot, sourceFile, fileTable); + registerFile(projectId, projectRoot, modBarrel, fileTable); + + final var ctx = new FrontendPhaseContext( + projectTable, + fileTable, + new BuildStack(ReadOnlyList.wrap(List.of(projectId)))); + final var diagnostics = DiagnosticSink.empty(); + + final var irBackend = new PBSFrontendPhaseService().compile( + ctx, + diagnostics, + LogAggregator.empty(), + BuildingIssueSink.empty()); + + assertEquals(1, diagnostics.warningCount()); + assertEquals(0, diagnostics.errorCount()); + assertTrue(diagnostics.stream().anyMatch(d -> + d.getCode().equals(PbsSemanticsErrors.W_SEM_IGNORED_VALUE.name()))); + final var frameFn = irBackend.getExecutableFunctions().stream() + .filter(function -> "frame".equals(function.callableName())) + .findFirst() + .orElseThrow(); + assertTrue(frameFn.instructions().stream().anyMatch(instruction -> + instruction.kind() == p.studio.compiler.models.IRBackendExecutableFunction.InstructionKind.POP)); + } + @Test void shouldReportInvalidBarrelFilename() throws IOException { final var projectRoot = tempDir.resolve("project"); @@ -259,7 +316,8 @@ class PBSFrontendPhaseServiceTest { LogAggregator.empty(), BuildingIssueSink.empty()); - assertTrue(diagnostics.isEmpty(), diagnostics.stream().map(d -> d.getCode() + ":" + d.getMessage()).toList().toString()); + assertEquals(0, diagnostics.errorCount(), diagnostics.stream().map(d -> d.getCode() + ":" + d.getMessage()).toList().toString()); + assertEquals(1, diagnostics.warningCount()); final var targetFn = irBackend.getExecutableFunctions().stream() .filter(function -> "target".equals(function.callableName()))