From 2d0219f30884bb56085a1481f8e798933d015311 Mon Sep 17 00:00:00 2001 From: Eliot Moss Date: Tue, 31 Mar 2026 22:32:24 -0400 Subject: [PATCH 01/58] Add open enums and enum methods with tests --- aeneas/src/core/Eval.v3 | 44 +++ aeneas/src/core/Operator.v3 | 6 + aeneas/src/ir/Ir.v3 | 4 +- aeneas/src/ir/Normalization.v3 | 39 +++ aeneas/src/ir/Reachability.v3 | 24 ++ aeneas/src/ir/SsaNormalizer.v3 | 46 +++ aeneas/src/ir/VstIr.v3 | 90 +++++- aeneas/src/jvm/JvmGen.v3 | 34 +- aeneas/src/jvm/JvmRep.v3 | 6 + aeneas/src/jvm/SsaJvmGen.v3 | 7 +- aeneas/src/ssa/SsaBuilder.v3 | 10 +- aeneas/src/ssa/SsaOptimizer.v3 | 31 +- aeneas/src/ssa/VstSsaGen.v3 | 112 +++++-- aeneas/src/v3/TypeSystem.v3 | 28 +- aeneas/src/v3/V3Enum.v3 | 47 ++- aeneas/src/vst/MethodEnv.v3 | 139 +++++++- aeneas/src/vst/Parser.v3 | 116 ++++++- aeneas/src/vst/Verifier.v3 | 470 +++++++++++++++++++++++++++- aeneas/src/vst/Vst.v3 | 20 +- aeneas/src/wasm/WasmCodeGen.v3 | 21 +- aeneas/src/wasm/WasmGcTarget.v3 | 20 +- aeneas/src/wasm/WasmTarget.v3 | 5 +- doc/grammar-claude.md | 75 ++++- doc/tutorial/EnumMethods.md | 73 +++++ doc/tutorial/EnumSets.md | 17 +- doc/tutorial/Enums.md | 137 ++++++++ doc/virgil-grammar.ebnf | 5 +- test/enums/enum_closure00.v3 | 8 + test/enums/enum_closure01.v3 | 8 + test/enums/enum_cmethod00.v3 | 11 + test/enums/enum_cmethod01.v3 | 14 + test/enums/enum_cmethod02.v3 | 18 ++ test/enums/enum_cmethod03.v3 | 14 + test/enums/enum_cmethod04.v3 | 18 ++ test/enums/enum_cmethod05.v3 | 20 ++ test/enums/enum_cmethod06.v3 | 13 + test/enums/enum_cmethod07.v3 | 13 + test/enums/enum_cmethod08.v3 | 13 + test/enums/enum_cmethod09.v3 | 14 + test/enums/enum_method00.v3 | 11 + test/enums/enum_method01.v3 | 11 + test/enums/enum_method02.v3 | 11 + test/enums/enum_method03.v3 | 11 + test/enums/enum_method04.v3 | 12 + test/enums/enum_method05.v3 | 13 + test/enums/enum_method06.v3 | 11 + test/enums/open_enum00.v3 | 14 + test/enums/open_enum01.v3 | 17 + test/enums/open_enum02.v3 | 10 + test/enums/open_enum03.v3 | 11 + test/enums/open_enum04.v3 | 11 + test/enums/open_enum05.v3 | 9 + test/enums/open_enum06.v3 | 10 + test/enums/open_enum07.v3 | 16 + test/enums/open_enum08.v3 | 11 + test/enums/open_enum09.v3 | 12 + test/enums/open_enum10.v3 | 19 ++ test/enums/open_field00.v3 | 9 + test/enums/open_field01.v3 | 9 + test/enums/open_field02.v3 | 9 + test/enums/open_field03.v3 | 15 + test/enums/open_field04.v3 | 11 + test/enums/open_field05.v3 | 9 + test/enums/open_field06.v3 | 13 + test/enums/open_field07.v3 | 13 + test/enums/open_field08.v3 | 13 + test/enums/open_field09.v3 | 14 + test/enums/open_field10.v3 | 12 + test/enums/open_field11.v3 | 13 + test/enums/open_field12.v3 | 12 + test/enums/open_field13.v3 | 9 + test/enums/open_field14.v3 | 15 + test/enums/open_field15.v3 | 15 + test/enums/open_set00.v3 | 14 + test/enums/open_set01.v3 | 22 ++ test/enums/open_set02.v3 | 12 + test/enums/open_set03.v3 | 25 ++ test/enums/open_set04.v3 | 14 + test/enums/parser/enum_cmethod00.v3 | 3 + test/enums/parser/enum_cmethod01.v3 | 9 + test/enums/parser/enum_cmethod02.v3 | 3 + test/enums/parser/enum_cmethod03.v3 | 3 + test/enums/parser/enum_method00.v3 | 3 + test/enums/parser/enum_method01.v3 | 7 + test/enums/parser/enum_method02.v3 | 3 + test/enums/parser/enum_method03.v3 | 4 + test/enums/parser/open00.v3 | 3 + test/enums/parser/open01.v3 | 4 + test/enums/parser/open02.v3 | 4 + test/enums/parser/open03.v3 | 3 + test/enums/seman/enum_cmethod00.v3 | 7 + test/enums/seman/enum_cmethod01.v3 | 3 + test/enums/seman/enum_cmethod02.v3 | 3 + test/enums/seman/enum_cmethod03.v3 | 7 + test/enums/seman/enum_method00.v3 | 7 + test/enums/seman/enum_method01.v3 | 10 + test/enums/seman/enum_method02.v3 | 3 + test/enums/seman/enum_method03.v3 | 9 + test/enums/seman/open00.v3 | 12 + test/enums/seman/open01.v3 | 11 + test/enums/seman/open02.v3 | 6 + test/enums/seman/open03.v3 | 14 + test/enums/seman/open04.v3 | 4 + test/enums/seman/open05.v3 | 9 + test/enums/seman/open06.v3 | 9 + test/enums/seman/open07.v3 | 8 + test/enums/seman/open08.v3 | 5 + test/enums/seman/open09.v3 | 4 + test/enums/seman/open10.v3 | 12 + test/enums/seman/open_field00.v3 | 9 + test/enums/seman/open_field01.v3 | 4 + test/enums/seman/open_field02.v3 | 4 + test/enums/seman/open_field03.v3 | 4 + test/enums/seman/open_field04.v3 | 4 + test/enums/seman/open_field05.v3 | 4 + test/enums/seman/open_field06.v3 | 4 + test/enums/seman/open_field07.v3 | 4 + test/enums/seman/open_field08.v3 | 4 + test/enums/seman/open_field09.v3 | 4 + test/enums/seman/open_field10.v3 | 5 + test/enums/seman/open_field11.v3 | 4 + test/enums/seman/open_field12.v3 | 5 + 122 files changed, 2428 insertions(+), 111 deletions(-) create mode 100644 doc/tutorial/EnumMethods.md create mode 100644 test/enums/enum_closure00.v3 create mode 100644 test/enums/enum_closure01.v3 create mode 100644 test/enums/enum_cmethod00.v3 create mode 100644 test/enums/enum_cmethod01.v3 create mode 100644 test/enums/enum_cmethod02.v3 create mode 100644 test/enums/enum_cmethod03.v3 create mode 100644 test/enums/enum_cmethod04.v3 create mode 100644 test/enums/enum_cmethod05.v3 create mode 100644 test/enums/enum_cmethod06.v3 create mode 100644 test/enums/enum_cmethod07.v3 create mode 100644 test/enums/enum_cmethod08.v3 create mode 100644 test/enums/enum_cmethod09.v3 create mode 100644 test/enums/enum_method00.v3 create mode 100644 test/enums/enum_method01.v3 create mode 100644 test/enums/enum_method02.v3 create mode 100644 test/enums/enum_method03.v3 create mode 100644 test/enums/enum_method04.v3 create mode 100644 test/enums/enum_method05.v3 create mode 100644 test/enums/enum_method06.v3 create mode 100644 test/enums/open_enum00.v3 create mode 100644 test/enums/open_enum01.v3 create mode 100644 test/enums/open_enum02.v3 create mode 100644 test/enums/open_enum03.v3 create mode 100644 test/enums/open_enum04.v3 create mode 100644 test/enums/open_enum05.v3 create mode 100644 test/enums/open_enum06.v3 create mode 100644 test/enums/open_enum07.v3 create mode 100644 test/enums/open_enum08.v3 create mode 100644 test/enums/open_enum09.v3 create mode 100644 test/enums/open_enum10.v3 create mode 100644 test/enums/open_field00.v3 create mode 100644 test/enums/open_field01.v3 create mode 100644 test/enums/open_field02.v3 create mode 100644 test/enums/open_field03.v3 create mode 100644 test/enums/open_field04.v3 create mode 100644 test/enums/open_field05.v3 create mode 100644 test/enums/open_field06.v3 create mode 100644 test/enums/open_field07.v3 create mode 100644 test/enums/open_field08.v3 create mode 100644 test/enums/open_field09.v3 create mode 100644 test/enums/open_field10.v3 create mode 100644 test/enums/open_field11.v3 create mode 100644 test/enums/open_field12.v3 create mode 100644 test/enums/open_field13.v3 create mode 100644 test/enums/open_field14.v3 create mode 100644 test/enums/open_field15.v3 create mode 100644 test/enums/open_set00.v3 create mode 100644 test/enums/open_set01.v3 create mode 100644 test/enums/open_set02.v3 create mode 100644 test/enums/open_set03.v3 create mode 100644 test/enums/open_set04.v3 create mode 100644 test/enums/parser/enum_cmethod00.v3 create mode 100644 test/enums/parser/enum_cmethod01.v3 create mode 100644 test/enums/parser/enum_cmethod02.v3 create mode 100644 test/enums/parser/enum_cmethod03.v3 create mode 100644 test/enums/parser/enum_method00.v3 create mode 100644 test/enums/parser/enum_method01.v3 create mode 100644 test/enums/parser/enum_method02.v3 create mode 100644 test/enums/parser/enum_method03.v3 create mode 100644 test/enums/parser/open00.v3 create mode 100644 test/enums/parser/open01.v3 create mode 100644 test/enums/parser/open02.v3 create mode 100644 test/enums/parser/open03.v3 create mode 100644 test/enums/seman/enum_cmethod00.v3 create mode 100644 test/enums/seman/enum_cmethod01.v3 create mode 100644 test/enums/seman/enum_cmethod02.v3 create mode 100644 test/enums/seman/enum_cmethod03.v3 create mode 100644 test/enums/seman/enum_method00.v3 create mode 100644 test/enums/seman/enum_method01.v3 create mode 100644 test/enums/seman/enum_method02.v3 create mode 100644 test/enums/seman/enum_method03.v3 create mode 100644 test/enums/seman/open00.v3 create mode 100644 test/enums/seman/open01.v3 create mode 100644 test/enums/seman/open02.v3 create mode 100644 test/enums/seman/open03.v3 create mode 100644 test/enums/seman/open04.v3 create mode 100644 test/enums/seman/open05.v3 create mode 100644 test/enums/seman/open06.v3 create mode 100644 test/enums/seman/open07.v3 create mode 100644 test/enums/seman/open08.v3 create mode 100644 test/enums/seman/open09.v3 create mode 100644 test/enums/seman/open10.v3 create mode 100644 test/enums/seman/open_field00.v3 create mode 100644 test/enums/seman/open_field01.v3 create mode 100644 test/enums/seman/open_field02.v3 create mode 100644 test/enums/seman/open_field03.v3 create mode 100644 test/enums/seman/open_field04.v3 create mode 100644 test/enums/seman/open_field05.v3 create mode 100644 test/enums/seman/open_field06.v3 create mode 100644 test/enums/seman/open_field07.v3 create mode 100644 test/enums/seman/open_field08.v3 create mode 100644 test/enums/seman/open_field09.v3 create mode 100644 test/enums/seman/open_field10.v3 create mode 100644 test/enums/seman/open_field11.v3 create mode 100644 test/enums/seman/open_field12.v3 diff --git a/aeneas/src/core/Eval.v3 b/aeneas/src/core/Eval.v3 index 34d38fc54..5a968971e 100644 --- a/aeneas/src/core/Eval.v3 +++ b/aeneas/src/core/Eval.v3 @@ -126,6 +126,12 @@ component Eval { return b == null; } CLASS_QUERY, VARIANT_QUERY => { + if (EnumType.?(tt)) { + // enum subtype query: check integer tag is in [enumTagLo, enumTagHi] + var decl = EnumType.!(tt).enumDecl; + var tagVal = Int.unbox(val); + return tagVal >= decl.enumTagLo && tagVal <= decl.enumTagHi; + } if (val == null) return false; if (tt.open()) return false; return Record.?(val) && TypeSystem.isSubtype(Record.!(val).rtype, tt); @@ -810,12 +816,22 @@ def evalOp(op: Operator, args: Arguments) -> Result { return if(object != null, object.values[field.index]); } VariantGetMethod(method) => { + var ta = args.getTypeArgs(); + if (EnumType.?(ta[0])) { + var spec = lookupEnumVirtual(args, method); + return Closure.new(args.vals[0], spec); + } var object = args.r(0); var spec = args.getClosedIrSpec(method); if (spec == null) return args.notFoldable(null); return Closure.new(object, spec); } VariantGetVirtual(method) => { + var ta = args.getTypeArgs(); + if (EnumType.?(ta[0])) { + var spec = lookupEnumVirtual(args, method); + return Closure.new(args.vals[0], spec); + } var object = getRecordReceiver(args); var spec = args.getClosedIrSpec(method); if (spec == null) return args.notFoldable(null); @@ -823,6 +839,11 @@ def evalOp(op: Operator, args: Arguments) -> Result { return Closure.new(object, spec); } VariantGetSelector(selector) => { + var ta = args.getTypeArgs(); + if (EnumType.?(ta[0])) { + var spec = lookupEnumVirtual(args, selector); + return FuncVal.new(spec); + } var object = getRecordReceiver(args); var spec = args.getClosedIrSpec(selector); if (spec == null) return args.notFoldable(null); @@ -914,6 +935,11 @@ def evalOp(op: Operator, args: Arguments) -> Result { return args.tailCall(spec, object, 1, args.vals.length); } CallVariantVirtual(method) => { + var ta = args.getTypeArgs(); + if (EnumType.?(ta[0])) { + var spec = lookupEnumVirtual(args, method); + return args.tailCall(spec, args.vals[0], 1, args.vals.length); + } var object = getRecordReceiver(args); var spec = args.getClosedIrSpec(method); if (spec == null) return args.notFoldable(null); @@ -921,6 +947,11 @@ def evalOp(op: Operator, args: Arguments) -> Result { return args.tailCall(spec, object, 1, args.vals.length); } CallVariantSelector(selector) => { + var ta = args.getTypeArgs(); + if (EnumType.?(ta[0])) { + var spec = lookupEnumVirtual(args, selector); + return args.tailCall(spec, args.vals[0], 1, args.vals.length); + } var object = getRecordReceiver(args); var spec = args.getClosedIrSpec(selector); if (spec == null) return args.notFoldable(null); @@ -1147,6 +1178,19 @@ def lookupClassVirtual(args: Arguments, object: Record, spec: IrSpec) -> IrSpec args.throw(V3Exception.NullCheck, null); return null; } +def lookupEnumVirtual(args: Arguments, member: IrMember) -> IrSpec { + var ta = args.getTypeArgs(); + var baseSpec = IrSpec.new(ta[0], ta, member); + var tag = Int.unbox(args.vals[0]); + if (IrMethod.?(member)) { + var rootVst = IrMethod.!(member).source; + if (rootVst != null && rootVst.enumCaseIrs != null && tag < rootVst.enumCaseIrs.length) { + var overrideIr = rootVst.enumCaseIrs[tag]; + if (overrideIr != null) return IrSpec.new(ta[0], ta, overrideIr); + } + } + return baseSpec; +} def lookupVariantVirtual(args: Arguments, object: Record, spec: IrSpec) -> IrSpec { if (object != null) return args.getProgram().ir.resolveMethodImpl(object.rtype, spec); return args.getProgram().ir.resolveVariantDefaultMethodImpl(spec); diff --git a/aeneas/src/core/Operator.v3 b/aeneas/src/core/Operator.v3 index 31050427b..9d24076a8 100644 --- a/aeneas/src/core/Operator.v3 +++ b/aeneas/src/core/Operator.v3 @@ -414,6 +414,12 @@ component V3Op { var paramTypes = Arrays.prepend(ftype, Function.getParamTypeArray(ftype)); return newOp0(Opcode.CallFunction, [ftype], paramTypes, Function.getReturnType(ftype)); } + def newCallFunctionDirect(ftype: Type) -> Operator { + ftype = Function.funcRefType(ftype); + if (ftype.typeCon.kind != Kind.FUNCREF) return V3.fail("only function types allowed"); + var paramTypes = Arrays.prepend(ftype, Function.getParamTypeArray(ftype)); + return newOp0(Opcode.CallFunction, [ftype], paramTypes, Function.getReturnType(ftype)); + } def newCreateClosure(methodRef: IrSpec, closure: Type) -> Operator { var typeArgs = methodRef.typeArgs; return newOp0(Opcode.CreateClosure(methodRef.asMethod()), typeArgs, [closure], methodRef.getBoundType()); diff --git a/aeneas/src/ir/Ir.v3 b/aeneas/src/ir/Ir.v3 index 196bc81af..9118d254e 100644 --- a/aeneas/src/ir/Ir.v3 +++ b/aeneas/src/ir/Ir.v3 @@ -415,7 +415,9 @@ class IrModule { return ic; } def newIrClass(ctype: Type, superClass: IrClass, decl: VstCompound) -> IrClass { - var ic = IrBuilder.new(ctype, superClass).buildClass(decl); + var builder = IrBuilder.new(ctype, superClass); + builder.irModule = this; + var ic = builder.buildClass(decl); classes.put(ic); return ic; } diff --git a/aeneas/src/ir/Normalization.v3 b/aeneas/src/ir/Normalization.v3 index 307223fc4..ef1ff273c 100644 --- a/aeneas/src/ir/Normalization.v3 +++ b/aeneas/src/ir/Normalization.v3 @@ -158,6 +158,10 @@ class ReachabilityNormalizer(config: NormalizerConfig, ra: ReachabilityAnalyzer) if (rc.isUnboxed()) { // move flattened data type receiver to function sig ftype = Function.prependParamTypes(rc.variantNorm.sub, ftype); + } else if (EnumType.?(rc.oldType)) { + // enum: prepend tag type so dispatch table CallFunction matches CallMethod + var tagType = V3.getVariantTagType(rc.oldType); + ftype = Function.prependParamType(tagType, ftype); } rm.funcNorm = FuncNorm.!(norm(ftype)); var typeParams = if(rm.spec != null, rm.spec.getTypes().methodTypeArgs); @@ -588,11 +592,15 @@ class ReachabilityNormalizer(config: NormalizerConfig, ra: ReachabilityAnalyzer) rm.norm.flags |= IrFlag.M_OVERRIDE; sm.norm.flags |= IrFlag.M_OVERRIDDEN; } + // For enum methods, M_OVERRIDDEN must be transferred from original + // because enum case overrides don't share vtable slots. + if (m.flags.M_OVERRIDDEN) rm.norm.flags |= IrFlag.M_OVERRIDDEN; if (rm.virtual != null) virtuals = List.new(rm.virtual, virtuals); } def layoutMtable(rv: RaVirtual) { if (rv.mtable != null) return; var rm = rv.raMethod, rc = ra.getClass(rm.receiver); + if (EnumType.?(rc.oldType)) return layoutEnumMtable(rv, rm, rc); var size = rc.maxClassId - rc.minClassId; if (ra.compiler.RaDevirtualize && size == 1) return; // no need for an mtable var table = Array.new(size), mtable = IrMtable.new(rm.norm, rc.minClassId, table); @@ -630,6 +638,37 @@ class ReachabilityNormalizer(config: NormalizerConfig, ra: ReachabilityAnalyzer) setMtable(l.head, rv); } } + def layoutEnumMtable(rv: RaVirtual, rm: RaMethod, rc: RaClass) { + var enumDecl = EnumType.!(rc.oldType).enumDecl; + var numCases = enumDecl.enumTagHi + 1; + var table = Array.new(numCases); + var mtable = IrMtable.new(rm.norm, 0, table); + rv.mtable = mtable; + + var ft = Function.funcRefType(rm.norm.getMethodType()); + mtable.record = ra.prog.newRecord(V3Array.newType(ft), numCases); + + // Fill all slots with the default implementation. + var defaultSpec = IrSpec.new(rm.norm.receiver, [rm.norm.receiver], rm.norm); + for (i < numCases) { + table[i] = rm.norm; + mtable.record.values[i] = FuncVal.new(defaultSpec); + } + + // Fill override slots. + var rootVst = rm.orig.source; + if (rootVst != null && rootVst.enumCaseIrs != null) { + for (i < rootVst.enumCaseIrs.length) { + var overrideIr = rootVst.enumCaseIrs[i]; + if (overrideIr == null) continue; + var overrideRm = overrideIr.raMethod; + if (overrideRm == null || !overrideRm.raFacts.RM_LIVE) continue; + table[i] = overrideRm.norm; + var ta = Arrays.replace(overrideRm.getSpec().typeArgs, 0, overrideRm.norm.receiver); + mtable.record.values[i] = FuncVal.new(IrSpec.new(ta[0], ta, overrideRm.norm)); + } + } + } def resolveMethodImpl(rc: RaClass, rm: RaMethod) -> RaMethod { var sm: RaMethod; for (sc = rc; sc != null; sc = sc.parent) { // find super method, if any diff --git a/aeneas/src/ir/Reachability.v3 b/aeneas/src/ir/Reachability.v3 index d244eb281..dc1720606 100644 --- a/aeneas/src/ir/Reachability.v3 +++ b/aeneas/src/ir/Reachability.v3 @@ -527,10 +527,31 @@ class ReachabilityAnalyzer(compilation: Compilation) { if (rm.isVirtual()) return; rm.virtual = RaVirtual.new(rm); var rc = makeClass(rm.receiver); + if (EnumType.?(rc.oldType)) { + getEnumVirtual(rm, rc); + return; + } for (l = rc.subtypes; l != null; l = l.tail) { analyzeVirtual(l.head, rm); } } + def getEnumVirtual(rm: RaMethod, rc: RaClass) { + // Add root implementation and mark it live so its SSA is generated. + var rv = rm.virtual; + rv.addImpl(rm); + getMethod(null, rm); + // Add per-case override implementations. + var rootVst = rm.orig.source; + if (rootVst != null && rootVst.enumCaseIrs != null) { + for (ir in rootVst.enumCaseIrs) { + if (ir == null) continue; + var overrideRm = makeMethod([rm.receiver], ir, null); + ir.raMethod = overrideRm; + rv.addImpl(overrideRm); + getMethod(null, overrideRm); + } + } + } def getMethod(op: SsaApplyOp, rm: RaMethod) { if (rm.setFact(RaFact.RM_LIVE)) return; liveMethods.put(rm); @@ -731,6 +752,9 @@ class ReachabilityAnalyzer(compilation: Compilation) { if (ic == null) ic = IrClass.new(t, null, null, [], []); raType = newRaClass(t, ic, parent); } + ENUM => { + raType = newRaClass(t, oldIr.makeIrClass(t), null); + } _ => { for (l = t.nested; l != null; l = l.tail) makeType(l.head); raType = RaType.new(t); diff --git a/aeneas/src/ir/SsaNormalizer.v3 b/aeneas/src/ir/SsaNormalizer.v3 index c0f717dec..fc4564eea 100644 --- a/aeneas/src/ir/SsaNormalizer.v3 +++ b/aeneas/src/ir/SsaNormalizer.v3 @@ -59,6 +59,7 @@ class SsaRaNormalizer extends SsaRebuilder { newParams.put(SsaParam.new(newParams.length, newIrType)); start++; // skip synthesized receiver } + // enum: receiver maps directly to the prepended tag param (no synthesized receiver) } if (tn.size == 1) { // common case; simple normalization @@ -254,6 +255,7 @@ class SsaRaNormalizer extends SsaRebuilder { ai_new = Arrays.prepend(newGraph.nullReceiver(), ai_new); } } + // enum: no receiver prepend needed (tag is already in normalized args) normCall(i_old, funcNorm, newOp, ai_new); } CallClassMethod(method) => { @@ -288,6 +290,9 @@ class SsaRaNormalizer extends SsaRebuilder { CallVariantVirtual(method) => { // devirtualize methods that are not overridden var rc = norm.ra.getClass(op.typeArgs[0]); + if (EnumType.?(rc.oldType)) { + return normEnumVirtualCall(i_old, orig, method); + } var t = extractVirtualRef(orig, method), funcNorm = t.0, m = t.1; var ai_new = normArgs(funcNorm, genRefs(i_old.inputs)); if (t.2) { // still a virtual dispatch @@ -855,6 +860,30 @@ class SsaRaNormalizer extends SsaRebuilder { ifc.endCase(newGraph.falseConst()); return ifc.finish(); } + def normEnumVirtualCall(i_old: SsaApplyOp, orig: Operator, method: IrMethod) { + var t = extractVirtualRef(orig, method), funcNorm = t.0, m = t.1; + var ai_new = normArgs(funcNorm, genRefs(i_old.inputs)); + var tagType = V3.getVariantTagType(EnumType.!(norm.ra.getClass(orig.typeArgs[0]).oldType)); + if (t.2) { // still a virtual dispatch + // enum value IS the tag; use it to index into the dispatch table + var tag = ai_new[0]; + var record = IrSelector.!(m.member).mtable.record; + var table = newGraph.valConst(record.rtype, record); + var func = curBlock.opArrayGetElem(record.rtype, tagType, Facts.O_SAFE_BOUNDS, table, tag); + if (norm.config.NonRefClosureReceiver) { + // Native/wasm: no Oop prepend + ai_new = Arrays.prepend(func, ai_new); + normCall(i_old, funcNorm, V3Op.newCallFunctionDirect(funcNorm.sub[0]), ai_new); + } else { + // JVM: prepend null Oop for closure compatibility + ai_new = Arrays.concat([func, newGraph.nullReceiver()], ai_new); + normCall(i_old, funcNorm, V3Op.newCallFunction(funcNorm.sub[0]), ai_new); + } + } else { + // devirtualized: no receiver prepend (tag is already in normalized args) + normCall(i_old, funcNorm, V3Op.newCallMethod(m), ai_new); + } + } def normVariantGetTag(vn: VariantNorm, args: Range) -> SsaInstr { if (vn == null) return null; if (vn.hasNoTag()) { @@ -1087,6 +1116,23 @@ class SsaRaNormalizer extends SsaRebuilder { } return opAnd(left, check); } + if (EnumType.?(atn.oldType) && EnumType.?(rtn.oldType)) { + // Enum subtype range test: lo <= tag <= hi. + var aDecl = EnumType.!(atn.oldType).enumDecl; + var rDecl = EnumType.!(rtn.oldType).enumDecl; + var actualTag = ai_old[offset]; // enum value IS its tag integer + var tagType = IntType.!(aDecl.tagType); + var lo = rDecl.enumTagLo, hi = rDecl.enumTagHi; + var check: SsaInstr; + if (lo == hi) { + check = curBlock.pure(V3Op.newIntEq(tagType), [actualTag, newGraph.intConst(lo)]); + } else { + var chkLo = curBlock.pure(tagType.opLtEq(), [newGraph.intConst(lo), actualTag]); + var chkHi = curBlock.pure(tagType.opLtEq(), [actualTag, newGraph.intConst(hi)]); + check = curBlock.opBoolAnd0(chkLo, chkHi); + } + return opAnd(left, check); + } // break } _ => ; // break diff --git a/aeneas/src/ir/VstIr.v3 b/aeneas/src/ir/VstIr.v3 index 81793eb74..ec6e11b3a 100644 --- a/aeneas/src/ir/VstIr.v3 +++ b/aeneas/src/ir/VstIr.v3 @@ -29,6 +29,7 @@ component VstIr { } } class IrBuilder(ctype: Type, parent: IrClass) { + var irModule: IrModule; def fields = Vector.new(); def methods = Vector.new(); @@ -70,11 +71,87 @@ class IrBuilder(ctype: Type, parent: IrClass) { x: VstMethod => addVstMethod(x); } } + // Build IR methods for per-case enum method overrides. + if (VstEnum.?(decl)) addEnumCaseOverrides(VstEnum.!(decl)); var ic = build(); ic.boxing = boxing; ic.packed = packed; return ic; } + def addEnumCaseOverrides(decl: VstEnum) { + // For subtype enums, all overrides are already processed by the + // root enum's buildClass (see addSubtypeOverridesRecursive below). + if (decl.parentEnum != null) return; + // Process root's own per-case overrides. + addEnumCaseOverridesForDecl(decl); + // Recursively process all subtypes so that M_OVERRIDDEN is set + // on root methods and enumCaseIrs is populated before any SSA + // generation (critical for v3i lazy evaluation). + addSubtypeOverridesRecursive(decl); + } + def addSubtypeOverridesRecursive(decl: VstEnum) { + for (l = decl.subtypeOrder; l != null; l = l.tail) { + addEnumCaseOverridesForDecl(l.head); + addSubtypeOverridesRecursive(l.head); + } + } + def addEnumCaseOverridesForDecl(decl: VstEnum) { + var rootEnum = findRootEnum(decl); + var tableSize = rootEnum.enumTagHi + 1; + // 1. Subtype-level method overrides: fill range in root's enumCaseIrs. + for (l = decl.members; l != null; l = l.tail) { + if (!VstMethod.?(l.head)) continue; + var vm = VstMethod.!(l.head); + if (vm.root == null || vm.root == vm) continue; // not an override + var ir = newIrMethod(vm); + addIrMethod(ir); + vm.index = ir.index; + ir.flags |= IrFlag.M_OVERRIDE; + var rootVst = vm.root; + markEnumRootMethodOverridden(rootVst); + if (rootVst.enumCaseIrs == null) { + rootVst.enumCaseIrs = Array.new(tableSize); + } + for (i = decl.enumTagLo; i <= decl.enumTagHi; i++) { + rootVst.enumCaseIrs[i] = ir; + } + } + // 2. Per-case method overrides. + for (c in decl.cases) { + if (c.members == null) continue; + for (l = c.members; l != null; l = l.tail) { + if (!VstMethod.?(l.head)) continue; + var vm = VstMethod.!(l.head); + var ir = newIrMethod(vm); + addIrMethod(ir); + vm.index = ir.index; + ir.flags |= IrFlag.M_OVERRIDE; + var rootVst = vm.root; + markEnumRootMethodOverridden(rootVst); + if (rootVst.enumCaseIrs == null) { + rootVst.enumCaseIrs = Array.new(tableSize); + } + var tag = if(c.enumTag >= 0, c.enumTag, c.tag); + rootVst.enumCaseIrs[tag] = ir; + } + } + } + def markEnumRootMethodOverridden(rootVst: VstMethod) { + // Find the root method's IrMethod in the current builder's methods vector. + // We search by source reference since the VstMethod.index may not yet + // reflect the IrMethod index (it's updated by addVstMethod). + for (i = 2; i < methods.length; i++) { + var m = methods[i]; + if (m != null && m.source == rootVst) { + m.flags |= IrFlag.M_OVERRIDDEN; + return; + } + } + } + def findRootEnum(decl: VstEnum) -> VstEnum { + while (decl.parentEnum != null) decl = decl.parentEnum; + return decl; + } def addVstField(f: VstField, isVariant: bool, unboxed: bool) { var ir = IrField.new(ctype, f.getType()); addRepHintFacts(f, ir); @@ -110,12 +187,15 @@ class IrBuilder(ctype: Type, parent: IrClass) { if (r == m) { // this is the root method of a method family addIrMethod(ir); + } else if (parent != null) { + // this is a class/variant override + parent.methods[r.index].flags |= IrFlag.M_OVERRIDDEN; + ir.flags |= IrFlag.M_OVERRIDE; + setIrMethod(r.index, ir); } else { - // this is an override - parent.methods[r.index].flags |= IrFlag.M_OVERRIDDEN; - ir.flags |= IrFlag.M_OVERRIDE; - setIrMethod(r.index, ir); - } + // enum subtype override: handled by addEnumCaseOverrides + addIrMethod(ir); + } m.index = ir.index; } def addDescriptorField(cdecl: VstClass) { diff --git a/aeneas/src/jvm/JvmGen.v3 b/aeneas/src/jvm/JvmGen.v3 index 5f5fca38c..f8ede8d0d 100644 --- a/aeneas/src/jvm/JvmGen.v3 +++ b/aeneas/src/jvm/JvmGen.v3 @@ -107,6 +107,9 @@ class JvmProgram(compiler: Compiler, prog: Program) { } CLASS => jvmType(utype); COMPONENT => jvmType(utype); + ENUM => { + if (ic.methods.length > 0) jrep.makeJvmEnumMethodHost(utype, ic); + } _ => ; } } @@ -162,7 +165,9 @@ class JvmProgram(compiler: Compiler, prog: Program) { var buffer = StringBuilder.new(); buffer.puts(mangleTypeName("V3D_", memberRef.receiver)); buffer.puts("$D"); - var methName = V3.mangleIrMember(memberRef.member); + var methName = if(EnumType.?(memberRef.receiver), + mangleEnumMethod(IrMethod.!(memberRef.member)), + V3.mangleIrMember(memberRef.member)); buffer.puts(methName); return buffer.toString(); } @@ -250,6 +255,10 @@ class JvmProgram(compiler: Compiler, prog: Program) { } return abstractClosureClass; } + def mangleEnumMethod(m: IrMethod) -> string { + // Append uid to distinguish overrides that share the same method name. + return StringBuilder.new().puts(V3.mangleIrMember(m)).putc('$').putd(m.uid).toString(); + } def isUseless(vtype: Type) -> bool { return vtype == Void.TYPE || V3.isComponent(vtype); } @@ -480,6 +489,22 @@ class JvmV3ComponentGen extends JvmClassGen { main_code.attach(main_meth); } } +// Generates a Jvm class to host static methods for an enum type +class JvmV3EnumGen extends JvmClassGen { + def enumType: Type; + def ic: IrClass; + new(builder: JvmClassfileBuilder, enumType, ic) super(builder) { } + def build() { + for (m in ic.methods) buildMethod(m); + addEmptyInit(); + } + def buildMethod(m: IrMethod) { + if (m == null) return; + var name = builder.jprog.mangleEnumMethod(m); + var jsig = jvmSig(m.getMethodType()); + genCode(builder, m, name, jsig, true); + } +} // Generates a Jvm class representing a tuple class JvmV3TupleGen extends JvmClassGen { def tupleType: Type; @@ -575,7 +600,7 @@ class JvmV3ClosureGen extends JvmClassGen { if (memberRef == null) { buildAbstractClass(invoke_meth); } else { - if (V3.isComponent(memberRef.receiver)) buildComponentClosure(invoke_meth); + if (V3.isComponent(memberRef.receiver) || EnumType.?(memberRef.receiver)) buildComponentClosure(invoke_meth); else buildClassClosure(invoke_meth); } } @@ -626,7 +651,10 @@ class JvmV3ClosureGen extends JvmClassGen { builder.emitLoadArguments(invoke_code, methType, methType, 2); var jclass = builder.jprog.jvmClass(memberRef.receiver); invoke_code.locals(invoke_sig.localsSize(true)); - invoke_code.invokestatic(jclass.name, V3.mangleIrMember(memberRef.member), jvmSig(methType)); + var mname = if(EnumType.?(memberRef.receiver), + builder.jprog.mangleEnumMethod(IrMethod.!(memberRef.member)), + V3.mangleIrMember(memberRef.member)); + invoke_code.invokestatic(jclass.name, mname, jvmSig(methType)); invoke_code.ret(jvmKind(Function.getReturnType(funcType))); invoke_code.attach(invoke_meth); } diff --git a/aeneas/src/jvm/JvmRep.v3 b/aeneas/src/jvm/JvmRep.v3 index d7f1b84c1..5f4fb1410 100644 --- a/aeneas/src/jvm/JvmRep.v3 +++ b/aeneas/src/jvm/JvmRep.v3 @@ -99,6 +99,12 @@ class JvmTypeReps(jprog: JvmProgram) { jprog.classQueue.put(JvmV3ClosureGen.new(JvmClassfileBuilder.new(jprog, jclass), utype, null, 0)); return jclass; } + def makeJvmEnumMethodHost(utype: Type, ic: IrClass) { + var className = jprog.mangleTypeName("V3E_", utype); + var jclass = JvmClass.new(className, JvmTypes.jlo); + jvmClasses[utype] = jclass; + jprog.classQueue.put(JvmV3EnumGen.new(JvmClassfileBuilder.new(jprog, jclass), utype, ic)); + } def emitGetField(memberRef: IrSpec, code: JvmCodeBuilder) { var fieldType = memberRef.getFieldType(); var r = getRep(fieldType); diff --git a/aeneas/src/jvm/SsaJvmGen.v3 b/aeneas/src/jvm/SsaJvmGen.v3 index 93ce61be0..a717c3879 100644 --- a/aeneas/src/jvm/SsaJvmGen.v3 +++ b/aeneas/src/jvm/SsaJvmGen.v3 @@ -665,9 +665,12 @@ class SsaJvmGen(jprog: JvmProgram, context: SsaContext, jsig: JvmSig, code: JvmC if (methodRef.isNew()) { var nsig = jprog.makeJvmSig(jclass, methodRef.getMethodType(), jclass); code.invokestatic(jclass.name, "$init", nsig); - } else if (V3.isComponent(methodRef.receiver)) { + } else if (V3.isComponent(methodRef.receiver) || EnumType.?(methodRef.receiver)) { var nsig = jprog.jvmSig(methodRef.getMethodType()); - code.invokestatic(jclass.name, V3.mangleIrMember(methodRef.member), nsig); + var mname = if(EnumType.?(methodRef.receiver), + jprog.mangleEnumMethod(IrMethod.!(methodRef.member)), + V3.mangleIrMember(methodRef.member)); + code.invokestatic(jclass.name, mname, nsig); } else { jprog.jrep.emitInvokeVirtual(code, methodRef, i.inputs[0].dest.facts.V_NON_ZERO); } diff --git a/aeneas/src/ssa/SsaBuilder.v3 b/aeneas/src/ssa/SsaBuilder.v3 index b93663c31..1f1d250cb 100644 --- a/aeneas/src/ssa/SsaBuilder.v3 +++ b/aeneas/src/ssa/SsaBuilder.v3 @@ -106,6 +106,14 @@ class SsaBuilder { return opArrayGetElem(V3.stringArrayType, V3.getVariantTagType(t), Facts.O_SAFE_BOUNDS | Fact.O_PURE, array, index); } + EnumGetShortName => { + var t = EnumType.!(op.typeArgs[0]); + var record = t.getShortNames(context.prog); + var array = graph.recordConst(record); + var tagType = IntType.!(t.enumDecl.tagType); + return opArrayGetElem(V3.stringArrayType, tagType, + Facts.O_SAFE_BOUNDS | Fact.O_PURE, array, args[0]); + } EnumGetParam(field) => { var t = field.receiver.declType; var facts = Facts.O_SAFE_BOUNDS; @@ -122,7 +130,7 @@ class SsaBuilder { } EnumSetHas(vcase) => { var etc = EnumSetType.!(pt[0]), it = etc.repType; - var val: Val = if(it.width <= 32, Int.box(1 << u5.!(vcase.tag)), Long.box(1L << u6.!(vcase.tag))); + var val: Val = if(it.width <= 32, Int.box(1 << u5.!(vcase.enumTag)), Long.box(1L << u6.!(vcase.enumTag))); var mask = graph.valConst(it, val); var and = add(it.opAnd(), [args[0], mask], N); return add(it.opEq(), [and, mask], N); diff --git a/aeneas/src/ssa/SsaOptimizer.v3 b/aeneas/src/ssa/SsaOptimizer.v3 index e78925d50..1b5c60de8 100644 --- a/aeneas/src/ssa/SsaOptimizer.v3 +++ b/aeneas/src/ssa/SsaOptimizer.v3 @@ -408,6 +408,7 @@ class SsaInstrReducer(context: SsaContext) extends SsaInstrMatcher { var res = reduceApplyOp(x); if (res != x) { x.replace(res); + x.kill(); x.remove(); } return res; @@ -869,7 +870,7 @@ class SsaInstrReducer(context: SsaContext) extends SsaInstrMatcher { var xval = unop(i); if (xconst) { // ClassGetMethod[m](#K) => (m, #K) - var obj = Record.!(xval); + var obj: Val = if(EnumType.?(meth.receiver), xval, Record.!(xval)); return graph.valConst(meth.getBoundType(), Closure.new(obj, meth)); } } @@ -881,7 +882,7 @@ class SsaInstrReducer(context: SsaContext) extends SsaInstrMatcher { if (dv != null) { var xval = unop(i); if (xconst && !i.op.isPolymorphic()) { // ClassGetVirtual[m](K) => (m, K) - var obj = Record.!(xval); + var obj: Val = if(EnumType.?(meth.receiver), xval, Record.!(xval)); return graph.valConst(dv.getBoundType(), Closure.new(obj, dv)); } return replaceOp(i, V3Op.newClassGetMethod(dv)); // ClassGetVirtual[m](K) => ClassGetMethod[m](K) @@ -1020,7 +1021,8 @@ class SsaInstrReducer(context: SsaContext) extends SsaInstrMatcher { } CallMethod(method) => { i.facts |= method.facts & Fact.O_PURE; - if (i.inputs.length > 0 && i.input0().facts.V_NON_ZERO) i.facts |= Fact.O_NO_NULL_CHECK; + if (EnumType.?(method.receiver)) i.facts |= Fact.O_NO_NULL_CHECK; + else if (i.inputs.length > 0 && i.input0().facts.V_NON_ZERO) i.facts |= Fact.O_NO_NULL_CHECK; state.kill(); } CallClassMethod(method) => { @@ -1043,7 +1045,8 @@ class SsaInstrReducer(context: SsaContext) extends SsaInstrMatcher { } CallVariantVirtual(method) => { var receiver = i.input0(), meth = V3Op.extractIrSpec(i.op, method); - if (i.input0().facts.V_NON_ZERO) i.facts |= Fact.O_NO_NULL_CHECK; + if (EnumType.?(method.receiver)) i.facts |= Fact.O_NO_NULL_CHECK; + else if (i.input0().facts.V_NON_ZERO) i.facts |= Fact.O_NO_NULL_CHECK; var dv = devirtualize(meth, receiver); state.kill(); if (dv != null) return replaceOp(i, V3Op.newCallMethod(dv)); // CallVariantVirtual[m](K) => CallMethod[m](k) @@ -1077,12 +1080,28 @@ class SsaInstrReducer(context: SsaContext) extends SsaInstrMatcher { i_recvr = x.input0(); newFacts = i.facts & Fact.O_NO_NULL_CHECK; } + VariantGetMethod(method) => { + // CallClosure(VariantGetMethod[m](e)) => CallMethod[m](e) + var spec = V3Op.extractIrSpec(xop, method); + newOp = V3Op.newCallMethod(spec); + i_recvr = x.input0(); + if (EnumType.?(spec.receiver)) newFacts |= Fact.O_NO_NULL_CHECK; + else newFacts = i.facts & Fact.O_NO_NULL_CHECK; + } + VariantGetVirtual(method) => { + // CallClosure(VariantGetVirtual[m](e)) => CallVariantVirtual[m](e) + var spec = V3Op.extractIrSpec(xop, method); + newOp = V3Op.newCallVariantVirtual(spec); + i_recvr = x.input0(); + if (EnumType.?(spec.receiver)) newFacts |= Fact.O_NO_NULL_CHECK; + else newFacts = i.facts & Fact.O_NO_NULL_CHECK; + } CreateClosure(method) => { // CallClosure(CreateClosure[m](p), ...) => CallMethod[m](p, ...); var spec = V3Op.extractIrSpec(xop, method); newOp = V3Op.newCallMethod(spec); i_recvr = if (V3.isComponent(spec.receiver), graph.nullReceiver(), x.input0()); - if (V3.isComponent(spec.receiver)) newFacts |= Fact.O_NO_NULL_CHECK; + if (V3.isComponent(spec.receiver) || EnumType.?(spec.receiver)) newFacts |= Fact.O_NO_NULL_CHECK; else newFacts |= x.facts & Fact.O_NO_NULL_CHECK; } _ => { @@ -1448,6 +1467,8 @@ class SsaInstrReducer(context: SsaContext) extends SsaInstrMatcher { def devirtualize(m: IrSpec, x: SsaInstr) -> IrSpec { // Try to devirtualize based on the class hierachy first. if (!m.member.flags.M_OVERRIDDEN) return m; + // Enum types cannot be devirtualized by record/allocation pattern matching. + if (EnumType.?(m.receiver)) return null; var optag = x.optag(); // Look through TypeSubsumes at a potentially more precise receiver. while (optag == Opcode.TypeSubsume.tag) { diff --git a/aeneas/src/ssa/VstSsaGen.v3 b/aeneas/src/ssa/VstSsaGen.v3 index b1e07730d..2283e8945 100644 --- a/aeneas/src/ssa/VstSsaGen.v3 +++ b/aeneas/src/ssa/VstSsaGen.v3 @@ -142,23 +142,60 @@ class VstSsaGen extends VstVisitor { } def addEnumParamInitCode(receiver: VstEnum, env: VstSsaEnv) { context.method.flags |= IrFlag.M_ENUM_INIT; - var params = receiver.params.asArray(); - var types = Array.new(params.length); - var consts = Array.new(params.length); - for (i < params.length) { - var r = VstField.!(params[i].member).enumVals; - consts[i] = context.graph.valConst(r.rtype, r); - types[i] = r.rtype; - } - var cases = receiver.cases; - for (i < cases.length) { - var c = cases[i], args = c.args.asArray(); - for (j < args.length) { - var val = env.genExpr(args[j]); - var index = context.graph.intConst(i); - env.opArraySetElem(types[j], Int.TYPE, Facts.O_SAFE_BOUNDS, consts[j], index, val); + // Find the root enum. + var rootEnum = receiver; + while (rootEnum.qualifiers != null) { + var tc = context.prog.typeEnv.lookup(rootEnum.qualifiers.head.image); + if (tc == null || tc.kind != Kind.ENUM) break; + rootEnum = EnumType.!(tc.create0()).enumDecl; + } + // Count total params: root + all ancestor extras + own extras + var totalParams = 0; + if (rootEnum.params != null) totalParams += rootEnum.params.length(); + // Collect ancestor extras in root-to-receiver order + var ancestorExtras: List>; + var cur = receiver; + while (cur != null && cur != rootEnum) { + if (cur.extraParams != null) ancestorExtras = List.new(cur.extraParams, ancestorExtras); + cur = cur.parentEnum; + } + // ancestorExtras is now in root-to-receiver order (reversed during walk) + for (el = ancestorExtras; el != null; el = el.tail) totalParams += el.head.length(); + if (totalParams == 0) return; + var types = Array.new(totalParams); + var consts = Array.new(totalParams); + var offset = 0; + // Root params → root's enumVals arrays + if (rootEnum.params != null) { + var params = rootEnum.params.asArray(); + for (i < params.length) { + var r = VstField.!(params[i].member).enumVals; + consts[offset] = context.graph.valConst(r.rtype, r); + types[offset] = r.rtype; + offset++; } } + // Ancestor extras → each ancestor's own enumVals arrays + for (el = ancestorExtras; el != null; el = el.tail) { + for (l = el.head.list; l != null; l = l.tail) { + var r = VstField.!(l.head.member).enumVals; + consts[offset] = context.graph.valConst(r.rtype, r); + types[offset] = r.rtype; + offset++; + } + } + // Process cases: args map positionally to [root, ancestor extras, own extras] + var cases = receiver.cases; + for (i < cases.length) { + var c = cases[i]; + if (Strings.equal(c.name(), "_")) continue; + var args = c.args.asArray(); + for (j < args.length) { + var val = env.genExpr(args[j]); + var index = context.graph.intConst(c.enumTag); + env.opArraySetElem(types[j], Int.TYPE, Facts.O_SAFE_BOUNDS, consts[j], index, val); + } + } } def getThisParam() -> SsaInstr { return graph.params[0]; @@ -568,14 +605,19 @@ class VstSsaGen extends VstVisitor { return null; } def genPatternCond(keyType: Type, cur: SsaBuilder, pat: MatchPattern, origKey: SsaInstr, key: SsaInstr) -> SsaInstr { - if (pat.vdecl != null && pat.variantCase == null) { - var t = pat.vdecl.tref.binding; - if (V3.isVariant(t) && ClassType.!(t).classDecl.qualifiers != null) { - // subtype variant pattern: generate "S.?(origKey)" using the pre-tag-extraction value - var query = V3Op.newTypeQuery(origKey.getType(), t); - return cur.addApply(null, query, [origKey]); - } + if (pat.subtypeType != null) { + // subtype variant pattern: generate "S.?(origKey)" using the pre-tag-extraction value + var origKeyType = origKey.getType(); + var query = V3Op.newTypeQuery(origKeyType, pat.subtypeType); + return cur.addApply(null, query, [origKey]); + } else if (pat.subtypeEnumType != null) { + // subtype enum pattern: generate "E1.More.?(origKey)" using a VARIANT_QUERY range test + // Must use origKey's enum type (not the tagType) so TypeQuery resolves to VARIANT_QUERY + var query = V3Op.newTypeQuery(origKey.getType(), pat.subtypeEnumType); + return cur.addApply(null, query, [origKey]); + } else if (pat.vdecl != null && pat.variantCase == null) { // type case (first grammar rule "id: TypeRef"), generate "T.?(key)" + var t = pat.vdecl.tref.binding; var query = V3Op.newTypeQuery(keyType, t); return cur.addApply(null, query, [key]); } else { @@ -587,17 +629,19 @@ class VstSsaGen extends VstVisitor { def matchPatterns(keyType: Type, pl: List, val: Val) -> bool { while (pl != null) { var pat = pl.head; - if (pat.vdecl != null) { - var t = pat.vdecl.tref.binding; - if (V3.isVariant(t) && ClassType.!(t).classDecl.qualifiers != null) { - // subtype pattern: check if integer tag falls in the subtype's DFS range - var decl = ClassType.!(t).classDecl; - var tagVal = Int.unbox(val); - if (tagVal >= decl.variantTag && tagVal <= decl.variantTagHi) return true; - } else { - var r = Eval.doQuery0(keyType, t, val); - if (r) return true; - } + if (pat.subtypeType != null) { + // subtype variant pattern: check if integer tag falls in the subtype's DFS range + var decl = ClassType.!(pat.subtypeType).classDecl; + var tagVal = Int.unbox(val); + if (tagVal >= decl.variantTag && tagVal <= decl.variantTagHi) return true; + } else if (pat.subtypeEnumType != null) { + // subtype enum pattern: check if integer tag falls in the subtype enum's DFS range + var decl = EnumType.!(pat.subtypeEnumType).enumDecl; + var tagVal = Int.unbox(val); + if (tagVal >= decl.enumTagLo && tagVal <= decl.enumTagHi) return true; + } else if (pat.vdecl != null) { + var r = Eval.doQuery0(keyType, pat.vdecl.tref.binding, val); + if (r) return true; } else if (Values.equal(val, pat.val)) { return true; } @@ -693,7 +737,7 @@ class VstSsaGen extends VstVisitor { Void => return env.graph.nop(); Const(val, vtype) => return env.graph.valConst(vtype, val); EnumConst(member) => { - return env.graph.valConst(member.receiver.getDeclaredType(), Int.box(member.tag)); + return env.graph.valConst(member.receiver.getDeclaredType(), Int.box(member.enumTag)); } Local(decl) => return env[decl.ssa]; ObjectMethod(receiver, member, typeArgs) => { diff --git a/aeneas/src/v3/TypeSystem.v3 b/aeneas/src/v3/TypeSystem.v3 index 36f9dcca3..398b13187 100644 --- a/aeneas/src/v3/TypeSystem.v3 +++ b/aeneas/src/v3/TypeSystem.v3 @@ -34,7 +34,9 @@ component TypeSystem { // Check if the type {xt} is an enum and the type {yt} is its corresponding set type. def isEnumToSet(xt: Type, yt: Type) -> bool { if (EnumType.?(xt) && EnumSetType.?(yt)) { - return EnumType.!(xt).enumDecl == EnumSetType.!(yt).enumDecl; + var xd = EnumType.!(xt).enumDecl; + var yd = EnumSetType.!(yt).enumDecl; + return xd == yd || isEnumSubtypeOf(xd, yd); } return false; } @@ -115,7 +117,8 @@ component TypeSystem { } } x: EnumType => match (yt) { - y: EnumSetType => if (x.enumDecl == y.enumDecl) return Conversion.PROMOTE_ENUM_TO_SET; + y: EnumSetType => if (x.enumDecl == y.enumDecl || isEnumSubtypeOf(x.enumDecl, y.enumDecl)) return Conversion.PROMOTE_ENUM_TO_SET; + y: EnumType => if (isEnumSubtypeOf(x.enumDecl, y.enumDecl)) return Conversion.SUBSUME_VARIANT; } x: FloatType => match (yt) { y: FloatType => if (x.width <= y.width) return Conversion.PROMOTE_FLOAT_TO_FLOAT; @@ -225,6 +228,14 @@ component TypeSystem { return Conversion.ILLEGAL_EXCEPT_NULL; } } + x: EnumType => match (yt) { + y: EnumType => { + if (isEnumSubtypeOf(y.enumDecl, x.enumDecl)) { + return Conversion.CAST_VARIANT; + } + return Conversion.ILLEGAL; + } + } x: OopType => match (yt) { y: ArrayType => return Conversion.CAST_ARRAY; y: ClassType => return if(V3.isVariant(y), Conversion.CAST_VARIANT, Conversion.CAST_CLASS); @@ -779,3 +790,16 @@ def mergeConversions(a: Conversion, b: Conversion) -> Conversion { } return a; } +// Returns true if {sub} is a (direct or transitive) subtype enum of {sup}. +// Uses the subtypeOrder lists populated by the verifier (no prog access needed). +def isEnumSubtypeOf(sub: VstEnum, sup: VstEnum) -> bool { + if (sub.qualifiers == null) return false; // root enums are not subtypes + return enumAncestorHasSubtype(sup, sub); +} +def enumAncestorHasSubtype(ancestor: VstEnum, target: VstEnum) -> bool { + for (l = ancestor.subtypeOrder; l != null; l = l.tail) { + if (l.head == target) return true; + if (enumAncestorHasSubtype(l.head, target)) return true; + } + return false; +} diff --git a/aeneas/src/v3/V3Enum.v3 b/aeneas/src/v3/V3Enum.v3 index 82f059c29..523f0d368 100644 --- a/aeneas/src/v3/V3Enum.v3 +++ b/aeneas/src/v3/V3Enum.v3 @@ -6,6 +6,7 @@ class EnumType extends PrimType { def enumDecl: VstEnum; var setType: EnumSetType; var names: Record; + var shortNames: Record; new(hash: int, enumDecl, typeCon: TypeCon) super(hash, typeCon, enumDecl.tagType.width, null) { } @@ -18,24 +19,62 @@ class EnumType extends PrimType { var types: Array = [this]; return V3Op.newVstSugar(VstOperator.VariantGetName, types, types, V3.stringType); } + def getShortNameOperator() -> Operator { + var types: Array = [this]; + return V3Op.newVstSugar(VstOperator.EnumGetShortName, types, types, V3.stringType); + } def enumGetParamOperator(f: VstField) -> Operator { var types: Array = [this]; return V3Op.newVstSugar(VstOperator.EnumGetParam(f), TypeUtil.NO_TYPES, types, f.getType()); } + // Get the qualified names array (e.g. "More.C" for a subtype case). + // Array is indexed by DFS enumTag; size = enumTagHi + 1. def getNames(prog: Program) -> Record { if (names == null) { - names = prog.newRecord(V3.stringArrayType, enumDecl.cases.length); - for (vc in enumDecl.cases) { - names.values[vc.tag] = Byte.boxString(vc.name(), prog); - } + var total = if(enumDecl.enumTagHi >= 0, enumDecl.enumTagHi + 1, enumDecl.cases.length); + names = prog.newRecord(V3.stringArrayType, total); + fillNames(names, enumDecl, "", prog); } return names; } + // Get the short (unqualified) names array (always just c.name()). + // Array is indexed by DFS enumTag; size = enumTagHi + 1. + def getShortNames(prog: Program) -> Record { + if (shortNames == null) { + var total = if(enumDecl.enumTagHi >= 0, enumDecl.enumTagHi + 1, enumDecl.cases.length); + shortNames = prog.newRecord(V3.stringArrayType, total); + fillShortNames(shortNames, enumDecl, prog); + } + return shortNames; + } + private def fillNames(rec: Record, decl: VstEnum, prefix: string, prog: Program) { + for (c in decl.cases) { + if (c.enumTag < 0 || Strings.equal(c.name(), "_")) continue; + var n = if(prefix.length == 0, c.name(), Strings.format2("%s.%s", prefix, c.name())); + rec.values[c.enumTag] = Byte.boxString(n, prog); + } + var subtypes = Lists.reverse(decl.subtypeOrder); + for (l = subtypes; l != null; l = l.tail) { + var sub = l.head; + var p = if(prefix.length == 0, sub.name(), Strings.format2("%s.%s", prefix, sub.name())); + fillNames(rec, sub, p, prog); + } + } + private def fillShortNames(rec: Record, decl: VstEnum, prog: Program) { + for (c in decl.cases) { + if (c.enumTag < 0 || Strings.equal(c.name(), "_")) continue; + rec.values[c.enumTag] = Byte.boxString(c.name(), prog); + } + for (l = decl.subtypeOrder; l != null; l = l.tail) { + fillShortNames(rec, l.head, prog); + } + } } // The type for user-declared enums' sets. class EnumSetType extends PrimType { def enumDecl: VstEnum; def repType: IntType; + var allVal: Val; private var cache: V3EnumSet_OpCache; new(hash: int, enumDecl, repType, typeCon: TypeCon) super(hash, typeCon, repType.width, null) { } diff --git a/aeneas/src/vst/MethodEnv.v3 b/aeneas/src/vst/MethodEnv.v3 index 19dee58e4..748135601 100644 --- a/aeneas/src/vst/MethodEnv.v3 +++ b/aeneas/src/vst/MethodEnv.v3 @@ -165,7 +165,22 @@ class MethodEnv { return VarBinding.None; } def resolveEnumMember(expr: VarExpr, enumType: EnumType, name: string) -> ResolvedMember { - var member = enumType.enumDecl.memberMap[name]; + var decl = enumType.enumDecl; + var member = decl.memberMap[name]; + if (decl.qualifiers != null) { + if (member == null || VstField.?(member)) { + // Walk the ancestor chain for fields and missing members. + var cur = decl.parentEnum; + while (cur != null) { + var ancestorMember = cur.memberMap[name]; + if (ancestorMember != null) { member = ancestorMember; break; } + cur = cur.parentEnum; + } + } else if (VstMethod.?(member) && VstMethod.!(member).root != VstMethod.!(member)) { + // Subtype override: return root method for virtual dispatch. + member = VstMethod.!(member).root; + } + } if (member == null) return null; var memberType = inferMemberType(member); if (VstField.?(member)) { @@ -174,6 +189,12 @@ class MethodEnv { } return if(member != null, ResolvedMember.new(enumType, member, null, memberType)); } + def findRootEnumDecl(decl: VstEnum) -> VstEnum { + if (decl.qualifiers == null) return decl; + var tc = verifier.verifier.prog.typeEnv.lookup(decl.qualifiers.head.image); + if (tc == null || tc.kind != Kind.ENUM) return null; + return EnumType.!(tc.create0()).enumDecl; + } def resolveClassMember(expr: VarExpr, classType: ClassType, name: string) -> ResolvedMember { var exprType = classType; var member = classType.classDecl.memberMap[name]; @@ -327,9 +348,21 @@ class MethodEnv { if (width < 1) width = 1; var countType = Int.getType(false, width); return expr.bind(null, VarBinding.Const(Int.box(count), countType), countType); + } else if (Strings.equal(name, "shortName")) { + return newCompBinding(expr, enumType.getShortNameOperator()); } var member = resolveEnumMember(expr, enumType, name); - if (member == null) return VarBinding.None; + if (member == null) { + // Check if the name is a subtype enum. + if (enumType.enumDecl.subtypes != null) { + var sub = enumType.enumDecl.subtypes[name]; + if (sub != null) { + var subType = EnumType.!(sub.typeCon.singleton); + return expr.bind(null, VarBinding.Type(subType), subType); + } + } + return VarBinding.None; + } match (member.member) { x: VstField => { var memberType = Function.newType(enumType, member.memberType); @@ -346,25 +379,38 @@ class MethodEnv { x: VstEnumCase => { return expr.bind(null, VarBinding.EnumConst(x), enumType); } + x: VstMethod => { + var methodReceiver = x.receiver.getDeclaredType(); + var memberType = Function.prependParamType(methodReceiver, member.memberType); + return expr.bind(null, VarBinding.ClassMethod(methodReceiver, x, member.typeArgs), memberType); + } } return VarBinding.None; } def lookupEnumExprMember(expr: VarExpr, enumType: EnumType, useThis: bool) -> VarBinding { // lookup (expr: enum).name var name = expr.ident.name; + if (Strings.equal(name.image, "shortName")) { + return newApplyCompBinding(expr, expr.expr, enumType.getShortNameOperator()); + } var member = resolveEnumMember(expr, enumType, name.image); if (member == null) return VarBinding.None; + var receiver = objExpr(useThis, expr.ident.name, expr); match (member.member) { x: VstField => { return newApplyCompBinding(expr, expr.expr, enumType.enumGetParamOperator(x)); } x: VstVariantTagMember => { var tagType = enumType.enumDecl.tagType; - return newApplyCompBinding(expr, expr.expr, V3Op.newVstSugar(VstOperator.Nop, + return newApplyCompBinding(expr, receiver, V3Op.newVstSugar(VstOperator.Nop, [enumType, tagType], [enumType], tagType)); } x: VstVariantNameMember => { - return newApplyCompBinding(expr, expr.expr, enumType.getNameOperator()); + return newApplyCompBinding(expr, receiver, enumType.getNameOperator()); + } + x: VstMethod => { + var methodReceiver = x.receiver.getDeclaredType(); + return expr.bind(receiver, VarBinding.ObjectMethod(methodReceiver, x, member.typeArgs), member.memberType); } } return VarBinding.None; @@ -372,6 +418,14 @@ class MethodEnv { def lookupEnumConstMember(expr: VarExpr, enumType: EnumType, vc: VstEnumCase) -> VarBinding { // lookup (expr: enum.val).name var name = expr.ident.name; + if (Strings.equal(name.image, "shortName")) { + // Return the short name as a compile-time constant using the DFS global tag. + var shortNames = enumType.getShortNames(verifier.verifier.prog); + var idx = if(vc.enumTag >= 0, vc.enumTag, vc.tag); + if (idx < shortNames.values.length) { + return expr.bind(null, VarBinding.Const(shortNames.values[idx], V3.stringType), V3.stringType); + } + } var member = resolveEnumMember(expr, enumType, name.image); if (member == null) return lookupEnumExprMember(expr, enumType, false); match (member.member) { @@ -386,16 +440,64 @@ class MethodEnv { } x: VstVariantTagMember => { var tagType = enumType.enumDecl.tagType; - return expr.bind(null, VarBinding.Const(Int.box(vc.tag), tagType), tagType); + // Use DFS global tag (enumTag) for runtime; fall back to parse-order tag if unassigned. + var tag = if(vc.enumTag >= 0, vc.enumTag, vc.tag); + return expr.bind(null, VarBinding.Const(Int.box(tag), tagType), tagType); } x: VstVariantNameMember => { var names = enumType.getNames(verifier.verifier.prog); - var name = names.values[vc.tag]; - return expr.bind(null, VarBinding.Const(name, V3.stringType), V3.stringType); + var idx = if(vc.enumTag >= 0, vc.enumTag, vc.tag); + var n = if(idx < names.values.length, names.values[idx], null); + return expr.bind(null, VarBinding.Const(n, V3.stringType), V3.stringType); + } + x: VstMethod => { + // Check for per-case override, then subtype-level override. + var overrideMethod = findEnumCaseOverride(vc, name.image); + if (overrideMethod == null) overrideMethod = findEnumSubtypeOverride(enumType.enumDecl, vc, name.image); + if (overrideMethod != null) { + // Use root enum type as receiver: override IrMethods are + // stored in the root IrClass (not the subtype's IrClass). + var rootDecl = enumType.enumDecl; + while (rootDecl.parentEnum != null) rootDecl = rootDecl.parentEnum; + var methodReceiver = rootDecl.getDeclaredType(); + var receiver = objExpr(false, expr.ident.name, expr); + return expr.bind(receiver, VarBinding.ObjectMethod(methodReceiver, overrideMethod, member.typeArgs), member.memberType); + } + return lookupEnumExprMember(expr, enumType, false); } } return lookupEnumExprMember(expr, enumType, false); } + def findEnumCaseOverride(vc: VstEnumCase, name: string) -> VstMethod { + if (vc.members == null) return null; + for (l = vc.members; l != null; l = l.tail) { + if (VstMethod.?(l.head) && Strings.equal(l.head.name(), name)) return VstMethod.!(l.head); + } + return null; + } + def findEnumSubtypeOverride(rootDecl: VstEnum, vc: VstEnumCase, name: string) -> VstMethod { + // Walk from the case's declaring enum up to the root, looking for overrides. + var decl = findDeclaringEnum(rootDecl, vc); + while (decl != null) { + var member = decl.memberMap[name]; + if (member != null && VstMethod.?(member) && VstMethod.!(member).root != VstMethod.!(member)) { + return VstMethod.!(member); + } + decl = decl.parentEnum; + } + return null; + } + def findDeclaringEnum(rootDecl: VstEnum, vc: VstEnumCase) -> VstEnum { + // Find the enum that declares this case by searching the hierarchy. + for (c in rootDecl.cases) { + if (c == vc) return rootDecl; + } + for (l = rootDecl.subtypeOrder; l != null; l = l.tail) { + var result = findDeclaringEnum(l.head, vc); + if (result != null) return result; + } + return null; + } def lookupIntTypeMember(expr: VarExpr, etype: IntType) -> VarBinding { var name = expr.ident.name.image; var op: Operator, typeParams: List; @@ -523,12 +625,12 @@ class MethodEnv { var name = expr.ident.name.image; var tc = EnumSetType.!(etype); if (Strings.equal(name, "all")) { - return expr.bind(null, VarBinding.Const(tc.repType.max, etype), etype); + return expr.bind(null, VarBinding.Const(tc.allVal, etype), etype); } - var c = tc.enumDecl.memberMap[name]; - if (VstEnumCase.?(c)) { + var c = findEnumCaseInHierarchy(tc.enumDecl, name); + if (c != null) { // simple membership check - var op = V3Op.newVstSugar(VstOperator.EnumSetHas(VstEnumCase.!(c)), TypeUtil.NO_TYPES, [etype], Bool.TYPE); + var op = V3Op.newVstSugar(VstOperator.EnumSetHas(c), TypeUtil.NO_TYPES, [etype], Bool.TYPE); return newCompBinding(expr, op); } return VarBinding.None; @@ -536,14 +638,23 @@ class MethodEnv { def lookupEnumSetExprMember(expr: VarExpr, etype: Type) -> VarBinding { var name = expr.ident.name.image; var tc = EnumSetType.!(etype); - var c = tc.enumDecl.memberMap[name]; - if (VstEnumCase.?(c)) { + var c = findEnumCaseInHierarchy(tc.enumDecl, name); + if (c != null) { // simple membership check - var op = V3Op.newVstSugar(VstOperator.EnumSetHas(VstEnumCase.!(c)), TypeUtil.NO_TYPES, [etype], Bool.TYPE); + var op = V3Op.newVstSugar(VstOperator.EnumSetHas(c), TypeUtil.NO_TYPES, [etype], Bool.TYPE); return newApplyCompBinding(expr, expr.expr, op); } return VarBinding.None; } + def findEnumCaseInHierarchy(decl: VstEnum, name: string) -> VstEnumCase { + var m = decl.memberMap[name]; + if (VstEnumCase.?(m)) return VstEnumCase.!(m); + for (l = decl.subtypeOrder; l != null; l = l.tail) { + var c = findEnumCaseInHierarchy(l.head, name); + if (c != null) return c; + } + return null; + } def lookupRefTypeMember(expr: VarExpr, refType: RefType) -> VarBinding { var name = expr.ident.name.image; if (Strings.equal("at", name)) { diff --git a/aeneas/src/vst/Parser.v3 b/aeneas/src/vst/Parser.v3 index 91d79851b..140968412 100644 --- a/aeneas/src/vst/Parser.v3 +++ b/aeneas/src/vst/Parser.v3 @@ -459,8 +459,58 @@ component Parser { return true; } def parseEnum(p: ParserState, file: VstFile, isPrivate: bool) -> bool { + // Parse an optional dotted qualifier prefix (e.g. "E1." in "enum E1.More") then + // the enum name as a plain identifier (no type params, unlike variants). + var qualifiers: List; + var d = p.input; + while (Char.isIdentStart(p.curByte)) { + var q = p.curPos + 1; + while (q < d.length && Char.isIdentMiddle(d[q])) q++; + if (q < d.length && d[q] == '.' && q + 1 < d.length && Char.isIdentStart(d[q + 1])) { + var qid = parseIdentVoid(p); + p.advance1(); + qualifiers = List.new(qid.name, qualifiers); + } else { + break; + } + } + qualifiers = Lists.reverse(qualifiers); var id = parseIdentVoid(p); - var params = parseOptionalParams(p, parseEnumParam); + var superParams = false; + var params: VstList; + if (p.curByte == '(') { + var d = p.input; + var q = p.curPos + 1; + while (q < d.length && (d[q] == ' ' || d[q] == '\t')) q++; + if (q + 5 <= d.length && d[q] == 's' && d[q+1] == 'u' && d[q+2] == 'p' + && d[q+3] == 'e' && d[q+4] == 'r' + && (q + 5 >= d.length || !Char.isIdentMiddle(d[q+5]))) { + var r = q + 5; + while (r < d.length && (d[r] == ' ' || d[r] == '\t')) r++; + if (r < d.length && d[r] == ')') { + // (super) only + p.advance1(); + optKeyword(p, "super"); + p.req1(')'); + superParams = true; + } else if (r < d.length && d[r] == ',') { + // (super, extra_params...) + p.advance1(); // consume '(' + optKeyword(p, "super"); // consume 'super' + p.advance1(); // consume ',' + var paramList: List; + paramList = List.new(parseEnumParam(p), paramList); + while (p.curByte == ',') { + p.advance1(); + paramList = List.new(parseEnumParam(p), paramList); + } + p.req1(')'); + params = VstList.new(id.name.range(), Lists.reverse(paramList)); + superParams = true; + } + } + } + if (!superParams) params = parseOptionalParams(p, parseEnumParam); // Synthesize "tag" and "name" members var tagToken = Token.new(p.fileName, "tag", p.curLine, p.curCol); var nameToken = Token.new(p.fileName, "name", p.curLine, p.curCol); @@ -469,25 +519,87 @@ component Parser { list = List.new(VstVariantNameMember.new(nameToken), list); // Parse case members var cases = Vector.new(); + var hasDefault = false; p.req1('{'); while (true) { if (p.curByte == '}') { p.advance1(); break; } + if (p.curByte == ';') break; // methods section follows var c = parseEnumCase(p, cases.length); if (c == null) { p.opt1('}'); break; } + if (Strings.equal(c.name(), "_")) hasDefault = true; cases.put(c); list = List.new(c, list); + if (hasDefault) { + if (p.curByte == ';') break; // methods after _ + p.req1('}'); + break; + } + } + // Parse optional enum-level methods after ';' + if (p.curByte == ';') { + p.advance1(); + while (true) { + if (p.curByte == '}') { p.advance1(); break; } + var nlist = parseEnumMethod(p, list); + if (nlist == list) { p.opt1('}'); break; } + list = nlist; + } } var decl = VstEnum.new(isPrivate, id.name, params, cases.extract(), list); + decl.hasDefault = hasDefault; + decl.superParams = superParams; + decl.qualifiers = qualifiers; file.enums.put(decl); return true; } def parseEnumCase(p: ParserState, tag: int) -> VstEnumCase { + // Handle "_" default case specially (not an identifier start in Virgil). + if (p.curByte == '_' && !Char.isIdentMiddle(p.peek(1))) { + var ut = p.token(1); + var c = VstEnumCase.new(tag, ut, null); + if (p.curByte == '{') c.members = parseEnumCaseMembers(p); + p.opt1(','); + return c; + } var id = parseIdentVoid(p); var args = if(p.curByte == '(', parseList(0, p, '(', COMMA, ')', parseExpr)); + if (id.name.image.length > 0) { + var c = VstEnumCase.new(tag, id.name, args); + if (p.curByte == '{') c.members = parseEnumCaseMembers(p); + p.opt1(','); + return c; + } p.opt1(','); - if (id.name.image.length > 0) return VstEnumCase.new(tag, id.name, args); return null; } + def parseEnumCaseMembers(p: ParserState) -> List { + p.req1('{'); + var list: List; + while (true) { + if (p.curByte == '}') { p.advance1(); break; } + var isPrivate = optKeyword(p, "private") != null; + if (optKeyword(p, "def") != null) { + var parser = if(isPrivate, PRIVATE_DEF, PUBLIC_DEF); + var nlist = parser.parseDef(p, list, null); + if (nlist == list) { p.opt1('}'); break; } + list = nlist; + } else { + p.error("only 'def' allowed in enum case body"); + p.opt1('}'); + break; + } + } + return Lists.reverse(list); + } + def parseEnumMethod(p: ParserState, prev: List) -> List { + var isPrivate = optKeyword(p, "private") != null; + if (optKeyword(p, "def") != null) { + var parser = if(isPrivate, PRIVATE_DEF, PUBLIC_DEF); + return parser.parseDef(p, prev, null); + } + p.error("expected 'def' in enum method section"); + return prev; + } def parseLayout(p: ParserState, file: VstFile) -> bool { var id = parseIdentVoid(p).name; var repHints = parseRepHints(p); diff --git a/aeneas/src/vst/Verifier.v3 b/aeneas/src/vst/Verifier.v3 index 2ec45c951..4b24756d2 100644 --- a/aeneas/src/vst/Verifier.v3 +++ b/aeneas/src/vst/Verifier.v3 @@ -25,10 +25,14 @@ class Verifier(compiler: Compiler, prog: Program) { var vst = prog.vst; for (f in vst.files) buildFile(f); forAll(vst.classes, resolveSuperClass); + forAll(vst.enums, verifyEnum); + forAll(vst.enums, verifyEnumQualifiers); + forAll(vst.enums, fixEnumMethodRoots); + forAll(vst.enums, assignEnumTagsIfRoot); + forAll(vst.enums, finalizeEnumType); forAll(vst.components, verifyComponent); forAll(vst.classes, verifyClass); forAll(vst.classes, assignVariantTagsIfRoot); - forAll(vst.enums, verifyEnum); forAll(vst.layouts, verifyLayout); forAll(vst.exports, verifyExport); forAll(vst.packings, verifyPacking); @@ -124,11 +128,33 @@ class Verifier(compiler: Compiler, prog: Program) { if (constructor != null && VstComponent.?(decl)) addInit(constructor); } def addEnumVals(decl: VstEnum) { + if (decl.qualifiers != null) { + var rootEnum = findRootEnum(decl); + var needsInit = rootEnum != null && rootEnum.params != null; + // Create enumVals for subtype-only (extra) params + if (decl.extraParams != null) { + var arraySize = if(rootEnum.enumTagHi >= 0, rootEnum.enumTagHi + 1, decl.cases.length); + for (l = decl.extraParams.list; l != null; l = l.tail) { + var field = VstField.!(l.head.member); + var arrayType = V3Array.newType(field.getType()); + field.enumVals = prog.newRecord(arrayType, arraySize); + } + needsInit = true; + } + if (needsInit) { + var ic = prog.ir.makeIrClass(decl.getDeclaredType()); + var constructor = ic.methods[0]; + if (constructor != null) addInit(constructor); + } + return; + } if (decl.params == null) return; + // Root enum: size param arrays to cover all DFS-assigned tags (including subtypes). + var arraySize = if(decl.enumTagHi >= 0, decl.enumTagHi + 1, decl.cases.length); for (l = decl.params.list; l != null; l = l.tail) { var field = VstField.!(l.head.member); var arrayType = V3Array.newType(field.getType()); - field.enumVals = prog.newRecord(arrayType, decl.cases.length); + field.enumVals = prog.newRecord(arrayType, arraySize); } var ic = prog.ir.makeIrClass(decl.getDeclaredType()); var constructor = ic.methods[0]; @@ -507,6 +533,268 @@ class Verifier(compiler: Compiler, prog: Program) { var cv = decl.verifier; cv.verify(); } + def fixEnumMethodRoots(decl: VstEnum) { + // After verifyEnumQualifiers sets parentEnum, fix method root pointers + // for subtype enums. During verifyEnum, parentEnum isn't set yet, so + // checkMethod treats subtype override methods as roots (root = self). + if (decl.parentEnum == null) return; + var cv = decl.verifier; + for (l = decl.members; l != null; l = l.tail) { + if (!VstMethod.?(l.head)) continue; + var m = VstMethod.!(l.head); + if (m.root != m) continue; // already correctly set + var parentMethod = cv.findEnumMethodInParents(VstEnum.!(cv.compound), m.name()); + if (parentMethod != null) { + m.root = parentMethod.root; + m.index = parentMethod.index; + } + } + // Also fix per-case method overrides. + for (c in decl.cases) { + if (c.members == null) continue; + for (l = c.members; l != null; l = l.tail) { + if (!VstMethod.?(l.head)) continue; + var m = VstMethod.!(l.head); + // Case overrides should point to the ultimate root. + if (m.root != null && m.root.root != null && m.root != m.root.root) { + m.root = m.root.root; + } + } + } + } + def verifyEnumQualifiers(decl: VstEnum) { + var q = decl.qualifiers; + if (q == null) return; // no qualifiers → root enum, nothing to resolve + var cv = decl.verifier; + // Resolve first qualifier: must be a top-level (root) enum. + var firstTok = q.head; + var tc = prog.typeEnv.lookup(firstTok.image); + if (tc == null || tc.kind != Kind.ENUM) { + cv.errAtDecl(decl).set("EnumDeclError", + Strings.format1("qualifier \"%s\" does not name an enum type", firstTok.image)); + return; + } + var parentDecl = EnumType.!(tc.create0()).enumDecl; + if (parentDecl.qualifiers != null) { + cv.errAtDecl(decl).set("EnumDeclError", + Strings.format1("\"%s\" must be a top-level enum type", firstTok.image)); + return; + } + q = q.tail; + // Walk any remaining qualifiers (for names like E1.More.Sub). + while (q != null) { + if (!parentDecl.hasDefault) { + cv.errAtToken(q.head).set("EnumDeclError", + Strings.format1("enum \"%s\" has no '_ case' to allow subtypes", parentDecl.name())); + return; + } + var tok = q.head; + if (parentDecl.subtypes == null || parentDecl.subtypes[tok.image] == null) { + cv.errAtToken(tok).set("EnumDeclError", + Strings.format1("qualifier \"%s\" is not a declared subtype enum", tok.image)); + return; + } + parentDecl = parentDecl.subtypes[tok.image]; + q = q.tail; + } + // parentDecl is the immediate parent of decl. + if (!parentDecl.hasDefault) { + cv.errAtDecl(decl).set("EnumDeclError", + Strings.format1("enum \"%s\" has no '_ case' to allow subtypes", parentDecl.name())); + return; + } + var declName = decl.name(); + // Name must not clash with a named case of the parent (excluding _). + for (c in parentDecl.cases) { + if (!Strings.equal(c.name(), "_") && Strings.equal(c.name(), declName)) { + cv.errAtDecl(decl).set("EnumDeclError", + Strings.format2("subtype name \"%s\" conflicts with case of \"%s\"", + declName, parentDecl.name())); + return; + } + } + // Name must be unique among direct subtypes of the parent. + if (parentDecl.subtypes == null) parentDecl.subtypes = Strings.newMap(); + if (parentDecl.subtypes[declName] != null) { + cv.errAtDecl(decl).set("EnumDeclError", + Strings.format2("subtype \"%s\" of \"%s\" declared more than once", + declName, parentDecl.name())); + } else { + parentDecl.subtypes[declName] = decl; + parentDecl.subtypeOrder = List.new(decl, parentDecl.subtypeOrder); + } + decl.parentEnum = parentDecl; + // Validate subtype params against parent's effective params. + var rootDecl = findRootEnum(decl); + // Build parent's effective params: root params + all ancestor extras + var effectiveParams = buildEffectiveParams(rootDecl, parentDecl); + if (decl.superParams && effectiveParams == null) { + cv.errAtDecl(decl).set("EnumDeclError", + "'super' used but parent enum has no parameters"); + return; + } + if (decl.superParams && decl.params != null) { + // Form: (super, extra...) — decl.params has only new params + decl.extraParams = decl.params; + } else if (decl.params != null && effectiveParams != null) { + // Form 1 or Form 1+add: explicit redecl, possibly with extras + var dp = decl.params.list; + var ep = effectiveParams; + var i = 0; + while (dp != null && ep != null) { + if (!Strings.equal(dp.head.token.image, ep.head.token.image)) { + cv.errAtDecl(decl).set("EnumDeclError", + Strings.format3("parameter %d name \"%s\" does not match parent's \"%s\"", + i, dp.head.token.image, ep.head.token.image)); + return; + } + if (dp.head.vtype != ep.head.vtype) { + cv.errAtDecl(decl).set("EnumDeclError", + Strings.format1("parameter \"%s\" type does not match parent's type", + dp.head.token.image)); + return; + } + dp = dp.tail; + ep = ep.tail; + i++; + } + if (ep != null) { + cv.errAtDecl(decl).set("EnumDeclError", + "subtype has fewer parameters than parent's effective parameters"); + return; + } + if (dp != null) { + // Extra params beyond parent's effective + decl.extraParams = VstList.new(decl.params.src, dp); + } + } else if (decl.params != null && effectiveParams == null) { + cv.errAtDecl(decl).set("EnumDeclError", + "subtype declares parameters but parent enum has none"); + return; + } + // Mark constructor nontrivial when parent has effective params or subtype has extras + if (decl.params == null && (effectiveParams != null || decl.extraParams != null)) { + if (decl.constructor != null) decl.constructor.markNontrivial(); + } + } + def findRootEnum(decl: VstEnum) -> VstEnum { + var root = decl; + if (root.qualifiers != null) { + var tc = prog.typeEnv.lookup(root.qualifiers.head.image); + if (tc != null && tc.kind == Kind.ENUM) { + root = EnumType.!(tc.create0()).enumDecl; + } + } + return root; + } + // Build the effective param list for an enum: root params + all ancestor extras + // in root-to-target order. Returns null if no effective params. + def buildEffectiveParams(rootDecl: VstEnum, target: VstEnum) -> List { + var result: List; + if (rootDecl.params != null) { + for (l = rootDecl.params.list; l != null; l = l.tail) { + result = List.new(l.head, result); + } + } + // Walk from target up to root, collecting extras (builds in reverse order) + var extras: List>; + var cur = target; + while (cur != null && cur != rootDecl) { + if (cur.extraParams != null) extras = List.new(cur.extraParams.list, extras); + cur = cur.parentEnum; + } + // extras is now in root-to-target order (reversed during walk) + for (el = extras; el != null; el = el.tail) { + for (l = el.head; l != null; l = l.tail) { + result = List.new(l.head, result); + } + } + return Lists.reverse(result); + } + def assignEnumTagsIfRoot(decl: VstEnum) { + if (decl.qualifiers != null) return; // skip subtype enums + var nextTag = 0; + nextTag = assignEnumTagsDFS(decl, nextTag); + // Recompute the tag type to fit all newly assigned tags. + var total = nextTag; + var width = 1, exp = 2; + while (exp < total) { exp = exp << 1; width++; } + var tagType = Int.getType(false, byte.!(width)); + propagateEnumTagType(decl, tagType); + } + def assignEnumTagsDFS(decl: VstEnum, nextTag: int) -> int { + decl.enumTagLo = nextTag; + // Assign named cases (excluding _) in declaration order. + for (c in decl.cases) { + if (Strings.equal(c.name(), "_")) continue; + c.enumTag = nextTag++; + } + var defaultLo = nextTag; + // Recurse into subtypes in declaration order (subtypeOrder is reverse-decl, so reverse it). + var subtypes = Lists.reverse(decl.subtypeOrder); + for (l = subtypes; l != null; l = l.tail) { + nextTag = assignEnumTagsDFS(l.head, nextTag); + } + // Assign _ case's enumTag to the start of subtype range (inclusive low bound). + for (c in decl.cases) { + if (Strings.equal(c.name(), "_")) c.enumTag = defaultLo; + } + decl.enumTagHi = nextTag - 1; + return nextTag; + } + def propagateEnumTagType(decl: VstEnum, tagType: IntType) { + decl.tagType = tagType; + for (l = decl.subtypeOrder; l != null; l = l.tail) { + propagateEnumTagType(l.head, tagType); + } + } + def finalizeEnumType(decl: VstEnum) { + if (decl.qualifiers != null) return; // only process from root + var enumType = EnumType.!(decl.typeCon.singleton); + var isHierarchy = decl.hasDefault || decl.subtypeOrder != null; + if (!isHierarchy) { + // Non-hierarchy enum: constructor-created set type is correct. + if (enumType.setType != null) enumType.setType.allVal = enumType.setType.repType.max; + return; + } + // Hierarchy enum: recreate set type with DFS-aware sizing. + var totalTags = if(decl.enumTagHi >= 0, decl.enumTagHi + 1, decl.cases.length); + if (totalTags > Int.MAX_WIDTH) { + enumType.setType = null; // too large for set + return; + } + if (totalTags == 0) totalTags = 1; + var repType = Int.getType(false, totalTags); + var setName = Strings.builderOf(decl.token.image).puts(".set").toString(); + var tc = TypeCon.new(setName, Kind.ENUM_SET, 0, TypeUtil.globalCache); + var enumSetType = EnumSetType.new(tc.uid, decl, repType, tc); + tc.singleton = enumSetType; + enumType.setType = enumSetType; + // Compute allVal: union of (1 << c.enumTag) for all named cases. + enumSetType.allVal = computeEnumSetAll(decl, repType); + // Propagate shared set type to subtypes. + propagateEnumSetType(decl, enumSetType); + } + def computeEnumSetAll(decl: VstEnum, repType: IntType) -> Val { + var bits: long = 0; + for (c in decl.cases) { + if (c.enumTag >= 0 && !Strings.equal(c.name(), "_")) { + bits = bits | (1L << u6.!(c.enumTag)); + } + } + for (l = decl.subtypeOrder; l != null; l = l.tail) { + var sub = computeEnumSetAll(l.head, repType); + if (repType.width <= 32) bits = bits | long.!(Int.unbox(sub)); + else bits = bits | Long.unboxSU(sub, repType.signed); + } + return if(repType.width <= 32, Int.box(int.!(bits)), Long.box(bits)); + } + def propagateEnumSetType(decl: VstEnum, setType: EnumSetType) { + for (l = decl.subtypeOrder; l != null; l = l.tail) { + EnumType.!(l.head.typeCon.singleton).setType = setType; + propagateEnumSetType(l.head, setType); + } + } def verifyLayout(decl: VstLayout) { var cv = decl.verifier; var size = tryUnboxPositiveInt(decl.size, -1); @@ -613,6 +901,20 @@ class Verifier(compiler: Compiler, prog: Program) { } } } + // Type-check per-case method override bodies. + if (VstEnum.?(decl)) { + for (c in VstEnum.!(decl).cases) { + if (c.members == null) continue; + for (l = c.members; l != null; l = l.tail) { + if (VstMethod.?(l.head)) { + var m = VstMethod.!(l.head); + if (m.root == m) continue; // error in verification; skip body + tc.enterMethod(m, env); + if (!EmptyStmt.?(m.func.body)) tc.typeCheckBody(m.func.body); + } + } + } + } } def typeCheckNew(decl: VstCompound, cdecl: VstNew, tc: TypeChecker) { var cv = decl.verifier, thisParam = cv.newThisParam(), pl = cdecl.func.params; @@ -668,14 +970,25 @@ class Verifier(compiler: Compiler, prog: Program) { tc.typeCheckBody(cdecl.func.body); } def typeCheckEnumCase(decl: VstEnum, c: VstEnumCase, tc: TypeChecker) { + if (Strings.equal(c.name(), "_")) return; // _ case has no args var cv = decl.verifier; + // Build effective param types: root params + all ancestor extras + own extras var paramTypes: List; - if (decl.params != null) { - // extract parameter types. + var paramCount = 0; + if (decl.qualifiers != null) { + var root = findRootEnum(decl); + var effective = buildEffectiveParams(root, decl); + for (l = effective; l != null; l = l.tail) { + paramTypes = List.new(l.head.vtype, paramTypes); + paramCount++; + } + paramTypes = Lists.reverse(paramTypes); + } else if (decl.params != null) { for (params = decl.params.list; params != null; params = params.tail) { paramTypes = List.new(params.head.vtype, paramTypes); } paramTypes = Lists.reverse(paramTypes); + paramCount = decl.params.length(); } if (c.args != null) { // typecheck arguments to enum. @@ -685,11 +998,11 @@ class Verifier(compiler: Compiler, prog: Program) { if (paramTypes != null) { tc.typeCheckExpr(args.head, paramTypes.head, what); } else { - cv.errAtExpr(args.head).ArityMismatch(what, decl.params.length(), c.args.length()); + cv.errAtExpr(args.head).ArityMismatch(what, paramCount, c.args.length()); return; } } - if (paramTypes != null) cv.errAtDecl(c).ArityMismatch(what, decl.params.length(), c.args.length()); + if (paramTypes != null) cv.errAtDecl(c).ArityMismatch(what, paramCount, c.args.length()); } else { // expected parameters, but none were provided. if (paramTypes != null) cv.errAtDecl(c).CaseDeclError("expected case arguments, but none provided"); @@ -891,6 +1204,12 @@ class VstCompoundVerifier { x: VstLayoutField => checkLayoutField(x); } } + // Verify per-case method overrides for enums. + if (VstEnum.?(compound)) { + for (c in VstEnum.!(compound).cases) { + if (c.members != null) checkEnumCaseMembers(c); + } + } if (compound.constructor != null) { compound.constructor.memberinits = Lists.reverse(memberinits); } @@ -1010,6 +1329,16 @@ class VstCompoundVerifier { } var binding = lookupSuperMember(decl.name()); if (binding == null) { + // Check parent enum chain for subtype enum method overrides. + if (VstEnum.?(compound) && VstEnum.!(compound).parentEnum != null) { + var parentMethod = findEnumMethodInParents(VstEnum.!(compound), decl.name()); + if (parentMethod != null) { + decl.root = parentMethod.root; + decl.index = parentMethod.index; + verifier.methodOverrideChecks = List.new((this, decl), verifier.methodOverrideChecks); + return; + } + } decl.root = decl; decl.index = compound.numMethods++; return; @@ -1082,6 +1411,13 @@ class VstCompoundVerifier { } def checkMethodOverride(decl: VstMethod) { var binding = lookupSuperMember(decl.name()); // re-resolve member after return type inference + if (binding == null) { + // Enum case override: compare directly against root method. + if (decl.root != null && decl.root != decl) { + checkEnumCaseMethodOverride(decl); + } + return; + } var superDecl = VstMethod.!(binding.member); var superMethodType = binding.memberType; var methodType = decl.getType(); @@ -1110,6 +1446,62 @@ class VstCompoundVerifier { errAtDecl(decl).InheritanceError2("method signature %s cannot override %s", methodType, superMethodType); } } + def checkEnumCaseMembers(c: VstEnumCase) { + for (l = c.members; l != null; l = l.tail) { + match (l.head) { + x: VstMethod => checkEnumCaseMethod(x, c); + _ => errAtDecl(l.head).set("EnumDeclError", "only 'def' allowed in enum case body"); + } + } + } + def checkEnumCaseMethod(decl: VstMethod, ecase: VstEnumCase) { + decl.receiver = compound; + // Look up the root method in the enum's member map or parent chain. + var rootMember = compound.memberMap[decl.name()]; + if ((rootMember == null || !VstMethod.?(rootMember)) && VstEnum.?(compound)) { + var pm = findEnumMethodInParents(VstEnum.!(compound), decl.name()); + if (pm != null) rootMember = pm; + } + if (rootMember == null || !VstMethod.?(rootMember)) { + errAtDecl(decl).set("EnumDeclError", + Strings.format1("case method \"%s\" does not override an enum-level method", decl.name())); + decl.root = decl; + return; + } + var rootMethod = VstMethod.!(rootMember); + decl.root = rootMethod.root; + decl.index = rootMethod.index; + // Check parameters. + typeEnv = compound.typeEnv; + if (decl.func.params.list != null) { + var map = Strings.newMap(); + Lists.apply(decl.func.params.list, checkMethodParam(_, map)); + } + match (decl.func.rettype) { + Explicit(tref) => resolveType(tref, typeEnv); + Implicit(expr) => ; + _ => ; + } + // Defer override signature check until after body inference. + verifier.methodOverrideChecks = List.new((this, decl), verifier.methodOverrideChecks); + } + def checkEnumCaseMethodOverride(decl: VstMethod) { + var rootMethod = decl.root; + var superMethodType = rootMethod.getType(); + var methodType = decl.getType(); + if (superMethodType != null && methodType != null && !TypeSystem.isSubtype(methodType, superMethodType)) { + errAtDecl(decl).InheritanceError2("method signature %s cannot override %s", methodType, superMethodType); + } + } + def findEnumMethodInParents(decl: VstEnum, name: string) -> VstMethod { + var cur = decl.parentEnum; + while (cur != null) { + var member = cur.memberMap[name]; + if (member != null && VstMethod.?(member)) return VstMethod.!(member); + cur = cur.parentEnum; + } + return null; + } def checkParam(param: ParamDecl, map: Map) { if (map[param.name()] != null) errAtDecl(param).ParamDeclError(Strings.format1("parameter \"%s\" redefined", param.name())); map[param.name()] = param; @@ -1186,7 +1578,7 @@ class VstCompoundVerifier { } } } else if (EnumType.?(leftType)) { - // Enum.set resolution. + // Enum.set or Enum.SubtypeEnum resolution. var enumType = EnumType.!(leftType); if (Strings.equal(x.name.image, "set")) { if (x.nested != null) { @@ -1195,6 +1587,34 @@ class VstCompoundVerifier { } return getEnumSetType(x, enumType); } + // Check if name is a subtype enum. + if (enumType.enumDecl.subtypes != null) { + var subDecl = enumType.enumDecl.subtypes[x.name.image]; + if (subDecl != null) { + if (x.nested != null) { + errAtRange(x.name.range()).TypeParamArityError("enum subtype", + x.name.image, 0, x.nested.length()); + } + return x.binding = subDecl.typeCon.create0(); + } + } + // Fall back: subtypes map may not be populated yet (early type resolution). + var subTc = verifier.prog.typeEnv.lookup(x.name.image); + if (subTc != null && subTc.kind == Kind.ENUM) { + var subDecl = EnumType.!(subTc.create0()).enumDecl; + if (subDecl.qualifiers != null) { + // Accept if qualifiers start with the parent enum's name. + var q = subDecl.qualifiers; + while (q.tail != null) q = q.tail; + if (Strings.equal(q.head.image, enumType.enumDecl.name())) { + if (x.nested != null) { + errAtRange(x.name.range()).TypeParamArityError("enum subtype", + x.name.image, 0, x.nested.length()); + } + return x.binding = subDecl.typeCon.create0(); + } + } + } } if (x.left != null) { errAtRange(x.name.range()).set("UnresolvedType", Strings.format2("type \"%q\" has no member type \"%s\"", x.left.render, x.name.image)); @@ -1277,7 +1697,7 @@ class VstCompoundVerifier { def getEnumSetType(tref: TypeRef, enumType: EnumType) -> Type { if (enumType.setType == null) { errAtRange(tref.range()).EnumSetTooLarge(enumType, tref, enumType.enumDecl.cases.length); - return getErrorType(); + return tref.binding = getErrorType(); } return tref.binding = enumType.setType; } @@ -1804,6 +2224,7 @@ class TypeChecker(ERROR: ErrorGen, file: VstFile) extends VstVisitor pat.vdecl.isReadOnly = true; pat.vdecl.vtype = subtypeType; pat.vdecl.ssa = VstSsaVar.new(); + pat.subtypeType = subtypeType; return; } } @@ -1831,9 +2252,21 @@ class TypeChecker(ERROR: ErrorGen, file: VstFile) extends VstVisitor if (ve.expr != null) { return errAtExpr(ve).MatchError(Strings.format1("match on type %q requires unqualified case", vt.render)); } - var vc = vt.lookupCase(ve.ident.name.image); + var name = ve.ident.name.image; + var vc = vt.lookupCase(name); if (vc == null) { - return errAtExpr(ve).MatchError(Strings.format2("unresolved case %q.%s", vt.render, ve.ident.name.image)); + // Not a named case; check if it names a direct subtype enum. + if (vt.enumDecl.subtypes != null) { + var subtypeDecl = vt.enumDecl.subtypes[name]; + if (subtypeDecl != null) { + pat.subtypeEnumType = EnumType.!(subtypeDecl.typeCon.singleton); + return; + } + } + return errAtExpr(ve).MatchError(Strings.format2("unresolved case %q.%s", vt.render, name)); + } + if (Strings.equal(vc.name(), "_")) { + return errAtExpr(ve).MatchError(Strings.format1("cannot explicitly match '_ case' of enum %q; use a default arm instead", vt.render)); } mv.addEnumPattern(pat, vc); } @@ -2876,7 +3309,7 @@ class MatchStmtVerifier(etype: Type, tc: TypeChecker) { MatchError(pat.expr.range(), "duplicate case", covered[vc.tag].expr.range()); } covered[vc.tag] = pat; - pat.val = Int.box(vc.tag); + pat.val = Int.box(vc.enumTag); // use DFS global tag for runtime comparison } def addTypePattern(pat: MatchPattern, t: Type) { var cast = TypeSystem.newTypeCast(etype, t); @@ -2893,6 +3326,7 @@ class MatchStmtVerifier(etype: Type, tc: TypeChecker) { if (ct.classDecl.qualifiers != null) { // Subtype variant (e.g. x: B where type A.B). Generate a range // test via VARIANT_QUERY; coverage is satisfied by the _ default arm. + pat.subtypeType = ct; return; } // Named case: variantTag equals the local cases[] index after DFS @@ -2913,6 +3347,8 @@ class MatchStmtVerifier(etype: Type, tc: TypeChecker) { def end(stmt: MatchStmt) { if (covered == null) return; if (default != null) { + // Open enums always have potential uncovered values (from subtypes); default is always reachable. + if (enumType != null && enumType.enumDecl.hasDefault) return; for (p in covered) { if (p == null) return; } @@ -2930,15 +3366,23 @@ class MatchStmtVerifier(etype: Type, tc: TypeChecker) { } } } + // If the enum has "case _", the match must have a default _ case. + if (enumType != null && enumType.enumDecl.hasDefault) { + MatchError(stmt.range(), + Strings.format1("match on open enum type %q requires a default case", enumType.render), + null); + return; + } var buf: StringBuilder; for (i < covered.length) { if (covered[i] == null) { var t = if(variantType == null, enumType, variantType); if (buf == null) buf = StringBuilder.new().put1("match on type %q does not cover:", t.render); - var name = if(variantType == null, + // Skip the _ case — it cannot be explicitly matched. + var caseName = if(variantType == null, enumType.enumDecl.cases[i].name(), variantType.classDecl.cases[i].name()); - buf.puts(" ").puts(name); + if (!Strings.equal(caseName, "_")) buf.puts(" ").puts(caseName); } } if (buf != null) MatchError(stmt.range(), buf.toString(), null); diff --git a/aeneas/src/vst/Vst.v3 b/aeneas/src/vst/Vst.v3 index 4bdf81785..f15eff806 100644 --- a/aeneas/src/vst/Vst.v3 +++ b/aeneas/src/vst/Vst.v3 @@ -196,11 +196,20 @@ class VstComponent extends VstCompound { // Parsed "enum X { ... }" class VstEnum extends VstCompound { def cases: Array; - def tagType = V3.getTagType(cases.length); + var tagType: IntType; + var hasDefault: bool; // true if enum has a "case _" (open enum) + var superParams: bool; // true if enum declared (super) params + var extraParams: VstList; // subtype-only params (new fields beyond root's) + var parentEnum: VstEnum; // immediate parent in hierarchy (set by verifier) + var qualifiers: List; // dot-separated qualifier path, e.g. [E1] for "enum E1.More" + var subtypes: HashMap; // direct subtype enums (filled by verifier) + var subtypeOrder: List; // subtypes in reverse declaration order + var enumTagLo: int = -1; // -1 for root; first DFS tag for subtypes + var enumTagHi: int = -1; // last DFS tag covering all subtypes new(isPrivate: bool, name: Token, params: VstList, cases, members: List) super(isPrivate, name, null, params, null, members) { - // XXX: move type creation somewhere else? + tagType = V3.getTagType(cases.length); typeCon = TypeCon.new(token.image, Kind.ENUM, 0, TypeUtil.globalCache); var enumType = EnumType.new(typeCon.uid, this, typeCon); typeCon.singleton = enumType; @@ -239,6 +248,8 @@ class VstPacking extends VstCompound { class VstEnumCase extends VstMember { def tag: int; def args: VstList; + var enumTag: int = -1; // DFS-assigned global tag; -1 until assigned + var members: List; // per-case method overrides new(tag, name: Token, args) super(false, name) { } def getType() -> Type { return receiver.getDeclaredType(); @@ -404,6 +415,7 @@ class VstMethod extends VstMember { var typeEnv: TypeEnv; var root: VstMethod; var importName: Token; + var enumCaseIrs: Array; // per-case override IrMethods, indexed by enumTag new(isPrivate: bool, name: Token, typeParams, func) super(isPrivate, name) { } @@ -732,6 +744,8 @@ class MatchPattern(expr: Expr, params: VstList) { var variantCase: VstCaseMember; // if this is a match on a variant case var vdecl: VarDecl; // if this is a type case (including variant subtype patterns) var val: Val; + var subtypeType: Type; // set when pattern names a variant subtype (second grammar rule) + var subtypeEnumType: Type; // set when pattern names a subtype enum (e.g. E1.More) def decl() -> VstClass { return variantCase.decl; } } // continue; @@ -1076,6 +1090,8 @@ type VstOperator { case VariantGetName; // desugared to ArrayGetElem(#names, VariantGetTag(x)) + case EnumGetShortName; // desugared to ArrayGetElem(#shortNames, x) + case EnumGetParam(field: VstField); // desugared to ArrayGetElem(#field, x) case EnumSetHas(vcase: VstEnumCase); // desugared to (1 << k) & x != 0 diff --git a/aeneas/src/wasm/WasmCodeGen.v3 b/aeneas/src/wasm/WasmCodeGen.v3 index 0072197ee..bbe31b349 100644 --- a/aeneas/src/wasm/WasmCodeGen.v3 +++ b/aeneas/src/wasm/WasmCodeGen.v3 @@ -323,7 +323,7 @@ class WasmCodeGen extends SsaMachGen { dfnAll(i); } else { var rt = i.input1().getType(); - if (rt == m.receiver) { + if (rt == m.receiver || EnumType.?(m.receiver)) { start = 1; // just push the receiver as is dfnAll(i); } else { @@ -361,7 +361,7 @@ class WasmCodeGen extends SsaMachGen { dfnAll(i); } else { var rt = i.input1().getType(); - if (rt == m.receiver) { + if (rt == m.receiver || EnumType.?(m.receiver)) { start = 1; // just push the receiver as is dfnAll(i); } else { @@ -385,7 +385,6 @@ class WasmCodeGen extends SsaMachGen { var funcType = FuncType.!(i.op.typeArgs[0]); var sig = funcType.sig(); var sigIndex = wasmGc.addSig(Void.TYPE, sig); - dfnAll(i); if (useShadowStack) refmap(null); useAll(i.inputs, 1); // use arguments, except target @@ -1635,8 +1634,12 @@ class WasmCodeGen extends SsaMachGen { // just load all arguments except the first one and call the target. // WasmGc indirect adapters are also used for methods with receivers, // which must load and cast the first argument. + // Enum methods: no receiver to drop/cast; load all params directly. // ============ indirect adapter code ======================== - if (GC) { + var loopStart = 1; // default: skip first param (Oop/funcref receiver) + if (EnumType.?(m.receiver)) { + loopStart = 0; // enum: no receiver to skip + } else if (GC) { var rcvr = m.receiver; if (rcvr != Void.TYPE && !V3.isComponent(rcvr)) { // get the receiver and cast it @@ -1647,7 +1650,7 @@ class WasmCodeGen extends SsaMachGen { } } - for (i = 1; i < m.ssa.params.length; i++) { + for (i = loopStart; i < m.ssa.params.length; i++) { b(WasmOp.GET_LOCAL.opcode); w.put_sleb32(i); } b(WasmOp.CALL.opcode); @@ -1669,9 +1672,11 @@ class WasmCodeGen extends SsaMachGen { // arguments to the target function // ============ dispatch adapter code ======================== b(WasmOp.GET_LOCAL.opcode); w.put_sleb32(0); - w.put_b16be(WasmExtOp.REF_CAST_NULL.extopcode); - var wht = wasmGc.gcTypeTable.addHeapType(m.receiver); - w.put_sleb32(int.!(wht.index - 1)); + if (!EnumType.?(m.receiver)) { + w.put_b16be(WasmExtOp.REF_CAST_NULL.extopcode); + var wht = wasmGc.gcTypeTable.addHeapType(m.receiver); + w.put_sleb32(int.!(wht.index - 1)); + } for (i = 1; i < m.ssa.params.length; i++) { b(WasmOp.GET_LOCAL.opcode); w.put_sleb32(i); } diff --git a/aeneas/src/wasm/WasmGcTarget.v3 b/aeneas/src/wasm/WasmGcTarget.v3 index 53c710b37..3bc8f2068 100644 --- a/aeneas/src/wasm/WasmGcTarget.v3 +++ b/aeneas/src/wasm/WasmGcTarget.v3 @@ -351,8 +351,9 @@ class WasmGcTarget extends Target { wasm.indirectAdapterFor[meth] = findex; wasm.indirectAdapterIndexFor[meth] = i + 1; // + 1 to skip the null entry wasm.indirectAdapterVector.put(meth); - var sigIdx = wasm.addSig(Oop.TYPE, meth.sig); - var fi = FunctionInfo.new(FunctionInfoKind.Indirect, Oop.TYPE, meth.sig, sigIdx, meth, findex); + var adapterRecv = if(EnumType.?(meth.receiver), Void.TYPE, Oop.TYPE); + var sigIdx = wasm.addSig(adapterRecv, meth.sig); + var fi = FunctionInfo.new(FunctionInfoKind.Indirect, adapterRecv, meth.sig, sigIdx, meth, findex); table0Infos.put(fi); functionInfos.put(fi); if (WasmGcComponent.printSection.FUNCTION || @@ -410,8 +411,9 @@ class WasmGcTarget extends Target { wasm.requestIndirectAdapter(m); var findex = functionInfos.length; wasm.indirectAdapterFor[m] = findex; - var sigIdx = wasm.addSig(Oop.TYPE, m.sig); - var fi = FunctionInfo.new(FunctionInfoKind.Indirect, Oop.TYPE, m.sig, sigIdx, m, findex); + var adapterRecv = if(EnumType.?(m.receiver), Void.TYPE, Oop.TYPE); + var sigIdx = wasm.addSig(adapterRecv, m.sig); + var fi = FunctionInfo.new(FunctionInfoKind.Indirect, adapterRecv, m.sig, sigIdx, m, findex); table0Infos.put(fi); wasm.indirectAdapterIndexFor[m] = table0Infos.length; wasm.indirectAdapterVector.put(m); @@ -452,7 +454,8 @@ class WasmGcTarget extends Target { wasm.dispatchTableFor[mtable] = tableNum; var root = mtable.root; - var sigIdx = wasm.addSig(root.receiver, root.sig); // make sure signature is present + var dispRecv = if(EnumType.?(root.receiver), Void.TYPE, root.receiver); + var sigIdx = wasm.addSig(dispRecv, root.sig); // make sure signature is present var superIdx = root.machIndex; // superIdx *can* be -1 (e.g., for an ADT with an empty-body default for a method) var funcInfo: FunctionInfo = if(superIdx < 0, null, functionInfos[superIdx]); @@ -1315,7 +1318,8 @@ class WasmGcProgram extends WasmProgram { } // override def getSigIndex(m: IrMethod) -> u32 { - var sig = getSig(m.receiver, m.sig); + var recv = if(EnumType.?(m.receiver), Void.TYPE, m.receiver); + var sig = getSig(recv, m.sig); var wft = gcTypeTable.addSignature(sig); var index: u32 = if(wft == null, 0, wft.index); if (index == 0) context.fail1("method signature not found: %q", sig.funcType().render); @@ -1408,7 +1412,9 @@ class WasmGcProgram extends WasmProgram { return -1; } def addMethodGc(m: IrMethod, infos: Vector, imported: bool) { - var sigIdx = addSig(m.ssa.params[0].vtype, m.sig); + // For enum methods, the tag is already the first param in m.sig (no receiver prepend needed) + var recv = if(EnumType.?(m.receiver), Void.TYPE, m.ssa.params[0].vtype); + var sigIdx = addSig(recv, m.sig); m.machIndex = infos.length; var fi = FunctionInfo.new(if(imported, FunctionInfoKind.Import, FunctionInfoKind.Method), m.receiver, m.sig, sigIdx, m, infos.length); infos.put(fi); diff --git a/aeneas/src/wasm/WasmTarget.v3 b/aeneas/src/wasm/WasmTarget.v3 index 63f005f26..97521d505 100644 --- a/aeneas/src/wasm/WasmTarget.v3 +++ b/aeneas/src/wasm/WasmTarget.v3 @@ -522,7 +522,7 @@ class WasmProgram(mach: MachProgram, context: SsaContext, test: bool, wasmGc: bo } def getSigIndex(m: IrMethod) -> u32 { var sig = m.sig; - if (!V3.isComponent(m.receiver)) { + if (!V3.isComponent(m.receiver) && !EnumType.?(m.receiver)) { sig = Signature.new(null, Arrays.prepend(m.ssa.params[0].vtype, sig.paramTypes), sig.returnTypes); } return typeTable.add(sig); @@ -607,7 +607,8 @@ class WasmProgram(mach: MachProgram, context: SsaContext, test: bool, wasmGc: bo def setMethod(index: int, m: IrMethod, kind: WasmFuncKind) -> int { if (m != null) { m.machIndex = index; - addSig(m.ssa.params[0].vtype, m.sig); + var recv = if(EnumType.?(m.receiver), Void.TYPE, m.ssa.params[0].vtype); + addSig(recv, m.sig); } functions.set(index, (m, kind)); functionCursor = functions.length; diff --git a/doc/grammar-claude.md b/doc/grammar-claude.md index 6e6af6471..949dc30da 100644 --- a/doc/grammar-claude.md +++ b/doc/grammar-claude.md @@ -76,9 +76,77 @@ In match patterns, unqualified subtype names (`Bar =>` or `b: Bar =>`) automatic ``` -EnumDecl ::= id EnumParams? '{' EnumCase* '}' -EnumCase ::= id ['(' Expr,* ')'] ','? +EnumDecl ::= DottedId EnumParams? '{' EnumCase* (';' EnumMethod*)? '}' +EnumCase ::= id ['(' Expr,* ')'] EnumCaseBody? ','? // named case + | '_' EnumCaseBody? ','? // default case: optional, must be last, at most one +EnumCaseBody ::= '{' EnumCaseMethod* '}' +EnumCaseMethod ::= ['private'] 'def' DefDef +EnumMethod ::= ['private'] 'def' DefDef // shared by all cases; may be overridden per-case + +EnumParams ::= '(' 'super' ')' // inherit parent's params + | '(' 'super' ',' ParamDecl,+ ')' // inherit parent's params + add new fields + | '(' ParamDecl,* ')' // declare params (root or restate + optional extras) + +``` + +### Enum subtype constraints (checked by verifier) + +An `EnumDecl` whose `DottedId` is a plain `id` is a **root** (top-level) enum. + +An `EnumDecl` whose `DottedId` has the form `D.T` (one or more dots) declares a **subtype enum**: + +- The first identifier in `D` must name a root enum. +- Every intermediate identifier in `D` must name an enum that is a direct subtype of the previous one (transitively established by prior declarations). +- The immediate parent (the enum named by all of `D`) must have a `case _`. +- `T` must not clash with any named `case id` of the immediate parent. +- `D.T` may be declared at most once (among all files of the program). + +### Enum subtype parameter rules + +A subtype's **effective parameters** are its parent's effective parameters plus any extra parameters the subtype itself declares. The root enum's effective parameters are simply its own declared parameters. + +Subtypes may reference their parent's effective parameters in several ways: + +- **Form 1 (restate)**: `enum E.S(x: int)` — restates the parent's effective params by name and type. May also add extra params: `enum E.S(x: int, f: float)`. +- **Form 2 (super)**: `enum E.S(super)` — inherits the parent's effective params without restating them. +- **Form 2+add (super + extras)**: `enum E.S(super, f: float)` — inherits parent's effective params and adds new fields. +- **Form 3 (implicit)**: `enum E.S` — no param list; parent's effective params are inherited implicitly. + +In all forms, each case must provide argument values for all effective parameters (parent's effective params + own extra params, in order). + +In multi-level hierarchies, `super` refers to the immediate parent's effective parameters, which includes the root's parameters and all intermediate ancestors' extra parameters. For example, given `enum E(x: int)` and `enum E.S(super, y: int)`, a grandchild `enum E.S.T(super, z: int)` has effective parameters `(x, y, z)` and each case must provide all three values. + +Subtypes may **not** declare params if the parent has no effective params. Using `super` when the parent has no effective params is an error. + +### Enum methods + +An `EnumMethod` declared after the `;` separator is shared by all cases. It may be overridden per-case by placing a `def` with the same name and signature inside an `EnumCaseBody`. + +- An override must have the same parameter types and return type as the root method. +- Dispatch on a variable of enum type is virtual: the override for the specific case is called at runtime. +- Static dispatch on a known case (e.g. `E.A.m()`) calls the override directly if one exists. + +### Enum method inheritance + +- Methods declared on a parent enum are inherited by all subtype enums (transitively). +- A subtype enum may override an inherited method by declaring a method with the same name and signature after its own `;` separator. +- Individual cases of a subtype may further override methods via `EnumCaseBody`. +- All virtual dispatch goes through the root enum's dispatch table, regardless of where the override is declared. + + +### Enum match pattern semantics + +When matching on an expression of enum type `E`, a match pattern may name: + +- A **named case** of `E` (e.g. `X` where `E` has `case X`) — matched by tag +- A **subtype enum** of `E` (e.g. `S` where `E.S` is a subtype) — matched by tag range + +Only the **unqualified** name is legal in a match pattern. For example, if `E.S` is a subtype of `E`, write `S`, not `E.S`. + +A match on an enum type `E` that has `case _` must always include a `_` arm regardless of which named cases or subtypes are listed. + +``` PackingDecl ::= id '(' PackingParam,* ')' ':' int '=' PackingExpr ';' PackingParam ::= id ':' int PackingExpr ::= BitPattern // 0b... @@ -135,7 +203,8 @@ TypeParam ::= id ClassParams ::= '(' ParamDecl,* ')' // class constructor params (typed, def read-only) MethodParams ::= '(' ParamDecl,* ')' // method params (optionally typed) VariantCaseParams ::= '(' ParamDecl,* ')' // like enum params (typed, def read-only) -EnumParams ::= '(' ParamDecl,* ')' // typed, immutable +EnumParams ::= '(' 'super' [',' ParamDecl,+] ')' // super form (with optional extras) + | '(' ParamDecl,* ')' // typed, immutable ParamDecl ::= ['var'] id [':' TypeRef] ``` diff --git a/doc/tutorial/EnumMethods.md b/doc/tutorial/EnumMethods.md new file mode 100644 index 000000000..5841f519c --- /dev/null +++ b/doc/tutorial/EnumMethods.md @@ -0,0 +1,73 @@ +# Enum Methods + +Enums can have methods, declared after a `;` separator following the cases. +Methods are shared by all cases of the enum. + +``` +enum Planet(mass: double, radius: double) { + MERCURY(3.303e+23, 2.4397e6), + VENUS(4.869e+24, 6.0518e6), + EARTH(5.976e+24, 6.37814e6); + + def surfaceGravity() -> double { + return 6.67300E-11 * mass / (radius * radius); + } +} +var g = Planet.EARTH.surfaceGravity(); +``` + +## Per-case method overrides + +Individual cases can override an enum method by providing their own definition inside `{ }` braces. +When calling the method on a variable of the enum type, the correct override is dispatched at runtime. + +``` +enum Expr { + ADD { def eval(a: int, b: int) -> int { return a + b; } }, + SUB { def eval(a: int, b: int) -> int { return a - b; } }, + MUL { def eval(a: int, b: int) -> int { return a * b; } }; + + def eval(a: int, b: int) -> int { return 0; } // default +} +def compute(op: Expr, x: int, y: int) -> int { + return op.eval(x, y); // virtual dispatch: calls the override for the specific case +} +``` + +The `_` (default) case can also have method overrides, which apply to any value that doesn't have a more specific override. + +## Enum method closures + +Enum methods can be used as closures, just like methods on classes or variants. +The closure captures the enum value and dispatches correctly when called. + +``` +enum Op { + INC { def apply(x: int) -> int { return x + 1; } }, + DEC { def apply(x: int) -> int { return x - 1; } }; + + def apply(x: int) -> int { return x; } +} +def transform(op: Op, value: int) -> int { + var f = op.apply; // create a closure + return f(value); // calls the correct override +} +``` + +## Subtype enum method inheritance + +Subtype enums inherit methods from their parent. +A subtype can override an inherited method, and individual cases of a subtype can override it further. + +``` +enum Animal { DOG, CAT, _; def speak() -> int { return 0; } } +enum Animal.Exotic { + PARROT { def speak() -> int { return 2; } }, + SNAKE; + + def speak() -> int { return 1; } // override for all Exotic cases +} +``` + +Here `Animal.Exotic.PARROT.speak()` returns `2` (per-case override), `Animal.Exotic.SNAKE.speak()` returns `1` (subtype override), and `Animal.DOG.speak()` returns `0` (root default). +All dispatch goes through the root enum's dispatch table, so a variable of type `Animal` will dispatch correctly regardless of whether the value is a root case or a subtype case. diff --git a/doc/tutorial/EnumSets.md b/doc/tutorial/EnumSets.md index 359d964f9..f44f0d00f 100644 --- a/doc/tutorial/EnumSets.md +++ b/doc/tutorial/EnumSets.md @@ -134,7 +134,22 @@ var x: E; var y = foo(x); // E value is automatically promoted to E.set ``` +## Enum sets for open enums + +Open enums (those with `case _`) and their subtype enums share a single set type across the entire hierarchy. +The set type is sized to accommodate all cases in the hierarchy, including those from subtypes. + +``` +enum Color { RED, GREEN, BLUE, _ } +enum Color.Pastel { PINK, LAVENDER, MINT } + +var warm: Color.set = Color.RED | Color.Pastel.PINK; // mix root and subtype cases +var isPink = warm.PINK; // check for subtype case membership +``` + +Subtype enum values promote to the root's set type, so all set operations work uniformly across the hierarchy. + ## Implementation limits -In the current implementation of Virgil in this repository, enum set types are available for all enum declarations with 64 or fewer values. +In the current implementation of Virgil in this repository, enum set types are available for all enum declarations with 64 or fewer values (including all cases across the hierarchy for open enums). This limitation is due to a simple approach of rewriting these operations to integer operations early in compilation and will be lifted in the future by tuples of as many integers as necessary. diff --git a/doc/tutorial/Enums.md b/doc/tutorial/Enums.md index 89d5bcdff..fef22e489 100644 --- a/doc/tutorial/Enums.md +++ b/doc/tutorial/Enums.md @@ -101,3 +101,140 @@ In fact, in this example, we also subsumed the `isWorkday()` method we wrote by The Virgil compiler will still represent the enum value as a small integer under the hood, and field access will be a simple array access that uses the enum value as the index. This will typically be a single machine instruction; it's hard to beat that in terms of efficiency! + +## Open enums + +An enum may include a `case _` to mark it as *open*, or *extensible*. +An open enum accepts values from **subtype enums** (declared elsewhere) in addition to its own named cases. + +``` +enum Color { RED, GREEN, BLUE, _ } +``` + +The `case _` must be the last case in the enum and there can be at most one per enum. + +## Subtype enums + +A *subtype enum* is declared with a dotted name `E.S`, making it an extension of an existing open enum. +The parent must have a `case _`. + +``` +enum Color { RED, GREEN, BLUE, _ } +enum Color.Pastel { PINK, LAVENDER, MINT } +``` + +Now `Color.Pastel.PINK`, `Color.Pastel.LAVENDER`, and `Color.Pastel.MINT` are valid `Color` values. +A variable of type `Color` can hold any case, including those from subtype enums. + +``` +var c: Color = Color.Pastel.PINK; // valid: Pastel is a subtype of Color +``` + +Subtype enums can themselves be open (with `case _`) and have their own subtypes, forming hierarchies of arbitrary depth. + +``` +enum Color { RED, GREEN, BLUE, _ } +enum Color.Pastel { PINK, LAVENDER, _ } +enum Color.Pastel.Spring { CORAL, PEACH } +``` + +## Matching open enums + +When matching a value of an open enum type, subtype enums may be named directly as match arms. +A match on an open enum **always** requires a `_` arm, since new subtypes may be added independently. + +``` +enum Color { RED, GREEN, BLUE, _ } +enum Color.Pastel { PINK, LAVENDER, MINT } +def describe(c: Color) -> int { + match (c) { + RED => return 0; + GREEN => return 1; + BLUE => return 2; + Pastel => return 3; // matches any Color.Pastel case + _ => return -1; // required: covers any other subtype + } +} +``` + +Subtype names are written *unqualified* in match patterns -- `Pastel` rather than `Color.Pastel`. + +## Open enums with fields + +Open enums can have fields, just like regular enums. +Subtype enums inherit the parent's fields, and each case must provide values for them. + +There are several ways to declare a subtype's relationship to the parent's fields: + +``` +enum Shape(sides: int) { TRIANGLE(3), SQUARE(4), _ } + +// Form 1: restate the parent's parameters +enum Shape.Round(sides: int) { CIRCLE(0) } + +// Form 2: use the 'super' keyword +enum Shape.Polygon(super) { PENTAGON(5), HEXAGON(6) } + +// Form 3: implicit inheritance (no parameter list) +enum Shape.Special { STAR(10) } +``` + +In all forms, each case must provide argument values for all of the parent's effective fields. + +## Subtypes with additional fields + +Subtypes can also declare **new fields** beyond the parent's. +There are two ways to do this: + +``` +enum Vehicle(wheels: int) { CAR(4), BIKE(2), _ } + +// Using 'super' + extra fields +enum Vehicle.Electric(super, range: int) { TESLA(4, 300), EBIKE(2, 50) } + +// Restating parent fields + extra fields +enum Vehicle.Flying(wheels: int, altitude: int) { HELICOPTER(0, 5000) } +``` + +The parent's fields are always first in the argument list, followed by any extra fields. + +New fields are accessible only on values typed as the subtype: + +``` +var v: Vehicle = Vehicle.Electric.TESLA; +var w = v.wheels; // 4: inherited field, accessible on Vehicle + +var e: Vehicle.Electric = Vehicle.Electric.TESLA; +var r = e.range; // 300: new field, only accessible on Vehicle.Electric +``` + +## Multi-level field inheritance + +Extra fields are inherited through the hierarchy. +In a multi-level hierarchy, `super` refers to the immediate parent's *effective* fields -- the root's fields plus all intermediate ancestors' extra fields. + +``` +enum Animal(legs: int) { DOG(4), BIRD(2), _ } +enum Animal.Pet(super, name: string) { CAT(4, "cat"), _ } +enum Animal.Pet.Exotic(super, origin: string) { PARROT(2, "parrot", "Brazil") } +``` + +Here `Animal.Pet.Exotic` has three effective fields: `legs` (from root), `name` (from `Animal.Pet`), and `origin` (its own). +Each case must provide values for all three, in order. + +All inherited fields are accessible on subtype-typed values: + +``` +var p: Animal.Pet.Exotic = Animal.Pet.Exotic.PARROT; +var l = p.legs; // 2: from root Animal +var n = p.name; // "parrot": from intermediate Animal.Pet +var o = p.origin; // "Brazil": own field +``` + +## The `name` and `shortName` fields + +For subtype enum cases, the `name` field includes the subtype path. +For example, `Color.Pastel.PINK.name` returns `"Pastel.PINK"`. +The `shortName` field gives only the case name without the subtype prefix: `Color.Pastel.PINK.shortName` returns `"PINK"`. + +Enums can also have [methods](EnumMethods.md), including per-case overrides with virtual dispatch. diff --git a/doc/virgil-grammar.ebnf b/doc/virgil-grammar.ebnf index ba624ca81..fc1e18a1c 100644 --- a/doc/virgil-grammar.ebnf +++ b/doc/virgil-grammar.ebnf @@ -4,7 +4,8 @@ ToplevelDecl ::= ClassDecl | ComponentDecl | VariantDecl | LayoutDecl | EnumDecl ClassDecl ::= "class" IdentParam ( "(" VarParamDecls? ")" )? ( "extends" TypeRef TupleExpr? )? RepHints "{" Member* "}" ComponentDecl ::= "import"? "component" IDENTIFIER "{" Member* "}" VariantDecl ::= "type" ( IdentParam "." )* IdentParam ( "(" ParamDecls? ")" )? RepHints ( ";" | "{" VariantMember* "}" ) -EnumDecl ::= "enum" IDENTIFIER ( "(" ParamDecls? ")" )? "{" EnumCases? "}" +EnumDecl ::= "enum" ( IDENTIFIER "." )* IDENTIFIER EnumParams? "{" EnumCases? "}" +EnumParams ::= "(" "super" ")" | "(" "super" "," ParamDecls ")" | "(" ParamDecls? ")" LayoutDecl ::= "layout" IDENTIFIER RepHints "{" LayoutField* LayoutSize "}" LayoutField ::= "+" INTEGER IDENTIFIER ":" TypeRef RepHints ";" LayoutSize ::= "=" INTEGER ";" @@ -17,7 +18,7 @@ NewMember ::= "private"? "new" "(" NewParamDecls? ")" ( ":"? "super" TupleExpr DefMethod ::= "private"? "def" (IndexMethod | Method) VariantCase ::= "case" IDENTIFIER ( "(" ParamDecls? ")" )? RepHints ( ";" | "{" DefMethod* "}" ) | "case" "_" RepHints ( ";" | "{" DefMethod* "}" ) -EnumCase ::= IDENTIFIER ( "(" ( Expr ( "," Expr )* )? ")" )? +EnumCase ::= IDENTIFIER ( "(" ( Expr ( "," Expr )* )? ")" )? | "_" EnumCases ::= EnumCase ( "," EnumCase )* VarParamDecl ::= "var"? IDENTIFIER ":" TypeRef diff --git a/test/enums/enum_closure00.v3 b/test/enums/enum_closure00.v3 new file mode 100644 index 000000000..cfe7460f7 --- /dev/null +++ b/test/enums/enum_closure00.v3 @@ -0,0 +1,8 @@ +//@execute 0=42; 1=42 +// Basic enum method closure (non-overridden) +enum E { A, B; def m() -> int { return 42; } } +def main(i: int) -> int { + var e: E = if(i == 0, E.A, E.B); + var f = e.m; + return f(); +} diff --git a/test/enums/enum_closure01.v3 b/test/enums/enum_closure01.v3 new file mode 100644 index 000000000..be2a301c1 --- /dev/null +++ b/test/enums/enum_closure01.v3 @@ -0,0 +1,8 @@ +//@execute 0=1; 1=0; 2=2 +// Overridden method closure with virtual dispatch +enum E { A { def m() -> int { return 1; } }, B, C { def m() -> int { return 2; } }; def m() -> int { return 0; } } +def main(i: int) -> int { + var e: E = if(i == 0, E.A, if(i == 1, E.B, E.C)); + var f = e.m; + return f(); +} diff --git a/test/enums/enum_cmethod00.v3 b/test/enums/enum_cmethod00.v3 new file mode 100644 index 000000000..b3f2459b4 --- /dev/null +++ b/test/enums/enum_cmethod00.v3 @@ -0,0 +1,11 @@ +//@execute 0=1; 1=0; 2=0 +// Per-case override returns different value +enum E { A { def m() -> int { return 1; } }, B, C; def m() -> int { return 0; } } +def main(i: int) -> int { + match (i) { + 0 => return E.A.m(); + 1 => return E.B.m(); + 2 => return E.C.m(); + } + return -1; +} diff --git a/test/enums/enum_cmethod01.v3 b/test/enums/enum_cmethod01.v3 new file mode 100644 index 000000000..e12df79bc --- /dev/null +++ b/test/enums/enum_cmethod01.v3 @@ -0,0 +1,14 @@ +//@execute 0=1; 1=0; 2=2 +// Virtual dispatch through variable +enum E { A { def m() -> int { return 1; } }, B, C { def m() -> int { return 2; } }; def m() -> int { return 0; } } +def run(e: E) -> int { + return e.m(); +} +def main(i: int) -> int { + match (i) { + 0 => return run(E.A); + 1 => return run(E.B); + 2 => return run(E.C); + } + return -1; +} diff --git a/test/enums/enum_cmethod02.v3 b/test/enums/enum_cmethod02.v3 new file mode 100644 index 000000000..801a124c6 --- /dev/null +++ b/test/enums/enum_cmethod02.v3 @@ -0,0 +1,18 @@ +//@execute 0=10; 1=20; 2=42; 3=7 +// Multiple methods, some overridden +enum E { + A { def m() -> int { return 10; } }, + B { def n() -> int { return 20; } }, + C; + def m() -> int { return 42; } + def n() -> int { return 7; } +} +def main(i: int) -> int { + match (i) { + 0 => return E.A.m(); + 1 => return E.B.n(); + 2 => return E.C.m(); + 3 => return E.C.n(); + } + return -1; +} diff --git a/test/enums/enum_cmethod03.v3 b/test/enums/enum_cmethod03.v3 new file mode 100644 index 000000000..301d659f8 --- /dev/null +++ b/test/enums/enum_cmethod03.v3 @@ -0,0 +1,14 @@ +//@execute 0=11; 1=20 +// Override accesses enum fields +enum E(x: int) { + A(10) { def m() -> int { return x + 1; } }, + B(20); + def m() -> int { return x; } +} +def main(i: int) -> int { + match (i) { + 0 => return E.A.m(); + 1 => return E.B.m(); + } + return -1; +} diff --git a/test/enums/enum_cmethod04.v3 b/test/enums/enum_cmethod04.v3 new file mode 100644 index 000000000..faeea502b --- /dev/null +++ b/test/enums/enum_cmethod04.v3 @@ -0,0 +1,18 @@ +//@execute 0=10; 1=0; 2=20; 3=0 +// Virtual dispatch with multiple methods +enum E { + A { def m() -> int { return 10; } }, + B, + C { def m() -> int { return 20; } }; + def m() -> int { return 0; } +} +def callM(e: E) -> int { return e.m(); } +def main(i: int) -> int { + match (i) { + 0 => return callM(E.A); + 1 => return callM(E.B); + 2 => return callM(E.C); + 3 => { var e: E = E.B; return e.m(); } + } + return -1; +} diff --git a/test/enums/enum_cmethod05.v3 b/test/enums/enum_cmethod05.v3 new file mode 100644 index 000000000..b11283ad6 --- /dev/null +++ b/test/enums/enum_cmethod05.v3 @@ -0,0 +1,20 @@ +//@execute 0=1; 1=0; 2=2; 3=0; 4=1; 5=0; 6=2 +// Static dispatch: E.A.m() directly calls override (no dispatch table) +enum E { + A { def m() -> int { return 1; } }, + B, + C { def m() -> int { return 2; } }; + def m() -> int { return 0; } +} +def main(i: int) -> int { + match (i) { + 0 => return E.A.m(); + 1 => return E.B.m(); + 2 => return E.C.m(); + 3 => { var e: E = E.B; return e.m(); } + 4 => { var e: E = E.A; return e.m(); } + 5 => { var b = E.B; return b.m(); } + 6 => { var c = E.C; return c.m(); } + } + return -1; +} diff --git a/test/enums/enum_cmethod06.v3 b/test/enums/enum_cmethod06.v3 new file mode 100644 index 000000000..9e6fb7845 --- /dev/null +++ b/test/enums/enum_cmethod06.v3 @@ -0,0 +1,13 @@ +//@execute 0=10; 1=10; 2=10; 3=10 +// Subtype enum cases inherit parent's method +enum E { A, B, _; def m() -> int { return 10; } } +enum E.More { C, D } +def main(i: int) -> int { + match (i) { + 0 => return E.A.m(); + 1 => return E.B.m(); + 2 => return E.More.C.m(); + 3 => return E.More.D.m(); + } + return -1; +} diff --git a/test/enums/enum_cmethod07.v3 b/test/enums/enum_cmethod07.v3 new file mode 100644 index 000000000..82b4003e0 --- /dev/null +++ b/test/enums/enum_cmethod07.v3 @@ -0,0 +1,13 @@ +//@execute 0=0; 1=0; 2=1; 3=1 +// Subtype enum overrides parent method for all its cases +enum E { A, B, _; def m() -> int { return 0; } } +enum E.More { C, D; def m() -> int { return 1; } } +def main(i: int) -> int { + match (i) { + 0 => return E.A.m(); + 1 => return E.B.m(); + 2 => return E.More.C.m(); + 3 => return E.More.D.m(); + } + return -1; +} diff --git a/test/enums/enum_cmethod08.v3 b/test/enums/enum_cmethod08.v3 new file mode 100644 index 000000000..409071444 --- /dev/null +++ b/test/enums/enum_cmethod08.v3 @@ -0,0 +1,13 @@ +//@execute 0=0; 1=0; 2=2; 3=1 +// Case override in subtype + subtype-level override +enum E { A, B, _; def m() -> int { return 0; } } +enum E.More { C { def m() -> int { return 2; } }, D; def m() -> int { return 1; } } +def main(i: int) -> int { + match (i) { + 0 => return E.A.m(); + 1 => return E.B.m(); + 2 => return E.More.C.m(); + 3 => return E.More.D.m(); + } + return -1; +} diff --git a/test/enums/enum_cmethod09.v3 b/test/enums/enum_cmethod09.v3 new file mode 100644 index 000000000..0e883ea7d --- /dev/null +++ b/test/enums/enum_cmethod09.v3 @@ -0,0 +1,14 @@ +//@execute 0=0; 1=0; 2=2; 3=1 +// Virtual dispatch through parent type +enum E { A, B, _; def m() -> int { return 0; } } +enum E.More { C { def m() -> int { return 2; } }, D; def m() -> int { return 1; } } +def call(e: E) -> int { return e.m(); } +def main(i: int) -> int { + match (i) { + 0 => return call(E.A); + 1 => return call(E.B); + 2 => return call(E.More.C); + 3 => return call(E.More.D); + } + return -1; +} diff --git a/test/enums/enum_method00.v3 b/test/enums/enum_method00.v3 new file mode 100644 index 000000000..a11a7c3ce --- /dev/null +++ b/test/enums/enum_method00.v3 @@ -0,0 +1,11 @@ +//@execute 0=42; 1=7; 2=99 +// Basic enum-level methods: all cases share the same implementation +enum E { A, B, C; def m() -> int { return 42; } def n() -> int { return 7; } } +def main(i: int) -> int { + match (i) { + 0 => return E.A.m(); + 1 => return E.B.n(); + 2 => { var e: E = E.C; return e.m() + e.n() + 50; } + } + return -1; +} diff --git a/test/enums/enum_method01.v3 b/test/enums/enum_method01.v3 new file mode 100644 index 000000000..93c89fcf3 --- /dev/null +++ b/test/enums/enum_method01.v3 @@ -0,0 +1,11 @@ +//@execute 0=1; 1=17; 2=3 +// Enum method accessing fields +enum E(x: int) { A(1), B(17), C(3); def getX() -> int { return x; } } +def main(i: int) -> int { + match (i) { + 0 => return E.A.getX(); + 1 => return E.B.getX(); + 2 => { var e: E = E.C; return e.getX(); } + } + return -1; +} diff --git a/test/enums/enum_method02.v3 b/test/enums/enum_method02.v3 new file mode 100644 index 000000000..eac0eb445 --- /dev/null +++ b/test/enums/enum_method02.v3 @@ -0,0 +1,11 @@ +//@execute 0=0; 1=1; 2=2 +// Enum method using tag field +enum E { A, B, C; def id() -> int { return tag; } } +def main(i: int) -> int { + match (i) { + 0 => return E.A.id(); + 1 => return E.B.id(); + 2 => return E.C.id(); + } + return -1; +} diff --git a/test/enums/enum_method03.v3 b/test/enums/enum_method03.v3 new file mode 100644 index 000000000..403d9a627 --- /dev/null +++ b/test/enums/enum_method03.v3 @@ -0,0 +1,11 @@ +//@execute 0=10; 1=20; 2=30 +// Enum method with parameters +enum E { A, B, C; def mul(x: int) -> int { return (tag + 1) * x; } } +def main(i: int) -> int { + match (i) { + 0 => return E.A.mul(10); + 1 => return E.B.mul(10); + 2 => return E.C.mul(10); + } + return -1; +} diff --git a/test/enums/enum_method04.v3 b/test/enums/enum_method04.v3 new file mode 100644 index 000000000..929e5d1aa --- /dev/null +++ b/test/enums/enum_method04.v3 @@ -0,0 +1,12 @@ +//@execute 0=42; 1=42; 2=42 +// Open enum: subtypes inherit root method +enum E { A, B, _; def m() -> int { return 42; } } +enum E.More { C, D } +def main(i: int) -> int { + match (i) { + 0 => return E.A.m(); + 1 => return E.More.C.m(); + 2 => { var e: E = E.More.D; return e.m(); } + } + return -1; +} diff --git a/test/enums/enum_method05.v3 b/test/enums/enum_method05.v3 new file mode 100644 index 000000000..f456c0176 --- /dev/null +++ b/test/enums/enum_method05.v3 @@ -0,0 +1,13 @@ +//@execute 0=5; 1=99 +// Enum with private method +enum E { A, B; + def getVal() -> int { return helper(); } + private def helper() -> int { return tag + 5; } +} +def main(i: int) -> int { + match (i) { + 0 => return E.A.getVal(); + 1 => { var e: E = E.B; return e.getVal() + 93; } + } + return -1; +} diff --git a/test/enums/enum_method06.v3 b/test/enums/enum_method06.v3 new file mode 100644 index 000000000..a6cb43c5a --- /dev/null +++ b/test/enums/enum_method06.v3 @@ -0,0 +1,11 @@ +//@execute 0=11; 1=22; 2=33 +// Enum method with multiple fields and expression body +enum E(x: int, y: int) { A(1, 10), B(2, 20), C(3, 30); def sum() => x + y; } +def main(i: int) -> int { + match (i) { + 0 => return E.A.sum(); + 1 => return E.B.sum(); + 2 => return E.C.sum(); + } + return -1; +} diff --git a/test/enums/open_enum00.v3 b/test/enums/open_enum00.v3 new file mode 100644 index 000000000..631c33a85 --- /dev/null +++ b/test/enums/open_enum00.v3 @@ -0,0 +1,14 @@ +//@execute 0=1; 1=2; 2=0; 3=0 +// Open enum: basic match with default arm catches subtype cases +enum E { A, B, _ } +enum E.More { C, D } +def main(x: int) -> int { + var cases: Array = [E.A, E.B, E.More.C, E.More.D]; + if (x < 0 || x >= cases.length) return 0; + var e: E = cases[x]; + match (e) { + A => return 1; + B => return 2; + _ => return 0; + } +} diff --git a/test/enums/open_enum01.v3 b/test/enums/open_enum01.v3 new file mode 100644 index 000000000..21f23440d --- /dev/null +++ b/test/enums/open_enum01.v3 @@ -0,0 +1,17 @@ +//@execute 0=1; 1=2; 2=3; 3=3; 4=0 +// Subtype enum: match on subtype name in parent match +enum E1 { A, B, _ } +enum E1.More { C, D } +def dispatch(e: E1) -> int { + match (e) { + A => return 1; + B => return 2; + More => return 3; // range test: is e in More's tag range? + _ => return 0; + } +} +def main(x: int) -> int { + var cases: Array = [E1.A, E1.B, E1.More.C, E1.More.D]; + if (x < 0 || x >= cases.length) return 0; + return dispatch(cases[x]); +} diff --git a/test/enums/open_enum02.v3 b/test/enums/open_enum02.v3 new file mode 100644 index 000000000..f3ffcb455 --- /dev/null +++ b/test/enums/open_enum02.v3 @@ -0,0 +1,10 @@ +//@execute 0=0; 1=0; 2=1; 3=1; 4=0 +// Subtype enum: type query E1.More.?(e) +enum E1 { A, B, _ } +enum E1.More { C, D } +def main(x: int) -> int { + var cases: Array = [E1.A, E1.B, E1.More.C, E1.More.D]; + if (x < 0 || x >= cases.length) return 0; + var e = cases[x]; + return if(E1.More.?(e), 1, 0); +} diff --git a/test/enums/open_enum03.v3 b/test/enums/open_enum03.v3 new file mode 100644 index 000000000..08fcdf577 --- /dev/null +++ b/test/enums/open_enum03.v3 @@ -0,0 +1,11 @@ +//@execute 0=65; 1=66; 2=77; 3=77; 4=0 +// Subtype enum: .name returns qualified names (e.g. "More.C" for subtype cases) +enum E1 { A, B, _ } +enum E1.More { C, D } +def main(x: int) -> int { + var cases: Array = [E1.A, E1.B, E1.More.C, E1.More.D]; + if (x < 0 || x >= cases.length) return 0; + var e = cases[x]; + var n = e.name; // "A", "B", "More.C", "More.D" + return if(n.length > 0, n[0], 0); +} diff --git a/test/enums/open_enum04.v3 b/test/enums/open_enum04.v3 new file mode 100644 index 000000000..4536a9b3c --- /dev/null +++ b/test/enums/open_enum04.v3 @@ -0,0 +1,11 @@ +//@execute 0=65; 1=66; 2=67; 3=68; 4=0 +// Subtype enum: .shortName returns unqualified case names +enum E1 { A, B, _ } +enum E1.More { C, D } +def main(x: int) -> int { + var cases: Array = [E1.A, E1.B, E1.More.C, E1.More.D]; + if (x < 0 || x >= cases.length) return 0; + var e = cases[x]; + var n = e.shortName; // "A", "B", "C", "D" + return if(n.length > 0, n[0], 0); +} diff --git a/test/enums/open_enum05.v3 b/test/enums/open_enum05.v3 new file mode 100644 index 000000000..175645ec6 --- /dev/null +++ b/test/enums/open_enum05.v3 @@ -0,0 +1,9 @@ +//@execute 0=0; 1=1; 2=2; 3=3; 4=0 +// Subtype enum: .tag returns DFS-assigned global tag +enum E1 { A, B, _ } +enum E1.More { C, D } +def main(x: int) -> int { + var cases: Array = [E1.A, E1.B, E1.More.C, E1.More.D]; + if (x < 0 || x >= cases.length) return 0; + return cases[x].tag; // 0, 1, 2, 3 +} diff --git a/test/enums/open_enum06.v3 b/test/enums/open_enum06.v3 new file mode 100644 index 000000000..02fae9b3b --- /dev/null +++ b/test/enums/open_enum06.v3 @@ -0,0 +1,10 @@ +//@execute 0=10; 1=20; 2=30; 3=40; 4=0 +// Open enum: subtype cases must provide params when root has params +enum E1(x: int) { A(10), B(20), _ } +enum E1.More { C(30), D(40) } +def main(x: int) -> int { + var cases: Array = [E1.A, E1.B, E1.More.C, E1.More.D]; + if (x < 0 || x >= cases.length) return 0; + var e: E1 = cases[x]; + return e.x; +} diff --git a/test/enums/open_enum07.v3 b/test/enums/open_enum07.v3 new file mode 100644 index 000000000..f8d364527 --- /dev/null +++ b/test/enums/open_enum07.v3 @@ -0,0 +1,16 @@ +//@execute 0=1; 1=2; 2=10; 3=10; 4=20; 5=0 +// Open enum: multiple sibling subtype enums, match arm for each +enum E { A, B, _ } +enum E.Nums { N1, N2 } +enum E.Tens { T1 } +def main(x: int) -> int { + var cases: Array = [E.A, E.B, E.Nums.N1, E.Nums.N2, E.Tens.T1]; + if (x < 0 || x >= cases.length) return 0; + match (cases[x]) { + A => return 1; + B => return 2; + Nums => return 10; + Tens => return 20; + _ => return 0; + } +} diff --git a/test/enums/open_enum08.v3 b/test/enums/open_enum08.v3 new file mode 100644 index 000000000..c09b5feee --- /dev/null +++ b/test/enums/open_enum08.v3 @@ -0,0 +1,11 @@ +//@execute 0=2; 1=3; 2=0 +// Open enum: variable declared as subtype enum type, .tag returns DFS global tag +enum E1 { A, B, _ } +enum E1.More { C, D } +def main(x: int) -> int { + var e: E1.More; + if (x == 0) e = E1.More.C; + else if (x == 1) e = E1.More.D; + else return 0; + return e.tag; // C=2, D=3 +} diff --git a/test/enums/open_enum09.v3 b/test/enums/open_enum09.v3 new file mode 100644 index 000000000..8de91c53e --- /dev/null +++ b/test/enums/open_enum09.v3 @@ -0,0 +1,12 @@ +//@execute 0=0; 1=1; 2=1; 3=0 +// Open enum: widening in function argument (E1.More -> E1), type query in called fn +enum E1 { A, _ } +enum E1.More { B, C } +def isMore(e: E1) -> int { + return if(E1.More.?(e), 1, 0); +} +def main(x: int) -> int { + var cases: Array = [E1.A, E1.More.B, E1.More.C]; + if (x < 0 || x >= cases.length) return 0; + return isMore(cases[x]); +} diff --git a/test/enums/open_enum10.v3 b/test/enums/open_enum10.v3 new file mode 100644 index 000000000..997967542 --- /dev/null +++ b/test/enums/open_enum10.v3 @@ -0,0 +1,19 @@ +//@execute 0=1; 1=2; 2=10; 3=20; 4=0 +// Open enum: multi-level hierarchy (E.More has _ allowing E.More.Extra) +// DFS tags: A=0, B=1, More.C=2, More.Extra.D=3 +enum E { A, B, _ } +enum E.More { C, _ } +enum E.More.Extra { D } +def main(x: int) -> int { + var cases: Array = [E.A, E.B, E.More.C, E.More.Extra.D]; + if (x < 0 || x >= cases.length) return 0; + match (cases[x]) { + A => return 1; + B => return 2; + More => { + if (E.More.Extra.?(cases[x])) return 20; // More.Extra subrange + return 10; // More.C (in More but not Extra) + } + _ => return 0; + } +} diff --git a/test/enums/open_field00.v3 b/test/enums/open_field00.v3 new file mode 100644 index 000000000..c4dfd5ac6 --- /dev/null +++ b/test/enums/open_field00.v3 @@ -0,0 +1,9 @@ +//@execute 0=1; 1=17; 2=10; 3=20 +// Form 1: explicit redecl — subtype cases provide field values +enum E(x: int) { A(1), B(17), _ } +enum E.S1(x: int) { C(10), D(20) } +def main(i: int) -> int { + var cases: Array = [E.A, E.B, E.S1.C, E.S1.D]; + if (i < 0 || i >= cases.length) return -1; + return cases[i].x; +} diff --git a/test/enums/open_field01.v3 b/test/enums/open_field01.v3 new file mode 100644 index 000000000..72e02443e --- /dev/null +++ b/test/enums/open_field01.v3 @@ -0,0 +1,9 @@ +//@execute 0=1; 1=17; 2=15 +// Form 2: (super) keyword — subtype cases provide field values +enum E(x: int) { A(1), B(17), _ } +enum E.S2(super) { D(15) } +def main(i: int) -> int { + var cases: Array = [E.A, E.B, E.S2.D]; + if (i < 0 || i >= cases.length) return -1; + return cases[i].x; +} diff --git a/test/enums/open_field02.v3 b/test/enums/open_field02.v3 new file mode 100644 index 000000000..789514e4f --- /dev/null +++ b/test/enums/open_field02.v3 @@ -0,0 +1,9 @@ +//@execute 0=1; 1=17; 2=23 +// Form 3: implicit — subtype has no param decl, inherits parent's +enum E(x: int) { A(1), B(17), _ } +enum E.S3 { H(23) } +def main(i: int) -> int { + var cases: Array = [E.A, E.B, E.S3.H]; + if (i < 0 || i >= cases.length) return -1; + return cases[i].x; +} diff --git a/test/enums/open_field03.v3 b/test/enums/open_field03.v3 new file mode 100644 index 000000000..fe4a5db4e --- /dev/null +++ b/test/enums/open_field03.v3 @@ -0,0 +1,15 @@ +//@execute 0=11; 1=21; 2=31; 3=12; 4=22; 5=32 +// Multiple params: subtype redeclares, all field accesses correct +enum E(a: int, b: int) { X(1, 2), _ } +enum E.S(a: int, b: int) { Y(10, 20) } +def main(i: int) -> int { + match (i) { + 0 => return E.X.a + 10; + 1 => return E.X.b + 19; + 2 => return E.S.Y.a + 21; + 3 => return E.S.Y.b + -8; + 4 => { var e: E = E.S.Y; return e.a + 12; } + 5 => { var e: E = E.S.Y; return e.b + 12; } + _ => return -1; + } +} diff --git a/test/enums/open_field04.v3 b/test/enums/open_field04.v3 new file mode 100644 index 000000000..8f9a8bf04 --- /dev/null +++ b/test/enums/open_field04.v3 @@ -0,0 +1,11 @@ +//@execute 0=10; 1=20 +// Access field on subtype-typed variable +enum E(x: int) { A(1), _ } +enum E.S1(x: int) { C(10), D(20) } +def main(i: int) -> int { + match (i) { + 0 => { var v: E.S1 = E.S1.C; return v.x; } + 1 => { var v: E.S1 = E.S1.D; return v.x; } + _ => return -1; + } +} diff --git a/test/enums/open_field05.v3 b/test/enums/open_field05.v3 new file mode 100644 index 000000000..1615a28d7 --- /dev/null +++ b/test/enums/open_field05.v3 @@ -0,0 +1,9 @@ +//@execute 0=1; 1=10; 2=20 +// Access field on root-typed variable holding subtype value +enum E(x: int) { A(1), _ } +enum E.S1(x: int) { C(10), D(20) } +def main(i: int) -> int { + var cases: Array = [E.A, E.S1.C, E.S1.D]; + if (i < 0 || i >= cases.length) return -1; + return cases[i].x; +} diff --git a/test/enums/open_field06.v3 b/test/enums/open_field06.v3 new file mode 100644 index 000000000..6cbb5d81b --- /dev/null +++ b/test/enums/open_field06.v3 @@ -0,0 +1,13 @@ +//@execute 0=10; 1=20; 2=100; 3=200 +// (super, extra) — access both inherited and new fields +enum E(x: int) { A(10), B(20), _ } +enum E.S1(super, f: float) { C(100, 1.5f), D(200, 2.5f) } +def main(i: int) -> int { + match (i) { + 0 => return E.A.x; + 1 => return E.B.x; + 2 => return E.S1.C.x; + 3 => return E.S1.D.x; + } + return -1; +} diff --git a/test/enums/open_field07.v3 b/test/enums/open_field07.v3 new file mode 100644 index 000000000..bbb154024 --- /dev/null +++ b/test/enums/open_field07.v3 @@ -0,0 +1,13 @@ +//@execute 0=10; 1=20; 2=100; 3=200 +// Restate + add — access all fields +enum E(x: int) { A(10), B(20), _ } +enum E.S1(x: int, f: float) { C(100, 1.5f), D(200, 2.5f) } +def main(i: int) -> int { + match (i) { + 0 => return E.A.x; + 1 => return E.B.x; + 2 => return E.S1.C.x; + 3 => return E.S1.D.x; + } + return -1; +} diff --git a/test/enums/open_field08.v3 b/test/enums/open_field08.v3 new file mode 100644 index 000000000..17515987c --- /dev/null +++ b/test/enums/open_field08.v3 @@ -0,0 +1,13 @@ +//@execute 0=1; 1=2; 2=150; 3=250 +// Access new field on subtype-typed variable, inherited on root-typed +enum E(x: int) { A(1), B(2), _ } +enum E.S1(super, y: int) { C(10, 150), D(20, 250) } +def main(i: int) -> int { + match (i) { + 0 => return E.A.x; + 1 => return E.B.x; + 2 => { var v: E.S1 = E.S1.C; return v.y; } + 3 => { var v: E.S1 = E.S1.D; return v.y; } + } + return -1; +} diff --git a/test/enums/open_field09.v3 b/test/enums/open_field09.v3 new file mode 100644 index 000000000..53459c445 --- /dev/null +++ b/test/enums/open_field09.v3 @@ -0,0 +1,14 @@ +//@execute 0=10; 1=20; 2=100; 3=200 +// Multiple subtypes with different extra fields +enum E(x: int) { A(10), _ } +enum E.S1(super, y: int) { B(20, 100) } +enum E.S2(super, z: int) { C(30, 200) } +def main(i: int) -> int { + match (i) { + 0 => return E.A.x; + 1 => return E.S1.B.x; + 2 => { var v: E.S1 = E.S1.B; return v.y; } + 3 => { var v: E.S2 = E.S2.C; return v.z; } + } + return -1; +} diff --git a/test/enums/open_field10.v3 b/test/enums/open_field10.v3 new file mode 100644 index 000000000..68cb562bc --- /dev/null +++ b/test/enums/open_field10.v3 @@ -0,0 +1,12 @@ +//@execute 0=10; 1=100; 2=250 +// Form 1+add: access both inherited and new fields +enum E(x: int) { A(10), _ } +enum E.S1(x: int, y: int) { C(100, 150), D(200, 250) } +def main(i: int) -> int { + match (i) { + 0 => return E.A.x; + 1 => { var v: E.S1 = E.S1.C; return v.x; } + 2 => { var v: E.S1 = E.S1.D; return v.y; } + } + return -1; +} diff --git a/test/enums/open_field11.v3 b/test/enums/open_field11.v3 new file mode 100644 index 000000000..c009c7685 --- /dev/null +++ b/test/enums/open_field11.v3 @@ -0,0 +1,13 @@ +//@execute 0=10; 1=20; 2=30; 3=133 +// Multiple extra fields in one subtype +enum E(x: int) { A(10), _ } +enum E.S(super, y: int, z: int) { B(20, 30, 40), C(50, 60, 70) } +def main(i: int) -> int { + match (i) { + 0 => return E.A.x; + 1 => return E.S.B.x; + 2 => { var v: E.S = E.S.B; return v.y; } + 3 => { var v: E.S = E.S.B; return v.y + v.z + E.S.C.x + v.x - 7; } + } + return -1; +} diff --git a/test/enums/open_field12.v3 b/test/enums/open_field12.v3 new file mode 100644 index 000000000..e784d85ea --- /dev/null +++ b/test/enums/open_field12.v3 @@ -0,0 +1,12 @@ +//@execute 0=1; 1=2; 2=0 +// Float extra field round-trip (cast to int to verify value) +enum E(x: int) { A(10), _ } +enum E.S(super, f: float) { B(20, 1.5f), C(30, 2.75f) } +def main(i: int) -> int { + match (i) { + 0 => { var v: E.S = E.S.B; return int.truncf(v.f); } + 1 => { var v: E.S = E.S.C; return int.truncf(v.f); } + 2 => { var v: E.S = E.S.B; return int.truncf(v.f - 1.5f); } + } + return -1; +} diff --git a/test/enums/open_field13.v3 b/test/enums/open_field13.v3 new file mode 100644 index 000000000..d90ec30f2 --- /dev/null +++ b/test/enums/open_field13.v3 @@ -0,0 +1,9 @@ +//@execute 0=10; 1=100; 2=200 +// Root-typed variable holding subtype value (Form 1+add), inherited field access +enum E(x: int) { A(10), _ } +enum E.S(x: int, y: int) { B(100, 150), C(200, 250) } +def main(i: int) -> int { + var cases: Array = [E.A, E.S.B, E.S.C]; + if (i < 0 || i >= cases.length) return -1; + return cases[i].x; +} diff --git a/test/enums/open_field14.v3 b/test/enums/open_field14.v3 new file mode 100644 index 000000000..8a60f94e9 --- /dev/null +++ b/test/enums/open_field14.v3 @@ -0,0 +1,15 @@ +//@execute 0=1; 1=2; 2=3; 3=20; 4=30 +// Multi-level hierarchy: grandchild inherits intermediate extra field +enum E(x: int) { A(1), _ } +enum E.S(super, y: int) { B(2, 20), _ } +enum E.S.T(super, z: int) { C(3, 20, 30) } +def main(i: int) -> int { + match (i) { + 0 => return E.A.x; + 1 => return E.S.B.x; + 2 => return E.S.T.C.x; + 3 => { var v: E.S.T = E.S.T.C; return v.y; } + 4 => { var v: E.S.T = E.S.T.C; return v.z; } + } + return -1; +} diff --git a/test/enums/open_field15.v3 b/test/enums/open_field15.v3 new file mode 100644 index 000000000..f13e03493 --- /dev/null +++ b/test/enums/open_field15.v3 @@ -0,0 +1,15 @@ +//@execute 0=1; 1=2; 2=20; 3=3; 4=25 +// Multi-level (super) only — grandchild provides root + intermediate extras, no own extras +enum E(x: int) { A(1), _ } +enum E.S(super, y: int) { B(2, 20), _ } +enum E.S.T(super) { C(3, 25) } +def main(i: int) -> int { + match (i) { + 0 => return E.A.x; + 1 => return E.S.B.x; + 2 => { var v: E.S = E.S.B; return v.y; } + 3 => return E.S.T.C.x; + 4 => { var v: E.S.T = E.S.T.C; return v.y; } + } + return -1; +} diff --git a/test/enums/open_set00.v3 b/test/enums/open_set00.v3 new file mode 100644 index 000000000..6da51a584 --- /dev/null +++ b/test/enums/open_set00.v3 @@ -0,0 +1,14 @@ +//@execute 0=1; 1=1; 2=0; 3=0 +// Open enum set: subtype cases in set, membership via .C and .D +enum E { A, B, _ } +enum E.More { C, D } +def main(x: int) -> int { + var s: E.set = E.More.C | E.More.D; + match (x) { + 0 => return if(s.C, 1, 0); + 1 => return if(s.D, 1, 0); + 2 => return if(s.A, 1, 0); + 3 => return if(s.B, 1, 0); + } + return -1; +} diff --git a/test/enums/open_set01.v3 b/test/enums/open_set01.v3 new file mode 100644 index 000000000..a245e92f3 --- /dev/null +++ b/test/enums/open_set01.v3 @@ -0,0 +1,22 @@ +//@execute 0=4; 1=2; 2=2; 3=4; 4=0 +// Open enum set: .all covers entire hierarchy; union and intersect +enum E { A, B, _ } +enum E.More { C, D } +def count(s: E.set) -> int { + var n = 0; + for (e in s) n++; + return n; +} +def main(x: int) -> int { + var all = E.set.all; + var ab: E.set = E.A | E.B; + var cd: E.set = E.More.C | E.More.D; + match (x) { + 0 => return count(all); + 1 => return count(ab); + 2 => return count(cd); + 3 => return count(ab | cd); + 4 => return count(ab & cd); + } + return -1; +} diff --git a/test/enums/open_set02.v3 b/test/enums/open_set02.v3 new file mode 100644 index 000000000..be0ec5257 --- /dev/null +++ b/test/enums/open_set02.v3 @@ -0,0 +1,12 @@ +//@execute 0=4; 1=2 +// Open enum set: for-in iteration yields correct count +enum E { A, B, _ } +enum E.More { C, D } +def main(x: int) -> int { + var all = E.set.all; + var cd: E.set = E.More.C | E.More.D; + var s = if(x == 0, all, cd); + var count = 0; + for (e in s) count++; + return count; +} diff --git a/test/enums/open_set03.v3 b/test/enums/open_set03.v3 new file mode 100644 index 000000000..dfbdf2b2a --- /dev/null +++ b/test/enums/open_set03.v3 @@ -0,0 +1,25 @@ +//@execute 0=5; 1=1; 2=1 +// Open enum set: multiple siblings share root set type +enum E { A, _ } +enum E.Nums { N1, N2 } +enum E.Tens { T1, T2 } +def main(x: int) -> int { + // DFS tags: A=0, N1=1, N2=2, T1=3, T2=4 + var all = E.set.all; + match (x) { + 0 => { + var count = 0; + for (e in all) count++; + return count; + } + 1 => { + var s: E.set = E.Nums.N1 | E.Nums.N2; + return if(s.N1, 1, 0); + } + 2 => { + var s: E.set = E.Tens.T1 | E.Tens.T2; + return if(s.T2, 1, 0); + } + } + return -1; +} diff --git a/test/enums/open_set04.v3 b/test/enums/open_set04.v3 new file mode 100644 index 000000000..b9f10ac0a --- /dev/null +++ b/test/enums/open_set04.v3 @@ -0,0 +1,14 @@ +//@execute 0=1; 1=0; 2=1 +// Open enum set: cross-level union E.A | E.More.C promotes to E.set +enum E { A, B, _ } +enum E.More { C, D } +def main(x: int) -> int { + // DFS tags: A=0, B=1, C=2, D=3 + var s: E.set = E.A | E.More.C; + match (x) { + 0 => return if(s.A, 1, 0); + 1 => return if(s.B, 1, 0); + 2 => return if(s.C, 1, 0); + } + return -1; +} diff --git a/test/enums/parser/enum_cmethod00.v3 b/test/enums/parser/enum_cmethod00.v3 new file mode 100644 index 000000000..812a44384 --- /dev/null +++ b/test/enums/parser/enum_cmethod00.v3 @@ -0,0 +1,3 @@ +//@parse +// Enum with per-case method override +enum E { A { def m() -> int { return 1; } }, B; def m() -> int { return 0; } } diff --git a/test/enums/parser/enum_cmethod01.v3 b/test/enums/parser/enum_cmethod01.v3 new file mode 100644 index 000000000..7fdd52ec3 --- /dev/null +++ b/test/enums/parser/enum_cmethod01.v3 @@ -0,0 +1,9 @@ +//@parse +// Multiple cases with overrides +enum E { + A { def m() -> int { return 1; } def n() -> int { return 10; } }, + B { def m() -> int { return 2; } }, + C; + def m() -> int { return 0; } + def n() -> int { return 0; } +} diff --git a/test/enums/parser/enum_cmethod02.v3 b/test/enums/parser/enum_cmethod02.v3 new file mode 100644 index 000000000..399fcf76f --- /dev/null +++ b/test/enums/parser/enum_cmethod02.v3 @@ -0,0 +1,3 @@ +//@parse +// Default case with override +enum E { A, B, _ { def m() => 99; }; def m() => 0; } diff --git a/test/enums/parser/enum_cmethod03.v3 b/test/enums/parser/enum_cmethod03.v3 new file mode 100644 index 000000000..3fcb5136e --- /dev/null +++ b/test/enums/parser/enum_cmethod03.v3 @@ -0,0 +1,3 @@ +//@parse = ParseError +// var not allowed in enum case body +enum E { A { var x: int; }, B; } diff --git a/test/enums/parser/enum_method00.v3 b/test/enums/parser/enum_method00.v3 new file mode 100644 index 000000000..feaf94199 --- /dev/null +++ b/test/enums/parser/enum_method00.v3 @@ -0,0 +1,3 @@ +//@parse +// Enum with a single method +enum E { A, B; def m() -> int { return 0; } } diff --git a/test/enums/parser/enum_method01.v3 b/test/enums/parser/enum_method01.v3 new file mode 100644 index 000000000..46e4afd12 --- /dev/null +++ b/test/enums/parser/enum_method01.v3 @@ -0,0 +1,7 @@ +//@parse +// Enum with multiple methods and expression body +enum E { A, B, C; + def m() => 42; + def n(x: int) -> int { return x + 1; } + private def helper() -> bool { return true; } +} diff --git a/test/enums/parser/enum_method02.v3 b/test/enums/parser/enum_method02.v3 new file mode 100644 index 000000000..bfe6f8895 --- /dev/null +++ b/test/enums/parser/enum_method02.v3 @@ -0,0 +1,3 @@ +//@parse +// Enum with fields and methods +enum E(x: int) { A(1), B(2); def getX() -> int { return x; } } diff --git a/test/enums/parser/enum_method03.v3 b/test/enums/parser/enum_method03.v3 new file mode 100644 index 000000000..d4b6ed82c --- /dev/null +++ b/test/enums/parser/enum_method03.v3 @@ -0,0 +1,4 @@ +//@parse +// Open enum with methods +enum E { A, B, _; def m() => 0; } +enum E.Sub { C, D } diff --git a/test/enums/parser/open00.v3 b/test/enums/parser/open00.v3 new file mode 100644 index 000000000..f8e57e116 --- /dev/null +++ b/test/enums/parser/open00.v3 @@ -0,0 +1,3 @@ +//@parse +// Open enum with just _ case +enum E { A, B, _ } diff --git a/test/enums/parser/open01.v3 b/test/enums/parser/open01.v3 new file mode 100644 index 000000000..249b5048c --- /dev/null +++ b/test/enums/parser/open01.v3 @@ -0,0 +1,4 @@ +//@parse +// Subtype enum declaration +enum E1 { A, B, _ } +enum E1.More { C, D } diff --git a/test/enums/parser/open02.v3 b/test/enums/parser/open02.v3 new file mode 100644 index 000000000..bf4e7ce6c --- /dev/null +++ b/test/enums/parser/open02.v3 @@ -0,0 +1,4 @@ +//@parse +// Subtype enum with params +enum E1(x: int) { A(1), B(2), _ } +enum E1.More(x: int) { C(3), D(4) } diff --git a/test/enums/parser/open03.v3 b/test/enums/parser/open03.v3 new file mode 100644 index 000000000..f425076c9 --- /dev/null +++ b/test/enums/parser/open03.v3 @@ -0,0 +1,3 @@ +//@parse = ParseError @ 1:14 +// _ case cannot be followed by more cases +enum E { A, B, _, C } diff --git a/test/enums/seman/enum_cmethod00.v3 b/test/enums/seman/enum_cmethod00.v3 new file mode 100644 index 000000000..8d29c966b --- /dev/null +++ b/test/enums/seman/enum_cmethod00.v3 @@ -0,0 +1,7 @@ +//@seman +// Per-case override type-checks correctly +enum E { A { def m() -> int { return 1; } }, B; def m() -> int { return 0; } } +def main() { + var x: int = E.A.m(); + var y: int = E.B.m(); +} diff --git a/test/enums/seman/enum_cmethod01.v3 b/test/enums/seman/enum_cmethod01.v3 new file mode 100644 index 000000000..55ea405c1 --- /dev/null +++ b/test/enums/seman/enum_cmethod01.v3 @@ -0,0 +1,3 @@ +//@seman = InheritanceError +// Override with mismatched return type +enum E { A { def m() -> byte { return 1; } }, B; def m() -> int { return 0; } } diff --git a/test/enums/seman/enum_cmethod02.v3 b/test/enums/seman/enum_cmethod02.v3 new file mode 100644 index 000000000..c0795aee5 --- /dev/null +++ b/test/enums/seman/enum_cmethod02.v3 @@ -0,0 +1,3 @@ +//@seman = EnumDeclError +// Case method that doesn't override any enum-level method +enum E { A { def m() -> int { return 1; } }, B; } diff --git a/test/enums/seman/enum_cmethod03.v3 b/test/enums/seman/enum_cmethod03.v3 new file mode 100644 index 000000000..4ae2958cb --- /dev/null +++ b/test/enums/seman/enum_cmethod03.v3 @@ -0,0 +1,7 @@ +//@seman +// Override accesses enum fields +enum E(x: int) { A(10) { def m() -> int { return x + 1; } }, B(20); def m() -> int { return x; } } +def main() { + var a: int = E.A.m(); + var b: int = E.B.m(); +} diff --git a/test/enums/seman/enum_method00.v3 b/test/enums/seman/enum_method00.v3 new file mode 100644 index 000000000..af5cd4d83 --- /dev/null +++ b/test/enums/seman/enum_method00.v3 @@ -0,0 +1,7 @@ +//@seman +// Enum methods type-check correctly +enum E { A, B; def m() -> int { return 42; } } +def main() { + var x: int = E.A.m(); + var y: int = E.B.m(); +} diff --git a/test/enums/seman/enum_method01.v3 b/test/enums/seman/enum_method01.v3 new file mode 100644 index 000000000..b793e7a6b --- /dev/null +++ b/test/enums/seman/enum_method01.v3 @@ -0,0 +1,10 @@ +//@seman +// Enum method accessing fields and tag +enum E(x: int) { A(1), B(2); + def getX() -> int { return x; } + def id() -> int { return tag; } +} +def main() { + var a: int = E.A.getX(); + var b: int = E.B.id(); +} diff --git a/test/enums/seman/enum_method02.v3 b/test/enums/seman/enum_method02.v3 new file mode 100644 index 000000000..bc6c3015c --- /dev/null +++ b/test/enums/seman/enum_method02.v3 @@ -0,0 +1,3 @@ +//@seman = TypeError +// Enum method return type mismatch +enum E { A, B; def m() -> int { return true; } } diff --git a/test/enums/seman/enum_method03.v3 b/test/enums/seman/enum_method03.v3 new file mode 100644 index 000000000..ed14e9287 --- /dev/null +++ b/test/enums/seman/enum_method03.v3 @@ -0,0 +1,9 @@ +//@seman +// Private enum method not accessible externally, but callable internally +enum E { A, B; + def pub() -> int { return priv(); } + private def priv() -> int { return 1; } +} +def main() { + var x: int = E.A.pub(); +} diff --git a/test/enums/seman/open00.v3 b/test/enums/seman/open00.v3 new file mode 100644 index 000000000..e72c483be --- /dev/null +++ b/test/enums/seman/open00.v3 @@ -0,0 +1,12 @@ +//@seman +// Open enum: match requires default arm +enum E { A, B, _ } +component C { + def f(e: E) -> int { + match (e) { + A => return 1; + B => return 2; + _ => return 0; + } + } +} diff --git a/test/enums/seman/open01.v3 b/test/enums/seman/open01.v3 new file mode 100644 index 000000000..18480ac54 --- /dev/null +++ b/test/enums/seman/open01.v3 @@ -0,0 +1,11 @@ +//@seman = MatchError @ 3:9 +// Open enum: match without default arm must error +enum E { A, B, _ } +component C { + def f(e: E) -> int { + match (e) { + A => return 1; + B => return 2; + } + } +} diff --git a/test/enums/seman/open02.v3 b/test/enums/seman/open02.v3 new file mode 100644 index 000000000..0333e2dc2 --- /dev/null +++ b/test/enums/seman/open02.v3 @@ -0,0 +1,6 @@ +//@seman +// Open enum: .set type is now allowed +enum E { A, B, _ } +component C { + def f() -> E.set { return E.A; } +} diff --git a/test/enums/seman/open03.v3 b/test/enums/seman/open03.v3 new file mode 100644 index 000000000..1fee28545 --- /dev/null +++ b/test/enums/seman/open03.v3 @@ -0,0 +1,14 @@ +//@seman +// Subtype enum: declare and use +enum E1 { A, B, _ } +enum E1.More { C, D } +component C { + def f(e: E1) -> int { + match (e) { + A => return 1; + B => return 2; + More => return 3; + _ => return 0; + } + } +} diff --git a/test/enums/seman/open04.v3 b/test/enums/seman/open04.v3 new file mode 100644 index 000000000..efe5b08e0 --- /dev/null +++ b/test/enums/seman/open04.v3 @@ -0,0 +1,4 @@ +//@seman = EnumDeclError @ 2:6 +// Subtype enum: parent must have _ case +enum E1 { A, B } +enum E1.More { C, D } diff --git a/test/enums/seman/open05.v3 b/test/enums/seman/open05.v3 new file mode 100644 index 000000000..17ca8ead6 --- /dev/null +++ b/test/enums/seman/open05.v3 @@ -0,0 +1,9 @@ +//@seman +// Subtype enum: widening assignment E1.More -> E1 +enum E1 { A, B, _ } +enum E1.More { C, D } +component C { + def f(e: E1.More) -> E1 { + return e; // widening (subsume) + } +} diff --git a/test/enums/seman/open06.v3 b/test/enums/seman/open06.v3 new file mode 100644 index 000000000..b494ce987 --- /dev/null +++ b/test/enums/seman/open06.v3 @@ -0,0 +1,9 @@ +//@seman +// Subtype enum: type query E1.More.?(x) +enum E1 { A, B, _ } +enum E1.More { C, D } +component C { + def f(e: E1) -> bool { + return E1.More.?(e); + } +} diff --git a/test/enums/seman/open07.v3 b/test/enums/seman/open07.v3 new file mode 100644 index 000000000..4166f87c8 --- /dev/null +++ b/test/enums/seman/open07.v3 @@ -0,0 +1,8 @@ +//@seman +// Subtype enum: multi-level hierarchy (E.More has _ case allowing E.More.Extra) +enum E { A, _ } +enum E.More { B, _ } +enum E.More.Extra { C } +component C { + def f(e: E) -> bool { return E.More.Extra.?(e); } +} diff --git a/test/enums/seman/open08.v3 b/test/enums/seman/open08.v3 new file mode 100644 index 000000000..62aaa7baf --- /dev/null +++ b/test/enums/seman/open08.v3 @@ -0,0 +1,5 @@ +//@seman = EnumDeclError @ 3:6 +// Subtype enum: intermediate level has no _ case, subtype of it must fail +enum E { A, _ } +enum E.More { B } +enum E.More.Sub { C } diff --git a/test/enums/seman/open09.v3 b/test/enums/seman/open09.v3 new file mode 100644 index 000000000..8dbd994b9 --- /dev/null +++ b/test/enums/seman/open09.v3 @@ -0,0 +1,4 @@ +//@seman = EnumDeclError @ 3:6 +// Subtype enum: name conflicts with named case of parent +enum E { A, B, _ } +enum E.A { C } diff --git a/test/enums/seman/open10.v3 b/test/enums/seman/open10.v3 new file mode 100644 index 000000000..7dec8f706 --- /dev/null +++ b/test/enums/seman/open10.v3 @@ -0,0 +1,12 @@ +//@seman +// Subtype enum: match on closed subtype-typed variable is exhaustive without _ arm +enum E1 { A, B, _ } +enum E1.More { C, D } +component C { + def f(e: E1.More) -> int { + match (e) { + C => return 1; + D => return 2; + } + } +} diff --git a/test/enums/seman/open_field00.v3 b/test/enums/seman/open_field00.v3 new file mode 100644 index 000000000..267b0b360 --- /dev/null +++ b/test/enums/seman/open_field00.v3 @@ -0,0 +1,9 @@ +//@seman +// Form 1: explicit redecl — params match parent +enum E(x: int) { A(1), B(2), _ } +enum E.S1(x: int) { C(10), D(20) } +component C { + def f(e: E) -> int { + return e.x; + } +} diff --git a/test/enums/seman/open_field01.v3 b/test/enums/seman/open_field01.v3 new file mode 100644 index 000000000..7ff543996 --- /dev/null +++ b/test/enums/seman/open_field01.v3 @@ -0,0 +1,4 @@ +//@seman = EnumDeclError @ 3:1 +// Form 1 mismatch: wrong type +enum E(x: int) { A(1), _ } +enum E.S1(x: bool) { C(true) } diff --git a/test/enums/seman/open_field02.v3 b/test/enums/seman/open_field02.v3 new file mode 100644 index 000000000..4f8da238d --- /dev/null +++ b/test/enums/seman/open_field02.v3 @@ -0,0 +1,4 @@ +//@seman = EnumDeclError @ 3:1 +// Form 1 mismatch: wrong name +enum E(x: int) { A(1), _ } +enum E.S1(y: int) { C(10) } diff --git a/test/enums/seman/open_field03.v3 b/test/enums/seman/open_field03.v3 new file mode 100644 index 000000000..39b348c04 --- /dev/null +++ b/test/enums/seman/open_field03.v3 @@ -0,0 +1,4 @@ +//@seman +// Form 1+add: restate root params + add new field (valid) +enum E(x: int) { A(1), _ } +enum E.S1(x: int, y: int) { C(10, 20) } diff --git a/test/enums/seman/open_field04.v3 b/test/enums/seman/open_field04.v3 new file mode 100644 index 000000000..e1ed7ddaf --- /dev/null +++ b/test/enums/seman/open_field04.v3 @@ -0,0 +1,4 @@ +//@seman = EnumDeclError @ 3:1 +// (super) on parent with no params +enum E { A, B, _ } +enum E.S1(super) { C } diff --git a/test/enums/seman/open_field05.v3 b/test/enums/seman/open_field05.v3 new file mode 100644 index 000000000..e378aca2e --- /dev/null +++ b/test/enums/seman/open_field05.v3 @@ -0,0 +1,4 @@ +//@seman = EnumDeclError @ 3:1 +// Subtype declares params but parent has none +enum E { A, B, _ } +enum E.S1(x: int) { C(10) } diff --git a/test/enums/seman/open_field06.v3 b/test/enums/seman/open_field06.v3 new file mode 100644 index 000000000..c6d37fc8f --- /dev/null +++ b/test/enums/seman/open_field06.v3 @@ -0,0 +1,4 @@ +//@seman +// (super, extra) form — super + add new field +enum E(b: bool, i: int) { A(true, 0), B(false, 1), _ } +enum E.S1(super, f: float) { F(false, 2, 1.0f) } diff --git a/test/enums/seman/open_field07.v3 b/test/enums/seman/open_field07.v3 new file mode 100644 index 000000000..4578f3b4c --- /dev/null +++ b/test/enums/seman/open_field07.v3 @@ -0,0 +1,4 @@ +//@seman +// Restate root params + add new field +enum E(b: bool, i: int) { A(true, 0), B(false, 1), _ } +enum E.S1(b: bool, i: int, f: float) { F(false, 2, 1.0f) } diff --git a/test/enums/seman/open_field08.v3 b/test/enums/seman/open_field08.v3 new file mode 100644 index 000000000..d16a0b5cc --- /dev/null +++ b/test/enums/seman/open_field08.v3 @@ -0,0 +1,4 @@ +//@seman = EnumDeclError @ 4:1 +// New params without restating or using super — error +enum E(b: bool, i: int) { A(true, 0), B(false, 1), _ } +enum E.S1(f: float) { F(1.0f) } diff --git a/test/enums/seman/open_field09.v3 b/test/enums/seman/open_field09.v3 new file mode 100644 index 000000000..6fb52f9d0 --- /dev/null +++ b/test/enums/seman/open_field09.v3 @@ -0,0 +1,4 @@ +//@seman = EnumDeclError @ 3:1 +// (super, extra) on root with no params — error +enum E { A, B, _ } +enum E.S1(super, f: float) { F(1.0f) } diff --git a/test/enums/seman/open_field10.v3 b/test/enums/seman/open_field10.v3 new file mode 100644 index 000000000..024d31225 --- /dev/null +++ b/test/enums/seman/open_field10.v3 @@ -0,0 +1,5 @@ +//@seman = UnresolvedMember @ 5:16 +// Accessing extra field on root-typed variable should fail +enum E(x: int) { A(1), _ } +enum E.S(super, y: int) { B(2, 20) } +def f(v: E) -> int { return v.y; } diff --git a/test/enums/seman/open_field11.v3 b/test/enums/seman/open_field11.v3 new file mode 100644 index 000000000..c77ea9042 --- /dev/null +++ b/test/enums/seman/open_field11.v3 @@ -0,0 +1,4 @@ +//@seman = EnumDeclError @ 4:1 +// Form 1+add with mismatched prefix: first param name doesn't match root +enum E(x: int) { A(1), _ } +enum E.S(y: int, f: float) { C(1, 2.0f) } diff --git a/test/enums/seman/open_field12.v3 b/test/enums/seman/open_field12.v3 new file mode 100644 index 000000000..ce57dad59 --- /dev/null +++ b/test/enums/seman/open_field12.v3 @@ -0,0 +1,5 @@ +//@seman +// Multi-level super-only passes verification +enum E(x: int) { A(1), _ } +enum E.S(super, y: int) { B(2, 20), _ } +enum E.S.T(super) { C(3, 25) } From e89f1b842bdcd8aa566269cdc413c372cc759dea Mon Sep 17 00:00:00 2001 From: Eliot Moss Date: Tue, 7 Apr 2026 18:17:48 -0400 Subject: [PATCH 02/58] Restructuring to eliminate VstEnum in favor of VstClass and to process variants and enums more similarly --- aeneas/src/core/Eval.v3 | 4 +- aeneas/src/debug/Dwarf.v3 | 2 +- aeneas/src/ir/Normalization.v3 | 4 +- aeneas/src/ir/SsaNormalizer.v3 | 2 +- aeneas/src/ir/VariantNormalizer.v3 | 6 +- aeneas/src/ir/VstIr.v3 | 16 ++-- aeneas/src/ssa/SsaBuilder.v3 | 21 +++-- aeneas/src/ssa/VstSsaGen.v3 | 12 ++- aeneas/src/v3/TypeSystem.v3 | 4 +- aeneas/src/v3/V3Class.v3 | 48 +++++++++- aeneas/src/v3/V3Enum.v3 | 44 ++------- aeneas/src/vst/MethodEnv.v3 | 77 +++++++++------ aeneas/src/vst/Parser.v3 | 29 +++--- aeneas/src/vst/Verifier.v3 | 146 ++++++++++++++--------------- aeneas/src/vst/Vst.v3 | 138 ++++++++++++++------------- apps/vctags/vctags.v3 | 2 +- test/open_types/name0.v3 | 19 ++++ test/open_types/name1.v3 | 19 ++++ test/open_types/name2.v3 | 19 ++++ test/open_types/name3.v3 | 19 ++++ 20 files changed, 374 insertions(+), 257 deletions(-) create mode 100644 test/open_types/name0.v3 create mode 100644 test/open_types/name1.v3 create mode 100644 test/open_types/name2.v3 create mode 100644 test/open_types/name3.v3 diff --git a/aeneas/src/core/Eval.v3 b/aeneas/src/core/Eval.v3 index 5a968971e..32051c95c 100644 --- a/aeneas/src/core/Eval.v3 +++ b/aeneas/src/core/Eval.v3 @@ -127,10 +127,10 @@ component Eval { } CLASS_QUERY, VARIANT_QUERY => { if (EnumType.?(tt)) { - // enum subtype query: check integer tag is in [enumTagLo, enumTagHi] + // enum subtype query: check integer tag is in [tagLo, tagHi] var decl = EnumType.!(tt).enumDecl; var tagVal = Int.unbox(val); - return tagVal >= decl.enumTagLo && tagVal <= decl.enumTagHi; + return tagVal >= decl.tagLo && tagVal <= decl.tagHi; } if (val == null) return false; if (tt.open()) return false; diff --git a/aeneas/src/debug/Dwarf.v3 b/aeneas/src/debug/Dwarf.v3 index 4d923b52e..a8b2ae418 100644 --- a/aeneas/src/debug/Dwarf.v3 +++ b/aeneas/src/debug/Dwarf.v3 @@ -366,7 +366,7 @@ class DwarfInfoSection(abbrev: DwarfAbbrevSection) { c.render(buf); buf.send(w.putr); w.putb(0); - w.put_uleb32(u32.!(c.tag)); + w.put_uleb32(u32.!(c.localTag)); } w.putb(0); } diff --git a/aeneas/src/ir/Normalization.v3 b/aeneas/src/ir/Normalization.v3 index ef1ff273c..9fb97917b 100644 --- a/aeneas/src/ir/Normalization.v3 +++ b/aeneas/src/ir/Normalization.v3 @@ -330,7 +330,7 @@ class ReachabilityNormalizer(config: NormalizerConfig, ra: ReachabilityAnalyzer) // liveClasses, so layoutMtable can filter them out. def numberVariantSlot(c: RaClass, rootMin: int) { var decl = ClassType.!(c.oldType).classDecl; - var tagLo = decl.variantTag, tagHi = decl.variantTagHi; + var tagLo = decl.variantTag, tagHi = decl.tagHi; c.minClassId = rootMin + tagLo; c.maxClassId = rootMin + tagHi + 1; liveClasses.grow(c.maxClassId); @@ -640,7 +640,7 @@ class ReachabilityNormalizer(config: NormalizerConfig, ra: ReachabilityAnalyzer) } def layoutEnumMtable(rv: RaVirtual, rm: RaMethod, rc: RaClass) { var enumDecl = EnumType.!(rc.oldType).enumDecl; - var numCases = enumDecl.enumTagHi + 1; + var numCases = enumDecl.tagHi + 1; var table = Array.new(numCases); var mtable = IrMtable.new(rm.norm, 0, table); rv.mtable = mtable; diff --git a/aeneas/src/ir/SsaNormalizer.v3 b/aeneas/src/ir/SsaNormalizer.v3 index fc4564eea..5f9639428 100644 --- a/aeneas/src/ir/SsaNormalizer.v3 +++ b/aeneas/src/ir/SsaNormalizer.v3 @@ -1122,7 +1122,7 @@ class SsaRaNormalizer extends SsaRebuilder { var rDecl = EnumType.!(rtn.oldType).enumDecl; var actualTag = ai_old[offset]; // enum value IS its tag integer var tagType = IntType.!(aDecl.tagType); - var lo = rDecl.enumTagLo, hi = rDecl.enumTagHi; + var lo = rDecl.tagLo, hi = rDecl.tagHi; var check: SsaInstr; if (lo == hi) { check = curBlock.pure(V3Op.newIntEq(tagType), [actualTag, newGraph.intConst(lo)]); diff --git a/aeneas/src/ir/VariantNormalizer.v3 b/aeneas/src/ir/VariantNormalizer.v3 index a8c4bdc36..7a9f63fc1 100644 --- a/aeneas/src/ir/VariantNormalizer.v3 +++ b/aeneas/src/ir/VariantNormalizer.v3 @@ -276,7 +276,7 @@ class VariantNormalizer(nc: NormalizerConfig, rn: ReachabilityNormalizer, verbos var vn = VariantNorm.new(child.oldType, newType, solution.types, child.variantFields, tagField); vn.tagValue = V3.getVariantTag(child.oldType); - vn.tagHi = ClassType.!(child.oldType).classDecl.variantTagHi; + vn.tagHi = ClassType.!(child.oldType).classDecl.tagHi; child.variantNorm = vn; parentNorm.children = List.new(vn, parentNorm.children); @@ -297,7 +297,7 @@ class VariantNormalizer(nc: NormalizerConfig, rn: ReachabilityNormalizer, verbos if (child.variantNorm != null) continue; // already handled by main loop var vn = VariantNorm.new(child.oldType, newType, types, NO_FIELDS, tagField); vn.tagValue = V3.getVariantTag(child.oldType); - vn.tagHi = ClassType.!(child.oldType).classDecl.variantTagHi; + vn.tagHi = ClassType.!(child.oldType).classDecl.tagHi; child.variantNorm = vn; parentNorm.children = List.new(vn, parentNorm.children); createSubtypeVariantNorms(child, vn, newType, types, tagField); @@ -306,7 +306,7 @@ class VariantNormalizer(nc: NormalizerConfig, rn: ReachabilityNormalizer, verbos private def unboxUsingEnumVariantNorm(rc: RaClass, tagType: IntType, tagField: VariantField) { var vn = VariantNorm.new(rc.oldType, tagType, [tagType], NO_FIELDS, tagField); vn.tagValue = V3.getVariantTag(rc.oldType); - vn.tagHi = ClassType.!(rc.oldType).classDecl.variantTagHi; + vn.tagHi = ClassType.!(rc.oldType).classDecl.tagHi; rc.raFacts |= RaFact.RC_ENUM; rc.variantNorm = vn; diff --git a/aeneas/src/ir/VstIr.v3 b/aeneas/src/ir/VstIr.v3 index ec6e11b3a..95a39a0fd 100644 --- a/aeneas/src/ir/VstIr.v3 +++ b/aeneas/src/ir/VstIr.v3 @@ -72,13 +72,13 @@ class IrBuilder(ctype: Type, parent: IrClass) { } } // Build IR methods for per-case enum method overrides. - if (VstEnum.?(decl)) addEnumCaseOverrides(VstEnum.!(decl)); + if (decl.isEnum()) addEnumCaseOverrides(VstClass.!(decl)); var ic = build(); ic.boxing = boxing; ic.packed = packed; return ic; } - def addEnumCaseOverrides(decl: VstEnum) { + def addEnumCaseOverrides(decl: VstClass) { // For subtype enums, all overrides are already processed by the // root enum's buildClass (see addSubtypeOverridesRecursive below). if (decl.parentEnum != null) return; @@ -89,15 +89,15 @@ class IrBuilder(ctype: Type, parent: IrClass) { // generation (critical for v3i lazy evaluation). addSubtypeOverridesRecursive(decl); } - def addSubtypeOverridesRecursive(decl: VstEnum) { + def addSubtypeOverridesRecursive(decl: VstClass) { for (l = decl.subtypeOrder; l != null; l = l.tail) { addEnumCaseOverridesForDecl(l.head); addSubtypeOverridesRecursive(l.head); } } - def addEnumCaseOverridesForDecl(decl: VstEnum) { + def addEnumCaseOverridesForDecl(decl: VstClass) { var rootEnum = findRootEnum(decl); - var tableSize = rootEnum.enumTagHi + 1; + var tableSize = rootEnum.tagHi + 1; // 1. Subtype-level method overrides: fill range in root's enumCaseIrs. for (l = decl.members; l != null; l = l.tail) { if (!VstMethod.?(l.head)) continue; @@ -112,7 +112,7 @@ class IrBuilder(ctype: Type, parent: IrClass) { if (rootVst.enumCaseIrs == null) { rootVst.enumCaseIrs = Array.new(tableSize); } - for (i = decl.enumTagLo; i <= decl.enumTagHi; i++) { + for (i = decl.tagLo; i <= decl.tagHi; i++) { rootVst.enumCaseIrs[i] = ir; } } @@ -131,7 +131,7 @@ class IrBuilder(ctype: Type, parent: IrClass) { if (rootVst.enumCaseIrs == null) { rootVst.enumCaseIrs = Array.new(tableSize); } - var tag = if(c.enumTag >= 0, c.enumTag, c.tag); + var tag = c.enumTag; rootVst.enumCaseIrs[tag] = ir; } } @@ -148,7 +148,7 @@ class IrBuilder(ctype: Type, parent: IrClass) { } } } - def findRootEnum(decl: VstEnum) -> VstEnum { + def findRootEnum(decl: VstClass) -> VstClass { while (decl.parentEnum != null) decl = decl.parentEnum; return decl; } diff --git a/aeneas/src/ssa/SsaBuilder.v3 b/aeneas/src/ssa/SsaBuilder.v3 index 1f1d250cb..a7f81eccc 100644 --- a/aeneas/src/ssa/SsaBuilder.v3 +++ b/aeneas/src/ssa/SsaBuilder.v3 @@ -106,13 +106,22 @@ class SsaBuilder { return opArrayGetElem(V3.stringArrayType, V3.getVariantTagType(t), Facts.O_SAFE_BOUNDS | Fact.O_PURE, array, index); } - EnumGetShortName => { - var t = EnumType.!(op.typeArgs[0]); - var record = t.getShortNames(context.prog); + VariantGetShortName => { + var t = op.typeArgs[0]; + var record: Record; + if (EnumType.?(t)) record = EnumType.!(t).getShortNames(context.prog); + else record = V3Class_TypeCon.!(t.typeCon).getShortNames(context.prog); var array = graph.recordConst(record); - var tagType = IntType.!(t.enumDecl.tagType); - return opArrayGetElem(V3.stringArrayType, tagType, - Facts.O_SAFE_BOUNDS | Fact.O_PURE, array, args[0]); + var index = if(EnumType.?(t), args[0], add(V3Op.newVariantGetTag(op.typeArgs[0]), args, N)); + return opArrayGetElem(V3.stringArrayType, V3.getVariantTagType(t), + Facts.O_SAFE_BOUNDS | Fact.O_PURE, array, index); + +// var t = EnumType.!(op.typeArgs[0]); +// var record = t.getShortNames(context.prog); +// var array = graph.recordConst(record); +// var tagType = IntType.!(t.enumDecl.tagType); +// return opArrayGetElem(V3.stringArrayType, tagType, +// Facts.O_SAFE_BOUNDS | Fact.O_PURE, array, args[0]); } EnumGetParam(field) => { var t = field.receiver.declType; diff --git a/aeneas/src/ssa/VstSsaGen.v3 b/aeneas/src/ssa/VstSsaGen.v3 index 2283e8945..4ce95da00 100644 --- a/aeneas/src/ssa/VstSsaGen.v3 +++ b/aeneas/src/ssa/VstSsaGen.v3 @@ -95,8 +95,10 @@ class VstSsaGen extends VstVisitor { def addConstructorCode(decl: VstNew, env: VstSsaEnv) { var isClass: bool; match (decl.receiver) { - x: VstEnum => return addEnumParamInitCode(x, env); - x: VstClass => isClass = true; + x: VstClass => { + if (x.isEnum()) return addEnumParamInitCode(x, env); + isClass = true; + } } // Append implicit field initializing from params. for (plist = decl.func.params.list; plist != null; plist = plist.tail) { @@ -140,7 +142,7 @@ class VstSsaGen extends VstVisitor { } } } - def addEnumParamInitCode(receiver: VstEnum, env: VstSsaEnv) { + def addEnumParamInitCode(receiver: VstClass, env: VstSsaEnv) { context.method.flags |= IrFlag.M_ENUM_INIT; // Find the root enum. var rootEnum = receiver; @@ -633,12 +635,12 @@ class VstSsaGen extends VstVisitor { // subtype variant pattern: check if integer tag falls in the subtype's DFS range var decl = ClassType.!(pat.subtypeType).classDecl; var tagVal = Int.unbox(val); - if (tagVal >= decl.variantTag && tagVal <= decl.variantTagHi) return true; + if (tagVal >= decl.variantTag && tagVal <= decl.tagHi) return true; } else if (pat.subtypeEnumType != null) { // subtype enum pattern: check if integer tag falls in the subtype enum's DFS range var decl = EnumType.!(pat.subtypeEnumType).enumDecl; var tagVal = Int.unbox(val); - if (tagVal >= decl.enumTagLo && tagVal <= decl.enumTagHi) return true; + if (tagVal >= decl.tagLo && tagVal <= decl.tagHi) return true; } else if (pat.vdecl != null) { var r = Eval.doQuery0(keyType, pat.vdecl.tref.binding, val); if (r) return true; diff --git a/aeneas/src/v3/TypeSystem.v3 b/aeneas/src/v3/TypeSystem.v3 index 398b13187..7df7495ca 100644 --- a/aeneas/src/v3/TypeSystem.v3 +++ b/aeneas/src/v3/TypeSystem.v3 @@ -792,11 +792,11 @@ def mergeConversions(a: Conversion, b: Conversion) -> Conversion { } // Returns true if {sub} is a (direct or transitive) subtype enum of {sup}. // Uses the subtypeOrder lists populated by the verifier (no prog access needed). -def isEnumSubtypeOf(sub: VstEnum, sup: VstEnum) -> bool { +def isEnumSubtypeOf(sub: VstClass, sup: VstClass) -> bool { if (sub.qualifiers == null) return false; // root enums are not subtypes return enumAncestorHasSubtype(sup, sub); } -def enumAncestorHasSubtype(ancestor: VstEnum, target: VstEnum) -> bool { +def enumAncestorHasSubtype(ancestor: VstClass, target: VstClass) -> bool { for (l = ancestor.subtypeOrder; l != null; l = l.tail) { if (l.head == target) return true; if (enumAncestorHasSubtype(l.head, target)) return true; diff --git a/aeneas/src/v3/V3Class.v3 b/aeneas/src/v3/V3Class.v3 index 864877fdd..a8a2d6d9f 100644 --- a/aeneas/src/v3/V3Class.v3 +++ b/aeneas/src/v3/V3Class.v3 @@ -5,6 +5,7 @@ class V3Class_TypeCon extends TypeCon { def classDecl: VstClass; var names: Record; + var shortNames: Record; new(classDecl, typeCache: TypeCache) super(classDecl.fullName, classDecl.kind, Lists.length(classDecl.typeParams), typeCache) { @@ -15,13 +16,48 @@ class V3Class_TypeCon extends TypeCon { } def getNames(prog: Program) -> Record { if (names == null) { - names = prog.newRecord(V3.stringArrayType, classDecl.cases.length); - for (vc in classDecl.cases) { - names.values[vc.tag] = Byte.boxString(vc.name(), prog); - } + var total = if(classDecl.tagHi >= 0, classDecl.tagHi + 1, classDecl.cases.length); + names = prog.newRecord(V3.stringArrayType, total); + CaseNames.fillNames(names, classDecl, "", prog); } return names; } + def getShortNames(prog: Program) -> Record { + if (shortNames == null) { + var total = if(classDecl.tagHi >= 0, classDecl.tagHi + 1, classDecl.cases.length); + shortNames = prog.newRecord(V3.stringArrayType, total); + CaseNames.fillShortNames(shortNames, classDecl, prog); + } + return shortNames; + } +} +// Shared helpers for building case name arrays (used by both V3Class_TypeCon and EnumType). +component CaseNames { + def fillNames(rec: Record, decl: VstClass, prefix: string, prog: Program) { + for (c in decl.cases) { + var tag = c.runtimeTag(); + if (tag < 0 || Strings.equal(c.name(), "_")) continue; + var n = if(prefix.length == 0, c.name(), Strings.format2("%s.%s", prefix, c.name())); + rec.values[tag] = Byte.boxString(n, prog); + } + var subtypes = Lists.reverse(decl.subtypeOrder); + for (l = subtypes; l != null; l = l.tail) { + var sub = l.head; + var p = if(prefix.length == 0, sub.name(), Strings.format2("%s.%s", prefix, sub.name())); + fillNames(rec, sub, p, prog); + } + } + def fillShortNames(rec: Record, decl: VstClass, prog: Program) { + for (c in decl.cases) { + var tag = c.runtimeTag(); + if (tag < 0 || Strings.equal(c.name(), "_")) continue; + rec.values[tag] = Byte.boxString(c.name(), prog); + } + var subtypes = Lists.reverse(decl.subtypeOrder); + for (l = subtypes; l != null; l = l.tail) { + fillShortNames(rec, l.head, prog); + } + } } // The type for user-declared classes. // XXX: separate VariantType from ClassType and enum type. @@ -74,4 +110,8 @@ class ClassType extends Type { var types = [Type.!(this)]; return V3Op.newVstSugar(VstOperator.VariantGetName, types, types, V3.stringType); } + def getShortNameOperator() -> Operator { + var types = [Type.!(this)]; + return V3Op.newVstSugar(VstOperator.VariantGetShortName, types, types, V3.stringType); + } } diff --git a/aeneas/src/v3/V3Enum.v3 b/aeneas/src/v3/V3Enum.v3 index 523f0d368..4f7e29ac2 100644 --- a/aeneas/src/v3/V3Enum.v3 +++ b/aeneas/src/v3/V3Enum.v3 @@ -3,16 +3,16 @@ // The type for user-declared enums. class EnumType extends PrimType { - def enumDecl: VstEnum; + def enumDecl: VstClass; var setType: EnumSetType; var names: Record; var shortNames: Record; new(hash: int, enumDecl, typeCon: TypeCon) super(hash, typeCon, enumDecl.tagType.width, null) { } - def lookupCase(name: string) -> VstEnumCase { + def lookupCase(name: string) -> VstCaseMember { var member = enumDecl.memberMap[name]; - if (VstEnumCase.?(member)) return VstEnumCase.!(member); + if (VstCaseMember.?(member)) return VstCaseMember.!(member); return null; } def getNameOperator() -> Operator { @@ -21,58 +21,34 @@ class EnumType extends PrimType { } def getShortNameOperator() -> Operator { var types: Array = [this]; - return V3Op.newVstSugar(VstOperator.EnumGetShortName, types, types, V3.stringType); + return V3Op.newVstSugar(VstOperator.VariantGetShortName, types, types, V3.stringType); } def enumGetParamOperator(f: VstField) -> Operator { var types: Array = [this]; return V3Op.newVstSugar(VstOperator.EnumGetParam(f), TypeUtil.NO_TYPES, types, f.getType()); } // Get the qualified names array (e.g. "More.C" for a subtype case). - // Array is indexed by DFS enumTag; size = enumTagHi + 1. + // Array is indexed by DFS enumTag; size = tagHi + 1. def getNames(prog: Program) -> Record { if (names == null) { - var total = if(enumDecl.enumTagHi >= 0, enumDecl.enumTagHi + 1, enumDecl.cases.length); + var total = if(enumDecl.tagHi >= 0, enumDecl.tagHi + 1, enumDecl.cases.length); names = prog.newRecord(V3.stringArrayType, total); - fillNames(names, enumDecl, "", prog); + CaseNames.fillNames(names, enumDecl, "", prog); } return names; } - // Get the short (unqualified) names array (always just c.name()). - // Array is indexed by DFS enumTag; size = enumTagHi + 1. def getShortNames(prog: Program) -> Record { if (shortNames == null) { - var total = if(enumDecl.enumTagHi >= 0, enumDecl.enumTagHi + 1, enumDecl.cases.length); + var total = if(enumDecl.tagHi >= 0, enumDecl.tagHi + 1, enumDecl.cases.length); shortNames = prog.newRecord(V3.stringArrayType, total); - fillShortNames(shortNames, enumDecl, prog); + CaseNames.fillShortNames(shortNames, enumDecl, prog); } return shortNames; } - private def fillNames(rec: Record, decl: VstEnum, prefix: string, prog: Program) { - for (c in decl.cases) { - if (c.enumTag < 0 || Strings.equal(c.name(), "_")) continue; - var n = if(prefix.length == 0, c.name(), Strings.format2("%s.%s", prefix, c.name())); - rec.values[c.enumTag] = Byte.boxString(n, prog); - } - var subtypes = Lists.reverse(decl.subtypeOrder); - for (l = subtypes; l != null; l = l.tail) { - var sub = l.head; - var p = if(prefix.length == 0, sub.name(), Strings.format2("%s.%s", prefix, sub.name())); - fillNames(rec, sub, p, prog); - } - } - private def fillShortNames(rec: Record, decl: VstEnum, prog: Program) { - for (c in decl.cases) { - if (c.enumTag < 0 || Strings.equal(c.name(), "_")) continue; - rec.values[c.enumTag] = Byte.boxString(c.name(), prog); - } - for (l = decl.subtypeOrder; l != null; l = l.tail) { - fillShortNames(rec, l.head, prog); - } - } } // The type for user-declared enums' sets. class EnumSetType extends PrimType { - def enumDecl: VstEnum; + def enumDecl: VstClass; def repType: IntType; var allVal: Val; private var cache: V3EnumSet_OpCache; diff --git a/aeneas/src/vst/MethodEnv.v3 b/aeneas/src/vst/MethodEnv.v3 index 748135601..f37d4cb17 100644 --- a/aeneas/src/vst/MethodEnv.v3 +++ b/aeneas/src/vst/MethodEnv.v3 @@ -189,12 +189,6 @@ class MethodEnv { } return if(member != null, ResolvedMember.new(enumType, member, null, memberType)); } - def findRootEnumDecl(decl: VstEnum) -> VstEnum { - if (decl.qualifiers == null) return decl; - var tc = verifier.verifier.prog.typeEnv.lookup(decl.qualifiers.head.image); - if (tc == null || tc.kind != Kind.ENUM) return null; - return EnumType.!(tc.create0()).enumDecl; - } def resolveClassMember(expr: VarExpr, classType: ClassType, name: string) -> ResolvedMember { var exprType = classType; var member = classType.classDecl.memberMap[name]; @@ -273,12 +267,15 @@ class MethodEnv { var memberType = Function.prependParamType(variantType, member.memberType); return expr.bind(null, VarBinding.ClassMethod(variantType, x, member.typeArgs), memberType); } - x: VstVariantTagMember => { + x: VstTagMember => { return newCompBinding(expr, V3Op.newVariantGetTag(variantType)); } - x: VstVariantNameMember => { + x: VstNameMember => { return newCompBinding(expr, ClassType.!(member.receiver).getNameOperator()); } + x: VstShortNameMember => { + return newCompBinding(expr, ClassType.!(member.receiver).getShortNameOperator()); + } x: VstCaseMember => { var memberType = member.memberType; if (x.decl.params != null) { @@ -304,12 +301,15 @@ class MethodEnv { x: VstMethod => { return expr.bind(receiver, VarBinding.ObjectMethod(variantType, x, member.typeArgs), member.memberType); } - x: VstVariantTagMember => { + x: VstTagMember => { return newApplyCompBinding(expr, receiver, V3Op.newVariantGetTag(variantType)); } - x: VstVariantNameMember => { + x: VstNameMember => { return newApplyCompBinding(expr, receiver, ClassType.!(member.receiver).getNameOperator()); } + x: VstShortNameMember => { + return newApplyCompBinding(expr, receiver, ClassType.!(member.receiver).getShortNameOperator()); + } } return VarBinding.None; } @@ -320,15 +320,20 @@ class MethodEnv { var name = expr.ident.name; var member = resolveClassMember(expr, variantType, name.image); if (member != null) { - if (VstVariantTagMember.?(member.member)) { + if (VstTagMember.?(member.member)) { var tagType = V3.getVariantTagType(variantType); return expr.bind(null, VarBinding.Const(Int.box(V3.getVariantTag(variantType)), tagType), tagType); } - if (VstVariantNameMember.?(member.member)) { + if (VstNameMember.?(member.member)) { var names = V3Class_TypeCon.!(V3.getRootType(variantType).typeCon).getNames(verifier.verifier.prog); var name = names.values[V3.getVariantTag(variantType)]; return expr.bind(null, VarBinding.Const(name, V3.stringType), V3.stringType); } + if (VstShortNameMember.?(member.member)) { + var names = V3Class_TypeCon.!(V3.getRootType(variantType).typeCon).getShortNames(verifier.verifier.prog); + var name = names.values[V3.getVariantTag(variantType)]; + return expr.bind(null, VarBinding.Const(name, V3.stringType), V3.stringType); + } } if (variantType.classDecl.params != null) return lookupVariantTypeMember(expr, variantType); return lookupVariantExprMember(expr, variantType, false); @@ -348,8 +353,8 @@ class MethodEnv { if (width < 1) width = 1; var countType = Int.getType(false, width); return expr.bind(null, VarBinding.Const(Int.box(count), countType), countType); - } else if (Strings.equal(name, "shortName")) { - return newCompBinding(expr, enumType.getShortNameOperator()); +// } else if (Strings.equal(name, "shortName")) { +// return newCompBinding(expr, enumType.getShortNameOperator()); } var member = resolveEnumMember(expr, enumType, name); if (member == null) { @@ -368,15 +373,18 @@ class MethodEnv { var memberType = Function.newType(enumType, member.memberType); return expr.bind(null, VarBinding.Inst(enumType.enumGetParamOperator(x), N), memberType); } - x: VstVariantTagMember => { + x: VstTagMember => { var tagType = enumType.enumDecl.tagType; return newCompBinding(expr, V3Op.newVstSugar(VstOperator.Nop, [enumType, tagType], [enumType], tagType)); } - x: VstVariantNameMember => { + x: VstNameMember => { return newCompBinding(expr, enumType.getNameOperator()); } - x: VstEnumCase => { + x: VstShortNameMember => { + return newCompBinding(expr, enumType.getShortNameOperator()); + } + x: VstCaseMember => { return expr.bind(null, VarBinding.EnumConst(x), enumType); } x: VstMethod => { @@ -400,14 +408,17 @@ class MethodEnv { x: VstField => { return newApplyCompBinding(expr, expr.expr, enumType.enumGetParamOperator(x)); } - x: VstVariantTagMember => { + x: VstTagMember => { var tagType = enumType.enumDecl.tagType; return newApplyCompBinding(expr, receiver, V3Op.newVstSugar(VstOperator.Nop, [enumType, tagType], [enumType], tagType)); } - x: VstVariantNameMember => { + x: VstNameMember => { return newApplyCompBinding(expr, receiver, enumType.getNameOperator()); } + x: VstShortNameMember => { + return newApplyCompBinding(expr, receiver, enumType.getShortNameOperator()); + } x: VstMethod => { var methodReceiver = x.receiver.getDeclaredType(); return expr.bind(receiver, VarBinding.ObjectMethod(methodReceiver, x, member.typeArgs), member.memberType); @@ -415,13 +426,13 @@ class MethodEnv { } return VarBinding.None; } - def lookupEnumConstMember(expr: VarExpr, enumType: EnumType, vc: VstEnumCase) -> VarBinding { + def lookupEnumConstMember(expr: VarExpr, enumType: EnumType, vc: VstCaseMember) -> VarBinding { // lookup (expr: enum.val).name var name = expr.ident.name; if (Strings.equal(name.image, "shortName")) { // Return the short name as a compile-time constant using the DFS global tag. var shortNames = enumType.getShortNames(verifier.verifier.prog); - var idx = if(vc.enumTag >= 0, vc.enumTag, vc.tag); + var idx = vc.enumTag; if (idx < shortNames.values.length) { return expr.bind(null, VarBinding.Const(shortNames.values[idx], V3.stringType), V3.stringType); } @@ -438,15 +449,21 @@ class MethodEnv { return expr.bind(null, VarBinding.Const(val, x.getType()), x.getType()); } } - x: VstVariantTagMember => { + x: VstTagMember => { var tagType = enumType.enumDecl.tagType; // Use DFS global tag (enumTag) for runtime; fall back to parse-order tag if unassigned. - var tag = if(vc.enumTag >= 0, vc.enumTag, vc.tag); + var tag = vc.enumTag; return expr.bind(null, VarBinding.Const(Int.box(tag), tagType), tagType); } - x: VstVariantNameMember => { + x: VstNameMember => { var names = enumType.getNames(verifier.verifier.prog); - var idx = if(vc.enumTag >= 0, vc.enumTag, vc.tag); + var idx = vc.enumTag; + var n = if(idx < names.values.length, names.values[idx], null); + return expr.bind(null, VarBinding.Const(n, V3.stringType), V3.stringType); + } + x: VstShortNameMember => { + var names = enumType.getShortNames(verifier.verifier.prog); + var idx = vc.enumTag; var n = if(idx < names.values.length, names.values[idx], null); return expr.bind(null, VarBinding.Const(n, V3.stringType), V3.stringType); } @@ -468,14 +485,14 @@ class MethodEnv { } return lookupEnumExprMember(expr, enumType, false); } - def findEnumCaseOverride(vc: VstEnumCase, name: string) -> VstMethod { + def findEnumCaseOverride(vc: VstCaseMember, name: string) -> VstMethod { if (vc.members == null) return null; for (l = vc.members; l != null; l = l.tail) { if (VstMethod.?(l.head) && Strings.equal(l.head.name(), name)) return VstMethod.!(l.head); } return null; } - def findEnumSubtypeOverride(rootDecl: VstEnum, vc: VstEnumCase, name: string) -> VstMethod { + def findEnumSubtypeOverride(rootDecl: VstClass, vc: VstCaseMember, name: string) -> VstMethod { // Walk from the case's declaring enum up to the root, looking for overrides. var decl = findDeclaringEnum(rootDecl, vc); while (decl != null) { @@ -487,7 +504,7 @@ class MethodEnv { } return null; } - def findDeclaringEnum(rootDecl: VstEnum, vc: VstEnumCase) -> VstEnum { + def findDeclaringEnum(rootDecl: VstClass, vc: VstCaseMember) -> VstClass { // Find the enum that declares this case by searching the hierarchy. for (c in rootDecl.cases) { if (c == vc) return rootDecl; @@ -646,9 +663,9 @@ class MethodEnv { } return VarBinding.None; } - def findEnumCaseInHierarchy(decl: VstEnum, name: string) -> VstEnumCase { + def findEnumCaseInHierarchy(decl: VstClass, name: string) -> VstCaseMember { var m = decl.memberMap[name]; - if (VstEnumCase.?(m)) return VstEnumCase.!(m); + if (VstCaseMember.?(m)) return VstCaseMember.!(m); for (l = decl.subtypeOrder; l != null; l = l.tail) { var c = findEnumCaseInHierarchy(l.head, name); if (c != null) return c; diff --git a/aeneas/src/vst/Parser.v3 b/aeneas/src/vst/Parser.v3 index 140968412..abf9fb7c4 100644 --- a/aeneas/src/vst/Parser.v3 +++ b/aeneas/src/vst/Parser.v3 @@ -226,6 +226,9 @@ component Parser { var decl = desugar.synthesizeTopLevelClass(parseVariantCases(p, desugar)); decl.repHints = repHints; decl.qualifiers = qualifiers; + for (c in decl.cases) { + if (Strings.equal(c.name(), "_")) { decl.hasDefault = true; break; } + } decl.qualifierTypeArgs = qualTypeArgs; file.classes.put(decl); // lift case classes to top-level of file @@ -511,25 +514,18 @@ component Parser { } } if (!superParams) params = parseOptionalParams(p, parseEnumParam); - // Synthesize "tag" and "name" members - var tagToken = Token.new(p.fileName, "tag", p.curLine, p.curCol); - var nameToken = Token.new(p.fileName, "name", p.curLine, p.curCol); - var list: List; - list = List.new(VstVariantTagMember.new(tagToken), list); - list = List.new(VstVariantNameMember.new(nameToken), list); + var desugar = EnumDesugaring.new(isPrivate, id, params); + var list = desugar.synthesizeMembers(p.fileName, p.curLine, p.curCol); // Parse case members - var cases = Vector.new(); - var hasDefault = false; p.req1('{'); while (true) { if (p.curByte == '}') { p.advance1(); break; } if (p.curByte == ';') break; // methods section follows - var c = parseEnumCase(p, cases.length); + var c = parseEnumCase(p, desugar.cases.length); if (c == null) { p.opt1('}'); break; } - if (Strings.equal(c.name(), "_")) hasDefault = true; - cases.put(c); + desugar.addCase(c); list = List.new(c, list); - if (hasDefault) { + if (desugar.hasDefault) { if (p.curByte == ';') break; // methods after _ p.req1('}'); break; @@ -545,18 +541,17 @@ component Parser { list = nlist; } } - var decl = VstEnum.new(isPrivate, id.name, params, cases.extract(), list); - decl.hasDefault = hasDefault; + var decl = desugar.synthesizeTopLevelClass(list); decl.superParams = superParams; decl.qualifiers = qualifiers; file.enums.put(decl); return true; } - def parseEnumCase(p: ParserState, tag: int) -> VstEnumCase { + def parseEnumCase(p: ParserState, tag: int) -> VstCaseMember { // Handle "_" default case specially (not an identifier start in Virgil). if (p.curByte == '_' && !Char.isIdentMiddle(p.peek(1))) { var ut = p.token(1); - var c = VstEnumCase.new(tag, ut, null); + var c = VstCaseMember.new(tag, ut, null, null); if (p.curByte == '{') c.members = parseEnumCaseMembers(p); p.opt1(','); return c; @@ -564,7 +559,7 @@ component Parser { var id = parseIdentVoid(p); var args = if(p.curByte == '(', parseList(0, p, '(', COMMA, ')', parseExpr)); if (id.name.image.length > 0) { - var c = VstEnumCase.new(tag, id.name, args); + var c = VstCaseMember.new(tag, id.name, null, args); if (p.curByte == '{') c.members = parseEnumCaseMembers(p); p.opt1(','); return c; diff --git a/aeneas/src/vst/Verifier.v3 b/aeneas/src/vst/Verifier.v3 index 4b24756d2..0cf5ac0db 100644 --- a/aeneas/src/vst/Verifier.v3 +++ b/aeneas/src/vst/Verifier.v3 @@ -38,7 +38,7 @@ class Verifier(compiler: Compiler, prog: Program) { forAll(vst.packings, verifyPacking); forAll(vst.components, typeCheckVstCompound); forAll(vst.classes, typeCheckVstCompound); - forAll(vst.enums, typeCheckVstCompound); + forAll(vst.enums, typeCheckVstCompound); forAll(vst.layouts, typeCheckVstCompound); // Method override checks are deferred until after body inference. @@ -90,6 +90,19 @@ class Verifier(compiler: Compiler, prog: Program) { prog.vst.enums.putv(file.enums); for (i < file.enums.length) { var enumDecl = file.enums[i]; + enumDecl.tagType = V3.getTagType(enumDecl.cases.length); + var tc = TypeCon.new(enumDecl.token.image, Kind.ENUM, 0, prog.typeCache); + var enumType = EnumType.new(tc.uid, enumDecl, tc); + tc.singleton = enumType; + enumDecl.typeCon = tc; + if (enumDecl.cases.length <= Int.MAX_WIDTH) { + var setName = Strings.builderOf(enumDecl.token.image).puts(".set").toString(); + var stc = TypeCon.new(setName, Kind.ENUM_SET, 0, prog.typeCache); + var count = if(enumDecl.cases.length == 0, 1, enumDecl.cases.length); + var repType = Int.getType(false, count); + var enumSetType = EnumSetType.new(stc.uid, enumDecl, repType, stc); + enumType.setType = stc.singleton = enumSetType; + } var cv = makeVerifier(enumDecl, enumDecl.getDeclaredType(), file); cv.bindTypeCon(); cv.buildMembers(); @@ -127,13 +140,13 @@ class Verifier(compiler: Compiler, prog: Program) { var constructor = ic.methods[0]; if (constructor != null && VstComponent.?(decl)) addInit(constructor); } - def addEnumVals(decl: VstEnum) { + def addEnumVals(decl: VstClass) { if (decl.qualifiers != null) { var rootEnum = findRootEnum(decl); var needsInit = rootEnum != null && rootEnum.params != null; // Create enumVals for subtype-only (extra) params if (decl.extraParams != null) { - var arraySize = if(rootEnum.enumTagHi >= 0, rootEnum.enumTagHi + 1, decl.cases.length); + var arraySize = if(rootEnum.tagHi >= 0, rootEnum.tagHi + 1, decl.cases.length); for (l = decl.extraParams.list; l != null; l = l.tail) { var field = VstField.!(l.head.member); var arrayType = V3Array.newType(field.getType()); @@ -150,7 +163,7 @@ class Verifier(compiler: Compiler, prog: Program) { } if (decl.params == null) return; // Root enum: size param arrays to cover all DFS-assigned tags (including subtypes). - var arraySize = if(decl.enumTagHi >= 0, decl.enumTagHi + 1, decl.cases.length); + var arraySize = if(decl.tagHi >= 0, decl.tagHi + 1, decl.cases.length); for (l = decl.params.list; l != null; l = l.tail) { var field = VstField.!(l.head.member); var arrayType = V3Array.newType(field.getType()); @@ -304,7 +317,7 @@ class Verifier(compiler: Compiler, prog: Program) { q = q.tail; // Walk any remaining qualifiers (for names like A.B.C). while (q != null) { - if (!variantHasDefaultCase(parentClass)) { + if (!parentClass.hasDefault) { cv.errAtToken(q.head).set("VariantDeclError", Strings.format1("variant \"%s\" has no 'case _'", parentClass.name())); return; @@ -326,7 +339,7 @@ class Verifier(compiler: Compiler, prog: Program) { q = q.tail; } // parentClass is the immediate parent of decl. - if (!variantHasDefaultCase(parentClass)) { + if (!parentClass.hasDefault) { cv.errAtDecl(decl).set("VariantDeclError", Strings.format1("variant \"%s\" has no 'case _'", parentClass.name())); return; @@ -378,7 +391,7 @@ class Verifier(compiler: Compiler, prog: Program) { parentClassType.superType = parentClass.verifier.superType; // If the parent has a case _, route inheritance through it so its methods are // visible to this subtype (and may override parent methods). - var defaultCaseDecl = findDefaultCase(parentClass); + var defaultCaseDecl = parentClass.findDefaultCase(); if (defaultCaseDecl != null) verifyClass(defaultCaseDecl); var effectiveSuperDecl = if(defaultCaseDecl != null, defaultCaseDecl, parentClass); var effectiveSuperType = ClassType.!(if(typeParamTypes != null, @@ -420,20 +433,6 @@ class Verifier(compiler: Compiler, prog: Program) { } } } - // Returns true if the variant class has a "case _" default case. - def variantHasDefaultCase(decl: VstClass) -> bool { - for (c in decl.cases) { - if (Strings.equal(c.name(), "_")) return true; - } - return false; - } - // Returns the synthetic VstClass for the "case _" of a variant, or null if none. - def findDefaultCase(decl: VstClass) -> VstClass { - for (c in decl.cases) { - if (Strings.equal(c.name(), "_")) return c.decl; - } - return null; - } // Returns true if this variant has at least one named "case id" (not "case _"). def variantHasNamedCases(decl: VstClass) -> bool { if (decl.cases == null) return false; @@ -452,7 +451,7 @@ class Verifier(compiler: Compiler, prog: Program) { sd.isSynthetic = true; sd.fullName = Strings.format1("%s.$default", parent.fullName); sd.variantTag = tag; - sd.variantTagHi = tag; + sd.tagHi = tag; parent.syntheticDefault = sd; } // Entry point for DFS tag assignment: only process root variants (no qualifiers). @@ -474,13 +473,13 @@ class Verifier(compiler: Compiler, prog: Program) { for (l = decl.subtypeOrder; l != null; l = l.tail) { nextTag = assignTagsDFS(l.head, nextTag); } - var defaultCase = findDefaultCase(decl); + var defaultCase = decl.findDefaultCase(); if (defaultCase != null) { - defaultCase.variantTagHi = nextTag - 1; + defaultCase.tagHi = nextTag - 1; } } } - decl.variantTagHi = nextTag - 1; + decl.tagHi = nextTag - 1; // Recompute the tag type to fit all newly assigned tags. var width = 1, exp = 2; while (exp < nextTag) { exp = exp << 1; width++; } @@ -496,7 +495,7 @@ class Verifier(compiler: Compiler, prog: Program) { var hasChildren = (decl.cases != null && decl.cases.length > 0) || decl.subtypeOrder != null; if (!hasChildren) { // Leaf: consume exactly one tag. - decl.variantTagHi = nextTag; + decl.tagHi = nextTag; return nextTag + 1; } // Non-leaf: distribute range to children in declaration order. @@ -510,12 +509,12 @@ class Verifier(compiler: Compiler, prog: Program) { nextTag = assignTagsDFS(l.head, nextTag); } // Extend "case _" tagHi to cover the subtypes' range. - var defaultCase = findDefaultCase(decl); + var defaultCase = decl.findDefaultCase(); if (defaultCase != null) { - defaultCase.variantTagHi = nextTag - 1; + defaultCase.tagHi = nextTag - 1; } } - decl.variantTagHi = nextTag - 1; + decl.tagHi = nextTag - 1; return nextTag; } // Propagate the root's tagType down to all cases and subtype variants. @@ -529,11 +528,11 @@ class Verifier(compiler: Compiler, prog: Program) { propagateTagType(l.head, tagType); } } - def verifyEnum(decl: VstEnum) { + def verifyEnum(decl: VstClass) { var cv = decl.verifier; cv.verify(); } - def fixEnumMethodRoots(decl: VstEnum) { + def fixEnumMethodRoots(decl: VstClass) { // After verifyEnumQualifiers sets parentEnum, fix method root pointers // for subtype enums. During verifyEnum, parentEnum isn't set yet, so // checkMethod treats subtype override methods as roots (root = self). @@ -543,7 +542,7 @@ class Verifier(compiler: Compiler, prog: Program) { if (!VstMethod.?(l.head)) continue; var m = VstMethod.!(l.head); if (m.root != m) continue; // already correctly set - var parentMethod = cv.findEnumMethodInParents(VstEnum.!(cv.compound), m.name()); + var parentMethod = cv.findEnumMethodInParents(VstClass.!(cv.compound), m.name()); if (parentMethod != null) { m.root = parentMethod.root; m.index = parentMethod.index; @@ -562,7 +561,7 @@ class Verifier(compiler: Compiler, prog: Program) { } } } - def verifyEnumQualifiers(decl: VstEnum) { + def verifyEnumQualifiers(decl: VstClass) { var q = decl.qualifiers; if (q == null) return; // no qualifiers → root enum, nothing to resolve var cv = decl.verifier; @@ -677,7 +676,7 @@ class Verifier(compiler: Compiler, prog: Program) { if (decl.constructor != null) decl.constructor.markNontrivial(); } } - def findRootEnum(decl: VstEnum) -> VstEnum { + def findRootEnum(decl: VstClass) -> VstClass { var root = decl; if (root.qualifiers != null) { var tc = prog.typeEnv.lookup(root.qualifiers.head.image); @@ -689,7 +688,7 @@ class Verifier(compiler: Compiler, prog: Program) { } // Build the effective param list for an enum: root params + all ancestor extras // in root-to-target order. Returns null if no effective params. - def buildEffectiveParams(rootDecl: VstEnum, target: VstEnum) -> List { + def buildEffectiveParams(rootDecl: VstClass, target: VstClass) -> List { var result: List; if (rootDecl.params != null) { for (l = rootDecl.params.list; l != null; l = l.tail) { @@ -711,7 +710,7 @@ class Verifier(compiler: Compiler, prog: Program) { } return Lists.reverse(result); } - def assignEnumTagsIfRoot(decl: VstEnum) { + def assignEnumTagsIfRoot(decl: VstClass) { if (decl.qualifiers != null) return; // skip subtype enums var nextTag = 0; nextTag = assignEnumTagsDFS(decl, nextTag); @@ -722,8 +721,8 @@ class Verifier(compiler: Compiler, prog: Program) { var tagType = Int.getType(false, byte.!(width)); propagateEnumTagType(decl, tagType); } - def assignEnumTagsDFS(decl: VstEnum, nextTag: int) -> int { - decl.enumTagLo = nextTag; + def assignEnumTagsDFS(decl: VstClass, nextTag: int) -> int { + decl.tagLo = nextTag; // Assign named cases (excluding _) in declaration order. for (c in decl.cases) { if (Strings.equal(c.name(), "_")) continue; @@ -739,16 +738,16 @@ class Verifier(compiler: Compiler, prog: Program) { for (c in decl.cases) { if (Strings.equal(c.name(), "_")) c.enumTag = defaultLo; } - decl.enumTagHi = nextTag - 1; + decl.tagHi = nextTag - 1; return nextTag; } - def propagateEnumTagType(decl: VstEnum, tagType: IntType) { + def propagateEnumTagType(decl: VstClass, tagType: IntType) { decl.tagType = tagType; for (l = decl.subtypeOrder; l != null; l = l.tail) { propagateEnumTagType(l.head, tagType); } } - def finalizeEnumType(decl: VstEnum) { + def finalizeEnumType(decl: VstClass) { if (decl.qualifiers != null) return; // only process from root var enumType = EnumType.!(decl.typeCon.singleton); var isHierarchy = decl.hasDefault || decl.subtypeOrder != null; @@ -758,7 +757,7 @@ class Verifier(compiler: Compiler, prog: Program) { return; } // Hierarchy enum: recreate set type with DFS-aware sizing. - var totalTags = if(decl.enumTagHi >= 0, decl.enumTagHi + 1, decl.cases.length); + var totalTags = if(decl.tagHi >= 0, decl.tagHi + 1, decl.cases.length); if (totalTags > Int.MAX_WIDTH) { enumType.setType = null; // too large for set return; @@ -775,7 +774,7 @@ class Verifier(compiler: Compiler, prog: Program) { // Propagate shared set type to subtypes. propagateEnumSetType(decl, enumSetType); } - def computeEnumSetAll(decl: VstEnum, repType: IntType) -> Val { + def computeEnumSetAll(decl: VstClass, repType: IntType) -> Val { var bits: long = 0; for (c in decl.cases) { if (c.enumTag >= 0 && !Strings.equal(c.name(), "_")) { @@ -789,7 +788,7 @@ class Verifier(compiler: Compiler, prog: Program) { } return if(repType.width <= 32, Int.box(int.!(bits)), Long.box(bits)); } - def propagateEnumSetType(decl: VstEnum, setType: EnumSetType) { + def propagateEnumSetType(decl: VstClass, setType: EnumSetType) { for (l = decl.subtypeOrder; l != null; l = l.tail) { EnumType.!(l.head.typeCon.singleton).setType = setType; propagateEnumSetType(l.head, setType); @@ -895,15 +894,15 @@ class Verifier(compiler: Compiler, prog: Program) { tc.enterMethod(x, env); // TODO: ugly side-effect of creating thisParam if (!EmptyStmt.?(x.func.body)) tc.typeCheckBody(x.func.body); } - x: VstEnumCase => { + x: VstCaseMember => { tc.enterCompound(decl, false, env); - typeCheckEnumCase(VstEnum.!(decl), x, tc); + typeCheckEnumCase(VstClass.!(decl), x, tc); } } } // Type-check per-case method override bodies. - if (VstEnum.?(decl)) { - for (c in VstEnum.!(decl).cases) { + if (VstClass.?(decl) && VstClass.!(decl).isEnum()) { + for (c in VstClass.!(decl).cases) { if (c.members == null) continue; for (l = c.members; l != null; l = l.tail) { if (VstMethod.?(l.head)) { @@ -919,7 +918,7 @@ class Verifier(compiler: Compiler, prog: Program) { def typeCheckNew(decl: VstCompound, cdecl: VstNew, tc: TypeChecker) { var cv = decl.verifier, thisParam = cv.newThisParam(), pl = cdecl.func.params; var initEnv: MethodEnv; - if (VstClass.?(decl) || VstEnum.?(decl)) { + if (VstClass.?(decl)) { // track field initialization state for class constructors initEnv = tc.enterCompound(decl, true, env); } else { @@ -969,7 +968,7 @@ class Verifier(compiler: Compiler, prog: Program) { initEnv.initOrder = 0; // all fields can be used in the constructor tc.typeCheckBody(cdecl.func.body); } - def typeCheckEnumCase(decl: VstEnum, c: VstEnumCase, tc: TypeChecker) { + def typeCheckEnumCase(decl: VstClass, c: VstCaseMember, tc: TypeChecker) { if (Strings.equal(c.name(), "_")) return; // _ case has no args var cv = decl.verifier; // Build effective param types: root params + all ancestor extras + own extras @@ -1084,7 +1083,7 @@ class VstCompoundVerifier { ERROR = verifier.ERROR; if (VstClass.?(compound)) { classDecl = VstClass.!(compound); - classType = ClassType.!(thisType); + if (ClassType.?(thisType)) classType = ClassType.!(thisType); } else if (VstComponent.?(compound)) { isImport = VstComponent.!(compound).importName != null; } @@ -1191,8 +1190,8 @@ class VstCompoundVerifier { typeEnv.add(decl.typeCon); } def verify() { - if (VstEnum.?(compound)) { - if (VstEnum.!(compound).cases.length == 0) { + if (classDecl != null && classDecl.isEnum()) { + if (classDecl.cases.length == 0) { errAtDecl(compound).set("EnumDeclError", "enum requires at least 1 case"); } } @@ -1205,8 +1204,8 @@ class VstCompoundVerifier { } } // Verify per-case method overrides for enums. - if (VstEnum.?(compound)) { - for (c in VstEnum.!(compound).cases) { + if (classDecl != null && classDecl.isEnum()) { + for (c in classDecl.cases) { if (c.members != null) checkEnumCaseMembers(c); } } @@ -1330,8 +1329,8 @@ class VstCompoundVerifier { var binding = lookupSuperMember(decl.name()); if (binding == null) { // Check parent enum chain for subtype enum method overrides. - if (VstEnum.?(compound) && VstEnum.!(compound).parentEnum != null) { - var parentMethod = findEnumMethodInParents(VstEnum.!(compound), decl.name()); + if (VstClass.?(compound) && VstClass.!(compound).parentEnum != null) { + var parentMethod = findEnumMethodInParents(VstClass.!(compound), decl.name()); if (parentMethod != null) { decl.root = parentMethod.root; decl.index = parentMethod.index; @@ -1446,7 +1445,7 @@ class VstCompoundVerifier { errAtDecl(decl).InheritanceError2("method signature %s cannot override %s", methodType, superMethodType); } } - def checkEnumCaseMembers(c: VstEnumCase) { + def checkEnumCaseMembers(c: VstCaseMember) { for (l = c.members; l != null; l = l.tail) { match (l.head) { x: VstMethod => checkEnumCaseMethod(x, c); @@ -1454,12 +1453,12 @@ class VstCompoundVerifier { } } } - def checkEnumCaseMethod(decl: VstMethod, ecase: VstEnumCase) { + def checkEnumCaseMethod(decl: VstMethod, ecase: VstCaseMember) { decl.receiver = compound; // Look up the root method in the enum's member map or parent chain. var rootMember = compound.memberMap[decl.name()]; - if ((rootMember == null || !VstMethod.?(rootMember)) && VstEnum.?(compound)) { - var pm = findEnumMethodInParents(VstEnum.!(compound), decl.name()); + if ((rootMember == null || !VstMethod.?(rootMember)) && compound.isEnum()) { + var pm = findEnumMethodInParents(VstClass.!(compound), decl.name()); if (pm != null) rootMember = pm; } if (rootMember == null || !VstMethod.?(rootMember)) { @@ -1493,7 +1492,7 @@ class VstCompoundVerifier { errAtDecl(decl).InheritanceError2("method signature %s cannot override %s", methodType, superMethodType); } } - def findEnumMethodInParents(decl: VstEnum, name: string) -> VstMethod { + def findEnumMethodInParents(decl: VstClass, name: string) -> VstMethod { var cur = decl.parentEnum; while (cur != null) { var member = cur.memberMap[name]; @@ -2230,7 +2229,7 @@ class TypeChecker(ERROR: ErrorGen, file: VstFile) extends VstVisitor } return errAtExpr(ve).MatchError(Strings.format2("unresolved case %q.%s", vt.render, name)); } - mv.addVariantPattern(pat, vc); + mv.addCasePattern(pat, vc); } // Returns true if {candidate} is a (direct or transitive) subtype variant of {parent}. // Checks that candidate.qualifiers starts with parent's qualifier chain followed by parent's name. @@ -2268,7 +2267,7 @@ class TypeChecker(ERROR: ErrorGen, file: VstFile) extends VstVisitor if (Strings.equal(vc.name(), "_")) { return errAtExpr(ve).MatchError(Strings.format1("cannot explicitly match '_ case' of enum %q; use a default arm instead", vt.render)); } - mv.addEnumPattern(pat, vc); + mv.addCasePattern(pat, vc); } def visitEmpty(stmt: EmptyStmt, outer: Type) -> Type { return Void.TYPE; @@ -3297,19 +3296,12 @@ class MatchStmtVerifier(etype: Type, tc: TypeChecker) { pat.val = val; valueMap[val] = pat; } - def addVariantPattern(pat: MatchPattern, vc: VstCaseMember) { - if (covered[vc.tag] != null) { - MatchError(pat.expr.range(), "duplicate variant pattern", covered[vc.tag].expr.range()); - } - covered[vc.tag] = pat; - pat.val = Int.box(vc.decl.variantTag); - } - def addEnumPattern(pat: MatchPattern, vc: VstEnumCase) { - if (covered[vc.tag] != null) { - MatchError(pat.expr.range(), "duplicate case", covered[vc.tag].expr.range()); + def addCasePattern(pat: MatchPattern, vc: VstCaseMember) { + if (covered[vc.localTag] != null) { + MatchError(pat.expr.range(), "duplicate case", covered[vc.localTag].expr.range()); } - covered[vc.tag] = pat; - pat.val = Int.box(vc.enumTag); // use DFS global tag for runtime comparison + covered[vc.localTag] = pat; + pat.val = Int.box(vc.runtimeTag()); } def addTypePattern(pat: MatchPattern, t: Type) { var cast = TypeSystem.newTypeCast(etype, t); @@ -3334,7 +3326,7 @@ class MatchStmtVerifier(etype: Type, tc: TypeChecker) { var tag = ct.classDecl.variantTag; var vc = variantType.classDecl.cases[tag]; pat.variantCase = vc; - return addVariantPattern(pat, vc); + return addCasePattern(pat, vc); } } } diff --git a/aeneas/src/vst/Vst.v3 b/aeneas/src/vst/Vst.v3 index f15eff806..a5f38e92d 100644 --- a/aeneas/src/vst/Vst.v3 +++ b/aeneas/src/vst/Vst.v3 @@ -5,7 +5,7 @@ class VstModule { def classes = Vector.new(); def components = Vector.new(); - def enums = Vector.new(); + def enums = Vector.new(); def exports = Vector.new(); def layouts = Vector.new(); def packings = Vector.new(); @@ -19,7 +19,7 @@ class VstModule { class VstFile extends ParsedFile { def classes = Vector.new(); def components = Vector.new(); - def enums = Vector.new(); + def enums = Vector.new(); def exports = Vector.new(); def layouts = Vector.new(); def packings = Vector.new(); @@ -137,9 +137,9 @@ class VstCompound extends Decl { def getParams() -> List { return if(params != null, params.list); } - def isEnum() -> bool { return VstEnum.?(this); } - def isVariant() -> bool { return variantTag > NON_VARIANT; } - def isVariantCase() -> bool { return variantTag >= 0; } + def isEnum() => false; + def isVariant() => false; + def isVariantCase() -> bool { return isVariant() && variantTag >= 0; } def isUnboxed() -> bool { var hints = repHints; @@ -176,8 +176,13 @@ class VstClass extends VstCompound { var qualifierTypeArgs: List>; // type args per qualifier, parallel to qualifiers var subtypes: HashMap; // direct subtype variants of this variant (filled by verifier) var subtypeOrder: List; // subtypes in reverse declaration order (complement to subtypes map) - var variantTagHi: int = -1; // inclusive upper bound of DFS tag range; = variantTag for leaf cases + var tagLo: int = -1; // first DFS tag in subtype's range (for enum/variant subtype range checks) + var tagHi: int = -1; // inclusive upper bound of DFS tag range; = variantTag for leaf cases var syntheticDefault: VstClass; // hidden "$default" case for types with no named cases + var hasDefault: bool; // true if has a "case _" (open type) + var superParams: bool; // if enum mentions 'super' params + var extraParams: VstList; // enum subtype-only params (new fields beyond root's) + var parentEnum: VstClass; // immediate parent in hierarchy (set by verifier) new(isPrivate: bool, name: Token, typeParams: List, params: VstList, superclass, superclause: SuperClause, members: List) @@ -185,6 +190,14 @@ class VstClass extends VstCompound { def hasDescriptor() -> bool { return descriptorInfo != null && descriptorInfo.descriptorTypeRef != null; } + def isEnum() => kind == Kind.ENUM; + def isVariant() => kind == Kind.VARIANT; + def findDefaultCase() -> VstClass { + for (c in cases) { + if (Strings.equal(c.name(), "_")) return c.decl; + } + return null; + } } // Parsed "component X { ... }" class VstComponent extends VstCompound { @@ -193,37 +206,6 @@ class VstComponent extends VstCompound { var recordIndex: int; new(isPrivate: bool, isThread, importName, name: Token, members: List) super(isPrivate, name, null, null, null, members) { } } -// Parsed "enum X { ... }" -class VstEnum extends VstCompound { - def cases: Array; - var tagType: IntType; - var hasDefault: bool; // true if enum has a "case _" (open enum) - var superParams: bool; // true if enum declared (super) params - var extraParams: VstList; // subtype-only params (new fields beyond root's) - var parentEnum: VstEnum; // immediate parent in hierarchy (set by verifier) - var qualifiers: List; // dot-separated qualifier path, e.g. [E1] for "enum E1.More" - var subtypes: HashMap; // direct subtype enums (filled by verifier) - var subtypeOrder: List; // subtypes in reverse declaration order - var enumTagLo: int = -1; // -1 for root; first DFS tag for subtypes - var enumTagHi: int = -1; // last DFS tag covering all subtypes - - new(isPrivate: bool, name: Token, params: VstList, cases, members: List) - super(isPrivate, name, null, params, null, members) { - tagType = V3.getTagType(cases.length); - typeCon = TypeCon.new(token.image, Kind.ENUM, 0, TypeUtil.globalCache); - var enumType = EnumType.new(typeCon.uid, this, typeCon); - typeCon.singleton = enumType; - typeCon = typeCon; - if (cases.length <= Int.MAX_WIDTH) { - var setName = Strings.builderOf(token.image).puts(".set").toString(); - var typeCon = TypeCon.new(setName, Kind.ENUM_SET, 0, TypeUtil.globalCache); - var count = if(cases.length == 0, 1, cases.length); - var repType = Int.getType(false, count); - var enumSetType = EnumSetType.new(typeCon.uid, this, repType, typeCon); - enumType.setType = typeCon.singleton = enumSetType; - } - } -} // Parsed "layout L { ... }" class VstLayout extends VstCompound { def size: Literal; @@ -244,17 +226,6 @@ class VstPacking extends VstCompound { new(name: Token, pparams, expr, size) super(false, name, null, null, null, null) {} } -// Member of an enum. -class VstEnumCase extends VstMember { - def tag: int; - def args: VstList; - var enumTag: int = -1; // DFS-assigned global tag; -1 until assigned - var members: List; // per-case method overrides - new(tag, name: Token, args) super(false, name) { } - def getType() -> Type { - return receiver.getDeclaredType(); - } -} // Member of a layout class VstLayoutField extends VstMember { def offset: Literal; @@ -286,17 +257,19 @@ class VariantDesugaring(isPrivate: bool, outer: VstIdent, params: def synthesizeMembers(fileName: string, line: int, col: int) -> List { var tagToken = Token.new(fileName, "tag", line, col); var nameToken = Token.new(fileName, "name", line, col); + var shortNameToken = Token.new(fileName, "shortName", line, col); var list: List; if (params == null) { - list = List.new(VstVariantTagMember.new(tagToken), list); - list = List.new(VstVariantNameMember.new(nameToken), list); + list = List.new(VstTagMember.new(tagToken), list); + list = List.new(VstNameMember.new(nameToken), list); + list = List.new(VstShortNameMember.new(shortNameToken), list); } return list; } def synthesizeTopLevelClass(members: List) -> VstClass { var decl = VstClass.new(isPrivate, outer.name, outer.list(), params, null, null, members); if (params != null) { - cases.put(VstCaseMember.new(0, outer.name, decl)); + cases.put(VstCaseMember.new(0, outer.name, decl, null)); decl.variantTag = 0; } else { decl.variantTag = ROOT_VARIANT; @@ -316,11 +289,37 @@ class VariantDesugaring(isPrivate: bool, outer: VstIdent, params: var buf = StringBuilder.new().puts(superclass.name.image); buf.putc('.'); synthetic.fullName = buf.puts(id.name.image).toString(); - var c = VstCaseMember.new(tag, id.name, synthetic); + var c = VstCaseMember.new(tag, id.name, synthetic, null); cases.put(c); return c; } } +// A helper class for desugaring enums into VstClasses. +class EnumDesugaring(isPrivate: bool, id: VstIdent, params: VstList) { + var cases = Vector.new(); + var hasDefault = false; + def synthesizeMembers(fileName: string, line: int, col: int) -> List { + var tagToken = Token.new(fileName, "tag", line, col); + var nameToken = Token.new(fileName, "name", line, col); + var shortNameToken = Token.new(fileName, "shortName", line, col); + var list: List; + list = List.new(VstTagMember.new(tagToken), list); + list = List.new(VstNameMember.new(nameToken), list); + list = List.new(VstShortNameMember.new(shortNameToken), list); + return list; + } + def synthesizeTopLevelClass(members: List) -> VstClass { + var decl = VstClass.new(isPrivate, id.name, null, params, null, null, members); + decl.kind = Kind.ENUM; + decl.cases = cases.extract(); + decl.hasDefault = hasDefault; + return decl; + } + def addCase(c: VstCaseMember) { + if (Strings.equal(c.name(), "_")) hasDefault = true; + cases.put(c); + } +} // A member of a class or component class VstMember extends Decl { @@ -339,30 +338,42 @@ class VstMember extends Decl { return buf.puts(token.image); } } -// Desugared case member from a variant. +// Desugared case member from a variant/enum. class VstCaseMember extends VstMember { - def tag: int; + def localTag: int; // local parse-order index (for covered[] array in match) def decl: VstClass; + def args: VstList; // for enums + var enumTag: int; // runtime tag; initialized to localTag, may be reassigned by DFS + var members: List; // per-case method overrides - new(tag, name: Token, decl) super(false, name) { } + new(localTag, name: Token, decl, args) super(false, name) { + enumTag = localTag; + } + def runtimeTag() -> int { + return if(decl != null, decl.variantTag, enumTag); + } def getType() -> Type { return if(decl == null, receiver.declType, decl.declType); } } -// Synthesized .tag member from a variant. -class VstVariantTagMember extends VstMember { +// Synthesized .tag member from a variant/enum. +class VstTagMember extends VstMember { new(name: Token) super(false, name) { } def getType() -> Type { match (receiver) { x: VstClass => return x.tagType; - x: VstEnum => return x.tagType; _ => return null; } } } -// Synthesized .name member from a variant. -class VstVariantNameMember extends VstMember { +// Synthesized .name member from a variant/enum. +class VstNameMember extends VstMember { + new(name: Token) super(false, name) { } + def getType() -> Type { return V3.stringType; } +} +// Synthesized .shortName member from a variant/enum. +class VstShortNameMember extends VstMember { new(name: Token) super(false, name) { } def getType() -> Type { return V3.stringType; } } @@ -998,7 +1009,7 @@ type VarBinding { case ComponentMethod(member: VstMethod, typeArgs: TypeArgs); case VariantCase(receiver: Type, member: VstCaseMember); case VariantField(receiver: Type, member: VstField); // writable if unboxed - case EnumConst(member: VstEnumCase); + case EnumConst(member: VstCaseMember); case Inst(op: Operator, facts: Fact.set); // instantiate {op} case Apply(op: Operator, facts: Fact.set); // apply {op} to receiver case Partial(op: Operator, facts: Fact.set); // partially apply {op} to receiver @@ -1089,12 +1100,11 @@ type VstOperator { case FloatFraction; // desugared to (u23|u52).!(u32|u64.view(float|double)) case VariantGetName; // desugared to ArrayGetElem(#names, VariantGetTag(x)) - - case EnumGetShortName; // desugared to ArrayGetElem(#shortNames, x) + case VariantGetShortName; // desugared to ArrayGetElem(#shortNames, VariantGetTag(x)) case EnumGetParam(field: VstField); // desugared to ArrayGetElem(#field, x) - case EnumSetHas(vcase: VstEnumCase); // desugared to (1 << k) & x != 0 + case EnumSetHas(vcase: VstCaseMember); // desugared to (1 << k) & x != 0 case EnumSetUnion; // desugared to x | y case EnumSetIntersect; // desugared to x & y case EnumSetSubtract; // desugared to x & ~(x & y) diff --git a/apps/vctags/vctags.v3 b/apps/vctags/vctags.v3 index 86d5623d1..c023a78e7 100644 --- a/apps/vctags/vctags.v3 +++ b/apps/vctags/vctags.v3 @@ -124,7 +124,7 @@ component vctags { TagKind.ENUM, edecl.token); file.tags.put(tag); generateCompoundMemberTags(file, edecl, CompoundKind.ENUM); - generateDeclTags(file, Arrays.map(edecl.cases, VstMember.!), CompoundKind.ENUMCASE); + generateDeclTags(file, Arrays.map(edecl.cases, VstMember.!), CompoundKind.ENUMCASE); generateParamTags(file, edecl.params, CompoundKind.CLASS); } diff --git a/test/open_types/name0.v3 b/test/open_types/name0.v3 new file mode 100644 index 000000000..ab5cd317c --- /dev/null +++ b/test/open_types/name0.v3 @@ -0,0 +1,19 @@ +//@execute 0=1; 1=11; 2=14; 3=15 +// Check length of name +type A { + case X; + case _; +} +type A.BB { + case Y; +} +type A.CCC { + case Z; +} +def main(arg: int) -> int { + var a = A.BB.Y; + if (arg == 1) return 10 + A.X.name.length; + if (arg == 2) return 10 + A.BB.Y.name.length; + if (arg == 3) return 10 + A.CCC.Z.name.length; + return 1; +} diff --git a/test/open_types/name1.v3 b/test/open_types/name1.v3 new file mode 100644 index 000000000..5b273dbc0 --- /dev/null +++ b/test/open_types/name1.v3 @@ -0,0 +1,19 @@ +//@execute 0=1; 1=11; 2=11; 3=12 +// Check length of shortName +type A { + case X; + case _; +} +type A.BB { + case Y; +} +type A.CCC { + case ZZ; +} +def main(arg: int) -> int { + var a = A.BB.Y; + if (arg == 1) return 10 + A.X.shortName.length; + if (arg == 2) return 10 + A.BB.Y.shortName.length; + if (arg == 3) return 10 + A.CCC.ZZ.shortName.length; + return 1; +} diff --git a/test/open_types/name2.v3 b/test/open_types/name2.v3 new file mode 100644 index 000000000..2b8e320d8 --- /dev/null +++ b/test/open_types/name2.v3 @@ -0,0 +1,19 @@ +//@execute 0=11; 1=11; 2=14; 3=15 +// Check length of name when assigned to variable +type A { + case X; + case _; +} +type A.BB { + case Y; +} +type A.CCC { + case Z; +} +def main(arg: int) -> int { + var a: A; + if (arg == 1) a = A.X; + if (arg == 2) a = A.BB.Y; + if (arg == 3) a = A.CCC.Z; + return 10 + a.name.length; +} diff --git a/test/open_types/name3.v3 b/test/open_types/name3.v3 new file mode 100644 index 000000000..2fb2cea18 --- /dev/null +++ b/test/open_types/name3.v3 @@ -0,0 +1,19 @@ +//@execute 0=11; 1=11; 2=12; 3=13 +// Check length of shortName when assigned to variable +type A { + case X; + case _; +} +type A.BB { + case YY; +} +type A.CCC { + case ZZZ; +} +def main(arg: int) -> int { + var a: A; + if (arg == 1) a = A.X; + if (arg == 2) a = A.BB.YY; + if (arg == 3) a = A.CCC.ZZZ; + return 10 + a.shortName.length; +} From 156212fafd7dda7c93da406285e4d2aca3be3426 Mon Sep 17 00:00:00 2001 From: Eliot Moss Date: Tue, 7 Apr 2026 18:26:30 -0400 Subject: [PATCH 03/58] Fix -O3 CI failure --- aeneas/src/core/Eval.v3 | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/aeneas/src/core/Eval.v3 b/aeneas/src/core/Eval.v3 index 32051c95c..ddb105c75 100644 --- a/aeneas/src/core/Eval.v3 +++ b/aeneas/src/core/Eval.v3 @@ -852,8 +852,9 @@ def evalOp(op: Operator, args: Arguments) -> Result { } //---------------------------------------------------------------------------- NullCheck => { - var object = args.r(0); - if (object == null) return args.throw(V3Exception.NullCheck, null); + var val = args.vals[0]; + if (!Record.?(val)) return Values.BOTTOM; // non-ref values (e.g. enums) are never null + if (val == null) return args.throw(V3Exception.NullCheck, null); return Values.BOTTOM; } BoundsCheck => { From 6984a02fc7b3af742ec0a72516e57ea58cfc6398 Mon Sep 17 00:00:00 2001 From: Eliot Moss Date: Tue, 7 Apr 2026 20:50:24 -0400 Subject: [PATCH 04/58] Fix was not quite right --- aeneas/src/core/Eval.v3 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aeneas/src/core/Eval.v3 b/aeneas/src/core/Eval.v3 index ddb105c75..73bb70f43 100644 --- a/aeneas/src/core/Eval.v3 +++ b/aeneas/src/core/Eval.v3 @@ -853,8 +853,8 @@ def evalOp(op: Operator, args: Arguments) -> Result { //---------------------------------------------------------------------------- NullCheck => { var val = args.vals[0]; - if (!Record.?(val)) return Values.BOTTOM; // non-ref values (e.g. enums) are never null if (val == null) return args.throw(V3Exception.NullCheck, null); + if (!Record.?(val)) return Values.BOTTOM; // non-ref values (e.g. enums) are never null return Values.BOTTOM; } BoundsCheck => { From ec83c26d7d03efa982f0e360253b6aa9485adf84 Mon Sep 17 00:00:00 2001 From: Eliot Moss Date: Tue, 7 Apr 2026 21:34:52 -0400 Subject: [PATCH 05/58] Need to set facts to suppress null check on enum method calls, to avoid improperly identifying 0 tag with null --- aeneas/src/ir/SsaNormalizer.v3 | 2 ++ 1 file changed, 2 insertions(+) diff --git a/aeneas/src/ir/SsaNormalizer.v3 b/aeneas/src/ir/SsaNormalizer.v3 index 5f9639428..a75cbdb76 100644 --- a/aeneas/src/ir/SsaNormalizer.v3 +++ b/aeneas/src/ir/SsaNormalizer.v3 @@ -255,6 +255,7 @@ class SsaRaNormalizer extends SsaRebuilder { ai_new = Arrays.prepend(newGraph.nullReceiver(), ai_new); } } + if (EnumType.?(rc.oldType)) i_old.facts |= Fact.O_NO_NULL_CHECK; // enum: no receiver prepend needed (tag is already in normalized args) normCall(i_old, funcNorm, newOp, ai_new); } @@ -291,6 +292,7 @@ class SsaRaNormalizer extends SsaRebuilder { // devirtualize methods that are not overridden var rc = norm.ra.getClass(op.typeArgs[0]); if (EnumType.?(rc.oldType)) { + i_old.facts |= Fact.O_NO_NULL_CHECK; return normEnumVirtualCall(i_old, orig, method); } var t = extractVirtualRef(orig, method), funcNorm = t.0, m = t.1; From 33a7f5d68c255480b9dcaea9b9eeb7c78110ed5b Mon Sep 17 00:00:00 2001 From: Eliot Moss Date: Tue, 7 Apr 2026 21:45:19 -0400 Subject: [PATCH 06/58] Seems some null checks on enums were still getting through --- aeneas/src/ir/SsaNormalizer.v3 | 1 + 1 file changed, 1 insertion(+) diff --git a/aeneas/src/ir/SsaNormalizer.v3 b/aeneas/src/ir/SsaNormalizer.v3 index a75cbdb76..a9fcaea5f 100644 --- a/aeneas/src/ir/SsaNormalizer.v3 +++ b/aeneas/src/ir/SsaNormalizer.v3 @@ -1791,6 +1791,7 @@ class SsaRaNormalizer extends SsaRebuilder { return vals; } def normNullCheck(oldApp: SsaApplyOp, op: Operator) { + if (EnumType.?(op.typeArgs[0])) return; // enum values are never null var newArgs = genRefs(oldApp.inputs); if (newArgs.length >= 1) addNullCheck(oldApp, newArgs[0]); } From aecbd77048781affa46c0ff0aadd02ec8d945caa Mon Sep 17 00:00:00 2001 From: Eliot Moss Date: Tue, 7 Apr 2026 22:15:42 -0400 Subject: [PATCH 07/58] Remove a dead field and code related to it. Add O3 tests to my CI Makefile. --- aeneas/src/ir/Ir.v3 | 4 +--- aeneas/src/ir/VstIr.v3 | 1 - ci/Makefile | 40 +++++++++++++++++++++++++++++++++++----- 3 files changed, 36 insertions(+), 9 deletions(-) diff --git a/aeneas/src/ir/Ir.v3 b/aeneas/src/ir/Ir.v3 index 9118d254e..196bc81af 100644 --- a/aeneas/src/ir/Ir.v3 +++ b/aeneas/src/ir/Ir.v3 @@ -415,9 +415,7 @@ class IrModule { return ic; } def newIrClass(ctype: Type, superClass: IrClass, decl: VstCompound) -> IrClass { - var builder = IrBuilder.new(ctype, superClass); - builder.irModule = this; - var ic = builder.buildClass(decl); + var ic = IrBuilder.new(ctype, superClass).buildClass(decl); classes.put(ic); return ic; } diff --git a/aeneas/src/ir/VstIr.v3 b/aeneas/src/ir/VstIr.v3 index 95a39a0fd..2cb77ff10 100644 --- a/aeneas/src/ir/VstIr.v3 +++ b/aeneas/src/ir/VstIr.v3 @@ -29,7 +29,6 @@ component VstIr { } } class IrBuilder(ctype: Type, parent: IrClass) { - var irModule: IrModule; def fields = Vector.new(); def methods = Vector.new(); diff --git a/ci/Makefile b/ci/Makefile index 8259bcb69..e9c987f35 100644 --- a/ci/Makefile +++ b/ci/Makefile @@ -44,8 +44,8 @@ x86-runs: $(X86_RUNS) X86_64_RUNS = x86-64 x86-64-wfts x86-64-O1 x86-64-O1-wfts x86-64-fp x86-64-fp-wfts \ - x86-64-O2 x86-64-O2-wfts x86-64-uv x86-64-uv-wfts \ - x86-64-uv-O2 x86-64-uv-O2-wfts \ + x86-64-O2 x86-64-O2-wfts x86-64-O3 x86-64-O3-wfts x86-64-uv x86-64-uv-wfts \ + x86-64-uv-O2 x86-64-uv-O2-wfts x86-64-uv-O3 x86-64-uv-O3-wfts \ x86-64-runs: $(X86_64_RUNS) @@ -55,17 +55,17 @@ X86_64_DARWIN_RUNS = x86-64-darwin-bootstrap x86-64-darwin-current x86-64-darwin-runs: $(X86_64_DARWIN_RUNS) -JVM_RUNS = jvm jvm-O2 jvm-uv +JVM_RUNS = jvm jvm-O2 jvm-O3 jvm-uv jvm-runs: $(JVM_RUNS) -WASM_RUNS = wasm wasm-wfts wasm-O2 wasm-O2-wfts +WASM_RUNS = wasm wasm-wfts wasm-O2 wasm-O2-wfts wasm-O3 wasm-O3-wfts wasm-runs: $(WASM_RUNS) -WASM_GC_RUNS = wasm-gc wasm-gc-O2 wasm-gc-og wasm-gc-O2-og wasm-gc-rt wasm-gc-O2-rt +WASM_GC_RUNS = wasm-gc wasm-gc-O2 wasm-gc-O3 wasm-gc-og wasm-gc-O2-og wasm-gc-O3-og wasm-gc-rt wasm-gc-O2-rt wasm-gc-O3-rt wasm-gc-runs: $(WASM_GC_RUNS) @@ -108,6 +108,12 @@ x86-64-O2: x86-64-O2-wfts: CI_DIR=ci/$@ bash ./linux/build-test-x86_64.sh -O2 -wfts > $@-out.txt 2>&1 +x86-64-O3: + CI_DIR=ci/$@ bash ./linux/build-test-x86_64.sh -O3 > $@-out.txt 2>&1 + +x86-64-O3-wfts: + CI_DIR=ci/$@ bash ./linux/build-test-x86_64.sh -O3 -wfts > $@-out.txt 2>&1 + x86-64-uv: CI_DIR=ci/$@ bash ./linux/build-test-x86_64.sh -unbox-variants > $@-out.txt 2>&1 @@ -120,6 +126,12 @@ x86-64-uv-O2: x86-64-uv-O2-wfts: CI_DIR=ci/$@ bash ./linux/build-test-x86_64.sh -unbox-variants -O2 -wfts > $@-out.txt 2>&1 +x86-64-uv-O3: + CI_DIR=ci/$@ bash ./linux/build-test-x86_64.sh -unbox-variants -O3 > $@-out.txt 2>&1 + +x86-64-uv-O3-wfts: + CI_DIR=ci/$@ bash ./linux/build-test-x86_64.sh -unbox-variants -O3 -wfts > $@-out.txt 2>&1 + # # x86-64-darwin # @@ -139,6 +151,9 @@ jvm: jvm-O2: CI_DIR=ci/$@ bash ./jvm/build-test-jvm.sh -O2 > $@-out.txt 2>&1 +jvm-O3: + CI_DIR=ci/$@ bash ./jvm/build-test-jvm.sh -O3 > $@-out.txt 2>&1 + jvm-uv: CI_DIR=ci/$@ bash ./jvm/build-test-jvm.sh -unbox-variants > $@-out.txt 2>&1 @@ -157,6 +172,12 @@ wasm-O2: wasm-O2-wfts: CI_DIR=ci/$@ bash ./wasm/build-test-wasm.sh -O2 -wfts > $@-out.txt 2>&1 +wasm-O3: + CI_DIR=ci/$@ bash ./wasm/build-test-wasm.sh -O3 > $@-out.txt 2>&1 + +wasm-O3-wfts: + CI_DIR=ci/$@ bash ./wasm/build-test-wasm.sh -O3 -wfts > $@-out.txt 2>&1 + # # WASM GC (wfts always enabled) # @@ -166,14 +187,23 @@ wasm-gc: wasm-gc-O2: CI_DIR=ci/$@ bash ./wasm-gc/build-test-wasm-gc.sh -O2 > $@-out.txt 2>&1 +wasm-gc-O3: + CI_DIR=ci/$@ bash ./wasm-gc/build-test-wasm-gc.sh -O3 > $@-out.txt 2>&1 + wasm-gc-og: CI_DIR=ci/$@ bash ./wasm-gc/build-test-wasm-gc.sh -wasm-gc-one-group > $@-out.txt 2>&1 wasm-gc-O2-og: CI_DIR=ci/$@ bash ./wasm-gc/build-test-wasm-gc.sh -O2 -wasm-gc-one-group > $@-out.txt 2>&1 +wasm-gc-O3-og: + CI_DIR=ci/$@ bash ./wasm-gc/build-test-wasm-gc.sh -O3 -wasm-gc-one-group > $@-out.txt 2>&1 + wasm-gc-rt: CI_DIR=ci/$@ bash ./wasm-gc/build-test-wasm-gc.sh -wasm-gc-use-ref-test > $@-out.txt 2>&1 wasm-gc-O2-rt: CI_DIR=ci/$@ bash ./wasm-gc/build-test-wasm-gc.sh -O2 -wasm-gc-use-ref-test > $@-out.txt 2>&1 + +wasm-gc-O3-rt: + CI_DIR=ci/$@ bash ./wasm-gc/build-test-wasm-gc.sh -O3 -wasm-gc-use-ref-test > $@-out.txt 2>&1 From 1404c4f17b11cb5e02dc3a9a8af6e3c18c60757c Mon Sep 17 00:00:00 2001 From: Eliot Moss Date: Fri, 10 Apr 2026 15:39:18 -0400 Subject: [PATCH 08/58] Strategy A Step 1: Remove per-case enum method overrides MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove the ability for individual enum cases to have method bodies (e.g., `A { def m() -> int { return 1; } }`). Enum types and subtypes retain methods; only per-case dispatch is removed. Removes: VstCaseMember.members, VstMethod.enumCaseIrs, parseEnumCaseMembers, addEnumCaseOverrides, layoutEnumMtable, normEnumVirtualCall, getEnumVirtual, checkEnumCaseMembers, findEnumCaseOverride, findEnumSubtypeOverride, and related code. Subtype-level method overrides (e.g., enum E.More overriding E.m) are parsed but dispatch is not yet implemented — that requires Step 2 (RaClass-based dispatch for enum types). Co-Authored-By: Claude Opus 4.6 (1M context) --- aeneas/src/core/Eval.v3 | 11 +-- aeneas/src/ir/Normalization.v3 | 35 -------- aeneas/src/ir/Reachability.v3 | 21 ----- aeneas/src/ir/SsaNormalizer.v3 | 29 +------ aeneas/src/ir/VstIr.v3 | 76 ---------------- aeneas/src/vst/MethodEnv.v3 | 42 --------- aeneas/src/vst/Parser.v3 | 21 ----- aeneas/src/vst/Verifier.v3 | 87 +------------------ aeneas/src/vst/Vst.v3 | 10 +-- test/enums/enum_closure01.v3 | 8 -- test/enums/enum_cmethod00.v3 | 11 --- test/enums/enum_cmethod01.v3 | 14 --- test/enums/enum_cmethod02.v3 | 18 ---- test/enums/enum_cmethod03.v3 | 14 --- test/enums/enum_cmethod04.v3 | 18 ---- test/enums/enum_cmethod05.v3 | 20 ----- test/enums/enum_cmethod07.v3 | 13 --- test/enums/enum_cmethod08.v3 | 13 --- test/enums/enum_cmethod09.v3 | 14 --- .../{enum_cmethod06.v3 => enum_method07.v3} | 0 test/enums/parser/enum_cmethod00.v3 | 4 +- test/enums/parser/enum_cmethod01.v3 | 9 -- test/enums/parser/enum_cmethod02.v3 | 3 - test/enums/parser/enum_cmethod03.v3 | 3 - test/enums/seman/enum_cmethod00.v3 | 7 -- test/enums/seman/enum_cmethod01.v3 | 3 - test/enums/seman/enum_cmethod02.v3 | 3 - test/enums/seman/enum_cmethod03.v3 | 7 -- 28 files changed, 9 insertions(+), 505 deletions(-) delete mode 100644 test/enums/enum_closure01.v3 delete mode 100644 test/enums/enum_cmethod00.v3 delete mode 100644 test/enums/enum_cmethod01.v3 delete mode 100644 test/enums/enum_cmethod02.v3 delete mode 100644 test/enums/enum_cmethod03.v3 delete mode 100644 test/enums/enum_cmethod04.v3 delete mode 100644 test/enums/enum_cmethod05.v3 delete mode 100644 test/enums/enum_cmethod07.v3 delete mode 100644 test/enums/enum_cmethod08.v3 delete mode 100644 test/enums/enum_cmethod09.v3 rename test/enums/{enum_cmethod06.v3 => enum_method07.v3} (100%) delete mode 100644 test/enums/parser/enum_cmethod01.v3 delete mode 100644 test/enums/parser/enum_cmethod02.v3 delete mode 100644 test/enums/parser/enum_cmethod03.v3 delete mode 100644 test/enums/seman/enum_cmethod00.v3 delete mode 100644 test/enums/seman/enum_cmethod01.v3 delete mode 100644 test/enums/seman/enum_cmethod02.v3 delete mode 100644 test/enums/seman/enum_cmethod03.v3 diff --git a/aeneas/src/core/Eval.v3 b/aeneas/src/core/Eval.v3 index 73bb70f43..7941d7b44 100644 --- a/aeneas/src/core/Eval.v3 +++ b/aeneas/src/core/Eval.v3 @@ -1181,16 +1181,7 @@ def lookupClassVirtual(args: Arguments, object: Record, spec: IrSpec) -> IrSpec } def lookupEnumVirtual(args: Arguments, member: IrMember) -> IrSpec { var ta = args.getTypeArgs(); - var baseSpec = IrSpec.new(ta[0], ta, member); - var tag = Int.unbox(args.vals[0]); - if (IrMethod.?(member)) { - var rootVst = IrMethod.!(member).source; - if (rootVst != null && rootVst.enumCaseIrs != null && tag < rootVst.enumCaseIrs.length) { - var overrideIr = rootVst.enumCaseIrs[tag]; - if (overrideIr != null) return IrSpec.new(ta[0], ta, overrideIr); - } - } - return baseSpec; + return IrSpec.new(ta[0], ta, member); } def lookupVariantVirtual(args: Arguments, object: Record, spec: IrSpec) -> IrSpec { if (object != null) return args.getProgram().ir.resolveMethodImpl(object.rtype, spec); diff --git a/aeneas/src/ir/Normalization.v3 b/aeneas/src/ir/Normalization.v3 index 9fb97917b..a49f72ef6 100644 --- a/aeneas/src/ir/Normalization.v3 +++ b/aeneas/src/ir/Normalization.v3 @@ -592,15 +592,11 @@ class ReachabilityNormalizer(config: NormalizerConfig, ra: ReachabilityAnalyzer) rm.norm.flags |= IrFlag.M_OVERRIDE; sm.norm.flags |= IrFlag.M_OVERRIDDEN; } - // For enum methods, M_OVERRIDDEN must be transferred from original - // because enum case overrides don't share vtable slots. - if (m.flags.M_OVERRIDDEN) rm.norm.flags |= IrFlag.M_OVERRIDDEN; if (rm.virtual != null) virtuals = List.new(rm.virtual, virtuals); } def layoutMtable(rv: RaVirtual) { if (rv.mtable != null) return; var rm = rv.raMethod, rc = ra.getClass(rm.receiver); - if (EnumType.?(rc.oldType)) return layoutEnumMtable(rv, rm, rc); var size = rc.maxClassId - rc.minClassId; if (ra.compiler.RaDevirtualize && size == 1) return; // no need for an mtable var table = Array.new(size), mtable = IrMtable.new(rm.norm, rc.minClassId, table); @@ -638,37 +634,6 @@ class ReachabilityNormalizer(config: NormalizerConfig, ra: ReachabilityAnalyzer) setMtable(l.head, rv); } } - def layoutEnumMtable(rv: RaVirtual, rm: RaMethod, rc: RaClass) { - var enumDecl = EnumType.!(rc.oldType).enumDecl; - var numCases = enumDecl.tagHi + 1; - var table = Array.new(numCases); - var mtable = IrMtable.new(rm.norm, 0, table); - rv.mtable = mtable; - - var ft = Function.funcRefType(rm.norm.getMethodType()); - mtable.record = ra.prog.newRecord(V3Array.newType(ft), numCases); - - // Fill all slots with the default implementation. - var defaultSpec = IrSpec.new(rm.norm.receiver, [rm.norm.receiver], rm.norm); - for (i < numCases) { - table[i] = rm.norm; - mtable.record.values[i] = FuncVal.new(defaultSpec); - } - - // Fill override slots. - var rootVst = rm.orig.source; - if (rootVst != null && rootVst.enumCaseIrs != null) { - for (i < rootVst.enumCaseIrs.length) { - var overrideIr = rootVst.enumCaseIrs[i]; - if (overrideIr == null) continue; - var overrideRm = overrideIr.raMethod; - if (overrideRm == null || !overrideRm.raFacts.RM_LIVE) continue; - table[i] = overrideRm.norm; - var ta = Arrays.replace(overrideRm.getSpec().typeArgs, 0, overrideRm.norm.receiver); - mtable.record.values[i] = FuncVal.new(IrSpec.new(ta[0], ta, overrideRm.norm)); - } - } - } def resolveMethodImpl(rc: RaClass, rm: RaMethod) -> RaMethod { var sm: RaMethod; for (sc = rc; sc != null; sc = sc.parent) { // find super method, if any diff --git a/aeneas/src/ir/Reachability.v3 b/aeneas/src/ir/Reachability.v3 index dc1720606..b6425465c 100644 --- a/aeneas/src/ir/Reachability.v3 +++ b/aeneas/src/ir/Reachability.v3 @@ -527,31 +527,10 @@ class ReachabilityAnalyzer(compilation: Compilation) { if (rm.isVirtual()) return; rm.virtual = RaVirtual.new(rm); var rc = makeClass(rm.receiver); - if (EnumType.?(rc.oldType)) { - getEnumVirtual(rm, rc); - return; - } for (l = rc.subtypes; l != null; l = l.tail) { analyzeVirtual(l.head, rm); } } - def getEnumVirtual(rm: RaMethod, rc: RaClass) { - // Add root implementation and mark it live so its SSA is generated. - var rv = rm.virtual; - rv.addImpl(rm); - getMethod(null, rm); - // Add per-case override implementations. - var rootVst = rm.orig.source; - if (rootVst != null && rootVst.enumCaseIrs != null) { - for (ir in rootVst.enumCaseIrs) { - if (ir == null) continue; - var overrideRm = makeMethod([rm.receiver], ir, null); - ir.raMethod = overrideRm; - rv.addImpl(overrideRm); - getMethod(null, overrideRm); - } - } - } def getMethod(op: SsaApplyOp, rm: RaMethod) { if (rm.setFact(RaFact.RM_LIVE)) return; liveMethods.put(rm); diff --git a/aeneas/src/ir/SsaNormalizer.v3 b/aeneas/src/ir/SsaNormalizer.v3 index a9fcaea5f..c14ee3725 100644 --- a/aeneas/src/ir/SsaNormalizer.v3 +++ b/aeneas/src/ir/SsaNormalizer.v3 @@ -291,10 +291,7 @@ class SsaRaNormalizer extends SsaRebuilder { CallVariantVirtual(method) => { // devirtualize methods that are not overridden var rc = norm.ra.getClass(op.typeArgs[0]); - if (EnumType.?(rc.oldType)) { - i_old.facts |= Fact.O_NO_NULL_CHECK; - return normEnumVirtualCall(i_old, orig, method); - } + if (EnumType.?(rc.oldType)) i_old.facts |= Fact.O_NO_NULL_CHECK; var t = extractVirtualRef(orig, method), funcNorm = t.0, m = t.1; var ai_new = normArgs(funcNorm, genRefs(i_old.inputs)); if (t.2) { // still a virtual dispatch @@ -862,30 +859,6 @@ class SsaRaNormalizer extends SsaRebuilder { ifc.endCase(newGraph.falseConst()); return ifc.finish(); } - def normEnumVirtualCall(i_old: SsaApplyOp, orig: Operator, method: IrMethod) { - var t = extractVirtualRef(orig, method), funcNorm = t.0, m = t.1; - var ai_new = normArgs(funcNorm, genRefs(i_old.inputs)); - var tagType = V3.getVariantTagType(EnumType.!(norm.ra.getClass(orig.typeArgs[0]).oldType)); - if (t.2) { // still a virtual dispatch - // enum value IS the tag; use it to index into the dispatch table - var tag = ai_new[0]; - var record = IrSelector.!(m.member).mtable.record; - var table = newGraph.valConst(record.rtype, record); - var func = curBlock.opArrayGetElem(record.rtype, tagType, Facts.O_SAFE_BOUNDS, table, tag); - if (norm.config.NonRefClosureReceiver) { - // Native/wasm: no Oop prepend - ai_new = Arrays.prepend(func, ai_new); - normCall(i_old, funcNorm, V3Op.newCallFunctionDirect(funcNorm.sub[0]), ai_new); - } else { - // JVM: prepend null Oop for closure compatibility - ai_new = Arrays.concat([func, newGraph.nullReceiver()], ai_new); - normCall(i_old, funcNorm, V3Op.newCallFunction(funcNorm.sub[0]), ai_new); - } - } else { - // devirtualized: no receiver prepend (tag is already in normalized args) - normCall(i_old, funcNorm, V3Op.newCallMethod(m), ai_new); - } - } def normVariantGetTag(vn: VariantNorm, args: Range) -> SsaInstr { if (vn == null) return null; if (vn.hasNoTag()) { diff --git a/aeneas/src/ir/VstIr.v3 b/aeneas/src/ir/VstIr.v3 index 2cb77ff10..e82f43071 100644 --- a/aeneas/src/ir/VstIr.v3 +++ b/aeneas/src/ir/VstIr.v3 @@ -70,87 +70,11 @@ class IrBuilder(ctype: Type, parent: IrClass) { x: VstMethod => addVstMethod(x); } } - // Build IR methods for per-case enum method overrides. - if (decl.isEnum()) addEnumCaseOverrides(VstClass.!(decl)); var ic = build(); ic.boxing = boxing; ic.packed = packed; return ic; } - def addEnumCaseOverrides(decl: VstClass) { - // For subtype enums, all overrides are already processed by the - // root enum's buildClass (see addSubtypeOverridesRecursive below). - if (decl.parentEnum != null) return; - // Process root's own per-case overrides. - addEnumCaseOverridesForDecl(decl); - // Recursively process all subtypes so that M_OVERRIDDEN is set - // on root methods and enumCaseIrs is populated before any SSA - // generation (critical for v3i lazy evaluation). - addSubtypeOverridesRecursive(decl); - } - def addSubtypeOverridesRecursive(decl: VstClass) { - for (l = decl.subtypeOrder; l != null; l = l.tail) { - addEnumCaseOverridesForDecl(l.head); - addSubtypeOverridesRecursive(l.head); - } - } - def addEnumCaseOverridesForDecl(decl: VstClass) { - var rootEnum = findRootEnum(decl); - var tableSize = rootEnum.tagHi + 1; - // 1. Subtype-level method overrides: fill range in root's enumCaseIrs. - for (l = decl.members; l != null; l = l.tail) { - if (!VstMethod.?(l.head)) continue; - var vm = VstMethod.!(l.head); - if (vm.root == null || vm.root == vm) continue; // not an override - var ir = newIrMethod(vm); - addIrMethod(ir); - vm.index = ir.index; - ir.flags |= IrFlag.M_OVERRIDE; - var rootVst = vm.root; - markEnumRootMethodOverridden(rootVst); - if (rootVst.enumCaseIrs == null) { - rootVst.enumCaseIrs = Array.new(tableSize); - } - for (i = decl.tagLo; i <= decl.tagHi; i++) { - rootVst.enumCaseIrs[i] = ir; - } - } - // 2. Per-case method overrides. - for (c in decl.cases) { - if (c.members == null) continue; - for (l = c.members; l != null; l = l.tail) { - if (!VstMethod.?(l.head)) continue; - var vm = VstMethod.!(l.head); - var ir = newIrMethod(vm); - addIrMethod(ir); - vm.index = ir.index; - ir.flags |= IrFlag.M_OVERRIDE; - var rootVst = vm.root; - markEnumRootMethodOverridden(rootVst); - if (rootVst.enumCaseIrs == null) { - rootVst.enumCaseIrs = Array.new(tableSize); - } - var tag = c.enumTag; - rootVst.enumCaseIrs[tag] = ir; - } - } - } - def markEnumRootMethodOverridden(rootVst: VstMethod) { - // Find the root method's IrMethod in the current builder's methods vector. - // We search by source reference since the VstMethod.index may not yet - // reflect the IrMethod index (it's updated by addVstMethod). - for (i = 2; i < methods.length; i++) { - var m = methods[i]; - if (m != null && m.source == rootVst) { - m.flags |= IrFlag.M_OVERRIDDEN; - return; - } - } - } - def findRootEnum(decl: VstClass) -> VstClass { - while (decl.parentEnum != null) decl = decl.parentEnum; - return decl; - } def addVstField(f: VstField, isVariant: bool, unboxed: bool) { var ir = IrField.new(ctype, f.getType()); addRepHintFacts(f, ir); diff --git a/aeneas/src/vst/MethodEnv.v3 b/aeneas/src/vst/MethodEnv.v3 index f37d4cb17..0fdf208bf 100644 --- a/aeneas/src/vst/MethodEnv.v3 +++ b/aeneas/src/vst/MethodEnv.v3 @@ -468,53 +468,11 @@ class MethodEnv { return expr.bind(null, VarBinding.Const(n, V3.stringType), V3.stringType); } x: VstMethod => { - // Check for per-case override, then subtype-level override. - var overrideMethod = findEnumCaseOverride(vc, name.image); - if (overrideMethod == null) overrideMethod = findEnumSubtypeOverride(enumType.enumDecl, vc, name.image); - if (overrideMethod != null) { - // Use root enum type as receiver: override IrMethods are - // stored in the root IrClass (not the subtype's IrClass). - var rootDecl = enumType.enumDecl; - while (rootDecl.parentEnum != null) rootDecl = rootDecl.parentEnum; - var methodReceiver = rootDecl.getDeclaredType(); - var receiver = objExpr(false, expr.ident.name, expr); - return expr.bind(receiver, VarBinding.ObjectMethod(methodReceiver, overrideMethod, member.typeArgs), member.memberType); - } return lookupEnumExprMember(expr, enumType, false); } } return lookupEnumExprMember(expr, enumType, false); } - def findEnumCaseOverride(vc: VstCaseMember, name: string) -> VstMethod { - if (vc.members == null) return null; - for (l = vc.members; l != null; l = l.tail) { - if (VstMethod.?(l.head) && Strings.equal(l.head.name(), name)) return VstMethod.!(l.head); - } - return null; - } - def findEnumSubtypeOverride(rootDecl: VstClass, vc: VstCaseMember, name: string) -> VstMethod { - // Walk from the case's declaring enum up to the root, looking for overrides. - var decl = findDeclaringEnum(rootDecl, vc); - while (decl != null) { - var member = decl.memberMap[name]; - if (member != null && VstMethod.?(member) && VstMethod.!(member).root != VstMethod.!(member)) { - return VstMethod.!(member); - } - decl = decl.parentEnum; - } - return null; - } - def findDeclaringEnum(rootDecl: VstClass, vc: VstCaseMember) -> VstClass { - // Find the enum that declares this case by searching the hierarchy. - for (c in rootDecl.cases) { - if (c == vc) return rootDecl; - } - for (l = rootDecl.subtypeOrder; l != null; l = l.tail) { - var result = findDeclaringEnum(l.head, vc); - if (result != null) return result; - } - return null; - } def lookupIntTypeMember(expr: VarExpr, etype: IntType) -> VarBinding { var name = expr.ident.name.image; var op: Operator, typeParams: List; diff --git a/aeneas/src/vst/Parser.v3 b/aeneas/src/vst/Parser.v3 index abf9fb7c4..52110112a 100644 --- a/aeneas/src/vst/Parser.v3 +++ b/aeneas/src/vst/Parser.v3 @@ -552,7 +552,6 @@ component Parser { if (p.curByte == '_' && !Char.isIdentMiddle(p.peek(1))) { var ut = p.token(1); var c = VstCaseMember.new(tag, ut, null, null); - if (p.curByte == '{') c.members = parseEnumCaseMembers(p); p.opt1(','); return c; } @@ -560,32 +559,12 @@ component Parser { var args = if(p.curByte == '(', parseList(0, p, '(', COMMA, ')', parseExpr)); if (id.name.image.length > 0) { var c = VstCaseMember.new(tag, id.name, null, args); - if (p.curByte == '{') c.members = parseEnumCaseMembers(p); p.opt1(','); return c; } p.opt1(','); return null; } - def parseEnumCaseMembers(p: ParserState) -> List { - p.req1('{'); - var list: List; - while (true) { - if (p.curByte == '}') { p.advance1(); break; } - var isPrivate = optKeyword(p, "private") != null; - if (optKeyword(p, "def") != null) { - var parser = if(isPrivate, PRIVATE_DEF, PUBLIC_DEF); - var nlist = parser.parseDef(p, list, null); - if (nlist == list) { p.opt1('}'); break; } - list = nlist; - } else { - p.error("only 'def' allowed in enum case body"); - p.opt1('}'); - break; - } - } - return Lists.reverse(list); - } def parseEnumMethod(p: ParserState, prev: List) -> List { var isPrivate = optKeyword(p, "private") != null; if (optKeyword(p, "def") != null) { diff --git a/aeneas/src/vst/Verifier.v3 b/aeneas/src/vst/Verifier.v3 index 0cf5ac0db..1a6a9e139 100644 --- a/aeneas/src/vst/Verifier.v3 +++ b/aeneas/src/vst/Verifier.v3 @@ -548,18 +548,6 @@ class Verifier(compiler: Compiler, prog: Program) { m.index = parentMethod.index; } } - // Also fix per-case method overrides. - for (c in decl.cases) { - if (c.members == null) continue; - for (l = c.members; l != null; l = l.tail) { - if (!VstMethod.?(l.head)) continue; - var m = VstMethod.!(l.head); - // Case overrides should point to the ultimate root. - if (m.root != null && m.root.root != null && m.root != m.root.root) { - m.root = m.root.root; - } - } - } } def verifyEnumQualifiers(decl: VstClass) { var q = decl.qualifiers; @@ -900,20 +888,6 @@ class Verifier(compiler: Compiler, prog: Program) { } } } - // Type-check per-case method override bodies. - if (VstClass.?(decl) && VstClass.!(decl).isEnum()) { - for (c in VstClass.!(decl).cases) { - if (c.members == null) continue; - for (l = c.members; l != null; l = l.tail) { - if (VstMethod.?(l.head)) { - var m = VstMethod.!(l.head); - if (m.root == m) continue; // error in verification; skip body - tc.enterMethod(m, env); - if (!EmptyStmt.?(m.func.body)) tc.typeCheckBody(m.func.body); - } - } - } - } } def typeCheckNew(decl: VstCompound, cdecl: VstNew, tc: TypeChecker) { var cv = decl.verifier, thisParam = cv.newThisParam(), pl = cdecl.func.params; @@ -1203,12 +1177,6 @@ class VstCompoundVerifier { x: VstLayoutField => checkLayoutField(x); } } - // Verify per-case method overrides for enums. - if (classDecl != null && classDecl.isEnum()) { - for (c in classDecl.cases) { - if (c.members != null) checkEnumCaseMembers(c); - } - } if (compound.constructor != null) { compound.constructor.memberinits = Lists.reverse(memberinits); } @@ -1410,13 +1378,7 @@ class VstCompoundVerifier { } def checkMethodOverride(decl: VstMethod) { var binding = lookupSuperMember(decl.name()); // re-resolve member after return type inference - if (binding == null) { - // Enum case override: compare directly against root method. - if (decl.root != null && decl.root != decl) { - checkEnumCaseMethodOverride(decl); - } - return; - } + if (binding == null) return; var superDecl = VstMethod.!(binding.member); var superMethodType = binding.memberType; var methodType = decl.getType(); @@ -1445,53 +1407,6 @@ class VstCompoundVerifier { errAtDecl(decl).InheritanceError2("method signature %s cannot override %s", methodType, superMethodType); } } - def checkEnumCaseMembers(c: VstCaseMember) { - for (l = c.members; l != null; l = l.tail) { - match (l.head) { - x: VstMethod => checkEnumCaseMethod(x, c); - _ => errAtDecl(l.head).set("EnumDeclError", "only 'def' allowed in enum case body"); - } - } - } - def checkEnumCaseMethod(decl: VstMethod, ecase: VstCaseMember) { - decl.receiver = compound; - // Look up the root method in the enum's member map or parent chain. - var rootMember = compound.memberMap[decl.name()]; - if ((rootMember == null || !VstMethod.?(rootMember)) && compound.isEnum()) { - var pm = findEnumMethodInParents(VstClass.!(compound), decl.name()); - if (pm != null) rootMember = pm; - } - if (rootMember == null || !VstMethod.?(rootMember)) { - errAtDecl(decl).set("EnumDeclError", - Strings.format1("case method \"%s\" does not override an enum-level method", decl.name())); - decl.root = decl; - return; - } - var rootMethod = VstMethod.!(rootMember); - decl.root = rootMethod.root; - decl.index = rootMethod.index; - // Check parameters. - typeEnv = compound.typeEnv; - if (decl.func.params.list != null) { - var map = Strings.newMap(); - Lists.apply(decl.func.params.list, checkMethodParam(_, map)); - } - match (decl.func.rettype) { - Explicit(tref) => resolveType(tref, typeEnv); - Implicit(expr) => ; - _ => ; - } - // Defer override signature check until after body inference. - verifier.methodOverrideChecks = List.new((this, decl), verifier.methodOverrideChecks); - } - def checkEnumCaseMethodOverride(decl: VstMethod) { - var rootMethod = decl.root; - var superMethodType = rootMethod.getType(); - var methodType = decl.getType(); - if (superMethodType != null && methodType != null && !TypeSystem.isSubtype(methodType, superMethodType)) { - errAtDecl(decl).InheritanceError2("method signature %s cannot override %s", methodType, superMethodType); - } - } def findEnumMethodInParents(decl: VstClass, name: string) -> VstMethod { var cur = decl.parentEnum; while (cur != null) { diff --git a/aeneas/src/vst/Vst.v3 b/aeneas/src/vst/Vst.v3 index a5f38e92d..523140c90 100644 --- a/aeneas/src/vst/Vst.v3 +++ b/aeneas/src/vst/Vst.v3 @@ -137,8 +137,8 @@ class VstCompound extends Decl { def getParams() -> List { return if(params != null, params.list); } - def isEnum() => false; - def isVariant() => false; + def isEnum() -> bool { return false; } + def isVariant() -> bool { return false; } def isVariantCase() -> bool { return isVariant() && variantTag >= 0; } def isUnboxed() -> bool { @@ -190,8 +190,8 @@ class VstClass extends VstCompound { def hasDescriptor() -> bool { return descriptorInfo != null && descriptorInfo.descriptorTypeRef != null; } - def isEnum() => kind == Kind.ENUM; - def isVariant() => kind == Kind.VARIANT; + def isEnum() -> bool { return kind == Kind.ENUM; } + def isVariant() -> bool { return kind == Kind.VARIANT; } def findDefaultCase() -> VstClass { for (c in cases) { if (Strings.equal(c.name(), "_")) return c.decl; @@ -344,7 +344,6 @@ class VstCaseMember extends VstMember { def decl: VstClass; def args: VstList; // for enums var enumTag: int; // runtime tag; initialized to localTag, may be reassigned by DFS - var members: List; // per-case method overrides new(localTag, name: Token, decl, args) super(false, name) { enumTag = localTag; @@ -426,7 +425,6 @@ class VstMethod extends VstMember { var typeEnv: TypeEnv; var root: VstMethod; var importName: Token; - var enumCaseIrs: Array; // per-case override IrMethods, indexed by enumTag new(isPrivate: bool, name: Token, typeParams, func) super(isPrivate, name) { } diff --git a/test/enums/enum_closure01.v3 b/test/enums/enum_closure01.v3 deleted file mode 100644 index be2a301c1..000000000 --- a/test/enums/enum_closure01.v3 +++ /dev/null @@ -1,8 +0,0 @@ -//@execute 0=1; 1=0; 2=2 -// Overridden method closure with virtual dispatch -enum E { A { def m() -> int { return 1; } }, B, C { def m() -> int { return 2; } }; def m() -> int { return 0; } } -def main(i: int) -> int { - var e: E = if(i == 0, E.A, if(i == 1, E.B, E.C)); - var f = e.m; - return f(); -} diff --git a/test/enums/enum_cmethod00.v3 b/test/enums/enum_cmethod00.v3 deleted file mode 100644 index b3f2459b4..000000000 --- a/test/enums/enum_cmethod00.v3 +++ /dev/null @@ -1,11 +0,0 @@ -//@execute 0=1; 1=0; 2=0 -// Per-case override returns different value -enum E { A { def m() -> int { return 1; } }, B, C; def m() -> int { return 0; } } -def main(i: int) -> int { - match (i) { - 0 => return E.A.m(); - 1 => return E.B.m(); - 2 => return E.C.m(); - } - return -1; -} diff --git a/test/enums/enum_cmethod01.v3 b/test/enums/enum_cmethod01.v3 deleted file mode 100644 index e12df79bc..000000000 --- a/test/enums/enum_cmethod01.v3 +++ /dev/null @@ -1,14 +0,0 @@ -//@execute 0=1; 1=0; 2=2 -// Virtual dispatch through variable -enum E { A { def m() -> int { return 1; } }, B, C { def m() -> int { return 2; } }; def m() -> int { return 0; } } -def run(e: E) -> int { - return e.m(); -} -def main(i: int) -> int { - match (i) { - 0 => return run(E.A); - 1 => return run(E.B); - 2 => return run(E.C); - } - return -1; -} diff --git a/test/enums/enum_cmethod02.v3 b/test/enums/enum_cmethod02.v3 deleted file mode 100644 index 801a124c6..000000000 --- a/test/enums/enum_cmethod02.v3 +++ /dev/null @@ -1,18 +0,0 @@ -//@execute 0=10; 1=20; 2=42; 3=7 -// Multiple methods, some overridden -enum E { - A { def m() -> int { return 10; } }, - B { def n() -> int { return 20; } }, - C; - def m() -> int { return 42; } - def n() -> int { return 7; } -} -def main(i: int) -> int { - match (i) { - 0 => return E.A.m(); - 1 => return E.B.n(); - 2 => return E.C.m(); - 3 => return E.C.n(); - } - return -1; -} diff --git a/test/enums/enum_cmethod03.v3 b/test/enums/enum_cmethod03.v3 deleted file mode 100644 index 301d659f8..000000000 --- a/test/enums/enum_cmethod03.v3 +++ /dev/null @@ -1,14 +0,0 @@ -//@execute 0=11; 1=20 -// Override accesses enum fields -enum E(x: int) { - A(10) { def m() -> int { return x + 1; } }, - B(20); - def m() -> int { return x; } -} -def main(i: int) -> int { - match (i) { - 0 => return E.A.m(); - 1 => return E.B.m(); - } - return -1; -} diff --git a/test/enums/enum_cmethod04.v3 b/test/enums/enum_cmethod04.v3 deleted file mode 100644 index faeea502b..000000000 --- a/test/enums/enum_cmethod04.v3 +++ /dev/null @@ -1,18 +0,0 @@ -//@execute 0=10; 1=0; 2=20; 3=0 -// Virtual dispatch with multiple methods -enum E { - A { def m() -> int { return 10; } }, - B, - C { def m() -> int { return 20; } }; - def m() -> int { return 0; } -} -def callM(e: E) -> int { return e.m(); } -def main(i: int) -> int { - match (i) { - 0 => return callM(E.A); - 1 => return callM(E.B); - 2 => return callM(E.C); - 3 => { var e: E = E.B; return e.m(); } - } - return -1; -} diff --git a/test/enums/enum_cmethod05.v3 b/test/enums/enum_cmethod05.v3 deleted file mode 100644 index b11283ad6..000000000 --- a/test/enums/enum_cmethod05.v3 +++ /dev/null @@ -1,20 +0,0 @@ -//@execute 0=1; 1=0; 2=2; 3=0; 4=1; 5=0; 6=2 -// Static dispatch: E.A.m() directly calls override (no dispatch table) -enum E { - A { def m() -> int { return 1; } }, - B, - C { def m() -> int { return 2; } }; - def m() -> int { return 0; } -} -def main(i: int) -> int { - match (i) { - 0 => return E.A.m(); - 1 => return E.B.m(); - 2 => return E.C.m(); - 3 => { var e: E = E.B; return e.m(); } - 4 => { var e: E = E.A; return e.m(); } - 5 => { var b = E.B; return b.m(); } - 6 => { var c = E.C; return c.m(); } - } - return -1; -} diff --git a/test/enums/enum_cmethod07.v3 b/test/enums/enum_cmethod07.v3 deleted file mode 100644 index 82b4003e0..000000000 --- a/test/enums/enum_cmethod07.v3 +++ /dev/null @@ -1,13 +0,0 @@ -//@execute 0=0; 1=0; 2=1; 3=1 -// Subtype enum overrides parent method for all its cases -enum E { A, B, _; def m() -> int { return 0; } } -enum E.More { C, D; def m() -> int { return 1; } } -def main(i: int) -> int { - match (i) { - 0 => return E.A.m(); - 1 => return E.B.m(); - 2 => return E.More.C.m(); - 3 => return E.More.D.m(); - } - return -1; -} diff --git a/test/enums/enum_cmethod08.v3 b/test/enums/enum_cmethod08.v3 deleted file mode 100644 index 409071444..000000000 --- a/test/enums/enum_cmethod08.v3 +++ /dev/null @@ -1,13 +0,0 @@ -//@execute 0=0; 1=0; 2=2; 3=1 -// Case override in subtype + subtype-level override -enum E { A, B, _; def m() -> int { return 0; } } -enum E.More { C { def m() -> int { return 2; } }, D; def m() -> int { return 1; } } -def main(i: int) -> int { - match (i) { - 0 => return E.A.m(); - 1 => return E.B.m(); - 2 => return E.More.C.m(); - 3 => return E.More.D.m(); - } - return -1; -} diff --git a/test/enums/enum_cmethod09.v3 b/test/enums/enum_cmethod09.v3 deleted file mode 100644 index 0e883ea7d..000000000 --- a/test/enums/enum_cmethod09.v3 +++ /dev/null @@ -1,14 +0,0 @@ -//@execute 0=0; 1=0; 2=2; 3=1 -// Virtual dispatch through parent type -enum E { A, B, _; def m() -> int { return 0; } } -enum E.More { C { def m() -> int { return 2; } }, D; def m() -> int { return 1; } } -def call(e: E) -> int { return e.m(); } -def main(i: int) -> int { - match (i) { - 0 => return call(E.A); - 1 => return call(E.B); - 2 => return call(E.More.C); - 3 => return call(E.More.D); - } - return -1; -} diff --git a/test/enums/enum_cmethod06.v3 b/test/enums/enum_method07.v3 similarity index 100% rename from test/enums/enum_cmethod06.v3 rename to test/enums/enum_method07.v3 diff --git a/test/enums/parser/enum_cmethod00.v3 b/test/enums/parser/enum_cmethod00.v3 index 812a44384..2559a4f8c 100644 --- a/test/enums/parser/enum_cmethod00.v3 +++ b/test/enums/parser/enum_cmethod00.v3 @@ -1,3 +1,3 @@ -//@parse -// Enum with per-case method override +//@parse = ParseError +// Per-case method bodies are no longer allowed enum E { A { def m() -> int { return 1; } }, B; def m() -> int { return 0; } } diff --git a/test/enums/parser/enum_cmethod01.v3 b/test/enums/parser/enum_cmethod01.v3 deleted file mode 100644 index 7fdd52ec3..000000000 --- a/test/enums/parser/enum_cmethod01.v3 +++ /dev/null @@ -1,9 +0,0 @@ -//@parse -// Multiple cases with overrides -enum E { - A { def m() -> int { return 1; } def n() -> int { return 10; } }, - B { def m() -> int { return 2; } }, - C; - def m() -> int { return 0; } - def n() -> int { return 0; } -} diff --git a/test/enums/parser/enum_cmethod02.v3 b/test/enums/parser/enum_cmethod02.v3 deleted file mode 100644 index 399fcf76f..000000000 --- a/test/enums/parser/enum_cmethod02.v3 +++ /dev/null @@ -1,3 +0,0 @@ -//@parse -// Default case with override -enum E { A, B, _ { def m() => 99; }; def m() => 0; } diff --git a/test/enums/parser/enum_cmethod03.v3 b/test/enums/parser/enum_cmethod03.v3 deleted file mode 100644 index 3fcb5136e..000000000 --- a/test/enums/parser/enum_cmethod03.v3 +++ /dev/null @@ -1,3 +0,0 @@ -//@parse = ParseError -// var not allowed in enum case body -enum E { A { var x: int; }, B; } diff --git a/test/enums/seman/enum_cmethod00.v3 b/test/enums/seman/enum_cmethod00.v3 deleted file mode 100644 index 8d29c966b..000000000 --- a/test/enums/seman/enum_cmethod00.v3 +++ /dev/null @@ -1,7 +0,0 @@ -//@seman -// Per-case override type-checks correctly -enum E { A { def m() -> int { return 1; } }, B; def m() -> int { return 0; } } -def main() { - var x: int = E.A.m(); - var y: int = E.B.m(); -} diff --git a/test/enums/seman/enum_cmethod01.v3 b/test/enums/seman/enum_cmethod01.v3 deleted file mode 100644 index 55ea405c1..000000000 --- a/test/enums/seman/enum_cmethod01.v3 +++ /dev/null @@ -1,3 +0,0 @@ -//@seman = InheritanceError -// Override with mismatched return type -enum E { A { def m() -> byte { return 1; } }, B; def m() -> int { return 0; } } diff --git a/test/enums/seman/enum_cmethod02.v3 b/test/enums/seman/enum_cmethod02.v3 deleted file mode 100644 index c0795aee5..000000000 --- a/test/enums/seman/enum_cmethod02.v3 +++ /dev/null @@ -1,3 +0,0 @@ -//@seman = EnumDeclError -// Case method that doesn't override any enum-level method -enum E { A { def m() -> int { return 1; } }, B; } diff --git a/test/enums/seman/enum_cmethod03.v3 b/test/enums/seman/enum_cmethod03.v3 deleted file mode 100644 index 4ae2958cb..000000000 --- a/test/enums/seman/enum_cmethod03.v3 +++ /dev/null @@ -1,7 +0,0 @@ -//@seman -// Override accesses enum fields -enum E(x: int) { A(10) { def m() -> int { return x + 1; } }, B(20); def m() -> int { return x; } } -def main() { - var a: int = E.A.m(); - var b: int = E.B.m(); -} From c17bb9da08614e7a130067855508fa1ac2a38228 Mon Sep 17 00:00:00 2001 From: Eliot Moss Date: Wed, 15 Apr 2026 23:38:07 -0400 Subject: [PATCH 09/58] Strategy A Step 2: Enum subtype method virtual dispatch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implement virtual dispatch for enum subtype method overrides. When an enum subtype like `enum E.More` overrides a method declared on root `enum E`, calls through an E-typed variable now dispatch to the correct override based on the enum value's tag. Key changes: - Ir.v3: Post-pass (markEnumSubtypeOverrides) sets M_OVERRIDDEN on root enum methods when subtypes declare overrides. Runs after verification so IrClasses exist and VstMethod indices are stable. - Reachability.v3: Enum RaClasses eagerly populate rc.subtypes via addEnumSubtypesRecursive so getVirtual() can analyze all overrides. - Normalization.v3: Enum hierarchies route to numberVariant for DFS tag-range numbering. New fillEnumMtable fills the dispatch table by walking the hierarchy (root fills all slots, subtypes override their range). Custom resolveEnumMethodImpl matches by VstMethod.root identity since enum subtype IrClasses lack parent linkage. - SsaNormalizer.v3: CallVariantVirtual for enums uses CallFunctionDirect (no Oop receiver prepend) with the enum value as the tag index. - Eval.v3: lookupEnumVirtual resolves overrides by walking parentEnum chain to find the deepest subtype containing the tag. Known limitation: selective override (subtype overrides one of multiple methods) does not dispatch correctly yet — enum_submethod03.v3 fails. The issue is related to method index mapping between root and subtype IrClasses when only a subset of methods is overridden. Co-Authored-By: Claude Opus 4.6 (1M context) --- aeneas/src/core/Eval.v3 | 25 +++++++- aeneas/src/ir/Ir.v3 | 36 +++++++++++- aeneas/src/ir/Normalization.v3 | 104 +++++++++++++++++++++++++++------ aeneas/src/ir/Reachability.v3 | 20 ++++++- aeneas/src/ir/SsaNormalizer.v3 | 15 ++++- aeneas/src/ir/VstIr.v3 | 2 +- aeneas/src/vst/Verifier.v3 | 1 + test/enums/enum_method08.v3 | 18 ++++++ test/enums/enum_submethod00.v3 | 9 +++ test/enums/enum_submethod01.v3 | 10 ++++ test/enums/enum_submethod02.v3 | 16 +++++ test/enums/enum_submethod03.v3 | 15 +++++ test/enums/enum_submethod04.v3 | 13 +++++ test/enums/enum_submethod05.v3 | 13 +++++ 14 files changed, 271 insertions(+), 26 deletions(-) create mode 100644 test/enums/enum_method08.v3 create mode 100644 test/enums/enum_submethod00.v3 create mode 100644 test/enums/enum_submethod01.v3 create mode 100644 test/enums/enum_submethod02.v3 create mode 100644 test/enums/enum_submethod03.v3 create mode 100644 test/enums/enum_submethod04.v3 create mode 100644 test/enums/enum_submethod05.v3 diff --git a/aeneas/src/core/Eval.v3 b/aeneas/src/core/Eval.v3 index 7941d7b44..f69f502f4 100644 --- a/aeneas/src/core/Eval.v3 +++ b/aeneas/src/core/Eval.v3 @@ -1181,7 +1181,30 @@ def lookupClassVirtual(args: Arguments, object: Record, spec: IrSpec) -> IrSpec } def lookupEnumVirtual(args: Arguments, member: IrMember) -> IrSpec { var ta = args.getTypeArgs(); - return IrSpec.new(ta[0], ta, member); + var enumType = EnumType.!(ta[0]); + var tag = Int.unbox(args.vals[0]); + var targetDecl = findEnumDeclForTag(enumType.enumDecl, tag); + var ir = args.getProgram().ir; + var m: IrMethod; + var vc = targetDecl; + while (vc != null) { + var ic = ir.makeIrClass(vc.getDeclaredType()); + if (ic != null && member.index < ic.methods.length) { + var candidate = ic.methods[member.index]; + if (candidate != null) { m = candidate; break; } + } + vc = vc.parentEnum; + } + if (m == null) m = IrMethod.!(member); + return IrSpec.new(ta[0], ta, m); +} +def findEnumDeclForTag(decl: VstClass, tag: int) -> VstClass { + for (l = decl.subtypeOrder; l != null; l = l.tail) { + if (l.head.tagLo <= tag && tag <= l.head.tagHi) { + return findEnumDeclForTag(l.head, tag); + } + } + return decl; } def lookupVariantVirtual(args: Arguments, object: Record, spec: IrSpec) -> IrSpec { if (object != null) return args.getProgram().ir.resolveMethodImpl(object.rtype, spec); diff --git a/aeneas/src/ir/Ir.v3 b/aeneas/src/ir/Ir.v3 index 196bc81af..65d699f63 100644 --- a/aeneas/src/ir/Ir.v3 +++ b/aeneas/src/ir/Ir.v3 @@ -353,8 +353,10 @@ class IrModule { } def resolveMethodImpl(classType: Type, irSpec: IrSpec) -> IrSpec { var ic = makeIrClass(classType); - var m = ic.methods[irSpec.member.index]; - if (m == irSpec.member) return irSpec; + var idx = irSpec.member.index; + if (idx >= ic.methods.length) return irSpec; + var m = ic.methods[idx]; + if (m == null || m == irSpec.member) return irSpec; while (ic.inherits(m)) { classType = V3.getSuperType(classType); ic = ic.parent; @@ -419,6 +421,36 @@ class IrModule { classes.put(ic); return ic; } + def markEnumSubtypeOverrides(enums: Vector) { + for (i < enums.length) { + var decl = enums[i]; + if (decl.parentEnum != null) continue; + makeIrClass(decl.getDeclaredType()); + markOverridesRecursive(decl); + } + } + private def markOverridesRecursive(decl: VstClass) { + for (l = decl.subtypeOrder; l != null; l = l.tail) { + var sub = l.head; + var subIc = makeIrClass(sub.getDeclaredType()); + if (subIc != null) { + for (j < subIc.methods.length) { + var m = subIc.methods[j]; + if (m == null || m.source == null) continue; + if (!VstMethod.?(m.source)) continue; + var vm = VstMethod.!(m.source); + if (vm.root != vm) { + var rootIc = makeIrClass(vm.root.receiver.getDeclaredType()); + if (rootIc != null && vm.root.index < rootIc.methods.length) { + var rootM = rootIc.methods[vm.root.index]; + if (rootM != null) rootM.flags |= IrFlag.M_OVERRIDDEN; + } + } + } + } + markOverridesRecursive(sub); + } + } def isEnum(t: Type) -> bool { return makeIrClass(t).flags.C_ENUM; } diff --git a/aeneas/src/ir/Normalization.v3 b/aeneas/src/ir/Normalization.v3 index a49f72ef6..6c6511aeb 100644 --- a/aeneas/src/ir/Normalization.v3 +++ b/aeneas/src/ir/Normalization.v3 @@ -114,6 +114,10 @@ class ReachabilityNormalizer(config: NormalizerConfig, ra: ReachabilityAnalyzer) norm(ic.oldType); numberVariant(ic); if (!ic.isUnboxed()) makeNormFieldsAll(ic); + } else if (EnumType.?(ic.oldType) && EnumType.!(ic.oldType).enumDecl.subtypeOrder != null) { + norm(ic.oldType); + numberVariant(ic); + makeNormFieldsAll(ic); } else { layoutClass(ic); } @@ -319,6 +323,15 @@ class ReachabilityNormalizer(config: NormalizerConfig, ra: ReachabilityAnalyzer) } else { for (l = rc.children; l != null; l = l.tail) numberVariantSlot(l.head, rc.minClassId); } + // For enum roots, ensure we reserve the full DFS tag range. Children + // are only subtype enum RaClasses (not individual cases), so they may + // not cover all tag slots. + if (EnumType.?(rc.oldType)) { + var tagHi = EnumType.!(rc.oldType).enumDecl.tagHi; + var endId = rc.minClassId + tagHi + 1; + liveClasses.grow(endId); + if (liveClasses.length < endId) liveClasses.length = endId; + } rc.maxClassId = liveClasses.length; } // Recursively assign class IDs to a variant case/subtype using DFS tag ranges. @@ -329,8 +342,7 @@ class ReachabilityNormalizer(config: NormalizerConfig, ra: ReachabilityAnalyzer) // Non-leaf range nodes whose tagLo equals a child's tagLo are NOT stored in // liveClasses, so layoutMtable can filter them out. def numberVariantSlot(c: RaClass, rootMin: int) { - var decl = ClassType.!(c.oldType).classDecl; - var tagLo = decl.variantTag, tagHi = decl.tagHi; + var tagLo = getTagLo(c.oldType), tagHi = getTagHi(c.oldType); c.minClassId = rootMin + tagLo; c.maxClassId = rootMin + tagHi + 1; liveClasses.grow(c.maxClassId); @@ -341,12 +353,26 @@ class ReachabilityNormalizer(config: NormalizerConfig, ra: ReachabilityAnalyzer) // Check if this node has its own slot (no child starts at the same tag). var ownSlot = true; for (l = c.children; l != null; l = l.tail) { - if (ClassType.!(l.head.oldType).classDecl.variantTag == tagLo) { ownSlot = false; break; } + if (getTagLo(l.head.oldType) == tagLo) { ownSlot = false; break; } } if (ownSlot) liveClasses[rootMin + tagLo] = c; // e.g. "case _" before subtypes for (l = c.children; l != null; l = l.tail) numberVariantSlot(l.head, rootMin); } } + private def getTagLo(t: Type) -> int { + match (t) { + x: ClassType => return x.classDecl.variantTag; + x: EnumType => return x.enumDecl.tagLo; + _ => return 0; + } + } + private def getTagHi(t: Type) -> int { + match (t) { + x: ClassType => return x.classDecl.tagHi; + x: EnumType => return x.enumDecl.tagHi; + _ => return 0; + } + } def mapSimple(t: Type) -> TypeNorm { return typeMap[t] = TypeNorm.new(t, t, null); } @@ -593,6 +619,7 @@ class ReachabilityNormalizer(config: NormalizerConfig, ra: ReachabilityAnalyzer) sm.norm.flags |= IrFlag.M_OVERRIDDEN; } if (rm.virtual != null) virtuals = List.new(rm.virtual, virtuals); + if (m.flags.M_OVERRIDDEN) rm.norm.flags |= IrFlag.M_OVERRIDDEN; } def layoutMtable(rv: RaVirtual) { if (rv.mtable != null) return; @@ -602,31 +629,70 @@ class ReachabilityNormalizer(config: NormalizerConfig, ra: ReachabilityAnalyzer) var table = Array.new(size), mtable = IrMtable.new(rm.norm, rc.minClassId, table); rv.mtable = mtable; - if (rc.isUnboxed()) { + if (rc.isUnboxed() || EnumType.?(rc.oldType)) { var ft = Function.funcRefType(rm.norm.getMethodType()); mtable.record = ra.prog.newRecord(V3Array.newType(ft), size); } if (mtable.table.length > 0) { - for (l = rc.subtypes; l != null; l = l.tail) { // fill out mtable - // Skip non-leaf variant range nodes (e.g. "case _" and intermediate subtype - // variants). Only the actual leaf at each DFS tag slot fills the mtable. - if (liveClasses[l.head.minClassId] != l.head) continue; - var impl = resolveMethodImpl(l.head, rm); - var index = l.head.minClassId - mtable.rootId; - mtable.table[index] = impl.norm; - if (CLOptions.PRINT_WRAP_EXTRA.get()) { - FunctionWrapping.printWrap().put3("layoutMtable: slot %d class %q subclass %q", index, rc.orig.ctype.render, l.head.orig.ctype.render) - .put3(" impl %q{%d/%d}", impl.norm.render, impl.norm.index, impl.norm.uid).outln(); - } - if (mtable.record != null) { - var ta = Arrays.replace(impl.getSpec().typeArgs, 0, impl.norm.receiver); - var spec = IrSpec.new(ta[0], ta, impl.norm); - mtable.record.values[index] = FuncVal.new(spec); + if (EnumType.?(rc.oldType)) { + // Enum: fill by walking the hierarchy. Each enum type + // covers a tag range; subtypes override their parent's range. + fillEnumMtable(mtable, rc, rm); + } else { + for (l = rc.subtypes; l != null; l = l.tail) { // fill out mtable + // Skip non-leaf variant range nodes (e.g. "case _" and intermediate subtype + // variants). Only the actual leaf at each DFS tag slot fills the mtable. + if (liveClasses[l.head.minClassId] != l.head) continue; + fillMtableSlot(mtable, l.head, rm); } } } setMtable(rc, rv); // set mtable for all child virtual methods } + private def fillMtableSlot(mtable: IrMtable, rc: RaClass, rm: RaMethod) { + var impl = resolveMethodImpl(rc, rm); + var index = rc.minClassId - mtable.rootId; + mtable.table[index] = impl.norm; + if (mtable.record != null) { + var ta = Arrays.replace(impl.getSpec().typeArgs, 0, impl.norm.receiver); + var spec = IrSpec.new(ta[0], ta, impl.norm); + mtable.record.values[index] = FuncVal.new(spec); + } + } + private def fillEnumMtable(mtable: IrMtable, rc: RaClass, rm: RaMethod) { + var impl = resolveEnumMethodImpl(rc, rm); + if (impl == null) impl = rm; // fall back to root + var tagLo = if(EnumType.?(rc.oldType), EnumType.!(rc.oldType).enumDecl.tagLo, 0); + var tagHi = if(EnumType.?(rc.oldType), EnumType.!(rc.oldType).enumDecl.tagHi, 0); + if (tagLo < 0) tagLo = 0; + for (tag = tagLo; tag <= tagHi; tag++) { + mtable.table[tag] = impl.norm; + if (mtable.record != null) { + var ta = Arrays.replace(impl.getSpec().typeArgs, 0, impl.norm.receiver); + var spec = IrSpec.new(ta[0], ta, impl.norm); + mtable.record.values[tag] = FuncVal.new(spec); + } + } + // Subtypes override their range + var decl = EnumType.!(rc.oldType).enumDecl; + for (l = decl.subtypeOrder; l != null; l = l.tail) { + var childRc = ra.getClass(l.head.getDeclaredType()); + if (childRc != null) fillEnumMtable(mtable, RaClass.!(childRc), rm); + } + } + // Resolve an enum method by matching VstMethod.root identity, since enum + // subtype IrClasses don't inherit parent's method array. + private def resolveEnumMethodImpl(rc: RaClass, rm: RaMethod) -> RaMethod { + var rootSource = rm.orig.source; + for (ml in rc.methods) { + for (l = ml; l != null; l = l.tail) { + var m = l.head; + if (m.orig.source == rootSource) return m; + if (VstMethod.?(m.orig.source) && VstMethod.!(m.orig.source).root == rootSource) return m; + } + } + return null; + } def setMtable(rc: RaClass, rv: RaVirtual) { var rm = rc.findRaMethod(rv.raMethod); if (rm != null && rm.virtual != null) rm.virtual.mtable = rv.mtable; diff --git a/aeneas/src/ir/Reachability.v3 b/aeneas/src/ir/Reachability.v3 index b6425465c..48cde18e1 100644 --- a/aeneas/src/ir/Reachability.v3 +++ b/aeneas/src/ir/Reachability.v3 @@ -732,7 +732,16 @@ class ReachabilityAnalyzer(compilation: Compilation) { raType = newRaClass(t, ic, parent); } ENUM => { - raType = newRaClass(t, oldIr.makeIrClass(t), null); + var enumDecl = EnumType.!(t).enumDecl; + var rc = newRaClass(t, oldIr.makeIrClass(t), null); + raType = rc; + typeMap[t] = raType; + // Populate rc.subtypes with self plus all descendant subtype + // RaClasses so getVirtual() can drive analyzeVirtual across the + // hierarchy. Enum subtype IrClasses don't inherit parent's fields + // (unlike variants), so we do NOT set RaClass.parent — instead we + // manage subtypes directly. + addEnumSubtypesRecursive(rc, enumDecl); } _ => { for (l = t.nested; l != null; l = l.tail) makeType(l.head); @@ -747,6 +756,15 @@ class ReachabilityAnalyzer(compilation: Compilation) { classes.put(rc); return rc; } + private def addEnumSubtypesRecursive(rc: RaClass, decl: VstClass) { + rc.subtypes = List.new(rc, rc.subtypes); + for (l = decl.subtypeOrder; l != null; l = l.tail) { + var childRc = RaClass.!(makeClass(l.head.getDeclaredType())); + for (gl = childRc.subtypes; gl != null; gl = gl.tail) { + rc.subtypes = List.new(gl.head, rc.subtypes); + } + } + } def mono(t: Type, spec: IrSpec) -> Type { return if(spec != null, spec.instantiateType(t), t); } diff --git a/aeneas/src/ir/SsaNormalizer.v3 b/aeneas/src/ir/SsaNormalizer.v3 index c14ee3725..fe3c34e3a 100644 --- a/aeneas/src/ir/SsaNormalizer.v3 +++ b/aeneas/src/ir/SsaNormalizer.v3 @@ -256,7 +256,6 @@ class SsaRaNormalizer extends SsaRebuilder { } } if (EnumType.?(rc.oldType)) i_old.facts |= Fact.O_NO_NULL_CHECK; - // enum: no receiver prepend needed (tag is already in normalized args) normCall(i_old, funcNorm, newOp, ai_new); } CallClassMethod(method) => { @@ -295,7 +294,16 @@ class SsaRaNormalizer extends SsaRebuilder { var t = extractVirtualRef(orig, method), funcNorm = t.0, m = t.1; var ai_new = normArgs(funcNorm, genRefs(i_old.inputs)); if (t.2) { // still a virtual dispatch - if (rc.isUnboxed()) { + if (EnumType.?(rc.oldType)) { + // enum: tag IS the value; use CallFunctionDirect (no Oop receiver) + var tag = ai_new[0]; + var tagType = V3.getVariantTagType(rc.oldType); + var record = IrSelector.!(m.member).mtable.record; + var table = newGraph.valConst(record.rtype, record); + var func = curBlock.opArrayGetElem(record.rtype, tagType, Facts.O_SAFE_BOUNDS, table, tag); + ai_new = Arrays.prepend(func, ai_new); + normCall(i_old, funcNorm, V3Op.newCallFunctionDirect(funcNorm.sub[0]), ai_new); + } else if (rc.isUnboxed()) { // use the variant tag as an index into a table of functions var tag = normVariantGetTag(rc.variantNorm, ai_new); var record = IrSelector.!(m.member).mtable.record; @@ -311,6 +319,9 @@ class SsaRaNormalizer extends SsaRebuilder { // flattened data type becomes component call and needs new receiver ai_new = Arrays.prepend(newGraph.nullReceiver(), ai_new); } + if (EnumType.?(rc.oldType)) { + // devirtualized enum: tag already in args + } normCall(i_old, funcNorm, V3Op.newCallMethod(m), ai_new); } } diff --git a/aeneas/src/ir/VstIr.v3 b/aeneas/src/ir/VstIr.v3 index e82f43071..ad8acfb89 100644 --- a/aeneas/src/ir/VstIr.v3 +++ b/aeneas/src/ir/VstIr.v3 @@ -116,7 +116,7 @@ class IrBuilder(ctype: Type, parent: IrClass) { ir.flags |= IrFlag.M_OVERRIDE; setIrMethod(r.index, ir); } else { - // enum subtype override: handled by addEnumCaseOverrides + // enum subtype override (no IrClass parent) addIrMethod(ir); } m.index = ir.index; diff --git a/aeneas/src/vst/Verifier.v3 b/aeneas/src/vst/Verifier.v3 index 1a6a9e139..2ec11c858 100644 --- a/aeneas/src/vst/Verifier.v3 +++ b/aeneas/src/vst/Verifier.v3 @@ -52,6 +52,7 @@ class Verifier(compiler: Compiler, prog: Program) { vst.components.apply(addInitFor); vst.classes.apply(addInitFor); vst.enums.apply(addEnumVals); + ir.markEnumSubtypeOverrides(vst.enums); } if (mainMethod != null) { var spec = ir.fromVstMemberAndType(mainMethod.receiver.getDeclaredType(), mainMethod); diff --git a/test/enums/enum_method08.v3 b/test/enums/enum_method08.v3 new file mode 100644 index 000000000..6ce5ffc2e --- /dev/null +++ b/test/enums/enum_method08.v3 @@ -0,0 +1,18 @@ +//@execute 0=7; 1=7; 2=7; 3=7; 4=7 +// Multi-level enum hierarchy with a root-level method. +// Exercises recursive subtype RaClass materialization; all cases (including +// deeply-nested ones) dispatch to the root method. +enum E { A, _; def m() -> int { return 7; } } +enum E.More { B, _ } +enum E.More.Deep { C } +def main(i: int) -> int { + var cases: Array = [E.A, E.More.B, E.More.Deep.C]; + match (i) { + 0 => return E.A.m(); + 1 => return E.More.B.m(); + 2 => return E.More.Deep.C.m(); + 3 => return cases[0].m(); + 4 => return cases[2].m(); + } + return -1; +} diff --git a/test/enums/enum_submethod00.v3 b/test/enums/enum_submethod00.v3 new file mode 100644 index 000000000..b6e3ac7c4 --- /dev/null +++ b/test/enums/enum_submethod00.v3 @@ -0,0 +1,9 @@ +//@execute 0=1; 1=1; 2=2; 3=2; 4=-1 +// Subtype method override: E.More overrides E.m() +enum E { A, B, _; def m() -> int { return 1; } } +enum E.More { C, D; def m() -> int { return 2; } } +def main(i: int) -> int { + var cases: Array = [E.A, E.B, E.More.C, E.More.D]; + if (i < 0 || i >= cases.length) return -1; + return cases[i].m(); +} diff --git a/test/enums/enum_submethod01.v3 b/test/enums/enum_submethod01.v3 new file mode 100644 index 000000000..ca78fd5c8 --- /dev/null +++ b/test/enums/enum_submethod01.v3 @@ -0,0 +1,10 @@ +//@execute 0=1; 1=2; 2=3; 3=-1 +// Multi-level override: E.More overrides, E.More.Deep overrides further +enum E { A, _; def m() -> int { return 1; } } +enum E.More { B, _; def m() -> int { return 2; } } +enum E.More.Deep { C; def m() -> int { return 3; } } +def main(i: int) -> int { + var cases: Array = [E.A, E.More.B, E.More.Deep.C]; + if (i < 0 || i >= cases.length) return -1; + return cases[i].m(); +} diff --git a/test/enums/enum_submethod02.v3 b/test/enums/enum_submethod02.v3 new file mode 100644 index 000000000..57d14287e --- /dev/null +++ b/test/enums/enum_submethod02.v3 @@ -0,0 +1,16 @@ +//@execute 0=1; 1=1; 2=2; 3=-1 +// Partial override: E.Left overrides, E.Right inherits root +enum E { A, _; def m() -> int { return 1; } } +enum E.Left { B; def m() -> int { return 2; } } +enum E.Right { C } +def main(i: int) -> int { + var a: E = E.A; + var b: E = E.Left.B; + var c: E = E.Right.C; + match (i) { + 0 => return a.m(); + 1 => return c.m(); + 2 => return b.m(); + } + return -1; +} diff --git a/test/enums/enum_submethod03.v3 b/test/enums/enum_submethod03.v3 new file mode 100644 index 000000000..f5cfe16e5 --- /dev/null +++ b/test/enums/enum_submethod03.v3 @@ -0,0 +1,15 @@ +//@execute 0=10; 1=20; 2=11; 3=20; 4=-1 +// Multiple methods: subtype overrides m1 but inherits m2 +enum E { A, _; def m1() -> int { return 10; } def m2() -> int { return 20; } } +enum E.More { B; def m1() -> int { return 11; } } +def main(i: int) -> int { + var a: E = E.A; + var b: E = E.More.B; + match (i) { + 0 => return a.m1(); + 1 => return a.m2(); + 2 => return b.m1(); + 3 => return b.m2(); + } + return -1; +} diff --git a/test/enums/enum_submethod04.v3 b/test/enums/enum_submethod04.v3 new file mode 100644 index 000000000..f6538f9ed --- /dev/null +++ b/test/enums/enum_submethod04.v3 @@ -0,0 +1,13 @@ +//@execute 0=10; 1=10; 2=20; 3=20 +// Static vs dynamic dispatch: direct case call vs E-typed call +enum E { A, B, _; def m() -> int { return 10; } } +enum E.More { C, D; def m() -> int { return 20; } } +def main(i: int) -> int { + match (i) { + 0 => return E.A.m(); + 1 => return E.B.m(); + 2 => return E.More.C.m(); + 3 => return E.More.D.m(); + } + return -1; +} diff --git a/test/enums/enum_submethod05.v3 b/test/enums/enum_submethod05.v3 new file mode 100644 index 000000000..63cf29508 --- /dev/null +++ b/test/enums/enum_submethod05.v3 @@ -0,0 +1,13 @@ +//@execute 0=100; 1=200; 2=600; 3=600 +// Override method with parameters +enum E(x: int) { A(100), B(200), _; def scale(n: int) -> int { return x * n; } } +enum E.More { C(300); def scale(n: int) -> int { return x * n * 2; } } +def main(i: int) -> int { + match (i) { + 0 => return E.A.scale(1); + 1 => return E.B.scale(1); + 2 => return E.More.C.scale(1); + 3 => { var e: E = E.More.C; return e.scale(1); } + } + return -1; +} From 46a19f2dc2f409842463a6fb74d305b5f8915cf8 Mon Sep 17 00:00:00 2001 From: Eliot Moss Date: Thu, 16 Apr 2026 19:23:12 -0400 Subject: [PATCH 10/58] Add edge-case tests for _ case + method; fix null mtable crash MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add tests for enum types with _ case and methods but no (reachable) subtypes: - enum_submethod06: root with _ and method, no subtypes declared - enum_submethod07: subtype with only _ case and override, no sub-subtypes Fix NullCheckException in SsaNormalizer when the mtable is not built (size-1 table skipped by RaDevirtualize) but the method is still marked virtual. Fall through to direct CallMethod when mtable is null. Known: enum_submethod07 fails on JVM target (NoSuchMethodError in JVM enum method host class generation — separate JVM backend issue). Co-Authored-By: Claude Opus 4.6 (1M context) --- aeneas/src/ir/SsaNormalizer.v3 | 22 ++++++++++++++-------- test/enums/enum_submethod06.v3 | 9 +++++++++ test/enums/enum_submethod07.v3 | 11 +++++++++++ 3 files changed, 34 insertions(+), 8 deletions(-) create mode 100644 test/enums/enum_submethod06.v3 create mode 100644 test/enums/enum_submethod07.v3 diff --git a/aeneas/src/ir/SsaNormalizer.v3 b/aeneas/src/ir/SsaNormalizer.v3 index fe3c34e3a..5b6e5e9b8 100644 --- a/aeneas/src/ir/SsaNormalizer.v3 +++ b/aeneas/src/ir/SsaNormalizer.v3 @@ -295,14 +295,20 @@ class SsaRaNormalizer extends SsaRebuilder { var ai_new = normArgs(funcNorm, genRefs(i_old.inputs)); if (t.2) { // still a virtual dispatch if (EnumType.?(rc.oldType)) { - // enum: tag IS the value; use CallFunctionDirect (no Oop receiver) - var tag = ai_new[0]; - var tagType = V3.getVariantTagType(rc.oldType); - var record = IrSelector.!(m.member).mtable.record; - var table = newGraph.valConst(record.rtype, record); - var func = curBlock.opArrayGetElem(record.rtype, tagType, Facts.O_SAFE_BOUNDS, table, tag); - ai_new = Arrays.prepend(func, ai_new); - normCall(i_old, funcNorm, V3Op.newCallFunctionDirect(funcNorm.sub[0]), ai_new); + var sel = IrSelector.!(m.member); + if (sel.mtable != null && sel.mtable.record != null) { + // enum: tag IS the value; use CallFunctionDirect (no Oop receiver) + var tag = ai_new[0]; + var tagType = V3.getVariantTagType(rc.oldType); + var record = sel.mtable.record; + var table = newGraph.valConst(record.rtype, record); + var func = curBlock.opArrayGetElem(record.rtype, tagType, Facts.O_SAFE_BOUNDS, table, tag); + ai_new = Arrays.prepend(func, ai_new); + normCall(i_old, funcNorm, V3Op.newCallFunctionDirect(funcNorm.sub[0]), ai_new); + } else { + // mtable not built (single impl live); direct call + normCall(i_old, funcNorm, V3Op.newCallMethod(m), ai_new); + } } else if (rc.isUnboxed()) { // use the variant tag as an index into a table of functions var tag = normVariantGetTag(rc.variantNorm, ai_new); diff --git a/test/enums/enum_submethod06.v3 b/test/enums/enum_submethod06.v3 new file mode 100644 index 000000000..7a038db37 --- /dev/null +++ b/test/enums/enum_submethod06.v3 @@ -0,0 +1,9 @@ +//@execute 0=1; 1=1; 2=-1 +// Enum with _ case, method, but no subtypes: _ is unreachable, +// all reachable cases dispatch to root method. +enum E { A, B, _; def m() -> int { return 1; } } +def main(i: int) -> int { + var cases: Array = [E.A, E.B]; + if (i < 0 || i >= cases.length) return -1; + return cases[i].m(); +} diff --git a/test/enums/enum_submethod07.v3 b/test/enums/enum_submethod07.v3 new file mode 100644 index 000000000..837425968 --- /dev/null +++ b/test/enums/enum_submethod07.v3 @@ -0,0 +1,11 @@ +//@execute 0=1; 1=-1 +// Subtype with only _ case and an override method, but no subtypes of its +// own: E.More.m is unreachable (no named cases produce values in its range), +// so all values dispatch to the root method. +enum E { A, _; def m() -> int { return 1; } } +enum E.More { _; def m() -> int { return 2; } } +def main(i: int) -> int { + var cases: Array = [E.A]; + if (i < 0 || i >= cases.length) return -1; + return cases[i].m(); +} From 19639184dd52b3bb645f618fd8b8bc9330457b65 Mon Sep 17 00:00:00 2001 From: Eliot Moss Date: Thu, 16 Apr 2026 20:08:34 -0400 Subject: [PATCH 11/58] Fix multi-method selective override dispatch Root cause: enum subtype IrClasses placed override methods at sequential indices (next available slot), ignoring the root method's index. When E declares m1 and m2, the members list order may place m2 at index 2 and m1 at index 3. E.More's override of m1 went to index 2 (mismatch with root's index 3), so lookupEnumVirtual couldn't find it. Fix: in addVstMethod for enum subtype overrides (no IrClass parent), pad the methods array to place the override at the root method's index. This ensures resolveMethodImpl, lookupEnumVirtual, and analyzeVirtual all find the override via consistent index lookup. All 224 enum tests now pass on v3i, x86-linux, x86-64-linux. JVM target has a separate FunctionWrappers crash for some override patterns (deferred). Co-Authored-By: Claude Opus 4.6 (1M context) --- aeneas/src/ir/VstIr.v3 | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/aeneas/src/ir/VstIr.v3 b/aeneas/src/ir/VstIr.v3 index ad8acfb89..15e318f06 100644 --- a/aeneas/src/ir/VstIr.v3 +++ b/aeneas/src/ir/VstIr.v3 @@ -116,8 +116,11 @@ class IrBuilder(ctype: Type, parent: IrClass) { ir.flags |= IrFlag.M_OVERRIDE; setIrMethod(r.index, ir); } else { - // enum subtype override (no IrClass parent) - addIrMethod(ir); + // enum subtype override (no IrClass parent): place at root's + // index so resolveMethodImpl and lookupEnumVirtual can find it. + while (methods.length <= r.index) methods.put(null); + setIndex(r.index, ir); + methods[r.index] = ir; } m.index = ir.index; } From 25dd610358ab0933513d35eb0849f6736b3864be Mon Sep 17 00:00:00 2001 From: Eliot Moss Date: Thu, 16 Apr 2026 21:47:31 -0400 Subject: [PATCH 12/58] Fix JVM backend for enum virtual dispatch - SsaNormalizer: target-aware enum virtual dispatch. Native/wasm uses CallFunctionDirect (no Oop receiver). JVM uses CallFunction (with Oop receiver) since JVM closures require the Oop convention. - FunctionWrappers: guard against null resolveMethodImpl result for enum subtypes that don't override the method. All 224 enum tests pass on v3i, x86-linux, x86-64-linux, and jvm. Co-Authored-By: Claude Opus 4.6 (1M context) --- aeneas/src/ir/FunctionWrappers.v3 | 1 + aeneas/src/ir/SsaNormalizer.v3 | 13 ++++++++++--- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/aeneas/src/ir/FunctionWrappers.v3 b/aeneas/src/ir/FunctionWrappers.v3 index 37c17c942..3e53a3f2d 100644 --- a/aeneas/src/ir/FunctionWrappers.v3 +++ b/aeneas/src/ir/FunctionWrappers.v3 @@ -738,6 +738,7 @@ class FunctionWrappers { for (l = rc.subtypes; l != null; l = l.tail) { var rcThis = l.head; var impl = rn.resolveMethodImpl(rcThis, rm); + if (impl == null) continue; var orig = impl.orig; var origType = if(impl.spec == null, impl.orig.getMethodType(), impl.spec.getMethodType()); var origSig = FuncType.!(origType).sig(); diff --git a/aeneas/src/ir/SsaNormalizer.v3 b/aeneas/src/ir/SsaNormalizer.v3 index 5b6e5e9b8..abbb87806 100644 --- a/aeneas/src/ir/SsaNormalizer.v3 +++ b/aeneas/src/ir/SsaNormalizer.v3 @@ -297,14 +297,21 @@ class SsaRaNormalizer extends SsaRebuilder { if (EnumType.?(rc.oldType)) { var sel = IrSelector.!(m.member); if (sel.mtable != null && sel.mtable.record != null) { - // enum: tag IS the value; use CallFunctionDirect (no Oop receiver) + // enum: tag IS the value; index into mtable var tag = ai_new[0]; var tagType = V3.getVariantTagType(rc.oldType); var record = sel.mtable.record; var table = newGraph.valConst(record.rtype, record); var func = curBlock.opArrayGetElem(record.rtype, tagType, Facts.O_SAFE_BOUNDS, table, tag); - ai_new = Arrays.prepend(func, ai_new); - normCall(i_old, funcNorm, V3Op.newCallFunctionDirect(funcNorm.sub[0]), ai_new); + if (context.compiler.NormConfig.NonRefClosureReceiver) { + // native/wasm: CallFunctionDirect (no Oop receiver) + ai_new = Arrays.prepend(func, ai_new); + normCall(i_old, funcNorm, V3Op.newCallFunctionDirect(funcNorm.sub[0]), ai_new); + } else { + // JVM: CallFunction (with Oop receiver) + ai_new = Arrays.concat([func, newGraph.nullReceiver()], ai_new); + normCall(i_old, funcNorm, V3Op.newCallFunction(funcNorm.sub[0]), ai_new); + } } else { // mtable not built (single impl live); direct call normCall(i_old, funcNorm, V3Op.newCallMethod(m), ai_new); From 3348619c2e569ce77b761b8af4b98e8c52163921 Mon Sep 17 00:00:00 2001 From: Eliot Moss Date: Fri, 17 Apr 2026 12:18:11 -0400 Subject: [PATCH 13/58] Add enum method closure tests with subtype overrides Four new tests for the `var f = e.m; f()` pattern with subtype method overrides: - closure01: basic closure from virtual enum method - closure02: selective override + inherited method closures - closure03: 3-level hierarchy closures - closure04: pass closure to another function Fix VariantGetVirtual for enums: use mtable array indexing instead of opGetSelector (which dereferences the tag as a pointer, causing NullCheckException). Enum tags are integers, not record pointers. All pass on v3i, x86-linux, x86-64-linux, wasm. closure04 fails on JVM and wasm-gc due to pre-existing limitation: enum integer values cannot serve as object receivers in reference-typed closure conventions. Co-Authored-By: Claude Opus 4.6 (1M context) --- aeneas/src/ir/SsaNormalizer.v3 | 18 +++++++++++++++++- test/enums/enum_closure01.v3 | 10 ++++++++++ test/enums/enum_closure02.v3 | 15 +++++++++++++++ test/enums/enum_closure03.v3 | 11 +++++++++++ test/enums/enum_closure04.v3 | 12 ++++++++++++ 5 files changed, 65 insertions(+), 1 deletion(-) create mode 100644 test/enums/enum_closure01.v3 create mode 100644 test/enums/enum_closure02.v3 create mode 100644 test/enums/enum_closure03.v3 create mode 100644 test/enums/enum_closure04.v3 diff --git a/aeneas/src/ir/SsaNormalizer.v3 b/aeneas/src/ir/SsaNormalizer.v3 index abbb87806..716e08379 100644 --- a/aeneas/src/ir/SsaNormalizer.v3 +++ b/aeneas/src/ir/SsaNormalizer.v3 @@ -221,9 +221,25 @@ class SsaRaNormalizer extends SsaRebuilder { mapN(i_old, [funcRef(extractMethodRef(orig, method).1), obj]); } VariantGetVirtual(method) => { + var rc = norm.ra.getClass(op.typeArgs[0]); var t = extractVirtualRef(orig, method), obj = genRef1(args[0]); if (t.2) { // still a virtual dispatch - mapN(i_old, [curBlock.opGetSelector(t.1, obj), obj]); + if (EnumType.?(rc.oldType)) { + // enum: index mtable with tag (not selector on record) + var sel = IrSelector.!(t.1.member); + if (sel.mtable != null && sel.mtable.record != null) { + var tag = obj; + var tagType = V3.getVariantTagType(rc.oldType); + var record = sel.mtable.record; + var table = newGraph.valConst(record.rtype, record); + var func = curBlock.opArrayGetElem(record.rtype, tagType, Facts.O_SAFE_BOUNDS, table, tag); + mapN(i_old, [func, obj]); + } else { + mapN(i_old, [funcRef(t.1), obj]); + } + } else { + mapN(i_old, [curBlock.opGetSelector(t.1, obj), obj]); + } } else { mapN(i_old, [funcRef(t.1), obj]); } diff --git a/test/enums/enum_closure01.v3 b/test/enums/enum_closure01.v3 new file mode 100644 index 000000000..6561e0531 --- /dev/null +++ b/test/enums/enum_closure01.v3 @@ -0,0 +1,10 @@ +//@execute 0=1; 1=2; 2=-1 +// Enum method closure with subtype override +enum E { A, _; def m() -> int { return 1; } } +enum E.More { B; def m() -> int { return 2; } } +def main(i: int) -> int { + var cases: Array = [E.A, E.More.B]; + if (i < 0 || i >= cases.length) return -1; + var f = cases[i].m; + return f(); +} diff --git a/test/enums/enum_closure02.v3 b/test/enums/enum_closure02.v3 new file mode 100644 index 000000000..be6a62d9f --- /dev/null +++ b/test/enums/enum_closure02.v3 @@ -0,0 +1,15 @@ +//@execute 0=10; 1=11; 2=20; 3=20; 4=-1 +// Enum method closure: selective override + inherited method +enum E { A, _; def m1() -> int { return 10; } def m2() -> int { return 20; } } +enum E.More { B; def m1() -> int { return 11; } } +def main(i: int) -> int { + var a: E = E.A; + var b: E = E.More.B; + match (i) { + 0 => { var f = a.m1; return f(); } + 1 => { var f = b.m1; return f(); } + 2 => { var f = a.m2; return f(); } + 3 => { var f = b.m2; return f(); } + } + return -1; +} diff --git a/test/enums/enum_closure03.v3 b/test/enums/enum_closure03.v3 new file mode 100644 index 000000000..ecfa4bac1 --- /dev/null +++ b/test/enums/enum_closure03.v3 @@ -0,0 +1,11 @@ +//@execute 0=1; 1=2; 2=3; 3=-1 +// Enum method closure: 3-level hierarchy +enum E { A, _; def m() -> int { return 1; } } +enum E.More { B, _; def m() -> int { return 2; } } +enum E.More.Deep { C; def m() -> int { return 3; } } +def main(i: int) -> int { + var cases: Array = [E.A, E.More.B, E.More.Deep.C]; + if (i < 0 || i >= cases.length) return -1; + var f = cases[i].m; + return f(); +} diff --git a/test/enums/enum_closure04.v3 b/test/enums/enum_closure04.v3 new file mode 100644 index 000000000..f27b872e3 --- /dev/null +++ b/test/enums/enum_closure04.v3 @@ -0,0 +1,12 @@ +//@execute 0=10; 1=20; 2=30 +// Enum method closure: store in variable, pass to function +enum E { A, _; def m() -> int { return 10; } } +enum E.More { B; def m() -> int { return 20; } } +enum E.Most { C; def m() -> int { return 30; } } +def call(f: void -> int) -> int { return f(); } +def main(i: int) -> int { + var cases: Array = [E.A, E.More.B, E.Most.C]; + if (i < 0 || i >= cases.length) return -1; + var f = cases[i].m; + return call(f); +} From 7ebd2f0e8f98f7e0e58ea22ed79aaf2c0028b7a8 Mon Sep 17 00:00:00 2001 From: Eliot Moss Date: Fri, 17 Apr 2026 13:28:22 -0400 Subject: [PATCH 14/58] JVM enum closure boxing: box tag to Integer for Oop receiver slot MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When enum method closures are used on JVM, the enum tag (int) must be boxed to Integer for the JVM's Object-typed closure receiver slot. Changes: - SsaNormalizer: boxEnumClosureReceiver() inserts TypeSubsume(int, Oop) for enum closure receivers on !NonRefClosureReceiver targets (JVM). Applied in VariantGetVirtual, VariantGetMethod, and CallVariantVirtual. For CallVariantVirtual JVM path: tag moves from args to Oop slot (boxed). - JvmGen buildComponentClosure: for enum types, unbox Integer receiver (local 1) via checkcast + intValue() to get the tag, then load user args from local 2+. - SsaJvmGen TypeSubsume: handle int→Oop via Integer.valueOf(). enum_closure02 and enum_closure04 now pass on JVM. enum_closure01/03 have a JVM stack height scheduling issue with TypeSubsume placement (doesn't affect correctness on other targets). Co-Authored-By: Claude Opus 4.6 (1M context) --- aeneas/src/ir/SsaNormalizer.v3 | 18 ++++++++++++++++-- aeneas/src/jvm/JvmGen.v3 | 18 +++++++++++++++++- aeneas/src/jvm/SsaJvmGen.v3 | 11 ++++++++++- 3 files changed, 43 insertions(+), 4 deletions(-) diff --git a/aeneas/src/ir/SsaNormalizer.v3 b/aeneas/src/ir/SsaNormalizer.v3 index 716e08379..5dc246c2c 100644 --- a/aeneas/src/ir/SsaNormalizer.v3 +++ b/aeneas/src/ir/SsaNormalizer.v3 @@ -218,6 +218,8 @@ class SsaRaNormalizer extends SsaRebuilder { VariantGetField(field) => normGetField(true, i_old, field, op); VariantGetMethod(method) => { var obj = genRef1(args[0]); + var rc = norm.ra.getClass(op.typeArgs[0]); + obj = boxEnumClosureReceiver(rc, obj); mapN(i_old, [funcRef(extractMethodRef(orig, method).1), obj]); } VariantGetVirtual(method) => { @@ -233,6 +235,7 @@ class SsaRaNormalizer extends SsaRebuilder { var record = sel.mtable.record; var table = newGraph.valConst(record.rtype, record); var func = curBlock.opArrayGetElem(record.rtype, tagType, Facts.O_SAFE_BOUNDS, table, tag); + obj = boxEnumClosureReceiver(rc, obj); mapN(i_old, [func, obj]); } else { mapN(i_old, [funcRef(t.1), obj]); @@ -241,6 +244,7 @@ class SsaRaNormalizer extends SsaRebuilder { mapN(i_old, [curBlock.opGetSelector(t.1, obj), obj]); } } else { + obj = boxEnumClosureReceiver(rc, obj); mapN(i_old, [funcRef(t.1), obj]); } } @@ -324,8 +328,11 @@ class SsaRaNormalizer extends SsaRebuilder { ai_new = Arrays.prepend(func, ai_new); normCall(i_old, funcNorm, V3Op.newCallFunctionDirect(funcNorm.sub[0]), ai_new); } else { - // JVM: CallFunction (with Oop receiver) - ai_new = Arrays.concat([func, newGraph.nullReceiver()], ai_new); + // JVM: CallFunction with boxed tag as Oop receiver. + // The tag moves from ai_new[0] to the Oop slot (boxed); + // remaining user args follow. + var boxedTag = curBlock.opTypeSubsume(tagType, Oop.TYPE, ai_new[0]); + ai_new = Arrays.concat([func, boxedTag], Arrays.range(ai_new, 1, ai_new.length)); normCall(i_old, funcNorm, V3Op.newCallFunction(funcNorm.sub[0]), ai_new); } } else { @@ -1894,6 +1901,13 @@ class SsaRaNormalizer extends SsaRebuilder { private def funcRef(m: IrSpec) -> SsaInstr { return newGraph.valConst(Function.funcRefType(m.getFuncType()), FuncVal.new(m)); } + // For JVM/wasm-gc: box enum tag to Oop for closure receiver slot. + private def boxEnumClosureReceiver(rc: RaClass, obj: SsaInstr) -> SsaInstr { + if (EnumType.?(rc.oldType) && !context.compiler.NormConfig.NonRefClosureReceiver) { + return curBlock.opTypeSubsume(V3.getVariantTagType(rc.oldType), Oop.TYPE, obj); + } + return obj; + } private def nonzero(tn: TypeNorm) -> TypeNorm { if (tn.size == 0) context.fail("expected at least one type"); return tn; diff --git a/aeneas/src/jvm/JvmGen.v3 b/aeneas/src/jvm/JvmGen.v3 index f8ede8d0d..51e225f8f 100644 --- a/aeneas/src/jvm/JvmGen.v3 +++ b/aeneas/src/jvm/JvmGen.v3 @@ -648,9 +648,25 @@ class JvmV3ClosureGen extends JvmClassGen { // emit the invocation method var invoke_code = builder.newCodeBuilder(); - builder.emitLoadArguments(invoke_code, methType, methType, 2); var jclass = builder.jprog.jvmClass(memberRef.receiver); invoke_code.locals(invoke_sig.localsSize(true)); + if (EnumType.?(memberRef.receiver)) { + // Enum closure: the Oop receiver (local 1) is a boxed Integer (the tag). + // Unbox it, then load user args from local 2+. + invoke_code.load(JvmTypes.KIND_OBJECT, 1); + invoke_code.checkcast(JvmTypes.java_lang_Integer); + invoke_code.invokevirtual(JvmTypes.java_lang_Integer.name, "intValue", JvmTypes.SIG_VOID_INT); + var paramList = Function.getParamTypeList(methType); + if (paramList != null) paramList = paramList.tail; // skip prepended tag + var localIdx = 2; + for (l = paramList; l != null; l = l.tail) { + var jtype = builder.jprog.jvmType(l.head); + invoke_code.load(jtype.kind, localIdx); + localIdx += jtype.kind.width; + } + } else { + builder.emitLoadArguments(invoke_code, methType, methType, 2); + } var mname = if(EnumType.?(memberRef.receiver), builder.jprog.mangleEnumMethod(IrMethod.!(memberRef.member)), V3.mangleIrMember(memberRef.member)); diff --git a/aeneas/src/jvm/SsaJvmGen.v3 b/aeneas/src/jvm/SsaJvmGen.v3 index a717c3879..d4b75eaae 100644 --- a/aeneas/src/jvm/SsaJvmGen.v3 +++ b/aeneas/src/jvm/SsaJvmGen.v3 @@ -364,6 +364,12 @@ class SsaJvmGen(jprog: JvmProgram, context: SsaContext, jsig: JvmSig, code: JvmC if (SsaConst.?(i)) code.builder.emitValue(code, i.getType(), SsaConst.!(i).val); else code.load(jprog.jvmKind(i.getType()), ssaDecon.mapReg(i)); } + def emitLoadBoxed(i: SsaInstr) { + emitLoad(i); + if (PrimType.?(i.getType())) { + code.invokestatic(JvmTypes.java_lang_Integer.name, "valueOf", JvmTypes.SIG_INT_INTEGER); + } + } // emit a single apply instruction def emitApply(i: SsaApplyOp, op: Operator, leave: bool) -> bool { var popped = false; @@ -445,7 +451,10 @@ class SsaJvmGen(jprog: JvmProgram, context: SsaContext, jsig: JvmSig, code: JvmC TypeQuery => emitTypeQuery(op.typeArgs[0], op.typeArgs[1]); TypeSubsume => { var implicitType = op.typeArgs[1]; - if (V3.isFunction(implicitType)) { + if (implicitType == Oop.TYPE && PrimType.?(op.typeArgs[0])) { + // Box int to Integer (e.g. enum tag for closure receiver) + code.invokestatic(JvmTypes.java_lang_Integer.name, "valueOf", JvmTypes.SIG_INT_INTEGER); + } else if (V3.isFunction(implicitType)) { var exactType = op.typeArgs[0]; if (exactType == Oop.TYPE || exactType.typeCon.kind == Kind.ANYFUNC) { // a cast inserted from overflow params. code.checkcast(jprog.jvmType(implicitType)); From a3662679bfac090618e8ed885cb1d7fa199633a7 Mon Sep 17 00:00:00 2001 From: Eliot Moss Date: Fri, 17 Apr 2026 14:27:07 -0400 Subject: [PATCH 15/58] Revert JVM boxing experiments; fix inline closure JVM path Revert the TypeSubsume(int, Oop) boxing approach for JVM enum closures. Fix CallVariantVirtual JVM path: use null Oop + raw tag in args (not boxed TypeSubsume), fixing inline closures 01-03 on JVM. Remaining: enum_closure04 (escaped closure) on JVM needs adapter class. Co-Authored-By: Claude Opus 4.6 (1M context) --- aeneas/src/ir/SsaNormalizer.v3 | 10 ++-------- aeneas/src/jvm/JvmGen.v3 | 18 +----------------- aeneas/src/jvm/SsaJvmGen.v3 | 5 +---- 3 files changed, 4 insertions(+), 29 deletions(-) diff --git a/aeneas/src/ir/SsaNormalizer.v3 b/aeneas/src/ir/SsaNormalizer.v3 index 5dc246c2c..34e8d9454 100644 --- a/aeneas/src/ir/SsaNormalizer.v3 +++ b/aeneas/src/ir/SsaNormalizer.v3 @@ -328,11 +328,8 @@ class SsaRaNormalizer extends SsaRebuilder { ai_new = Arrays.prepend(func, ai_new); normCall(i_old, funcNorm, V3Op.newCallFunctionDirect(funcNorm.sub[0]), ai_new); } else { - // JVM: CallFunction with boxed tag as Oop receiver. - // The tag moves from ai_new[0] to the Oop slot (boxed); - // remaining user args follow. - var boxedTag = curBlock.opTypeSubsume(tagType, Oop.TYPE, ai_new[0]); - ai_new = Arrays.concat([func, boxedTag], Arrays.range(ai_new, 1, ai_new.length)); + // JVM: CallFunction with null Oop + raw tag in args + ai_new = Arrays.concat([func, newGraph.nullReceiver()], ai_new); normCall(i_old, funcNorm, V3Op.newCallFunction(funcNorm.sub[0]), ai_new); } } else { @@ -1903,9 +1900,6 @@ class SsaRaNormalizer extends SsaRebuilder { } // For JVM/wasm-gc: box enum tag to Oop for closure receiver slot. private def boxEnumClosureReceiver(rc: RaClass, obj: SsaInstr) -> SsaInstr { - if (EnumType.?(rc.oldType) && !context.compiler.NormConfig.NonRefClosureReceiver) { - return curBlock.opTypeSubsume(V3.getVariantTagType(rc.oldType), Oop.TYPE, obj); - } return obj; } private def nonzero(tn: TypeNorm) -> TypeNorm { diff --git a/aeneas/src/jvm/JvmGen.v3 b/aeneas/src/jvm/JvmGen.v3 index 51e225f8f..bae03d056 100644 --- a/aeneas/src/jvm/JvmGen.v3 +++ b/aeneas/src/jvm/JvmGen.v3 @@ -650,23 +650,7 @@ class JvmV3ClosureGen extends JvmClassGen { var invoke_code = builder.newCodeBuilder(); var jclass = builder.jprog.jvmClass(memberRef.receiver); invoke_code.locals(invoke_sig.localsSize(true)); - if (EnumType.?(memberRef.receiver)) { - // Enum closure: the Oop receiver (local 1) is a boxed Integer (the tag). - // Unbox it, then load user args from local 2+. - invoke_code.load(JvmTypes.KIND_OBJECT, 1); - invoke_code.checkcast(JvmTypes.java_lang_Integer); - invoke_code.invokevirtual(JvmTypes.java_lang_Integer.name, "intValue", JvmTypes.SIG_VOID_INT); - var paramList = Function.getParamTypeList(methType); - if (paramList != null) paramList = paramList.tail; // skip prepended tag - var localIdx = 2; - for (l = paramList; l != null; l = l.tail) { - var jtype = builder.jprog.jvmType(l.head); - invoke_code.load(jtype.kind, localIdx); - localIdx += jtype.kind.width; - } - } else { - builder.emitLoadArguments(invoke_code, methType, methType, 2); - } + builder.emitLoadArguments(invoke_code, methType, methType, 2); var mname = if(EnumType.?(memberRef.receiver), builder.jprog.mangleEnumMethod(IrMethod.!(memberRef.member)), V3.mangleIrMember(memberRef.member)); diff --git a/aeneas/src/jvm/SsaJvmGen.v3 b/aeneas/src/jvm/SsaJvmGen.v3 index d4b75eaae..b8a771c32 100644 --- a/aeneas/src/jvm/SsaJvmGen.v3 +++ b/aeneas/src/jvm/SsaJvmGen.v3 @@ -451,10 +451,7 @@ class SsaJvmGen(jprog: JvmProgram, context: SsaContext, jsig: JvmSig, code: JvmC TypeQuery => emitTypeQuery(op.typeArgs[0], op.typeArgs[1]); TypeSubsume => { var implicitType = op.typeArgs[1]; - if (implicitType == Oop.TYPE && PrimType.?(op.typeArgs[0])) { - // Box int to Integer (e.g. enum tag for closure receiver) - code.invokestatic(JvmTypes.java_lang_Integer.name, "valueOf", JvmTypes.SIG_INT_INTEGER); - } else if (V3.isFunction(implicitType)) { + if (V3.isFunction(implicitType)) { var exactType = op.typeArgs[0]; if (exactType == Oop.TYPE || exactType.typeCon.kind == Kind.ANYFUNC) { // a cast inserted from overflow params. code.checkcast(jprog.jvmType(implicitType)); From 2b13898f2876c30588bde644516b08a3b8cfe5a7 Mon Sep 17 00:00:00 2001 From: Eliot Moss Date: Fri, 17 Apr 2026 16:06:59 -0400 Subject: [PATCH 16/58] JVM: always box enum tag for user-defined enum methods MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Enum method calls on JVM now use Oop (boxed Integer) for the tag parameter instead of the raw int tag. This ensures JVM closure class invoke signatures match the user-facing closure type, fixing escaped enum closures passed to functions. Changes: - Normalization visitMethod: prepend Oop (not raw tag) for JVM user enum methods. layoutMtable: user funcRefType for mtable array. - SsaNormalizer genGraph: Oop param with unboxing TypeSubsume(Oop→int) for JVM user enum methods. CallMethod: box tag via TypeSubsume. CallVariantVirtual JVM: box tag + CallFunction with user type. boxEnumClosureReceiver: box for VariantGetVirtual/VariantGetMethod. - SsaBuilder: prevent Oop constant folding, handle Oop→int subsumption. - SsaJvmGen TypeSubsume: int→Oop via Integer.valueOf, Oop→int via checkcast Integer + intValue(). - JvmV3ClosureAdapterGen: handle param count mismatch (extra Oop tag param) by loading boxed tag from adapter's Oop receiver. All 228 enum tests pass on v3i, x86-linux, x86-64-linux, jvm, wasm. enum_closure04 fails on wasm-gc (needs i31ref boxing — separate task). Co-Authored-By: Claude Opus 4.6 (1M context) --- aeneas/src/ir/Normalization.v3 | 20 ++++++++++++++--- aeneas/src/ir/SsaNormalizer.v3 | 39 +++++++++++++++++++++++++++++----- aeneas/src/jvm/JvmGen.v3 | 28 +++++++++++++++++++++++- aeneas/src/jvm/SsaJvmGen.v3 | 7 +++++- aeneas/src/ssa/SsaBuilder.v3 | 4 +++- 5 files changed, 87 insertions(+), 11 deletions(-) diff --git a/aeneas/src/ir/Normalization.v3 b/aeneas/src/ir/Normalization.v3 index 6c6511aeb..b9c1fea5d 100644 --- a/aeneas/src/ir/Normalization.v3 +++ b/aeneas/src/ir/Normalization.v3 @@ -163,9 +163,12 @@ class ReachabilityNormalizer(config: NormalizerConfig, ra: ReachabilityAnalyzer) // move flattened data type receiver to function sig ftype = Function.prependParamTypes(rc.variantNorm.sub, ftype); } else if (EnumType.?(rc.oldType)) { - // enum: prepend tag type so dispatch table CallFunction matches CallMethod var tagType = V3.getVariantTagType(rc.oldType); - ftype = Function.prependParamType(tagType, ftype); + if (!config.NonRefClosureReceiver && m.source != null && VstMethod.?(m.source)) { + ftype = Function.prependParamType(Oop.TYPE, ftype); + } else { + ftype = Function.prependParamType(tagType, ftype); + } } rm.funcNorm = FuncNorm.!(norm(ftype)); var typeParams = if(rm.spec != null, rm.spec.getTypes().methodTypeArgs); @@ -630,7 +633,18 @@ class ReachabilityNormalizer(config: NormalizerConfig, ra: ReachabilityAnalyzer) rv.mtable = mtable; if (rc.isUnboxed() || EnumType.?(rc.oldType)) { - var ft = Function.funcRefType(rm.norm.getMethodType()); + var methType = rm.norm.getMethodType(); + // JVM enum user methods: strip Oop tag from mtable funcRefType + if (!config.NonRefClosureReceiver && EnumType.?(rc.oldType) + && rm.orig.source != null && VstMethod.?(rm.orig.source)) { + var paramList = Function.getParamTypeList(methType); + if (paramList != null && paramList.head == Oop.TYPE) { + var userParamType: Type = Void.TYPE; + if (paramList.tail != null) userParamType = Tuple.fromTypeArray(Lists.toArray(paramList.tail)); + methType = Function.newType(userParamType, Function.getReturnType(methType)); + } + } + var ft = Function.funcRefType(methType); mtable.record = ra.prog.newRecord(V3Array.newType(ft), size); } if (mtable.table.length > 0) { diff --git a/aeneas/src/ir/SsaNormalizer.v3 b/aeneas/src/ir/SsaNormalizer.v3 index 34e8d9454..302b449a9 100644 --- a/aeneas/src/ir/SsaNormalizer.v3 +++ b/aeneas/src/ir/SsaNormalizer.v3 @@ -58,8 +58,13 @@ class SsaRaNormalizer extends SsaRebuilder { var newIrType = norm.ra.getClass(tn.oldType).newIrType; newParams.put(SsaParam.new(newParams.length, newIrType)); start++; // skip synthesized receiver + } else if (EnumType.?(tn.oldType) && !norm.config.NonRefClosureReceiver + && context.method.source != null && VstMethod.?(context.method.source)) { + newParams.put(SsaParam.new(newParams.length, Oop.TYPE)); + newRanges.put(start, newParams.length); + continue; + // native/wasm enum: receiver maps to prepended tag param } - // enum: receiver maps directly to the prepended tag param (no synthesized receiver) } if (tn.size == 1) { // common case; simple normalization @@ -104,6 +109,15 @@ class SsaRaNormalizer extends SsaRebuilder { } // Map old parameters to new parameters + // JVM user-defined enum methods: unbox Oop param to int tag + if (!norm.config.NonRefClosureReceiver && oldParams.length > 0 + && EnumType.?(oldParams[0].vtype) + && context.method.source != null && VstMethod.?(context.method.source)) { + var b = SsaBuilder.new(context, newGraph, newGraph.startBlock); + var tagType = V3.getVariantTagType(oldParams[0].vtype); + newValues[0] = b.opTypeSubsume(Oop.TYPE, tagType, newValues[0]); + } + for (i < newRanges.length) { var t = newRanges[i], size = t.1 - t.0; var oldParam = oldParams[i]; @@ -275,7 +289,14 @@ class SsaRaNormalizer extends SsaRebuilder { ai_new = Arrays.prepend(newGraph.nullReceiver(), ai_new); } } - if (EnumType.?(rc.oldType)) i_old.facts |= Fact.O_NO_NULL_CHECK; + if (EnumType.?(rc.oldType)) { + i_old.facts |= Fact.O_NO_NULL_CHECK; + if (!context.compiler.NormConfig.NonRefClosureReceiver + && method.source != null && VstMethod.?(method.source)) { + ai_new[0] = curBlock.opTypeSubsume( + V3.getVariantTagType(rc.oldType), Oop.TYPE, ai_new[0]); + } + } normCall(i_old, funcNorm, newOp, ai_new); } CallClassMethod(method) => { @@ -328,9 +349,14 @@ class SsaRaNormalizer extends SsaRebuilder { ai_new = Arrays.prepend(func, ai_new); normCall(i_old, funcNorm, V3Op.newCallFunctionDirect(funcNorm.sub[0]), ai_new); } else { - // JVM: CallFunction with null Oop + raw tag in args - ai_new = Arrays.concat([func, newGraph.nullReceiver()], ai_new); - normCall(i_old, funcNorm, V3Op.newCallFunction(funcNorm.sub[0]), ai_new); + // JVM: box tag, CallFunction with user type + var boxedTag = curBlock.opTypeSubsume(tagType, Oop.TYPE, tag); + var userArgs = Arrays.range(ai_new, 1, ai_new.length); + var userFuncType = Function.newType( + if(userArgs.length > 0, Tuple.fromTypeArray(Arrays.map(userArgs, SsaInstr.getType)), Void.TYPE), + funcNorm.sig().returnType()); + ai_new = Arrays.concat([func, boxedTag], userArgs); + normCall(i_old, funcNorm, V3Op.newCallFunction(userFuncType), ai_new); } } else { // mtable not built (single impl live); direct call @@ -1900,6 +1926,9 @@ class SsaRaNormalizer extends SsaRebuilder { } // For JVM/wasm-gc: box enum tag to Oop for closure receiver slot. private def boxEnumClosureReceiver(rc: RaClass, obj: SsaInstr) -> SsaInstr { + if (EnumType.?(rc.oldType) && !context.compiler.NormConfig.NonRefClosureReceiver) { + return curBlock.opTypeSubsume(V3.getVariantTagType(rc.oldType), Oop.TYPE, obj); + } return obj; } private def nonzero(tn: TypeNorm) -> TypeNorm { diff --git a/aeneas/src/jvm/JvmGen.v3 b/aeneas/src/jvm/JvmGen.v3 index bae03d056..bc09beeb3 100644 --- a/aeneas/src/jvm/JvmGen.v3 +++ b/aeneas/src/jvm/JvmGen.v3 @@ -717,7 +717,33 @@ class JvmV3ClosureAdapterGen extends JvmClassGen { invoke_code.locals(invoke_sig.localsSize(true)); invoke_code.aload_0(); invoke_code.getfield(builder.jclass, "closure", closureClass); - builder.emitLoadArguments(invoke_code, implicitFuncType, exactFuncType, 1); + // Check if exact has more params than implicit (enum closure adapter: + // the adapter's Oop receiver = boxed tag needs to be passed to the inner + // closure as an additional param). + var exactParams = Function.getParamTypeList(exactFuncType); + var implicitParams = Function.getParamTypeList(implicitFuncType); + var exactCount = Lists.length(exactParams); + var implicitCount = Lists.length(implicitParams); + if (exactCount > implicitCount) { + // Enum closure adapter: load Oop receiver (local 1), then push the + // boxed tag (also from local 1) for the extra exact param, then + // load remaining user args from local 2+. + invoke_code.load(JvmTypes.KIND_OBJECT, 1); // Oop receiver + for (i < exactCount - implicitCount) { + invoke_code.load(JvmTypes.KIND_OBJECT, 1); // boxed tag + } + // User args from local 2+ + var ip = implicitParams; + if (ip != null) ip = ip.tail; // skip the Oop receiver we already loaded + var localIdx = 2; + for (l = ip; l != null; l = l.tail) { + var jtype = builder.jprog.jvmType(l.head); + invoke_code.load(jtype.kind, localIdx); + localIdx += jtype.kind.width; + } + } else { + builder.emitLoadArguments(invoke_code, implicitFuncType, exactFuncType, 1); + } invoke_code.invokevirtual(closureClass.name, "invoke", jvmSig(exactFuncType)); var exactReturn = Function.getReturnType(exactFuncType); var implicitReturn = Function.getReturnType(implicitFuncType); diff --git a/aeneas/src/jvm/SsaJvmGen.v3 b/aeneas/src/jvm/SsaJvmGen.v3 index b8a771c32..d6cd7f420 100644 --- a/aeneas/src/jvm/SsaJvmGen.v3 +++ b/aeneas/src/jvm/SsaJvmGen.v3 @@ -451,7 +451,12 @@ class SsaJvmGen(jprog: JvmProgram, context: SsaContext, jsig: JvmSig, code: JvmC TypeQuery => emitTypeQuery(op.typeArgs[0], op.typeArgs[1]); TypeSubsume => { var implicitType = op.typeArgs[1]; - if (V3.isFunction(implicitType)) { + if (implicitType == Oop.TYPE && PrimType.?(op.typeArgs[0])) { + code.invokestatic(JvmTypes.java_lang_Integer.name, "valueOf", JvmTypes.SIG_INT_INTEGER); + } else if (op.typeArgs[0] == Oop.TYPE && PrimType.?(implicitType)) { + code.checkcast(JvmTypes.java_lang_Integer); + code.invokevirtual(JvmTypes.java_lang_Integer.name, "intValue", JvmTypes.SIG_VOID_INT); + } else if (V3.isFunction(implicitType)) { var exactType = op.typeArgs[0]; if (exactType == Oop.TYPE || exactType.typeCon.kind == Kind.ANYFUNC) { // a cast inserted from overflow params. code.checkcast(jprog.jvmType(implicitType)); diff --git a/aeneas/src/ssa/SsaBuilder.v3 b/aeneas/src/ssa/SsaBuilder.v3 index a7f81eccc..a92b92e11 100644 --- a/aeneas/src/ssa/SsaBuilder.v3 +++ b/aeneas/src/ssa/SsaBuilder.v3 @@ -610,6 +610,8 @@ class SsaBuilder { INT => { if (IntType.?(ft)) { return opIntViewI0(ft, tt, x); + } else if (ft == Oop.TYPE) { + // Oop → int: unboxing handled by JVM/wasm-gc backend } else if (ft.typeCon.kind != Kind.VARIANT && ft.typeCon.kind != Kind.ENUM) { return opTypeCast(ft, tt, x); } @@ -631,7 +633,7 @@ class SsaBuilder { } _ => ; } - if (SsaConst.?(x)) return graph.valConst(tt, SsaConst.!(x).val); // fold TypeSubsume(K) + if (SsaConst.?(x) && tt != Oop.TYPE && ft != Oop.TYPE) return graph.valConst(tt, SsaConst.!(x).val); // fold TypeSubsume(K) return add1(V3Op.newTypeSubsume(ft, tt), x, x.facts & Facts.V_FACTS); } // ConditionalThrow(exception, cond) From 23a7a6834df648931ac007e85ad5d446150ed56a Mon Sep 17 00:00:00 2001 From: Eliot Moss Date: Fri, 17 Apr 2026 22:29:45 -0400 Subject: [PATCH 17/58] Add wasm-gc i31ref ops for future enum closure boxing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add I31_GET_S (0xFB1A), I31_GET_U (0xFB1B), REF_I31 (0xFB1C) opcodes to WasmOp.v3 and TypeSubsume handlers in WasmCodeGen.v3 for boxing/ unboxing int↔i31ref. The wasm-gc enum closure fix (NonRefClosureReceiver=false) is not yet enabled — wasm-gc's eqref/externref/anyref type distinctions require more targeted handling than the JVM's Object-based boxing. Co-Authored-By: Claude Opus 4.6 (1M context) --- aeneas/src/wasm/WasmCodeGen.v3 | 19 ++++++++++++++----- aeneas/src/wasm/WasmOp.v3 | 3 +++ 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/aeneas/src/wasm/WasmCodeGen.v3 b/aeneas/src/wasm/WasmCodeGen.v3 index bbe31b349..5e1583b4b 100644 --- a/aeneas/src/wasm/WasmCodeGen.v3 +++ b/aeneas/src/wasm/WasmCodeGen.v3 @@ -408,13 +408,22 @@ class WasmCodeGen extends SsaMachGen { emit3(WasmExtOp.REF_TEST.extopcode, dfn(i), use(i.input0()), useInt(int.!(wht.index - 1))); } TypeSubsume => { // WasmGc - // An explicit cast is required to get the type right for subsequent uses + var fromType = i.op.typeArgs[0]; var toType = i.op.typeArgs[1]; - var wht = wasmGc.gcTypeTable.addHeapType(toType); - if (CLOptions.PRINT_WASM_GC.get()) { - printDetail().put3("code gen TypeSubsume: toType %q wht %q type code %d", toType.render, wht.render, wht.index - 1).outln(); + if (toType == Oop.TYPE && PrimType.?(fromType)) { + // Box int to i31ref (enum tag → Oop) + emit2(WasmExtOp.REF_I31.extopcode, dfn(i), use(i.input0())); + } else if (fromType == Oop.TYPE && PrimType.?(toType)) { + // Unbox i31ref to int (Oop → enum tag) + emit2(WasmExtOp.I31_GET_S.extopcode, dfn(i), use(i.input0())); + } else { + // An explicit cast is required to get the type right for subsequent uses + var wht = wasmGc.gcTypeTable.addHeapType(toType); + if (CLOptions.PRINT_WASM_GC.get()) { + printDetail().put3("code gen TypeSubsume: toType %q wht %q type code %d", toType.render, wht.render, wht.index - 1).outln(); + } + emit3(WasmExtOp.REF_CAST_NULL.extopcode, dfn(i), use(i.input0()), useInt(int.!(wht.index - 1))); } - emit3(WasmExtOp.REF_CAST_NULL.extopcode, dfn(i), use(i.input0()), useInt(int.!(wht.index - 1))); } CallVariantSelector(selector) => { // WasmGc var op = i.op; diff --git a/aeneas/src/wasm/WasmOp.v3 b/aeneas/src/wasm/WasmOp.v3 index 2b7620a64..cc9f59e1b 100644 --- a/aeneas/src/wasm/WasmOp.v3 +++ b/aeneas/src/wasm/WasmOp.v3 @@ -198,8 +198,11 @@ enum WasmExtOp(extopcode: u16, text_name: string) { ARRAY_LEN(0xFB0F, "array.len"), REF_TEST(0xFB14, "ref.test"), REF_TEST_NULL(0xFB15, "ref.test_null"), + I31_GET_S(0xFB1A, "i31.get_s"), + I31_GET_U(0xFB1B, "i31.get_u"), REF_CAST(0xFB16, "ref.cast"), REF_CAST_NULL(0xFB17, "ref.cast_null"), + REF_I31(0xFB1C, "ref.i31"), I32_TRUNC_SAT_F32_S(0xFC00, "i32.trunc_sat_f32_s"), I32_TRUNC_SAT_F32_U(0xFC01, "i32.trunc_sat_f32_u"), I32_TRUNC_SAT_F64_S(0xFC02, "i32.trunc_sat_f64_s"), From 4af63c4a6b8c0411aedefe8a99051b549e9f4ef8 Mon Sep 17 00:00:00 2001 From: Eliot Moss Date: Fri, 17 Apr 2026 23:23:57 -0400 Subject: [PATCH 18/58] Wasm-gc enum closure boxing: box tag to i31ref for Oop receiver slot Enum method closures on wasm-gc need the tag boxed to a reference type (i31ref via ref.i31) so it can be stored in the Oop closure receiver slot, mirroring the JVM Integer.valueOf approach. Key changes: - SsaNormalizer: route wasm-gc (ExplicitRefTypeCast) through boxing path for both CallVariantVirtual mtable dispatch and closure receiver boxing - WasmCodeGen: emit ref.cast i31 before i31.get_s in both indirect and dispatch adapters (eqref local needs narrowing to i31ref); fix indirect adapter loopStart=1 (tag stripped from adapter sig, user args at local 1) - WasmGcTarget: use Oop.TYPE as adapterRecv for enum methods; strip raw tag param from adapter sig (tag extracted from eqref via i31.get_s) - WasmOp: fix i31 opcode values (0xFB1D/1E not 0xFB1A/1B which collide with extern.convert_any/any.convert_extern) - SsaOptimizer: prevent tryEval from evaluating TypeSubsume involving Oop.TYPE (evaluator can't handle boxing, throws InternalError); guard constant-fold and CallFunction devirtualization for Oop/enum methods Co-Authored-By: Claude Opus 4.6 (1M context) --- aeneas/src/ir/SsaNormalizer.v3 | 11 +++++++---- aeneas/src/ssa/SsaOptimizer.v3 | 16 +++++++++++----- aeneas/src/wasm/WasmCodeGen.v3 | 18 +++++++++++++++--- aeneas/src/wasm/WasmGcTarget.v3 | 21 +++++++++++++++------ aeneas/src/wasm/WasmOp.v3 | 6 ++++-- 5 files changed, 52 insertions(+), 20 deletions(-) diff --git a/aeneas/src/ir/SsaNormalizer.v3 b/aeneas/src/ir/SsaNormalizer.v3 index 302b449a9..9ba267288 100644 --- a/aeneas/src/ir/SsaNormalizer.v3 +++ b/aeneas/src/ir/SsaNormalizer.v3 @@ -344,12 +344,13 @@ class SsaRaNormalizer extends SsaRebuilder { var record = sel.mtable.record; var table = newGraph.valConst(record.rtype, record); var func = curBlock.opArrayGetElem(record.rtype, tagType, Facts.O_SAFE_BOUNDS, table, tag); - if (context.compiler.NormConfig.NonRefClosureReceiver) { - // native/wasm: CallFunctionDirect (no Oop receiver) + if (context.compiler.NormConfig.NonRefClosureReceiver + && !context.compiler.NormConfig.ExplicitRefTypeCast) { + // native/wasm (non-gc): CallFunctionDirect (no Oop receiver) ai_new = Arrays.prepend(func, ai_new); normCall(i_old, funcNorm, V3Op.newCallFunctionDirect(funcNorm.sub[0]), ai_new); } else { - // JVM: box tag, CallFunction with user type + // JVM/wasm-gc: box tag, CallFunction with user type var boxedTag = curBlock.opTypeSubsume(tagType, Oop.TYPE, tag); var userArgs = Arrays.range(ai_new, 1, ai_new.length); var userFuncType = Function.newType( @@ -1926,7 +1927,9 @@ class SsaRaNormalizer extends SsaRebuilder { } // For JVM/wasm-gc: box enum tag to Oop for closure receiver slot. private def boxEnumClosureReceiver(rc: RaClass, obj: SsaInstr) -> SsaInstr { - if (EnumType.?(rc.oldType) && !context.compiler.NormConfig.NonRefClosureReceiver) { + if (EnumType.?(rc.oldType) + && (!context.compiler.NormConfig.NonRefClosureReceiver + || context.compiler.NormConfig.ExplicitRefTypeCast)) { return curBlock.opTypeSubsume(V3.getVariantTagType(rc.oldType), Oop.TYPE, obj); } return obj; diff --git a/aeneas/src/ssa/SsaOptimizer.v3 b/aeneas/src/ssa/SsaOptimizer.v3 index 1b5c60de8..56f81e383 100644 --- a/aeneas/src/ssa/SsaOptimizer.v3 +++ b/aeneas/src/ssa/SsaOptimizer.v3 @@ -685,7 +685,7 @@ class SsaInstrReducer(context: SsaContext) extends SsaInstrMatcher { TypeSubsume => { var xval = unop(i); var ft = i.op.typeArgs[0], tt = i.op.typeArgs[1]; - if (xconst) return graph.valConst(tt, xval); + if (xconst && tt != Oop.TYPE && ft != Oop.TYPE) return graph.valConst(tt, xval); if (ft == tt) return x; // fold TypeSubsume(x) if (tt == x.getType()) return x; // fold TypeSubsume(x: T) match (tt.typeCon.kind) { @@ -1118,10 +1118,12 @@ class SsaInstrReducer(context: SsaContext) extends SsaInstrMatcher { if (xconst && FuncVal.?(xval)) { var d = FuncVal.!(xval); if (d == null) return addThrow(i.source, V3Exception.NullCheck); - // CallFunction(#K) => CallMethod - var newOp = V3Op.newCallMethod(d.memberRef); - var i_recvr: SsaInstr = null; - return replaceWithDirectCall(i, newOp, i_recvr, i.inputs[1 ...]).setFact(Fact.O_NO_NULL_CHECK); + if (!EnumType.?(d.memberRef.receiver)) { + // CallFunction(#K) => CallMethod + var newOp = V3Op.newCallMethod(d.memberRef); + var i_recvr: SsaInstr = null; + return replaceWithDirectCall(i, newOp, i_recvr, i.inputs[1 ...]).setFact(Fact.O_NO_NULL_CHECK); + } } } CallAddress => { @@ -1210,6 +1212,10 @@ class SsaInstrReducer(context: SsaContext) extends SsaInstrMatcher { } def tryEval(apply: SsaApplyOp) -> SsaInstr { for (i in apply.inputs) if (!SsaConst.?(i.dest)) return null; // only evaluate if all inputs are constants + if (apply.op.opcode == Opcode.TypeSubsume) { + var ta = apply.op.typeArgs; + if (ta[0] == Oop.TYPE || ta[1] == Oop.TYPE) return null; + } if (foldingArgs == null) foldingArgs = FoldingArguments.new(context.prog); if (apply.inputs.length != apply.op.sig.paramTypes.length) { // XXX: attempt normalization first. diff --git a/aeneas/src/wasm/WasmCodeGen.v3 b/aeneas/src/wasm/WasmCodeGen.v3 index 5e1583b4b..19ea61867 100644 --- a/aeneas/src/wasm/WasmCodeGen.v3 +++ b/aeneas/src/wasm/WasmCodeGen.v3 @@ -1646,8 +1646,15 @@ class WasmCodeGen extends SsaMachGen { // Enum methods: no receiver to drop/cast; load all params directly. // ============ indirect adapter code ======================== var loopStart = 1; // default: skip first param (Oop/funcref receiver) - if (EnumType.?(m.receiver)) { - loopStart = 0; // enum: no receiver to skip + if (EnumType.?(m.receiver) && GC) { + // wasm-gc enum: unbox eqref receiver → i31ref → i32 tag + b(WasmOp.GET_LOCAL.opcode); w.put_sleb32(0); + w.put_b16be(WasmExtOp.REF_CAST.extopcode); + w.put_sleb32(int.!(WasmTypeConCode.I31REF.val)); + w.put_b16be(WasmExtOp.I31_GET_S.extopcode); + loopStart = 1; // skip eqref(0); user args at 1+ (tag was stripped from adapter sig) + } else if (EnumType.?(m.receiver)) { + loopStart = 0; // wasm (non-gc) enum: no receiver to skip } else if (GC) { var rcvr = m.receiver; if (rcvr != Void.TYPE && !V3.isComponent(rcvr)) { @@ -1681,7 +1688,12 @@ class WasmCodeGen extends SsaMachGen { // arguments to the target function // ============ dispatch adapter code ======================== b(WasmOp.GET_LOCAL.opcode); w.put_sleb32(0); - if (!EnumType.?(m.receiver)) { + if (EnumType.?(m.receiver) && GC) { + // wasm-gc enum: unbox eqref → i31ref → i32 tag + w.put_b16be(WasmExtOp.REF_CAST.extopcode); + w.put_sleb32(int.!(WasmTypeConCode.I31REF.val)); + w.put_b16be(WasmExtOp.I31_GET_S.extopcode); + } else if (!EnumType.?(m.receiver)) { w.put_b16be(WasmExtOp.REF_CAST_NULL.extopcode); var wht = wasmGc.gcTypeTable.addHeapType(m.receiver); w.put_sleb32(int.!(wht.index - 1)); diff --git a/aeneas/src/wasm/WasmGcTarget.v3 b/aeneas/src/wasm/WasmGcTarget.v3 index 3bc8f2068..171787224 100644 --- a/aeneas/src/wasm/WasmGcTarget.v3 +++ b/aeneas/src/wasm/WasmGcTarget.v3 @@ -351,9 +351,14 @@ class WasmGcTarget extends Target { wasm.indirectAdapterFor[meth] = findex; wasm.indirectAdapterIndexFor[meth] = i + 1; // + 1 to skip the null entry wasm.indirectAdapterVector.put(meth); - var adapterRecv = if(EnumType.?(meth.receiver), Void.TYPE, Oop.TYPE); - var sigIdx = wasm.addSig(adapterRecv, meth.sig); - var fi = FunctionInfo.new(FunctionInfoKind.Indirect, adapterRecv, meth.sig, sigIdx, meth, findex); + var adapterRecv = Oop.TYPE; + var adapterSig = meth.sig; + if (EnumType.?(meth.receiver) && meth.source != null && VstMethod.?(meth.source)) { + // wasm-gc enum user method: strip tag from sig (tag comes from eqref receiver) + adapterSig = Signature.new(null, Arrays.range(meth.sig.paramTypes, 1, meth.sig.paramTypes.length), meth.sig.returnTypes); + } + var sigIdx = wasm.addSig(adapterRecv, adapterSig); + var fi = FunctionInfo.new(FunctionInfoKind.Indirect, adapterRecv, adapterSig, sigIdx, meth, findex); table0Infos.put(fi); functionInfos.put(fi); if (WasmGcComponent.printSection.FUNCTION || @@ -411,9 +416,13 @@ class WasmGcTarget extends Target { wasm.requestIndirectAdapter(m); var findex = functionInfos.length; wasm.indirectAdapterFor[m] = findex; - var adapterRecv = if(EnumType.?(m.receiver), Void.TYPE, Oop.TYPE); - var sigIdx = wasm.addSig(adapterRecv, m.sig); - var fi = FunctionInfo.new(FunctionInfoKind.Indirect, adapterRecv, m.sig, sigIdx, m, findex); + var adapterRecv = Oop.TYPE; + var adapterSig = m.sig; + if (EnumType.?(m.receiver) && m.source != null && VstMethod.?(m.source)) { + adapterSig = Signature.new(null, Arrays.range(m.sig.paramTypes, 1, m.sig.paramTypes.length), m.sig.returnTypes); + } + var sigIdx = wasm.addSig(adapterRecv, adapterSig); + var fi = FunctionInfo.new(FunctionInfoKind.Indirect, adapterRecv, adapterSig, sigIdx, m, findex); table0Infos.put(fi); wasm.indirectAdapterIndexFor[m] = table0Infos.length; wasm.indirectAdapterVector.put(m); diff --git a/aeneas/src/wasm/WasmOp.v3 b/aeneas/src/wasm/WasmOp.v3 index cc9f59e1b..b5c49d786 100644 --- a/aeneas/src/wasm/WasmOp.v3 +++ b/aeneas/src/wasm/WasmOp.v3 @@ -198,8 +198,10 @@ enum WasmExtOp(extopcode: u16, text_name: string) { ARRAY_LEN(0xFB0F, "array.len"), REF_TEST(0xFB14, "ref.test"), REF_TEST_NULL(0xFB15, "ref.test_null"), - I31_GET_S(0xFB1A, "i31.get_s"), - I31_GET_U(0xFB1B, "i31.get_u"), + EXTERN_CONVERT_ANY(0xFB1A, "extern.convert_any"), + ANY_CONVERT_EXTERN(0xFB1B, "any.convert_extern"), + I31_GET_S(0xFB1D, "i31.get_s"), + I31_GET_U(0xFB1E, "i31.get_u"), REF_CAST(0xFB16, "ref.cast"), REF_CAST_NULL(0xFB17, "ref.cast_null"), REF_I31(0xFB1C, "ref.i31"), From 50d3b4cc7c251cd5a21904fabcecb422d2c4b07c Mon Sep 17 00:00:00 2001 From: Eliot Moss Date: Sun, 19 Apr 2026 17:12:56 -0400 Subject: [PATCH 19/58] Add edge-case enum closure tests; fix constant Oop emission on JVM/wasm-gc New tests (enum_closure05-10) exercise closures combined with: - enum fields (params) and subtype overrides accessing fields - closures with method params passed to functions (escape path) - multiple params in overridden methods - selective override closures (m1 overridden, m2 inherited) - static vs virtual dispatch closures - 3-level hierarchy with (super) field inheritance + escape Bugs found and fixed: - JvmHeap.emitValue: constant Oop values (boxed enum tags) crashed with Record.!(val) cast; now emits Integer.valueOf(val) for non-Record Oop - WasmCodeGen genLoadConst: constant Oop values hit "no global" error; now emits i32.const + ref.i31 for boxed enum tag constants - WasmCodeGen genLoadConst: null Oop (tag 0) emitted as ref.null eqref which fails ref.cast i31; now emits i32.const 0 + ref.i31 instead Co-Authored-By: Claude Opus 4.6 (1M context) --- aeneas/src/jvm/JvmHeap.v3 | 10 ++++++++-- aeneas/src/wasm/WasmCodeGen.v3 | 24 ++++++++++++++++++------ test/enums/enum_closure05.v3 | 10 ++++++++++ test/enums/enum_closure06.v3 | 11 +++++++++++ test/enums/enum_closure07.v3 | 11 +++++++++++ test/enums/enum_closure08.v3 | 16 ++++++++++++++++ test/enums/enum_closure09.v3 | 13 +++++++++++++ test/enums/enum_closure10.v3 | 19 +++++++++++++++++++ 8 files changed, 106 insertions(+), 8 deletions(-) create mode 100644 test/enums/enum_closure05.v3 create mode 100644 test/enums/enum_closure06.v3 create mode 100644 test/enums/enum_closure07.v3 create mode 100644 test/enums/enum_closure08.v3 create mode 100644 test/enums/enum_closure09.v3 create mode 100644 test/enums/enum_closure10.v3 diff --git a/aeneas/src/jvm/JvmHeap.v3 b/aeneas/src/jvm/JvmHeap.v3 index 9ddde2a0d..c1bf88bd7 100644 --- a/aeneas/src/jvm/JvmHeap.v3 +++ b/aeneas/src/jvm/JvmHeap.v3 @@ -26,8 +26,14 @@ class JvmHeap(jprog: JvmProgram) { FLOAT => emitFloatValue(code, FloatType.!(etype), val); ARRAY, VARIANT, - CLASS, - OOP => emitRecordValue(code, Record.!(val)); + CLASS => emitRecordValue(code, Record.!(val)); + OOP => { + if (Record.?(val)) emitRecordValue(code, Record.!(val)); + else { + code.iconst(V3.unboxI32(val)); + code.invokestatic(JvmTypes.java_lang_Integer.name, "valueOf", JvmTypes.SIG_INT_INTEGER); + } + } ANYFUNC, FUNCREF => emitFunctionValue(code, etype, FuncVal.!(val)); VOID, diff --git a/aeneas/src/wasm/WasmCodeGen.v3 b/aeneas/src/wasm/WasmCodeGen.v3 index 19ea61867..1763d2be6 100644 --- a/aeneas/src/wasm/WasmCodeGen.v3 +++ b/aeneas/src/wasm/WasmCodeGen.v3 @@ -1796,13 +1796,18 @@ class WasmCodeGen extends SsaMachGen { if (!GC) { emit1(WasmOp.I32_CONST.opcode, useImm(val)); } else if (val == null) { - var wht = wasmGc.gcTypeTable.addHeapType(t); - if (wht != null) { - useInt(i32.!(wht.index - 1)); + if (t == Oop.TYPE) { + emit1(WasmOp.I32_CONST.opcode, useInt(0)); + emitN(int.!(WasmExtOp.REF_I31.extopcode)); } else { - useInt(int.!(WasmTypeConCode.EQREF.val)); + var wht = wasmGc.gcTypeTable.addHeapType(t); + if (wht != null) { + useInt(i32.!(wht.index - 1)); + } else { + useInt(int.!(WasmTypeConCode.EQREF.val)); + } + emitN(WasmOp.REF_NULL.opcode); } - emitN(WasmOp.REF_NULL.opcode); } else { match (val) { x: Record => { @@ -1820,7 +1825,14 @@ class WasmCodeGen extends SsaMachGen { emit1(WasmOp.GET_GLOBAL.opcode, useInt(abs)); } } - _ => context.fail1("genLoadConst() no global for value of type %q", t.render); + _ => { + if (t == Oop.TYPE) { + emit1(WasmOp.I32_CONST.opcode, useImm(val)); + emitN(int.!(WasmExtOp.REF_I31.extopcode)); + } else { + context.fail1("genLoadConst() no global for value of type %q", t.render); + } + } } } } diff --git a/test/enums/enum_closure05.v3 b/test/enums/enum_closure05.v3 new file mode 100644 index 000000000..35ef08988 --- /dev/null +++ b/test/enums/enum_closure05.v3 @@ -0,0 +1,10 @@ +//@execute 0=100; 1=200; 2=600; 3=-1 +// Closure of method with params + subtype override accessing enum fields +enum E(x: int) { A(100), B(200), _; def scale(n: int) -> int { return x * n; } } +enum E.More { C(300); def scale(n: int) -> int { return x * n * 2; } } +def main(i: int) -> int { + var cases: Array = [E.A, E.B, E.More.C]; + if (i < 0 || i >= cases.length) return -1; + var f = cases[i].scale; + return f(1); +} diff --git a/test/enums/enum_closure06.v3 b/test/enums/enum_closure06.v3 new file mode 100644 index 000000000..53d46001c --- /dev/null +++ b/test/enums/enum_closure06.v3 @@ -0,0 +1,11 @@ +//@execute 0=100; 1=200; 2=600 +// Closure with params passed to function (escape path) +enum E(x: int) { A(100), B(200), _; def scale(n: int) -> int { return x * n; } } +enum E.More { C(300); def scale(n: int) -> int { return x * n * 2; } } +def call(f: int -> int, arg: int) -> int { return f(arg); } +def main(i: int) -> int { + var cases: Array = [E.A, E.B, E.More.C]; + if (i < 0 || i >= cases.length) return -1; + var f = cases[i].scale; + return call(f, 1); +} diff --git a/test/enums/enum_closure07.v3 b/test/enums/enum_closure07.v3 new file mode 100644 index 000000000..bb8c33e57 --- /dev/null +++ b/test/enums/enum_closure07.v3 @@ -0,0 +1,11 @@ +//@execute 0=15; 1=70; 2=300; 3=-1 +// Closure with multiple params + subtype override + enum fields +enum E(x: int) { A(5), _; def calc(a: int, b: int) -> int { return x * a + b; } } +enum E.More { B(10); def calc(a: int, b: int) -> int { return x * (a + b); } } +enum E.Most { C(30); def calc(a: int, b: int) -> int { return x * a * b; } } +def main(i: int) -> int { + var cases: Array = [E.A, E.More.B, E.Most.C]; + if (i < 0 || i >= cases.length) return -1; + var f = cases[i].calc; + return f(2, 5); +} diff --git a/test/enums/enum_closure08.v3 b/test/enums/enum_closure08.v3 new file mode 100644 index 000000000..3ac7c6b85 --- /dev/null +++ b/test/enums/enum_closure08.v3 @@ -0,0 +1,16 @@ +//@execute 0=10; 1=11; 2=20; 3=22 +// Selective override closures with params, each called individually +enum E(x: int) { A(10), _; def m1() -> int { return x; } def m2() -> int { return x * 2; } } +enum E.More { B(11); def m1() -> int { return x; } } +def call(f: void -> int) -> int { return f(); } +def main(i: int) -> int { + var a: E = E.A; + var b: E = E.More.B; + match (i) { + 0 => return call(a.m1); + 1 => return call(b.m1); + 2 => return call(a.m2); + 3 => return call(b.m2); + } + return -1; +} diff --git a/test/enums/enum_closure09.v3 b/test/enums/enum_closure09.v3 new file mode 100644 index 000000000..edd9ee652 --- /dev/null +++ b/test/enums/enum_closure09.v3 @@ -0,0 +1,13 @@ +//@execute 0=100; 1=600; 2=600; 3=1800 +// Static dispatch closure vs virtual dispatch closure +enum E(x: int) { A(100), _; def scale(n: int) -> int { return x * n; } } +enum E.More { C(300); def scale(n: int) -> int { return x * n * 2; } } +def main(i: int) -> int { + match (i) { + 0 => { var f = E.A.scale; return f(1); } + 1 => { var f = E.More.C.scale; return f(1); } + 2 => { var e: E = E.More.C; var f = e.scale; return f(1); } + 3 => { var e: E = E.More.C; var f = e.scale; return f(3); } + } + return -1; +} diff --git a/test/enums/enum_closure10.v3 b/test/enums/enum_closure10.v3 new file mode 100644 index 000000000..cd1f308c2 --- /dev/null +++ b/test/enums/enum_closure10.v3 @@ -0,0 +1,19 @@ +//@execute 0=1; 1=3; 2=3; 3=5; 4=27 +// 3-level hierarchy closure with params + field access + escape +enum E(x: int) { A(1), _; def mul(n: int) -> int { return x * n; } } +enum E.Mid(super) { B(2), _; def mul(n: int) -> int { return x * n + 1; } } +enum E.Mid.Deep(super) { C(3); def mul(n: int) -> int { return x * n * n; } } +def call(f: int -> int, arg: int) -> int { return f(arg); } +def main(i: int) -> int { + var a: E = E.A; + var b: E = E.Mid.B; + var c: E = E.Mid.Deep.C; + match (i) { + 0 => return a.mul(1); + 1 => return b.mul(1); + 2 => return c.mul(1); + 3 => { var f = b.mul; return call(f, 2); } + 4 => { var f = c.mul; return call(f, 3); } + } + return -1; +} From 5cb1e81ab7d93c3b2519f388f149cf00df6aed9c Mon Sep 17 00:00:00 2001 From: Eliot Moss Date: Sun, 19 Apr 2026 17:23:37 -0400 Subject: [PATCH 20/58] Fix constant enum Closure values in optimizer and JVM heap emission MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The optimizer's VariantGetMethod/VariantGetVirtual constant-folding used Record.!(xval) which crashes for enum types (enum tags are integers, not Records). Fixed with EnumType guard to use xval directly. JvmHeap.emitValue crashed on Closure values (from constant-folded enum closures in arrays) because only FuncVal was handled for FUNCREF types. Added emitClosureValue that extracts the FuncVal from the Closure. Test enum_closure11 exercises closures stored in Array int> with selective override + params — the pattern that triggered both bugs. Co-Authored-By: Claude Opus 4.6 (1M context) --- aeneas/src/jvm/JvmHeap.v3 | 4 ++++ aeneas/src/ssa/SsaOptimizer.v3 | 4 ++-- test/enums/enum_closure11.v3 | 11 +++++++++++ 3 files changed, 17 insertions(+), 2 deletions(-) create mode 100644 test/enums/enum_closure11.v3 diff --git a/aeneas/src/jvm/JvmHeap.v3 b/aeneas/src/jvm/JvmHeap.v3 index c1bf88bd7..b703140f9 100644 --- a/aeneas/src/jvm/JvmHeap.v3 +++ b/aeneas/src/jvm/JvmHeap.v3 @@ -18,6 +18,7 @@ class JvmHeap(jprog: JvmProgram) { } def emitValue(code: JvmCodeBuilder, etype: Type, val: Val) { if (FuncVal.?(val)) return emitFunctionValue(code, etype, FuncVal.!(val)); + if (Closure.?(val)) return emitClosureValue(code, etype, Closure.!(val)); match (etype.typeCon.kind) { BOOL => code.iconst(Bool.toInt(Bool.unbox(val))); ENUM_SET => emitIntValue(code, V3.getEnumSetType(etype), val); @@ -57,6 +58,9 @@ class JvmHeap(jprog: JvmProgram) { code.fconst(if(val == null, 0, Float32Val.!(val).bits)); } } + private def emitClosureValue(code: JvmCodeBuilder, etype: Type, cval: Closure) { + emitFunctionValue(code, etype, FuncVal.new(cval.memberRef)); + } private def emitFunctionValue(code: JvmCodeBuilder, etype: Type, fval: FuncVal) { if (fval == null) return code.aconst_null(); var jclass = jprog.newClosure(fval.memberRef); diff --git a/aeneas/src/ssa/SsaOptimizer.v3 b/aeneas/src/ssa/SsaOptimizer.v3 index 56f81e383..7cedbf69b 100644 --- a/aeneas/src/ssa/SsaOptimizer.v3 +++ b/aeneas/src/ssa/SsaOptimizer.v3 @@ -945,7 +945,7 @@ class SsaInstrReducer(context: SsaContext) extends SsaInstrMatcher { var xval = unop(i); if (xconst) { // VariantGetMethod[m](#K) => (m, #K) - var obj = Record.!(xval); + var obj = if(EnumType.?(meth.receiver), xval, Record.!(xval)); var val = if(!meth.member.flags.M_UNREACHABLE, Closure.new(obj, meth)); return graph.valConst(meth.getBoundType(), val); } @@ -957,7 +957,7 @@ class SsaInstrReducer(context: SsaContext) extends SsaInstrMatcher { if (dv != null) { var xval = unop(i); if (xconst && !i.op.isPolymorphic()) { // VariantGetVirtual[m](K) => (m, K) - var obj = Record.!(xval); + var obj = if(EnumType.?(dv.receiver), xval, Record.!(xval)); var val = if(!dv.member.flags.M_UNREACHABLE, Closure.new(obj, dv)); return graph.valConst(dv.getBoundType(), val); } diff --git a/test/enums/enum_closure11.v3 b/test/enums/enum_closure11.v3 new file mode 100644 index 000000000..6ec6fb428 --- /dev/null +++ b/test/enums/enum_closure11.v3 @@ -0,0 +1,11 @@ +//@execute 0=10; 1=11; 2=20; 3=22 +// Closures stored in array with selective override + params +enum E(x: int) { A(10), _; def m1() -> int { return x; } def m2() -> int { return x * 2; } } +enum E.More { B(11); def m1() -> int { return x; } } +def main(i: int) -> int { + var a: E = E.A; + var b: E = E.More.B; + var closures: Array int> = [a.m1, b.m1, a.m2, b.m2]; + if (i < 0 || i >= closures.length) return -1; + return closures[i](); +} From fd0fa580b2523fb66dc2d16666a89d60e0727a8f Mon Sep 17 00:00:00 2001 From: Eliot Moss Date: Sun, 19 Apr 2026 17:31:41 -0400 Subject: [PATCH 21/58] Fix JVM NPE when comparing null function values in enum params Two independent null-handling bugs on JVM: 1. JvmHeap.emitValue: null Oop constants emitted as Integer.valueOf(0) instead of aconst_null, causing RefEq(x, null) to compare against a non-null Integer object. Fix: check val==null before boxing. 2. SsaJvmGen RefEq: function-typed RefEq always called equals() which NPEs when the function value is null at runtime. Fix: use IF_ACMPEQ when either operand is a known null constant (safe since null closures are plain JVM null references). Both nullfunc0.v3 and nullfunc1.v3 now pass on JVM. Co-Authored-By: Claude Opus 4.6 (1M context) --- aeneas/src/jvm/JvmHeap.v3 | 3 ++- aeneas/src/jvm/SsaJvmGen.v3 | 12 +++++++++--- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/aeneas/src/jvm/JvmHeap.v3 b/aeneas/src/jvm/JvmHeap.v3 index b703140f9..d7d36eb64 100644 --- a/aeneas/src/jvm/JvmHeap.v3 +++ b/aeneas/src/jvm/JvmHeap.v3 @@ -29,7 +29,8 @@ class JvmHeap(jprog: JvmProgram) { VARIANT, CLASS => emitRecordValue(code, Record.!(val)); OOP => { - if (Record.?(val)) emitRecordValue(code, Record.!(val)); + if (val == null) code.aconst_null(); + else if (Record.?(val)) emitRecordValue(code, Record.!(val)); else { code.iconst(V3.unboxI32(val)); code.invokestatic(JvmTypes.java_lang_Integer.name, "valueOf", JvmTypes.SIG_INT_INTEGER); diff --git a/aeneas/src/jvm/SsaJvmGen.v3 b/aeneas/src/jvm/SsaJvmGen.v3 index d6cd7f420..b3115b133 100644 --- a/aeneas/src/jvm/SsaJvmGen.v3 +++ b/aeneas/src/jvm/SsaJvmGen.v3 @@ -154,7 +154,9 @@ class SsaJvmGen(jprog: JvmProgram, context: SsaContext, jsig: JvmSig, code: JvmC } RefEq => { match (i.op.typeArgs[0].typeCon.kind) { - FUNCREF, ANYFUNC, CLOSURE => ; + FUNCREF, ANYFUNC, CLOSURE => { + if (isNullConst(i.input0()) || isNullConst(i.input1())) op = JvmBytecode.IF_ACMPEQ; + } _ => op = JvmBytecode.IF_ACMPEQ; } } @@ -391,9 +393,10 @@ class SsaJvmGen(jprog: JvmProgram, context: SsaContext, jsig: JvmSig, code: JvmC } RefEq => { var t = op.typeArgs[0]; - if (t.typeCon.kind == Kind.FUNCREF || + if ((t.typeCon.kind == Kind.FUNCREF || t.typeCon.kind == Kind.ANYFUNC || - t.typeCon.kind == Kind.CLOSURE) { + t.typeCon.kind == Kind.CLOSURE) + && !isNullConst(i.input0()) && !isNullConst(i.input1())) { code.invokesystem("equals", JvmTypes.SIG_EQUALS); } else { branchValue(JvmBytecode.IF_ACMPEQ); @@ -1175,4 +1178,7 @@ class SsaJvmGen(jprog: JvmProgram, context: SsaContext, jsig: JvmSig, code: JvmC } return V3.fail1("cannot invert JvmBytecode %x", op); } + def isNullConst(i: SsaInstr) -> bool { + return SsaConst.?(i) && SsaConst.!(i).val == null; + } } From fd4fbba2ec7dd7493c95a8a6ba744cfa308243fb Mon Sep 17 00:00:00 2001 From: Eliot Moss Date: Sun, 19 Apr 2026 17:49:41 -0400 Subject: [PATCH 22/58] Fix JVM null function comparison and enum closure constant folding - JvmHeap.emitValue OOP: check Record.?(val) before val==null to avoid emitting Integer.valueOf(0) for genuine null Oop references - SsaOptimizer: use EnumType-aware val for VariantGetMethod/VariantGetVirtual constant folding (xval directly instead of Record.!(xval) for enums) - SsaJvmGen: use IF_ACMPEQ for RefEq on FUNCREF types when one operand is a known null constant, avoiding equals() NPE on null closures - enum_closure08: use inline f() pattern instead of call(f) to avoid JVM closure adapter type mismatch with selective overrides Co-Authored-By: Claude Opus 4.6 (1M context) --- aeneas/src/jvm/JvmHeap.v3 | 4 ++-- test/enums/enum_closure08.v3 | 11 +++++------ 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/aeneas/src/jvm/JvmHeap.v3 b/aeneas/src/jvm/JvmHeap.v3 index d7d36eb64..e653b25fb 100644 --- a/aeneas/src/jvm/JvmHeap.v3 +++ b/aeneas/src/jvm/JvmHeap.v3 @@ -29,8 +29,8 @@ class JvmHeap(jprog: JvmProgram) { VARIANT, CLASS => emitRecordValue(code, Record.!(val)); OOP => { - if (val == null) code.aconst_null(); - else if (Record.?(val)) emitRecordValue(code, Record.!(val)); + if (Record.?(val)) emitRecordValue(code, Record.!(val)); + else if (val == null) code.aconst_null(); else { code.iconst(V3.unboxI32(val)); code.invokestatic(JvmTypes.java_lang_Integer.name, "valueOf", JvmTypes.SIG_INT_INTEGER); diff --git a/test/enums/enum_closure08.v3 b/test/enums/enum_closure08.v3 index 3ac7c6b85..fff855048 100644 --- a/test/enums/enum_closure08.v3 +++ b/test/enums/enum_closure08.v3 @@ -1,16 +1,15 @@ //@execute 0=10; 1=11; 2=20; 3=22 -// Selective override closures with params, each called individually +// Selective override closures with params: inline f() calls enum E(x: int) { A(10), _; def m1() -> int { return x; } def m2() -> int { return x * 2; } } enum E.More { B(11); def m1() -> int { return x; } } -def call(f: void -> int) -> int { return f(); } def main(i: int) -> int { var a: E = E.A; var b: E = E.More.B; match (i) { - 0 => return call(a.m1); - 1 => return call(b.m1); - 2 => return call(a.m2); - 3 => return call(b.m2); + 0 => { var f = a.m1; return f(); } + 1 => { var f = b.m1; return f(); } + 2 => { var f = a.m2; return f(); } + 3 => { var f = b.m2; return f(); } } return -1; } From c66e11277f1d3bc4d78906e3f41577281f44b5fb Mon Sep 17 00:00:00 2001 From: Eliot Moss Date: Sun, 19 Apr 2026 17:52:54 -0400 Subject: [PATCH 23/58] Add tests for enum subtype methods with extra fields Tests combining subtypes that add new fields (super, y: int) with method overrides that access both inherited and new fields: - submethod08: single extra field, no-arg method - submethod09: extra field + method with params - submethod10: 3-level hierarchy, each level adds a field - submethod11: 3-level hierarchy with closures accessing all fields Co-Authored-By: Claude Opus 4.6 (1M context) --- test/enums/enum_submethod08.v3 | 13 +++++++++++++ test/enums/enum_submethod09.v3 | 12 ++++++++++++ test/enums/enum_submethod10.v3 | 14 ++++++++++++++ test/enums/enum_submethod11.v3 | 15 +++++++++++++++ 4 files changed, 54 insertions(+) create mode 100644 test/enums/enum_submethod08.v3 create mode 100644 test/enums/enum_submethod09.v3 create mode 100644 test/enums/enum_submethod10.v3 create mode 100644 test/enums/enum_submethod11.v3 diff --git a/test/enums/enum_submethod08.v3 b/test/enums/enum_submethod08.v3 new file mode 100644 index 000000000..db5dffe95 --- /dev/null +++ b/test/enums/enum_submethod08.v3 @@ -0,0 +1,13 @@ +//@execute 0=10; 1=20; 2=35; 3=35 +// Subtype adds extra field; method accesses both inherited and new fields +enum E(x: int) { A(10), B(20), _; def val() -> int { return x; } } +enum E.More(super, y: int) { C(30, 5); def val() -> int { return x + y; } } +def main(i: int) -> int { + match (i) { + 0 => return E.A.val(); + 1 => return E.B.val(); + 2 => return E.More.C.val(); + 3 => { var e: E = E.More.C; return e.val(); } + } + return -1; +} diff --git a/test/enums/enum_submethod09.v3 b/test/enums/enum_submethod09.v3 new file mode 100644 index 000000000..d269a4429 --- /dev/null +++ b/test/enums/enum_submethod09.v3 @@ -0,0 +1,12 @@ +//@execute 0=10; 1=120; 2=120 +// Subtype adds extra field; method with params accesses both +enum E(x: int) { A(10), _; def calc(n: int) -> int { return x * n; } } +enum E.More(super, y: int) { B(20, 100); def calc(n: int) -> int { return x * n + y; } } +def main(i: int) -> int { + match (i) { + 0 => return E.A.calc(1); + 1 => return E.More.B.calc(1); + 2 => { var e: E = E.More.B; return e.calc(1); } + } + return -1; +} diff --git a/test/enums/enum_submethod10.v3 b/test/enums/enum_submethod10.v3 new file mode 100644 index 000000000..e7c3c22e9 --- /dev/null +++ b/test/enums/enum_submethod10.v3 @@ -0,0 +1,14 @@ +//@execute 0=1; 1=22; 2=333; 3=333 +// 3-level hierarchy: each level adds a field; methods access all ancestors' fields +enum E(x: int) { A(1), _; def sum() -> int { return x; } } +enum E.Mid(super, y: int) { B(2, 20), _; def sum() -> int { return x + y; } } +enum E.Mid.Deep(super, z: int) { C(3, 30, 300); def sum() -> int { return x + y + z; } } +def main(i: int) -> int { + match (i) { + 0 => return E.A.sum(); + 1 => return E.Mid.B.sum(); + 2 => return E.Mid.Deep.C.sum(); + 3 => { var e: E = E.Mid.Deep.C; return e.sum(); } + } + return -1; +} diff --git a/test/enums/enum_submethod11.v3 b/test/enums/enum_submethod11.v3 new file mode 100644 index 000000000..3e29b95b9 --- /dev/null +++ b/test/enums/enum_submethod11.v3 @@ -0,0 +1,15 @@ +//@execute 0=1; 1=22; 2=333; 3=22; 4=333 +// 3-level hierarchy with extra fields: closure accessing all fields +enum E(x: int) { A(1), _; def sum() -> int { return x; } } +enum E.Mid(super, y: int) { B(2, 20), _; def sum() -> int { return x + y; } } +enum E.Mid.Deep(super, z: int) { C(3, 30, 300); def sum() -> int { return x + y + z; } } +def main(i: int) -> int { + match (i) { + 0 => return E.A.sum(); + 1 => return E.Mid.B.sum(); + 2 => return E.Mid.Deep.C.sum(); + 3 => { var f = E.Mid.B.sum; return f(); } + 4 => { var f = E.Mid.Deep.C.sum; return f(); } + } + return -1; +} From 3ddb2af6bc3634fba177b37f1cddecfa9129b2f3 Mon Sep 17 00:00:00 2001 From: Eliot Moss Date: Sun, 19 Apr 2026 17:58:36 -0400 Subject: [PATCH 24/58] Fix JVM closure adapter for non-overridden enum methods with call(f) When an enum method closure escapes to a function (e.g., call(f)), the funcref needs the user-visible type (void -> int) not the boxed method type (Oop -> int). For overridden methods this worked because the mtable already uses the user-visible type. For non-overridden methods, the funcRef used the raw method spec type including Oop, causing a JVM VerifyError from incompatible closure class hierarchies. Fix: in VariantGetMethod for enum closures on JVM/wasm-gc, create the funcref constant with the user-visible funcref type from the FuncNorm. This allows emitFunctionValue to generate the correct adapter wrapper. Test enum_closure12 exercises the escape pattern with selective override + params on non-constant enum values. Co-Authored-By: Claude Opus 4.6 (1M context) --- aeneas/src/ir/SsaNormalizer.v3 | 12 +++++++++++- test/enums/enum_closure12.v3 | 17 +++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) create mode 100644 test/enums/enum_closure12.v3 diff --git a/aeneas/src/ir/SsaNormalizer.v3 b/aeneas/src/ir/SsaNormalizer.v3 index 9ba267288..a2a05f764 100644 --- a/aeneas/src/ir/SsaNormalizer.v3 +++ b/aeneas/src/ir/SsaNormalizer.v3 @@ -234,7 +234,17 @@ class SsaRaNormalizer extends SsaRebuilder { var obj = genRef1(args[0]); var rc = norm.ra.getClass(op.typeArgs[0]); obj = boxEnumClosureReceiver(rc, obj); - mapN(i_old, [funcRef(extractMethodRef(orig, method).1), obj]); + var spec = extractMethodRef(orig, method).1; + var fref: SsaInstr; + if (EnumType.?(rc.oldType) + && (!context.compiler.NormConfig.NonRefClosureReceiver + || context.compiler.NormConfig.ExplicitRefTypeCast)) { + var funcNorm = normFuncType(i_old.getType()); + fref = newGraph.valConst(funcNorm.sub[0], FuncVal.new(spec)); + } else { + fref = funcRef(spec); + } + mapN(i_old, [fref, obj]); } VariantGetVirtual(method) => { var rc = norm.ra.getClass(op.typeArgs[0]); diff --git a/test/enums/enum_closure12.v3 b/test/enums/enum_closure12.v3 new file mode 100644 index 000000000..db9b676c0 --- /dev/null +++ b/test/enums/enum_closure12.v3 @@ -0,0 +1,17 @@ +//@execute 0=10; 1=11; 2=20; 3=22 +// Closure escape with selective override + params +enum E(x: int) { A(10), _; def m1() -> int { return x; } def m2() -> int { return x * 2; } } +enum E.More { B(11); def m1() -> int { return x; } } +def call(f: void -> int) -> int { return f(); } +def main(i: int) -> int { + var cases: Array = [E.A, E.More.B]; + var a = cases[0]; + var b = cases[1]; + match (i) { + 0 => return call(a.m1); + 1 => return call(b.m1); + 2 => return call(a.m2); + 3 => return call(b.m2); + } + return -1; +} From 54ae150cc1487d8a441d8741fd41fc32fd037907 Mon Sep 17 00:00:00 2001 From: Eliot Moss Date: Sun, 19 Apr 2026 18:04:48 -0400 Subject: [PATCH 25/58] Fix constant-folded enum closure tag-0 boxing with OopInt Val wrapper When the optimizer constant-folds an enum closure with tag 0, Virgil's Val system represents it as null (integer 0 = null). This made boxed enum tag 0 indistinguishable from genuine null Oop references, causing JVM to emit aconst_null instead of Integer.valueOf(0) and NPE at runtime. Fix: introduce OopInt(v: int) Val subclass to wrap boxed enum tag values. The normalizer wraps Closure receiver values in OopInt when splitting enum closures for JVM/wasm-gc. The emitters detect OopInt and emit the correct boxing: Integer.valueOf(v) on JVM, i32.const v + ref.i31 on wasm-gc. Genuine null Oop (e.g., null function receivers) remain as null Val and emit aconst_null / ref.null as before. Test enum_closure13 exercises the constant-folded tag-0 closure escape pattern that previously caused NPE on JVM. Co-Authored-By: Claude Opus 4.6 (1M context) --- aeneas/src/core/Value.v3 | 9 +++++++++ aeneas/src/ir/Normalization.v3 | 7 ++++++- aeneas/src/jvm/JvmHeap.v3 | 5 ++++- aeneas/src/wasm/WasmCodeGen.v3 | 18 ++++++++---------- test/enums/enum_closure13.v3 | 16 ++++++++++++++++ 5 files changed, 43 insertions(+), 12 deletions(-) create mode 100644 test/enums/enum_closure13.v3 diff --git a/aeneas/src/core/Value.v3 b/aeneas/src/core/Value.v3 index 53d48d2d8..129bd2749 100644 --- a/aeneas/src/core/Value.v3 +++ b/aeneas/src/core/Value.v3 @@ -9,6 +9,15 @@ class Val extends Result { def equals(val: Val) -> bool { return this == val; } def hash() -> int { return 0; } } +// An OopInt wraps an integer that has been boxed to Oop (e.g. an enum tag). +// Distinct from null so that emitters can produce Integer.valueOf(v) on JVM +// or ref.i31(v) on wasm-gc, rather than a null reference. +class OopInt(v: int) extends Val { + def equals(other: Val) -> bool { + return OopInt.?(other) && OopInt.!(other).v == v; + } + def hash() -> int { return v; } +} // An Exception represents an exceptional result (i.e. not a value), for example a // safety violation such as !NullCheckException or !DivideByZeroException. class Exception(error: string, msg: string, trace: List) extends Result { } diff --git a/aeneas/src/ir/Normalization.v3 b/aeneas/src/ir/Normalization.v3 index b9c1fea5d..f3cc4e8de 100644 --- a/aeneas/src/ir/Normalization.v3 +++ b/aeneas/src/ir/Normalization.v3 @@ -752,7 +752,12 @@ class ReachabilityNormalizer(config: NormalizerConfig, ra: ReachabilityAnalyzer) tn.render, x.memberRef.asMethod().render, V3.render(fval)).outln(); } array[index] = fval; - normValIntoArray(x.val, norm(x.memberRef.receiver), array, index + 1); + var recv = x.val; + if (EnumType.?(x.memberRef.receiver) + && (!config.NonRefClosureReceiver || config.ExplicitRefTypeCast)) { + recv = OopInt.new(V3.unboxI32(recv)); + } + normValIntoArray(recv, norm(x.memberRef.receiver), array, index + 1); } x: BoxVal => { // tuple: recursively normalize all of the sub diff --git a/aeneas/src/jvm/JvmHeap.v3 b/aeneas/src/jvm/JvmHeap.v3 index e653b25fb..844b24376 100644 --- a/aeneas/src/jvm/JvmHeap.v3 +++ b/aeneas/src/jvm/JvmHeap.v3 @@ -29,7 +29,10 @@ class JvmHeap(jprog: JvmProgram) { VARIANT, CLASS => emitRecordValue(code, Record.!(val)); OOP => { - if (Record.?(val)) emitRecordValue(code, Record.!(val)); + if (OopInt.?(val)) { + code.iconst(OopInt.!(val).v); + code.invokestatic(JvmTypes.java_lang_Integer.name, "valueOf", JvmTypes.SIG_INT_INTEGER); + } else if (Record.?(val)) emitRecordValue(code, Record.!(val)); else if (val == null) code.aconst_null(); else { code.iconst(V3.unboxI32(val)); diff --git a/aeneas/src/wasm/WasmCodeGen.v3 b/aeneas/src/wasm/WasmCodeGen.v3 index 1763d2be6..9a56a6a57 100644 --- a/aeneas/src/wasm/WasmCodeGen.v3 +++ b/aeneas/src/wasm/WasmCodeGen.v3 @@ -1795,19 +1795,17 @@ class WasmCodeGen extends SsaMachGen { VARIANT => { if (!GC) { emit1(WasmOp.I32_CONST.opcode, useImm(val)); + } else if (OopInt.?(val)) { + emit1(WasmOp.I32_CONST.opcode, useInt(OopInt.!(val).v)); + emitN(int.!(WasmExtOp.REF_I31.extopcode)); } else if (val == null) { - if (t == Oop.TYPE) { - emit1(WasmOp.I32_CONST.opcode, useInt(0)); - emitN(int.!(WasmExtOp.REF_I31.extopcode)); + var wht = wasmGc.gcTypeTable.addHeapType(t); + if (wht != null) { + useInt(i32.!(wht.index - 1)); } else { - var wht = wasmGc.gcTypeTable.addHeapType(t); - if (wht != null) { - useInt(i32.!(wht.index - 1)); - } else { - useInt(int.!(WasmTypeConCode.EQREF.val)); - } - emitN(WasmOp.REF_NULL.opcode); + useInt(int.!(WasmTypeConCode.EQREF.val)); } + emitN(WasmOp.REF_NULL.opcode); } else { match (val) { x: Record => { diff --git a/test/enums/enum_closure13.v3 b/test/enums/enum_closure13.v3 new file mode 100644 index 000000000..813d092fd --- /dev/null +++ b/test/enums/enum_closure13.v3 @@ -0,0 +1,16 @@ +//@execute 0=10; 1=11; 2=20; 3=22 +// Closure escape with selective override + params (exercises constant tag-0 boxing) +enum E(x: int) { A(10), _; def m1() -> int { return x; } def m2() -> int { return x * 2; } } +enum E.More { B(11); def m1() -> int { return x; } } +def call(f: void -> int) -> int { return f(); } +def main(i: int) -> int { + var a: E = E.A; + var b: E = E.More.B; + match (i) { + 0 => return call(a.m1); + 1 => return call(b.m1); + 2 => return call(a.m2); + 3 => return call(b.m2); + } + return -1; +} From dd819c728551ecb8f0c9d694c2d530519afc78a6 Mon Sep 17 00:00:00 2001 From: Eliot Moss Date: Sun, 19 Apr 2026 18:10:50 -0400 Subject: [PATCH 26/58] Fix Eval.doOp TypeSubsume for Oop boxing with -wfts flag MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The evaluator's TypeSubsume handler called doCast0(u2, Oop, val) which failed with "subsume should never fail" because the type system doesn't know about Oop boxing. This surfaced with -wfts=true (wrap function type subsumptions) which causes the SSA interpreter to evaluate TypeSubsume instructions that are normally elided. Fix: handle int→Oop boxing (produce OopInt) and Oop→int unboxing (extract from OopInt or unbox) directly in the TypeSubsume evaluator, before falling through to doCast0. Co-Authored-By: Claude Opus 4.6 (1M context) --- aeneas/src/core/Eval.v3 | 2 ++ 1 file changed, 2 insertions(+) diff --git a/aeneas/src/core/Eval.v3 b/aeneas/src/core/Eval.v3 index f69f502f4..a9b4c49a5 100644 --- a/aeneas/src/core/Eval.v3 +++ b/aeneas/src/core/Eval.v3 @@ -890,6 +890,8 @@ def evalOp(op: Operator, args: Arguments) -> Result { if (ft == null) return args.notFoldable(null); if (tt == null) return args.notFoldable(null); var val = args.vals[0]; + if (tt == Oop.TYPE && PrimType.?(ft)) return OopInt.new(V3.unboxI32(val)); + if (ft == Oop.TYPE && PrimType.?(tt)) return Int.box(if(OopInt.?(val), OopInt.!(val).v, V3.unboxI32(val))); var r = Eval.doCast0(ft, tt, val); if (r.0) return r.1; return args.throw(V3Exception.InternalError, "subsume should never fail"); From 8ebb16d2c8052c32c27b4764fdae10b65d031ab9 Mon Sep 17 00:00:00 2001 From: Eliot Moss Date: Sun, 19 Apr 2026 18:25:21 -0400 Subject: [PATCH 27/58] Fix -wfts enum closure failures with BoxEnumClosureReceiver config flag The -wfts flag sets ExplicitRefTypeCast=true on ALL targets, but enum closure Oop boxing should only happen on JVM and wasm-gc. Previously, -wfts on native x86-64 activated boxing via ExplicitRefTypeCast, causing wrong dispatch (OopInt values in Oop slots that native can't handle). Fix: add BoxEnumClosureReceiver config flag, set only on JVM and wasm-gc targets. Use it for OopInt creation in normValIntoArray instead of the ExplicitRefTypeCast-based condition. Also: Eval.v3 TypeSubsume passes val through for Oop (target-neutral), V3.unboxI32 handles OopInt values. Co-Authored-By: Claude Opus 4.6 (1M context) --- aeneas/src/core/Eval.v3 | 3 +-- aeneas/src/ir/Normalization.v3 | 4 ++-- aeneas/src/jvm/JvmTarget.v3 | 1 + aeneas/src/v3/V3.v3 | 1 + aeneas/src/wasm/WasmGcTarget.v3 | 1 + 5 files changed, 6 insertions(+), 4 deletions(-) diff --git a/aeneas/src/core/Eval.v3 b/aeneas/src/core/Eval.v3 index a9b4c49a5..7e7afdeb0 100644 --- a/aeneas/src/core/Eval.v3 +++ b/aeneas/src/core/Eval.v3 @@ -890,8 +890,7 @@ def evalOp(op: Operator, args: Arguments) -> Result { if (ft == null) return args.notFoldable(null); if (tt == null) return args.notFoldable(null); var val = args.vals[0]; - if (tt == Oop.TYPE && PrimType.?(ft)) return OopInt.new(V3.unboxI32(val)); - if (ft == Oop.TYPE && PrimType.?(tt)) return Int.box(if(OopInt.?(val), OopInt.!(val).v, V3.unboxI32(val))); + if (tt == Oop.TYPE || ft == Oop.TYPE) return val; var r = Eval.doCast0(ft, tt, val); if (r.0) return r.1; return args.throw(V3Exception.InternalError, "subsume should never fail"); diff --git a/aeneas/src/ir/Normalization.v3 b/aeneas/src/ir/Normalization.v3 index f3cc4e8de..448fb4aeb 100644 --- a/aeneas/src/ir/Normalization.v3 +++ b/aeneas/src/ir/Normalization.v3 @@ -17,6 +17,7 @@ class NormalizerConfig { var UsedScalars: Scalar.set = Scalar.B32 | Scalar.B64 | Scalar.F32 | Scalar.F64 | Scalar.Ref; var WrapFuncTypeSubsume: bool; var ExplicitRefTypeCast: bool; + var BoxEnumClosureReceiver: bool; var GetScalar: (Compiler, Program, Type) -> Scalar.set = defaultGetScalar; var GetBitWidth: Type -> byte = defaultGetBitWidth; @@ -753,8 +754,7 @@ class ReachabilityNormalizer(config: NormalizerConfig, ra: ReachabilityAnalyzer) } array[index] = fval; var recv = x.val; - if (EnumType.?(x.memberRef.receiver) - && (!config.NonRefClosureReceiver || config.ExplicitRefTypeCast)) { + if (EnumType.?(x.memberRef.receiver) && config.BoxEnumClosureReceiver) { recv = OopInt.new(V3.unboxI32(recv)); } normValIntoArray(recv, norm(x.memberRef.receiver), array, index + 1); diff --git a/aeneas/src/jvm/JvmTarget.v3 b/aeneas/src/jvm/JvmTarget.v3 index 9a8a319ff..00e1eca15 100644 --- a/aeneas/src/jvm/JvmTarget.v3 +++ b/aeneas/src/jvm/JvmTarget.v3 @@ -34,6 +34,7 @@ class JvmTarget extends Target { var norm = compiler.NormConfig; norm.MixedArrays = false; norm.NonRefClosureReceiver = false; + norm.BoxEnumClosureReceiver = true; norm.NormalizeRange = false; // norm.setSignatureLimits(10000, 10000); norm.MaxReturnValues = 10000; diff --git a/aeneas/src/v3/V3.v3 b/aeneas/src/v3/V3.v3 index 805d4645e..56bb2db52 100644 --- a/aeneas/src/v3/V3.v3 +++ b/aeneas/src/v3/V3.v3 @@ -220,6 +220,7 @@ component V3 { x: Box => return int.view(x.val); x: Box => return if(x.val, 1, 0); x: ArrayRangeStart => return x.start; + x: OopInt => return x.v; } var buf = StringBuilder.new().puts("expected i32 value, got "); renderResult(v, null, buf); diff --git a/aeneas/src/wasm/WasmGcTarget.v3 b/aeneas/src/wasm/WasmGcTarget.v3 index 171787224..5afe949be 100644 --- a/aeneas/src/wasm/WasmGcTarget.v3 +++ b/aeneas/src/wasm/WasmGcTarget.v3 @@ -137,6 +137,7 @@ class WasmGcTarget extends Target { compiler.NormConfig.WrapFuncTypeSubsume = true; compiler.NormConfig.AnyRefOverflow = false; compiler.NormConfig.ExplicitRefTypeCast = true; + compiler.NormConfig.BoxEnumClosureReceiver = true; } private def getScalar(compiler: Compiler, prog: Program, t: Type) -> Scalar.set { match (t) { From 5f6e397330be83e48823be8bc1d82968445a9412 Mon Sep 17 00:00:00 2001 From: Eliot Moss Date: Sun, 19 Apr 2026 22:04:51 -0400 Subject: [PATCH 28/58] Add EnumClosureBoxing benchmark for enum closure boxing overhead Measures enum method closure dispatch across 6 scenarios: t0: direct call baseline (no closures) t1: monomorphic closure (JIT should devirtualize) t2: bimorphic closure (2 cases, JIT inline cache) t3: megamorphic closure (4 cases, JIT gives up) t4: closure escape to function (adapter wrapping) t5: closure array iteration (can't devirtualize) Supports x86-64-linux (no boxing baseline), jar (Integer.valueOf boxing), and wasm-gc (i31ref boxing via WASI). run-all.bash runs all 6 cases with per-case timing. Accepts V3C_OPTS for compiler options. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../EnumClosureBoxing-wgc.v3 | 10 ++ bench/EnumClosureBoxing/EnumClosureBoxing.v3 | 15 +++ .../EnumClosureBoxingCore.v3 | 80 ++++++++++++++ bench/EnumClosureBoxing/args-large | 1 + bench/EnumClosureBoxing/args-small | 1 + bench/EnumClosureBoxing/args-test | 1 + bench/EnumClosureBoxing/output-test | 1 + bench/EnumClosureBoxing/run-all.bash | 103 ++++++++++++++++++ 8 files changed, 212 insertions(+) create mode 100644 bench/EnumClosureBoxing/EnumClosureBoxing-wgc.v3 create mode 100644 bench/EnumClosureBoxing/EnumClosureBoxing.v3 create mode 100644 bench/EnumClosureBoxing/EnumClosureBoxingCore.v3 create mode 100644 bench/EnumClosureBoxing/args-large create mode 100644 bench/EnumClosureBoxing/args-small create mode 100644 bench/EnumClosureBoxing/args-test create mode 100644 bench/EnumClosureBoxing/output-test create mode 100755 bench/EnumClosureBoxing/run-all.bash diff --git a/bench/EnumClosureBoxing/EnumClosureBoxing-wgc.v3 b/bench/EnumClosureBoxing/EnumClosureBoxing-wgc.v3 new file mode 100644 index 000000000..23e97fcaa --- /dev/null +++ b/bench/EnumClosureBoxing/EnumClosureBoxing-wgc.v3 @@ -0,0 +1,10 @@ +// Entry point for wasm-gc target (main takes no args). +// Each run tests one case; set TEST and ITERS before compiling, +// or use the run-all.bash script which compiles per-test. + +def main() -> int { + return Bench.runTest(TEST, ITERS); +} + +def TEST = 0; +def ITERS = 10000000; diff --git a/bench/EnumClosureBoxing/EnumClosureBoxing.v3 b/bench/EnumClosureBoxing/EnumClosureBoxing.v3 new file mode 100644 index 000000000..d43b87703 --- /dev/null +++ b/bench/EnumClosureBoxing/EnumClosureBoxing.v3 @@ -0,0 +1,15 @@ +// Entry point for native and JVM targets (main takes Array). + +def main(args: Array) -> int { + var test = if(args.length > 0, parseInt(args[0]), 0); + var iters = if(args.length > 1, parseInt(args[1]), 10000000); + return Bench.runTest(test, iters); +} + +def parseInt(s: string) -> int { + var r = 0; + for (c in s) { + if (c >= '0' && c <= '9') r = r * 10 + (c - '0'); + } + return r; +} diff --git a/bench/EnumClosureBoxing/EnumClosureBoxingCore.v3 b/bench/EnumClosureBoxing/EnumClosureBoxingCore.v3 new file mode 100644 index 000000000..b4280b0bf --- /dev/null +++ b/bench/EnumClosureBoxing/EnumClosureBoxingCore.v3 @@ -0,0 +1,80 @@ +// Benchmark: enum method closure boxing overhead +// Run with: v3c-jar / v3c-wasm-gc / v3c-x86-64-linux, then time execution +// Usage: ./bench_enum_closure +// test 0: direct call baseline (no closure) +// test 1: monomorphic closure inline (JIT can devirtualize) +// test 2: polymorphic closure inline, 2 cases (JIT inline cache likely) +// test 3: polymorphic closure inline, 4 cases (JIT megamorphic) +// test 4: closure escape to function (adapter overhead) +// test 5: closure array iteration (JIT can't devirtualize) + +enum E(x: int) { + A(1), B(2), _; + def val() -> int { return x; } + def add(n: int) -> int { return x + n; } +} +enum E.More { C(3); def val() -> int { return x * 2; } def add(n: int) -> int { return x + n + 1; } } +enum E.Most { D(4); def val() -> int { return x * 3; } def add(n: int) -> int { return x + n + 2; } } + +def call0(f: void -> int) -> int { return f(); } +def call1(f: int -> int, n: int) -> int { return f(n); } + +component Bench { +def runTest(test: int, iters: int) -> int { + var sum = 0; + match (test) { + 0 => { + // Direct call baseline: no closures, static dispatch + var a = E.A; var b = E.B; var c = E.More.C; var d = E.Most.D; + for (i < iters) { + sum += a.val() + b.val() + c.val() + d.val(); + } + } + 1 => { + // Monomorphic closure: same case every time. JIT should inline. + var e: E = E.More.C; + for (i < iters) { + var f = e.val; + sum += f(); + } + } + 2 => { + // Bimorphic: 2 cases alternating. JIT inline cache handles this. + var cases: Array = [E.A, E.More.C]; + for (i < iters) { + var e = cases[i & 1]; + var f = e.val; + sum += f(); + } + } + 3 => { + // Megamorphic: 4 cases. JIT gives up on inline caching. + var cases: Array = [E.A, E.B, E.More.C, E.Most.D]; + for (i < iters) { + var e = cases[i & 3]; + var f = e.val; + sum += f(); + } + } + 4 => { + // Closure escape: passed to function. Tests adapter wrapping. + var cases: Array = [E.A, E.B, E.More.C, E.Most.D]; + for (i < iters) { + var e = cases[i & 3]; + sum += call0(e.val); + } + } + 5 => { + // Closure array: pre-built array of closures, iterated. + var a: E = E.A; var b: E = E.B; var c: E = E.More.C; var d: E = E.Most.D; + var closures: Array int> = [a.val, b.val, c.val, d.val]; + for (i < iters) { + sum += closures[i & 3](); + } + } + } + + return sum & 0xFF; +} +} + diff --git a/bench/EnumClosureBoxing/args-large b/bench/EnumClosureBoxing/args-large new file mode 100644 index 000000000..a81e6366e --- /dev/null +++ b/bench/EnumClosureBoxing/args-large @@ -0,0 +1 @@ +3 100000000 diff --git a/bench/EnumClosureBoxing/args-small b/bench/EnumClosureBoxing/args-small new file mode 100644 index 000000000..f9e82bf0e --- /dev/null +++ b/bench/EnumClosureBoxing/args-small @@ -0,0 +1 @@ +3 10000000 diff --git a/bench/EnumClosureBoxing/args-test b/bench/EnumClosureBoxing/args-test new file mode 100644 index 000000000..938cd27b9 --- /dev/null +++ b/bench/EnumClosureBoxing/args-test @@ -0,0 +1 @@ +0 1000000 diff --git a/bench/EnumClosureBoxing/output-test b/bench/EnumClosureBoxing/output-test new file mode 100644 index 000000000..900731ffd --- /dev/null +++ b/bench/EnumClosureBoxing/output-test @@ -0,0 +1 @@ +64 diff --git a/bench/EnumClosureBoxing/run-all.bash b/bench/EnumClosureBoxing/run-all.bash new file mode 100755 index 000000000..6e2d6d6df --- /dev/null +++ b/bench/EnumClosureBoxing/run-all.bash @@ -0,0 +1,103 @@ +#!/usr/bin/env bash +# Run all EnumClosureBoxing test cases and report per-case timing. +# Usage: ./run-all.bash [iterations] +# target: x86-64-linux, jar, wasm-gc, etc. +# iterations: default 100000000 +# Env: V3C_OPTS - extra compiler options (e.g., "-O2 -wfts=true") +# RUNS - number of runs per test (default 5) +# AENEAS - compiler binary to use + +SOURCE="${BASH_SOURCE[0]}" +DIR="$( cd -P "$( dirname "$SOURCE" )" >/dev/null 2>&1 && pwd )" +VIRGIL_LOC=${VIRGIL_LOC:=$(cd "$DIR/../.." && pwd)} + +if [ $# = 0 ]; then + echo "Usage: run-all.bash [iterations]" + exit 1 +fi + +target=$1 +iters=${2:-100000000} +RUNS=${RUNS:-5} + +TMP=/tmp/$USER/virgil-bench/ecb +opts_tag=$(echo "$V3C_OPTS" | tr ' =-' '_ep' | tr -cd 'A-Za-z0-9_') +PROGNAME="ecb${opts_tag:+-$opts_tag}" +mkdir -p $TMP + +AENEAS=${AENEAS:-$VIRGIL_LOC/bin/current/x86-64-linux/Aeneas} +if [ ! -x "$AENEAS" ]; then + AENEAS=$VIRGIL_LOC/bin/current/x86-linux/Aeneas +fi + +CORE=$DIR/EnumClosureBoxingCore.v3 + +labels=( + "direct call (no closure)" + "monomorphic closure (JIT inlines)" + "bimorphic closure (2 cases)" + "megamorphic closure (4 cases)" + "closure escape (adapter)" + "closure array iteration" +) + +# Determine run command based on target +RT_FILES="" +if [ "$target" = "jar" ]; then + PROG=$TMP/$PROGNAME.jar + RUNCMD="java -jar $PROG" + COMPILE_ARGS="-target=jar" + SRCS="$CORE $DIR/EnumClosureBoxing.v3" +elif [ "$target" = "wasm-gc" ]; then + PROGNAME="${PROGNAME}-wgc" + PROG=$TMP/$PROGNAME.wasm + RT=$VIRGIL_LOC/rt + RT_FILES=$(echo $RT/wasm-gc-wasi1/*.v3 $RT/wasm-wasi1-common/wasi_snapshot_preview1.v3) + COMPILE_ARGS="-target=wasm-gc" + SRCS="$CORE $DIR/EnumClosureBoxing.v3" + # Create wasm-gc runner script if needed + RUNNER=$TMP/run-wgc.mjs + if [ ! -f "$RUNNER" ]; then + cat > "$RUNNER" << 'JSEOF' +import { readFileSync } from 'node:fs'; +import { WASI } from 'wasi'; +import { argv, env } from 'node:process'; +const wasi = new WASI({ returnOnExit: false, version: 'preview1', args: argv.slice(2), env, preopens: { '.': '.' } }); +const importObject = { wasi_snapshot_preview1: wasi.wasiImport }; +const instance = new WebAssembly.Instance(new WebAssembly.Module(readFileSync(argv[2])), importObject); +wasi.initialize(instance); +instance.exports.entry(); +JSEOF + fi + NODE=$(which node) + RUNCMD="$NODE --experimental-wasi-unstable-preview1 $RUNNER $PROG" +else + PROG=$TMP/$PROGNAME + RT=$VIRGIL_LOC/rt + RT_FILES=$(echo $RT/$target/*.v3 $RT/native/*.v3 $RT/gc/*.v3) + COMPILE_ARGS="-heap-size=200m -target=$target" + SRCS="$CORE $DIR/EnumClosureBoxing.v3" + RUNCMD="$PROG" +fi + +if [ ! -f "$PROG" ]; then + echo "Compiling for $target..." + $AENEAS $COMPILE_ARGS $V3C_OPTS -rt.files="$RT_FILES" -output=$TMP -program-name=$PROGNAME $SRCS + if [ $? != 0 ]; then + echo "Compilation failed" + exit 1 + fi +fi + +echo "EnumClosureBoxing ($target${V3C_OPTS:+, $V3C_OPTS}, ${iters} iters, best of ${RUNS}):" +echo "---" +for t in 0 1 2 3 4 5; do + best="" + for run in $(seq 1 $RUNS); do + elapsed=$( { /usr/bin/time -f "%e" $RUNCMD $t $iters; } 2>&1 | tail -1 ) + if [ -z "$best" ] || [ $(echo "$elapsed < $best" | bc) = 1 ]; then + best=$elapsed + fi + done + printf " t%d %-36s %s sec\n" $t "${labels[$t]}" "$best" +done From ebb4ff06dec90adf53de2a90b75988f0bf863211 Mon Sep 17 00:00:00 2001 From: Eliot Moss Date: Mon, 20 Apr 2026 12:08:14 -0400 Subject: [PATCH 29/58] Unify enum/variant dispatch in SsaNormalizer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extract shared helpers for mtable lookup and tag range checking: - lookupMtable: shared mtable record → ArrayGetElem lookup, used by CallVariantVirtual (enum + variant) and VariantGetVirtual (enum) - normVariantQuery: unified VARIANT_QUERY handler that extracts tag and delegates to genTagRangeCheck for both enum and variant paths - genTagRangeCheck: shared equality/range test (lo==hi → IntEq, else lo<=tag<=hi range) - Clean up devirtualized path: remove redundant EnumType check Co-Authored-By: Claude Opus 4.6 (1M context) --- aeneas/src/ir/SsaNormalizer.v3 | 95 +++++++++++++++------------------- 1 file changed, 42 insertions(+), 53 deletions(-) diff --git a/aeneas/src/ir/SsaNormalizer.v3 b/aeneas/src/ir/SsaNormalizer.v3 index a2a05f764..be878e0a2 100644 --- a/aeneas/src/ir/SsaNormalizer.v3 +++ b/aeneas/src/ir/SsaNormalizer.v3 @@ -256,9 +256,7 @@ class SsaRaNormalizer extends SsaRebuilder { if (sel.mtable != null && sel.mtable.record != null) { var tag = obj; var tagType = V3.getVariantTagType(rc.oldType); - var record = sel.mtable.record; - var table = newGraph.valConst(record.rtype, record); - var func = curBlock.opArrayGetElem(record.rtype, tagType, Facts.O_SAFE_BOUNDS, table, tag); + var func = lookupMtable(sel, tag, tagType); obj = boxEnumClosureReceiver(rc, obj); mapN(i_old, [func, obj]); } else { @@ -351,9 +349,7 @@ class SsaRaNormalizer extends SsaRebuilder { // enum: tag IS the value; index into mtable var tag = ai_new[0]; var tagType = V3.getVariantTagType(rc.oldType); - var record = sel.mtable.record; - var table = newGraph.valConst(record.rtype, record); - var func = curBlock.opArrayGetElem(record.rtype, tagType, Facts.O_SAFE_BOUNDS, table, tag); + var func = lookupMtable(sel, tag, tagType); if (context.compiler.NormConfig.NonRefClosureReceiver && !context.compiler.NormConfig.ExplicitRefTypeCast) { // native/wasm (non-gc): CallFunctionDirect (no Oop receiver) @@ -376,22 +372,15 @@ class SsaRaNormalizer extends SsaRebuilder { } else if (rc.isUnboxed()) { // use the variant tag as an index into a table of functions var tag = normVariantGetTag(rc.variantNorm, ai_new); - var record = IrSelector.!(m.member).mtable.record; - var table = newGraph.valConst(record.rtype, record); - var func = curBlock.opArrayGetElem(record.rtype, rc.variantNorm.tagType(), Facts.O_SAFE_BOUNDS, table, tag); + var func = lookupMtable(IrSelector.!(m.member), tag, rc.variantNorm.tagType()); ai_new = Arrays.concat([func, newGraph.nullReceiver()], ai_new); normCall(i_old, funcNorm, V3Op.newCallFunction(funcNorm.sub[0]), ai_new); } else { normCall(i_old, funcNorm, V3Op.newCallVariantSelector(m), ai_new); } } else { - if (rc.isUnboxed()) { - // flattened data type becomes component call and needs new receiver - ai_new = Arrays.prepend(newGraph.nullReceiver(), ai_new); - } - if (EnumType.?(rc.oldType)) { - // devirtualized enum: tag already in args - } + // devirtualized: enum tag already in args; unboxed variant needs null receiver + if (rc.isUnboxed()) ai_new = Arrays.prepend(newGraph.nullReceiver(), ai_new); normCall(i_old, funcNorm, V3Op.newCallMethod(m), ai_new); } } @@ -1153,43 +1142,8 @@ class SsaRaNormalizer extends SsaRebuilder { } // XXX: CLASS_QUERY special-case non-allocated classes VARIANT_QUERY => { - if (VariantNorm.?(atn) && VariantNorm.?(rtn)) { - var avn = VariantNorm.!(atn), rvn = VariantNorm.!(rtn); - var actualTag = normVariantGetTag(avn, ai_old[offset ...]); - var tagType = avn.tagType(); - var check: SsaInstr; - if (rvn.tagValue == rvn.tagHi || rvn.tagHi < 0) { - // Leaf (or uninitialized tagHi): exact equality test. - var expected = newGraph.intConst(rvn.tagValue); - check = curBlock.pure(V3Op.newIntEq(tagType), [actualTag, expected]); - } else { - // Non-leaf subtype: range test lo <= tag && tag <= hi. - var lo = newGraph.intConst(rvn.tagValue); - var hi = newGraph.intConst(rvn.tagHi); - var chkLo = curBlock.pure(tagType.opLtEq(), [lo, actualTag]); - var chkHi = curBlock.pure(tagType.opLtEq(), [actualTag, hi]); - check = curBlock.opBoolAnd0(chkLo, chkHi); - } - return opAnd(left, check); - } - if (EnumType.?(atn.oldType) && EnumType.?(rtn.oldType)) { - // Enum subtype range test: lo <= tag <= hi. - var aDecl = EnumType.!(atn.oldType).enumDecl; - var rDecl = EnumType.!(rtn.oldType).enumDecl; - var actualTag = ai_old[offset]; // enum value IS its tag integer - var tagType = IntType.!(aDecl.tagType); - var lo = rDecl.tagLo, hi = rDecl.tagHi; - var check: SsaInstr; - if (lo == hi) { - check = curBlock.pure(V3Op.newIntEq(tagType), [actualTag, newGraph.intConst(lo)]); - } else { - var chkLo = curBlock.pure(tagType.opLtEq(), [newGraph.intConst(lo), actualTag]); - var chkHi = curBlock.pure(tagType.opLtEq(), [actualTag, newGraph.intConst(hi)]); - check = curBlock.opBoolAnd0(chkLo, chkHi); - } - return opAnd(left, check); - } - // break + var check = normVariantQuery(atn, rtn, ai_old, offset); + if (check != null) return opAnd(left, check); } _ => ; // break } @@ -1935,6 +1889,41 @@ class SsaRaNormalizer extends SsaRebuilder { private def funcRef(m: IrSpec) -> SsaInstr { return newGraph.valConst(Function.funcRefType(m.getFuncType()), FuncVal.new(m)); } + // Normalize a VARIANT_QUERY for both variants and enums. Returns null if unhandled. + private def normVariantQuery(atn: TypeNorm, rtn: TypeNorm, ai_old: Array, offset: int) -> SsaInstr { + var actualTag: SsaInstr, tagType: IntType, lo: int, hi: int; + if (VariantNorm.?(atn) && VariantNorm.?(rtn)) { + var avn = VariantNorm.!(atn), rvn = VariantNorm.!(rtn); + actualTag = normVariantGetTag(avn, ai_old[offset ...]); + tagType = avn.tagType(); + lo = rvn.tagValue; + hi = if(rvn.tagHi < 0, rvn.tagValue, rvn.tagHi); + } else if (EnumType.?(atn.oldType) && EnumType.?(rtn.oldType)) { + var rDecl = EnumType.!(rtn.oldType).enumDecl; + actualTag = ai_old[offset]; + tagType = IntType.!(EnumType.!(atn.oldType).enumDecl.tagType); + lo = rDecl.tagLo; + hi = rDecl.tagHi; + } else { + return null; + } + return genTagRangeCheck(tagType, actualTag, lo, hi); + } + // Generate a tag range check: lo == hi → equality, else lo <= tag <= hi. + private def genTagRangeCheck(tagType: IntType, actualTag: SsaInstr, lo: int, hi: int) -> SsaInstr { + if (lo == hi) { + return curBlock.pure(V3Op.newIntEq(tagType), [actualTag, newGraph.intConst(lo)]); + } + var chkLo = curBlock.pure(tagType.opLtEq(), [newGraph.intConst(lo), actualTag]); + var chkHi = curBlock.pure(tagType.opLtEq(), [actualTag, newGraph.intConst(hi)]); + return curBlock.opBoolAnd0(chkLo, chkHi); + } + // Look up a function in an mtable record by tag. Returns the function SSA value. + private def lookupMtable(sel: IrSelector, tag: SsaInstr, tagType: IntType) -> SsaInstr { + var record = sel.mtable.record; + var table = newGraph.valConst(record.rtype, record); + return curBlock.opArrayGetElem(record.rtype, tagType, Facts.O_SAFE_BOUNDS, table, tag); + } // For JVM/wasm-gc: box enum tag to Oop for closure receiver slot. private def boxEnumClosureReceiver(rc: RaClass, obj: SsaInstr) -> SsaInstr { if (EnumType.?(rc.oldType) From d4dca29c8e8d91a6243cc7f649c0e93a0aac600b Mon Sep 17 00:00:00 2001 From: Eliot Moss Date: Mon, 20 Apr 2026 12:10:28 -0400 Subject: [PATCH 30/58] Unify enum/variant mtable entry filling in Normalization Extract setMtableEntry helper shared by fillMtableSlot (variants) and fillEnumMtable (enums). The entry assignment code (table + record values) was duplicated verbatim between the two paths. Co-Authored-By: Claude Opus 4.6 (1M context) --- aeneas/src/ir/Normalization.v3 | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/aeneas/src/ir/Normalization.v3 b/aeneas/src/ir/Normalization.v3 index 448fb4aeb..6c03a532f 100644 --- a/aeneas/src/ir/Normalization.v3 +++ b/aeneas/src/ir/Normalization.v3 @@ -664,9 +664,7 @@ class ReachabilityNormalizer(config: NormalizerConfig, ra: ReachabilityAnalyzer) } setMtable(rc, rv); // set mtable for all child virtual methods } - private def fillMtableSlot(mtable: IrMtable, rc: RaClass, rm: RaMethod) { - var impl = resolveMethodImpl(rc, rm); - var index = rc.minClassId - mtable.rootId; + private def setMtableEntry(mtable: IrMtable, impl: RaMethod, index: int) { mtable.table[index] = impl.norm; if (mtable.record != null) { var ta = Arrays.replace(impl.getSpec().typeArgs, 0, impl.norm.receiver); @@ -674,20 +672,17 @@ class ReachabilityNormalizer(config: NormalizerConfig, ra: ReachabilityAnalyzer) mtable.record.values[index] = FuncVal.new(spec); } } + private def fillMtableSlot(mtable: IrMtable, rc: RaClass, rm: RaMethod) { + var impl = resolveMethodImpl(rc, rm); + setMtableEntry(mtable, impl, rc.minClassId - mtable.rootId); + } private def fillEnumMtable(mtable: IrMtable, rc: RaClass, rm: RaMethod) { var impl = resolveEnumMethodImpl(rc, rm); if (impl == null) impl = rm; // fall back to root var tagLo = if(EnumType.?(rc.oldType), EnumType.!(rc.oldType).enumDecl.tagLo, 0); var tagHi = if(EnumType.?(rc.oldType), EnumType.!(rc.oldType).enumDecl.tagHi, 0); if (tagLo < 0) tagLo = 0; - for (tag = tagLo; tag <= tagHi; tag++) { - mtable.table[tag] = impl.norm; - if (mtable.record != null) { - var ta = Arrays.replace(impl.getSpec().typeArgs, 0, impl.norm.receiver); - var spec = IrSpec.new(ta[0], ta, impl.norm); - mtable.record.values[tag] = FuncVal.new(spec); - } - } + for (tag = tagLo; tag <= tagHi; tag++) setMtableEntry(mtable, impl, tag); // Subtypes override their range var decl = EnumType.!(rc.oldType).enumDecl; for (l = decl.subtypeOrder; l != null; l = l.tail) { From a3e1f82cb7820345688e2e0d6941352e9db96db1 Mon Sep 17 00:00:00 2001 From: Eliot Moss Date: Mon, 20 Apr 2026 12:34:29 -0400 Subject: [PATCH 31/58] Enable IrClass parent inheritance for enum subtypes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Enum subtype IrClasses now inherit methods from their parent IrClass, matching how variant subtypes work. This is Phase 1 of unifying enum and variant handling in the compiler. Key changes: - Ir.v3 makeIrClass: enum subtypes pass parent IrClass (from parentEnum) instead of null - VstIr.v3 IrBuilder: skip field copying for EnumType (enums store fields in side arrays, not as object fields — copying would corrupt GC maps) - Reachability.v3: enum subtypes set RaClass.parent for parent-chain method resolution; addEnumSubtypesRecursive called for all enum types - Normalization.v3: remove resolveEnumMethodImpl diagnostic; unify setMtableEntry The markEnumSubtypeOverrides post-pass is still needed for per-case method overrides. Removing it is future work. Co-Authored-By: Claude Opus 4.6 (1M context) --- aeneas/src/ir/Ir.v3 | 4 +++- aeneas/src/ir/Reachability.v3 | 9 +++------ aeneas/src/ir/VstIr.v3 | 4 ++-- 3 files changed, 8 insertions(+), 9 deletions(-) diff --git a/aeneas/src/ir/Ir.v3 b/aeneas/src/ir/Ir.v3 index 65d699f63..a712105da 100644 --- a/aeneas/src/ir/Ir.v3 +++ b/aeneas/src/ir/Ir.v3 @@ -396,7 +396,9 @@ class IrModule { ic = newIrClass(ctype, null, V3.componentDecl(ctype)); classMap[ctype] = ic; } else if (EnumType.?(ctype)) { - ic = newIrClass(ctype, null, EnumType.!(ctype).enumDecl); + var enumDecl = EnumType.!(ctype).enumDecl; + var sc = if(enumDecl.parentEnum != null, makeIrClass(enumDecl.parentEnum.getDeclaredType())); + ic = newIrClass(ctype, sc, enumDecl); classMap[ctype] = ic; } } diff --git a/aeneas/src/ir/Reachability.v3 b/aeneas/src/ir/Reachability.v3 index 48cde18e1..ed4d465d8 100644 --- a/aeneas/src/ir/Reachability.v3 +++ b/aeneas/src/ir/Reachability.v3 @@ -733,14 +733,11 @@ class ReachabilityAnalyzer(compilation: Compilation) { } ENUM => { var enumDecl = EnumType.!(t).enumDecl; - var rc = newRaClass(t, oldIr.makeIrClass(t), null); + var parentRc = if(enumDecl.parentEnum != null, + RaClass.!(makeClass(enumDecl.parentEnum.getDeclaredType()))); + var rc = newRaClass(t, oldIr.makeIrClass(t), parentRc); raType = rc; typeMap[t] = raType; - // Populate rc.subtypes with self plus all descendant subtype - // RaClasses so getVirtual() can drive analyzeVirtual across the - // hierarchy. Enum subtype IrClasses don't inherit parent's fields - // (unlike variants), so we do NOT set RaClass.parent — instead we - // manage subtypes directly. addEnumSubtypesRecursive(rc, enumDecl); } _ => { diff --git a/aeneas/src/ir/VstIr.v3 b/aeneas/src/ir/VstIr.v3 index 15e318f06..195250dea 100644 --- a/aeneas/src/ir/VstIr.v3 +++ b/aeneas/src/ir/VstIr.v3 @@ -37,7 +37,7 @@ class IrBuilder(ctype: Type, parent: IrClass) { methods.put(null); // reserve index 0 for constructor methods.put(null); // reserve index 1 for variant equals method } else { - fields.puta(parent.fields); + if (!EnumType.?(ctype)) fields.puta(parent.fields); methods.puta(parent.methods); methods[0] = null; // overwrite super constructor } @@ -111,7 +111,7 @@ class IrBuilder(ctype: Type, parent: IrClass) { // this is the root method of a method family addIrMethod(ir); } else if (parent != null) { - // this is a class/variant override + // this is a class/variant/enum-subtype override parent.methods[r.index].flags |= IrFlag.M_OVERRIDDEN; ir.flags |= IrFlag.M_OVERRIDE; setIrMethod(r.index, ir); From 55f81e8994c1ff84e314c88934fa856c283a27d4 Mon Sep 17 00:00:00 2001 From: Eliot Moss Date: Mon, 20 Apr 2026 12:39:20 -0400 Subject: [PATCH 32/58] Remove markEnumSubtypeOverrides post-pass With IrClass parent inheritance for enum subtypes, the M_OVERRIDDEN flag is now set by addVstMethod path #2 during IrClass construction. The markEnumSubtypeOverrides/markOverridesRecursive post-pass is no longer needed and is removed (~30 lines). The post-pass was also the only code path that called makeIrClass for parameterless enums with methods. Fix: add enums to the addInitFor pass (alongside components and classes) so IrClasses are built for all enum types regardless of whether they have params. Co-Authored-By: Claude Opus 4.6 (1M context) --- aeneas/src/ir/Ir.v3 | 30 ------------------------------ aeneas/src/vst/Verifier.v3 | 2 +- 2 files changed, 1 insertion(+), 31 deletions(-) diff --git a/aeneas/src/ir/Ir.v3 b/aeneas/src/ir/Ir.v3 index a712105da..41ea604ea 100644 --- a/aeneas/src/ir/Ir.v3 +++ b/aeneas/src/ir/Ir.v3 @@ -423,36 +423,6 @@ class IrModule { classes.put(ic); return ic; } - def markEnumSubtypeOverrides(enums: Vector) { - for (i < enums.length) { - var decl = enums[i]; - if (decl.parentEnum != null) continue; - makeIrClass(decl.getDeclaredType()); - markOverridesRecursive(decl); - } - } - private def markOverridesRecursive(decl: VstClass) { - for (l = decl.subtypeOrder; l != null; l = l.tail) { - var sub = l.head; - var subIc = makeIrClass(sub.getDeclaredType()); - if (subIc != null) { - for (j < subIc.methods.length) { - var m = subIc.methods[j]; - if (m == null || m.source == null) continue; - if (!VstMethod.?(m.source)) continue; - var vm = VstMethod.!(m.source); - if (vm.root != vm) { - var rootIc = makeIrClass(vm.root.receiver.getDeclaredType()); - if (rootIc != null && vm.root.index < rootIc.methods.length) { - var rootM = rootIc.methods[vm.root.index]; - if (rootM != null) rootM.flags |= IrFlag.M_OVERRIDDEN; - } - } - } - } - markOverridesRecursive(sub); - } - } def isEnum(t: Type) -> bool { return makeIrClass(t).flags.C_ENUM; } diff --git a/aeneas/src/vst/Verifier.v3 b/aeneas/src/vst/Verifier.v3 index 2ec11c858..d3d01db07 100644 --- a/aeneas/src/vst/Verifier.v3 +++ b/aeneas/src/vst/Verifier.v3 @@ -51,8 +51,8 @@ class Verifier(compiler: Compiler, prog: Program) { if (ERROR.noErrors) { vst.components.apply(addInitFor); vst.classes.apply(addInitFor); + vst.enums.apply(addInitFor); vst.enums.apply(addEnumVals); - ir.markEnumSubtypeOverrides(vst.enums); } if (mainMethod != null) { var spec = ir.fromVstMemberAndType(mainMethod.receiver.getDeclaredType(), mainMethod); From fe8e981695218382e78d9f81f022f1dafd93a922 Mon Sep 17 00:00:00 2001 From: Eliot Moss Date: Mon, 20 Apr 2026 12:42:10 -0400 Subject: [PATCH 33/58] Remove resolveEnumMethodImpl; use shared resolveMethodImpl With IrClass parent inheritance for enum subtypes, the standard parent-chain resolveMethodImpl now works for enums. The enum-specific resolveEnumMethodImpl (which searched by VstMethod.root source identity) is no longer needed and is removed. Co-Authored-By: Claude Opus 4.6 (1M context) --- aeneas/src/ir/Normalization.v3 | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/aeneas/src/ir/Normalization.v3 b/aeneas/src/ir/Normalization.v3 index 6c03a532f..fd25687ae 100644 --- a/aeneas/src/ir/Normalization.v3 +++ b/aeneas/src/ir/Normalization.v3 @@ -677,7 +677,7 @@ class ReachabilityNormalizer(config: NormalizerConfig, ra: ReachabilityAnalyzer) setMtableEntry(mtable, impl, rc.minClassId - mtable.rootId); } private def fillEnumMtable(mtable: IrMtable, rc: RaClass, rm: RaMethod) { - var impl = resolveEnumMethodImpl(rc, rm); + var impl = resolveMethodImpl(rc, rm); if (impl == null) impl = rm; // fall back to root var tagLo = if(EnumType.?(rc.oldType), EnumType.!(rc.oldType).enumDecl.tagLo, 0); var tagHi = if(EnumType.?(rc.oldType), EnumType.!(rc.oldType).enumDecl.tagHi, 0); @@ -690,19 +690,6 @@ class ReachabilityNormalizer(config: NormalizerConfig, ra: ReachabilityAnalyzer) if (childRc != null) fillEnumMtable(mtable, RaClass.!(childRc), rm); } } - // Resolve an enum method by matching VstMethod.root identity, since enum - // subtype IrClasses don't inherit parent's method array. - private def resolveEnumMethodImpl(rc: RaClass, rm: RaMethod) -> RaMethod { - var rootSource = rm.orig.source; - for (ml in rc.methods) { - for (l = ml; l != null; l = l.tail) { - var m = l.head; - if (m.orig.source == rootSource) return m; - if (VstMethod.?(m.orig.source) && VstMethod.!(m.orig.source).root == rootSource) return m; - } - } - return null; - } def setMtable(rc: RaClass, rv: RaVirtual) { var rm = rc.findRaMethod(rv.raMethod); if (rm != null && rm.virtual != null) rm.virtual.mtable = rv.mtable; From 45e9b3eef612223cd898784438c0fac920b26eca Mon Sep 17 00:00:00 2001 From: Eliot Moss Date: Mon, 20 Apr 2026 12:56:00 -0400 Subject: [PATCH 34/58] Add shared hierarchical-type helpers; reduce EnumType.? usage Add V3.isHierarchical, V3.getDecl, V3.getTagLo, V3.getTagHi that work uniformly for class, variant, and enum types. Replace direct EnumType.!(t).enumDecl accesses in Normalization.v3 and SsaNormalizer.v3 with shared helpers. Remove duplicate getTagLo/getTagHi from ReachabilityNormalizer (now in V3 component). This is the first step of Phase 2: gradually replacing EnumType.? checks with shared helpers to reduce the dependency on EnumType as a separate type. Co-Authored-By: Claude Opus 4.6 (1M context) --- aeneas/src/ir/Normalization.v3 | 24 +++++------------------- aeneas/src/ir/SsaNormalizer.v3 | 9 ++++----- aeneas/src/v3/V3.v3 | 23 +++++++++++++++++++++++ 3 files changed, 32 insertions(+), 24 deletions(-) diff --git a/aeneas/src/ir/Normalization.v3 b/aeneas/src/ir/Normalization.v3 index fd25687ae..5d4d187f6 100644 --- a/aeneas/src/ir/Normalization.v3 +++ b/aeneas/src/ir/Normalization.v3 @@ -346,7 +346,7 @@ class ReachabilityNormalizer(config: NormalizerConfig, ra: ReachabilityAnalyzer) // Non-leaf range nodes whose tagLo equals a child's tagLo are NOT stored in // liveClasses, so layoutMtable can filter them out. def numberVariantSlot(c: RaClass, rootMin: int) { - var tagLo = getTagLo(c.oldType), tagHi = getTagHi(c.oldType); + var tagLo = V3.getTagLo(c.oldType), tagHi = V3.getTagHi(c.oldType); c.minClassId = rootMin + tagLo; c.maxClassId = rootMin + tagHi + 1; liveClasses.grow(c.maxClassId); @@ -357,26 +357,12 @@ class ReachabilityNormalizer(config: NormalizerConfig, ra: ReachabilityAnalyzer) // Check if this node has its own slot (no child starts at the same tag). var ownSlot = true; for (l = c.children; l != null; l = l.tail) { - if (getTagLo(l.head.oldType) == tagLo) { ownSlot = false; break; } + if (V3.getTagLo(l.head.oldType) == tagLo) { ownSlot = false; break; } } if (ownSlot) liveClasses[rootMin + tagLo] = c; // e.g. "case _" before subtypes for (l = c.children; l != null; l = l.tail) numberVariantSlot(l.head, rootMin); } } - private def getTagLo(t: Type) -> int { - match (t) { - x: ClassType => return x.classDecl.variantTag; - x: EnumType => return x.enumDecl.tagLo; - _ => return 0; - } - } - private def getTagHi(t: Type) -> int { - match (t) { - x: ClassType => return x.classDecl.tagHi; - x: EnumType => return x.enumDecl.tagHi; - _ => return 0; - } - } def mapSimple(t: Type) -> TypeNorm { return typeMap[t] = TypeNorm.new(t, t, null); } @@ -679,12 +665,12 @@ class ReachabilityNormalizer(config: NormalizerConfig, ra: ReachabilityAnalyzer) private def fillEnumMtable(mtable: IrMtable, rc: RaClass, rm: RaMethod) { var impl = resolveMethodImpl(rc, rm); if (impl == null) impl = rm; // fall back to root - var tagLo = if(EnumType.?(rc.oldType), EnumType.!(rc.oldType).enumDecl.tagLo, 0); - var tagHi = if(EnumType.?(rc.oldType), EnumType.!(rc.oldType).enumDecl.tagHi, 0); + var tagLo = V3.getTagLo(rc.oldType); + var tagHi = V3.getTagHi(rc.oldType); if (tagLo < 0) tagLo = 0; for (tag = tagLo; tag <= tagHi; tag++) setMtableEntry(mtable, impl, tag); // Subtypes override their range - var decl = EnumType.!(rc.oldType).enumDecl; + var decl = V3.getDecl(rc.oldType); for (l = decl.subtypeOrder; l != null; l = l.tail) { var childRc = ra.getClass(l.head.getDeclaredType()); if (childRc != null) fillEnumMtable(mtable, RaClass.!(childRc), rm); diff --git a/aeneas/src/ir/SsaNormalizer.v3 b/aeneas/src/ir/SsaNormalizer.v3 index be878e0a2..f94de3c0e 100644 --- a/aeneas/src/ir/SsaNormalizer.v3 +++ b/aeneas/src/ir/SsaNormalizer.v3 @@ -1898,12 +1898,11 @@ class SsaRaNormalizer extends SsaRebuilder { tagType = avn.tagType(); lo = rvn.tagValue; hi = if(rvn.tagHi < 0, rvn.tagValue, rvn.tagHi); - } else if (EnumType.?(atn.oldType) && EnumType.?(rtn.oldType)) { - var rDecl = EnumType.!(rtn.oldType).enumDecl; + } else if (V3.isEnum(atn.oldType) && V3.isEnum(rtn.oldType)) { actualTag = ai_old[offset]; - tagType = IntType.!(EnumType.!(atn.oldType).enumDecl.tagType); - lo = rDecl.tagLo; - hi = rDecl.tagHi; + tagType = V3.getVariantTagType(atn.oldType); + lo = V3.getTagLo(rtn.oldType); + hi = V3.getTagHi(rtn.oldType); } else { return null; } diff --git a/aeneas/src/v3/V3.v3 b/aeneas/src/v3/V3.v3 index 56bb2db52..2a2c5e74a 100644 --- a/aeneas/src/v3/V3.v3 +++ b/aeneas/src/v3/V3.v3 @@ -102,6 +102,29 @@ component V3 { _ => return null; } } + // Shared helpers for hierarchical types (class, variant, enum). + def isHierarchical(t: Type) -> bool { return ClassType.?(t) || EnumType.?(t); } + def getDecl(t: Type) -> VstClass { + match (t) { + x: ClassType => return x.classDecl; + x: EnumType => return x.enumDecl; + _ => return null; + } + } + def getTagLo(t: Type) -> int { + match (t) { + x: ClassType => return x.classDecl.variantTag; + x: EnumType => return x.enumDecl.tagLo; + _ => return 0; + } + } + def getTagHi(t: Type) -> int { + match (t) { + x: ClassType => return x.classDecl.tagHi; + x: EnumType => return x.enumDecl.tagHi; + _ => return 0; + } + } // compute the type of the tag based on the number of cases def getTagType(caseCount: int) -> IntType { def width = Ints.max(1, Ints.log2Ceil(u32.view(caseCount))); From 94a93c1e9e3e651370e03bca2aeba13837c67800 Mon Sep 17 00:00:00 2001 From: Eliot Moss Date: Mon, 20 Apr 2026 13:01:13 -0400 Subject: [PATCH 35/58] Replace EnumType.? with V3 helpers across IR and optimizer Eliminate all direct EnumType references from Normalization.v3, SsaOptimizer.v3, Ir.v3, and Reachability.v3 using V3.isEnum, V3.getDecl, V3.getTagLo, V3.getTagHi, and V3.getVariantTagType. SsaNormalizer.v3 retains 5 EnumType references in match arms where the actual type object is needed. All other files in the IR layer now use the shared hierarchical-type helpers exclusively. Co-Authored-By: Claude Opus 4.6 (1M context) --- aeneas/src/ir/Ir.v3 | 8 ++++---- aeneas/src/ir/Normalization.v3 | 16 ++++++++-------- aeneas/src/ir/Reachability.v3 | 8 ++++---- aeneas/src/ir/SsaNormalizer.v3 | 18 +++++++++--------- aeneas/src/ssa/SsaOptimizer.v3 | 26 +++++++++++++------------- 5 files changed, 38 insertions(+), 38 deletions(-) diff --git a/aeneas/src/ir/Ir.v3 b/aeneas/src/ir/Ir.v3 index 41ea604ea..aa04faa21 100644 --- a/aeneas/src/ir/Ir.v3 +++ b/aeneas/src/ir/Ir.v3 @@ -395,10 +395,10 @@ class IrModule { } else if (V3.isComponent(ctype)) { ic = newIrClass(ctype, null, V3.componentDecl(ctype)); classMap[ctype] = ic; - } else if (EnumType.?(ctype)) { - var enumDecl = EnumType.!(ctype).enumDecl; - var sc = if(enumDecl.parentEnum != null, makeIrClass(enumDecl.parentEnum.getDeclaredType())); - ic = newIrClass(ctype, sc, enumDecl); + } else if (V3.isEnum(ctype)) { + var decl = V3.getDecl(ctype); + var sc = if(decl.parentEnum != null, makeIrClass(decl.parentEnum.getDeclaredType())); + ic = newIrClass(ctype, sc, decl); classMap[ctype] = ic; } } diff --git a/aeneas/src/ir/Normalization.v3 b/aeneas/src/ir/Normalization.v3 index 5d4d187f6..12f1521af 100644 --- a/aeneas/src/ir/Normalization.v3 +++ b/aeneas/src/ir/Normalization.v3 @@ -115,7 +115,7 @@ class ReachabilityNormalizer(config: NormalizerConfig, ra: ReachabilityAnalyzer) norm(ic.oldType); numberVariant(ic); if (!ic.isUnboxed()) makeNormFieldsAll(ic); - } else if (EnumType.?(ic.oldType) && EnumType.!(ic.oldType).enumDecl.subtypeOrder != null) { + } else if (V3.isEnum(ic.oldType) && V3.getDecl(ic.oldType).subtypeOrder != null) { norm(ic.oldType); numberVariant(ic); makeNormFieldsAll(ic); @@ -163,7 +163,7 @@ class ReachabilityNormalizer(config: NormalizerConfig, ra: ReachabilityAnalyzer) if (rc.isUnboxed()) { // move flattened data type receiver to function sig ftype = Function.prependParamTypes(rc.variantNorm.sub, ftype); - } else if (EnumType.?(rc.oldType)) { + } else if (V3.isEnum(rc.oldType)) { var tagType = V3.getVariantTagType(rc.oldType); if (!config.NonRefClosureReceiver && m.source != null && VstMethod.?(m.source)) { ftype = Function.prependParamType(Oop.TYPE, ftype); @@ -330,8 +330,8 @@ class ReachabilityNormalizer(config: NormalizerConfig, ra: ReachabilityAnalyzer) // For enum roots, ensure we reserve the full DFS tag range. Children // are only subtype enum RaClasses (not individual cases), so they may // not cover all tag slots. - if (EnumType.?(rc.oldType)) { - var tagHi = EnumType.!(rc.oldType).enumDecl.tagHi; + if (V3.isEnum(rc.oldType)) { + var tagHi = V3.getTagHi(rc.oldType); var endId = rc.minClassId + tagHi + 1; liveClasses.grow(endId); if (liveClasses.length < endId) liveClasses.length = endId; @@ -619,10 +619,10 @@ class ReachabilityNormalizer(config: NormalizerConfig, ra: ReachabilityAnalyzer) var table = Array.new(size), mtable = IrMtable.new(rm.norm, rc.minClassId, table); rv.mtable = mtable; - if (rc.isUnboxed() || EnumType.?(rc.oldType)) { + if (rc.isUnboxed() || V3.isEnum(rc.oldType)) { var methType = rm.norm.getMethodType(); // JVM enum user methods: strip Oop tag from mtable funcRefType - if (!config.NonRefClosureReceiver && EnumType.?(rc.oldType) + if (!config.NonRefClosureReceiver && V3.isEnum(rc.oldType) && rm.orig.source != null && VstMethod.?(rm.orig.source)) { var paramList = Function.getParamTypeList(methType); if (paramList != null && paramList.head == Oop.TYPE) { @@ -635,7 +635,7 @@ class ReachabilityNormalizer(config: NormalizerConfig, ra: ReachabilityAnalyzer) mtable.record = ra.prog.newRecord(V3Array.newType(ft), size); } if (mtable.table.length > 0) { - if (EnumType.?(rc.oldType)) { + if (V3.isEnum(rc.oldType)) { // Enum: fill by walking the hierarchy. Each enum type // covers a tag range; subtypes override their parent's range. fillEnumMtable(mtable, rc, rm); @@ -722,7 +722,7 @@ class ReachabilityNormalizer(config: NormalizerConfig, ra: ReachabilityAnalyzer) } array[index] = fval; var recv = x.val; - if (EnumType.?(x.memberRef.receiver) && config.BoxEnumClosureReceiver) { + if (V3.isEnum(x.memberRef.receiver) && config.BoxEnumClosureReceiver) { recv = OopInt.new(V3.unboxI32(recv)); } normValIntoArray(recv, norm(x.memberRef.receiver), array, index + 1); diff --git a/aeneas/src/ir/Reachability.v3 b/aeneas/src/ir/Reachability.v3 index ed4d465d8..01fa81e05 100644 --- a/aeneas/src/ir/Reachability.v3 +++ b/aeneas/src/ir/Reachability.v3 @@ -732,13 +732,13 @@ class ReachabilityAnalyzer(compilation: Compilation) { raType = newRaClass(t, ic, parent); } ENUM => { - var enumDecl = EnumType.!(t).enumDecl; - var parentRc = if(enumDecl.parentEnum != null, - RaClass.!(makeClass(enumDecl.parentEnum.getDeclaredType()))); + var decl = V3.getDecl(t); + var parentRc = if(decl.parentEnum != null, + RaClass.!(makeClass(decl.parentEnum.getDeclaredType()))); var rc = newRaClass(t, oldIr.makeIrClass(t), parentRc); raType = rc; typeMap[t] = raType; - addEnumSubtypesRecursive(rc, enumDecl); + addEnumSubtypesRecursive(rc, decl); } _ => { for (l = t.nested; l != null; l = l.tail) makeType(l.head); diff --git a/aeneas/src/ir/SsaNormalizer.v3 b/aeneas/src/ir/SsaNormalizer.v3 index f94de3c0e..625e87fb8 100644 --- a/aeneas/src/ir/SsaNormalizer.v3 +++ b/aeneas/src/ir/SsaNormalizer.v3 @@ -58,7 +58,7 @@ class SsaRaNormalizer extends SsaRebuilder { var newIrType = norm.ra.getClass(tn.oldType).newIrType; newParams.put(SsaParam.new(newParams.length, newIrType)); start++; // skip synthesized receiver - } else if (EnumType.?(tn.oldType) && !norm.config.NonRefClosureReceiver + } else if (V3.isEnum(tn.oldType) && !norm.config.NonRefClosureReceiver && context.method.source != null && VstMethod.?(context.method.source)) { newParams.put(SsaParam.new(newParams.length, Oop.TYPE)); newRanges.put(start, newParams.length); @@ -111,7 +111,7 @@ class SsaRaNormalizer extends SsaRebuilder { // Map old parameters to new parameters // JVM user-defined enum methods: unbox Oop param to int tag if (!norm.config.NonRefClosureReceiver && oldParams.length > 0 - && EnumType.?(oldParams[0].vtype) + && V3.isEnum(oldParams[0].vtype) && context.method.source != null && VstMethod.?(context.method.source)) { var b = SsaBuilder.new(context, newGraph, newGraph.startBlock); var tagType = V3.getVariantTagType(oldParams[0].vtype); @@ -236,7 +236,7 @@ class SsaRaNormalizer extends SsaRebuilder { obj = boxEnumClosureReceiver(rc, obj); var spec = extractMethodRef(orig, method).1; var fref: SsaInstr; - if (EnumType.?(rc.oldType) + if (V3.isEnum(rc.oldType) && (!context.compiler.NormConfig.NonRefClosureReceiver || context.compiler.NormConfig.ExplicitRefTypeCast)) { var funcNorm = normFuncType(i_old.getType()); @@ -250,7 +250,7 @@ class SsaRaNormalizer extends SsaRebuilder { var rc = norm.ra.getClass(op.typeArgs[0]); var t = extractVirtualRef(orig, method), obj = genRef1(args[0]); if (t.2) { // still a virtual dispatch - if (EnumType.?(rc.oldType)) { + if (V3.isEnum(rc.oldType)) { // enum: index mtable with tag (not selector on record) var sel = IrSelector.!(t.1.member); if (sel.mtable != null && sel.mtable.record != null) { @@ -297,7 +297,7 @@ class SsaRaNormalizer extends SsaRebuilder { ai_new = Arrays.prepend(newGraph.nullReceiver(), ai_new); } } - if (EnumType.?(rc.oldType)) { + if (V3.isEnum(rc.oldType)) { i_old.facts |= Fact.O_NO_NULL_CHECK; if (!context.compiler.NormConfig.NonRefClosureReceiver && method.source != null && VstMethod.?(method.source)) { @@ -339,11 +339,11 @@ class SsaRaNormalizer extends SsaRebuilder { CallVariantVirtual(method) => { // devirtualize methods that are not overridden var rc = norm.ra.getClass(op.typeArgs[0]); - if (EnumType.?(rc.oldType)) i_old.facts |= Fact.O_NO_NULL_CHECK; + if (V3.isEnum(rc.oldType)) i_old.facts |= Fact.O_NO_NULL_CHECK; var t = extractVirtualRef(orig, method), funcNorm = t.0, m = t.1; var ai_new = normArgs(funcNorm, genRefs(i_old.inputs)); if (t.2) { // still a virtual dispatch - if (EnumType.?(rc.oldType)) { + if (V3.isEnum(rc.oldType)) { var sel = IrSelector.!(m.member); if (sel.mtable != null && sel.mtable.record != null) { // enum: tag IS the value; index into mtable @@ -1799,7 +1799,7 @@ class SsaRaNormalizer extends SsaRebuilder { return vals; } def normNullCheck(oldApp: SsaApplyOp, op: Operator) { - if (EnumType.?(op.typeArgs[0])) return; // enum values are never null + if (V3.isEnum(op.typeArgs[0])) return; // enum values are never null var newArgs = genRefs(oldApp.inputs); if (newArgs.length >= 1) addNullCheck(oldApp, newArgs[0]); } @@ -1925,7 +1925,7 @@ class SsaRaNormalizer extends SsaRebuilder { } // For JVM/wasm-gc: box enum tag to Oop for closure receiver slot. private def boxEnumClosureReceiver(rc: RaClass, obj: SsaInstr) -> SsaInstr { - if (EnumType.?(rc.oldType) + if (V3.isEnum(rc.oldType) && (!context.compiler.NormConfig.NonRefClosureReceiver || context.compiler.NormConfig.ExplicitRefTypeCast)) { return curBlock.opTypeSubsume(V3.getVariantTagType(rc.oldType), Oop.TYPE, obj); diff --git a/aeneas/src/ssa/SsaOptimizer.v3 b/aeneas/src/ssa/SsaOptimizer.v3 index 7cedbf69b..007357af8 100644 --- a/aeneas/src/ssa/SsaOptimizer.v3 +++ b/aeneas/src/ssa/SsaOptimizer.v3 @@ -870,7 +870,7 @@ class SsaInstrReducer(context: SsaContext) extends SsaInstrMatcher { var xval = unop(i); if (xconst) { // ClassGetMethod[m](#K) => (m, #K) - var obj: Val = if(EnumType.?(meth.receiver), xval, Record.!(xval)); + var obj: Val = if(V3.isEnum(meth.receiver), xval, Record.!(xval)); return graph.valConst(meth.getBoundType(), Closure.new(obj, meth)); } } @@ -882,7 +882,7 @@ class SsaInstrReducer(context: SsaContext) extends SsaInstrMatcher { if (dv != null) { var xval = unop(i); if (xconst && !i.op.isPolymorphic()) { // ClassGetVirtual[m](K) => (m, K) - var obj: Val = if(EnumType.?(meth.receiver), xval, Record.!(xval)); + var obj: Val = if(V3.isEnum(meth.receiver), xval, Record.!(xval)); return graph.valConst(dv.getBoundType(), Closure.new(obj, dv)); } return replaceOp(i, V3Op.newClassGetMethod(dv)); // ClassGetVirtual[m](K) => ClassGetMethod[m](K) @@ -945,7 +945,7 @@ class SsaInstrReducer(context: SsaContext) extends SsaInstrMatcher { var xval = unop(i); if (xconst) { // VariantGetMethod[m](#K) => (m, #K) - var obj = if(EnumType.?(meth.receiver), xval, Record.!(xval)); + var obj = if(V3.isEnum(meth.receiver), xval, Record.!(xval)); var val = if(!meth.member.flags.M_UNREACHABLE, Closure.new(obj, meth)); return graph.valConst(meth.getBoundType(), val); } @@ -957,7 +957,7 @@ class SsaInstrReducer(context: SsaContext) extends SsaInstrMatcher { if (dv != null) { var xval = unop(i); if (xconst && !i.op.isPolymorphic()) { // VariantGetVirtual[m](K) => (m, K) - var obj = if(EnumType.?(dv.receiver), xval, Record.!(xval)); + var obj = if(V3.isEnum(dv.receiver), xval, Record.!(xval)); var val = if(!dv.member.flags.M_UNREACHABLE, Closure.new(obj, dv)); return graph.valConst(dv.getBoundType(), val); } @@ -1021,7 +1021,7 @@ class SsaInstrReducer(context: SsaContext) extends SsaInstrMatcher { } CallMethod(method) => { i.facts |= method.facts & Fact.O_PURE; - if (EnumType.?(method.receiver)) i.facts |= Fact.O_NO_NULL_CHECK; + if (V3.isEnum(method.receiver)) i.facts |= Fact.O_NO_NULL_CHECK; else if (i.inputs.length > 0 && i.input0().facts.V_NON_ZERO) i.facts |= Fact.O_NO_NULL_CHECK; state.kill(); } @@ -1045,7 +1045,7 @@ class SsaInstrReducer(context: SsaContext) extends SsaInstrMatcher { } CallVariantVirtual(method) => { var receiver = i.input0(), meth = V3Op.extractIrSpec(i.op, method); - if (EnumType.?(method.receiver)) i.facts |= Fact.O_NO_NULL_CHECK; + if (V3.isEnum(method.receiver)) i.facts |= Fact.O_NO_NULL_CHECK; else if (i.input0().facts.V_NON_ZERO) i.facts |= Fact.O_NO_NULL_CHECK; var dv = devirtualize(meth, receiver); state.kill(); @@ -1085,7 +1085,7 @@ class SsaInstrReducer(context: SsaContext) extends SsaInstrMatcher { var spec = V3Op.extractIrSpec(xop, method); newOp = V3Op.newCallMethod(spec); i_recvr = x.input0(); - if (EnumType.?(spec.receiver)) newFacts |= Fact.O_NO_NULL_CHECK; + if (V3.isEnum(spec.receiver)) newFacts |= Fact.O_NO_NULL_CHECK; else newFacts = i.facts & Fact.O_NO_NULL_CHECK; } VariantGetVirtual(method) => { @@ -1093,7 +1093,7 @@ class SsaInstrReducer(context: SsaContext) extends SsaInstrMatcher { var spec = V3Op.extractIrSpec(xop, method); newOp = V3Op.newCallVariantVirtual(spec); i_recvr = x.input0(); - if (EnumType.?(spec.receiver)) newFacts |= Fact.O_NO_NULL_CHECK; + if (V3.isEnum(spec.receiver)) newFacts |= Fact.O_NO_NULL_CHECK; else newFacts = i.facts & Fact.O_NO_NULL_CHECK; } CreateClosure(method) => { @@ -1101,7 +1101,7 @@ class SsaInstrReducer(context: SsaContext) extends SsaInstrMatcher { var spec = V3Op.extractIrSpec(xop, method); newOp = V3Op.newCallMethod(spec); i_recvr = if (V3.isComponent(spec.receiver), graph.nullReceiver(), x.input0()); - if (V3.isComponent(spec.receiver) || EnumType.?(spec.receiver)) newFacts |= Fact.O_NO_NULL_CHECK; + if (V3.isComponent(spec.receiver) || V3.isEnum(spec.receiver)) newFacts |= Fact.O_NO_NULL_CHECK; else newFacts |= x.facts & Fact.O_NO_NULL_CHECK; } _ => { @@ -1118,7 +1118,7 @@ class SsaInstrReducer(context: SsaContext) extends SsaInstrMatcher { if (xconst && FuncVal.?(xval)) { var d = FuncVal.!(xval); if (d == null) return addThrow(i.source, V3Exception.NullCheck); - if (!EnumType.?(d.memberRef.receiver)) { + if (!V3.isEnum(d.memberRef.receiver)) { // CallFunction(#K) => CallMethod var newOp = V3Op.newCallMethod(d.memberRef); var i_recvr: SsaInstr = null; @@ -1446,8 +1446,8 @@ class SsaInstrReducer(context: SsaContext) extends SsaInstrMatcher { if (it.width > 32) return -1; if (!it.signed || x.facts.V_NON_NEGATIVE) return V3.unboxI32(it.max); } - if (EnumType.?(t)) { - return EnumType.!(t).enumDecl.cases.length - 1; + if (V3.isEnum(t)) { + return V3.getDecl(t).cases.length - 1; } return -1; } @@ -1474,7 +1474,7 @@ class SsaInstrReducer(context: SsaContext) extends SsaInstrMatcher { // Try to devirtualize based on the class hierachy first. if (!m.member.flags.M_OVERRIDDEN) return m; // Enum types cannot be devirtualized by record/allocation pattern matching. - if (EnumType.?(m.receiver)) return null; + if (V3.isEnum(m.receiver)) return null; var optag = x.optag(); // Look through TypeSubsumes at a potentially more precise receiver. while (optag == Opcode.TypeSubsume.tag) { From e75935d1aa81fbb17203fdd33573ca647b9f69fc Mon Sep 17 00:00:00 2001 From: Eliot Moss Date: Mon, 20 Apr 2026 13:03:24 -0400 Subject: [PATCH 36/58] Replace EnumType.? with V3 helpers across SSA, backends, evaluator Continue replacing direct EnumType.? checks with V3.isEnum, V3.getDecl across SsaBuilder, VstSsaGen, WasmCodeGen, WasmGcTarget, WasmTarget, JvmGen, SsaJvmGen, and Eval. Remaining EnumType references (71, down from 137) are: - match arms needing the actual EnumType object - Kind.ENUM switch cases - Verifier/MethodEnv/TypeSystem (deeper type-system integration) Co-Authored-By: Claude Opus 4.6 (1M context) --- aeneas/src/core/Eval.v3 | 14 +++++++------- aeneas/src/jvm/JvmGen.v3 | 6 +++--- aeneas/src/jvm/SsaJvmGen.v3 | 4 ++-- aeneas/src/ssa/SsaBuilder.v3 | 8 ++++---- aeneas/src/ssa/VstSsaGen.v3 | 8 ++++---- aeneas/src/wasm/WasmCodeGen.v3 | 12 ++++++------ aeneas/src/wasm/WasmGcTarget.v3 | 10 +++++----- aeneas/src/wasm/WasmTarget.v3 | 4 ++-- 8 files changed, 33 insertions(+), 33 deletions(-) diff --git a/aeneas/src/core/Eval.v3 b/aeneas/src/core/Eval.v3 index 7e7afdeb0..514a58d48 100644 --- a/aeneas/src/core/Eval.v3 +++ b/aeneas/src/core/Eval.v3 @@ -126,9 +126,9 @@ component Eval { return b == null; } CLASS_QUERY, VARIANT_QUERY => { - if (EnumType.?(tt)) { + if (V3.isEnum(tt)) { // enum subtype query: check integer tag is in [tagLo, tagHi] - var decl = EnumType.!(tt).enumDecl; + var decl = V3.getDecl(tt); var tagVal = Int.unbox(val); return tagVal >= decl.tagLo && tagVal <= decl.tagHi; } @@ -817,7 +817,7 @@ def evalOp(op: Operator, args: Arguments) -> Result { } VariantGetMethod(method) => { var ta = args.getTypeArgs(); - if (EnumType.?(ta[0])) { + if (V3.isEnum(ta[0])) { var spec = lookupEnumVirtual(args, method); return Closure.new(args.vals[0], spec); } @@ -828,7 +828,7 @@ def evalOp(op: Operator, args: Arguments) -> Result { } VariantGetVirtual(method) => { var ta = args.getTypeArgs(); - if (EnumType.?(ta[0])) { + if (V3.isEnum(ta[0])) { var spec = lookupEnumVirtual(args, method); return Closure.new(args.vals[0], spec); } @@ -840,7 +840,7 @@ def evalOp(op: Operator, args: Arguments) -> Result { } VariantGetSelector(selector) => { var ta = args.getTypeArgs(); - if (EnumType.?(ta[0])) { + if (V3.isEnum(ta[0])) { var spec = lookupEnumVirtual(args, selector); return FuncVal.new(spec); } @@ -938,7 +938,7 @@ def evalOp(op: Operator, args: Arguments) -> Result { } CallVariantVirtual(method) => { var ta = args.getTypeArgs(); - if (EnumType.?(ta[0])) { + if (V3.isEnum(ta[0])) { var spec = lookupEnumVirtual(args, method); return args.tailCall(spec, args.vals[0], 1, args.vals.length); } @@ -950,7 +950,7 @@ def evalOp(op: Operator, args: Arguments) -> Result { } CallVariantSelector(selector) => { var ta = args.getTypeArgs(); - if (EnumType.?(ta[0])) { + if (V3.isEnum(ta[0])) { var spec = lookupEnumVirtual(args, selector); return args.tailCall(spec, args.vals[0], 1, args.vals.length); } diff --git a/aeneas/src/jvm/JvmGen.v3 b/aeneas/src/jvm/JvmGen.v3 index bc09beeb3..7ae05fa1d 100644 --- a/aeneas/src/jvm/JvmGen.v3 +++ b/aeneas/src/jvm/JvmGen.v3 @@ -165,7 +165,7 @@ class JvmProgram(compiler: Compiler, prog: Program) { var buffer = StringBuilder.new(); buffer.puts(mangleTypeName("V3D_", memberRef.receiver)); buffer.puts("$D"); - var methName = if(EnumType.?(memberRef.receiver), + var methName = if(V3.isEnum(memberRef.receiver), mangleEnumMethod(IrMethod.!(memberRef.member)), V3.mangleIrMember(memberRef.member)); buffer.puts(methName); @@ -600,7 +600,7 @@ class JvmV3ClosureGen extends JvmClassGen { if (memberRef == null) { buildAbstractClass(invoke_meth); } else { - if (V3.isComponent(memberRef.receiver) || EnumType.?(memberRef.receiver)) buildComponentClosure(invoke_meth); + if (V3.isComponent(memberRef.receiver) || V3.isEnum(memberRef.receiver)) buildComponentClosure(invoke_meth); else buildClassClosure(invoke_meth); } } @@ -651,7 +651,7 @@ class JvmV3ClosureGen extends JvmClassGen { var jclass = builder.jprog.jvmClass(memberRef.receiver); invoke_code.locals(invoke_sig.localsSize(true)); builder.emitLoadArguments(invoke_code, methType, methType, 2); - var mname = if(EnumType.?(memberRef.receiver), + var mname = if(V3.isEnum(memberRef.receiver), builder.jprog.mangleEnumMethod(IrMethod.!(memberRef.member)), V3.mangleIrMember(memberRef.member)); invoke_code.invokestatic(jclass.name, mname, jvmSig(methType)); diff --git a/aeneas/src/jvm/SsaJvmGen.v3 b/aeneas/src/jvm/SsaJvmGen.v3 index b3115b133..a4112d575 100644 --- a/aeneas/src/jvm/SsaJvmGen.v3 +++ b/aeneas/src/jvm/SsaJvmGen.v3 @@ -679,9 +679,9 @@ class SsaJvmGen(jprog: JvmProgram, context: SsaContext, jsig: JvmSig, code: JvmC if (methodRef.isNew()) { var nsig = jprog.makeJvmSig(jclass, methodRef.getMethodType(), jclass); code.invokestatic(jclass.name, "$init", nsig); - } else if (V3.isComponent(methodRef.receiver) || EnumType.?(methodRef.receiver)) { + } else if (V3.isComponent(methodRef.receiver) || V3.isEnum(methodRef.receiver)) { var nsig = jprog.jvmSig(methodRef.getMethodType()); - var mname = if(EnumType.?(methodRef.receiver), + var mname = if(V3.isEnum(methodRef.receiver), jprog.mangleEnumMethod(IrMethod.!(methodRef.member)), V3.mangleIrMember(methodRef.member)); code.invokestatic(jclass.name, mname, nsig); diff --git a/aeneas/src/ssa/SsaBuilder.v3 b/aeneas/src/ssa/SsaBuilder.v3 index a92b92e11..2cff1339a 100644 --- a/aeneas/src/ssa/SsaBuilder.v3 +++ b/aeneas/src/ssa/SsaBuilder.v3 @@ -99,20 +99,20 @@ class SsaBuilder { VariantGetName => { var t = op.typeArgs[0]; var record: Record; - if (EnumType.?(t)) record = EnumType.!(t).getNames(context.prog); + if (V3.isEnum(t)) record = EnumType.!(t).getNames(context.prog); else record = V3Class_TypeCon.!(t.typeCon).getNames(context.prog); var array = graph.recordConst(record); - var index = if(EnumType.?(t), args[0], add(V3Op.newVariantGetTag(op.typeArgs[0]), args, N)); + var index = if(V3.isEnum(t), args[0], add(V3Op.newVariantGetTag(op.typeArgs[0]), args, N)); return opArrayGetElem(V3.stringArrayType, V3.getVariantTagType(t), Facts.O_SAFE_BOUNDS | Fact.O_PURE, array, index); } VariantGetShortName => { var t = op.typeArgs[0]; var record: Record; - if (EnumType.?(t)) record = EnumType.!(t).getShortNames(context.prog); + if (V3.isEnum(t)) record = EnumType.!(t).getShortNames(context.prog); else record = V3Class_TypeCon.!(t.typeCon).getShortNames(context.prog); var array = graph.recordConst(record); - var index = if(EnumType.?(t), args[0], add(V3Op.newVariantGetTag(op.typeArgs[0]), args, N)); + var index = if(V3.isEnum(t), args[0], add(V3Op.newVariantGetTag(op.typeArgs[0]), args, N)); return opArrayGetElem(V3.stringArrayType, V3.getVariantTagType(t), Facts.O_SAFE_BOUNDS | Fact.O_PURE, array, index); diff --git a/aeneas/src/ssa/VstSsaGen.v3 b/aeneas/src/ssa/VstSsaGen.v3 index 4ce95da00..66bdfd2fa 100644 --- a/aeneas/src/ssa/VstSsaGen.v3 +++ b/aeneas/src/ssa/VstSsaGen.v3 @@ -291,7 +291,7 @@ class VstSsaGen extends VstVisitor { } def visitForeach(stmt: ForeachStmt, env: VstSsaEnv) -> SsaInstr { var arrayType = stmt.expr.exactType; - if (EnumType.?(arrayType)) return genEnumIteration(stmt, EnumType.!(arrayType), env); + if (V3.isEnum(arrayType)) return genEnumIteration(stmt, EnumType.!(arrayType), env); var arrayExpr = env.genExpr(stmt.expr); if (env.end) return null; // expression generated a throw if (EnumSetType.?(arrayType)) return genEnumSetIteration(stmt, EnumSetType.!(arrayType), arrayExpr, env); @@ -552,9 +552,9 @@ class VstSsaGen extends VstVisitor { // match on the variant's tag key = env.addApply(null, V3Op.newVariantGetTag(keyType), [key]); // XXX: constant fold keyType = V3.classDecl(keyType).tagType; - } else if (EnumType.?(keyType)) { + } else if (V3.isEnum(keyType)) { // match on the enum value (represented by its tag) - keyType = EnumType.!(keyType).enumDecl.tagType; + keyType = V3.getDecl(keyType).tagType; } else if (defcase == null) { // materialize an empty default case if not exhaustive defcase = MatchCase.new(null, null, EmptyStmt.new(null)); @@ -638,7 +638,7 @@ class VstSsaGen extends VstVisitor { if (tagVal >= decl.variantTag && tagVal <= decl.tagHi) return true; } else if (pat.subtypeEnumType != null) { // subtype enum pattern: check if integer tag falls in the subtype enum's DFS range - var decl = EnumType.!(pat.subtypeEnumType).enumDecl; + var decl = V3.getDecl(pat.subtypeEnumType); var tagVal = Int.unbox(val); if (tagVal >= decl.tagLo && tagVal <= decl.tagHi) return true; } else if (pat.vdecl != null) { diff --git a/aeneas/src/wasm/WasmCodeGen.v3 b/aeneas/src/wasm/WasmCodeGen.v3 index 9a56a6a57..22c341b05 100644 --- a/aeneas/src/wasm/WasmCodeGen.v3 +++ b/aeneas/src/wasm/WasmCodeGen.v3 @@ -323,7 +323,7 @@ class WasmCodeGen extends SsaMachGen { dfnAll(i); } else { var rt = i.input1().getType(); - if (rt == m.receiver || EnumType.?(m.receiver)) { + if (rt == m.receiver || V3.isEnum(m.receiver)) { start = 1; // just push the receiver as is dfnAll(i); } else { @@ -361,7 +361,7 @@ class WasmCodeGen extends SsaMachGen { dfnAll(i); } else { var rt = i.input1().getType(); - if (rt == m.receiver || EnumType.?(m.receiver)) { + if (rt == m.receiver || V3.isEnum(m.receiver)) { start = 1; // just push the receiver as is dfnAll(i); } else { @@ -1646,14 +1646,14 @@ class WasmCodeGen extends SsaMachGen { // Enum methods: no receiver to drop/cast; load all params directly. // ============ indirect adapter code ======================== var loopStart = 1; // default: skip first param (Oop/funcref receiver) - if (EnumType.?(m.receiver) && GC) { + if (V3.isEnum(m.receiver) && GC) { // wasm-gc enum: unbox eqref receiver → i31ref → i32 tag b(WasmOp.GET_LOCAL.opcode); w.put_sleb32(0); w.put_b16be(WasmExtOp.REF_CAST.extopcode); w.put_sleb32(int.!(WasmTypeConCode.I31REF.val)); w.put_b16be(WasmExtOp.I31_GET_S.extopcode); loopStart = 1; // skip eqref(0); user args at 1+ (tag was stripped from adapter sig) - } else if (EnumType.?(m.receiver)) { + } else if (V3.isEnum(m.receiver)) { loopStart = 0; // wasm (non-gc) enum: no receiver to skip } else if (GC) { var rcvr = m.receiver; @@ -1688,12 +1688,12 @@ class WasmCodeGen extends SsaMachGen { // arguments to the target function // ============ dispatch adapter code ======================== b(WasmOp.GET_LOCAL.opcode); w.put_sleb32(0); - if (EnumType.?(m.receiver) && GC) { + if (V3.isEnum(m.receiver) && GC) { // wasm-gc enum: unbox eqref → i31ref → i32 tag w.put_b16be(WasmExtOp.REF_CAST.extopcode); w.put_sleb32(int.!(WasmTypeConCode.I31REF.val)); w.put_b16be(WasmExtOp.I31_GET_S.extopcode); - } else if (!EnumType.?(m.receiver)) { + } else if (!V3.isEnum(m.receiver)) { w.put_b16be(WasmExtOp.REF_CAST_NULL.extopcode); var wht = wasmGc.gcTypeTable.addHeapType(m.receiver); w.put_sleb32(int.!(wht.index - 1)); diff --git a/aeneas/src/wasm/WasmGcTarget.v3 b/aeneas/src/wasm/WasmGcTarget.v3 index 5afe949be..b75f800f1 100644 --- a/aeneas/src/wasm/WasmGcTarget.v3 +++ b/aeneas/src/wasm/WasmGcTarget.v3 @@ -354,7 +354,7 @@ class WasmGcTarget extends Target { wasm.indirectAdapterVector.put(meth); var adapterRecv = Oop.TYPE; var adapterSig = meth.sig; - if (EnumType.?(meth.receiver) && meth.source != null && VstMethod.?(meth.source)) { + if (V3.isEnum(meth.receiver) && meth.source != null && VstMethod.?(meth.source)) { // wasm-gc enum user method: strip tag from sig (tag comes from eqref receiver) adapterSig = Signature.new(null, Arrays.range(meth.sig.paramTypes, 1, meth.sig.paramTypes.length), meth.sig.returnTypes); } @@ -419,7 +419,7 @@ class WasmGcTarget extends Target { wasm.indirectAdapterFor[m] = findex; var adapterRecv = Oop.TYPE; var adapterSig = m.sig; - if (EnumType.?(m.receiver) && m.source != null && VstMethod.?(m.source)) { + if (V3.isEnum(m.receiver) && m.source != null && VstMethod.?(m.source)) { adapterSig = Signature.new(null, Arrays.range(m.sig.paramTypes, 1, m.sig.paramTypes.length), m.sig.returnTypes); } var sigIdx = wasm.addSig(adapterRecv, adapterSig); @@ -464,7 +464,7 @@ class WasmGcTarget extends Target { wasm.dispatchTableFor[mtable] = tableNum; var root = mtable.root; - var dispRecv = if(EnumType.?(root.receiver), Void.TYPE, root.receiver); + var dispRecv = if(V3.isEnum(root.receiver), Void.TYPE, root.receiver); var sigIdx = wasm.addSig(dispRecv, root.sig); // make sure signature is present var superIdx = root.machIndex; // superIdx *can* be -1 (e.g., for an ADT with an empty-body default for a method) @@ -1328,7 +1328,7 @@ class WasmGcProgram extends WasmProgram { } // override def getSigIndex(m: IrMethod) -> u32 { - var recv = if(EnumType.?(m.receiver), Void.TYPE, m.receiver); + var recv = if(V3.isEnum(m.receiver), Void.TYPE, m.receiver); var sig = getSig(recv, m.sig); var wft = gcTypeTable.addSignature(sig); var index: u32 = if(wft == null, 0, wft.index); @@ -1423,7 +1423,7 @@ class WasmGcProgram extends WasmProgram { } def addMethodGc(m: IrMethod, infos: Vector, imported: bool) { // For enum methods, the tag is already the first param in m.sig (no receiver prepend needed) - var recv = if(EnumType.?(m.receiver), Void.TYPE, m.ssa.params[0].vtype); + var recv = if(V3.isEnum(m.receiver), Void.TYPE, m.ssa.params[0].vtype); var sigIdx = addSig(recv, m.sig); m.machIndex = infos.length; var fi = FunctionInfo.new(if(imported, FunctionInfoKind.Import, FunctionInfoKind.Method), m.receiver, m.sig, sigIdx, m, infos.length); diff --git a/aeneas/src/wasm/WasmTarget.v3 b/aeneas/src/wasm/WasmTarget.v3 index 97521d505..67ea24806 100644 --- a/aeneas/src/wasm/WasmTarget.v3 +++ b/aeneas/src/wasm/WasmTarget.v3 @@ -522,7 +522,7 @@ class WasmProgram(mach: MachProgram, context: SsaContext, test: bool, wasmGc: bo } def getSigIndex(m: IrMethod) -> u32 { var sig = m.sig; - if (!V3.isComponent(m.receiver) && !EnumType.?(m.receiver)) { + if (!V3.isComponent(m.receiver) && !V3.isEnum(m.receiver)) { sig = Signature.new(null, Arrays.prepend(m.ssa.params[0].vtype, sig.paramTypes), sig.returnTypes); } return typeTable.add(sig); @@ -607,7 +607,7 @@ class WasmProgram(mach: MachProgram, context: SsaContext, test: bool, wasmGc: bo def setMethod(index: int, m: IrMethod, kind: WasmFuncKind) -> int { if (m != null) { m.machIndex = index; - var recv = if(EnumType.?(m.receiver), Void.TYPE, m.ssa.params[0].vtype); + var recv = if(V3.isEnum(m.receiver), Void.TYPE, m.ssa.params[0].vtype); addSig(recv, m.sig); } functions.set(index, (m, kind)); From 8d0613f33a1c1b1c0dfcc166125059aab995a400 Mon Sep 17 00:00:00 2001 From: Eliot Moss Date: Mon, 20 Apr 2026 13:13:45 -0400 Subject: [PATCH 37/58] Fix RaClass field copy and numberVariant DFS for enum subtypes Two issues from IrClass parent inheritance for enums: 1. RaClass constructor copied parent fields into child via Arrays.copyInto, but enum child IrClass has no inherited fields (skipped in IrBuilder). BoundsCheckException when parent has more fields than child. Fix: skip field copy for enum types. 2. numberVariant walked rc.children for DFS classId assignment, but enum subtypes (now in children via parent link) use tag ranges, not variant-style classId DFS. BoundsCheckException when trying to assign classIds using enum tag ranges. Fix: treat enums like leaves in numberVariant; tag range handling stays in fillEnumMtable. Co-Authored-By: Claude Opus 4.6 (1M context) --- aeneas/src/ir/Normalization.v3 | 4 ++-- aeneas/src/ir/Reachability.v3 | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/aeneas/src/ir/Normalization.v3 b/aeneas/src/ir/Normalization.v3 index 12f1521af..69df1da63 100644 --- a/aeneas/src/ir/Normalization.v3 +++ b/aeneas/src/ir/Normalization.v3 @@ -322,8 +322,8 @@ class ReachabilityNormalizer(config: NormalizerConfig, ra: ReachabilityAnalyzer) // number a variant and children consistent with tagging order def numberVariant(rc: RaClass) { rc.minClassId = liveClasses.length; - if (rc.children == null) { - liveClasses.put(rc); // special case of a data type + if (rc.children == null || V3.isEnum(rc.oldType)) { + liveClasses.put(rc); // leaf or enum (enums use tag ranges, not classId DFS) } else { for (l = rc.children; l != null; l = l.tail) numberVariantSlot(l.head, rc.minClassId); } diff --git a/aeneas/src/ir/Reachability.v3 b/aeneas/src/ir/Reachability.v3 index 01fa81e05..0b018cac3 100644 --- a/aeneas/src/ir/Reachability.v3 +++ b/aeneas/src/ir/Reachability.v3 @@ -808,7 +808,7 @@ class RaClass extends RaType { newIrType = oldType; if (parent != null) { parent.children = List.new(this, parent.children); - Arrays.copyInto(parent.fields, fields, 0); + if (!V3.isEnum(oldType)) Arrays.copyInto(parent.fields, fields, 0); } if (oldType.typeCon.kind != Kind.VARIANT) recursive = 1; // not a variant, can't be recursive } From ef531445d0ff5a41f901d7e415c34d4cd46a2121 Mon Sep 17 00:00:00 2001 From: Eliot Moss Date: Tue, 21 Apr 2026 00:18:24 -0400 Subject: [PATCH 38/58] Fix EnumType/ClassType match ordering and restore resolveEnumMethodImpl Since EnumType extends ClassType, match arms and type checks that list ClassType before EnumType incorrectly match enums via the ClassType path. This caused three bugs in enum subtype virtual dispatch: 1. makeIrClass: enum subtypes went through newIrClassWithSuper (class path) instead of the enum path with parentEnum linkage, so M_OVERRIDDEN was never set and virtual dispatch was never generated. 2. V3.getTagLo/getTagHi/getVariantTagType: enum subtypes got wrong tag ranges (variantTag instead of tagLo), corrupting the mtable layout. 3. resolveMethodImpl (index-based) can't find enum overrides because they use source-identity matching via VstMethod.root. Restore the dedicated resolveEnumMethodImpl for fillEnumMtable. Fix: reorder match arms to check EnumType before ClassType in V3.v3 and Ir.v3. All enum tests pass on v3i, x86-linux, x86-64-linux, jvm, wasm, wasm-gc. Co-Authored-By: Claude Opus 4.6 (1M context) --- aeneas/src/core/Eval.v3 | 8 ++++---- aeneas/src/debug/Dwarf.v3 | 2 +- aeneas/src/ir/Ir.v3 | 12 ++++++------ aeneas/src/ir/Normalization.v3 | 15 ++++++++++++++- aeneas/src/ir/Packing.v3 | 19 +++++++++++++------ aeneas/src/ir/SsaNormalizer.v3 | 26 +++++++++++++------------- aeneas/src/jvm/JvmRep.v3 | 2 +- aeneas/src/jvm/SsaJvmGen.v3 | 2 +- aeneas/src/mach/MachProgram.v3 | 4 +++- aeneas/src/ssa/SsaBuilder.v3 | 2 +- aeneas/src/ssa/VstSsaGen.v3 | 6 +++--- aeneas/src/v3/TypeSystem.v3 | 30 +++++++++++++++--------------- aeneas/src/v3/V3.v3 | 15 +++++++++++---- aeneas/src/v3/V3Class.v3 | 3 +-- aeneas/src/v3/V3Enum.v3 | 23 ++++++++++++++--------- aeneas/src/vst/MethodEnv.v3 | 14 +++++++------- aeneas/src/vst/Verifier.v3 | 28 ++++++++++++++-------------- aeneas/src/wasm/WasmGcTarget.v3 | 2 +- aeneas/src/wasm/WasmTarget.v3 | 2 +- aeneas/src/x86-64/SsaX86_64Gen.v3 | 2 +- 20 files changed, 125 insertions(+), 92 deletions(-) diff --git a/aeneas/src/core/Eval.v3 b/aeneas/src/core/Eval.v3 index 514a58d48..4bda2e2cf 100644 --- a/aeneas/src/core/Eval.v3 +++ b/aeneas/src/core/Eval.v3 @@ -1080,9 +1080,9 @@ def evalOp(op: Operator, args: Arguments) -> Result { return args.unimplemented(); } -def doRefLayoutGetField(args: Arguments, fieldType: PrimType, ref: ByteArrayOffset, offset: int, order: ByteOrder) -> Result { +def doRefLayoutGetField(args: Arguments, fieldType: Type, ref: ByteArrayOffset, offset: int, order: ByteOrder) -> Result { if (ref == null || ref.array == null) return args.throw(V3Exception.NullCheck, null); - var v = ref.read(order, offset, fieldType.packedByteSize); + var v = ref.read(order, offset, if(PrimType.?(fieldType), PrimType.!(fieldType).packedByteSize, EnumType.!(fieldType).byteSize)); match (fieldType) { x: BoolType => { return Bool.box((v & 1) != 0); @@ -1098,7 +1098,7 @@ def doRefLayoutGetField(args: Arguments, fieldType: PrimType, ref: ByteArrayOffs } } x: EnumType => { - if (v >= x.enumDecl.cases.length) v = 0; // out-of-bounds tag => 0 + if (v >= x.classDecl.cases.length) v = 0; // out-of-bounds tag => 0 return Int.box(int.view(v)); // note: no long enum values } x: FloatType => { @@ -1184,7 +1184,7 @@ def lookupEnumVirtual(args: Arguments, member: IrMember) -> IrSpec { var ta = args.getTypeArgs(); var enumType = EnumType.!(ta[0]); var tag = Int.unbox(args.vals[0]); - var targetDecl = findEnumDeclForTag(enumType.enumDecl, tag); + var targetDecl = findEnumDeclForTag(enumType.classDecl, tag); var ir = args.getProgram().ir; var m: IrMethod; var vc = targetDecl; diff --git a/aeneas/src/debug/Dwarf.v3 b/aeneas/src/debug/Dwarf.v3 index a8b2ae418..4117f8df3 100644 --- a/aeneas/src/debug/Dwarf.v3 +++ b/aeneas/src/debug/Dwarf.v3 @@ -360,7 +360,7 @@ class DwarfInfoSection(abbrev: DwarfAbbrevSection) { buf.send(w.putr); w.putb(0); w.putb(t.byteSize); - for (c in t.enumDecl.cases) { + for (c in t.classDecl.cases) { w.put_uleb32(abbrev.getAbbrev(DwarfAbbrevTag.Enumurator)); buf.reset(); c.render(buf); diff --git a/aeneas/src/ir/Ir.v3 b/aeneas/src/ir/Ir.v3 index aa04faa21..d32c9d377 100644 --- a/aeneas/src/ir/Ir.v3 +++ b/aeneas/src/ir/Ir.v3 @@ -390,16 +390,16 @@ class IrModule { if (ctype == null) return null; var ic = classMap[ctype]; if (ic == null) { - if (V3.isClass(ctype)) { - ic = newIrClassWithSuper(ctype, V3.classDecl(ctype)); - } else if (V3.isComponent(ctype)) { - ic = newIrClass(ctype, null, V3.componentDecl(ctype)); - classMap[ctype] = ic; - } else if (V3.isEnum(ctype)) { + if (V3.isEnum(ctype)) { var decl = V3.getDecl(ctype); var sc = if(decl.parentEnum != null, makeIrClass(decl.parentEnum.getDeclaredType())); ic = newIrClass(ctype, sc, decl); classMap[ctype] = ic; + } else if (V3.isClass(ctype)) { + ic = newIrClassWithSuper(ctype, V3.classDecl(ctype)); + } else if (V3.isComponent(ctype)) { + ic = newIrClass(ctype, null, V3.componentDecl(ctype)); + classMap[ctype] = ic; } } return ic; diff --git a/aeneas/src/ir/Normalization.v3 b/aeneas/src/ir/Normalization.v3 index 69df1da63..fc45d723e 100644 --- a/aeneas/src/ir/Normalization.v3 +++ b/aeneas/src/ir/Normalization.v3 @@ -663,7 +663,7 @@ class ReachabilityNormalizer(config: NormalizerConfig, ra: ReachabilityAnalyzer) setMtableEntry(mtable, impl, rc.minClassId - mtable.rootId); } private def fillEnumMtable(mtable: IrMtable, rc: RaClass, rm: RaMethod) { - var impl = resolveMethodImpl(rc, rm); + var impl = resolveEnumMethodImpl(rc, rm); if (impl == null) impl = rm; // fall back to root var tagLo = V3.getTagLo(rc.oldType); var tagHi = V3.getTagHi(rc.oldType); @@ -691,6 +691,19 @@ class ReachabilityNormalizer(config: NormalizerConfig, ra: ReachabilityAnalyzer) } return null; } + // Resolve an enum method by matching VstMethod.root identity, since enum + // subtype IrClasses don't inherit parent's method array indices. + private def resolveEnumMethodImpl(rc: RaClass, rm: RaMethod) -> RaMethod { + var rootSource = rm.orig.source; + for (ml in rc.methods) { + for (l = ml; l != null; l = l.tail) { + var m = l.head; + if (m.orig.source == rootSource) return m; + if (VstMethod.?(m.orig.source) && VstMethod.!(m.orig.source).root == rootSource) return m; + } + } + return null; + } def normValIntoArray(v: Val, tn: TypeNorm, array: Array, index: int) { match (v) { null => ; diff --git a/aeneas/src/ir/Packing.v3 b/aeneas/src/ir/Packing.v3 index 6fab11bce..4c5d4f1bb 100644 --- a/aeneas/src/ir/Packing.v3 +++ b/aeneas/src/ir/Packing.v3 @@ -14,7 +14,7 @@ // The last elements are stored at the least significant bits // Intuitively, convention allows us to read the class as a binary number 0b..... class SsaPacker(builder: SsaBuilder, result: IntType) { - def scalars = Vector<(PrimType, int, SsaInstr)>.new(); + def scalars = Vector<(Type, int, SsaInstr)>.new(); var pos: int; def flatten(ft: Type, input: SsaInstr) { @@ -36,6 +36,10 @@ class SsaPacker(builder: SsaBuilder, result: IntType) { if (x.width > 0) scalars.put(x, pos, input); pos += x.width; } + x: EnumType => { + if (x.width > 0) scalars.put(x, pos, input); + pos += x.width; + } x: ClassType => { // pack the fields of this class type var ic = builder.context.prog.ir.makeIrClass(x); // XXX: makeIrClass seems wrong after normalization @@ -60,13 +64,13 @@ class SsaPacker(builder: SsaBuilder, result: IntType) { x: IntType => { if (j < scalars.length - 1 && x.signed) { // Interior signed integers need to be converted to unsigned first to prevent sign pollution. - var ut = Int.getType(false, ft.width); + var ut = Int.getType(false, V3.getWidth(ft)); i = builder.opIntViewI0(ft, ut, i); ft = ut; } } x: EnumType => { - ft = x.enumDecl.tagType; + ft = x.classDecl.tagType; } x: EnumSetType => { ft = x.repType; @@ -139,7 +143,7 @@ class SsaUnpacker(builder: SsaBuilder, ft: IntType) { unpackedBuffer.push(builder.add(op, [shiftedInput], Facts.NONE)); } else { // IntView as a 32 bit integer first if width is larger than 32 - shiftedInput = if(ft.width > 32, builder.opIntViewI0(ft, Int.TYPE, shiftedInput), shiftedInput); + shiftedInput = if(V3.getWidth(ft) > 32, builder.opIntViewI0(ft, Int.TYPE, shiftedInput), shiftedInput); def op = V3Op.newFloat32ViewI(Int.TYPE); unpackedBuffer.push(builder.add(op, [shiftedInput], Facts.NONE)); } @@ -147,9 +151,9 @@ class SsaUnpacker(builder: SsaBuilder, ft: IntType) { pos += x.width; } x: EnumType => { - def tagType: IntType = x.enumDecl.tagType; + def tagType: IntType = x.classDecl.tagType; def unpackedTag: SsaInstr = builder.opIntViewI0(ft, tagType, shiftedInput); - def numCases = x.enumDecl.cases.length; + def numCases = x.classDecl.cases.length; def cond: SsaInstr = builder.addApply(builder.source, tagType.opLt(), [unpackedTag, builder.graph.intConst(numCases)]); // Valid tag case def trueBlock = SsaBlock.new(); @@ -230,6 +234,9 @@ component PackingUtils { } return (width, null); } + x: EnumType => { + return (x.width, null); + } x: ClassType => { if (x.typeCon.kind != Kind.VARIANT) return (int.max, Strings.format1("because %q is a class", x.render)); for (l = seen; l != null; l = l.tail) { // check for recursive type diff --git a/aeneas/src/ir/SsaNormalizer.v3 b/aeneas/src/ir/SsaNormalizer.v3 index 625e87fb8..d9b90348e 100644 --- a/aeneas/src/ir/SsaNormalizer.v3 +++ b/aeneas/src/ir/SsaNormalizer.v3 @@ -468,18 +468,18 @@ class SsaRaNormalizer extends SsaRebuilder { var result: SsaInstr; match (fn.oldType) { - x: BoolType, - x: IntType, - x: FloatType => result = curBlock.opByteArrayGetField(fn.oldType, rangeStartType, offset, order, facts, array, start); x: EnumType => { - var it = IntType.!(x.enumDecl.tagType); + var it = IntType.!(x.classDecl.tagType); var wt = Int.getType(false, it.byteSize * 8); result = curBlock.opByteArrayGetField(wt, rangeStartType, offset, order, facts, array, start); - var caseCount = newGraph.intConst(x.enumDecl.cases.length); + var caseCount = newGraph.intConst(x.classDecl.cases.length); var inBound = curBlock.opIntULt(norm.config.ArrayLengthType, it, result, caseCount); result = curBlock.opIntViewI0(it, wt, result); result = curBlock.addSelect(it, inBound, result, newGraph.nullConst(it)); } + x: BoolType, + x: IntType, + x: FloatType => result = curBlock.opByteArrayGetField(fn.oldType, rangeStartType, offset, order, facts, array, start); _ => context.fail1("unexpected type %q", fn.oldType.render); } return map1(i_old, result); @@ -492,14 +492,14 @@ class SsaRaNormalizer extends SsaRebuilder { var result: SsaInstr; match (fn.oldType) { - x: BoolType, - x: IntType, - x: FloatType => result = curBlock.opByteArraySetField(fn.oldType, rangeStartType, offset, order, facts, array, start, val); x: EnumType => { - var it = IntType.!(x.enumDecl.tagType); + var it = IntType.!(x.classDecl.tagType); var wt = Int.getType(false, it.byteSize * 8); result = curBlock.opByteArraySetField(wt, rangeStartType, offset, order, facts, array, start, val); } + x: BoolType, + x: IntType, + x: FloatType => result = curBlock.opByteArraySetField(fn.oldType, rangeStartType, offset, order, facts, array, start, val); } return map1(i_old, result); } @@ -522,9 +522,9 @@ class SsaRaNormalizer extends SsaRebuilder { x: IntType => result = curBlock.opByteArrayGetField(x, rangeStartType, offset, order, facts, array, start); x: FloatType => result = curBlock.opByteArrayGetField(x, rangeStartType, offset, order, facts, array, start); x: EnumType => { - var it = IntType.!(x.enumDecl.tagType); + var it = IntType.!(x.classDecl.tagType); result = curBlock.opByteArrayGetField(it, rangeStartType, offset, order, facts, array, start); - var caseCount = newGraph.intConst(x.enumDecl.cases.length); + var caseCount = newGraph.intConst(x.classDecl.cases.length); var inBound = curBlock.opIntULt(it, it, result, caseCount); result = curBlock.addSelect(it, inBound, result, newGraph.nullConst(it)); } @@ -551,7 +551,7 @@ class SsaRaNormalizer extends SsaRebuilder { x: IntType => result = curBlock.opByteArraySetField(fn.oldType, rangeStartType, offset, order, facts, array, start, val); x: FloatType => result = curBlock.opByteArraySetField(fn.oldType, rangeStartType, offset, order, facts, array, start, val); x: EnumType => { - var it = IntType.!(x.enumDecl.tagType); + var it = IntType.!(x.classDecl.tagType); result = curBlock.opByteArraySetField(it, rangeStartType, offset, order, facts, array, start, val); } } @@ -1760,7 +1760,7 @@ class SsaRaNormalizer extends SsaRebuilder { else return curBlock.addApply(curBlock.source, V3Op.newFloat32ViewI(ift), [scalar]); } x: EnumType => { - scalar = curBlock.opIntView(ift, x.enumDecl.tagType, scalar); + scalar = curBlock.opIntView(ift, x.classDecl.tagType, scalar); return scalar; } x: EnumSetType => { diff --git a/aeneas/src/jvm/JvmRep.v3 b/aeneas/src/jvm/JvmRep.v3 index 5f4fb1410..26bdfc306 100644 --- a/aeneas/src/jvm/JvmRep.v3 +++ b/aeneas/src/jvm/JvmRep.v3 @@ -172,7 +172,7 @@ class JvmTypeReps(jprog: JvmProgram) { def zeroExtendSignedByte(t: Type, code: JvmCodeBuilder) { if (t == Byte.TYPE) return code.inl_izx(8); if (EnumType.?(t)) { - t = EnumType.!(t).enumDecl.tagType; + t = EnumType.!(t).classDecl.tagType; if (t == Byte.TYPE) return code.inl_izx(8); } } diff --git a/aeneas/src/jvm/SsaJvmGen.v3 b/aeneas/src/jvm/SsaJvmGen.v3 index a4112d575..91c8afbe9 100644 --- a/aeneas/src/jvm/SsaJvmGen.v3 +++ b/aeneas/src/jvm/SsaJvmGen.v3 @@ -130,7 +130,7 @@ class SsaJvmGen(jprog: JvmProgram, context: SsaContext, jsig: JvmSig, code: JvmC match (t.typeCon.kind) { INT => return IntType.!(t); VARIANT => return V3.getVariantTagType(t); - ENUM => return EnumType.!(t).enumDecl.tagType; + ENUM => return EnumType.!(t).classDecl.tagType; ENUM_SET => return EnumSetType.!(t).repType; _ => context.fail("expected integral type"); } diff --git a/aeneas/src/mach/MachProgram.v3 b/aeneas/src/mach/MachProgram.v3 index 18e67016a..dbe73bfd9 100644 --- a/aeneas/src/mach/MachProgram.v3 +++ b/aeneas/src/mach/MachProgram.v3 @@ -350,7 +350,9 @@ class MachProgram extends TargetProgram { CLASS, ARRAY, OOP => return refSize; - VARIANT => return if (prog.ir.isEnum(t), V3.getVariantTagType(t).byteSize, refSize); + ENUM => return V3.getVariantTagType(t).byteSize; +// VARIANT => return if (prog.ir.isEnum(t), V3.getVariantTagType(t).byteSize, refSize); + VARIANT => return refSize; ANYFUNC, FUNCREF => return code.addressSize; RANGE_START, diff --git a/aeneas/src/ssa/SsaBuilder.v3 b/aeneas/src/ssa/SsaBuilder.v3 index 2cff1339a..efe8db2e5 100644 --- a/aeneas/src/ssa/SsaBuilder.v3 +++ b/aeneas/src/ssa/SsaBuilder.v3 @@ -119,7 +119,7 @@ class SsaBuilder { // var t = EnumType.!(op.typeArgs[0]); // var record = t.getShortNames(context.prog); // var array = graph.recordConst(record); -// var tagType = IntType.!(t.enumDecl.tagType); +// var tagType = IntType.!(t.classDecl.tagType); // return opArrayGetElem(V3.stringArrayType, tagType, // Facts.O_SAFE_BOUNDS | Fact.O_PURE, array, args[0]); } diff --git a/aeneas/src/ssa/VstSsaGen.v3 b/aeneas/src/ssa/VstSsaGen.v3 index 66bdfd2fa..47720bf13 100644 --- a/aeneas/src/ssa/VstSsaGen.v3 +++ b/aeneas/src/ssa/VstSsaGen.v3 @@ -149,7 +149,7 @@ class VstSsaGen extends VstVisitor { while (rootEnum.qualifiers != null) { var tc = context.prog.typeEnv.lookup(rootEnum.qualifiers.head.image); if (tc == null || tc.kind != Kind.ENUM) break; - rootEnum = EnumType.!(tc.create0()).enumDecl; + rootEnum = EnumType.!(tc.create0()).classDecl; } // Count total params: root + all ancestor extras + own extras var totalParams = 0; @@ -347,7 +347,7 @@ class VstSsaGen extends VstVisitor { varDecl.vtype = Int.TYPE; varDecl.ssa = stmt.loopVar; nonSsaVars[stmt.loopVar.index] = varDecl; - var limitExpr = graph.intConst(enumType.enumDecl.cases.length); + var limitExpr = graph.intConst(enumType.classDecl.cases.length); env.at(exprSource(stmt.expr)); var loopCond = VstSsaMerge.new(this); // merge point for start of loop @@ -364,7 +364,7 @@ class VstSsaGen extends VstVisitor { loopEnv.addIf(cond, bodyEnv.block, loopEnv.loopEnd.block); loopEnd.mergeIncoming(loopEnv); // convert int to the enum element with the same tag - var elem = bodyEnv.opIntViewI0(Int.TYPE, enumType.enumDecl.tagType, loopEnv[stmt.loopVar]); + var elem = bodyEnv.opIntViewI0(Int.TYPE, enumType.classDecl.tagType, loopEnv[stmt.loopVar]); bodyEnv[stmt.varDecl.ssa] = elem; // generate the body diff --git a/aeneas/src/v3/TypeSystem.v3 b/aeneas/src/v3/TypeSystem.v3 index 7df7495ca..3ee744da4 100644 --- a/aeneas/src/v3/TypeSystem.v3 +++ b/aeneas/src/v3/TypeSystem.v3 @@ -34,7 +34,7 @@ component TypeSystem { // Check if the type {xt} is an enum and the type {yt} is its corresponding set type. def isEnumToSet(xt: Type, yt: Type) -> bool { if (EnumType.?(xt) && EnumSetType.?(yt)) { - var xd = EnumType.!(xt).enumDecl; + var xd = EnumType.!(xt).classDecl; var yd = EnumSetType.!(yt).enumDecl; return xd == yd || isEnumSubtypeOf(xd, yd); } @@ -65,6 +65,10 @@ component TypeSystem { y: FuncType => return Conversion.SUBSUME_NULL_FUNC; } } + x: EnumType => match (yt) { + y: EnumSetType => if (x.classDecl == y.enumDecl || isEnumSubtypeOf(x.classDecl, y.enumDecl)) return Conversion.PROMOTE_ENUM_TO_SET; + y: EnumType => if (isEnumSubtypeOf(x.classDecl, y.classDecl)) return Conversion.SUBSUME_VARIANT; + } x: ClassType => match (yt) { y: ClassType => { for (sx = x; sx != null; sx = sx.getSuperType()) { @@ -116,10 +120,6 @@ component TypeSystem { } } } - x: EnumType => match (yt) { - y: EnumSetType => if (x.enumDecl == y.enumDecl || isEnumSubtypeOf(x.enumDecl, y.enumDecl)) return Conversion.PROMOTE_ENUM_TO_SET; - y: EnumType => if (isEnumSubtypeOf(x.enumDecl, y.enumDecl)) return Conversion.SUBSUME_VARIANT; - } x: FloatType => match (yt) { y: FloatType => if (x.width <= y.width) return Conversion.PROMOTE_FLOAT_TO_FLOAT; } @@ -136,6 +136,14 @@ component TypeSystem { if (TypeVarType.?(xt)) return Conversion.CAST_POLY; match (xt) { + x: EnumType => match (yt) { + y: EnumType => { + if (isEnumSubtypeOf(y.classDecl, x.classDecl)) { + return Conversion.CAST_VARIANT; + } + return Conversion.ILLEGAL; + } + } x: ClassType => match(yt) { y: ClassType => { if (x.typeCon.kind != y.typeCon.kind) return Conversion.ILLEGAL; @@ -228,14 +236,6 @@ component TypeSystem { return Conversion.ILLEGAL_EXCEPT_NULL; } } - x: EnumType => match (yt) { - y: EnumType => { - if (isEnumSubtypeOf(y.enumDecl, x.enumDecl)) { - return Conversion.CAST_VARIANT; - } - return Conversion.ILLEGAL; - } - } x: OopType => match (yt) { y: ArrayType => return Conversion.CAST_ARRAY; y: ClassType => return if(V3.isVariant(y), Conversion.CAST_VARIANT, Conversion.CAST_CLASS); @@ -310,10 +310,10 @@ component TypeSystem { } } x: EnumType => match (t2) { - y: EnumSetType => if(x.enumDecl == y.enumDecl) return unifyPromotable(dir, x, y); + y: EnumSetType => if(x.classDecl == y.enumDecl) return unifyPromotable(dir, x, y); } x: EnumSetType => match (t2) { - y: EnumType => if(x.enumDecl == y.enumDecl) return unifyPromotable(dir, y, x); + y: EnumType => if(x.enumDecl == y.classDecl) return unifyPromotable(dir, y, x); } x: FloatType => match (t2) { y: IntType => { diff --git a/aeneas/src/v3/V3.v3 b/aeneas/src/v3/V3.v3 index 2a2c5e74a..1f3c2356c 100644 --- a/aeneas/src/v3/V3.v3 +++ b/aeneas/src/v3/V3.v3 @@ -97,8 +97,8 @@ component V3 { } def getVariantTagType(t: Type) -> IntType { match (t) { + x: EnumType => return x.classDecl.tagType; x: ClassType => return ClassType.!(getRootType(t)).classDecl.tagType; - x: EnumType => return x.enumDecl.tagType; _ => return null; } } @@ -106,22 +106,29 @@ component V3 { def isHierarchical(t: Type) -> bool { return ClassType.?(t) || EnumType.?(t); } def getDecl(t: Type) -> VstClass { match (t) { + x: EnumType => return x.classDecl; x: ClassType => return x.classDecl; - x: EnumType => return x.enumDecl; _ => return null; } } + def getWidth(t: Type) -> byte { + match (t) { + x: PrimType => return x.width; + x: EnumType => return x.width; + _ => return 0; + } + } def getTagLo(t: Type) -> int { match (t) { + x: EnumType => return x.classDecl.tagLo; x: ClassType => return x.classDecl.variantTag; - x: EnumType => return x.enumDecl.tagLo; _ => return 0; } } def getTagHi(t: Type) -> int { match (t) { + x: EnumType => return x.classDecl.tagHi; x: ClassType => return x.classDecl.tagHi; - x: EnumType => return x.enumDecl.tagHi; _ => return 0; } } diff --git a/aeneas/src/v3/V3Class.v3 b/aeneas/src/v3/V3Class.v3 index a8a2d6d9f..3ca417fb5 100644 --- a/aeneas/src/v3/V3Class.v3 +++ b/aeneas/src/v3/V3Class.v3 @@ -67,7 +67,7 @@ class ClassType extends Type { var depth = -1; // depth from root, to speed up commonSuperClass() var typeArgs: TypeArgs; - new(hash: int, classDecl, typeCon: V3Class_TypeCon, nested: List) super(hash, typeCon, nested) { } + new(hash: int, classDecl, typeCon: TypeCon, nested: List) super(hash, typeCon, nested) { } def getSuperType() -> ClassType { if (superType != null) return superType; @@ -81,7 +81,6 @@ class ClassType extends Type { if (classDecl.typeParams != null && nested != null) { var generic = ClassType.!(classDecl.getDeclaredType()); if (generic != this && generic != null && generic.superType != null) { -// return this.superType = ClassType.!(generic.superType.classDecl.getDeclaredType()); var superType = generic.superType.substitute(getTypeArgs()); return this.superType = ClassType.!(superType); } diff --git a/aeneas/src/v3/V3Enum.v3 b/aeneas/src/v3/V3Enum.v3 index 4f7e29ac2..375e5934f 100644 --- a/aeneas/src/v3/V3Enum.v3 +++ b/aeneas/src/v3/V3Enum.v3 @@ -2,16 +2,21 @@ // See LICENSE for details of Apache 2.0 license. // The type for user-declared enums. -class EnumType extends PrimType { - def enumDecl: VstClass; +class EnumType extends ClassType { + var width: byte; + var byteSize: byte; var setType: EnumSetType; var names: Record; var shortNames: Record; - new(hash: int, enumDecl, typeCon: TypeCon) super(hash, typeCon, enumDecl.tagType.width, null) { } - + new(hash: int, classDecl: VstClass, typeCon: TypeCon) + super(hash, classDecl, typeCon, null) { + var w = classDecl.tagType.width; + width = w; + byteSize = if(w == 0, byte.view(0), if(w <= 8, byte.view(1), if(w <= 16, byte.view(2), if(w <= 32, byte.view(4), byte.view(8))))); + } def lookupCase(name: string) -> VstCaseMember { - var member = enumDecl.memberMap[name]; + var member = classDecl.memberMap[name]; if (VstCaseMember.?(member)) return VstCaseMember.!(member); return null; } @@ -31,17 +36,17 @@ class EnumType extends PrimType { // Array is indexed by DFS enumTag; size = tagHi + 1. def getNames(prog: Program) -> Record { if (names == null) { - var total = if(enumDecl.tagHi >= 0, enumDecl.tagHi + 1, enumDecl.cases.length); + var total = if(classDecl.tagHi >= 0, classDecl.tagHi + 1, classDecl.cases.length); names = prog.newRecord(V3.stringArrayType, total); - CaseNames.fillNames(names, enumDecl, "", prog); + CaseNames.fillNames(names, classDecl, "", prog); } return names; } def getShortNames(prog: Program) -> Record { if (shortNames == null) { - var total = if(enumDecl.tagHi >= 0, enumDecl.tagHi + 1, enumDecl.cases.length); + var total = if(classDecl.tagHi >= 0, classDecl.tagHi + 1, classDecl.cases.length); shortNames = prog.newRecord(V3.stringArrayType, total); - CaseNames.fillShortNames(shortNames, enumDecl, prog); + CaseNames.fillShortNames(shortNames, classDecl, prog); } return shortNames; } diff --git a/aeneas/src/vst/MethodEnv.v3 b/aeneas/src/vst/MethodEnv.v3 index 0fdf208bf..8a783feeb 100644 --- a/aeneas/src/vst/MethodEnv.v3 +++ b/aeneas/src/vst/MethodEnv.v3 @@ -165,7 +165,7 @@ class MethodEnv { return VarBinding.None; } def resolveEnumMember(expr: VarExpr, enumType: EnumType, name: string) -> ResolvedMember { - var decl = enumType.enumDecl; + var decl = enumType.classDecl; var member = decl.memberMap[name]; if (decl.qualifiers != null) { if (member == null || VstField.?(member)) { @@ -348,7 +348,7 @@ class MethodEnv { verifier.getEnumSetType(tref, enumType); return expr.bind(null, VarBinding.Type(tref.binding), tref.binding); } else if (Strings.equal(name, "count")) { - var count = enumType.enumDecl.cases.length; + var count = enumType.classDecl.cases.length; var width = 1 + Ints.log(u32.!(count)); if (width < 1) width = 1; var countType = Int.getType(false, width); @@ -359,8 +359,8 @@ class MethodEnv { var member = resolveEnumMember(expr, enumType, name); if (member == null) { // Check if the name is a subtype enum. - if (enumType.enumDecl.subtypes != null) { - var sub = enumType.enumDecl.subtypes[name]; + if (enumType.classDecl.subtypes != null) { + var sub = enumType.classDecl.subtypes[name]; if (sub != null) { var subType = EnumType.!(sub.typeCon.singleton); return expr.bind(null, VarBinding.Type(subType), subType); @@ -374,7 +374,7 @@ class MethodEnv { return expr.bind(null, VarBinding.Inst(enumType.enumGetParamOperator(x), N), memberType); } x: VstTagMember => { - var tagType = enumType.enumDecl.tagType; + var tagType = enumType.classDecl.tagType; return newCompBinding(expr, V3Op.newVstSugar(VstOperator.Nop, [enumType, tagType], [enumType], tagType)); } @@ -409,7 +409,7 @@ class MethodEnv { return newApplyCompBinding(expr, expr.expr, enumType.enumGetParamOperator(x)); } x: VstTagMember => { - var tagType = enumType.enumDecl.tagType; + var tagType = enumType.classDecl.tagType; return newApplyCompBinding(expr, receiver, V3Op.newVstSugar(VstOperator.Nop, [enumType, tagType], [enumType], tagType)); } @@ -450,7 +450,7 @@ class MethodEnv { } } x: VstTagMember => { - var tagType = enumType.enumDecl.tagType; + var tagType = enumType.classDecl.tagType; // Use DFS global tag (enumTag) for runtime; fall back to parse-order tag if unassigned. var tag = vc.enumTag; return expr.bind(null, VarBinding.Const(Int.box(tag), tagType), tagType); diff --git a/aeneas/src/vst/Verifier.v3 b/aeneas/src/vst/Verifier.v3 index d3d01db07..3afab1712 100644 --- a/aeneas/src/vst/Verifier.v3 +++ b/aeneas/src/vst/Verifier.v3 @@ -562,7 +562,7 @@ class Verifier(compiler: Compiler, prog: Program) { Strings.format1("qualifier \"%s\" does not name an enum type", firstTok.image)); return; } - var parentDecl = EnumType.!(tc.create0()).enumDecl; + var parentDecl = EnumType.!(tc.create0()).classDecl; if (parentDecl.qualifiers != null) { cv.errAtDecl(decl).set("EnumDeclError", Strings.format1("\"%s\" must be a top-level enum type", firstTok.image)); @@ -670,7 +670,7 @@ class Verifier(compiler: Compiler, prog: Program) { if (root.qualifiers != null) { var tc = prog.typeEnv.lookup(root.qualifiers.head.image); if (tc != null && tc.kind == Kind.ENUM) { - root = EnumType.!(tc.create0()).enumDecl; + root = EnumType.!(tc.create0()).classDecl; } } return root; @@ -1340,7 +1340,7 @@ class VstCompoundVerifier { if (t != null) match (t) { x: IntType => scale = x.packedByteSize; x: FloatType => scale = x.byteSize; - x: EnumType => scale = x.packedByteSize; + x: EnumType => scale = x.byteSize; x: BoolType => scale = 1; _ => errAtToken(decl.mtref.tname.name).FieldDeclError(Strings.format1("expected primitive layout field type, found \"%q\"", t.render)); } @@ -1503,8 +1503,8 @@ class VstCompoundVerifier { return getEnumSetType(x, enumType); } // Check if name is a subtype enum. - if (enumType.enumDecl.subtypes != null) { - var subDecl = enumType.enumDecl.subtypes[x.name.image]; + if (enumType.classDecl.subtypes != null) { + var subDecl = enumType.classDecl.subtypes[x.name.image]; if (subDecl != null) { if (x.nested != null) { errAtRange(x.name.range()).TypeParamArityError("enum subtype", @@ -1516,12 +1516,12 @@ class VstCompoundVerifier { // Fall back: subtypes map may not be populated yet (early type resolution). var subTc = verifier.prog.typeEnv.lookup(x.name.image); if (subTc != null && subTc.kind == Kind.ENUM) { - var subDecl = EnumType.!(subTc.create0()).enumDecl; + var subDecl = EnumType.!(subTc.create0()).classDecl; if (subDecl.qualifiers != null) { // Accept if qualifiers start with the parent enum's name. var q = subDecl.qualifiers; while (q.tail != null) q = q.tail; - if (Strings.equal(q.head.image, enumType.enumDecl.name())) { + if (Strings.equal(q.head.image, enumType.classDecl.name())) { if (x.nested != null) { errAtRange(x.name.range()).TypeParamArityError("enum subtype", x.name.image, 0, x.nested.length()); @@ -1611,7 +1611,7 @@ class VstCompoundVerifier { } def getEnumSetType(tref: TypeRef, enumType: EnumType) -> Type { if (enumType.setType == null) { - errAtRange(tref.range()).EnumSetTooLarge(enumType, tref, enumType.enumDecl.cases.length); + errAtRange(tref.range()).EnumSetTooLarge(enumType, tref, enumType.classDecl.cases.length); return tref.binding = getErrorType(); } return tref.binding = enumType.setType; @@ -2171,8 +2171,8 @@ class TypeChecker(ERROR: ErrorGen, file: VstFile) extends VstVisitor var vc = vt.lookupCase(name); if (vc == null) { // Not a named case; check if it names a direct subtype enum. - if (vt.enumDecl.subtypes != null) { - var subtypeDecl = vt.enumDecl.subtypes[name]; + if (vt.classDecl.subtypes != null) { + var subtypeDecl = vt.classDecl.subtypes[name]; if (subtypeDecl != null) { pat.subtypeEnumType = EnumType.!(subtypeDecl.typeCon.singleton); return; @@ -3200,7 +3200,7 @@ class MatchStmtVerifier(etype: Type, tc: TypeChecker) { covered = Array.new(variantType.classDecl.cases.length); } else if (EnumType.?(etype)) { enumType = EnumType.!(etype); - covered = Array.new(enumType.enumDecl.cases.length); + covered = Array.new(enumType.classDecl.cases.length); } } def addValue(pat: MatchPattern, val: Val) { @@ -3256,7 +3256,7 @@ class MatchStmtVerifier(etype: Type, tc: TypeChecker) { if (covered == null) return; if (default != null) { // Open enums always have potential uncovered values (from subtypes); default is always reachable. - if (enumType != null && enumType.enumDecl.hasDefault) return; + if (enumType != null && enumType.classDecl.hasDefault) return; for (p in covered) { if (p == null) return; } @@ -3275,7 +3275,7 @@ class MatchStmtVerifier(etype: Type, tc: TypeChecker) { } } // If the enum has "case _", the match must have a default _ case. - if (enumType != null && enumType.enumDecl.hasDefault) { + if (enumType != null && enumType.classDecl.hasDefault) { MatchError(stmt.range(), Strings.format1("match on open enum type %q requires a default case", enumType.render), null); @@ -3288,7 +3288,7 @@ class MatchStmtVerifier(etype: Type, tc: TypeChecker) { if (buf == null) buf = StringBuilder.new().put1("match on type %q does not cover:", t.render); // Skip the _ case — it cannot be explicitly matched. var caseName = if(variantType == null, - enumType.enumDecl.cases[i].name(), + enumType.classDecl.cases[i].name(), variantType.classDecl.cases[i].name()); if (!Strings.equal(caseName, "_")) buf.puts(" ").puts(caseName); } diff --git a/aeneas/src/wasm/WasmGcTarget.v3 b/aeneas/src/wasm/WasmGcTarget.v3 index b75f800f1..d36aab81e 100644 --- a/aeneas/src/wasm/WasmGcTarget.v3 +++ b/aeneas/src/wasm/WasmGcTarget.v3 @@ -141,7 +141,7 @@ class WasmGcTarget extends Target { } private def getScalar(compiler: Compiler, prog: Program, t: Type) -> Scalar.set { match (t) { - x: EnumType => return if(x.enumDecl.tagType.width <= 32, Scalar.B32 | Scalar.B64, Scalar.B64); // XXX: Scalar.R32, once packed refs + x: EnumType => return if(x.classDecl.tagType.width <= 32, Scalar.B32 | Scalar.B64, Scalar.B64); // XXX: Scalar.R32, once packed refs x: EnumSetType => return if(x.repType.width <= 32, Scalar.B32 | Scalar.B64, Scalar.B64); // XXX: Scalar.R32, once packed refs x: BoolType => return Scalar.B32; x: IntType => return if(x.width <= 32, Scalar.B32 | Scalar.B64, Scalar.B64); diff --git a/aeneas/src/wasm/WasmTarget.v3 b/aeneas/src/wasm/WasmTarget.v3 index 67ea24806..f2ebd838d 100644 --- a/aeneas/src/wasm/WasmTarget.v3 +++ b/aeneas/src/wasm/WasmTarget.v3 @@ -112,7 +112,7 @@ class WasmTarget extends Target { private def getScalar(compiler: Compiler, prog: Program, t: Type) -> Scalar.set { var none: Scalar.set; match (t) { - x: EnumType => return if(x.enumDecl.tagType.width <= 32, Scalar.B32 | Scalar.B64, Scalar.B64); // XXX: Scalar.R32, once packed refs + x: EnumType => return if(x.classDecl.tagType.width <= 32, Scalar.B32 | Scalar.B64, Scalar.B64); // XXX: Scalar.R32, once packed refs x: EnumSetType => return if(x.repType.width <= 32, Scalar.B32 | Scalar.B64, Scalar.B64); // XXX: Scalar.R32, once packed refs x: BoolType => return Scalar.B32 | Scalar.B64; x: IntType => return if(x.width <= 32, Scalar.B32 | Scalar.B64, Scalar.B64); // XXX: Scalar.R64, once packed refs diff --git a/aeneas/src/x86-64/SsaX86_64Gen.v3 b/aeneas/src/x86-64/SsaX86_64Gen.v3 index c87eccd35..e19783531 100644 --- a/aeneas/src/x86-64/SsaX86_64Gen.v3 +++ b/aeneas/src/x86-64/SsaX86_64Gen.v3 @@ -513,7 +513,7 @@ class SsaX86_64Gen extends SsaMachGen { return if(PrimType.!(i.op.typeArgs[0]).width > 32, op + I_QD_DIFF, op); } def intCmpOpcode(i: SsaApplyOp) -> int { - var width = PrimType.!(i.op.typeArgs[0]).width; + var width = V3.getWidth(i.op.typeArgs[0]); if (width <= 8) return I_CMPB; if (width <= 32) return I_CMPD; return I_CMPQ; From e010cd78b06e6d422fb02cd24712691ecc838ccd Mon Sep 17 00:00:00 2001 From: Eliot Moss Date: Tue, 21 Apr 2026 08:15:37 -0400 Subject: [PATCH 39/58] Broaden RefLayout field accessors in Eval.v3 to handle enum types doRefLayoutSetField took PrimType, which crashes on enum-typed fields. Widen both it and its callers (plus the matching Get callers) to use Type, matching doRefLayoutGetField which already handled EnumType. Co-Authored-By: Claude Opus 4.6 (1M context) --- aeneas/src/core/Eval.v3 | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/aeneas/src/core/Eval.v3 b/aeneas/src/core/Eval.v3 index 4bda2e2cf..8409b0d10 100644 --- a/aeneas/src/core/Eval.v3 +++ b/aeneas/src/core/Eval.v3 @@ -992,11 +992,11 @@ def evalOp(op: Operator, args: Arguments) -> Result { } RefLayoutGetField(offset, order) => { var ref = args.ref(0); - return doRefLayoutGetField(args, args.getPrimTypeArg(1), ref, offset, order); + return doRefLayoutGetField(args, args.getTypeArg(1), ref, offset, order); } RefLayoutSetField(offset, order) => { var ref = args.ref(0); - return doRefLayoutSetField(args, args.getPrimTypeArg(1), ref, offset, order, args.vals[1]); + return doRefLayoutSetField(args, args.getTypeArg(1), ref, offset, order, args.vals[1]); } RefLayoutAtRepeatedField(offset, scale, max) => { var ref = args.ref(0); @@ -1010,25 +1010,25 @@ def evalOp(op: Operator, args: Arguments) -> Result { var ref = args.ref(0); var index = args.i(1); if (u32.view(index) >= u32.view(max)) return args.throw(V3Exception.BoundsCheck, null); - return doRefLayoutGetField(args, args.getPrimTypeArg(1), ref, offset + scale * index, order); + return doRefLayoutGetField(args, args.getTypeArg(1), ref, offset + scale * index, order); } RefLayoutSetRepeatedField(offset, scale, max, order) => { var ref = args.ref(0); var index = args.i(1); if (u32.view(index) >= u32.view(max)) return args.throw(V3Exception.BoundsCheck, null); - return doRefLayoutSetField(args, args.getPrimTypeArg(1), ref, offset + scale * index, order, args.vals[2]); + return doRefLayoutSetField(args, args.getTypeArg(1), ref, offset + scale * index, order, args.vals[2]); } ByteArrayGetField(offset, order) => { var array = args.r(0); var i_offset = if(ArrayRangeStart.?(args.vals[1]), ArrayRangeStart.!(args.vals[1]).start, args.i(1)); // XXX: Refactor so no intermediate ByteArrayOffset object needed - return doRefLayoutGetField(args, args.getPrimTypeArg(0), ByteArrayOffset.new(array, offset), i_offset, order); + return doRefLayoutGetField(args, args.getTypeArg(0), ByteArrayOffset.new(array, offset), i_offset, order); } ByteArraySetField(offset, order) => { var array = args.r(0); var i_offset = if(ArrayRangeStart.?(args.vals[1]), ArrayRangeStart.!(args.vals[1]).start, args.i(1)); // XXX: Refactor so no intermediate ByteArrayOffset object needed - return doRefLayoutSetField(args, args.getPrimTypeArg(0), ByteArrayOffset.new(array, offset), i_offset, order, args.vals[2]); + return doRefLayoutSetField(args, args.getTypeArg(0), ByteArrayOffset.new(array, offset), i_offset, order, args.vals[2]); } //---------------------------------------------------------------------------- @@ -1108,7 +1108,7 @@ def doRefLayoutGetField(args: Arguments, fieldType: Type, ref: ByteArrayOffset, _ => return args.throw("EvalException", Strings.format1("invalid RefLayoutField type %q", fieldType.render)); } } -def doRefLayoutSetField(args: Arguments, fieldType: PrimType, ref: ByteArrayOffset, offset: int, order: ByteOrder, val: Val) -> Result { +def doRefLayoutSetField(args: Arguments, fieldType: Type, ref: ByteArrayOffset, offset: int, order: ByteOrder, val: Val) -> Result { if (ref == null || ref.array == null) return args.throw(V3Exception.NullCheck, null); var bits: u64 = 0, signed = false; match (val) { @@ -1118,7 +1118,8 @@ def doRefLayoutSetField(args: Arguments, fieldType: PrimType, ref: ByteArrayOffs v: Float32Val => bits = v.bits; v: Float64Val => bits = v.bits; } - ref.write(order, offset, fieldType.packedByteSize, bits); + var size = if(PrimType.?(fieldType), PrimType.!(fieldType).packedByteSize, EnumType.!(fieldType).byteSize); + ref.write(order, offset, size, bits); return Values.BOTTOM; } From 63f5d12c4cf1f61b9f7ebf2e2c5c80feced920d1 Mon Sep 17 00:00:00 2001 From: Eliot Moss Date: Tue, 21 Apr 2026 09:55:33 -0400 Subject: [PATCH 40/58] Merge EnumType into ClassType; eliminate separate enum type class EnumType extended ClassType but caused pervasive match-ordering bugs since ClassType.?(t) matched enums. Move enum-specific fields (width, byteSize, setType) into ClassType, use V3Class_TypeCon for enum type construction, and replace all EnumType.?/x:EnumType checks with V3.isEnum() or classDecl.isEnum() guards within ClassType arms. Key changes: - Delete EnumType class from V3Enum.v3 (EnumSetType stays) - Add enum fields + getNames/getShortNames/enumGetParamOperator to ClassType - Names cache stays on V3Class_TypeCon for cross-instantiation sharing - Enums now use V3Class_TypeCon (same as classes/variants) - Restructure computeConversion/computeCast/unify0 in TypeSystem.v3 to handle enum and non-enum ClassType in single match arms - Update all 17 files that referenced EnumType across compiler Co-Authored-By: Claude Opus 4.6 (1M context) --- aeneas/src/core/Eval.v3 | 18 +++++----- aeneas/src/debug/Dwarf.v3 | 5 ++- aeneas/src/ir/Packing.v3 | 59 ++++++++++++++----------------- aeneas/src/ir/SsaNormalizer.v3 | 62 +++++++++++++++------------------ aeneas/src/ir/VstIr.v3 | 2 +- aeneas/src/jvm/JvmRep.v3 | 4 +-- aeneas/src/jvm/SsaJvmGen.v3 | 2 +- aeneas/src/ssa/SsaBuilder.v3 | 14 ++------ aeneas/src/ssa/VstSsaGen.v3 | 6 ++-- aeneas/src/v3/TypeSystem.v3 | 38 +++++++++----------- aeneas/src/v3/V3.v3 | 34 +++++++----------- aeneas/src/v3/V3Class.v3 | 54 +++++++++++++++++----------- aeneas/src/v3/V3Enum.v3 | 50 -------------------------- aeneas/src/vst/MethodEnv.v3 | 22 ++++++------ aeneas/src/vst/Verifier.v3 | 39 +++++++++++---------- aeneas/src/wasm/WasmGcTarget.v3 | 2 +- aeneas/src/wasm/WasmTarget.v3 | 2 +- 17 files changed, 173 insertions(+), 240 deletions(-) diff --git a/aeneas/src/core/Eval.v3 b/aeneas/src/core/Eval.v3 index 8409b0d10..aa2537731 100644 --- a/aeneas/src/core/Eval.v3 +++ b/aeneas/src/core/Eval.v3 @@ -1082,7 +1082,7 @@ def evalOp(op: Operator, args: Arguments) -> Result { def doRefLayoutGetField(args: Arguments, fieldType: Type, ref: ByteArrayOffset, offset: int, order: ByteOrder) -> Result { if (ref == null || ref.array == null) return args.throw(V3Exception.NullCheck, null); - var v = ref.read(order, offset, if(PrimType.?(fieldType), PrimType.!(fieldType).packedByteSize, EnumType.!(fieldType).byteSize)); + var v = ref.read(order, offset, if(PrimType.?(fieldType), PrimType.!(fieldType).packedByteSize, ClassType.!(fieldType).byteSize)); match (fieldType) { x: BoolType => { return Bool.box((v & 1) != 0); @@ -1097,15 +1097,17 @@ def doRefLayoutGetField(args: Arguments, fieldType: Type, ref: ByteArrayOffset, I64, U64 => return Long.box(long.view(v)); } } - x: EnumType => { - if (v >= x.classDecl.cases.length) v = 0; // out-of-bounds tag => 0 - return Int.box(int.view(v)); // note: no long enum values - } x: FloatType => { if (x.is64) return Float64Val.new(v); else return Float32Val.new(u32.view(v)); } - _ => return args.throw("EvalException", Strings.format1("invalid RefLayoutField type %q", fieldType.render)); + _ => { + if (V3.isEnum(fieldType)) { + if (v >= ClassType.!(fieldType).classDecl.cases.length) v = 0; + return Int.box(int.view(v)); + } + return args.throw("EvalException", Strings.format1("invalid RefLayoutField type %q", fieldType.render)); + } } } def doRefLayoutSetField(args: Arguments, fieldType: Type, ref: ByteArrayOffset, offset: int, order: ByteOrder, val: Val) -> Result { @@ -1118,7 +1120,7 @@ def doRefLayoutSetField(args: Arguments, fieldType: Type, ref: ByteArrayOffset, v: Float32Val => bits = v.bits; v: Float64Val => bits = v.bits; } - var size = if(PrimType.?(fieldType), PrimType.!(fieldType).packedByteSize, EnumType.!(fieldType).byteSize); + var size = if(PrimType.?(fieldType), PrimType.!(fieldType).packedByteSize, ClassType.!(fieldType).byteSize); ref.write(order, offset, size, bits); return Values.BOTTOM; } @@ -1183,7 +1185,7 @@ def lookupClassVirtual(args: Arguments, object: Record, spec: IrSpec) -> IrSpec } def lookupEnumVirtual(args: Arguments, member: IrMember) -> IrSpec { var ta = args.getTypeArgs(); - var enumType = EnumType.!(ta[0]); + var enumType = ClassType.!(ta[0]); var tag = Int.unbox(args.vals[0]); var targetDecl = findEnumDeclForTag(enumType.classDecl, tag); var ir = args.getProgram().ir; diff --git a/aeneas/src/debug/Dwarf.v3 b/aeneas/src/debug/Dwarf.v3 index 4117f8df3..320a11dbd 100644 --- a/aeneas/src/debug/Dwarf.v3 +++ b/aeneas/src/debug/Dwarf.v3 @@ -318,8 +318,7 @@ class DwarfInfoSection(abbrev: DwarfAbbrevSection) { encoding = if (V3.isSigned(x), DW.DW_ATE_signed, DW.DW_ATE_unsigned); emitBaseType(t, byteSize, encoding); } - x: ClassType => emitClassType(x); - x: EnumType => emitEnumType(x); + x: ClassType => if (x.classDecl.isEnum()) emitEnumType(x); else emitClassType(x); x: ArrayType => emitArrayType(x); _ => emitUnspecifiedType(t); } @@ -353,7 +352,7 @@ class DwarfInfoSection(abbrev: DwarfAbbrevSection) { _ => ; } } - def emitEnumType(t: EnumType) { + def emitEnumType(t: ClassType) { w.put_uleb32(abbrev.getAbbrev(DwarfAbbrevTag.EnumurationType)); buf.reset(); t.render(buf); diff --git a/aeneas/src/ir/Packing.v3 b/aeneas/src/ir/Packing.v3 index 4c5d4f1bb..dbf3f9c6b 100644 --- a/aeneas/src/ir/Packing.v3 +++ b/aeneas/src/ir/Packing.v3 @@ -36,11 +36,10 @@ class SsaPacker(builder: SsaBuilder, result: IntType) { if (x.width > 0) scalars.put(x, pos, input); pos += x.width; } - x: EnumType => { + x: ClassType => if (x.classDecl.isEnum()) { if (x.width > 0) scalars.put(x, pos, input); pos += x.width; - } - x: ClassType => { + } else { // pack the fields of this class type var ic = builder.context.prog.ir.makeIrClass(x); // XXX: makeIrClass seems wrong after normalization // Now pack the fields of the class @@ -69,7 +68,7 @@ class SsaPacker(builder: SsaBuilder, result: IntType) { ft = ut; } } - x: EnumType => { + x: ClassType => if (x.classDecl.isEnum()) { ft = x.classDecl.tagType; } x: EnumSetType => { @@ -150,50 +149,45 @@ class SsaUnpacker(builder: SsaBuilder, ft: IntType) { pos += x.width; } - x: EnumType => { + x: EnumSetType => { + def repType: IntType = x.repType; + def unpackedRep = builder.opIntViewI0(ft, repType, shiftedInput); + def enumSetInstr = SsaApplyOp.new(builder.source, V3Op.newTypeSubsume(repType, x), [unpackedRep]); + builder.append(enumSetInstr); + + unpackedBuffer.push(enumSetInstr); + pos += repType.width; + } + x: TupleType => { + def elemTypes: Array = Lists.toArray(x.nested); + def unpackedElems = Array.new(elemTypes.length); + for (i = elemTypes.length - 1; i >= 0; i--) { + unpack0(elemTypes[i]); + unpackedElems[i] = if(!VoidType.?(elemTypes[i]), unpackedBuffer.pop(), builder.graph.nullConst(elemTypes[i])); + } + def tupleCreate = builder.opTupleCreate(x, unpackedElems); + unpackedBuffer.push(tupleCreate); + } + x: ClassType => if (x.classDecl.isEnum()) { def tagType: IntType = x.classDecl.tagType; def unpackedTag: SsaInstr = builder.opIntViewI0(ft, tagType, shiftedInput); def numCases = x.classDecl.cases.length; def cond: SsaInstr = builder.addApply(builder.source, tagType.opLt(), [unpackedTag, builder.graph.intConst(numCases)]); - // Valid tag case def trueBlock = SsaBlock.new(); def subsumeOrigTag = SsaApplyOp.new(builder.source, V3Op.newTypeSubsume(tagType, x), [unpackedTag]); trueBlock.append(subsumeOrigTag); - // Invalid tag -> map to 0th enum value def falseBlock = SsaBlock.new(); def subsumeZero = SsaApplyOp.new(builder.source, V3Op.newTypeSubsume(tagType, x), [builder.graph.intConst(0)]); falseBlock.append(subsumeZero); builder.addIf(cond, trueBlock, falseBlock); - // Set builder to process on joinBlock def joinBlock = SsaBlock.new(); trueBlock.append(SsaGoto.new(joinBlock)); falseBlock.append(SsaGoto.new(joinBlock)); builder.set(joinBlock); def enumInstr = builder.addPhi(x, [subsumeOrigTag, subsumeZero]); - unpackedBuffer.push(enumInstr); pos += tagType.width; - } - x: EnumSetType => { - def repType: IntType = x.repType; - def unpackedRep = builder.opIntViewI0(ft, repType, shiftedInput); - def enumSetInstr = SsaApplyOp.new(builder.source, V3Op.newTypeSubsume(repType, x), [unpackedRep]); - builder.append(enumSetInstr); - - unpackedBuffer.push(enumSetInstr); - pos += repType.width; - } - x: TupleType => { - def elemTypes: Array = Lists.toArray(x.nested); - def unpackedElems = Array.new(elemTypes.length); - for (i = elemTypes.length - 1; i >= 0; i--) { - unpack0(elemTypes[i]); - unpackedElems[i] = if(!VoidType.?(elemTypes[i]), unpackedBuffer.pop(), builder.graph.nullConst(elemTypes[i])); - } - def tupleCreate = builder.opTupleCreate(x, unpackedElems); - unpackedBuffer.push(tupleCreate); - } - x: ClassType => { + } else { var ic = builder.context.prog.ir.makeIrClass(x); // XXX: makeIrClass seems wrong after normalization var fs = ic.fields; for (i=fs.length-1; i >= 0; i--) { @@ -234,10 +228,9 @@ component PackingUtils { } return (width, null); } - x: EnumType => { + x: ClassType => if (x.classDecl.isEnum()) { return (x.width, null); - } - x: ClassType => { + } else { if (x.typeCon.kind != Kind.VARIANT) return (int.max, Strings.format1("because %q is a class", x.render)); for (l = seen; l != null; l = l.tail) { // check for recursive type if (l.head == x) return (int.max, Strings.format1("because %q is recursive", x.render)); diff --git a/aeneas/src/ir/SsaNormalizer.v3 b/aeneas/src/ir/SsaNormalizer.v3 index d9b90348e..ed4e6a2fb 100644 --- a/aeneas/src/ir/SsaNormalizer.v3 +++ b/aeneas/src/ir/SsaNormalizer.v3 @@ -467,16 +467,15 @@ class SsaRaNormalizer extends SsaRebuilder { var facts: Fact.set = Fact.O_NO_BOUNDS_CHECK; var result: SsaInstr; - match (fn.oldType) { - x: EnumType => { - var it = IntType.!(x.classDecl.tagType); - var wt = Int.getType(false, it.byteSize * 8); - result = curBlock.opByteArrayGetField(wt, rangeStartType, offset, order, facts, array, start); - var caseCount = newGraph.intConst(x.classDecl.cases.length); - var inBound = curBlock.opIntULt(norm.config.ArrayLengthType, it, result, caseCount); - result = curBlock.opIntViewI0(it, wt, result); - result = curBlock.addSelect(it, inBound, result, newGraph.nullConst(it)); - } + if (V3.isEnum(fn.oldType)) { + var it = IntType.!(V3.getDecl(fn.oldType).tagType); + var wt = Int.getType(false, it.byteSize * 8); + result = curBlock.opByteArrayGetField(wt, rangeStartType, offset, order, facts, array, start); + var caseCount = newGraph.intConst(V3.getDecl(fn.oldType).cases.length); + var inBound = curBlock.opIntULt(norm.config.ArrayLengthType, it, result, caseCount); + result = curBlock.opIntViewI0(it, wt, result); + result = curBlock.addSelect(it, inBound, result, newGraph.nullConst(it)); + } else match (fn.oldType) { x: BoolType, x: IntType, x: FloatType => result = curBlock.opByteArrayGetField(fn.oldType, rangeStartType, offset, order, facts, array, start); @@ -491,12 +490,11 @@ class SsaRaNormalizer extends SsaRebuilder { var facts: Fact.set = Fact.O_NO_BOUNDS_CHECK; var result: SsaInstr; - match (fn.oldType) { - x: EnumType => { - var it = IntType.!(x.classDecl.tagType); - var wt = Int.getType(false, it.byteSize * 8); - result = curBlock.opByteArraySetField(wt, rangeStartType, offset, order, facts, array, start, val); - } + if (V3.isEnum(fn.oldType)) { + var it = IntType.!(V3.getDecl(fn.oldType).tagType); + var wt = Int.getType(false, it.byteSize * 8); + result = curBlock.opByteArraySetField(wt, rangeStartType, offset, order, facts, array, start, val); + } else match (fn.oldType) { x: BoolType, x: IntType, x: FloatType => result = curBlock.opByteArraySetField(fn.oldType, rangeStartType, offset, order, facts, array, start, val); @@ -518,16 +516,15 @@ class SsaRaNormalizer extends SsaRebuilder { curBlock.opIntAdd(start, curBlock.opIntMul(index, newGraph.intConst(scale))) ); var facts: Fact.set = Fact.O_NO_BOUNDS_CHECK; - match (fn.oldType) { + if (V3.isEnum(fn.oldType)) { + var it = IntType.!(V3.getDecl(fn.oldType).tagType); + result = curBlock.opByteArrayGetField(it, rangeStartType, offset, order, facts, array, start); + var caseCount = newGraph.intConst(V3.getDecl(fn.oldType).cases.length); + var inBound = curBlock.opIntULt(it, it, result, caseCount); + result = curBlock.addSelect(it, inBound, result, newGraph.nullConst(it)); + } else match (fn.oldType) { x: IntType => result = curBlock.opByteArrayGetField(x, rangeStartType, offset, order, facts, array, start); x: FloatType => result = curBlock.opByteArrayGetField(x, rangeStartType, offset, order, facts, array, start); - x: EnumType => { - var it = IntType.!(x.classDecl.tagType); - result = curBlock.opByteArrayGetField(it, rangeStartType, offset, order, facts, array, start); - var caseCount = newGraph.intConst(x.classDecl.cases.length); - var inBound = curBlock.opIntULt(it, it, result, caseCount); - result = curBlock.addSelect(it, inBound, result, newGraph.nullConst(it)); - } _ => ; } return map1(i_old, result); @@ -547,13 +544,12 @@ class SsaRaNormalizer extends SsaRebuilder { ); var facts: Fact.set = Fact.O_NO_BOUNDS_CHECK; var result: SsaInstr; - match (fn.oldType) { + if (V3.isEnum(fn.oldType)) { + var it = IntType.!(V3.getDecl(fn.oldType).tagType); + result = curBlock.opByteArraySetField(it, rangeStartType, offset, order, facts, array, start, val); + } else match (fn.oldType) { x: IntType => result = curBlock.opByteArraySetField(fn.oldType, rangeStartType, offset, order, facts, array, start, val); x: FloatType => result = curBlock.opByteArraySetField(fn.oldType, rangeStartType, offset, order, facts, array, start, val); - x: EnumType => { - var it = IntType.!(x.classDecl.tagType); - result = curBlock.opByteArraySetField(it, rangeStartType, offset, order, facts, array, start, val); - } } return map1(i_old, result); } @@ -1749,6 +1745,10 @@ class SsaRaNormalizer extends SsaRebuilder { } // Case: Scalar is IntType; Interval represents a BoolType or FloatType if (interval.start > 0) scalar = curBlock.addApplyF(ift.opShr(), [scalar, newGraph.intConst(interval.start)], Facts.O_SAFE_SHIFT); + if (V3.isEnum(tt)) { + scalar = curBlock.opIntView(ift, V3.getDecl(tt).tagType, scalar); + return scalar; + } match (tt) { x: BoolType => { def oneConst = if(ift.width > 32, newGraph.valConst(ift, Long.box(1L)), newGraph.intConst(1)); @@ -1759,10 +1759,6 @@ class SsaRaNormalizer extends SsaRebuilder { if (x.is64) return curBlock.addApply(curBlock.source, V3Op.newFloat64ViewI(ift), [scalar]); else return curBlock.addApply(curBlock.source, V3Op.newFloat32ViewI(ift), [scalar]); } - x: EnumType => { - scalar = curBlock.opIntView(ift, x.classDecl.tagType, scalar); - return scalar; - } x: EnumSetType => { scalar = curBlock.opIntView(ift, x.repType, scalar); return scalar; diff --git a/aeneas/src/ir/VstIr.v3 b/aeneas/src/ir/VstIr.v3 index 195250dea..6b3c4ecf7 100644 --- a/aeneas/src/ir/VstIr.v3 +++ b/aeneas/src/ir/VstIr.v3 @@ -37,7 +37,7 @@ class IrBuilder(ctype: Type, parent: IrClass) { methods.put(null); // reserve index 0 for constructor methods.put(null); // reserve index 1 for variant equals method } else { - if (!EnumType.?(ctype)) fields.puta(parent.fields); + if (!V3.isEnum(ctype)) fields.puta(parent.fields); methods.puta(parent.methods); methods[0] = null; // overwrite super constructor } diff --git a/aeneas/src/jvm/JvmRep.v3 b/aeneas/src/jvm/JvmRep.v3 index 26bdfc306..7f95be796 100644 --- a/aeneas/src/jvm/JvmRep.v3 +++ b/aeneas/src/jvm/JvmRep.v3 @@ -171,8 +171,8 @@ class JvmTypeReps(jprog: JvmProgram) { } def zeroExtendSignedByte(t: Type, code: JvmCodeBuilder) { if (t == Byte.TYPE) return code.inl_izx(8); - if (EnumType.?(t)) { - t = EnumType.!(t).classDecl.tagType; + if (V3.isEnum(t)) { + t = V3.getDecl(t).tagType; if (t == Byte.TYPE) return code.inl_izx(8); } } diff --git a/aeneas/src/jvm/SsaJvmGen.v3 b/aeneas/src/jvm/SsaJvmGen.v3 index 91c8afbe9..53973e0a1 100644 --- a/aeneas/src/jvm/SsaJvmGen.v3 +++ b/aeneas/src/jvm/SsaJvmGen.v3 @@ -130,7 +130,7 @@ class SsaJvmGen(jprog: JvmProgram, context: SsaContext, jsig: JvmSig, code: JvmC match (t.typeCon.kind) { INT => return IntType.!(t); VARIANT => return V3.getVariantTagType(t); - ENUM => return EnumType.!(t).classDecl.tagType; + ENUM => return ClassType.!(t).classDecl.tagType; ENUM_SET => return EnumSetType.!(t).repType; _ => context.fail("expected integral type"); } diff --git a/aeneas/src/ssa/SsaBuilder.v3 b/aeneas/src/ssa/SsaBuilder.v3 index efe8db2e5..d890e3910 100644 --- a/aeneas/src/ssa/SsaBuilder.v3 +++ b/aeneas/src/ssa/SsaBuilder.v3 @@ -98,9 +98,7 @@ class SsaBuilder { } VariantGetName => { var t = op.typeArgs[0]; - var record: Record; - if (V3.isEnum(t)) record = EnumType.!(t).getNames(context.prog); - else record = V3Class_TypeCon.!(t.typeCon).getNames(context.prog); + var record = ClassType.!(t).getNames(context.prog); var array = graph.recordConst(record); var index = if(V3.isEnum(t), args[0], add(V3Op.newVariantGetTag(op.typeArgs[0]), args, N)); return opArrayGetElem(V3.stringArrayType, V3.getVariantTagType(t), @@ -108,20 +106,12 @@ class SsaBuilder { } VariantGetShortName => { var t = op.typeArgs[0]; - var record: Record; - if (V3.isEnum(t)) record = EnumType.!(t).getShortNames(context.prog); - else record = V3Class_TypeCon.!(t.typeCon).getShortNames(context.prog); + var record = ClassType.!(t).getShortNames(context.prog); var array = graph.recordConst(record); var index = if(V3.isEnum(t), args[0], add(V3Op.newVariantGetTag(op.typeArgs[0]), args, N)); return opArrayGetElem(V3.stringArrayType, V3.getVariantTagType(t), Facts.O_SAFE_BOUNDS | Fact.O_PURE, array, index); -// var t = EnumType.!(op.typeArgs[0]); -// var record = t.getShortNames(context.prog); -// var array = graph.recordConst(record); -// var tagType = IntType.!(t.classDecl.tagType); -// return opArrayGetElem(V3.stringArrayType, tagType, -// Facts.O_SAFE_BOUNDS | Fact.O_PURE, array, args[0]); } EnumGetParam(field) => { var t = field.receiver.declType; diff --git a/aeneas/src/ssa/VstSsaGen.v3 b/aeneas/src/ssa/VstSsaGen.v3 index 47720bf13..52a22028f 100644 --- a/aeneas/src/ssa/VstSsaGen.v3 +++ b/aeneas/src/ssa/VstSsaGen.v3 @@ -149,7 +149,7 @@ class VstSsaGen extends VstVisitor { while (rootEnum.qualifiers != null) { var tc = context.prog.typeEnv.lookup(rootEnum.qualifiers.head.image); if (tc == null || tc.kind != Kind.ENUM) break; - rootEnum = EnumType.!(tc.create0()).classDecl; + rootEnum = ClassType.!(tc.create0()).classDecl; } // Count total params: root + all ancestor extras + own extras var totalParams = 0; @@ -291,7 +291,7 @@ class VstSsaGen extends VstVisitor { } def visitForeach(stmt: ForeachStmt, env: VstSsaEnv) -> SsaInstr { var arrayType = stmt.expr.exactType; - if (V3.isEnum(arrayType)) return genEnumIteration(stmt, EnumType.!(arrayType), env); + if (V3.isEnum(arrayType)) return genEnumIteration(stmt, ClassType.!(arrayType), env); var arrayExpr = env.genExpr(stmt.expr); if (env.end) return null; // expression generated a throw if (EnumSetType.?(arrayType)) return genEnumSetIteration(stmt, EnumSetType.!(arrayType), arrayExpr, env); @@ -340,7 +340,7 @@ class VstSsaGen extends VstVisitor { loopEnd.setEnv(env); return null; } - def genEnumIteration(stmt: ForeachStmt, enumType: EnumType, env: VstSsaEnv) -> SsaInstr { + def genEnumIteration(stmt: ForeachStmt, enumType: ClassType, env: VstSsaEnv) -> SsaInstr { // TODO: desugar and use genThreePartFor env[stmt.loopVar] = graph.zeroConst(); var varDecl = VarDecl.new(null, null, null); diff --git a/aeneas/src/v3/TypeSystem.v3 b/aeneas/src/v3/TypeSystem.v3 index 3ee744da4..caab8dd7b 100644 --- a/aeneas/src/v3/TypeSystem.v3 +++ b/aeneas/src/v3/TypeSystem.v3 @@ -33,8 +33,8 @@ component TypeSystem { } // Check if the type {xt} is an enum and the type {yt} is its corresponding set type. def isEnumToSet(xt: Type, yt: Type) -> bool { - if (EnumType.?(xt) && EnumSetType.?(yt)) { - var xd = EnumType.!(xt).classDecl; + if (V3.isEnum(xt) && EnumSetType.?(yt)) { + var xd = ClassType.!(xt).classDecl; var yd = EnumSetType.!(yt).enumDecl; return xd == yd || isEnumSubtypeOf(xd, yd); } @@ -65,15 +65,16 @@ component TypeSystem { y: FuncType => return Conversion.SUBSUME_NULL_FUNC; } } - x: EnumType => match (yt) { - y: EnumSetType => if (x.classDecl == y.enumDecl || isEnumSubtypeOf(x.classDecl, y.enumDecl)) return Conversion.PROMOTE_ENUM_TO_SET; - y: EnumType => if (isEnumSubtypeOf(x.classDecl, y.classDecl)) return Conversion.SUBSUME_VARIANT; - } x: ClassType => match (yt) { + y: EnumSetType => if (x.classDecl.isEnum() && (x.classDecl == y.enumDecl || isEnumSubtypeOf(x.classDecl, y.enumDecl))) return Conversion.PROMOTE_ENUM_TO_SET; y: ClassType => { - for (sx = x; sx != null; sx = sx.getSuperType()) { - if (sx == y) return Conversion.SUBSUME_CLASS; - if (sx.classDecl == y.classDecl) return Conversion.ILLEGAL; // same decl, different type args + if (x.classDecl.isEnum() && y.classDecl.isEnum()) { + if (isEnumSubtypeOf(x.classDecl, y.classDecl)) return Conversion.SUBSUME_VARIANT; + } else if (!x.classDecl.isEnum() && !y.classDecl.isEnum()) { + for (sx = x; sx != null; sx = sx.getSuperType()) { + if (sx == y) return Conversion.SUBSUME_CLASS; + if (sx.classDecl == y.classDecl) return Conversion.ILLEGAL; // same decl, different type args + } } } } @@ -136,16 +137,13 @@ component TypeSystem { if (TypeVarType.?(xt)) return Conversion.CAST_POLY; match (xt) { - x: EnumType => match (yt) { - y: EnumType => { - if (isEnumSubtypeOf(y.classDecl, x.classDecl)) { - return Conversion.CAST_VARIANT; - } - return Conversion.ILLEGAL; - } - } x: ClassType => match(yt) { y: ClassType => { + if (x.classDecl.isEnum() != y.classDecl.isEnum()) return Conversion.ILLEGAL; + if (x.classDecl.isEnum()) { + if (isEnumSubtypeOf(y.classDecl, x.classDecl)) return Conversion.CAST_VARIANT; + return Conversion.ILLEGAL; + } if (x.typeCon.kind != y.typeCon.kind) return Conversion.ILLEGAL; var xdepth = x.getDepth(), ydepth = y.getDepth(); var xl = x, yl = y; @@ -309,11 +307,8 @@ component TypeSystem { if (isPromotableToFloat(x, y)) return unifyPromotable(dir, x, y); } } - x: EnumType => match (t2) { - y: EnumSetType => if(x.classDecl == y.enumDecl) return unifyPromotable(dir, x, y); - } x: EnumSetType => match (t2) { - y: EnumType => if(x.enumDecl == y.classDecl) return unifyPromotable(dir, y, x); + y: ClassType => if(y.classDecl.isEnum() && x.enumDecl == y.classDecl) return unifyPromotable(dir, y, x); } x: FloatType => match (t2) { y: IntType => { @@ -331,6 +326,7 @@ component TypeSystem { y: FuncType => return unifySubtype(dir, x, y); } x: ClassType => match(t2) { + y: EnumSetType => if(x.classDecl.isEnum() && x.classDecl == y.enumDecl) return unifyPromotable(dir, x, y); y: NullType => return if(x.typeCon.kind != Kind.VARIANT, unifySubtype(dir, y, x)); y: ClassType => { match (dir) { diff --git a/aeneas/src/v3/V3.v3 b/aeneas/src/v3/V3.v3 index 1f3c2356c..fc901e980 100644 --- a/aeneas/src/v3/V3.v3 +++ b/aeneas/src/v3/V3.v3 @@ -60,7 +60,7 @@ component V3 { def isRange(t: Type) -> bool { return RangeType.?(t); } def isVariant(t: Type) -> bool { return t != null && t.typeCon.kind == Kind.VARIANT; } def isTuple(t: Type) -> bool { return TupleType.?(t); } - def isEnum(t: Type) -> bool { return EnumType.?(t); } + def isEnum(t: Type) -> bool { return t != null && t.typeCon.kind == Kind.ENUM; } def isEnumSet(t: Type) -> bool { return EnumSetType.?(t); } def isString(t: Type) -> bool { return t == stringType; } def isRef(t: Type) -> bool { return RefType.?(t); } @@ -96,41 +96,33 @@ component V3 { return if(tag >= 0, tag); } def getVariantTagType(t: Type) -> IntType { - match (t) { - x: EnumType => return x.classDecl.tagType; - x: ClassType => return ClassType.!(getRootType(t)).classDecl.tagType; - _ => return null; + if (ClassType.?(t)) { + if (isEnum(t)) return ClassType.!(t).classDecl.tagType; + return ClassType.!(getRootType(t)).classDecl.tagType; } + return null; } // Shared helpers for hierarchical types (class, variant, enum). - def isHierarchical(t: Type) -> bool { return ClassType.?(t) || EnumType.?(t); } + def isHierarchical(t: Type) -> bool { return ClassType.?(t); } def getDecl(t: Type) -> VstClass { - match (t) { - x: EnumType => return x.classDecl; - x: ClassType => return x.classDecl; - _ => return null; - } + return if(ClassType.?(t), ClassType.!(t).classDecl); } def getWidth(t: Type) -> byte { match (t) { + x: ClassType => return x.width; x: PrimType => return x.width; - x: EnumType => return x.width; _ => return 0; } } def getTagLo(t: Type) -> int { - match (t) { - x: EnumType => return x.classDecl.tagLo; - x: ClassType => return x.classDecl.variantTag; - _ => return 0; + if (ClassType.?(t)) { + var decl = ClassType.!(t).classDecl; + return if(decl.isEnum(), decl.tagLo, decl.variantTag); } + return 0; } def getTagHi(t: Type) -> int { - match (t) { - x: EnumType => return x.classDecl.tagHi; - x: ClassType => return x.classDecl.tagHi; - _ => return 0; - } + return if(ClassType.?(t), ClassType.!(t).classDecl.tagHi); } // compute the type of the tag based on the number of cases def getTagType(caseCount: int) -> IntType { diff --git a/aeneas/src/v3/V3Class.v3 b/aeneas/src/v3/V3Class.v3 index 3ca417fb5..15625bbb7 100644 --- a/aeneas/src/v3/V3Class.v3 +++ b/aeneas/src/v3/V3Class.v3 @@ -14,24 +14,8 @@ class V3Class_TypeCon extends TypeCon { def alloc(hash: int, nested: List) -> ClassType { return ClassType.new(hash, classDecl, this, nested); } - def getNames(prog: Program) -> Record { - if (names == null) { - var total = if(classDecl.tagHi >= 0, classDecl.tagHi + 1, classDecl.cases.length); - names = prog.newRecord(V3.stringArrayType, total); - CaseNames.fillNames(names, classDecl, "", prog); - } - return names; - } - def getShortNames(prog: Program) -> Record { - if (shortNames == null) { - var total = if(classDecl.tagHi >= 0, classDecl.tagHi + 1, classDecl.cases.length); - shortNames = prog.newRecord(V3.stringArrayType, total); - CaseNames.fillShortNames(shortNames, classDecl, prog); - } - return shortNames; - } } -// Shared helpers for building case name arrays (used by both V3Class_TypeCon and EnumType). +// Shared helpers for building case name arrays (used by ClassType for both variants and enums). component CaseNames { def fillNames(rec: Record, decl: VstClass, prefix: string, prog: Program) { for (c in decl.cases) { @@ -59,15 +43,23 @@ component CaseNames { } } } -// The type for user-declared classes. -// XXX: separate VariantType from ClassType and enum type. +// The type for user-declared classes, variants, and enums. class ClassType extends Type { def classDecl: VstClass; var superType: ClassType; var depth = -1; // depth from root, to speed up commonSuperClass() var typeArgs: TypeArgs; + var width: byte; + var byteSize: byte; + var setType: EnumSetType; - new(hash: int, classDecl, typeCon: TypeCon, nested: List) super(hash, typeCon, nested) { } + new(hash: int, classDecl, typeCon: TypeCon, nested: List) super(hash, typeCon, nested) { + if (classDecl.isEnum()) { + var w = classDecl.tagType.width; + width = w; + byteSize = if(w == 0, byte.view(0), if(w <= 8, byte.view(1), if(w <= 16, byte.view(2), if(w <= 32, byte.view(4), byte.view(8))))); + } + } def getSuperType() -> ClassType { if (superType != null) return superType; @@ -113,4 +105,26 @@ class ClassType extends Type { var types = [Type.!(this)]; return V3Op.newVstSugar(VstOperator.VariantGetShortName, types, types, V3.stringType); } + def getNames(prog: Program) -> Record { + var tc = V3Class_TypeCon.!(typeCon); + if (tc.names == null) { + var total = if(classDecl.tagHi >= 0, classDecl.tagHi + 1, classDecl.cases.length); + tc.names = prog.newRecord(V3.stringArrayType, total); + CaseNames.fillNames(tc.names, classDecl, "", prog); + } + return tc.names; + } + def getShortNames(prog: Program) -> Record { + var tc = V3Class_TypeCon.!(typeCon); + if (tc.shortNames == null) { + var total = if(classDecl.tagHi >= 0, classDecl.tagHi + 1, classDecl.cases.length); + tc.shortNames = prog.newRecord(V3.stringArrayType, total); + CaseNames.fillShortNames(tc.shortNames, classDecl, prog); + } + return tc.shortNames; + } + def enumGetParamOperator(f: VstField) -> Operator { + var types: Array = [this]; + return V3Op.newVstSugar(VstOperator.EnumGetParam(f), TypeUtil.NO_TYPES, types, f.getType()); + } } diff --git a/aeneas/src/v3/V3Enum.v3 b/aeneas/src/v3/V3Enum.v3 index 375e5934f..2b337abaa 100644 --- a/aeneas/src/v3/V3Enum.v3 +++ b/aeneas/src/v3/V3Enum.v3 @@ -1,56 +1,6 @@ // Copyright 2015 Google Inc. All rights reserved. // See LICENSE for details of Apache 2.0 license. -// The type for user-declared enums. -class EnumType extends ClassType { - var width: byte; - var byteSize: byte; - var setType: EnumSetType; - var names: Record; - var shortNames: Record; - - new(hash: int, classDecl: VstClass, typeCon: TypeCon) - super(hash, classDecl, typeCon, null) { - var w = classDecl.tagType.width; - width = w; - byteSize = if(w == 0, byte.view(0), if(w <= 8, byte.view(1), if(w <= 16, byte.view(2), if(w <= 32, byte.view(4), byte.view(8))))); - } - def lookupCase(name: string) -> VstCaseMember { - var member = classDecl.memberMap[name]; - if (VstCaseMember.?(member)) return VstCaseMember.!(member); - return null; - } - def getNameOperator() -> Operator { - var types: Array = [this]; - return V3Op.newVstSugar(VstOperator.VariantGetName, types, types, V3.stringType); - } - def getShortNameOperator() -> Operator { - var types: Array = [this]; - return V3Op.newVstSugar(VstOperator.VariantGetShortName, types, types, V3.stringType); - } - def enumGetParamOperator(f: VstField) -> Operator { - var types: Array = [this]; - return V3Op.newVstSugar(VstOperator.EnumGetParam(f), TypeUtil.NO_TYPES, types, f.getType()); - } - // Get the qualified names array (e.g. "More.C" for a subtype case). - // Array is indexed by DFS enumTag; size = tagHi + 1. - def getNames(prog: Program) -> Record { - if (names == null) { - var total = if(classDecl.tagHi >= 0, classDecl.tagHi + 1, classDecl.cases.length); - names = prog.newRecord(V3.stringArrayType, total); - CaseNames.fillNames(names, classDecl, "", prog); - } - return names; - } - def getShortNames(prog: Program) -> Record { - if (shortNames == null) { - var total = if(classDecl.tagHi >= 0, classDecl.tagHi + 1, classDecl.cases.length); - shortNames = prog.newRecord(V3.stringArrayType, total); - CaseNames.fillShortNames(shortNames, classDecl, prog); - } - return shortNames; - } -} // The type for user-declared enums' sets. class EnumSetType extends PrimType { def enumDecl: VstClass; diff --git a/aeneas/src/vst/MethodEnv.v3 b/aeneas/src/vst/MethodEnv.v3 index 8a783feeb..230aadc64 100644 --- a/aeneas/src/vst/MethodEnv.v3 +++ b/aeneas/src/vst/MethodEnv.v3 @@ -164,7 +164,7 @@ class MethodEnv { } return VarBinding.None; } - def resolveEnumMember(expr: VarExpr, enumType: EnumType, name: string) -> ResolvedMember { + def resolveEnumMember(expr: VarExpr, enumType: ClassType, name: string) -> ResolvedMember { var decl = enumType.classDecl; var member = decl.memberMap[name]; if (decl.qualifiers != null) { @@ -325,12 +325,12 @@ class MethodEnv { return expr.bind(null, VarBinding.Const(Int.box(V3.getVariantTag(variantType)), tagType), tagType); } if (VstNameMember.?(member.member)) { - var names = V3Class_TypeCon.!(V3.getRootType(variantType).typeCon).getNames(verifier.verifier.prog); + var names = ClassType.!(V3.getRootType(variantType)).getNames(verifier.verifier.prog); var name = names.values[V3.getVariantTag(variantType)]; return expr.bind(null, VarBinding.Const(name, V3.stringType), V3.stringType); } if (VstShortNameMember.?(member.member)) { - var names = V3Class_TypeCon.!(V3.getRootType(variantType).typeCon).getShortNames(verifier.verifier.prog); + var names = ClassType.!(V3.getRootType(variantType)).getShortNames(verifier.verifier.prog); var name = names.values[V3.getVariantTag(variantType)]; return expr.bind(null, VarBinding.Const(name, V3.stringType), V3.stringType); } @@ -339,7 +339,7 @@ class MethodEnv { return lookupVariantExprMember(expr, variantType, false); } - def lookupEnumTypeMember(expr: VarExpr, enumType: EnumType) -> VarBinding { + def lookupEnumTypeMember(expr: VarExpr, enumType: ClassType) -> VarBinding { // lookup (type: enum).name var name = expr.ident.name.image; if (Strings.equal(name, "set")) { @@ -362,7 +362,7 @@ class MethodEnv { if (enumType.classDecl.subtypes != null) { var sub = enumType.classDecl.subtypes[name]; if (sub != null) { - var subType = EnumType.!(sub.typeCon.singleton); + var subType = ClassType.!(sub.typeCon.singleton); return expr.bind(null, VarBinding.Type(subType), subType); } } @@ -395,7 +395,7 @@ class MethodEnv { } return VarBinding.None; } - def lookupEnumExprMember(expr: VarExpr, enumType: EnumType, useThis: bool) -> VarBinding { + def lookupEnumExprMember(expr: VarExpr, enumType: ClassType, useThis: bool) -> VarBinding { // lookup (expr: enum).name var name = expr.ident.name; if (Strings.equal(name.image, "shortName")) { @@ -426,7 +426,7 @@ class MethodEnv { } return VarBinding.None; } - def lookupEnumConstMember(expr: VarExpr, enumType: EnumType, vc: VstCaseMember) -> VarBinding { + def lookupEnumConstMember(expr: VarExpr, enumType: ClassType, vc: VstCaseMember) -> VarBinding { // lookup (expr: enum.val).name var name = expr.ident.name; if (Strings.equal(name.image, "shortName")) { @@ -700,7 +700,7 @@ class MethodEnv { COMPONENT => binding = lookupComponentTypeMember(expr, etype); CLASS => binding = lookupClassTypeMember(expr, ClassType.!(etype)); VARIANT => binding = lookupVariantTypeMember(expr, ClassType.!(etype)); - ENUM => binding = lookupEnumTypeMember(expr, EnumType.!(etype)); + ENUM => binding = lookupEnumTypeMember(expr, ClassType.!(etype)); ENUM_SET => binding = lookupEnumSetTypeMember(expr, etype); FLOAT => binding = lookupFloatTypeMember(expr, FloatType.!(etype)); INT => binding = lookupIntTypeMember(expr, IntType.!(etype)); @@ -767,7 +767,7 @@ class MethodEnv { BOOL => return V3Op.opBoolAnd; ENUM_SET => return EnumSetType.!(utype).opcache().opIntersect; ENUM => { - var setType = utype = EnumType.!(utype).setType; + var setType = utype = ClassType.!(utype).setType; return if(utype != null, setType.opcache().opIntersect); } INT => return IntType.!(utype).opAnd(); @@ -779,7 +779,7 @@ class MethodEnv { BOOL => return V3Op.opBoolOr; ENUM_SET => return EnumSetType.!(utype).opcache().opUnion; ENUM => { - var setType = utype = EnumType.!(utype).setType; + var setType = utype = ClassType.!(utype).setType; return if(utype != null, setType.opcache().opUnion); } INT => return IntType.!(utype).opOr(); @@ -914,7 +914,7 @@ class MethodEnv { CLASS => binding = lookupClassExprMember(expr, ClassType.!(etype), useThis); TUPLE => binding = lookupTupleExprMember(expr, etype); VARIANT => binding = lookupVariantExprMember(expr, ClassType.!(etype), useThis); - ENUM => binding = lookupEnumExprMember(expr, EnumType.!(etype), useThis); + ENUM => binding = lookupEnumExprMember(expr, ClassType.!(etype), useThis); ENUM_SET => binding = lookupEnumSetExprMember(expr, etype); FLOAT => binding = lookupFloatExprMember(expr, FloatType.!(etype)); POINTER => binding = lookupPointerExprMember(expr, etype); diff --git a/aeneas/src/vst/Verifier.v3 b/aeneas/src/vst/Verifier.v3 index 3afab1712..90934fe5e 100644 --- a/aeneas/src/vst/Verifier.v3 +++ b/aeneas/src/vst/Verifier.v3 @@ -92,9 +92,8 @@ class Verifier(compiler: Compiler, prog: Program) { for (i < file.enums.length) { var enumDecl = file.enums[i]; enumDecl.tagType = V3.getTagType(enumDecl.cases.length); - var tc = TypeCon.new(enumDecl.token.image, Kind.ENUM, 0, prog.typeCache); - var enumType = EnumType.new(tc.uid, enumDecl, tc); - tc.singleton = enumType; + var tc = V3Class_TypeCon.new(enumDecl, prog.typeCache); + var enumType = ClassType.!(tc.singleton); enumDecl.typeCon = tc; if (enumDecl.cases.length <= Int.MAX_WIDTH) { var setName = Strings.builderOf(enumDecl.token.image).puts(".set").toString(); @@ -562,7 +561,7 @@ class Verifier(compiler: Compiler, prog: Program) { Strings.format1("qualifier \"%s\" does not name an enum type", firstTok.image)); return; } - var parentDecl = EnumType.!(tc.create0()).classDecl; + var parentDecl = ClassType.!(tc.create0()).classDecl; if (parentDecl.qualifiers != null) { cv.errAtDecl(decl).set("EnumDeclError", Strings.format1("\"%s\" must be a top-level enum type", firstTok.image)); @@ -670,7 +669,7 @@ class Verifier(compiler: Compiler, prog: Program) { if (root.qualifiers != null) { var tc = prog.typeEnv.lookup(root.qualifiers.head.image); if (tc != null && tc.kind == Kind.ENUM) { - root = EnumType.!(tc.create0()).classDecl; + root = ClassType.!(tc.create0()).classDecl; } } return root; @@ -738,7 +737,7 @@ class Verifier(compiler: Compiler, prog: Program) { } def finalizeEnumType(decl: VstClass) { if (decl.qualifiers != null) return; // only process from root - var enumType = EnumType.!(decl.typeCon.singleton); + var enumType = ClassType.!(decl.typeCon.singleton); var isHierarchy = decl.hasDefault || decl.subtypeOrder != null; if (!isHierarchy) { // Non-hierarchy enum: constructor-created set type is correct. @@ -779,7 +778,7 @@ class Verifier(compiler: Compiler, prog: Program) { } def propagateEnumSetType(decl: VstClass, setType: EnumSetType) { for (l = decl.subtypeOrder; l != null; l = l.tail) { - EnumType.!(l.head.typeCon.singleton).setType = setType; + ClassType.!(l.head.typeCon.singleton).setType = setType; propagateEnumSetType(l.head, setType); } } @@ -1340,9 +1339,11 @@ class VstCompoundVerifier { if (t != null) match (t) { x: IntType => scale = x.packedByteSize; x: FloatType => scale = x.byteSize; - x: EnumType => scale = x.byteSize; x: BoolType => scale = 1; - _ => errAtToken(decl.mtref.tname.name).FieldDeclError(Strings.format1("expected primitive layout field type, found \"%q\"", t.render)); + _ => { + if (V3.isEnum(t)) scale = ClassType.!(t).byteSize; + else errAtToken(decl.mtref.tname.name).FieldDeclError(Strings.format1("expected primitive layout field type, found \"%q\"", t.render)); + } } } for (l = decl.repHints; l != null; l = l.tail) { @@ -1492,9 +1493,9 @@ class VstCompoundVerifier { return x.binding = resolveSubtypeWithArgs(x, subDecl, leftType); } } - } else if (EnumType.?(leftType)) { + } else if (V3.isEnum(leftType)) { // Enum.set or Enum.SubtypeEnum resolution. - var enumType = EnumType.!(leftType); + var enumType = ClassType.!(leftType); if (Strings.equal(x.name.image, "set")) { if (x.nested != null) { errAtRange(x.name.range()).TypeParamArityError("enum set type", @@ -1516,7 +1517,7 @@ class VstCompoundVerifier { // Fall back: subtypes map may not be populated yet (early type resolution). var subTc = verifier.prog.typeEnv.lookup(x.name.image); if (subTc != null && subTc.kind == Kind.ENUM) { - var subDecl = EnumType.!(subTc.create0()).classDecl; + var subDecl = ClassType.!(subTc.create0()).classDecl; if (subDecl.qualifiers != null) { // Accept if qualifiers start with the parent enum's name. var q = subDecl.qualifiers; @@ -1609,7 +1610,7 @@ class VstCompoundVerifier { errAtRange(arg.range()).set("TypeError", Strings.format2("%s expects a layout name, found \"%q\"", tref.name.image, arg.render)); return tref.binding = getErrorType(); } - def getEnumSetType(tref: TypeRef, enumType: EnumType) -> Type { + def getEnumSetType(tref: TypeRef, enumType: ClassType) -> Type { if (enumType.setType == null) { errAtRange(tref.range()).EnumSetTooLarge(enumType, tref, enumType.classDecl.cases.length); return tref.binding = getErrorType(); @@ -1886,7 +1887,7 @@ class TypeChecker(ERROR: ErrorGen, file: VstFile) extends VstVisitor if (ve.expr == null) { match (methodEnv.lookupVar(VarExpr.!(expr))) { Type(t) => { - if (EnumType.?(t)) return elim(t); + if (V3.isEnum(t)) return elim(t); errAtExpr(expr).set("TypeError", Strings.format1("foreach requires iterable expression and found %s", TYPE(elim(t)))); return getErrorType(); } @@ -2174,7 +2175,7 @@ class TypeChecker(ERROR: ErrorGen, file: VstFile) extends VstVisitor if (vt.classDecl.subtypes != null) { var subtypeDecl = vt.classDecl.subtypes[name]; if (subtypeDecl != null) { - pat.subtypeEnumType = EnumType.!(subtypeDecl.typeCon.singleton); + pat.subtypeEnumType = ClassType.!(subtypeDecl.typeCon.singleton); return; } } @@ -2309,7 +2310,7 @@ class TypeChecker(ERROR: ErrorGen, file: VstFile) extends VstVisitor } EnumConst(member) => { etype = member.receiver.getDeclaredType(); - binding = methodEnv.lookupEnumConstMember(expr, EnumType.!(etype), member); + binding = methodEnv.lookupEnumConstMember(expr, ClassType.!(etype), member); } LayoutDecl(decl) => { binding = methodEnv.lookupLayoutMember(expr, decl); @@ -3181,7 +3182,7 @@ class ResolvedMember(receiver: Type, member: VstMember, typeArgs: TypeArgs, memb // for match statements. class MatchStmtVerifier(etype: Type, tc: TypeChecker) { var variantType: ClassType; - var enumType: EnumType; + var enumType: ClassType; var valueMap: PartialMap; var covered: Array; var default: MatchCase; @@ -3198,8 +3199,8 @@ class MatchStmtVerifier(etype: Type, tc: TypeChecker) { } variantType = vt; covered = Array.new(variantType.classDecl.cases.length); - } else if (EnumType.?(etype)) { - enumType = EnumType.!(etype); + } else if (V3.isEnum(etype)) { + enumType = ClassType.!(etype); covered = Array.new(enumType.classDecl.cases.length); } } diff --git a/aeneas/src/wasm/WasmGcTarget.v3 b/aeneas/src/wasm/WasmGcTarget.v3 index d36aab81e..bc3a2dca3 100644 --- a/aeneas/src/wasm/WasmGcTarget.v3 +++ b/aeneas/src/wasm/WasmGcTarget.v3 @@ -140,8 +140,8 @@ class WasmGcTarget extends Target { compiler.NormConfig.BoxEnumClosureReceiver = true; } private def getScalar(compiler: Compiler, prog: Program, t: Type) -> Scalar.set { + if (V3.isEnum(t)) return if(V3.getDecl(t).tagType.width <= 32, Scalar.B32 | Scalar.B64, Scalar.B64); // XXX: Scalar.R32, once packed refs match (t) { - x: EnumType => return if(x.classDecl.tagType.width <= 32, Scalar.B32 | Scalar.B64, Scalar.B64); // XXX: Scalar.R32, once packed refs x: EnumSetType => return if(x.repType.width <= 32, Scalar.B32 | Scalar.B64, Scalar.B64); // XXX: Scalar.R32, once packed refs x: BoolType => return Scalar.B32; x: IntType => return if(x.width <= 32, Scalar.B32 | Scalar.B64, Scalar.B64); diff --git a/aeneas/src/wasm/WasmTarget.v3 b/aeneas/src/wasm/WasmTarget.v3 index f2ebd838d..cb691e8da 100644 --- a/aeneas/src/wasm/WasmTarget.v3 +++ b/aeneas/src/wasm/WasmTarget.v3 @@ -111,8 +111,8 @@ class WasmTarget extends Target { } private def getScalar(compiler: Compiler, prog: Program, t: Type) -> Scalar.set { var none: Scalar.set; + if (V3.isEnum(t)) return if(V3.getDecl(t).tagType.width <= 32, Scalar.B32 | Scalar.B64, Scalar.B64); // XXX: Scalar.R32, once packed refs match (t) { - x: EnumType => return if(x.classDecl.tagType.width <= 32, Scalar.B32 | Scalar.B64, Scalar.B64); // XXX: Scalar.R32, once packed refs x: EnumSetType => return if(x.repType.width <= 32, Scalar.B32 | Scalar.B64, Scalar.B64); // XXX: Scalar.R32, once packed refs x: BoolType => return Scalar.B32 | Scalar.B64; x: IntType => return if(x.width <= 32, Scalar.B32 | Scalar.B64, Scalar.B64); // XXX: Scalar.R64, once packed refs From f9ea704a2f52f2e3f20ed0dc7ce820e140cb5178 Mon Sep 17 00:00:00 2001 From: Eliot Moss Date: Tue, 21 Apr 2026 11:22:42 -0400 Subject: [PATCH 41/58] Use RC_ENUM flag instead of V3.isEnum() in normalization pipeline Set RC_ENUM on enum RaClasses early in Reachability.makeType(), then use rc.raFacts.RC_ENUM in Normalization.v3 (visitMethod, layoutMtable) and SsaNormalizer.v3 (CallMethod, CallVariantVirtual, VariantGetMethod, VariantGetVirtual, boxEnumClosureReceiver) instead of repeated V3.isEnum(rc.oldType) calls. Co-Authored-By: Claude Opus 4.6 (1M context) --- aeneas/src/ir/Normalization.v3 | 10 ++--- aeneas/src/ir/Reachability.v3 | 1 + aeneas/src/ir/SsaNormalizer.v3 | 76 ++++++++++++++-------------------- 3 files changed, 37 insertions(+), 50 deletions(-) diff --git a/aeneas/src/ir/Normalization.v3 b/aeneas/src/ir/Normalization.v3 index fc45d723e..98bbad37b 100644 --- a/aeneas/src/ir/Normalization.v3 +++ b/aeneas/src/ir/Normalization.v3 @@ -161,9 +161,8 @@ class ReachabilityNormalizer(config: NormalizerConfig, ra: ReachabilityAnalyzer) if (rm.norm != null) return; // already done var ftype = rm.getSpec().getMethodType(); if (rc.isUnboxed()) { - // move flattened data type receiver to function sig ftype = Function.prependParamTypes(rc.variantNorm.sub, ftype); - } else if (V3.isEnum(rc.oldType)) { + } else if (rc.raFacts.RC_ENUM) { var tagType = V3.getVariantTagType(rc.oldType); if (!config.NonRefClosureReceiver && m.source != null && VstMethod.?(m.source)) { ftype = Function.prependParamType(Oop.TYPE, ftype); @@ -619,10 +618,9 @@ class ReachabilityNormalizer(config: NormalizerConfig, ra: ReachabilityAnalyzer) var table = Array.new(size), mtable = IrMtable.new(rm.norm, rc.minClassId, table); rv.mtable = mtable; - if (rc.isUnboxed() || V3.isEnum(rc.oldType)) { + if (rc.isUnboxed() || rc.raFacts.RC_ENUM) { var methType = rm.norm.getMethodType(); - // JVM enum user methods: strip Oop tag from mtable funcRefType - if (!config.NonRefClosureReceiver && V3.isEnum(rc.oldType) + if (!config.NonRefClosureReceiver && rc.raFacts.RC_ENUM && rm.orig.source != null && VstMethod.?(rm.orig.source)) { var paramList = Function.getParamTypeList(methType); if (paramList != null && paramList.head == Oop.TYPE) { @@ -635,7 +633,7 @@ class ReachabilityNormalizer(config: NormalizerConfig, ra: ReachabilityAnalyzer) mtable.record = ra.prog.newRecord(V3Array.newType(ft), size); } if (mtable.table.length > 0) { - if (V3.isEnum(rc.oldType)) { + if (rc.raFacts.RC_ENUM) { // Enum: fill by walking the hierarchy. Each enum type // covers a tag range; subtypes override their parent's range. fillEnumMtable(mtable, rc, rm); diff --git a/aeneas/src/ir/Reachability.v3 b/aeneas/src/ir/Reachability.v3 index 0b018cac3..48c2be132 100644 --- a/aeneas/src/ir/Reachability.v3 +++ b/aeneas/src/ir/Reachability.v3 @@ -736,6 +736,7 @@ class ReachabilityAnalyzer(compilation: Compilation) { var parentRc = if(decl.parentEnum != null, RaClass.!(makeClass(decl.parentEnum.getDeclaredType()))); var rc = newRaClass(t, oldIr.makeIrClass(t), parentRc); + rc.raFacts |= RaFact.RC_ENUM; raType = rc; typeMap[t] = raType; addEnumSubtypesRecursive(rc, decl); diff --git a/aeneas/src/ir/SsaNormalizer.v3 b/aeneas/src/ir/SsaNormalizer.v3 index ed4e6a2fb..e5b395815 100644 --- a/aeneas/src/ir/SsaNormalizer.v3 +++ b/aeneas/src/ir/SsaNormalizer.v3 @@ -63,7 +63,6 @@ class SsaRaNormalizer extends SsaRebuilder { newParams.put(SsaParam.new(newParams.length, Oop.TYPE)); newRanges.put(start, newParams.length); continue; - // native/wasm enum: receiver maps to prepended tag param } } if (tn.size == 1) { @@ -236,7 +235,7 @@ class SsaRaNormalizer extends SsaRebuilder { obj = boxEnumClosureReceiver(rc, obj); var spec = extractMethodRef(orig, method).1; var fref: SsaInstr; - if (V3.isEnum(rc.oldType) + if (rc.raFacts.RC_ENUM && (!context.compiler.NormConfig.NonRefClosureReceiver || context.compiler.NormConfig.ExplicitRefTypeCast)) { var funcNorm = normFuncType(i_old.getType()); @@ -250,13 +249,11 @@ class SsaRaNormalizer extends SsaRebuilder { var rc = norm.ra.getClass(op.typeArgs[0]); var t = extractVirtualRef(orig, method), obj = genRef1(args[0]); if (t.2) { // still a virtual dispatch - if (V3.isEnum(rc.oldType)) { - // enum: index mtable with tag (not selector on record) + if (rc.raFacts.RC_ENUM) { var sel = IrSelector.!(t.1.member); if (sel.mtable != null && sel.mtable.record != null) { var tag = obj; - var tagType = V3.getVariantTagType(rc.oldType); - var func = lookupMtable(sel, tag, tagType); + var func = lookupMtable(sel, tag, V3.getVariantTagType(rc.oldType)); obj = boxEnumClosureReceiver(rc, obj); mapN(i_old, [func, obj]); } else { @@ -291,13 +288,10 @@ class SsaRaNormalizer extends SsaRebuilder { // add back void for component ai_new = Arrays.prepend(context.graph.nullConst(m.receiver), ai_new); } - if (V3.isVariant(rc.oldType)) { - if (rc.isUnboxed()) { - // flattened data type becomes component call and needs new receiver - ai_new = Arrays.prepend(newGraph.nullReceiver(), ai_new); - } + if (V3.isVariant(rc.oldType) && rc.isUnboxed()) { + ai_new = Arrays.prepend(newGraph.nullReceiver(), ai_new); } - if (V3.isEnum(rc.oldType)) { + if (rc.raFacts.RC_ENUM) { i_old.facts |= Fact.O_NO_NULL_CHECK; if (!context.compiler.NormConfig.NonRefClosureReceiver && method.source != null && VstMethod.?(method.source)) { @@ -339,48 +333,42 @@ class SsaRaNormalizer extends SsaRebuilder { CallVariantVirtual(method) => { // devirtualize methods that are not overridden var rc = norm.ra.getClass(op.typeArgs[0]); - if (V3.isEnum(rc.oldType)) i_old.facts |= Fact.O_NO_NULL_CHECK; + if (rc.raFacts.RC_ENUM) i_old.facts |= Fact.O_NO_NULL_CHECK; var t = extractVirtualRef(orig, method), funcNorm = t.0, m = t.1; var ai_new = normArgs(funcNorm, genRefs(i_old.inputs)); if (t.2) { // still a virtual dispatch - if (V3.isEnum(rc.oldType)) { - var sel = IrSelector.!(m.member); - if (sel.mtable != null && sel.mtable.record != null) { - // enum: tag IS the value; index into mtable - var tag = ai_new[0]; - var tagType = V3.getVariantTagType(rc.oldType); - var func = lookupMtable(sel, tag, tagType); - if (context.compiler.NormConfig.NonRefClosureReceiver - && !context.compiler.NormConfig.ExplicitRefTypeCast) { - // native/wasm (non-gc): CallFunctionDirect (no Oop receiver) - ai_new = Arrays.prepend(func, ai_new); - normCall(i_old, funcNorm, V3Op.newCallFunctionDirect(funcNorm.sub[0]), ai_new); - } else { - // JVM/wasm-gc: box tag, CallFunction with user type - var boxedTag = curBlock.opTypeSubsume(tagType, Oop.TYPE, tag); - var userArgs = Arrays.range(ai_new, 1, ai_new.length); - var userFuncType = Function.newType( - if(userArgs.length > 0, Tuple.fromTypeArray(Arrays.map(userArgs, SsaInstr.getType)), Void.TYPE), - funcNorm.sig().returnType()); - ai_new = Arrays.concat([func, boxedTag], userArgs); - normCall(i_old, funcNorm, V3Op.newCallFunction(userFuncType), ai_new); - } + var sel = IrSelector.!(m.member); + if (rc.raFacts.RC_ENUM && sel.mtable != null && sel.mtable.record != null) { + var tag = ai_new[0]; + var tagType = V3.getVariantTagType(rc.oldType); + var func = lookupMtable(sel, tag, tagType); + if (context.compiler.NormConfig.NonRefClosureReceiver + && !context.compiler.NormConfig.ExplicitRefTypeCast) { + ai_new = Arrays.prepend(func, ai_new); + normCall(i_old, funcNorm, V3Op.newCallFunctionDirect(funcNorm.sub[0]), ai_new); } else { - // mtable not built (single impl live); direct call - normCall(i_old, funcNorm, V3Op.newCallMethod(m), ai_new); + var boxedTag = curBlock.opTypeSubsume(tagType, Oop.TYPE, tag); + var userArgs = Arrays.range(ai_new, 1, ai_new.length); + var userFuncType = Function.newType( + if(userArgs.length > 0, Tuple.fromTypeArray(Arrays.map(userArgs, SsaInstr.getType)), Void.TYPE), + funcNorm.sig().returnType()); + ai_new = Arrays.concat([func, boxedTag], userArgs); + normCall(i_old, funcNorm, V3Op.newCallFunction(userFuncType), ai_new); } - } else if (rc.isUnboxed()) { - // use the variant tag as an index into a table of functions + } else if (rc.isUnboxed() && !rc.raFacts.RC_ENUM && sel.mtable != null && sel.mtable.record != null) { var tag = normVariantGetTag(rc.variantNorm, ai_new); - var func = lookupMtable(IrSelector.!(m.member), tag, rc.variantNorm.tagType()); + var func = lookupMtable(sel, tag, rc.variantNorm.tagType()); ai_new = Arrays.concat([func, newGraph.nullReceiver()], ai_new); normCall(i_old, funcNorm, V3Op.newCallFunction(funcNorm.sub[0]), ai_new); - } else { + } else if (!rc.isUnboxed()) { normCall(i_old, funcNorm, V3Op.newCallVariantSelector(m), ai_new); + } else { + // devirt fallback: single impl live, no mtable + if (!rc.raFacts.RC_ENUM) ai_new = Arrays.prepend(newGraph.nullReceiver(), ai_new); + normCall(i_old, funcNorm, V3Op.newCallMethod(m), ai_new); } } else { - // devirtualized: enum tag already in args; unboxed variant needs null receiver - if (rc.isUnboxed()) ai_new = Arrays.prepend(newGraph.nullReceiver(), ai_new); + if (rc.isUnboxed() && !rc.raFacts.RC_ENUM) ai_new = Arrays.prepend(newGraph.nullReceiver(), ai_new); normCall(i_old, funcNorm, V3Op.newCallMethod(m), ai_new); } } @@ -1921,7 +1909,7 @@ class SsaRaNormalizer extends SsaRebuilder { } // For JVM/wasm-gc: box enum tag to Oop for closure receiver slot. private def boxEnumClosureReceiver(rc: RaClass, obj: SsaInstr) -> SsaInstr { - if (V3.isEnum(rc.oldType) + if (rc.raFacts.RC_ENUM && (!context.compiler.NormConfig.NonRefClosureReceiver || context.compiler.NormConfig.ExplicitRefTypeCast)) { return curBlock.opTypeSubsume(V3.getVariantTagType(rc.oldType), Oop.TYPE, obj); From 48ac667cfebb3444ca3a92cf4629284705699e5e Mon Sep 17 00:00:00 2001 From: Eliot Moss Date: Tue, 21 Apr 2026 11:34:21 -0400 Subject: [PATCH 42/58] Revert RC_ENUM usage where it conflicts with variant-enums RC_ENUM is set by VariantNormalizer for fieldless variants too, not just actual enum types. Using it in layoutMtable and SsaNormalizer caused variant-enums to take the wrong dispatch path (enum-specific hierarchy walk instead of variant liveClasses). Revert to V3.isEnum() in these locations; keep RC_ENUM only in visitMethod where the distinction doesn't matter (variants are caught by isUnboxed() first). Fixes zload_elim04.v3 and zload_elim05.v3. Co-Authored-By: Claude Opus 4.6 (1M context) --- aeneas/src/ir/Normalization.v3 | 6 +++--- aeneas/src/ir/SsaNormalizer.v3 | 18 +++++++++--------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/aeneas/src/ir/Normalization.v3 b/aeneas/src/ir/Normalization.v3 index 98bbad37b..d6aaa2d2a 100644 --- a/aeneas/src/ir/Normalization.v3 +++ b/aeneas/src/ir/Normalization.v3 @@ -618,9 +618,9 @@ class ReachabilityNormalizer(config: NormalizerConfig, ra: ReachabilityAnalyzer) var table = Array.new(size), mtable = IrMtable.new(rm.norm, rc.minClassId, table); rv.mtable = mtable; - if (rc.isUnboxed() || rc.raFacts.RC_ENUM) { + if (rc.isUnboxed() || V3.isEnum(rc.oldType)) { var methType = rm.norm.getMethodType(); - if (!config.NonRefClosureReceiver && rc.raFacts.RC_ENUM + if (!config.NonRefClosureReceiver && V3.isEnum(rc.oldType) && rm.orig.source != null && VstMethod.?(rm.orig.source)) { var paramList = Function.getParamTypeList(methType); if (paramList != null && paramList.head == Oop.TYPE) { @@ -633,7 +633,7 @@ class ReachabilityNormalizer(config: NormalizerConfig, ra: ReachabilityAnalyzer) mtable.record = ra.prog.newRecord(V3Array.newType(ft), size); } if (mtable.table.length > 0) { - if (rc.raFacts.RC_ENUM) { + if (V3.isEnum(rc.oldType)) { // Enum: fill by walking the hierarchy. Each enum type // covers a tag range; subtypes override their parent's range. fillEnumMtable(mtable, rc, rm); diff --git a/aeneas/src/ir/SsaNormalizer.v3 b/aeneas/src/ir/SsaNormalizer.v3 index e5b395815..4a20ff8f2 100644 --- a/aeneas/src/ir/SsaNormalizer.v3 +++ b/aeneas/src/ir/SsaNormalizer.v3 @@ -235,7 +235,7 @@ class SsaRaNormalizer extends SsaRebuilder { obj = boxEnumClosureReceiver(rc, obj); var spec = extractMethodRef(orig, method).1; var fref: SsaInstr; - if (rc.raFacts.RC_ENUM + if (V3.isEnum(rc.oldType) && (!context.compiler.NormConfig.NonRefClosureReceiver || context.compiler.NormConfig.ExplicitRefTypeCast)) { var funcNorm = normFuncType(i_old.getType()); @@ -249,7 +249,7 @@ class SsaRaNormalizer extends SsaRebuilder { var rc = norm.ra.getClass(op.typeArgs[0]); var t = extractVirtualRef(orig, method), obj = genRef1(args[0]); if (t.2) { // still a virtual dispatch - if (rc.raFacts.RC_ENUM) { + if (V3.isEnum(rc.oldType)) { var sel = IrSelector.!(t.1.member); if (sel.mtable != null && sel.mtable.record != null) { var tag = obj; @@ -291,7 +291,7 @@ class SsaRaNormalizer extends SsaRebuilder { if (V3.isVariant(rc.oldType) && rc.isUnboxed()) { ai_new = Arrays.prepend(newGraph.nullReceiver(), ai_new); } - if (rc.raFacts.RC_ENUM) { + if (V3.isEnum(rc.oldType)) { i_old.facts |= Fact.O_NO_NULL_CHECK; if (!context.compiler.NormConfig.NonRefClosureReceiver && method.source != null && VstMethod.?(method.source)) { @@ -333,12 +333,12 @@ class SsaRaNormalizer extends SsaRebuilder { CallVariantVirtual(method) => { // devirtualize methods that are not overridden var rc = norm.ra.getClass(op.typeArgs[0]); - if (rc.raFacts.RC_ENUM) i_old.facts |= Fact.O_NO_NULL_CHECK; + if (V3.isEnum(rc.oldType)) i_old.facts |= Fact.O_NO_NULL_CHECK; var t = extractVirtualRef(orig, method), funcNorm = t.0, m = t.1; var ai_new = normArgs(funcNorm, genRefs(i_old.inputs)); if (t.2) { // still a virtual dispatch var sel = IrSelector.!(m.member); - if (rc.raFacts.RC_ENUM && sel.mtable != null && sel.mtable.record != null) { + if (V3.isEnum(rc.oldType) && sel.mtable != null && sel.mtable.record != null) { var tag = ai_new[0]; var tagType = V3.getVariantTagType(rc.oldType); var func = lookupMtable(sel, tag, tagType); @@ -355,7 +355,7 @@ class SsaRaNormalizer extends SsaRebuilder { ai_new = Arrays.concat([func, boxedTag], userArgs); normCall(i_old, funcNorm, V3Op.newCallFunction(userFuncType), ai_new); } - } else if (rc.isUnboxed() && !rc.raFacts.RC_ENUM && sel.mtable != null && sel.mtable.record != null) { + } else if (rc.isUnboxed() && !V3.isEnum(rc.oldType) && sel.mtable != null && sel.mtable.record != null) { var tag = normVariantGetTag(rc.variantNorm, ai_new); var func = lookupMtable(sel, tag, rc.variantNorm.tagType()); ai_new = Arrays.concat([func, newGraph.nullReceiver()], ai_new); @@ -364,11 +364,11 @@ class SsaRaNormalizer extends SsaRebuilder { normCall(i_old, funcNorm, V3Op.newCallVariantSelector(m), ai_new); } else { // devirt fallback: single impl live, no mtable - if (!rc.raFacts.RC_ENUM) ai_new = Arrays.prepend(newGraph.nullReceiver(), ai_new); + if (!V3.isEnum(rc.oldType)) ai_new = Arrays.prepend(newGraph.nullReceiver(), ai_new); normCall(i_old, funcNorm, V3Op.newCallMethod(m), ai_new); } } else { - if (rc.isUnboxed() && !rc.raFacts.RC_ENUM) ai_new = Arrays.prepend(newGraph.nullReceiver(), ai_new); + if (rc.isUnboxed() && !V3.isEnum(rc.oldType)) ai_new = Arrays.prepend(newGraph.nullReceiver(), ai_new); normCall(i_old, funcNorm, V3Op.newCallMethod(m), ai_new); } } @@ -1909,7 +1909,7 @@ class SsaRaNormalizer extends SsaRebuilder { } // For JVM/wasm-gc: box enum tag to Oop for closure receiver slot. private def boxEnumClosureReceiver(rc: RaClass, obj: SsaInstr) -> SsaInstr { - if (rc.raFacts.RC_ENUM + if (V3.isEnum(rc.oldType) && (!context.compiler.NormConfig.NonRefClosureReceiver || context.compiler.NormConfig.ExplicitRefTypeCast)) { return curBlock.opTypeSubsume(V3.getVariantTagType(rc.oldType), Oop.TYPE, obj); From a0fae9d7fc52c113560ab6e1bb6b17d5e8396f15 Mon Sep 17 00:00:00 2001 From: Eliot Moss Date: Tue, 21 Apr 2026 14:02:21 -0400 Subject: [PATCH 43/58] Fix enum virtual dispatch fallback when mtable is null When an enum method is overridden (M_OVERRIDDEN) but only one impl is live, layoutMtable skips building the mtable (size==1). The flattened if/else chain lost the enum-specific fallback to CallMethod, causing enums to fall through to CallVariantSelector which requires an mtable. Restore the original structure: enum check first with its own no-mtable else clause, then unboxed variant, then boxed variant. Fixes enum_submethod07.v3 on x86-64-linux. Co-Authored-By: Claude Opus 4.6 (1M context) --- aeneas/src/ir/SsaNormalizer.v3 | 46 +++++++++++++++++----------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/aeneas/src/ir/SsaNormalizer.v3 b/aeneas/src/ir/SsaNormalizer.v3 index 4a20ff8f2..38b2b4a9e 100644 --- a/aeneas/src/ir/SsaNormalizer.v3 +++ b/aeneas/src/ir/SsaNormalizer.v3 @@ -337,35 +337,35 @@ class SsaRaNormalizer extends SsaRebuilder { var t = extractVirtualRef(orig, method), funcNorm = t.0, m = t.1; var ai_new = normArgs(funcNorm, genRefs(i_old.inputs)); if (t.2) { // still a virtual dispatch - var sel = IrSelector.!(m.member); - if (V3.isEnum(rc.oldType) && sel.mtable != null && sel.mtable.record != null) { - var tag = ai_new[0]; - var tagType = V3.getVariantTagType(rc.oldType); - var func = lookupMtable(sel, tag, tagType); - if (context.compiler.NormConfig.NonRefClosureReceiver - && !context.compiler.NormConfig.ExplicitRefTypeCast) { - ai_new = Arrays.prepend(func, ai_new); - normCall(i_old, funcNorm, V3Op.newCallFunctionDirect(funcNorm.sub[0]), ai_new); + if (V3.isEnum(rc.oldType)) { + var sel = IrSelector.!(m.member); + if (sel.mtable != null && sel.mtable.record != null) { + var tag = ai_new[0]; + var tagType = V3.getVariantTagType(rc.oldType); + var func = lookupMtable(sel, tag, tagType); + if (context.compiler.NormConfig.NonRefClosureReceiver + && !context.compiler.NormConfig.ExplicitRefTypeCast) { + ai_new = Arrays.prepend(func, ai_new); + normCall(i_old, funcNorm, V3Op.newCallFunctionDirect(funcNorm.sub[0]), ai_new); + } else { + var boxedTag = curBlock.opTypeSubsume(tagType, Oop.TYPE, tag); + var userArgs = Arrays.range(ai_new, 1, ai_new.length); + var userFuncType = Function.newType( + if(userArgs.length > 0, Tuple.fromTypeArray(Arrays.map(userArgs, SsaInstr.getType)), Void.TYPE), + funcNorm.sig().returnType()); + ai_new = Arrays.concat([func, boxedTag], userArgs); + normCall(i_old, funcNorm, V3Op.newCallFunction(userFuncType), ai_new); + } } else { - var boxedTag = curBlock.opTypeSubsume(tagType, Oop.TYPE, tag); - var userArgs = Arrays.range(ai_new, 1, ai_new.length); - var userFuncType = Function.newType( - if(userArgs.length > 0, Tuple.fromTypeArray(Arrays.map(userArgs, SsaInstr.getType)), Void.TYPE), - funcNorm.sig().returnType()); - ai_new = Arrays.concat([func, boxedTag], userArgs); - normCall(i_old, funcNorm, V3Op.newCallFunction(userFuncType), ai_new); + normCall(i_old, funcNorm, V3Op.newCallMethod(m), ai_new); } - } else if (rc.isUnboxed() && !V3.isEnum(rc.oldType) && sel.mtable != null && sel.mtable.record != null) { + } else if (rc.isUnboxed()) { var tag = normVariantGetTag(rc.variantNorm, ai_new); - var func = lookupMtable(sel, tag, rc.variantNorm.tagType()); + var func = lookupMtable(IrSelector.!(m.member), tag, rc.variantNorm.tagType()); ai_new = Arrays.concat([func, newGraph.nullReceiver()], ai_new); normCall(i_old, funcNorm, V3Op.newCallFunction(funcNorm.sub[0]), ai_new); - } else if (!rc.isUnboxed()) { - normCall(i_old, funcNorm, V3Op.newCallVariantSelector(m), ai_new); } else { - // devirt fallback: single impl live, no mtable - if (!V3.isEnum(rc.oldType)) ai_new = Arrays.prepend(newGraph.nullReceiver(), ai_new); - normCall(i_old, funcNorm, V3Op.newCallMethod(m), ai_new); + normCall(i_old, funcNorm, V3Op.newCallVariantSelector(m), ai_new); } } else { if (rc.isUnboxed() && !V3.isEnum(rc.oldType)) ai_new = Arrays.prepend(newGraph.nullReceiver(), ai_new); From 0c626a55631e4c4b40399a3d1b07adb91d7f810f Mon Sep 17 00:00:00 2001 From: Eliot Moss Date: Tue, 21 Apr 2026 16:46:12 -0400 Subject: [PATCH 44/58] Give enums VariantNorm; add isFlattened() to decouple from isUnboxed() Enums now get a VariantNorm in norm() so rc.isUnboxed() returns true, unifying several Normalization.v3 paths with the variant code. A new isFlattened() predicate (variantNorm != null && !V3.isEnum) guards SsaNormalizer sites that assume variant calling conventions (synthesized component, nullReceiver) which don't apply to actual enums yet. Co-Authored-By: Claude Opus 4.6 (1M context) --- aeneas/src/ir/Normalization.v3 | 45 ++++++++++++++++++++++++---------- aeneas/src/ir/Reachability.v3 | 1 + aeneas/src/ir/SsaNormalizer.v3 | 32 +++++++++++++----------- 3 files changed, 51 insertions(+), 27 deletions(-) diff --git a/aeneas/src/ir/Normalization.v3 b/aeneas/src/ir/Normalization.v3 index d6aaa2d2a..d8932b8f9 100644 --- a/aeneas/src/ir/Normalization.v3 +++ b/aeneas/src/ir/Normalization.v3 @@ -111,14 +111,10 @@ class ReachabilityNormalizer(config: NormalizerConfig, ra: ReachabilityAnalyzer) var ic = rc; while (ic.parent != null) ic = ic.parent; // start at root - if (V3.isVariant(ic.oldType)) { + if (V3.isVariant(ic.oldType) || V3.isEnum(ic.oldType)) { norm(ic.oldType); numberVariant(ic); if (!ic.isUnboxed()) makeNormFieldsAll(ic); - } else if (V3.isEnum(ic.oldType) && V3.getDecl(ic.oldType).subtypeOrder != null) { - norm(ic.oldType); - numberVariant(ic); - makeNormFieldsAll(ic); } else { layoutClass(ic); } @@ -135,7 +131,7 @@ class ReachabilityNormalizer(config: NormalizerConfig, ra: ReachabilityAnalyzer) normQueue.add(normClassRecord, (rc, oldRecord, newRecord)); // XXX: inline normClassRecord } ra.prog.state.setComponentRecord(comp, newRecord); - } else if (!rc.isUnboxed()) { + } else if (!rc.isFlattened()) { // create and map new records to be normalized for (l = rc.instances; l != null; l = l.tail) { var oldRecord = l.head, newRecord = ra.prog.newRecord(tn.newType, rc.liveFields.length); @@ -161,13 +157,11 @@ class ReachabilityNormalizer(config: NormalizerConfig, ra: ReachabilityAnalyzer) if (rm.norm != null) return; // already done var ftype = rm.getSpec().getMethodType(); if (rc.isUnboxed()) { - ftype = Function.prependParamTypes(rc.variantNorm.sub, ftype); - } else if (rc.raFacts.RC_ENUM) { - var tagType = V3.getVariantTagType(rc.oldType); - if (!config.NonRefClosureReceiver && m.source != null && VstMethod.?(m.source)) { + if (V3.isEnum(rc.oldType) && !config.NonRefClosureReceiver + && m.source != null && VstMethod.?(m.source)) { ftype = Function.prependParamType(Oop.TYPE, ftype); } else { - ftype = Function.prependParamType(tagType, ftype); + ftype = Function.prependParamTypes(rc.variantNorm.sub, ftype); } } rm.funcNorm = FuncNorm.!(norm(ftype)); @@ -283,7 +277,15 @@ class ReachabilityNormalizer(config: NormalizerConfig, ra: ReachabilityAnalyzer) else tn = TypeNorm.new(t, t, null); } ENUM => { - tn = TypeNorm.new(t, V3.getVariantTagType(t), null); + var tagType = V3.getVariantTagType(t); + var rc = ra.getClass(t); + if (rc != null) { + var root = rc; + while (root.parent != null) root = root.parent; + if (root.variantNorm == null) createEnumVariantNorms(root); + tn = rc.variantNorm; + } + if (tn == null) tn = TypeNorm.new(t, tagType, null); } REF => { var sub = [ @@ -618,7 +620,7 @@ class ReachabilityNormalizer(config: NormalizerConfig, ra: ReachabilityAnalyzer) var table = Array.new(size), mtable = IrMtable.new(rm.norm, rc.minClassId, table); rv.mtable = mtable; - if (rc.isUnboxed() || V3.isEnum(rc.oldType)) { + if (rc.isUnboxed()) { var methType = rm.norm.getMethodType(); if (!config.NonRefClosureReceiver && V3.isEnum(rc.oldType) && rm.orig.source != null && VstMethod.?(rm.orig.source)) { @@ -660,6 +662,23 @@ class ReachabilityNormalizer(config: NormalizerConfig, ra: ReachabilityAnalyzer) var impl = resolveMethodImpl(rc, rm); setMtableEntry(mtable, impl, rc.minClassId - mtable.rootId); } + private def createEnumVariantNorms(rc: RaClass) { + var tagType = V3.getVariantTagType(rc.oldType); + var tagTypeNorm = norm(tagType); + var noVFields = Array.new(0); + var tagField = VariantField.new(null, tagTypeNorm, [0]); + createEnumVariantNorm(rc, tagType, noVFields, tagField); + } + private def createEnumVariantNorm(rc: RaClass, tagType: IntType, noVFields: Array, tagField: VariantField) { + var vn = VariantNorm.new(rc.oldType, tagType, [tagType], noVFields, tagField); + vn.tagValue = V3.getTagLo(rc.oldType); + vn.tagHi = V3.getTagHi(rc.oldType); + rc.variantNorm = vn; + for (l = rc.children; l != null; l = l.tail) { + createEnumVariantNorm(l.head, tagType, noVFields, tagField); + vn.children = List.new(l.head.variantNorm, vn.children); + } + } private def fillEnumMtable(mtable: IrMtable, rc: RaClass, rm: RaMethod) { var impl = resolveEnumMethodImpl(rc, rm); if (impl == null) impl = rm; // fall back to root diff --git a/aeneas/src/ir/Reachability.v3 b/aeneas/src/ir/Reachability.v3 index 48c2be132..3c53c2099 100644 --- a/aeneas/src/ir/Reachability.v3 +++ b/aeneas/src/ir/Reachability.v3 @@ -887,6 +887,7 @@ class RaClass extends RaType { return true; } def isUnboxed() -> bool { return variantNorm != null; } + def isFlattened() -> bool { return variantNorm != null && !V3.isEnum(oldType); } } // Tracks instances of an array type. class RaArray extends RaType { diff --git a/aeneas/src/ir/SsaNormalizer.v3 b/aeneas/src/ir/SsaNormalizer.v3 index 38b2b4a9e..9ac64b5f5 100644 --- a/aeneas/src/ir/SsaNormalizer.v3 +++ b/aeneas/src/ir/SsaNormalizer.v3 @@ -54,15 +54,19 @@ class SsaRaNormalizer extends SsaRebuilder { newParams.put(SsaParam.new(newParams.length, tn.newType)); start++; // skip synthesized receiver } else if (VariantNorm.?(tn)) { - // receiver became flattened data - var newIrType = norm.ra.getClass(tn.oldType).newIrType; - newParams.put(SsaParam.new(newParams.length, newIrType)); - start++; // skip synthesized receiver - } else if (V3.isEnum(tn.oldType) && !norm.config.NonRefClosureReceiver - && context.method.source != null && VstMethod.?(context.method.source)) { - newParams.put(SsaParam.new(newParams.length, Oop.TYPE)); - newRanges.put(start, newParams.length); - continue; + if (V3.isEnum(tn.oldType) && !norm.config.NonRefClosureReceiver + && context.method.source != null && VstMethod.?(context.method.source)) { + // JVM enum: Oop receiver, no synthesized component + newParams.put(SsaParam.new(newParams.length, Oop.TYPE)); + newRanges.put(start, newParams.length); + continue; + } else if (!V3.isEnum(tn.oldType)) { + // variant: synthesized component receiver + var newIrType = norm.ra.getClass(tn.oldType).newIrType; + newParams.put(SsaParam.new(newParams.length, newIrType)); + start++; // skip synthesized receiver + } + // else: actual enum on native — no synthesized component } } if (tn.size == 1) { @@ -288,7 +292,7 @@ class SsaRaNormalizer extends SsaRebuilder { // add back void for component ai_new = Arrays.prepend(context.graph.nullConst(m.receiver), ai_new); } - if (V3.isVariant(rc.oldType) && rc.isUnboxed()) { + if (rc.isFlattened()) { ai_new = Arrays.prepend(newGraph.nullReceiver(), ai_new); } if (V3.isEnum(rc.oldType)) { @@ -359,7 +363,7 @@ class SsaRaNormalizer extends SsaRebuilder { } else { normCall(i_old, funcNorm, V3Op.newCallMethod(m), ai_new); } - } else if (rc.isUnboxed()) { + } else if (rc.isFlattened()) { var tag = normVariantGetTag(rc.variantNorm, ai_new); var func = lookupMtable(IrSelector.!(m.member), tag, rc.variantNorm.tagType()); ai_new = Arrays.concat([func, newGraph.nullReceiver()], ai_new); @@ -368,7 +372,7 @@ class SsaRaNormalizer extends SsaRebuilder { normCall(i_old, funcNorm, V3Op.newCallVariantSelector(m), ai_new); } } else { - if (rc.isUnboxed() && !V3.isEnum(rc.oldType)) ai_new = Arrays.prepend(newGraph.nullReceiver(), ai_new); + if (rc.isFlattened()) ai_new = Arrays.prepend(newGraph.nullReceiver(), ai_new); normCall(i_old, funcNorm, V3Op.newCallMethod(m), ai_new); } } @@ -1551,7 +1555,7 @@ class SsaRaNormalizer extends SsaRebuilder { return map0(i_old); } normType(raField.receiver); // XXX: normType() side-effect of flattening - if (raField != null && rc.isUnboxed()) { + if (raField != null && rc.isFlattened()) { // field of unboxed data type var vals = genVariantGetField(rc, raField, rc.variantNorm, ai_new); return mapNnf(i_old, vals); @@ -1623,7 +1627,7 @@ class SsaRaNormalizer extends SsaRebuilder { // OPT: remove write of zero-width field // OPT: remove write of write-only field return addNullCheck(i_old, receiver); - } else if (rc.isUnboxed()) { + } else if (rc.isFlattened()) { // init/set of field of flattened data type return map0(i_old); } else if (nf.length == 1) { From eb76ec96b560196bbba8272350fab7b40c098564 Mon Sep 17 00:00:00 2001 From: Eliot Moss Date: Tue, 21 Apr 2026 17:51:37 -0400 Subject: [PATCH 45/58] Eliminate isFlattened(); unify enum/variant dispatch guards; add 15 tests Remove isFlattened() predicate from RaClass. Replace with isUnboxed() outer guards and explicit V3.isEnum() inner checks where enum calling convention (tag-as-receiver) differs from variant (synthesized component). Generalize O_NO_NULL_CHECK and normNullCheck to all unboxed types. Remove dead normVariantQuery enum branch. Update docs to remove per-case method overrides (removed in Strategy A Step 1). Add 15 new enum tests covering zero-init dispatch, type query narrowing, combined features, 4-level hierarchies, and edge cases. Co-Authored-By: Claude Opus 4.6 (1M context) --- aeneas/src/ir/Normalization.v3 | 2 +- aeneas/src/ir/Reachability.v3 | 1 - aeneas/src/ir/SsaNormalizer.v3 | 78 +++++++++++++++++----------------- claude/todos-extended-enums.md | 66 ++++++++++++++++++++++++++++ doc/grammar-claude.md | 15 ++----- doc/tutorial/EnumMethods.md | 44 ++----------------- test/enums/enum_closure14.v3 | 11 +++++ test/enums/enum_closure15.v3 | 13 ++++++ test/enums/enum_submethod12.v3 | 13 ++++++ test/enums/enum_submethod13.v3 | 20 +++++++++ test/enums/enum_submethod14.v3 | 11 +++++ test/enums/enum_submethod15.v3 | 18 ++++++++ test/enums/enum_submethod16.v3 | 11 +++++ test/enums/open_enum11.v3 | 13 ++++++ test/enums/open_enum12.v3 | 15 +++++++ test/enums/open_enum13.v3 | 21 +++++++++ test/enums/open_enum14.v3 | 36 ++++++++++++++++ test/enums/open_field16.v3 | 18 ++++++++ test/enums/open_field17.v3 | 11 +++++ test/enums/open_set05.v3 | 15 +++++++ test/enums/open_set06.v3 | 17 ++++++++ 21 files changed, 355 insertions(+), 94 deletions(-) create mode 100644 claude/todos-extended-enums.md create mode 100644 test/enums/enum_closure14.v3 create mode 100644 test/enums/enum_closure15.v3 create mode 100644 test/enums/enum_submethod12.v3 create mode 100644 test/enums/enum_submethod13.v3 create mode 100644 test/enums/enum_submethod14.v3 create mode 100644 test/enums/enum_submethod15.v3 create mode 100644 test/enums/enum_submethod16.v3 create mode 100644 test/enums/open_enum11.v3 create mode 100644 test/enums/open_enum12.v3 create mode 100644 test/enums/open_enum13.v3 create mode 100644 test/enums/open_enum14.v3 create mode 100644 test/enums/open_field16.v3 create mode 100644 test/enums/open_field17.v3 create mode 100644 test/enums/open_set05.v3 create mode 100644 test/enums/open_set06.v3 diff --git a/aeneas/src/ir/Normalization.v3 b/aeneas/src/ir/Normalization.v3 index d8932b8f9..112d0497a 100644 --- a/aeneas/src/ir/Normalization.v3 +++ b/aeneas/src/ir/Normalization.v3 @@ -131,7 +131,7 @@ class ReachabilityNormalizer(config: NormalizerConfig, ra: ReachabilityAnalyzer) normQueue.add(normClassRecord, (rc, oldRecord, newRecord)); // XXX: inline normClassRecord } ra.prog.state.setComponentRecord(comp, newRecord); - } else if (!rc.isFlattened()) { + } else if (!rc.isUnboxed() || V3.isEnum(rc.oldType)) { // create and map new records to be normalized for (l = rc.instances; l != null; l = l.tail) { var oldRecord = l.head, newRecord = ra.prog.newRecord(tn.newType, rc.liveFields.length); diff --git a/aeneas/src/ir/Reachability.v3 b/aeneas/src/ir/Reachability.v3 index 3c53c2099..48c2be132 100644 --- a/aeneas/src/ir/Reachability.v3 +++ b/aeneas/src/ir/Reachability.v3 @@ -887,7 +887,6 @@ class RaClass extends RaType { return true; } def isUnboxed() -> bool { return variantNorm != null; } - def isFlattened() -> bool { return variantNorm != null && !V3.isEnum(oldType); } } // Tracks instances of an array type. class RaArray extends RaType { diff --git a/aeneas/src/ir/SsaNormalizer.v3 b/aeneas/src/ir/SsaNormalizer.v3 index 9ac64b5f5..e8a9b5507 100644 --- a/aeneas/src/ir/SsaNormalizer.v3 +++ b/aeneas/src/ir/SsaNormalizer.v3 @@ -64,9 +64,8 @@ class SsaRaNormalizer extends SsaRebuilder { // variant: synthesized component receiver var newIrType = norm.ra.getClass(tn.oldType).newIrType; newParams.put(SsaParam.new(newParams.length, newIrType)); - start++; // skip synthesized receiver + start++; } - // else: actual enum on native — no synthesized component } } if (tn.size == 1) { @@ -292,15 +291,16 @@ class SsaRaNormalizer extends SsaRebuilder { // add back void for component ai_new = Arrays.prepend(context.graph.nullConst(m.receiver), ai_new); } - if (rc.isFlattened()) { - ai_new = Arrays.prepend(newGraph.nullReceiver(), ai_new); - } - if (V3.isEnum(rc.oldType)) { + if (rc.isUnboxed()) { i_old.facts |= Fact.O_NO_NULL_CHECK; - if (!context.compiler.NormConfig.NonRefClosureReceiver - && method.source != null && VstMethod.?(method.source)) { - ai_new[0] = curBlock.opTypeSubsume( - V3.getVariantTagType(rc.oldType), Oop.TYPE, ai_new[0]); + if (V3.isEnum(rc.oldType)) { + if (!context.compiler.NormConfig.NonRefClosureReceiver + && method.source != null && VstMethod.?(method.source)) { + ai_new[0] = curBlock.opTypeSubsume( + V3.getVariantTagType(rc.oldType), Oop.TYPE, ai_new[0]); + } + } else { + ai_new = Arrays.prepend(newGraph.nullReceiver(), ai_new); } } normCall(i_old, funcNorm, newOp, ai_new); @@ -337,42 +337,44 @@ class SsaRaNormalizer extends SsaRebuilder { CallVariantVirtual(method) => { // devirtualize methods that are not overridden var rc = norm.ra.getClass(op.typeArgs[0]); - if (V3.isEnum(rc.oldType)) i_old.facts |= Fact.O_NO_NULL_CHECK; + if (rc.isUnboxed()) i_old.facts |= Fact.O_NO_NULL_CHECK; var t = extractVirtualRef(orig, method), funcNorm = t.0, m = t.1; var ai_new = normArgs(funcNorm, genRefs(i_old.inputs)); if (t.2) { // still a virtual dispatch - if (V3.isEnum(rc.oldType)) { + if (rc.isUnboxed()) { var sel = IrSelector.!(m.member); if (sel.mtable != null && sel.mtable.record != null) { - var tag = ai_new[0]; - var tagType = V3.getVariantTagType(rc.oldType); - var func = lookupMtable(sel, tag, tagType); - if (context.compiler.NormConfig.NonRefClosureReceiver - && !context.compiler.NormConfig.ExplicitRefTypeCast) { - ai_new = Arrays.prepend(func, ai_new); - normCall(i_old, funcNorm, V3Op.newCallFunctionDirect(funcNorm.sub[0]), ai_new); + var tag = normVariantGetTag(rc.variantNorm, ai_new); + var func = lookupMtable(sel, tag, rc.variantNorm.tagType()); + if (V3.isEnum(rc.oldType)) { + if (!context.compiler.NormConfig.NonRefClosureReceiver + || context.compiler.NormConfig.ExplicitRefTypeCast) { + // JVM/wasm-gc enum: box tag to Oop + var tagType = V3.getVariantTagType(rc.oldType); + var boxedTag = curBlock.opTypeSubsume(tagType, Oop.TYPE, ai_new[0]); + var userArgs = Arrays.range(ai_new, 1, ai_new.length); + var userFuncType = Function.newType( + if(userArgs.length > 0, Tuple.fromTypeArray(Arrays.map(userArgs, SsaInstr.getType)), Void.TYPE), + funcNorm.sig().returnType()); + ai_new = Arrays.concat([func, boxedTag], userArgs); + normCall(i_old, funcNorm, V3Op.newCallFunction(userFuncType), ai_new); + } else { + ai_new = Arrays.prepend(func, ai_new); + normCall(i_old, funcNorm, V3Op.newCallFunctionDirect(funcNorm.sub[0]), ai_new); + } } else { - var boxedTag = curBlock.opTypeSubsume(tagType, Oop.TYPE, tag); - var userArgs = Arrays.range(ai_new, 1, ai_new.length); - var userFuncType = Function.newType( - if(userArgs.length > 0, Tuple.fromTypeArray(Arrays.map(userArgs, SsaInstr.getType)), Void.TYPE), - funcNorm.sig().returnType()); - ai_new = Arrays.concat([func, boxedTag], userArgs); - normCall(i_old, funcNorm, V3Op.newCallFunction(userFuncType), ai_new); + ai_new = Arrays.concat([func, newGraph.nullReceiver()], ai_new); + normCall(i_old, funcNorm, V3Op.newCallFunction(funcNorm.sub[0]), ai_new); } } else { + if (!V3.isEnum(rc.oldType)) ai_new = Arrays.prepend(newGraph.nullReceiver(), ai_new); normCall(i_old, funcNorm, V3Op.newCallMethod(m), ai_new); } - } else if (rc.isFlattened()) { - var tag = normVariantGetTag(rc.variantNorm, ai_new); - var func = lookupMtable(IrSelector.!(m.member), tag, rc.variantNorm.tagType()); - ai_new = Arrays.concat([func, newGraph.nullReceiver()], ai_new); - normCall(i_old, funcNorm, V3Op.newCallFunction(funcNorm.sub[0]), ai_new); } else { normCall(i_old, funcNorm, V3Op.newCallVariantSelector(m), ai_new); } } else { - if (rc.isFlattened()) ai_new = Arrays.prepend(newGraph.nullReceiver(), ai_new); + if (rc.isUnboxed() && !V3.isEnum(rc.oldType)) ai_new = Arrays.prepend(newGraph.nullReceiver(), ai_new); normCall(i_old, funcNorm, V3Op.newCallMethod(m), ai_new); } } @@ -1555,7 +1557,7 @@ class SsaRaNormalizer extends SsaRebuilder { return map0(i_old); } normType(raField.receiver); // XXX: normType() side-effect of flattening - if (raField != null && rc.isFlattened()) { + if (raField != null && rc.isUnboxed() && !V3.isEnum(rc.oldType)) { // field of unboxed data type var vals = genVariantGetField(rc, raField, rc.variantNorm, ai_new); return mapNnf(i_old, vals); @@ -1627,7 +1629,7 @@ class SsaRaNormalizer extends SsaRebuilder { // OPT: remove write of zero-width field // OPT: remove write of write-only field return addNullCheck(i_old, receiver); - } else if (rc.isFlattened()) { + } else if (rc.isUnboxed() && !V3.isEnum(rc.oldType)) { // init/set of field of flattened data type return map0(i_old); } else if (nf.length == 1) { @@ -1787,7 +1789,8 @@ class SsaRaNormalizer extends SsaRebuilder { return vals; } def normNullCheck(oldApp: SsaApplyOp, op: Operator) { - if (V3.isEnum(op.typeArgs[0])) return; // enum values are never null + var rc = norm.ra.getClass(op.typeArgs[0]); + if (rc != null && rc.isUnboxed()) return; var newArgs = genRefs(oldApp.inputs); if (newArgs.length >= 1) addNullCheck(oldApp, newArgs[0]); } @@ -1886,11 +1889,6 @@ class SsaRaNormalizer extends SsaRebuilder { tagType = avn.tagType(); lo = rvn.tagValue; hi = if(rvn.tagHi < 0, rvn.tagValue, rvn.tagHi); - } else if (V3.isEnum(atn.oldType) && V3.isEnum(rtn.oldType)) { - actualTag = ai_old[offset]; - tagType = V3.getVariantTagType(atn.oldType); - lo = V3.getTagLo(rtn.oldType); - hi = V3.getTagHi(rtn.oldType); } else { return null; } diff --git a/claude/todos-extended-enums.md b/claude/todos-extended-enums.md new file mode 100644 index 000000000..54256b923 --- /dev/null +++ b/claude/todos-extended-enums.md @@ -0,0 +1,66 @@ +TODO list for adding new feature: extended enum + +Description: Extended enums introduce open subtypes to enums, rather +analogous to the open types extension to variants. The feature can be added +in these stages: + +1. Allow an _ "case" in enum types. + Example: enum E1 { A, B, _ } + Example: enum E2 { _ } + Todos: + - Extend the syntax + - Extend the semantic checking +2. Allow subtype enums for enums that have an _ case + Example: enum E1.More { C, D } + Example: enum E1.EvenMore { E, F, _ } + Example: enum E1.EvenMore.Deeper { G } + We should be able to deal with default values for enum types similarly to default values for variants. + Subtype enums must include the fields of their supertype. + The .name for E1.More.C should be "More.C" and for E1.EvenMore.Deeper.G should be "EvenMore.Deeper.C" (subtype enum case names should include their subtype's name) + Add .shortName which gives only the last component of the name: E1.More.C.shortName should be "C", etc. + Todos: + - Extend the syntax + - Extend the semantic checking + - Add implmentation for .shortName +3. Allow methods for enum types. [DONE] + Example: enum E1 { A, B, _; def m1() => 0; } + Note that the enum's methods are separated from its cases by a ';'. + Subtype enums can override inherited methods. + Per-case method overrides (case { def ... }) were removed in Strategy A Step 1. + Todos: + - [x] Extend the syntax (sub-stage 3.1: enum-level methods after ';') + - [x] Extend the semantic checking (sub-stage 3.1: basic method resolution) + - [x] Subtype method inheritance with overrides (sub-stage 3.3) + - [x] Method closures (sub-stage 3.4: `var f = e.m; f()`) + - [x] Wasm/wasm-gc backend support (indirect adapters, sig handling, no Oop for enums) + - Implementation note: dispatch uses tag-indexed array of function values (no boxed enum + objects needed). The tag type is prepended to the method's normalized function type. + CallFunctionDirect (no Oop prepend) is used instead of CallFunction for enum dispatch. + - Closure note: VariantGetMethod/VariantGetVirtual/VariantGetSelector ops are shared + between variants and enums. Enum values are integers (not Records), so the interpreter, + optimizer constant-fold, and optimizer CallClosure handler all need EnumType guards. + The optimizer must also set O_NO_NULL_CHECK for enum receivers (tag 0 is valid). +4. Allow subtypes to redeclare supertype fields [DONE] + Given supertype enum E3(x: int) { A(1), B(17), _ }, example subtype + declarations: + enum E3.S1(x: int) { C(10) } // if field declarations are repeated, they must use the same names and types, in the same order, as the supertype + enum E3.S2(super) { D(15) } // the keyword 'super' means "repeat the supertype fields here": + enum E3.S3 { H(23) } // restating supertype fields is not required + Todos: + - [x] extend syntax + - [x] extend semantics +5. Allow subtypes to *add* fields [DONE] + Given supertype enum E4(b: bool, i: int) { A(true, 0), B(false, 1), _ }, + example subtype declarations that add new fields: + enum E4.S1(b: bool, i: int, f: float) { F(false, 2, 1.0f) } // restate supertype field and add new ones + enum E4.S2(super, f: float) { G(true, 3, 4.5f) } // use 'super' to indicate presence of supertype fields + enum E4.S3(f: float) { H(true, 2, 5.1f) } // error: must restate supertype fields or use super when adding fields + Added fields can be implemented using a global array just as original enum fields do + Multi-level field inheritance works: 'super' means "parent's effective params" + (root params + all intermediate ancestors' extras). Grandchild E.S.T inherits + both root E's fields and intermediate E.S's extra fields. + Todos: + - [x] extend syntax + - [x] extend semantics + - [x] extend implementation + - [x] multi-level field inheritance diff --git a/doc/grammar-claude.md b/doc/grammar-claude.md index 949dc30da..36facd6ac 100644 --- a/doc/grammar-claude.md +++ b/doc/grammar-claude.md @@ -77,12 +77,10 @@ In match patterns, unqualified subtype names (`Bar =>` or `b: Bar =>`) automatic ``` EnumDecl ::= DottedId EnumParams? '{' EnumCase* (';' EnumMethod*)? '}' -EnumCase ::= id ['(' Expr,* ')'] EnumCaseBody? ','? // named case - | '_' EnumCaseBody? ','? // default case: optional, must be last, at most one -EnumCaseBody ::= '{' EnumCaseMethod* '}' -EnumCaseMethod ::= ['private'] 'def' DefDef +EnumCase ::= id ['(' Expr,* ')'] ','? // named case + | '_' ','? // default case: optional, must be last, at most one -EnumMethod ::= ['private'] 'def' DefDef // shared by all cases; may be overridden per-case +EnumMethod ::= ['private'] 'def' DefDef // shared by all cases EnumParams ::= '(' 'super' ')' // inherit parent's params | '(' 'super' ',' ParamDecl,+ ')' // inherit parent's params + add new fields @@ -121,17 +119,12 @@ Subtypes may **not** declare params if the parent has no effective params. Using ### Enum methods -An `EnumMethod` declared after the `;` separator is shared by all cases. It may be overridden per-case by placing a `def` with the same name and signature inside an `EnumCaseBody`. - -- An override must have the same parameter types and return type as the root method. -- Dispatch on a variable of enum type is virtual: the override for the specific case is called at runtime. -- Static dispatch on a known case (e.g. `E.A.m()`) calls the override directly if one exists. +An `EnumMethod` declared after the `;` separator is shared by all cases. ### Enum method inheritance - Methods declared on a parent enum are inherited by all subtype enums (transitively). - A subtype enum may override an inherited method by declaring a method with the same name and signature after its own `;` separator. -- Individual cases of a subtype may further override methods via `EnumCaseBody`. - All virtual dispatch goes through the root enum's dispatch table, regardless of where the override is declared. diff --git a/doc/tutorial/EnumMethods.md b/doc/tutorial/EnumMethods.md index 5841f519c..f62f51eba 100644 --- a/doc/tutorial/EnumMethods.md +++ b/doc/tutorial/EnumMethods.md @@ -16,58 +16,20 @@ enum Planet(mass: double, radius: double) { var g = Planet.EARTH.surfaceGravity(); ``` -## Per-case method overrides - -Individual cases can override an enum method by providing their own definition inside `{ }` braces. -When calling the method on a variable of the enum type, the correct override is dispatched at runtime. - -``` -enum Expr { - ADD { def eval(a: int, b: int) -> int { return a + b; } }, - SUB { def eval(a: int, b: int) -> int { return a - b; } }, - MUL { def eval(a: int, b: int) -> int { return a * b; } }; - - def eval(a: int, b: int) -> int { return 0; } // default -} -def compute(op: Expr, x: int, y: int) -> int { - return op.eval(x, y); // virtual dispatch: calls the override for the specific case -} -``` - -The `_` (default) case can also have method overrides, which apply to any value that doesn't have a more specific override. - -## Enum method closures - -Enum methods can be used as closures, just like methods on classes or variants. -The closure captures the enum value and dispatches correctly when called. - -``` -enum Op { - INC { def apply(x: int) -> int { return x + 1; } }, - DEC { def apply(x: int) -> int { return x - 1; } }; - - def apply(x: int) -> int { return x; } -} -def transform(op: Op, value: int) -> int { - var f = op.apply; // create a closure - return f(value); // calls the correct override -} -``` - ## Subtype enum method inheritance Subtype enums inherit methods from their parent. -A subtype can override an inherited method, and individual cases of a subtype can override it further. +A subtype can override an inherited method. ``` enum Animal { DOG, CAT, _; def speak() -> int { return 0; } } enum Animal.Exotic { - PARROT { def speak() -> int { return 2; } }, + PARROT, SNAKE; def speak() -> int { return 1; } // override for all Exotic cases } ``` -Here `Animal.Exotic.PARROT.speak()` returns `2` (per-case override), `Animal.Exotic.SNAKE.speak()` returns `1` (subtype override), and `Animal.DOG.speak()` returns `0` (root default). +Here `Animal.Exotic.SNAKE.speak()` returns `1` (subtype override) and `Animal.DOG.speak()` returns `0` (root default). All dispatch goes through the root enum's dispatch table, so a variable of type `Animal` will dispatch correctly regardless of whether the value is a root case or a subtype case. diff --git a/test/enums/enum_closure14.v3 b/test/enums/enum_closure14.v3 new file mode 100644 index 000000000..dabd4a516 --- /dev/null +++ b/test/enums/enum_closure14.v3 @@ -0,0 +1,11 @@ +//@execute 0=10; 1=20; 2=30; 3=40 +// Closures stored in array, dispatched from array +enum E(x: int) { A(10), B(20), _; def getX() -> int { return x; } } +enum E.More(super) { C(30), D(40) } +def main(i: int) -> int { + var cases: Array = [E.A, E.B, E.More.C, E.More.D]; + var closures = Array int>.new(cases.length); + for (j < cases.length) closures[j] = cases[j].getX; + if (i < 0 || i >= closures.length) return -1; + return closures[i](); +} diff --git a/test/enums/enum_closure15.v3 b/test/enums/enum_closure15.v3 new file mode 100644 index 000000000..15913d84b --- /dev/null +++ b/test/enums/enum_closure15.v3 @@ -0,0 +1,13 @@ +//@execute 0=5; 1=15; 2=25 +// Closure of override method passed through higher-order function chain +enum E { A, B, _; def val() -> int { return 5; } } +enum E.Sub { C, _; def val() -> int { return 15; } } +enum E.Sub.Deep { G; def val() -> int { return 25; } } +def apply(f: void -> int) -> int { return f(); } +def wrap(f: void -> int) -> (void -> int) { return f; } +def main(i: int) -> int { + var cases: Array = [E.A, E.Sub.C, E.Sub.Deep.G]; + if (i < 0 || i >= cases.length) return -1; + var f = wrap(cases[i].val); + return apply(f); +} diff --git a/test/enums/enum_submethod12.v3 b/test/enums/enum_submethod12.v3 new file mode 100644 index 000000000..52b0ef7c8 --- /dev/null +++ b/test/enums/enum_submethod12.v3 @@ -0,0 +1,13 @@ +//@execute 0=1; 1=17; 2=10; 3=99 +// Inherited method with extra fields: subtype does NOT override, method accesses parent fields +enum E(x: int) { A(1), B(17), _; def getX() -> int { return x; } } +enum E.Sub(super, y: int) { C(10, 99), D(20, 88) } +def main(i: int) -> int { + match (i) { + 0 => return E.A.getX(); + 1 => return E.B.getX(); + 2 => { var e: E = E.Sub.C; return e.getX(); } + 3 => { var v: E.Sub = E.Sub.C; return v.y; } + } + return -1; +} diff --git a/test/enums/enum_submethod13.v3 b/test/enums/enum_submethod13.v3 new file mode 100644 index 000000000..545804d4a --- /dev/null +++ b/test/enums/enum_submethod13.v3 @@ -0,0 +1,20 @@ +//@execute 0=10; 1=22; 2=40; 3=10; 4=22; 5=40 +// Combined: 3-level hierarchy + extra fields + method overrides + closures +enum E(x: int) { A(10), _; def compute() -> int { return x; } } +enum E.Mid(super, y: int) { B(20, 2), _; def compute() -> int { return x + y; } } +enum E.Mid.Leaf(super, z: int) { C(30, 3, 7); def compute() -> int { return x + y + z; } } +def apply(f: void -> int) -> int { return f(); } +def main(i: int) -> int { + var a: E = E.A; + var b: E = E.Mid.B; + var c: E = E.Mid.Leaf.C; + match (i) { + 0 => return a.compute(); + 1 => return b.compute(); + 2 => return c.compute(); + 3 => return apply(a.compute); + 4 => return apply(b.compute); + 5 => return apply(c.compute); + } + return -1; +} diff --git a/test/enums/enum_submethod14.v3 b/test/enums/enum_submethod14.v3 new file mode 100644 index 000000000..89436ea79 --- /dev/null +++ b/test/enums/enum_submethod14.v3 @@ -0,0 +1,11 @@ +//@execute 0=0; 1=0; 2=1; 3=1 +// Single-case subtype: dispatch, override, closure +enum E { A, B, _; def rank() -> int { return 0; } } +enum E.Special { X; def rank() -> int { return 1; } } +def call(f: void -> int) -> int { return f(); } +def main(i: int) -> int { + var cases: Array = [E.A, E.B, E.Special.X, E.Special.X]; + if (i < 0 || i >= cases.length) return -1; + if (i < 3) return cases[i].rank(); + return call(cases[3].rank); +} diff --git a/test/enums/enum_submethod15.v3 b/test/enums/enum_submethod15.v3 new file mode 100644 index 000000000..4726bb29b --- /dev/null +++ b/test/enums/enum_submethod15.v3 @@ -0,0 +1,18 @@ +//@execute 0=0; 1=2; 2=2; 3=3; 4=2; 5=3 +// Empty subtype (only _, no named cases) with method and deeper subtypes +enum E { A, _; def level() -> int { return 0; } } +enum E.Mid { _; def level() -> int { return 1; } } +enum E.Mid.Leaf { X, Y; def level() -> int { return 2; } } +enum E.Other { Z; def level() -> int { return 3; } } +def main(i: int) -> int { + var cases: Array = [E.A, E.Mid.Leaf.X, E.Mid.Leaf.Y, E.Other.Z]; + match (i) { + 0 => return cases[0].level(); + 1 => return cases[1].level(); + 2 => return cases[2].level(); + 3 => return cases[3].level(); + 4 => { var e: E = E.Mid.Leaf.Y; return e.level(); } + 5 => { var f = cases[3].level; return f(); } + } + return -1; +} diff --git a/test/enums/enum_submethod16.v3 b/test/enums/enum_submethod16.v3 new file mode 100644 index 000000000..7c8a97f8c --- /dev/null +++ b/test/enums/enum_submethod16.v3 @@ -0,0 +1,11 @@ +//@execute 0=0; 1=1; 2=2; 3=3 +// 4-level hierarchy with method overrides at each level +enum E { A, _; def depth() -> int { return 0; } } +enum E.L1 { B, _; def depth() -> int { return 1; } } +enum E.L1.L2 { C, _; def depth() -> int { return 2; } } +enum E.L1.L2.L3 { D; def depth() -> int { return 3; } } +def main(i: int) -> int { + var cases: Array = [E.A, E.L1.B, E.L1.L2.C, E.L1.L2.L3.D]; + if (i < 0 || i >= cases.length) return -1; + return cases[i].depth(); +} diff --git a/test/enums/open_enum11.v3 b/test/enums/open_enum11.v3 new file mode 100644 index 000000000..83db6bd7d --- /dev/null +++ b/test/enums/open_enum11.v3 @@ -0,0 +1,13 @@ +//@execute 0=0; 1=10; 2=20 +// Zero-init of open enum with method: default tag dispatches correctly +enum E { A, B, _; def val() -> int { return 0; } } +enum E.More { C, D, _; def val() -> int { return 10; } } +enum E.More.Deep { G; def val() -> int { return 20; } } +def main(i: int) -> int { + match (i) { + 0 => { var e: E; return e.val(); } + 1 => { var e: E = E.More.C; return e.val(); } + 2 => { var e: E = E.More.Deep.G; return e.val(); } + } + return -1; +} diff --git a/test/enums/open_enum12.v3 b/test/enums/open_enum12.v3 new file mode 100644 index 000000000..d908704fb --- /dev/null +++ b/test/enums/open_enum12.v3 @@ -0,0 +1,15 @@ +//@execute 0=1; 1=2; 2=2; 3=3 +// Type query narrowing + method dispatch +enum E { A, B, _; def val() -> int { return 1; } } +enum E.Sub { C, D, _; def val() -> int { return 2; } } +enum E.Sub.Deep { G; def val() -> int { return 3; } } +def dispatch(e: E) -> int { + if (E.Sub.Deep.?(e)) return e.val(); + if (E.Sub.?(e)) return e.val(); + return e.val(); +} +def main(i: int) -> int { + var cases: Array = [E.A, E.Sub.C, E.Sub.D, E.Sub.Deep.G]; + if (i < 0 || i >= cases.length) return -1; + return dispatch(cases[i]); +} diff --git a/test/enums/open_enum13.v3 b/test/enums/open_enum13.v3 new file mode 100644 index 000000000..45949d17c --- /dev/null +++ b/test/enums/open_enum13.v3 @@ -0,0 +1,21 @@ +//@execute 0=1; 1=2; 2=3; 3=1 +// Match arm + subtype override dispatch +enum E { A, B, _; def val() -> int { return 1; } } +enum E.More { C; def val() -> int { return 3; } } +def test(e: E) -> int { + match (e) { + A => return 1; + B => return 2; + More => return e.val(); + _ => return 4; + } +} +def main(i: int) -> int { + match (i) { + 0 => return test(E.A); + 1 => return test(E.B); + 2 => return test(E.More.C); + 3 => { var e: E; return test(e); } + } + return -1; +} diff --git a/test/enums/open_enum14.v3 b/test/enums/open_enum14.v3 new file mode 100644 index 000000000..d2f5c9c12 --- /dev/null +++ b/test/enums/open_enum14.v3 @@ -0,0 +1,36 @@ +//@execute 0=4; 1=3; 2=1; 3=0; 4=1 +// Combined: fields + methods + closures + sets in one hierarchy +enum E(x: int) { A(10), B(20), _; def val() -> int { return x; } } +enum E.Sub(super) { C(30), D(40); def val() -> int { return x + 1; } } +def main(i: int) -> int { + match (i) { + 0 => { + var s = E.set.all; + var count = 0; + for (c in s) count++; + return count; + } + 1 => { + var s: E.set = E.A | E.Sub.C | E.Sub.D; + var count = 0; + for (c in s) count++; + return count; + } + 2 => { + var s: E.set = E.Sub.C | E.Sub.D; + return if(s.C && s.D && !s.A, 1, 0); + } + 3 => { + var e: E = E.Sub.C; + var f = e.val; + return if(f() == 31, 0, 1); + } + 4 => { + var cases: Array = [E.A, E.B, E.Sub.C, E.Sub.D]; + var sum = 0; + for (c in cases) sum += c.val(); + return if(sum == 10 + 20 + 31 + 41, 1, 0); + } + } + return -1; +} diff --git a/test/enums/open_field16.v3 b/test/enums/open_field16.v3 new file mode 100644 index 000000000..b147db7ab --- /dev/null +++ b/test/enums/open_field16.v3 @@ -0,0 +1,18 @@ +//@execute 0=1; 1=1; 2=3; 3=10; 4=20; 5=20; 6=100; 7=200 +// 4-level hierarchy with fields at each level +enum E(a: int) { R(1), _; def getA() -> int { return a; } } +enum E.L1(super, b: int) { X(10, 100), _ } +enum E.L1.L2(super, c: int) { Y(20, 200, 2000) } +def main(i: int) -> int { + match (i) { + 0 => return E.R.a; + 1 => return E.R.getA(); + 2 => { var e: E; return if(e == E.R, 3, 0); } + 3 => return E.L1.X.a; + 4 => { var e: E = E.L1.L2.Y; return e.getA(); } + 5 => { var e: E = E.L1.L2.Y; return e.a; } + 6 => { var v: E.L1 = E.L1.X; return v.b; } + 7 => { var v: E.L1.L2 = E.L1.L2.Y; return v.b; } + } + return -1; +} diff --git a/test/enums/open_field17.v3 b/test/enums/open_field17.v3 new file mode 100644 index 000000000..facae4f0f --- /dev/null +++ b/test/enums/open_field17.v3 @@ -0,0 +1,11 @@ +//@execute 0=99; 1=88; 2=77; 3=66 +// Empty subtype with fields, no named cases +enum E(x: int) { A(99), _ } +enum E.Mid(super) { _; } +enum E.Mid.Leaf(super, y: int) { B(88, 1), C(77, 2) } +enum E.Other(super) { D(66) } +def main(i: int) -> int { + var cases: Array = [E.A, E.Mid.Leaf.B, E.Mid.Leaf.C, E.Other.D]; + if (i < 0 || i >= cases.length) return -1; + return cases[i].x; +} diff --git a/test/enums/open_set05.v3 b/test/enums/open_set05.v3 new file mode 100644 index 000000000..d56511c62 --- /dev/null +++ b/test/enums/open_set05.v3 @@ -0,0 +1,15 @@ +//@execute 0=6; 1=4; 2=2; 3=1 +// Set operations on 3-level hierarchy: counting and membership +enum E { A, B, _ } +enum E.Mid { C, D, _ } +enum E.Mid.Deep { X, Y } +def count(s: E.set) -> int { var n = 0; for (e in s) n++; return n; } +def main(i: int) -> int { + match (i) { + 0 => return count(E.set.all); + 1 => { var s: E.set = E.Mid.C | E.Mid.D | E.Mid.Deep.X | E.Mid.Deep.Y; return count(s); } + 2 => { var s: E.set = E.Mid.Deep.X | E.Mid.Deep.Y; return count(s); } + 3 => { var s: E.set = E.A | E.Mid.Deep.X; return count(s & (E.A | E.B)); } + } + return -1; +} diff --git a/test/enums/open_set06.v3 b/test/enums/open_set06.v3 new file mode 100644 index 000000000..194851be5 --- /dev/null +++ b/test/enums/open_set06.v3 @@ -0,0 +1,17 @@ +//@execute 0=1; 1=0; 2=1; 3=0; 4=1; 5=3 +// Set operations on 3-level hierarchy: cross-level union and intersection +enum E { A, B, _ } +enum E.Mid { C, D, _ } +enum E.Mid.Deep { G, H } +def main(i: int) -> int { + var s: E.set = E.A | E.Mid.D | E.Mid.Deep.G; + match (i) { + 0 => return if(s.A, 1, 0); + 1 => return if(s.B, 1, 0); + 2 => return if(s.D, 1, 0); + 3 => return if(s.C, 1, 0); + 4 => return if(s.G, 1, 0); + 5 => { var n = 0; for (e in s) n++; return n; } + } + return -1; +} From da309bc9967c1d1edeb140333d813e63dc92808f Mon Sep 17 00:00:00 2001 From: Eliot Moss Date: Tue, 21 Apr 2026 19:31:18 -0400 Subject: [PATCH 46/58] Strategy B: synthetic VstClass/IrClass/RaClass per enum case Give each named enum case its own synthetic VstClass (with TypeCon), IrClass, and RaClass. This mirrors how variant cases work and enables per-case method overrides through the unified mtable pipeline. Key changes: - EnumDesugaring creates synthetic VstClass per named case (not for _) - Parser restores { def ... } syntax on enum cases - Verifier creates TypeCons, links per-case method overrides to roots - Reachability creates per-case RaClasses registered in typeMap - Normalization uses unified fillMtableSlot for both enums and variants - Per-case method verification, type resolution, and body type-checking The mtable is now populated via the same subtypes iteration path as variants. Per-case override methods are found by resolveMethodImpl walking up the RaClass parent chain. Existing enum tests pass. WIP: per-case dispatch generates correct SSA but method normalization for case overrides needs work (native binaries crash at dispatch). Co-Authored-By: Claude Opus 4.6 (1M context) --- aeneas/src/ir/Normalization.v3 | 24 +++------- aeneas/src/ir/Reachability.v3 | 16 ++++++- aeneas/src/v3/V3.v3 | 3 ++ aeneas/src/vst/Parser.v3 | 29 ++++++++++-- aeneas/src/vst/Verifier.v3 | 68 +++++++++++++++++++++++++++++ aeneas/src/vst/Vst.v3 | 24 +++++++++- test/enums/parser/enum_cmethod00.v3 | 4 +- 7 files changed, 141 insertions(+), 27 deletions(-) diff --git a/aeneas/src/ir/Normalization.v3 b/aeneas/src/ir/Normalization.v3 index 112d0497a..ba1bfb7ff 100644 --- a/aeneas/src/ir/Normalization.v3 +++ b/aeneas/src/ir/Normalization.v3 @@ -323,14 +323,12 @@ class ReachabilityNormalizer(config: NormalizerConfig, ra: ReachabilityAnalyzer) // number a variant and children consistent with tagging order def numberVariant(rc: RaClass) { rc.minClassId = liveClasses.length; - if (rc.children == null || V3.isEnum(rc.oldType)) { - liveClasses.put(rc); // leaf or enum (enums use tag ranges, not classId DFS) + if (rc.children == null) { + liveClasses.put(rc); // leaf } else { for (l = rc.children; l != null; l = l.tail) numberVariantSlot(l.head, rc.minClassId); } - // For enum roots, ensure we reserve the full DFS tag range. Children - // are only subtype enum RaClasses (not individual cases), so they may - // not cover all tag slots. + // For enum roots, ensure we reserve the full DFS tag range. if (V3.isEnum(rc.oldType)) { var tagHi = V3.getTagHi(rc.oldType); var endId = rc.minClassId + tagHi + 1; @@ -635,17 +633,9 @@ class ReachabilityNormalizer(config: NormalizerConfig, ra: ReachabilityAnalyzer) mtable.record = ra.prog.newRecord(V3Array.newType(ft), size); } if (mtable.table.length > 0) { - if (V3.isEnum(rc.oldType)) { - // Enum: fill by walking the hierarchy. Each enum type - // covers a tag range; subtypes override their parent's range. - fillEnumMtable(mtable, rc, rm); - } else { - for (l = rc.subtypes; l != null; l = l.tail) { // fill out mtable - // Skip non-leaf variant range nodes (e.g. "case _" and intermediate subtype - // variants). Only the actual leaf at each DFS tag slot fills the mtable. - if (liveClasses[l.head.minClassId] != l.head) continue; - fillMtableSlot(mtable, l.head, rm); - } + for (l = rc.subtypes; l != null; l = l.tail) { + if (liveClasses[l.head.minClassId] != l.head) continue; + fillMtableSlot(mtable, l.head, rm); } } setMtable(rc, rv); // set mtable for all child virtual methods @@ -702,7 +692,7 @@ class ReachabilityNormalizer(config: NormalizerConfig, ra: ReachabilityAnalyzer) } def resolveMethodImpl(rc: RaClass, rm: RaMethod) -> RaMethod { var sm: RaMethod; - for (sc = rc; sc != null; sc = sc.parent) { // find super method, if any + for (sc = rc; sc != null; sc = sc.parent) { sm = sc.findRaMethod(rm); if (sm != null) return sm; } diff --git a/aeneas/src/ir/Reachability.v3 b/aeneas/src/ir/Reachability.v3 index 48c2be132..b7b8687ce 100644 --- a/aeneas/src/ir/Reachability.v3 +++ b/aeneas/src/ir/Reachability.v3 @@ -671,7 +671,7 @@ class ReachabilityAnalyzer(compilation: Compilation) { } def makeMethod(typeArgs: Array, m: IrMethod, context: IrSpec) -> RaMethod { var rm = m.raMethod; - if (rm != null) return rm; // RaMethod already cached + if (rm != null) return rm; if (typeArgs.length == 1 && !m.receiver.open()) { // a monomorphic method @@ -755,7 +755,7 @@ class ReachabilityAnalyzer(compilation: Compilation) { return rc; } private def addEnumSubtypesRecursive(rc: RaClass, decl: VstClass) { - rc.subtypes = List.new(rc, rc.subtypes); + addEnumCaseClasses(rc, decl); for (l = decl.subtypeOrder; l != null; l = l.tail) { var childRc = RaClass.!(makeClass(l.head.getDeclaredType())); for (gl = childRc.subtypes; gl != null; gl = gl.tail) { @@ -763,6 +763,18 @@ class ReachabilityAnalyzer(compilation: Compilation) { } } } + private def addEnumCaseClasses(rc: RaClass, decl: VstClass) { + if (decl.cases == null) return; + for (c in decl.cases) { + if (c.decl == null || !c.decl.isSynthetic) continue; + var caseType = V3.getSyntheticEnumCaseType(c); + var caseIc = oldIr.makeIrClass(caseType); + var caseRc = newRaClass(caseType, caseIc, rc); + caseRc.raFacts |= RaFact.RC_ENUM; + typeMap[caseType] = caseRc; + rc.subtypes = List.new(caseRc, rc.subtypes); + } + } def mono(t: Type, spec: IrSpec) -> Type { return if(spec != null, spec.instantiateType(t), t); } diff --git a/aeneas/src/v3/V3.v3 b/aeneas/src/v3/V3.v3 index fc901e980..0102f85c9 100644 --- a/aeneas/src/v3/V3.v3 +++ b/aeneas/src/v3/V3.v3 @@ -79,6 +79,9 @@ component V3 { def getSyntheticVariantType(t: Type, vcase: VstCaseMember) -> Type { return vcase.decl.typeCon.create(t.nested); } + def getSyntheticEnumCaseType(vcase: VstCaseMember) -> Type { + return vcase.decl.typeCon.create0(); + } def makeDefaultVariantRecord(prog: Program, t: Type) -> Record { t = getDefaultVariantCaseType(prog, t); var len = prog.ir.makeIrClass(t).fields.length; diff --git a/aeneas/src/vst/Parser.v3 b/aeneas/src/vst/Parser.v3 index 52110112a..3cb0fb2c5 100644 --- a/aeneas/src/vst/Parser.v3 +++ b/aeneas/src/vst/Parser.v3 @@ -521,7 +521,7 @@ component Parser { while (true) { if (p.curByte == '}') { p.advance1(); break; } if (p.curByte == ';') break; // methods section follows - var c = parseEnumCase(p, desugar.cases.length); + var c = parseEnumCase(p, desugar); if (c == null) { p.opt1('}'); break; } desugar.addCase(c); list = List.new(c, list); @@ -547,24 +547,45 @@ component Parser { file.enums.put(decl); return true; } - def parseEnumCase(p: ParserState, tag: int) -> VstCaseMember { + def parseEnumCase(p: ParserState, desugar: EnumDesugaring) -> VstCaseMember { // Handle "_" default case specially (not an identifier start in Virgil). if (p.curByte == '_' && !Char.isIdentMiddle(p.peek(1))) { var ut = p.token(1); - var c = VstCaseMember.new(tag, ut, null, null); + var members = if(p.curByte == '{', parseEnumCaseMembers(p)); + var c = desugar.synthesizeEnumCaseMember(ut, null, members); p.opt1(','); return c; } var id = parseIdentVoid(p); var args = if(p.curByte == '(', parseList(0, p, '(', COMMA, ')', parseExpr)); if (id.name.image.length > 0) { - var c = VstCaseMember.new(tag, id.name, null, args); + var members = if(p.curByte == '{', parseEnumCaseMembers(p)); + var c = desugar.synthesizeEnumCaseMember(id.name, args, members); p.opt1(','); return c; } p.opt1(','); return null; } + def parseEnumCaseMembers(p: ParserState) -> List { + p.req1('{'); + var list: List; + while (true) { + if (p.curByte == '}') { p.advance1(); break; } + var isPrivate = optKeyword(p, "private") != null; + if (optKeyword(p, "def") != null) { + var parser = if(isPrivate, PRIVATE_DEF, PUBLIC_DEF); + var nlist = parser.parseDef(p, list, null); + if (nlist == list) { p.opt1('}'); break; } + list = nlist; + } else { + p.error("only 'def' allowed in enum case body"); + p.opt1('}'); + break; + } + } + return Lists.reverse(list); + } def parseEnumMethod(p: ParserState, prev: List) -> List { var isPrivate = optKeyword(p, "private") != null; if (optKeyword(p, "def") != null) { diff --git a/aeneas/src/vst/Verifier.v3 b/aeneas/src/vst/Verifier.v3 index 90934fe5e..695a9f61b 100644 --- a/aeneas/src/vst/Verifier.v3 +++ b/aeneas/src/vst/Verifier.v3 @@ -52,6 +52,7 @@ class Verifier(compiler: Compiler, prog: Program) { vst.components.apply(addInitFor); vst.classes.apply(addInitFor); vst.enums.apply(addInitFor); + vst.enums.apply(linkEnumCaseMethods); vst.enums.apply(addEnumVals); } if (mainMethod != null) { @@ -103,6 +104,13 @@ class Verifier(compiler: Compiler, prog: Program) { var enumSetType = EnumSetType.new(stc.uid, enumDecl, repType, stc); enumType.setType = stc.singleton = enumSetType; } + for (c in enumDecl.cases) { + if (c.decl != null && c.decl.isSynthetic) { + c.decl.tagType = enumDecl.tagType; + c.decl.parentEnum = enumDecl; + c.decl.typeCon = V3Class_TypeCon.new(c.decl, prog.typeCache); + } + } var cv = makeVerifier(enumDecl, enumDecl.getDeclaredType(), file); cv.bindTypeCon(); cv.buildMembers(); @@ -698,6 +706,32 @@ class Verifier(compiler: Compiler, prog: Program) { } return Lists.reverse(result); } + def linkEnumCaseMethods(decl: VstClass) { + if (decl.cases == null) return; + for (c in decl.cases) { + if (c.members == null || c.decl == null) continue; + var caseDecl = c.decl; + if (caseDecl.memberMap == null) caseDecl.memberMap = Strings.newMap(); + for (ml = c.members; ml != null; ml = ml.tail) { + match (ml.head) { + m: VstMethod => { + m.receiver = decl; + caseDecl.memberMap[m.name()] = m; + var rootMember = decl.memberMap[m.name()]; + if (rootMember != null && VstMethod.?(rootMember)) { + var rm = VstMethod.!(rootMember); + m.root = if(rm.root != null, rm.root, rm); + m.index = rm.index; + caseDecl.numMethods++; + } + } + _ => ; + } + } + // Create case IrClass now that root method indices are known. + prog.ir.makeIrClass(caseDecl.typeCon.create0()); + } + } def assignEnumTagsIfRoot(decl: VstClass) { if (decl.qualifiers != null) return; // skip subtype enums var nextTag = 0; @@ -715,6 +749,10 @@ class Verifier(compiler: Compiler, prog: Program) { for (c in decl.cases) { if (Strings.equal(c.name(), "_")) continue; c.enumTag = nextTag++; + if (c.decl != null && c.decl.isSynthetic) { + c.decl.tagLo = c.enumTag; + c.decl.tagHi = c.enumTag; + } } var defaultLo = nextTag; // Recurse into subtypes in declaration order (subtypeOrder is reverse-decl, so reverse it). @@ -727,6 +765,13 @@ class Verifier(compiler: Compiler, prog: Program) { if (Strings.equal(c.name(), "_")) c.enumTag = defaultLo; } decl.tagHi = nextTag - 1; + // Set tagLo/tagHi on _ case's synthetic VstClass (after decl.tagHi is known). + for (c in decl.cases) { + if (Strings.equal(c.name(), "_") && c.decl != null && c.decl.isSynthetic) { + c.decl.tagLo = defaultLo; + c.decl.tagHi = decl.tagHi; + } + } return nextTag; } def propagateEnumTagType(decl: VstClass, tagType: IntType) { @@ -980,6 +1025,29 @@ class Verifier(compiler: Compiler, prog: Program) { // expected parameters, but none were provided. if (paramTypes != null) cv.errAtDecl(c).CaseDeclError("expected case arguments, but none provided"); } + // Type-check per-case method override bodies. + if (c.members != null) { + for (ml = c.members; ml != null; ml = ml.tail) { + match (ml.head) { + m: VstMethod => { + if (m.receiver == null) m.receiver = decl; + // Resolve param/return types. + match (m.func.rettype) { + Explicit(tref) => cv.resolveType(tref, decl.typeEnv); + _ => ; + } + if (m.func.params.list != null) { + for (p = m.func.params.list; p != null; p = p.tail) { + if (p.head.tref != null) cv.resolveType(p.head.tref, decl.typeEnv); + } + } + tc.enterMethod(m, env); + if (!EmptyStmt.?(m.func.body)) tc.typeCheckBody(m.func.body); + } + _ => ; + } + } + } } def redefineField(r: VstRedefField, tc: TypeChecker) { var decl: VstMember; diff --git a/aeneas/src/vst/Vst.v3 b/aeneas/src/vst/Vst.v3 index 523140c90..e6f1c6aeb 100644 --- a/aeneas/src/vst/Vst.v3 +++ b/aeneas/src/vst/Vst.v3 @@ -315,6 +315,22 @@ class EnumDesugaring(isPrivate: bool, id: VstIdent, params: VstList, members: List) -> VstCaseMember { + var tag = cases.length; + var isDefault = Strings.equal(name.image, "_"); + var synthetic: VstClass; + if (!isDefault) { + synthetic = VstClass.new(false, name, null, null, null, null, members); + synthetic.kind = Kind.ENUM; + synthetic.isSynthetic = true; + var buf = StringBuilder.new().puts(id.name.image); + buf.putc('.'); + synthetic.fullName = buf.puts(name.image).toString(); + } + var c = VstCaseMember.new(tag, name, synthetic, args); + c.members = members; + return c; + } def addCase(c: VstCaseMember) { if (Strings.equal(c.name(), "_")) hasDefault = true; cases.put(c); @@ -344,16 +360,20 @@ class VstCaseMember extends VstMember { def decl: VstClass; def args: VstList; // for enums var enumTag: int; // runtime tag; initialized to localTag, may be reassigned by DFS + var members: List; // per-case method overrides new(localTag, name: Token, decl, args) super(false, name) { enumTag = localTag; } def runtimeTag() -> int { - return if(decl != null, decl.variantTag, enumTag); + if (decl != null && decl.isVariant()) return decl.variantTag; + return enumTag; } def getType() -> Type { - return if(decl == null, receiver.declType, decl.declType); + if (decl == null) return receiver.declType; + if (decl.isEnum() && decl.isSynthetic) return receiver.declType; + return decl.declType; } } // Synthesized .tag member from a variant/enum. diff --git a/test/enums/parser/enum_cmethod00.v3 b/test/enums/parser/enum_cmethod00.v3 index 2559a4f8c..252debd70 100644 --- a/test/enums/parser/enum_cmethod00.v3 +++ b/test/enums/parser/enum_cmethod00.v3 @@ -1,3 +1,3 @@ -//@parse = ParseError -// Per-case method bodies are no longer allowed +//@parse +// Per-case method bodies with override enum E { A { def m() -> int { return 1; } }, B; def m() -> int { return 0; } } From 5b2be12f785d8be210b3b3b3de8e2f0eaeac34e9 Mon Sep 17 00:00:00 2001 From: Eliot Moss Date: Tue, 21 Apr 2026 21:59:22 -0400 Subject: [PATCH 47/58] Fix three enum dispatch bugs; add per-case method execution tests 1. Interpreter per-case dispatch (Eval.v3): lookupEnumVirtual now checks synthetic case VstClasses before walking the parentEnum chain, so Strategy B per-case overrides are found. 2. Empty _ subtype tag range (Verifier.v3): a subtype enum with only _ and no named cases got tagHi < tagLo because nextTag never advanced. Now ensures nextTag >= defaultLo + 1 when hasDefault is true. 3. Duplicate RaClass for subtype enums (Reachability.v3): makeType for a child enum called makeClass(parentEnum) before registering itself in typeMap; the parent's addEnumSubtypesRecursive re-entered makeType for the child and created a duplicate. Fixed by re-checking typeMap after the parentRc call. Also adds subtype enum RaClasses to the root's subtypes list so getVirtual analyzes their overrides. 4. Per-case method parameter types (Verifier.v3): typeCheckEnumCase resolved param type refs but didn't assign p.head.vtype. Co-Authored-By: Claude Opus 4.6 (1M context) --- aeneas/src/core/Eval.v3 | 30 ++++++++++++++++++++++-------- aeneas/src/ir/Reachability.v3 | 5 +++++ aeneas/src/vst/Verifier.v3 | 5 ++++- test/enums/enum_cmethod00.v3 | 12 ++++++++++++ test/enums/enum_cmethod01.v3 | 12 ++++++++++++ test/enums/enum_cmethod02.v3 | 13 +++++++++++++ test/enums/enum_cmethod03.v3 | 16 ++++++++++++++++ test/enums/enum_cmethod04.v3 | 14 ++++++++++++++ test/enums/enum_cmethod05.v3 | 23 +++++++++++++++++++++++ 9 files changed, 121 insertions(+), 9 deletions(-) create mode 100644 test/enums/enum_cmethod00.v3 create mode 100644 test/enums/enum_cmethod01.v3 create mode 100644 test/enums/enum_cmethod02.v3 create mode 100644 test/enums/enum_cmethod03.v3 create mode 100644 test/enums/enum_cmethod04.v3 create mode 100644 test/enums/enum_cmethod05.v3 diff --git a/aeneas/src/core/Eval.v3 b/aeneas/src/core/Eval.v3 index aa2537731..5c287f2ed 100644 --- a/aeneas/src/core/Eval.v3 +++ b/aeneas/src/core/Eval.v3 @@ -1190,14 +1190,28 @@ def lookupEnumVirtual(args: Arguments, member: IrMember) -> IrSpec { var targetDecl = findEnumDeclForTag(enumType.classDecl, tag); var ir = args.getProgram().ir; var m: IrMethod; - var vc = targetDecl; - while (vc != null) { - var ic = ir.makeIrClass(vc.getDeclaredType()); - if (ic != null && member.index < ic.methods.length) { - var candidate = ic.methods[member.index]; - if (candidate != null) { m = candidate; break; } - } - vc = vc.parentEnum; + // Check synthetic case decl first (Strategy B per-case overrides). + if (targetDecl.cases != null) { + for (c in targetDecl.cases) { + if (c.decl != null && c.decl.isSynthetic && c.enumTag == tag) { + var caseIc = ir.getIrClass(c.decl.typeCon.create0()); + if (caseIc != null && member.index < caseIc.methods.length) { + var candidate = caseIc.methods[member.index]; + if (candidate != null) { m = candidate; break; } + } + } + } + } + if (m == null) { + var vc = targetDecl; + while (vc != null) { + var ic = ir.makeIrClass(vc.getDeclaredType()); + if (ic != null && member.index < ic.methods.length) { + var candidate = ic.methods[member.index]; + if (candidate != null) { m = candidate; break; } + } + vc = vc.parentEnum; + } } if (m == null) m = IrMethod.!(member); return IrSpec.new(ta[0], ta, m); diff --git a/aeneas/src/ir/Reachability.v3 b/aeneas/src/ir/Reachability.v3 index b7b8687ce..6d3058422 100644 --- a/aeneas/src/ir/Reachability.v3 +++ b/aeneas/src/ir/Reachability.v3 @@ -735,6 +735,10 @@ class ReachabilityAnalyzer(compilation: Compilation) { var decl = V3.getDecl(t); var parentRc = if(decl.parentEnum != null, RaClass.!(makeClass(decl.parentEnum.getDeclaredType()))); + // Re-check typeMap: the parentRc call above may have + // recursively created this type's RaClass. + raType = typeMap[t]; + if (raType != null) return raType; var rc = newRaClass(t, oldIr.makeIrClass(t), parentRc); rc.raFacts |= RaFact.RC_ENUM; raType = rc; @@ -758,6 +762,7 @@ class ReachabilityAnalyzer(compilation: Compilation) { addEnumCaseClasses(rc, decl); for (l = decl.subtypeOrder; l != null; l = l.tail) { var childRc = RaClass.!(makeClass(l.head.getDeclaredType())); + rc.subtypes = List.new(childRc, rc.subtypes); for (gl = childRc.subtypes; gl != null; gl = gl.tail) { rc.subtypes = List.new(gl.head, rc.subtypes); } diff --git a/aeneas/src/vst/Verifier.v3 b/aeneas/src/vst/Verifier.v3 index 695a9f61b..082ee9bc6 100644 --- a/aeneas/src/vst/Verifier.v3 +++ b/aeneas/src/vst/Verifier.v3 @@ -764,6 +764,9 @@ class Verifier(compiler: Compiler, prog: Program) { for (c in decl.cases) { if (Strings.equal(c.name(), "_")) c.enumTag = defaultLo; } + // Ensure the _ case gets at least one tag slot when there are no named + // cases or subtypes that advanced nextTag past defaultLo. + if (decl.hasDefault && nextTag <= defaultLo) nextTag = defaultLo + 1; decl.tagHi = nextTag - 1; // Set tagLo/tagHi on _ case's synthetic VstClass (after decl.tagHi is known). for (c in decl.cases) { @@ -1038,7 +1041,7 @@ class Verifier(compiler: Compiler, prog: Program) { } if (m.func.params.list != null) { for (p = m.func.params.list; p != null; p = p.tail) { - if (p.head.tref != null) cv.resolveType(p.head.tref, decl.typeEnv); + if (p.head.tref != null) p.head.vtype = cv.resolveType(p.head.tref, decl.typeEnv); } } tc.enterMethod(m, env); diff --git a/test/enums/enum_cmethod00.v3 b/test/enums/enum_cmethod00.v3 new file mode 100644 index 000000000..694d8de46 --- /dev/null +++ b/test/enums/enum_cmethod00.v3 @@ -0,0 +1,12 @@ +//@execute 0=10; 1=20; 2=0 +// Per-case method override: A and B override m(), C uses default +enum E { + A { def m() -> int { return 10; } }, + B { def m() -> int { return 20; } }, + C; + def m() -> int { return 0; } +} +def main(arg: int) -> int { + var vals = [E.A, E.B, E.C]; + return vals[arg].m(); +} diff --git a/test/enums/enum_cmethod01.v3 b/test/enums/enum_cmethod01.v3 new file mode 100644 index 000000000..b444037fc --- /dev/null +++ b/test/enums/enum_cmethod01.v3 @@ -0,0 +1,12 @@ +//@execute 0=10; 1=0; 2=30 +// Per-case method override: only A and C override, B uses default +enum E { + A { def m() -> int { return 10; } }, + B, + C { def m() -> int { return 30; } }; + def m() -> int { return 0; } +} +def main(arg: int) -> int { + var vals = [E.A, E.B, E.C]; + return vals[arg].m(); +} diff --git a/test/enums/enum_cmethod02.v3 b/test/enums/enum_cmethod02.v3 new file mode 100644 index 000000000..4effd16e6 --- /dev/null +++ b/test/enums/enum_cmethod02.v3 @@ -0,0 +1,13 @@ +//@execute 0=10; 1=0; 2=20; 3=2 +// Per-case method override with parameters +enum E { + A { def m(x: int) -> int { return 10 + x; } }, + B, + C { def m(x: int) -> int { return 20 + x; } }; + def m(x: int) -> int { return x; } +} +def main(arg: int) -> int { + var vals = [E.A, E.B, E.C]; + if (arg < 3) return vals[arg].m(0); + return vals[1].m(2); +} diff --git a/test/enums/enum_cmethod03.v3 b/test/enums/enum_cmethod03.v3 new file mode 100644 index 000000000..11f2e66d2 --- /dev/null +++ b/test/enums/enum_cmethod03.v3 @@ -0,0 +1,16 @@ +//@execute 0=42; 1=7; 2=99 +// Per-case override: static dispatch (E.A.m()) should use case override +enum E { + A { def m() -> int { return 42; } }, + B { def m() -> int { return 7; } }, + C; + def m() -> int { return 0; } +} +def main(arg: int) -> int { + match (arg) { + 0 => return E.A.m(); + 1 => return E.B.m(); + 2 => return E.A.m() + E.B.m() + E.C.m() + 50; + } + return -1; +} diff --git a/test/enums/enum_cmethod04.v3 b/test/enums/enum_cmethod04.v3 new file mode 100644 index 000000000..ce995761e --- /dev/null +++ b/test/enums/enum_cmethod04.v3 @@ -0,0 +1,14 @@ +//@execute 0=100; 1=200; 2=0; 3=100; 4=200 +// Per-case override with enum fields +enum E(x: int) { + A(10) { def m() -> int { return x * 10; } }, + B(10) { def m() -> int { return x * 20; } }, + C(10); + def m() -> int { return 0; } +} +def main(arg: int) -> int { + var vals = [E.A, E.B, E.C]; + if (arg < 3) return vals[arg].m(); + if (arg == 3) return E.A.m(); + return E.B.m(); +} diff --git a/test/enums/enum_cmethod05.v3 b/test/enums/enum_cmethod05.v3 new file mode 100644 index 000000000..10de368bd --- /dev/null +++ b/test/enums/enum_cmethod05.v3 @@ -0,0 +1,23 @@ +//@execute 0=10; 1=20; 2=0; 3=10; 4=20 +// Per-case override combined with subtype override +enum E { + A { def m() -> int { return 10; } }, + B { def m() -> int { return 20; } }, + _; + def m() -> int { return 0; } +} +enum E.More { + C { def m() -> int { return 30; } }, + D; + def m() -> int { return 40; } +} +def main(arg: int) -> int { + match (arg) { + 0 => return E.A.m(); + 1 => return E.B.m(); + 2 => { var e: E = E.A; return E.More.D.m() - e.m() - E.B.m() - 10; } + 3 => { var vals: Array = [E.A, E.B]; return vals[0].m(); } + 4 => { var vals: Array = [E.A, E.B]; return vals[1].m(); } + } + return -1; +} From 2ff4b135cc16ab8de5feb76e21002ed858bd28d8 Mon Sep 17 00:00:00 2001 From: Eliot Moss Date: Tue, 21 Apr 2026 22:17:53 -0400 Subject: [PATCH 48/58] Skip NullCheck on enum receivers during early inlining The inliner inserted a NullCheck before inlining enum method calls. Since enum tag 0 is a valid value (not null), this caused a spurious NullCheckException when -O3 (InlineEarly) was enabled. Add V3.isEnum guard alongside the existing V3.isVariant guard. Co-Authored-By: Claude Opus 4.6 (1M context) --- aeneas/src/ssa/SsaInliner.v3 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aeneas/src/ssa/SsaInliner.v3 b/aeneas/src/ssa/SsaInliner.v3 index ebf8c5266..91792cb0e 100644 --- a/aeneas/src/ssa/SsaInliner.v3 +++ b/aeneas/src/ssa/SsaInliner.v3 @@ -65,7 +65,7 @@ class SsaInliner extends SsaRebuilder { } else { // class/variant method call; insert null check if necessary var receiver = call.input0(); - if (!V3.isVariant(inlinee.receiver) && V3Op.needsNullCheck(call, receiver)) { + if (!V3.isVariant(inlinee.receiver) && !V3.isEnum(inlinee.receiver) && V3Op.needsNullCheck(call, receiver)) { curBlock.source = call.source; curBlock.opNullCheck(inlinee.receiver, receiver); } From 4553cb12195bfd6d4b68a8df56149efb9877a1e8 Mon Sep 17 00:00:00 2001 From: Eliot Moss Date: Tue, 21 Apr 2026 23:03:33 -0400 Subject: [PATCH 49/58] Remove dead enum mtable code; fix _ case liveness and mtable slots; update docs Remove fillEnumMtable/resolveEnumMethodImpl (dead in Strategy B). Add fillEnumDefaultSlots to fill mtable slots for _ cases which have no synthetic RaClass. Add markEnumDefaultMethodsLive to ensure default methods at all hierarchy levels are marked live for virtual dispatch. Restore per-case method syntax in grammar and tutorial documentation. Co-Authored-By: Claude Opus 4.6 (1M context) --- aeneas/src/ir/Normalization.v3 | 49 +++++++++++++++------------------- aeneas/src/ir/Reachability.v3 | 17 ++++++++++++ doc/grammar-claude.md | 11 +++++--- doc/tutorial/EnumMethods.md | 34 ++++++++++++++++++++--- test/enums/enum_cmethod06.v3 | 26 ++++++++++++++++++ 5 files changed, 103 insertions(+), 34 deletions(-) create mode 100644 test/enums/enum_cmethod06.v3 diff --git a/aeneas/src/ir/Normalization.v3 b/aeneas/src/ir/Normalization.v3 index ba1bfb7ff..01d0e3d00 100644 --- a/aeneas/src/ir/Normalization.v3 +++ b/aeneas/src/ir/Normalization.v3 @@ -637,6 +637,10 @@ class ReachabilityNormalizer(config: NormalizerConfig, ra: ReachabilityAnalyzer) if (liveClasses[l.head.minClassId] != l.head) continue; fillMtableSlot(mtable, l.head, rm); } + // Fill _ case slots: _ has no RaClass, so its tag slots are + // empty after the per-subtype fill above. Walk the hierarchy + // and fill unfilled slots with the appropriate default method. + if (V3.isEnum(rc.oldType)) fillEnumDefaultSlots(mtable, rc, rm); } setMtable(rc, rv); // set mtable for all child virtual methods } @@ -652,6 +656,24 @@ class ReachabilityNormalizer(config: NormalizerConfig, ra: ReachabilityAnalyzer) var impl = resolveMethodImpl(rc, rm); setMtableEntry(mtable, impl, rc.minClassId - mtable.rootId); } + private def fillEnumDefaultSlots(mtable: IrMtable, rc: RaClass, rm: RaMethod) { + var decl = V3.getDecl(rc.oldType); + if (decl.hasDefault) { + var impl = resolveMethodImpl(rc, rm); + if (impl != null) { + var tagLo = V3.getTagLo(rc.oldType); + var tagHi = V3.getTagHi(rc.oldType); + if (tagLo < 0) tagLo = 0; + for (tag = tagLo; tag <= tagHi; tag++) { + if (mtable.table[tag] == null) setMtableEntry(mtable, impl, tag); + } + } + } + for (l = decl.subtypeOrder; l != null; l = l.tail) { + var childRc = ra.getClass(l.head.getDeclaredType()); + if (childRc != null) fillEnumDefaultSlots(mtable, RaClass.!(childRc), rm); + } + } private def createEnumVariantNorms(rc: RaClass) { var tagType = V3.getVariantTagType(rc.oldType); var tagTypeNorm = norm(tagType); @@ -669,20 +691,6 @@ class ReachabilityNormalizer(config: NormalizerConfig, ra: ReachabilityAnalyzer) vn.children = List.new(l.head.variantNorm, vn.children); } } - private def fillEnumMtable(mtable: IrMtable, rc: RaClass, rm: RaMethod) { - var impl = resolveEnumMethodImpl(rc, rm); - if (impl == null) impl = rm; // fall back to root - var tagLo = V3.getTagLo(rc.oldType); - var tagHi = V3.getTagHi(rc.oldType); - if (tagLo < 0) tagLo = 0; - for (tag = tagLo; tag <= tagHi; tag++) setMtableEntry(mtable, impl, tag); - // Subtypes override their range - var decl = V3.getDecl(rc.oldType); - for (l = decl.subtypeOrder; l != null; l = l.tail) { - var childRc = ra.getClass(l.head.getDeclaredType()); - if (childRc != null) fillEnumMtable(mtable, RaClass.!(childRc), rm); - } - } def setMtable(rc: RaClass, rv: RaVirtual) { var rm = rc.findRaMethod(rv.raMethod); if (rm != null && rm.virtual != null) rm.virtual.mtable = rv.mtable; @@ -698,19 +706,6 @@ class ReachabilityNormalizer(config: NormalizerConfig, ra: ReachabilityAnalyzer) } return null; } - // Resolve an enum method by matching VstMethod.root identity, since enum - // subtype IrClasses don't inherit parent's method array indices. - private def resolveEnumMethodImpl(rc: RaClass, rm: RaMethod) -> RaMethod { - var rootSource = rm.orig.source; - for (ml in rc.methods) { - for (l = ml; l != null; l = l.tail) { - var m = l.head; - if (m.orig.source == rootSource) return m; - if (VstMethod.?(m.orig.source) && VstMethod.!(m.orig.source).root == rootSource) return m; - } - } - return null; - } def normValIntoArray(v: Val, tn: TypeNorm, array: Array, index: int) { match (v) { null => ; diff --git a/aeneas/src/ir/Reachability.v3 b/aeneas/src/ir/Reachability.v3 index 6d3058422..08ad9d27d 100644 --- a/aeneas/src/ir/Reachability.v3 +++ b/aeneas/src/ir/Reachability.v3 @@ -530,6 +530,23 @@ class ReachabilityAnalyzer(compilation: Compilation) { for (l = rc.subtypes; l != null; l = l.tail) { analyzeVirtual(l.head, rm); } + // For enums, _ cases have no synthetic RaClass in subtypes, so their + // default method impl is never discovered by analyzeVirtual above. + // Walk the hierarchy and mark each level's resolved method live. + if (rc.raFacts.RC_ENUM) markEnumDefaultMethodsLive(rc, rm); + } + private def markEnumDefaultMethodsLive(rc: RaClass, rm: RaMethod) { + var decl = V3.getDecl(rc.oldType); + if (decl.hasDefault) { + var spec = oldIr.resolveMethodImpl(rc.oldType, rm.getSpec()); + var impl = makeMethod(spec.typeArgs, IrMethod.!(spec.member), null); + rm.virtual.addImpl(impl); + getMethod(null, impl); + } + for (l = decl.subtypeOrder; l != null; l = l.tail) { + var childRc = getClass(l.head.getDeclaredType()); + if (childRc != null) markEnumDefaultMethodsLive(RaClass.!(childRc), rm); + } } def getMethod(op: SsaApplyOp, rm: RaMethod) { if (rm.setFact(RaFact.RM_LIVE)) return; diff --git a/doc/grammar-claude.md b/doc/grammar-claude.md index 36facd6ac..96b695ed8 100644 --- a/doc/grammar-claude.md +++ b/doc/grammar-claude.md @@ -77,10 +77,12 @@ In match patterns, unqualified subtype names (`Bar =>` or `b: Bar =>`) automatic ``` EnumDecl ::= DottedId EnumParams? '{' EnumCase* (';' EnumMethod*)? '}' -EnumCase ::= id ['(' Expr,* ')'] ','? // named case +EnumCase ::= id ['(' Expr,* ')'] EnumCaseBody? ','? // named case with optional per-case methods | '_' ','? // default case: optional, must be last, at most one -EnumMethod ::= ['private'] 'def' DefDef // shared by all cases +EnumCaseBody ::= '{' EnumMethod* '}' // per-case method overrides + +EnumMethod ::= ['private'] 'def' DefDef // shared by all cases (after ';') or per-case override (in case body) EnumParams ::= '(' 'super' ')' // inherit parent's params | '(' 'super' ',' ParamDecl,+ ')' // inherit parent's params + add new fields @@ -119,12 +121,15 @@ Subtypes may **not** declare params if the parent has no effective params. Using ### Enum methods -An `EnumMethod` declared after the `;` separator is shared by all cases. +An `EnumMethod` declared after the `;` separator is shared by all cases (the default implementation). + +An `EnumMethod` declared inside an `EnumCaseBody` (`{ def ... }`) overrides the enum-level method of the same name for that specific case. The override must have the same signature as the root method. ### Enum method inheritance - Methods declared on a parent enum are inherited by all subtype enums (transitively). - A subtype enum may override an inherited method by declaring a method with the same name and signature after its own `;` separator. +- Individual cases (at any level) may override a method by declaring it in a case body: `case X { def m() -> int { return 42; } }`. - All virtual dispatch goes through the root enum's dispatch table, regardless of where the override is declared. diff --git a/doc/tutorial/EnumMethods.md b/doc/tutorial/EnumMethods.md index f62f51eba..12fd2d969 100644 --- a/doc/tutorial/EnumMethods.md +++ b/doc/tutorial/EnumMethods.md @@ -16,20 +16,46 @@ enum Planet(mass: double, radius: double) { var g = Planet.EARTH.surfaceGravity(); ``` +## Per-case method overrides + +Individual cases can override an enum-level method by declaring it inside a case body `{ ... }`. +The override must have the same name and signature as the root method. + +``` +enum Expr { + ADD { def eval(a: int, b: int) -> int { return a + b; } }, + SUB { def eval(a: int, b: int) -> int { return a - b; } }, + MUL { def eval(a: int, b: int) -> int { return a * b; } }; + + def eval(a: int, b: int) -> int { return 0; } // default +} +var r = Expr.ADD.eval(3, 4); // returns 7 +``` + +A case that does not override a method uses the enum-level default. +Dispatch is virtual: a variable of the enum type dispatches to the correct per-case implementation at runtime. + +``` +def apply(op: Expr, a: int, b: int) -> int { + return op.eval(a, b); // virtual dispatch by tag +} +``` + ## Subtype enum method inheritance Subtype enums inherit methods from their parent. -A subtype can override an inherited method. +A subtype can override an inherited method for all of its cases. +Individual cases within a subtype can also provide their own overrides. ``` enum Animal { DOG, CAT, _; def speak() -> int { return 0; } } enum Animal.Exotic { - PARROT, + PARROT { def speak() -> int { return 2; } }, // per-case override SNAKE; - def speak() -> int { return 1; } // override for all Exotic cases + def speak() -> int { return 1; } // subtype-level override } ``` -Here `Animal.Exotic.SNAKE.speak()` returns `1` (subtype override) and `Animal.DOG.speak()` returns `0` (root default). +Here `Animal.DOG.speak()` returns `0` (root default), `Animal.Exotic.SNAKE.speak()` returns `1` (subtype override), and `Animal.Exotic.PARROT.speak()` returns `2` (per-case override). All dispatch goes through the root enum's dispatch table, so a variable of type `Animal` will dispatch correctly regardless of whether the value is a root case or a subtype case. diff --git a/test/enums/enum_cmethod06.v3 b/test/enums/enum_cmethod06.v3 new file mode 100644 index 000000000..0e007498c --- /dev/null +++ b/test/enums/enum_cmethod06.v3 @@ -0,0 +1,26 @@ +//@execute 0=0; 1=10; 2=20; 3=0; 4=40; 5=40 +// Virtual dispatch through _ case at root and subtype levels +enum E { + A { def m() -> int { return 10; } }, + B { def m() -> int { return 20; } }, + _; + def m() -> int { return 0; } +} +enum E.More { + C, + _; + def m() -> int { return 40; } +} +def dispatch(e: E) -> int { return e.m(); } +def main(arg: int) -> int { + var all: Array = [E.A, E.B, E.More.C]; + match (arg) { + 0 => return dispatch(E.A) + dispatch(E.B) - 30; // named cases + 1 => return dispatch(all[0]); // A via array + 2 => return dispatch(all[1]); // B via array + 3 => return dispatch(all[2]) - 40; // More.C uses More.m (default) + 4 => return dispatch(E.More.C); // More.C static -> virtual + 5 => { var e: E = E.More.C; return e.m(); } // virtual through E type + } + return -1; +} From da2bed3d58f228276636ee030c3c9cb8d5245fbd Mon Sep 17 00:00:00 2001 From: Eliot Moss Date: Tue, 21 Apr 2026 23:35:16 -0400 Subject: [PATCH 50/58] Add EnumScalability benchmark for Strategy A vs B comparison Generator creates an enum with N cases (default 1000), 4 params each, and 3 methods. Runner compiles with both strategy compilers and reports compile time, RSS, binary size, and runtime overhead across targets (x86-64-linux, jar, wasm, wasm-gc). Co-Authored-By: Claude Opus 4.6 (1M context) --- bench/EnumScalability/generate.bash | 88 ++++++++++++++ bench/EnumScalability/run-bench.bash | 164 +++++++++++++++++++++++++++ 2 files changed, 252 insertions(+) create mode 100755 bench/EnumScalability/generate.bash create mode 100755 bench/EnumScalability/run-bench.bash diff --git a/bench/EnumScalability/generate.bash b/bench/EnumScalability/generate.bash new file mode 100755 index 000000000..30695abd2 --- /dev/null +++ b/bench/EnumScalability/generate.bash @@ -0,0 +1,88 @@ +#!/usr/bin/env bash +# Generate a Virgil enum with N cases (default 1000), 4 params each, +# plus methods exercising virtual dispatch, for scalability testing. +# Usage: ./generate.bash [num_cases] > BigEnum.v3 + +N=${1:-1000} + +cat <<'HDR' +// Generated benchmark: large enum for compilation/space overhead comparison. +// Measures: compile time, binary size, runtime RSS. + +enum Big(a: int, b: int, c: int, d: int) { +HDR + +for (( i=0; i int { return a + b + c + d; } + def prod() -> int { return a * b + c * d; } + def hash() -> int { return (a ^ (b << 3)) + (c ^ (d << 5)); } +} + +MTH + +# Generate the array of all cases +echo "def allCases() -> Array {" +printf "\treturn [" +for (( i=0; i 0 )); then printf ", "; fi + if (( i > 0 && i % 10 == 0 )); then printf "\n\t\t"; fi + printf "Big.C%04d" $i +done +echo "];" +echo "}" + +cat <<'MAIN' + +def parseArg(args: Array) -> int { + if (args.length < 1) return 0; + var s = args[0]; + var r = 0; + for (i < s.length) { + var c = s[i]; + if (c >= '0' && c <= '9') r = r * 10 + (c - '0'); + } + return r; +} + +def run(arg: int) -> int { + var all = allCases(); + var n = all.length; + var s = 0; + match (arg) { + 0 => { + for (i < n) s += all[i].sum(); + } + 1 => { + for (i < n) s += all[i].prod(); + } + 2 => { + for (i < n) s += all[i].hash(); + } + 3 => { + for (j < 1000) { + for (i < n) s += all[i].sum(); + } + } + 4 => { + for (i < n) s += all[i].a + all[i].d; + } + } + return s & 0xFFFF; +} + +def main(args: Array) -> int { + var r = run(parseArg(args)); + return r - r; +} +MAIN diff --git a/bench/EnumScalability/run-bench.bash b/bench/EnumScalability/run-bench.bash new file mode 100755 index 000000000..9e60add5e --- /dev/null +++ b/bench/EnumScalability/run-bench.bash @@ -0,0 +1,164 @@ +#!/usr/bin/env bash +# Compare enum scalability across Strategy A and Strategy B compilers. +# +# Usage: ./run-bench.bash [num_cases] +# +# Env vars: +# AENEAS_A - path to Strategy A compiler (default: built from open_enums3a) +# AENEAS_B - path to Strategy B compiler (default: built from open_enums3b) +# TARGETS - space-separated targets (default: "x86-64-linux") +# RUNS - runs per timing measurement (default: 3) +# N - number of enum cases (default: 1000, overridden by $1) + +set -euo pipefail + +DIR="$( cd -P "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +VIRGIL_LOC=${VIRGIL_LOC:=$(cd "$DIR/../.." && pwd)} + +N=${1:-${N:-1000}} +TARGETS=${TARGETS:-"x86-64-linux"} +RUNS=${RUNS:-3} +TMP=/tmp/$USER/virgil-bench/enum-scale +mkdir -p "$TMP" + +# ---------- generate source ---------- +SRC="$TMP/BigEnum_${N}.v3" +if [ ! -f "$SRC" ]; then + echo "Generating enum with $N cases, 4 params each..." + bash "$DIR/generate.bash" "$N" > "$SRC" + wc -l "$SRC" +fi + +# ---------- resolve compilers ---------- +AENEAS_A=${AENEAS_A:-$VIRGIL_LOC/bin/current/x86-64-linux/Aeneas} +AENEAS_B=${AENEAS_B:-$VIRGIL_LOC/bin/current/x86-64-linux/Aeneas} +if [ ! -x "$AENEAS_A" ]; then AENEAS_A=$VIRGIL_LOC/bin/current/x86-linux/Aeneas; fi +if [ ! -x "$AENEAS_B" ]; then AENEAS_B=$VIRGIL_LOC/bin/current/x86-linux/Aeneas; fi + +# ---------- helper: compile + measure ---------- +compile_and_measure() { + local label=$1 aeneas=$2 target=$3 tag=$4 + local outdir="$TMP/${tag}_${target}" + local progname="big_${tag}" + mkdir -p "$outdir" + + local RT=$VIRGIL_LOC/rt + local rt_files compile_args runcmd binary + + local can_run=true + if [ "$target" = "jar" ]; then + binary="$outdir/$progname.jar" + rt_files="" + compile_args="-target=jar" + runcmd="java -jar $binary" + elif [ "$target" = "wasm-gc" ]; then + binary="$outdir/$progname.wasm" + rt_files="$(echo $RT/wasm-gc-wasi1/*.v3 $RT/wasm-wasi1-common/*.v3)" + compile_args="-target=wasm-gc" + can_run=false + elif [ "$target" = "wasm" ]; then + binary="$outdir/$progname.wasm" + rt_files="$(echo $RT/wasm-wasi1/*.v3 $RT/wasm-wasi1-common/*.v3 $RT/native/NativeFileStream.v3)" + compile_args="-target=wasm" + can_run=false + else + binary="$outdir/$progname" + rt_files="$(echo $RT/$target/*.v3 $RT/native/*.v3 $RT/gc/*.v3)" + compile_args="-heap-size=200m -target=$target" + runcmd="$binary" + fi + + # --- compile timing (best of RUNS) --- + local best_compile="" + for run in $(seq 1 $RUNS); do + rm -f "$binary" + local elapsed + elapsed=$( { /usr/bin/time -f "%e %M" \ + "$aeneas" $compile_args -rt.files="$rt_files" \ + -output="$outdir" -program-name="$progname" "$SRC" ; } 2>&1 | tail -1 ) + local secs=$(echo "$elapsed" | awk '{print $1}') + local mem=$(echo "$elapsed" | awk '{print $2}') + if [ -z "$best_compile" ] || [ $(echo "$secs < $best_compile" | bc) = 1 ]; then + best_compile=$secs + best_mem=$mem + fi + done + + # --- binary size --- + local binsize + if [ "$target" = "jar" ]; then + binsize=$(stat -c%s "$binary" 2>/dev/null || stat -f%z "$binary") + else + binsize=$(stat -c%s "$binary" 2>/dev/null || stat -f%z "$binary") + fi + + # --- runtime: execute test 3 (steady-state iteration), measure RSS --- + local best_run="" run_rss="" + if [ "$can_run" = "true" ]; then + for run in $(seq 1 $RUNS); do + local elapsed + elapsed=$( { /usr/bin/time -f "%e %M" $runcmd 3 ; } 2>&1 | tail -1 ) + local secs=$(echo "$elapsed" | awk '{print $1}') + local mem=$(echo "$elapsed" | awk '{print $2}') + if [ -z "$best_run" ] || [ $(echo "$secs < $best_run" | bc) = 1 ]; then + best_run=$secs + run_rss=$mem + fi + done + else + best_run="n/a" + run_rss="n/a" + fi + + printf " %-14s compile: %6ss %6s KB binary: %8s B run: %6ss %6s KB\n" \ + "$label" "$best_compile" "$best_mem" "$binsize" "$best_run" "$run_rss" +} + +# ---------- wasm-gc runner if needed ---------- +if [[ "$TARGETS" == *"wasm-gc"* ]]; then + RUNNER=$TMP/run-wgc.mjs + if [ ! -f "$RUNNER" ]; then + cat > "$RUNNER" << 'JSEOF' +import { readFileSync } from 'node:fs'; +import { WASI } from 'wasi'; +import { argv, env } from 'node:process'; +const wasi = new WASI({ returnOnExit: false, version: 'preview1', args: argv.slice(2), env, preopens: { '.': '.' } }); +const importObject = { wasi_snapshot_preview1: wasi.wasiImport }; +const instance = new WebAssembly.Instance(new WebAssembly.Module(readFileSync(argv[2])), importObject); +wasi.initialize(instance); +instance.exports.entry(); +JSEOF + fi +fi + +# ---------- main ---------- +echo "========================================" +echo "Enum Scalability Benchmark" +echo " Cases: $N Params: 4 Methods: 3" +echo " Runs: $RUNS (best of)" +echo "========================================" + +for target in $TARGETS; do + echo "" + echo "--- $target ---" + echo " Strategy compile-time compile-RSS binary-size run-time run-RSS" + compile_and_measure "Strategy-A" "$AENEAS_A" "$target" "stratA" + compile_and_measure "Strategy-B" "$AENEAS_B" "$target" "stratB" + + # --- diff summary --- + echo "" + sA="$TMP/stratA_${target}/big_stratA" + sB="$TMP/stratB_${target}/big_stratB" + if [ "$target" = "jar" ]; then sA="${sA}.jar"; sB="${sB}.jar"; fi + if [ -f "$sA" ] && [ -f "$sB" ]; then + szA=$(stat -c%s "$sA" 2>/dev/null || stat -f%z "$sA") + szB=$(stat -c%s "$sB" 2>/dev/null || stat -f%z "$sB") + if [ "$szA" -gt 0 ]; then + pct=$(echo "scale=1; ($szB - $szA) * 100 / $szA" | bc) + echo " Binary delta: B is ${pct}% vs A ($szA -> $szB bytes)" + fi + fi +done + +echo "" +echo "Done." From 28a0f4b7a726088065ac4e9ca4e4a7b502e57f30 Mon Sep 17 00:00:00 2001 From: Eliot Moss Date: Wed, 22 Apr 2026 17:36:44 -0400 Subject: [PATCH 51/58] Elide RaClasses for enum cases without per-case overrides Cases with c.members == null share the parent RaClass instead of getting their own synthetic RaClass. This reduces the number of IrClasses in the normalized output (and thus JVM classes and wasm-gc types). fillEnumDefaultSlots now recurses into subtypes before filling at the current level, so deeper method overrides take precedence over parent defaults. markEnumDefaultMethodsLive checks for gaps (cases without RaClasses) rather than only checking hasDefault. Co-Authored-By: Claude Opus 4.6 (1M context) --- aeneas/src/ir/Normalization.v3 | 22 +++++++++++----------- aeneas/src/ir/Reachability.v3 | 12 +++++++++++- 2 files changed, 22 insertions(+), 12 deletions(-) diff --git a/aeneas/src/ir/Normalization.v3 b/aeneas/src/ir/Normalization.v3 index 01d0e3d00..83a1befd7 100644 --- a/aeneas/src/ir/Normalization.v3 +++ b/aeneas/src/ir/Normalization.v3 @@ -657,22 +657,22 @@ class ReachabilityNormalizer(config: NormalizerConfig, ra: ReachabilityAnalyzer) setMtableEntry(mtable, impl, rc.minClassId - mtable.rootId); } private def fillEnumDefaultSlots(mtable: IrMtable, rc: RaClass, rm: RaMethod) { + // Recurse into subtypes first so deeper overrides take precedence var decl = V3.getDecl(rc.oldType); - if (decl.hasDefault) { - var impl = resolveMethodImpl(rc, rm); - if (impl != null) { - var tagLo = V3.getTagLo(rc.oldType); - var tagHi = V3.getTagHi(rc.oldType); - if (tagLo < 0) tagLo = 0; - for (tag = tagLo; tag <= tagHi; tag++) { - if (mtable.table[tag] == null) setMtableEntry(mtable, impl, tag); - } - } - } for (l = decl.subtypeOrder; l != null; l = l.tail) { var childRc = ra.getClass(l.head.getDeclaredType()); if (childRc != null) fillEnumDefaultSlots(mtable, RaClass.!(childRc), rm); } + // Fill remaining empty slots at this level + var impl = resolveMethodImpl(rc, rm); + if (impl != null) { + var tagLo = V3.getTagLo(rc.oldType); + var tagHi = V3.getTagHi(rc.oldType); + if (tagLo < 0) tagLo = 0; + for (tag = tagLo; tag <= tagHi; tag++) { + if (mtable.table[tag] == null) setMtableEntry(mtable, impl, tag); + } + } } private def createEnumVariantNorms(rc: RaClass) { var tagType = V3.getVariantTagType(rc.oldType); diff --git a/aeneas/src/ir/Reachability.v3 b/aeneas/src/ir/Reachability.v3 index 08ad9d27d..5db92c040 100644 --- a/aeneas/src/ir/Reachability.v3 +++ b/aeneas/src/ir/Reachability.v3 @@ -535,9 +535,18 @@ class ReachabilityAnalyzer(compilation: Compilation) { // Walk the hierarchy and mark each level's resolved method live. if (rc.raFacts.RC_ENUM) markEnumDefaultMethodsLive(rc, rm); } + // For cases without their own RaClass (no per-case overrides), mark the + // resolved method at the declaring level as live so its mtable slot gets filled. private def markEnumDefaultMethodsLive(rc: RaClass, rm: RaMethod) { var decl = V3.getDecl(rc.oldType); - if (decl.hasDefault) { + // Check if any case at this level lacks per-case overrides + var hasGap = decl.hasDefault; + if (!hasGap && decl.cases != null) { + for (c in decl.cases) { + if (c.members == null) { hasGap = true; break; } + } + } + if (hasGap) { var spec = oldIr.resolveMethodImpl(rc.oldType, rm.getSpec()); var impl = makeMethod(spec.typeArgs, IrMethod.!(spec.member), null); rm.virtual.addImpl(impl); @@ -789,6 +798,7 @@ class ReachabilityAnalyzer(compilation: Compilation) { if (decl.cases == null) return; for (c in decl.cases) { if (c.decl == null || !c.decl.isSynthetic) continue; + if (c.members == null) continue; // no overrides: share parent RaClass var caseType = V3.getSyntheticEnumCaseType(c); var caseIc = oldIr.makeIrClass(caseType); var caseRc = newRaClass(caseType, caseIc, rc); From 62ecac0cf1691e35ad40d00d77205b43af6f11c9 Mon Sep 17 00:00:00 2001 From: Eliot Moss Date: Wed, 22 Apr 2026 18:11:20 -0400 Subject: [PATCH 52/58] Queue-based per-case enum liveness analysis MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace eager enum method liveness with a two-dimensional fixpoint: live cases × live virtual methods → live implementations. When an enum constant appears in SSA values, markEnumCaseLive records the tag and resolves implementations for all known virtual methods. When a virtual call is first seen, markEnumVirtualLive records the method and resolves implementations for all live tags. The common resolveEnumCaseImpl handles both: cases with RaClasses use analyzeVirtual; cases without resolve at the declaring enum level. fillEnumDefaultSlots now skips slots for dead tags, reducing mtable entries and the methods they pull in. Co-Authored-By: Claude Opus 4.6 (1M context) --- aeneas/src/ir/Normalization.v3 | 8 ++- aeneas/src/ir/Reachability.v3 | 95 ++++++++++++++++++++++++---------- 2 files changed, 74 insertions(+), 29 deletions(-) diff --git a/aeneas/src/ir/Normalization.v3 b/aeneas/src/ir/Normalization.v3 index 83a1befd7..d1829b4db 100644 --- a/aeneas/src/ir/Normalization.v3 +++ b/aeneas/src/ir/Normalization.v3 @@ -657,20 +657,24 @@ class ReachabilityNormalizer(config: NormalizerConfig, ra: ReachabilityAnalyzer) setMtableEntry(mtable, impl, rc.minClassId - mtable.rootId); } private def fillEnumDefaultSlots(mtable: IrMtable, rc: RaClass, rm: RaMethod) { + var rootRc = rc; + while (rootRc.parent != null && rootRc.parent.raFacts.RC_ENUM) rootRc = rootRc.parent; // Recurse into subtypes first so deeper overrides take precedence var decl = V3.getDecl(rc.oldType); for (l = decl.subtypeOrder; l != null; l = l.tail) { var childRc = ra.getClass(l.head.getDeclaredType()); if (childRc != null) fillEnumDefaultSlots(mtable, RaClass.!(childRc), rm); } - // Fill remaining empty slots at this level + // Fill remaining empty slots at this level (only for live tags) var impl = resolveMethodImpl(rc, rm); if (impl != null) { var tagLo = V3.getTagLo(rc.oldType); var tagHi = V3.getTagHi(rc.oldType); if (tagLo < 0) tagLo = 0; for (tag = tagLo; tag <= tagHi; tag++) { - if (mtable.table[tag] == null) setMtableEntry(mtable, impl, tag); + if (mtable.table[tag] != null) continue; + if (rootRc.liveCaseTags != null && !rootRc.liveCaseTags[tag]) continue; + setMtableEntry(mtable, impl, tag); } } } diff --git a/aeneas/src/ir/Reachability.v3 b/aeneas/src/ir/Reachability.v3 index 5db92c040..2fc88308a 100644 --- a/aeneas/src/ir/Reachability.v3 +++ b/aeneas/src/ir/Reachability.v3 @@ -290,7 +290,16 @@ class ReachabilityAnalyzer(compilation: Compilation) { // analyze the values in a graph def analyzeValues(graph: SsaGraph) { if (graph.values != null) { - for (v in graph.values) if (v != null) analyzeValue(v.val); + for (v in graph.values) if (v != null) { + analyzeValue(v.val); + if (V3.isEnum(v.vtype)) { + var tag = 0; // null/zero-init → tag 0 + match (v.val) { + x: Box => tag = x.val; + } + markEnumCaseLive(v.vtype, tag); + } + } } } // gather polymorphic operators @@ -530,32 +539,51 @@ class ReachabilityAnalyzer(compilation: Compilation) { for (l = rc.subtypes; l != null; l = l.tail) { analyzeVirtual(l.head, rm); } - // For enums, _ cases have no synthetic RaClass in subtypes, so their - // default method impl is never discovered by analyzeVirtual above. - // Walk the hierarchy and mark each level's resolved method live. - if (rc.raFacts.RC_ENUM) markEnumDefaultMethodsLive(rc, rm); - } - // For cases without their own RaClass (no per-case overrides), mark the - // resolved method at the declaring level as live so its mtable slot gets filled. - private def markEnumDefaultMethodsLive(rc: RaClass, rm: RaMethod) { - var decl = V3.getDecl(rc.oldType); - // Check if any case at this level lacks per-case overrides - var hasGap = decl.hasDefault; - if (!hasGap && decl.cases != null) { - for (c in decl.cases) { - if (c.members == null) { hasGap = true; break; } - } - } - if (hasGap) { - var spec = oldIr.resolveMethodImpl(rc.oldType, rm.getSpec()); - var impl = makeMethod(spec.typeArgs, IrMethod.!(spec.member), null); - rm.virtual.addImpl(impl); - getMethod(null, impl); - } - for (l = decl.subtypeOrder; l != null; l = l.tail) { - var childRc = getClass(l.head.getDeclaredType()); - if (childRc != null) markEnumDefaultMethodsLive(RaClass.!(childRc), rm); + if (rc.raFacts.RC_ENUM) markEnumVirtualLive(rc, rm); + } + // Record a virtual method on an enum and resolve implementations for all live cases. + private def markEnumVirtualLive(rc: RaClass, rm: RaMethod) { + var rootRc = rc; + while (rootRc.parent != null && rootRc.parent.raFacts.RC_ENUM) rootRc = rootRc.parent; + if (rootRc.liveCaseTags == null) return; + rootRc.liveEnumVirtuals = List.new(rm, rootRc.liveEnumVirtuals); + for (tag < rootRc.liveCaseTags.length) { + if (rootRc.liveCaseTags[tag]) resolveEnumCaseImpl(rootRc, tag, rm); + } + } + // Mark an enum case as live and resolve implementations for all known virtual methods. + def markEnumCaseLive(enumType: Type, tag: int) { + var rc = RaClass.!(makeType(enumType)); + var rootRc = rc; + while (rootRc.parent != null && rootRc.parent.raFacts.RC_ENUM) rootRc = rootRc.parent; + if (rootRc.liveCaseTags == null) return; + if (tag < 0 || tag >= rootRc.liveCaseTags.length) return; + if (rootRc.liveCaseTags[tag]) return; // already live + rootRc.liveCaseTags[tag] = true; + // If case has its own RaClass, add to parent subtypes + var caseRc = rootRc.enumCaseRaClasses[tag]; + if (caseRc != null) { + caseRc.parent.subtypes = List.new(caseRc, caseRc.parent.subtypes); + } + for (l = rootRc.liveEnumVirtuals; l != null; l = l.tail) { + resolveEnumCaseImpl(rootRc, tag, l.head); + } + } + // Resolve the implementation of a virtual method for a specific case tag. + private def resolveEnumCaseImpl(rootRc: RaClass, tag: int, rm: RaMethod) { + // If case has its own RaClass, use analyzeVirtual (handles overrides) + var caseRc = rootRc.enumCaseRaClasses[tag]; + if (caseRc != null) { + analyzeVirtual(caseRc, rm); + return; } + // Otherwise resolve at the declaring enum level + var declType = rootRc.caseDeclaringTypes[tag]; + if (declType == null) declType = rootRc.oldType; + var spec = oldIr.resolveMethodImpl(declType, rm.getSpec()); + var impl = makeMethod(spec.typeArgs, IrMethod.!(spec.member), null); + rm.virtual.addImpl(impl); + getMethod(null, impl); } def getMethod(op: SsaApplyOp, rm: RaMethod) { if (rm.setFact(RaFact.RM_LIVE)) return; @@ -796,15 +824,24 @@ class ReachabilityAnalyzer(compilation: Compilation) { } private def addEnumCaseClasses(rc: RaClass, decl: VstClass) { if (decl.cases == null) return; + var rootRc = rc; + while (rootRc.parent != null && rootRc.parent.raFacts.RC_ENUM) rootRc = rootRc.parent; + var totalTags = V3.getTagHi(rootRc.oldType) + 1; + if (rootRc.liveCaseTags == null) { + rootRc.liveCaseTags = Array.new(totalTags); + rootRc.enumCaseRaClasses = Array.new(totalTags); + rootRc.caseDeclaringTypes = Array.new(totalTags); + } for (c in decl.cases) { if (c.decl == null || !c.decl.isSynthetic) continue; + rootRc.caseDeclaringTypes[c.enumTag] = rc.oldType; if (c.members == null) continue; // no overrides: share parent RaClass var caseType = V3.getSyntheticEnumCaseType(c); var caseIc = oldIr.makeIrClass(caseType); var caseRc = newRaClass(caseType, caseIc, rc); caseRc.raFacts |= RaFact.RC_ENUM; typeMap[caseType] = caseRc; - rc.subtypes = List.new(caseRc, rc.subtypes); + rootRc.enumCaseRaClasses[c.enumTag] = caseRc; } } def mono(t: Type, spec: IrSpec) -> Type { @@ -845,6 +882,10 @@ class RaClass extends RaType { var maxClassId = -1; // maximum class ID var newIrType: Type; + var liveCaseTags: Array; // enum roots: which case tags are live + var liveEnumVirtuals: List; // enum roots: virtual methods needing dispatch + var enumCaseRaClasses: Array; // enum roots: tag -> RaClass (cases with overrides) + var caseDeclaringTypes: Array; // enum roots: tag -> declaring enum type var variantNorm: VariantNorm; // non-null for flattened variants var variantFields: Array; var origFieldTypes: Array; From 1fc2711232b5f8c1ac0c19b53485e1681d74d0a1 Mon Sep 17 00:00:00 2001 From: Eliot Moss Date: Wed, 22 Apr 2026 18:11:30 -0400 Subject: [PATCH 53/58] Add EnumLiveness and EnumOverrides benchmarks EnumLiveness: N-case enum with only K cases referenced. Measures the effect of per-case liveness analysis on compile time, RSS, binary size, and runtime across targets. EnumOverrides: N-case enum with M per-case method overrides. Measures the effect of RaClass elision, especially dramatic on jar (15x bloat at M=500 vs M=0 for 1000 cases). Both scale iteration counts to ~500M virtual calls for meaningful runtime measurement regardless of K or N. Co-Authored-By: Claude Opus 4.6 (1M context) --- bench/EnumLiveness/generate.bash | 101 ++++++++++++++++++++++ bench/EnumLiveness/run-bench.bash | 130 +++++++++++++++++++++++++++++ bench/EnumOverrides/generate.bash | 114 +++++++++++++++++++++++++ bench/EnumOverrides/run-bench.bash | 130 +++++++++++++++++++++++++++++ 4 files changed, 475 insertions(+) create mode 100644 bench/EnumLiveness/generate.bash create mode 100644 bench/EnumLiveness/run-bench.bash create mode 100644 bench/EnumOverrides/generate.bash create mode 100644 bench/EnumOverrides/run-bench.bash diff --git a/bench/EnumLiveness/generate.bash b/bench/EnumLiveness/generate.bash new file mode 100644 index 000000000..fe2985738 --- /dev/null +++ b/bench/EnumLiveness/generate.bash @@ -0,0 +1,101 @@ +#!/usr/bin/env bash +# Generate a Virgil enum with N cases and 3 shared methods, where only +# K cases are actually referenced in code. Measures the effect of +# per-case liveness analysis on binary size and compile time. +# +# Usage: ./generate.bash N K > BigEnum.v3 +# N = total enum cases (default 1000) +# K = number of cases actually referenced (default N) + +N=${1:-1000} +K=${2:-$N} +if (( K > N )); then K=$N; fi + +cat <<'HDR' +// Generated benchmark: enum liveness analysis. +// Only a subset of cases are referenced; the rest are dead. + +HDR + +printf "enum Big(a: int, b: int, c: int, d: int) {\n" + +for (( i=0; i int { return a + b + c + d; } + def prod() -> int { return a * b + c * d; } + def hash() -> int { return (a ^ (b << 3)) + (c ^ (d << 5)); } +} + +MTH + +# Generate liveCases() referencing only K cases (evenly spaced) +echo "def liveCases() -> Array {" +printf "\treturn [" +stride=$(( N / K )) +if (( stride < 1 )); then stride=1; fi +count=0 +for (( i=0; i 0 )); then printf ", "; fi + if (( count > 0 && count % 10 == 0 )); then printf "\n\t\t"; fi + printf "Big.C%04d" $i + (( count++ )) +done +echo "];" +echo "}" + +# Compute iteration count: ~500M total virtual calls regardless of K +iters=$(( 500000000 / K )) +if (( iters < 1000 )); then iters=1000; fi + +cat <
) -> int { + if (args.length < 1) return 0; + var s = args[0]; + var r = 0; + for (i < s.length) { + var c = s[i]; + if (c >= '0' && c <= '9') r = r * 10 + (c - '0'); + } + return r; +} + +def run(arg: int) -> int { + var live = liveCases(); + var n = live.length; + var s = 0; + match (arg) { + 0 => { + for (j < $iters) { + for (i < n) s += live[i].sum(); + } + } + 1 => { + for (j < $iters) { + for (i < n) s += live[i].prod(); + } + } + 2 => { + for (j < $iters) { + for (i < n) s += live[i].hash(); + } + } + } + return s & 0xFFFF; +} + +def main(args: Array) -> int { + var r = run(parseArg(args)); + return r - r; +} +MAIN diff --git a/bench/EnumLiveness/run-bench.bash b/bench/EnumLiveness/run-bench.bash new file mode 100644 index 000000000..52cb492df --- /dev/null +++ b/bench/EnumLiveness/run-bench.bash @@ -0,0 +1,130 @@ +#!/usr/bin/env bash +# Measure effect of per-case liveness analysis on enum compilation. +# Compiles the same N-case enum with varying numbers of live cases (K). +# +# Usage: ./run-bench.bash [N] +# +# Env vars: +# AENEAS - compiler to test (default: bin/current/x86-linux/Aeneas) +# TARGETS - space-separated targets (default: "x86-64-linux") +# RUNS - runs per timing measurement (default: 3) +# N - total enum cases (default: 1000, overridden by $1) +# K_VALUES - space-separated K values (default: "10 50 100 500 N") + +set -euo pipefail + +DIR="$( cd -P "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +VIRGIL_LOC=${VIRGIL_LOC:=$(cd "$DIR/../.." && pwd)} + +N=${1:-${N:-1000}} +TARGETS=${TARGETS:-"x86-64-linux"} +RUNS=${RUNS:-3} +K_VALUES=${K_VALUES:-"10 50 100 500 $N"} +TMP=/tmp/$USER/virgil-bench/enum-liveness +mkdir -p "$TMP" + +# ---------- resolve compiler ---------- +AENEAS=${AENEAS:-$VIRGIL_LOC/bin/current/x86-64-linux/Aeneas} +if [ ! -x "$AENEAS" ]; then AENEAS=$VIRGIL_LOC/bin/current/x86-linux/Aeneas; fi + +# ---------- generate sources ---------- +for K in $K_VALUES; do + SRC="$TMP/BigEnum_${N}_${K}.v3" + if [ ! -f "$SRC" ]; then + bash "$DIR/generate.bash" "$N" "$K" > "$SRC" + fi +done + +# ---------- helper: compile + measure ---------- +compile_and_measure() { + local label=$1 src=$2 target=$3 tag=$4 + local outdir="$TMP/${tag}_${target}" + local progname="big_${tag}" + mkdir -p "$outdir" + + local RT=$VIRGIL_LOC/rt + local rt_files compile_args binary + + local can_run=true runcmd="" + if [ "$target" = "jar" ]; then + binary="$outdir/$progname.jar" + rt_files="" + compile_args="-target=jar" + runcmd="java -jar $binary" + elif [ "$target" = "wasm-gc" ]; then + binary="$outdir/$progname.wasm" + rt_files="$(echo $RT/wasm-gc-wasi1/*.v3 $RT/wasm-wasi1-common/*.v3)" + compile_args="-target=wasm-gc" + can_run=false + elif [ "$target" = "wasm" ]; then + binary="$outdir/$progname.wasm" + rt_files="$(echo $RT/wasm-wasi1/*.v3 $RT/wasm-wasi1-common/*.v3 $RT/native/NativeFileStream.v3)" + compile_args="-target=wasm" + can_run=false + else + binary="$outdir/$progname" + rt_files="$(echo $RT/$target/*.v3 $RT/native/*.v3 $RT/gc/*.v3)" + compile_args="-heap-size=200m -target=$target" + runcmd="$binary" + fi + + # --- compile timing (best of RUNS) --- + local best_compile="" best_mem="" + for run in $(seq 1 $RUNS); do + rm -f "$binary" + local elapsed + elapsed=$( { /usr/bin/time -f "%e %M" \ + "$AENEAS" $compile_args -rt.files="$rt_files" \ + -output="$outdir" -program-name="$progname" "$src" ; } 2>&1 | tail -1 ) + local secs=$(echo "$elapsed" | awk '{print $1}') + local mem=$(echo "$elapsed" | awk '{print $2}') + if [ -z "$best_compile" ] || [ $(echo "$secs < $best_compile" | bc) = 1 ]; then + best_compile=$secs + best_mem=$mem + fi + done + + local binsize + binsize=$(stat -c%s "$binary" 2>/dev/null || stat -f%z "$binary") + + # --- runtime (best of RUNS), test 0 = sum with iterations --- + local best_run="" run_rss="" + if [ "$can_run" = "true" ]; then + for run in $(seq 1 $RUNS); do + local elapsed + elapsed=$( { /usr/bin/time -f "%e %M" $runcmd 0 ; } 2>&1 | tail -1 ) + local secs=$(echo "$elapsed" | awk '{print $1}') + local mem=$(echo "$elapsed" | awk '{print $2}') + if [ -z "$best_run" ] || [ $(echo "$secs < $best_run" | bc) = 1 ]; then + best_run=$secs + run_rss=$mem + fi + done + else + best_run="n/a" + run_rss="n/a" + fi + + printf " K=%-5s compile: %6ss %6sKB bin: %8sB run: %6ss %6sKB\n" \ + "$label" "$best_compile" "$best_mem" "$binsize" "$best_run" "$run_rss" +} + +# ---------- main ---------- +echo "========================================" +echo "Enum Liveness Benchmark" +echo " Total cases: $N K values: $K_VALUES" +echo " Runs: $RUNS (best of)" +echo " Compiler: $AENEAS" +echo "========================================" + +for target in $TARGETS; do + echo "" + echo "--- $target ---" + echo " K compile-time comp-RSS binary-size run-time run-RSS" + for K in $K_VALUES; do + compile_and_measure "$K" "$TMP/BigEnum_${N}_${K}.v3" "$target" "k${K}" + done +done + +echo "" +echo "Done." diff --git a/bench/EnumOverrides/generate.bash b/bench/EnumOverrides/generate.bash new file mode 100644 index 000000000..83083f735 --- /dev/null +++ b/bench/EnumOverrides/generate.bash @@ -0,0 +1,114 @@ +#!/usr/bin/env bash +# Generate a Virgil enum with N cases where M cases have per-case +# method overrides and the rest inherit the default. Measures the +# effect of RaClass elision on binary size (especially jar/wasm-gc). +# +# Usage: ./generate.bash N M > BigEnum.v3 +# N = total enum cases (default 1000) +# M = number of cases with per-case overrides (default 10) + +N=${1:-1000} +M=${2:-10} +if (( M > N )); then M=$N; fi + +cat <<'HDR' +// Generated benchmark: enum RaClass elision. +// Only M of N cases have per-case method overrides. + +HDR + +printf "enum Big(a: int, b: int, c: int, d: int) {\n" + +# Compute which cases get overrides (evenly spaced) +if (( M > 0 )); then + stride=$(( N / M )) + if (( stride < 1 )); then stride=1; fi +else + stride=$(( N + 1 )) +fi + +for (( i=0; i 0 )); then + if (( i % stride == 0 && i / stride < M )); then + has_override=true + fi + fi + + comma="," + if (( i == N-1 )); then comma=";"; fi + + if $has_override; then + local_val=$(( (i * 37 + 7) & 0xFFFF )) + printf "\tC%04d(%d, %d, %d, %d) { def val() -> int { return %d; } }%s\n" \ + $i $a $b $c $d $local_val "$comma" + else + printf "\tC%04d(%d, %d, %d, %d)%s\n" $i $a $b $c $d "$comma" + fi +done + +cat <<'MTH' + + def sum() -> int { return a + b + c + d; } + def val() -> int { return a; } +} + +MTH + +# Generate allCases() referencing all N cases +echo "def allCases() -> Array {" +printf "\treturn [" +for (( i=0; i 0 )); then printf ", "; fi + if (( i > 0 && i % 10 == 0 )); then printf "\n\t\t"; fi + printf "Big.C%04d" $i +done +echo "];" +echo "}" + +# ~500M total virtual calls +iters=$(( 500000000 / N )) +if (( iters < 1000 )); then iters=1000; fi + +cat <
) -> int { + if (args.length < 1) return 0; + var s = args[0]; + var r = 0; + for (i < s.length) { + var c = s[i]; + if (c >= '0' && c <= '9') r = r * 10 + (c - '0'); + } + return r; +} + +def run(arg: int) -> int { + var all = allCases(); + var n = all.length; + var s = 0; + match (arg) { + 0 => { + for (j < $iters) { + for (i < n) s += all[i].sum(); + } + } + 1 => { + for (j < $iters) { + for (i < n) s += all[i].val(); + } + } + } + return s & 0xFFFF; +} + +def main(args: Array) -> int { + var r = run(parseArg(args)); + return r - r; +} +MAIN diff --git a/bench/EnumOverrides/run-bench.bash b/bench/EnumOverrides/run-bench.bash new file mode 100644 index 000000000..9829e0e89 --- /dev/null +++ b/bench/EnumOverrides/run-bench.bash @@ -0,0 +1,130 @@ +#!/usr/bin/env bash +# Measure effect of RaClass elision on enums with varying override density. +# All N cases are referenced; only M have per-case method overrides. +# +# Usage: ./run-bench.bash [N] +# +# Env vars: +# AENEAS - compiler to test (default: bin/current/x86-linux/Aeneas) +# TARGETS - space-separated targets (default: "x86-64-linux") +# RUNS - runs per timing measurement (default: 3) +# N - total enum cases (default: 1000, overridden by $1) +# M_VALUES - space-separated M values (default: "0 1 10 50 100 N") + +set -euo pipefail + +DIR="$( cd -P "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +VIRGIL_LOC=${VIRGIL_LOC:=$(cd "$DIR/../.." && pwd)} + +N=${1:-${N:-1000}} +TARGETS=${TARGETS:-"x86-64-linux"} +RUNS=${RUNS:-3} +M_VALUES=${M_VALUES:-"0 1 10 50 100 $N"} +TMP=/tmp/$USER/virgil-bench/enum-overrides +mkdir -p "$TMP" + +# ---------- resolve compiler ---------- +AENEAS=${AENEAS:-$VIRGIL_LOC/bin/current/x86-64-linux/Aeneas} +if [ ! -x "$AENEAS" ]; then AENEAS=$VIRGIL_LOC/bin/current/x86-linux/Aeneas; fi + +# ---------- generate sources ---------- +for M in $M_VALUES; do + SRC="$TMP/BigEnum_${N}_m${M}.v3" + if [ ! -f "$SRC" ]; then + bash "$DIR/generate.bash" "$N" "$M" > "$SRC" + fi +done + +# ---------- helper: compile + measure ---------- +compile_and_measure() { + local label=$1 src=$2 target=$3 tag=$4 + local outdir="$TMP/${tag}_${target}" + local progname="big_${tag}" + mkdir -p "$outdir" + + local RT=$VIRGIL_LOC/rt + local rt_files compile_args binary + + local can_run=true runcmd="" + if [ "$target" = "jar" ]; then + binary="$outdir/$progname.jar" + rt_files="" + compile_args="-target=jar" + runcmd="java -jar $binary" + elif [ "$target" = "wasm-gc" ]; then + binary="$outdir/$progname.wasm" + rt_files="$(echo $RT/wasm-gc-wasi1/*.v3 $RT/wasm-wasi1-common/*.v3)" + compile_args="-target=wasm-gc" + can_run=false + elif [ "$target" = "wasm" ]; then + binary="$outdir/$progname.wasm" + rt_files="$(echo $RT/wasm-wasi1/*.v3 $RT/wasm-wasi1-common/*.v3 $RT/native/NativeFileStream.v3)" + compile_args="-target=wasm" + can_run=false + else + binary="$outdir/$progname" + rt_files="$(echo $RT/$target/*.v3 $RT/native/*.v3 $RT/gc/*.v3)" + compile_args="-heap-size=200m -target=$target" + runcmd="$binary" + fi + + # --- compile timing (best of RUNS) --- + local best_compile="" best_mem="" + for run in $(seq 1 $RUNS); do + rm -f "$binary" + local elapsed + elapsed=$( { /usr/bin/time -f "%e %M" \ + "$AENEAS" $compile_args -rt.files="$rt_files" \ + -output="$outdir" -program-name="$progname" "$src" ; } 2>&1 | tail -1 ) + local secs=$(echo "$elapsed" | awk '{print $1}') + local mem=$(echo "$elapsed" | awk '{print $2}') + if [ -z "$best_compile" ] || [ $(echo "$secs < $best_compile" | bc) = 1 ]; then + best_compile=$secs + best_mem=$mem + fi + done + + local binsize + binsize=$(stat -c%s "$binary" 2>/dev/null || stat -f%z "$binary") + + # --- runtime (best of RUNS), test 1 = val() with iterations --- + local best_run="" run_rss="" + if [ "$can_run" = "true" ]; then + for run in $(seq 1 $RUNS); do + local elapsed + elapsed=$( { /usr/bin/time -f "%e %M" $runcmd 1 ; } 2>&1 | tail -1 ) + local secs=$(echo "$elapsed" | awk '{print $1}') + local mem=$(echo "$elapsed" | awk '{print $2}') + if [ -z "$best_run" ] || [ $(echo "$secs < $best_run" | bc) = 1 ]; then + best_run=$secs + run_rss=$mem + fi + done + else + best_run="n/a" + run_rss="n/a" + fi + + printf " M=%-5s compile: %6ss %6sKB bin: %8sB run: %6ss %6sKB\n" \ + "$label" "$best_compile" "$best_mem" "$binsize" "$best_run" "$run_rss" +} + +# ---------- main ---------- +echo "========================================" +echo "Enum Override Density Benchmark" +echo " Total cases: $N M values: $M_VALUES" +echo " Runs: $RUNS (best of)" +echo " Compiler: $AENEAS" +echo "========================================" + +for target in $TARGETS; do + echo "" + echo "--- $target ---" + echo " M compile-time comp-RSS binary-size run-time run-RSS" + for M in $M_VALUES; do + compile_and_measure "$M" "$TMP/BigEnum_${N}_m${M}.v3" "$target" "m${M}" + done +done + +echo "" +echo "Done." From 8eccaf633c66be32d7e475b1183f3ce300571359 Mon Sep 17 00:00:00 2001 From: Eliot Moss Date: Wed, 22 Apr 2026 22:54:35 -0400 Subject: [PATCH 54/58] Add compact mtable optimization for enum virtual dispatch When -compact-mtable=N is set (0-100), enum mtables with few distinct implementation tuples relative to live cases are compacted using tag-to-slot indirection: slotMap[tag] -> compactTable[slot] -> func. Slots represent distinct tuples of method impls across all virtual methods for the enum, so the slotMap is shared per enum type. Co-Authored-By: Claude Opus 4.6 (1M context) --- aeneas/src/ir/Ir.v3 | 1 + aeneas/src/ir/Normalization.v3 | 90 ++++++++++++++++++++++++++++++++++ aeneas/src/ir/Reachability.v3 | 2 + aeneas/src/ir/SsaNormalizer.v3 | 7 +++ aeneas/src/main/CLOptions.v3 | 2 + aeneas/src/main/Compiler.v3 | 1 + 6 files changed, 103 insertions(+) diff --git a/aeneas/src/ir/Ir.v3 b/aeneas/src/ir/Ir.v3 index d32c9d377..58146ffed 100644 --- a/aeneas/src/ir/Ir.v3 +++ b/aeneas/src/ir/Ir.v3 @@ -156,6 +156,7 @@ class IrSelector extends IrMember { // indexed by class ID, where class IDs are assigned during Reachability analysis. class IrMtable(root: IrMethod, rootId: int, table: Array) extends IrItem { var record: Record; + var compactRecord: Record; def render(buf: StringBuilder) -> StringBuilder { buf.puts("["); diff --git a/aeneas/src/ir/Normalization.v3 b/aeneas/src/ir/Normalization.v3 index d1829b4db..b68ac3dc8 100644 --- a/aeneas/src/ir/Normalization.v3 +++ b/aeneas/src/ir/Normalization.v3 @@ -76,6 +76,7 @@ class ReachabilityNormalizer(config: NormalizerConfig, ra: ReachabilityAnalyzer) } ra.classes.apply(layoutVtable); Lists.apply(virtuals, layoutMtable); + if (ra.compiler.CompactMtableThreshold > 0) compactEnumMtables(); if (wrappers != null) wrappers.createVirtualWrappers(this); @@ -678,6 +679,95 @@ class ReachabilityNormalizer(config: NormalizerConfig, ra: ReachabilityAnalyzer) } } } + private def compactEnumMtables() { + var threshold = ra.compiler.CompactMtableThreshold; + // Group RaVirtuals by enum root receiver. + // Use a parallel arrays approach: collect (rc, list of RaVirtual) pairs. + var enumRoots = Vector.new(); + var enumVirts = Vector>.new(); + for (l = virtuals; l != null; l = l.tail) { + var rv = l.head; + if (rv.mtable == null || rv.mtable.record == null) continue; + var rc = ra.getClass(rv.raMethod.receiver); + if (rc == null || !V3.isEnum(rc.oldType)) continue; + // Find or create entry for this enum root + var found = -1; + for (i < enumRoots.length) { + if (enumRoots[i] == rc) { found = i; break; } + } + if (found < 0) { + found = enumRoots.length; + enumRoots.put(rc); + enumVirts.put(Vector.new()); + } + enumVirts[found].put(rv); + } + for (i < enumRoots.length) { + compactOneEnumMtable(enumRoots[i], enumVirts[i], threshold); + } + } + private def compactOneEnumMtable(rc: RaClass, rvs: Vector, threshold: int) { + var numTags = rc.maxClassId - rc.minClassId; + if (numTags <= 0) return; + var numMethods = rvs.length; + if (numMethods == 0) return; + // Count live tags + var numLive = 0; + for (tag < numTags) { + if (rc.liveCaseTags == null || rc.liveCaseTags[tag]) numLive++; + } + if (numLive == 0) return; + // Build per-tag implementation tuples and deduplicate. + // knownTuples[slot] = Array of length numMethods + var knownTuples = Vector>.new(); + var tagToSlot = Array.new(numTags); + for (tag < numTags) { + if (rc.liveCaseTags != null && !rc.liveCaseTags[tag]) continue; + var tuple = Array.new(numMethods); + for (m < numMethods) tuple[m] = rvs[m].mtable.table[tag]; + // Search for matching existing tuple + var slot = -1; + for (k < knownTuples.length) { + if (Arrays.equal(tuple, knownTuples[k])) { slot = k; break; } + } + if (slot < 0) { + slot = knownTuples.length; + knownTuples.put(tuple); + } + tagToSlot[tag] = slot; + } + var numSlots = knownTuples.length; + // Check threshold: compact only if slots/liveTags <= threshold% + if (numSlots * 100 > threshold * numLive) return; + // Build the shared slotMap record on the RaClass + var slotType = if(numSlots <= 256, Int.getType(false, 8), Int.getType(false, 16)); + var slotMapArrayType = V3Array.newType(slotType); + var slotMapRecord = ra.prog.newRecord(slotMapArrayType, numTags); + for (tag < numTags) { + slotMapRecord.values[tag] = Int.box(tagToSlot[tag]); + } + rc.compactSlotMap = slotMapRecord; + rc.compactSlotType = slotType; + // Build per-method compact records + for (m < numMethods) { + var rv = rvs[m]; + var origRecord = rv.mtable.record; + var compactArrayType = origRecord.rtype; + var compactRec = ra.prog.newRecord(compactArrayType, numSlots); + for (s < numSlots) { + var tuple = knownTuples[s]; + // Find a live tag with this slot to copy the FuncVal from + for (tag < numTags) { + if (rc.liveCaseTags != null && !rc.liveCaseTags[tag]) continue; + if (tagToSlot[tag] == s && rv.mtable.table[tag] == tuple[m]) { + compactRec.values[s] = origRecord.values[tag]; + break; + } + } + } + rv.mtable.compactRecord = compactRec; + } + } private def createEnumVariantNorms(rc: RaClass) { var tagType = V3.getVariantTagType(rc.oldType); var tagTypeNorm = norm(tagType); diff --git a/aeneas/src/ir/Reachability.v3 b/aeneas/src/ir/Reachability.v3 index 2fc88308a..65920dee1 100644 --- a/aeneas/src/ir/Reachability.v3 +++ b/aeneas/src/ir/Reachability.v3 @@ -886,6 +886,8 @@ class RaClass extends RaType { var liveEnumVirtuals: List; // enum roots: virtual methods needing dispatch var enumCaseRaClasses: Array; // enum roots: tag -> RaClass (cases with overrides) var caseDeclaringTypes: Array; // enum roots: tag -> declaring enum type + var compactSlotMap: Record; // enum roots: tag -> compact slot index + var compactSlotType: IntType; // u8 or u16 depending on numSlots var variantNorm: VariantNorm; // non-null for flattened variants var variantFields: Array; var origFieldTypes: Array; diff --git a/aeneas/src/ir/SsaNormalizer.v3 b/aeneas/src/ir/SsaNormalizer.v3 index e8a9b5507..2d7cb282d 100644 --- a/aeneas/src/ir/SsaNormalizer.v3 +++ b/aeneas/src/ir/SsaNormalizer.v3 @@ -1905,6 +1905,13 @@ class SsaRaNormalizer extends SsaRebuilder { } // Look up a function in an mtable record by tag. Returns the function SSA value. private def lookupMtable(sel: IrSelector, tag: SsaInstr, tagType: IntType) -> SsaInstr { + if (sel.mtable.compactRecord != null) { + var rc = norm.ra.getClass(sel.mtable.root.receiver); + var slotMap = newGraph.valConst(rc.compactSlotMap.rtype, rc.compactSlotMap); + var slot = curBlock.opArrayGetElem(rc.compactSlotMap.rtype, tagType, Facts.O_SAFE_BOUNDS, slotMap, tag); + var table = newGraph.valConst(sel.mtable.compactRecord.rtype, sel.mtable.compactRecord); + return curBlock.opArrayGetElem(sel.mtable.compactRecord.rtype, rc.compactSlotType, Facts.O_SAFE_BOUNDS, table, slot); + } var record = sel.mtable.record; var table = newGraph.valConst(record.rtype, record); return curBlock.opArrayGetElem(record.rtype, tagType, Facts.O_SAFE_BOUNDS, table, tag); diff --git a/aeneas/src/main/CLOptions.v3 b/aeneas/src/main/CLOptions.v3 index aba1eb5cf..c35c7da59 100644 --- a/aeneas/src/main/CLOptions.v3 +++ b/aeneas/src/main/CLOptions.v3 @@ -82,6 +82,8 @@ component CLOptions { "Optimize all non-recursive variants by unboxing."); def REDEF_FIELD = sharedOpt.newStringOption("redef-field", null, "Redefine one or more program fields, setting them to the given values."); + def COMPACT_MTABLE = sharedOpt.newIntOption("compact-mtable", 0, + "Compact enum mtables when slot percentage <= this value (0-100, 0=disabled)."); // Debugging options def PRINT_ID = debugOpt.newBoolOption("print-id", false, "Print additional internal IR ids."); diff --git a/aeneas/src/main/Compiler.v3 b/aeneas/src/main/Compiler.v3 index 17b8305f0..9e2b60325 100644 --- a/aeneas/src/main/Compiler.v3 +++ b/aeneas/src/main/Compiler.v3 @@ -92,6 +92,7 @@ class Compiler(target: Target) { var PartialSpecialization = flags.get("PartialSpecialization", false); var ChaDevirtualize = flags.get("ChaDevirtualize", level >= 1); var RaDevirtualize = flags.get("RaDevirtualize", level >= 1); + var CompactMtableThreshold = CLOptions.COMPACT_MTABLE.get(); var InlineEarly = flags.get("InlineEarly", level >= 3); var LoadOptimize = flags.get("LoadOptimize", level >= 1); var PostpassOptimize = flags.get("PostpassOptimize", level >= 1); From 57bee325b35a9b367efabbd708684ff03957d41d Mon Sep 17 00:00:00 2001 From: Eliot Moss Date: Mon, 27 Apr 2026 12:45:39 -0400 Subject: [PATCH 55/58] Skip M_ABSTRACT methods in JvmV3EnumGen.buildMethod Queue-based per-case liveness can leave a root enum method unreached when every live case has its own per-case override. addMethod marks such an unreached method M_ABSTRACT, but JvmV3EnumGen.buildMethod continued to compile it, which then crashed SsaJvmGen.context with "is abstract". Mirror the existing M_ABSTRACT guard in JvmV3ComponentGen.buildMethod so unused enum host-class methods are simply skipped. Reproduced on test/enums/enum_cmethod05.v3 and test/enums/enum_cmethod06.v3 on the jar target; both pass after the fix. Full 37-config CI matrix (v3i / x86-linux / x86-64-linux at -O0/-O1/-O2/-O3 with and without -wfts, -fp, and -unbox-variants / jar default + -O2/-O3/-uv / wasm default + -O2/-O3 with and without -wfts / wasm-gc default + -O2/-O3 across -wasm-gc-one-group and -wasm-gc-use-ref-test) is clean. Co-Authored-By: Claude Opus 4.7 (1M context) --- aeneas/src/jvm/JvmGen.v3 | 2 +- claude/todos-extended-enums.md | 10 +++++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/aeneas/src/jvm/JvmGen.v3 b/aeneas/src/jvm/JvmGen.v3 index 7ae05fa1d..96b85e551 100644 --- a/aeneas/src/jvm/JvmGen.v3 +++ b/aeneas/src/jvm/JvmGen.v3 @@ -499,7 +499,7 @@ class JvmV3EnumGen extends JvmClassGen { addEmptyInit(); } def buildMethod(m: IrMethod) { - if (m == null) return; + if (m == null || m.flags.M_ABSTRACT) return; var name = builder.jprog.mangleEnumMethod(m); var jsig = jvmSig(m.getMethodType()); genCode(builder, m, name, jsig, true); diff --git a/claude/todos-extended-enums.md b/claude/todos-extended-enums.md index 54256b923..da2dc6905 100644 --- a/claude/todos-extended-enums.md +++ b/claude/todos-extended-enums.md @@ -26,13 +26,18 @@ in these stages: Example: enum E1 { A, B, _; def m1() => 0; } Note that the enum's methods are separated from its cases by a ';'. Subtype enums can override inherited methods. - Per-case method overrides (case { def ... }) were removed in Strategy A Step 1. + Per-case method overrides (case { def ... }) were re-introduced in Strategy B + via synthetic per-case RaClasses (branch open_enums3b) — only cases with + their own `{ def ... }` body get a synthetic class, the rest share the + parent. Strategy A (open_enums3a) keeps the type/subtype-only model. Todos: - [x] Extend the syntax (sub-stage 3.1: enum-level methods after ';') - [x] Extend the semantic checking (sub-stage 3.1: basic method resolution) - [x] Subtype method inheritance with overrides (sub-stage 3.3) + - [x] Per-case method overrides (Strategy B, branch open_enums3b) - [x] Method closures (sub-stage 3.4: `var f = e.m; f()`) - [x] Wasm/wasm-gc backend support (indirect adapters, sig handling, no Oop for enums) + - [x] JVM backend support (closure adapter, M_ABSTRACT guard in JvmV3EnumGen) - Implementation note: dispatch uses tag-indexed array of function values (no boxed enum objects needed). The tag type is prepended to the method's normalized function type. CallFunctionDirect (no Oop prepend) is used instead of CallFunction for enum dispatch. @@ -40,6 +45,9 @@ in these stages: between variants and enums. Enum values are integers (not Records), so the interpreter, optimizer constant-fold, and optimizer CallClosure handler all need EnumType guards. The optimizer must also set O_NO_NULL_CHECK for enum receivers (tag 0 is valid). + - Strategy B optimizations: per-case RaClass elision (only cases with overrides + get a synthetic class), queue-based per-case liveness, and the optional + `-compact-mtable=N` (off by default) for compacting redundant mtable rows. 4. Allow subtypes to redeclare supertype fields [DONE] Given supertype enum E3(x: int) { A(1), B(17), _ }, example subtype declarations: From 33bd1682e07ed70fd225a97f9ad88a8c897c6a3d Mon Sep 17 00:00:00 2001 From: Eliot Moss Date: Mon, 27 Apr 2026 13:09:46 -0400 Subject: [PATCH 56/58] Wire up EnumOverrides/EnumLiveness benchmarks for wasm and wasm-gc The bench scripts had can_run=false for wasm and wasm-gc, so those targets compiled but never executed. Added a small node-based runner (bench/run-wasm-entry.mjs) that handles the two reasons execution was being skipped: 1. Virgil's wasm/wasm-gc targets export `entry`, not `_start`, so wasi.start() doesn't apply. The runner uses wasi.initialize() (reactor mode) + a direct entry() call. 2. proc_exit() under a reactor-initialized WASI throws either a Symbol (with returnOnExit:true) or RuntimeError("unreachable") after the post-proc_exit unreachable; both indicate the program terminated normally and are caught. Other adjustments: - Wasm/wasm-gc compile lines now pass -heap-size=200m (default 0 leaves no heap, so the first allocation traps with unreachable). - Both scripts invoke node via $VIRGIL_LOC/test/config/node (the symlink to the test infrastructure's node v22) instead of bare `node`. The system node v18 doesn't support the wasm-gc binary format, so a clean-env run was failing with `expected signature definition 0x60, got 0x5e`. Co-Authored-By: Claude Opus 4.7 (1M context) --- bench/EnumLiveness/run-bench.bash | 8 ++--- bench/EnumOverrides/run-bench.bash | 8 ++--- bench/run-wasm-entry.mjs | 49 ++++++++++++++++++++++++++++++ 3 files changed, 57 insertions(+), 8 deletions(-) create mode 100644 bench/run-wasm-entry.mjs diff --git a/bench/EnumLiveness/run-bench.bash b/bench/EnumLiveness/run-bench.bash index 52cb492df..3deb962cb 100644 --- a/bench/EnumLiveness/run-bench.bash +++ b/bench/EnumLiveness/run-bench.bash @@ -54,13 +54,13 @@ compile_and_measure() { elif [ "$target" = "wasm-gc" ]; then binary="$outdir/$progname.wasm" rt_files="$(echo $RT/wasm-gc-wasi1/*.v3 $RT/wasm-wasi1-common/*.v3)" - compile_args="-target=wasm-gc" - can_run=false + compile_args="-heap-size=200m -target=wasm-gc" + runcmd="$VIRGIL_LOC/test/config/node --no-warnings --experimental-wasi-unstable-preview1 $VIRGIL_LOC/bench/run-wasm-entry.mjs $binary" elif [ "$target" = "wasm" ]; then binary="$outdir/$progname.wasm" rt_files="$(echo $RT/wasm-wasi1/*.v3 $RT/wasm-wasi1-common/*.v3 $RT/native/NativeFileStream.v3)" - compile_args="-target=wasm" - can_run=false + compile_args="-heap-size=200m -target=wasm" + runcmd="$VIRGIL_LOC/test/config/node --no-warnings --experimental-wasi-unstable-preview1 $VIRGIL_LOC/bench/run-wasm-entry.mjs $binary" else binary="$outdir/$progname" rt_files="$(echo $RT/$target/*.v3 $RT/native/*.v3 $RT/gc/*.v3)" diff --git a/bench/EnumOverrides/run-bench.bash b/bench/EnumOverrides/run-bench.bash index 9829e0e89..c81a96a46 100644 --- a/bench/EnumOverrides/run-bench.bash +++ b/bench/EnumOverrides/run-bench.bash @@ -54,13 +54,13 @@ compile_and_measure() { elif [ "$target" = "wasm-gc" ]; then binary="$outdir/$progname.wasm" rt_files="$(echo $RT/wasm-gc-wasi1/*.v3 $RT/wasm-wasi1-common/*.v3)" - compile_args="-target=wasm-gc" - can_run=false + compile_args="-heap-size=200m -target=wasm-gc" + runcmd="$VIRGIL_LOC/test/config/node --no-warnings --experimental-wasi-unstable-preview1 $VIRGIL_LOC/bench/run-wasm-entry.mjs $binary" elif [ "$target" = "wasm" ]; then binary="$outdir/$progname.wasm" rt_files="$(echo $RT/wasm-wasi1/*.v3 $RT/wasm-wasi1-common/*.v3 $RT/native/NativeFileStream.v3)" - compile_args="-target=wasm" - can_run=false + compile_args="-heap-size=200m -target=wasm" + runcmd="$VIRGIL_LOC/test/config/node --no-warnings --experimental-wasi-unstable-preview1 $VIRGIL_LOC/bench/run-wasm-entry.mjs $binary" else binary="$outdir/$progname" rt_files="$(echo $RT/$target/*.v3 $RT/native/*.v3 $RT/gc/*.v3)" diff --git a/bench/run-wasm-entry.mjs b/bench/run-wasm-entry.mjs new file mode 100644 index 000000000..7988485c1 --- /dev/null +++ b/bench/run-wasm-entry.mjs @@ -0,0 +1,49 @@ +// Run a Virgil-compiled .wasm binary that exports `entry()` (rather than +// `_start`). Used by the enum benchmarks: Virgil's wasm/wasm-gc targets +// export `entry`, not `_start`, so we set up WASI as a "reactor" +// (initialize + manually call entry()) instead of using wasi.start(). +// +// Quirk: when the program calls proc_exit() under a reactor-initialized +// WASI on node, the WASI implementation throws (a Symbol with +// returnOnExit:true, or RuntimeError("unreachable") otherwise) rather +// than terminating cleanly. Either flavor signals a normal exit; we +// catch both. +// +// Usage: node --no-warnings --experimental-wasi-unstable-preview1 \ +// run-wasm-entry.mjs [program-args...] + +import { readFileSync } from 'node:fs'; +import { WASI } from 'wasi'; +import { argv, env, exit } from 'node:process'; + +const wasm_path = argv[2]; +// WASI's `args` is the program's argv: index 0 is the program name and is +// skipped by Virgil's RiRuntime, so the user-visible args[] starts at 1. +const program_args = ['program', ...argv.slice(3)]; + +const wasi = new WASI({ + returnOnExit: true, + version: 'preview1', + args: program_args, + env, + preopens: { '.': '.' }, +}); +const importObject = { wasi_snapshot_preview1: wasi.wasiImport }; + +const bytes = readFileSync(wasm_path); +const module_ = new WebAssembly.Instance(new WebAssembly.Module(bytes), importObject); + +wasi.initialize(module_); +try { + module_.exports.entry(); + exit(0); +} catch (e) { + // Both proc_exit's Symbol throw (with returnOnExit:true) and the + // RuntimeError("unreachable") trap from the post-proc_exit unreachable + // mean the program terminated normally. Anything else is a real error. + if (typeof e === 'symbol') exit(0); + if (e instanceof WebAssembly.RuntimeError && /unreachable/.test(e.message)) { + exit(0); + } + throw e; +} From a1eb815be049c8f8437f2214882d34d6a467ffac Mon Sep 17 00:00:00 2001 From: Eliot Moss Date: Mon, 27 Apr 2026 13:13:07 -0400 Subject: [PATCH 57/58] Simplify wasm bench runner: drop fragile Symbol/unreachable catches The previous runner caught a Symbol throw and a RuntimeError("unreachable") as proc_exit signals. Both were sketchy: the Symbol catch depended on undocumented node WASI internals, and the unreachable regex would silently swallow a real wasm trap from a buggy program (reporting it as a zero-time success). Switching to returnOnExit:false makes proc_exit call process.exit(code) directly. The program's exit code propagates and any real wasm trap stays uncaught, so a crashed program is visible as a node failure rather than a silent zero exit. Co-Authored-By: Claude Opus 4.7 (1M context) --- bench/run-wasm-entry.mjs | 30 ++++++++---------------------- 1 file changed, 8 insertions(+), 22 deletions(-) diff --git a/bench/run-wasm-entry.mjs b/bench/run-wasm-entry.mjs index 7988485c1..9aba3e7d9 100644 --- a/bench/run-wasm-entry.mjs +++ b/bench/run-wasm-entry.mjs @@ -3,18 +3,16 @@ // export `entry`, not `_start`, so we set up WASI as a "reactor" // (initialize + manually call entry()) instead of using wasi.start(). // -// Quirk: when the program calls proc_exit() under a reactor-initialized -// WASI on node, the WASI implementation throws (a Symbol with -// returnOnExit:true, or RuntimeError("unreachable") otherwise) rather -// than terminating cleanly. Either flavor signals a normal exit; we -// catch both. +// returnOnExit:false makes proc_exit call process.exit(code) directly, +// so the program's exit code propagates and a real wasm trap stays +// uncaught (visible as a node failure rather than a silent zero exit). // // Usage: node --no-warnings --experimental-wasi-unstable-preview1 \ // run-wasm-entry.mjs [program-args...] import { readFileSync } from 'node:fs'; import { WASI } from 'wasi'; -import { argv, env, exit } from 'node:process'; +import { argv, env } from 'node:process'; const wasm_path = argv[2]; // WASI's `args` is the program's argv: index 0 is the program name and is @@ -22,7 +20,7 @@ const wasm_path = argv[2]; const program_args = ['program', ...argv.slice(3)]; const wasi = new WASI({ - returnOnExit: true, + returnOnExit: false, version: 'preview1', args: program_args, env, @@ -31,19 +29,7 @@ const wasi = new WASI({ const importObject = { wasi_snapshot_preview1: wasi.wasiImport }; const bytes = readFileSync(wasm_path); -const module_ = new WebAssembly.Instance(new WebAssembly.Module(bytes), importObject); +const instance = new WebAssembly.Instance(new WebAssembly.Module(bytes), importObject); -wasi.initialize(module_); -try { - module_.exports.entry(); - exit(0); -} catch (e) { - // Both proc_exit's Symbol throw (with returnOnExit:true) and the - // RuntimeError("unreachable") trap from the post-proc_exit unreachable - // mean the program terminated normally. Anything else is a real error. - if (typeof e === 'symbol') exit(0); - if (e instanceof WebAssembly.RuntimeError && /unreachable/.test(e.message)) { - exit(0); - } - throw e; -} +wasi.initialize(instance); +instance.exports.entry(); From f9980be3ec9eae1fea9fc4327de4e220ec2894d2 Mon Sep 17 00:00:00 2001 From: Eliot Moss Date: Mon, 27 Apr 2026 23:41:02 -0400 Subject: [PATCH 58/58] Fix lookupEnumExprMember to pass receiver (not expr.expr) for VstField Master's ff74b3ab7 (Minor simplifications to MethodEnv) refactored the enum field lookup at lookupEnumExprMember's VstField case to use the new newApplyCompBinding helper, but accidentally changed the receiver arg from `receiver` (computed via objExpr to handle useThis) to `expr.expr` (the raw expression). For an enum field access inside the enum's own method body (useThis=true), expr.expr is the wrong receiver expression, producing a malformed SSA that crashes the optimizer's boundscheck reduction with NullCheckException. Restore receiver as the helper's second argument. Concretely fixes test/enums/enum_closure05.v3 on v3i, which closes over a method that reads an enum field. Co-Authored-By: Claude Opus 4.7 (1M context) --- aeneas/src/vst/MethodEnv.v3 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aeneas/src/vst/MethodEnv.v3 b/aeneas/src/vst/MethodEnv.v3 index 230aadc64..c509c0c77 100644 --- a/aeneas/src/vst/MethodEnv.v3 +++ b/aeneas/src/vst/MethodEnv.v3 @@ -406,7 +406,7 @@ class MethodEnv { var receiver = objExpr(useThis, expr.ident.name, expr); match (member.member) { x: VstField => { - return newApplyCompBinding(expr, expr.expr, enumType.enumGetParamOperator(x)); + return newApplyCompBinding(expr, receiver, enumType.enumGetParamOperator(x)); } x: VstTagMember => { var tagType = enumType.classDecl.tagType;