diff --git a/aeneas/src/core/Eval.v3 b/aeneas/src/core/Eval.v3 index 34d38fc54..5c287f2ed 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 (V3.isEnum(tt)) { + // enum subtype query: check integer tag is in [tagLo, tagHi] + var decl = V3.getDecl(tt); + var tagVal = Int.unbox(val); + return tagVal >= decl.tagLo && tagVal <= decl.tagHi; + } 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 (V3.isEnum(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 (V3.isEnum(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 (V3.isEnum(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); @@ -831,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 (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 => { @@ -868,6 +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 || 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"); @@ -914,6 +937,11 @@ def evalOp(op: Operator, args: Arguments) -> Result { return args.tailCall(spec, object, 1, args.vals.length); } CallVariantVirtual(method) => { + var ta = args.getTypeArgs(); + if (V3.isEnum(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 +949,11 @@ def evalOp(op: Operator, args: Arguments) -> Result { return args.tailCall(spec, object, 1, args.vals.length); } CallVariantSelector(selector) => { + var ta = args.getTypeArgs(); + if (V3.isEnum(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); @@ -959,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); @@ -977,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]); } //---------------------------------------------------------------------------- @@ -1047,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, ClassType.!(fieldType).byteSize)); match (fieldType) { x: BoolType => { return Bool.box((v & 1) != 0); @@ -1064,18 +1097,20 @@ def doRefLayoutGetField(args: Arguments, fieldType: PrimType, ref: ByteArrayOffs I64, U64 => return Long.box(long.view(v)); } } - x: EnumType => { - if (v >= x.enumDecl.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: 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) { @@ -1085,7 +1120,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, ClassType.!(fieldType).byteSize); + ref.write(order, offset, size, bits); return Values.BOTTOM; } @@ -1147,6 +1183,47 @@ 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 enumType = ClassType.!(ta[0]); + var tag = Int.unbox(args.vals[0]); + var targetDecl = findEnumDeclForTag(enumType.classDecl, tag); + var ir = args.getProgram().ir; + var m: IrMethod; + // 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); +} +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); 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/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/debug/Dwarf.v3 b/aeneas/src/debug/Dwarf.v3 index 4d923b52e..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,20 +352,20 @@ class DwarfInfoSection(abbrev: DwarfAbbrevSection) { _ => ; } } - def emitEnumType(t: EnumType) { + def emitEnumType(t: ClassType) { w.put_uleb32(abbrev.getAbbrev(DwarfAbbrevTag.EnumurationType)); buf.reset(); t.render(buf); 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); 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/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/Ir.v3 b/aeneas/src/ir/Ir.v3 index 196bc81af..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("["); @@ -353,8 +354,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; @@ -388,14 +391,16 @@ class IrModule { if (ctype == null) return null; var ic = classMap[ctype]; if (ic == null) { - if (V3.isClass(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; - } else if (EnumType.?(ctype)) { - ic = newIrClass(ctype, null, EnumType.!(ctype).enumDecl); - classMap[ctype] = ic; } } return ic; diff --git a/aeneas/src/ir/Normalization.v3 b/aeneas/src/ir/Normalization.v3 index 307223fc4..b68ac3dc8 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; @@ -75,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); @@ -110,7 +112,7 @@ 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); @@ -130,7 +132,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.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); @@ -156,8 +158,12 @@ 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); + if (V3.isEnum(rc.oldType) && !config.NonRefClosureReceiver + && m.source != null && VstMethod.?(m.source)) { + ftype = Function.prependParamType(Oop.TYPE, ftype); + } else { + ftype = Function.prependParamTypes(rc.variantNorm.sub, ftype); + } } rm.funcNorm = FuncNorm.!(norm(ftype)); var typeParams = if(rm.spec != null, rm.spec.getTypes().methodTypeArgs); @@ -272,7 +278,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 = [ @@ -311,10 +325,17 @@ class ReachabilityNormalizer(config: NormalizerConfig, ra: ReachabilityAnalyzer) def numberVariant(rc: RaClass) { rc.minClassId = liveClasses.length; if (rc.children == null) { - liveClasses.put(rc); // special case of a data type + 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. + 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; + } rc.maxClassId = liveClasses.length; } // Recursively assign class IDs to a variant case/subtype using DFS tag ranges. @@ -325,8 +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 decl = ClassType.!(c.oldType).classDecl; - var tagLo = decl.variantTag, tagHi = decl.variantTagHi; + var tagLo = V3.getTagLo(c.oldType), tagHi = V3.getTagHi(c.oldType); c.minClassId = rootMin + tagLo; c.maxClassId = rootMin + tagHi + 1; liveClasses.grow(c.maxClassId); @@ -337,7 +357,7 @@ 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 (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); @@ -589,6 +609,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; @@ -599,30 +620,171 @@ class ReachabilityNormalizer(config: NormalizerConfig, ra: ReachabilityAnalyzer) rv.mtable = mtable; if (rc.isUnboxed()) { - var ft = Function.funcRefType(rm.norm.getMethodType()); + var methType = rm.norm.getMethodType(); + 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) { + 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) { - 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. + for (l = rc.subtypes; l != null; l = l.tail) { 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); - } + 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 } + 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); + var spec = IrSpec.new(ta[0], ta, impl.norm); + 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 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 (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) continue; + if (rootRc.liveCaseTags != null && !rootRc.liveCaseTags[tag]) continue; + setMtableEntry(mtable, impl, tag); + } + } + } + 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); + 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); + } + } def setMtable(rc: RaClass, rv: RaVirtual) { var rm = rc.findRaMethod(rv.raMethod); if (rm != null && rm.virtual != null) rm.virtual.mtable = rv.mtable; @@ -632,7 +794,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; } @@ -668,7 +830,11 @@ 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 (V3.isEnum(x.memberRef.receiver) && config.BoxEnumClosureReceiver) { + 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/ir/Packing.v3 b/aeneas/src/ir/Packing.v3 index 6fab11bce..dbf3f9c6b 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,7 +36,10 @@ class SsaPacker(builder: SsaBuilder, result: IntType) { if (x.width > 0) scalars.put(x, pos, input); pos += x.width; } - x: ClassType => { + x: ClassType => if (x.classDecl.isEnum()) { + if (x.width > 0) scalars.put(x, pos, input); + pos += x.width; + } 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 @@ -60,13 +63,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; + x: ClassType => if (x.classDecl.isEnum()) { + ft = x.classDecl.tagType; } x: EnumSetType => { ft = x.repType; @@ -139,40 +142,16 @@ 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)); } pos += x.width; } - x: EnumType => { - def tagType: IntType = x.enumDecl.tagType; - def unpackedTag: SsaInstr = builder.opIntViewI0(ft, tagType, shiftedInput); - def numCases = x.enumDecl.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 unpackedRep = builder.opIntViewI0(ft, repType, shiftedInput); def enumSetInstr = SsaApplyOp.new(builder.source, V3Op.newTypeSubsume(repType, x), [unpackedRep]); builder.append(enumSetInstr); @@ -188,8 +167,27 @@ class SsaUnpacker(builder: SsaBuilder, ft: IntType) { } def tupleCreate = builder.opTupleCreate(x, unpackedElems); unpackedBuffer.push(tupleCreate); - } - x: ClassType => { + } + 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)]); + def trueBlock = SsaBlock.new(); + def subsumeOrigTag = SsaApplyOp.new(builder.source, V3Op.newTypeSubsume(tagType, x), [unpackedTag]); + trueBlock.append(subsumeOrigTag); + 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); + 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; + } 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--) { @@ -230,7 +228,9 @@ component PackingUtils { } return (width, null); } - x: ClassType => { + x: ClassType => if (x.classDecl.isEnum()) { + return (x.width, null); + } 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/Reachability.v3 b/aeneas/src/ir/Reachability.v3 index d244eb281..65920dee1 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,6 +539,51 @@ class ReachabilityAnalyzer(compilation: Compilation) { for (l = rc.subtypes; l != null; l = l.tail) { analyzeVirtual(l.head, 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; @@ -671,7 +725,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 @@ -731,6 +785,20 @@ class ReachabilityAnalyzer(compilation: Compilation) { if (ic == null) ic = IrClass.new(t, null, null, [], []); raType = newRaClass(t, ic, parent); } + ENUM => { + 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; + typeMap[t] = raType; + addEnumSubtypesRecursive(rc, decl); + } _ => { for (l = t.nested; l != null; l = l.tail) makeType(l.head); raType = RaType.new(t); @@ -744,6 +812,38 @@ class ReachabilityAnalyzer(compilation: Compilation) { classes.put(rc); return rc; } + private def addEnumSubtypesRecursive(rc: RaClass, decl: VstClass) { + 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); + } + } + } + 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; + rootRc.enumCaseRaClasses[c.enumTag] = caseRc; + } + } def mono(t: Type, spec: IrSpec) -> Type { return if(spec != null, spec.instantiateType(t), t); } @@ -782,6 +882,12 @@ 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 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; @@ -790,7 +896,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 } diff --git a/aeneas/src/ir/SsaNormalizer.v3 b/aeneas/src/ir/SsaNormalizer.v3 index c0f717dec..2d7cb282d 100644 --- a/aeneas/src/ir/SsaNormalizer.v3 +++ b/aeneas/src/ir/SsaNormalizer.v3 @@ -54,10 +54,18 @@ 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 + 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++; + } } } if (tn.size == 1) { @@ -103,6 +111,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 + && 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); + 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]; @@ -217,13 +234,39 @@ class SsaRaNormalizer extends SsaRebuilder { VariantGetField(field) => normGetField(true, i_old, field, op); VariantGetMethod(method) => { var obj = genRef1(args[0]); - mapN(i_old, [funcRef(extractMethodRef(orig, method).1), obj]); + var rc = norm.ra.getClass(op.typeArgs[0]); + obj = boxEnumClosureReceiver(rc, obj); + var spec = extractMethodRef(orig, method).1; + var fref: SsaInstr; + if (V3.isEnum(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]); 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 (V3.isEnum(rc.oldType)) { + var sel = IrSelector.!(t.1.member); + if (sel.mtable != null && sel.mtable.record != null) { + var tag = obj; + var func = lookupMtable(sel, tag, V3.getVariantTagType(rc.oldType)); + obj = boxEnumClosureReceiver(rc, obj); + mapN(i_old, [func, obj]); + } else { + mapN(i_old, [funcRef(t.1), obj]); + } + } else { + mapN(i_old, [curBlock.opGetSelector(t.1, obj), obj]); + } } else { + obj = boxEnumClosureReceiver(rc, obj); mapN(i_old, [funcRef(t.1), obj]); } } @@ -248,9 +291,15 @@ 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 + if (rc.isUnboxed()) { + i_old.facts |= Fact.O_NO_NULL_CHECK; + 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); } } @@ -288,25 +337,44 @@ class SsaRaNormalizer extends SsaRebuilder { CallVariantVirtual(method) => { // devirtualize methods that are not overridden var rc = norm.ra.getClass(op.typeArgs[0]); + 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 (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); - ai_new = Arrays.concat([func, newGraph.nullReceiver()], ai_new); - normCall(i_old, funcNorm, V3Op.newCallFunction(funcNorm.sub[0]), ai_new); + var sel = IrSelector.!(m.member); + if (sel.mtable != null && sel.mtable.record != null) { + 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 { + 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 { 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 (rc.isUnboxed() && !V3.isEnum(rc.oldType)) ai_new = Arrays.prepend(newGraph.nullReceiver(), ai_new); normCall(i_old, funcNorm, V3Op.newCallMethod(m), ai_new); } } @@ -393,19 +461,18 @@ 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); + 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); - x: EnumType => { - var it = IntType.!(x.enumDecl.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 inBound = curBlock.opIntULt(norm.config.ArrayLengthType, it, result, caseCount); - result = curBlock.opIntViewI0(it, wt, result); - result = curBlock.addSelect(it, inBound, result, newGraph.nullConst(it)); - } _ => context.fail1("unexpected type %q", fn.oldType.render); } return map1(i_old, result); @@ -417,15 +484,14 @@ 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); + 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); - x: EnumType => { - var it = IntType.!(x.enumDecl.tagType); - var wt = Int.getType(false, it.byteSize * 8); - result = curBlock.opByteArraySetField(wt, rangeStartType, offset, order, facts, array, start, val); - } } return map1(i_old, result); } @@ -444,16 +510,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.enumDecl.tagType); - result = curBlock.opByteArrayGetField(it, rangeStartType, offset, order, facts, array, start); - var caseCount = newGraph.intConst(x.enumDecl.cases.length); - var inBound = curBlock.opIntULt(it, it, result, caseCount); - result = curBlock.addSelect(it, inBound, result, newGraph.nullConst(it)); - } _ => ; } return map1(i_old, result); @@ -473,13 +538,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.enumDecl.tagType); - result = curBlock.opByteArraySetField(it, rangeStartType, offset, order, facts, array, start, val); - } } return map1(i_old, result); } @@ -1068,26 +1132,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); - } - // break + var check = normVariantQuery(atn, rtn, ai_old, offset); + if (check != null) return opAnd(left, check); } _ => ; // break } @@ -1511,7 +1557,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.isUnboxed() && !V3.isEnum(rc.oldType)) { // field of unboxed data type var vals = genVariantGetField(rc, raField, rc.variantNorm, ai_new); return mapNnf(i_old, vals); @@ -1583,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.isUnboxed()) { + } 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) { @@ -1693,6 +1739,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)); @@ -1703,10 +1753,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.enumDecl.tagType, scalar); - return scalar; - } x: EnumSetType => { scalar = curBlock.opIntView(ift, x.repType, scalar); return scalar; @@ -1743,6 +1789,8 @@ class SsaRaNormalizer extends SsaRebuilder { return vals; } def normNullCheck(oldApp: SsaApplyOp, op: Operator) { + 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]); } @@ -1832,6 +1880,51 @@ 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 { + 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 { + 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); + } + // 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) + && (!context.compiler.NormConfig.NonRefClosureReceiver + || context.compiler.NormConfig.ExplicitRefTypeCast)) { + 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/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 81793eb74..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 { - fields.puta(parent.fields); + if (!V3.isEnum(ctype)) fields.puta(parent.fields); methods.puta(parent.methods); methods[0] = null; // overwrite super constructor } @@ -110,12 +110,18 @@ 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/enum-subtype 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 (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; } def addDescriptorField(cdecl: VstClass) { diff --git a/aeneas/src/jvm/JvmGen.v3 b/aeneas/src/jvm/JvmGen.v3 index 5f5fca38c..96b85e551 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(V3.isEnum(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 || m.flags.M_ABSTRACT) 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) || V3.isEnum(memberRef.receiver)) buildComponentClosure(invoke_meth); else buildClassClosure(invoke_meth); } } @@ -623,10 +648,13 @@ 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)); - invoke_code.invokestatic(jclass.name, V3.mangleIrMember(memberRef.member), jvmSig(methType)); + builder.emitLoadArguments(invoke_code, methType, methType, 2); + 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)); invoke_code.ret(jvmKind(Function.getReturnType(funcType))); invoke_code.attach(invoke_meth); } @@ -689,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/JvmHeap.v3 b/aeneas/src/jvm/JvmHeap.v3 index 9ddde2a0d..844b24376 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); @@ -26,8 +27,18 @@ 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 (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)); + code.invokestatic(JvmTypes.java_lang_Integer.name, "valueOf", JvmTypes.SIG_INT_INTEGER); + } + } ANYFUNC, FUNCREF => emitFunctionValue(code, etype, FuncVal.!(val)); VOID, @@ -51,6 +62,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/jvm/JvmRep.v3 b/aeneas/src/jvm/JvmRep.v3 index d7f1b84c1..7f95be796 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); @@ -165,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).enumDecl.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/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/jvm/SsaJvmGen.v3 b/aeneas/src/jvm/SsaJvmGen.v3 index 93ce61be0..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).enumDecl.tagType; + ENUM => return ClassType.!(t).classDecl.tagType; ENUM_SET => return EnumSetType.!(t).repType; _ => context.fail("expected integral type"); } @@ -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; } } @@ -364,6 +366,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; @@ -385,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); @@ -445,7 +454,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)); @@ -665,9 +679,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) || V3.isEnum(methodRef.receiver)) { var nsig = jprog.jvmSig(methodRef.getMethodType()); - code.invokestatic(jclass.name, V3.mangleIrMember(methodRef.member), nsig); + var mname = if(V3.isEnum(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); } @@ -1161,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; + } } 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/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); diff --git a/aeneas/src/ssa/SsaBuilder.v3 b/aeneas/src/ssa/SsaBuilder.v3 index b93663c31..d890e3910 100644 --- a/aeneas/src/ssa/SsaBuilder.v3 +++ b/aeneas/src/ssa/SsaBuilder.v3 @@ -98,14 +98,21 @@ class SsaBuilder { } VariantGetName => { var t = op.typeArgs[0]; - var record: Record; - if (EnumType.?(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(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 = 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); + + } EnumGetParam(field) => { var t = field.receiver.declType; var facts = Facts.O_SAFE_BOUNDS; @@ -122,7 +129,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); @@ -593,6 +600,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); } @@ -614,7 +623,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) 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); } diff --git a/aeneas/src/ssa/SsaOptimizer.v3 b/aeneas/src/ssa/SsaOptimizer.v3 index e78925d50..007357af8 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; @@ -684,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) { @@ -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(V3.isEnum(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(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) @@ -944,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(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); } @@ -956,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(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); } @@ -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 (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(); } 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 (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(); 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 (V3.isEnum(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 (V3.isEnum(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) || V3.isEnum(spec.receiver)) newFacts |= Fact.O_NO_NULL_CHECK; else newFacts |= x.facts & Fact.O_NO_NULL_CHECK; } _ => { @@ -1099,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 (!V3.isEnum(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 => { @@ -1191,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. @@ -1421,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; } @@ -1448,6 +1473,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 (V3.isEnum(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..52a22028f 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,25 +142,62 @@ class VstSsaGen extends VstVisitor { } } } - def addEnumParamInitCode(receiver: VstEnum, env: VstSsaEnv) { + def addEnumParamInitCode(receiver: VstClass, 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 = ClassType.!(tc.create0()).classDecl; + } + // 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]; @@ -252,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, 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); @@ -301,14 +340,14 @@ 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); 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 @@ -325,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 @@ -513,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)); @@ -568,14 +607,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 +631,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.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 = V3.getDecl(pat.subtypeEnumType); + var tagVal = Int.unbox(val); + 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; } else if (Values.equal(val, pat.val)) { return true; } @@ -693,7 +739,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..caab8dd7b 100644 --- a/aeneas/src/v3/TypeSystem.v3 +++ b/aeneas/src/v3/TypeSystem.v3 @@ -33,8 +33,10 @@ 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; + if (V3.isEnum(xt) && EnumSetType.?(yt)) { + var xd = ClassType.!(xt).classDecl; + var yd = EnumSetType.!(yt).enumDecl; + return xd == yd || isEnumSubtypeOf(xd, yd); } return false; } @@ -64,10 +66,15 @@ component TypeSystem { } } 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 + } } } } @@ -114,9 +121,6 @@ component TypeSystem { } } } - x: EnumType => match (yt) { - y: EnumSetType => if (x.enumDecl == y.enumDecl) return Conversion.PROMOTE_ENUM_TO_SET; - } x: FloatType => match (yt) { y: FloatType => if (x.width <= y.width) return Conversion.PROMOTE_FLOAT_TO_FLOAT; } @@ -135,6 +139,11 @@ component TypeSystem { match (xt) { 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; @@ -298,11 +307,8 @@ component TypeSystem { if (isPromotableToFloat(x, y)) return unifyPromotable(dir, x, y); } } - x: EnumType => match (t2) { - y: EnumSetType => if(x.enumDecl == y.enumDecl) return unifyPromotable(dir, x, y); - } x: EnumSetType => match (t2) { - y: EnumType => if(x.enumDecl == y.enumDecl) 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 => { @@ -320,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) { @@ -779,3 +786,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: VstClass, sup: VstClass) -> bool { + if (sub.qualifiers == null) return false; // root enums are not subtypes + return enumAncestorHasSubtype(sup, sub); +} +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; + } + return false; +} diff --git a/aeneas/src/v3/V3.v3 b/aeneas/src/v3/V3.v3 index 805d4645e..0102f85c9 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); } @@ -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; @@ -96,11 +99,33 @@ component V3 { return if(tag >= 0, tag); } def getVariantTagType(t: Type) -> IntType { + 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); } + def getDecl(t: Type) -> VstClass { + return if(ClassType.?(t), ClassType.!(t).classDecl); + } + def getWidth(t: Type) -> byte { match (t) { - x: ClassType => return ClassType.!(getRootType(t)).classDecl.tagType; - x: EnumType => return x.enumDecl.tagType; - _ => return null; + x: ClassType => return x.width; + x: PrimType => return x.width; + _ => return 0; + } + } + def getTagLo(t: Type) -> int { + if (ClassType.?(t)) { + var decl = ClassType.!(t).classDecl; + return if(decl.isEnum(), decl.tagLo, decl.variantTag); } + return 0; + } + def getTagHi(t: Type) -> int { + 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 { @@ -220,6 +245,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/v3/V3Class.v3 b/aeneas/src/v3/V3Class.v3 index 864877fdd..15625bbb7 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) { @@ -13,25 +14,52 @@ 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) { - names = prog.newRecord(V3.stringArrayType, classDecl.cases.length); - for (vc in classDecl.cases) { - names.values[vc.tag] = Byte.boxString(vc.name(), prog); - } +} +// 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) { + 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); } - return names; } } -// 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: V3Class_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; @@ -45,7 +73,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); } @@ -74,4 +101,30 @@ 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); + } + 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 82f059c29..2b337abaa 100644 --- a/aeneas/src/v3/V3Enum.v3 +++ b/aeneas/src/v3/V3Enum.v3 @@ -1,41 +1,11 @@ // 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 PrimType { - def enumDecl: VstEnum; - var setType: EnumSetType; - var names: Record; - - new(hash: int, enumDecl, typeCon: TypeCon) super(hash, typeCon, enumDecl.tagType.width, null) { } - - def lookupCase(name: string) -> VstEnumCase { - var member = enumDecl.memberMap[name]; - if (VstEnumCase.?(member)) return VstEnumCase.!(member); - return null; - } - def getNameOperator() -> Operator { - var types: Array = [this]; - return V3Op.newVstSugar(VstOperator.VariantGetName, 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()); - } - 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); - } - } - return names; - } -} // 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; 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..c509c0c77 100644 --- a/aeneas/src/vst/MethodEnv.v3 +++ b/aeneas/src/vst/MethodEnv.v3 @@ -164,8 +164,23 @@ class MethodEnv { } return VarBinding.None; } - def resolveEnumMember(expr: VarExpr, enumType: EnumType, name: string) -> ResolvedMember { - var member = enumType.enumDecl.memberMap[name]; + def resolveEnumMember(expr: VarExpr, enumType: ClassType, name: string) -> ResolvedMember { + var decl = enumType.classDecl; + 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)) { @@ -252,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) { @@ -283,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; } @@ -299,12 +320,17 @@ 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)) { - var names = V3Class_TypeCon.!(V3.getRootType(variantType).typeCon).getNames(verifier.verifier.prog); + if (VstNameMember.?(member.member)) { + 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 = 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); } @@ -313,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")) { @@ -322,56 +348,95 @@ 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); 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.classDecl.subtypes != null) { + var sub = enumType.classDecl.subtypes[name]; + if (sub != null) { + var subType = ClassType.!(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); return expr.bind(null, VarBinding.Inst(enumType.enumGetParamOperator(x), N), memberType); } - x: VstVariantTagMember => { - var tagType = enumType.enumDecl.tagType; + x: VstTagMember => { + var tagType = enumType.classDecl.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 => { + 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 { + def lookupEnumExprMember(expr: VarExpr, enumType: ClassType, 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)); + return newApplyCompBinding(expr, receiver, enumType.enumGetParamOperator(x)); } - x: VstVariantTagMember => { - var tagType = enumType.enumDecl.tagType; - return newApplyCompBinding(expr, expr.expr, V3Op.newVstSugar(VstOperator.Nop, + x: VstTagMember => { + var tagType = enumType.classDecl.tagType; + return newApplyCompBinding(expr, receiver, V3Op.newVstSugar(VstOperator.Nop, [enumType, tagType], [enumType], tagType)); } - x: VstVariantNameMember => { - return newApplyCompBinding(expr, expr.expr, enumType.getNameOperator()); + 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); } } return VarBinding.None; } - def lookupEnumConstMember(expr: VarExpr, enumType: EnumType, vc: VstEnumCase) -> 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")) { + // Return the short name as a compile-time constant using the DFS global tag. + var shortNames = enumType.getShortNames(verifier.verifier.prog); + var idx = vc.enumTag; + 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) { @@ -384,14 +449,26 @@ class MethodEnv { return expr.bind(null, VarBinding.Const(val, x.getType()), x.getType()); } } - x: VstVariantTagMember => { - var tagType = enumType.enumDecl.tagType; - return expr.bind(null, VarBinding.Const(Int.box(vc.tag), tagType), tagType); + x: VstTagMember => { + 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); } - x: VstVariantNameMember => { + x: VstNameMember => { 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 = 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); + } + x: VstMethod => { + return lookupEnumExprMember(expr, enumType, false); } } return lookupEnumExprMember(expr, enumType, false); @@ -523,12 +600,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 +613,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: VstClass, name: string) -> VstCaseMember { + var m = decl.memberMap[name]; + 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; + } + return null; + } def lookupRefTypeMember(expr: VarExpr, refType: RefType) -> VarBinding { var name = expr.ident.name.image; if (Strings.equal("at", name)) { @@ -614,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)); @@ -681,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(); @@ -693,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(); @@ -828,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/Parser.v3 b/aeneas/src/vst/Parser.v3 index 91d79851b..3cb0fb2c5 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 @@ -459,35 +462,139 @@ 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); - // 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 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); + var desugar = EnumDesugaring.new(isPrivate, id, params); + var list = desugar.synthesizeMembers(p.fileName, p.curLine, p.curCol); // Parse case members - var cases = Vector.new(); p.req1('{'); while (true) { if (p.curByte == '}') { p.advance1(); break; } - var c = parseEnumCase(p, cases.length); + if (p.curByte == ';') break; // methods section follows + var c = parseEnumCase(p, desugar); if (c == null) { p.opt1('}'); break; } - cases.put(c); + desugar.addCase(c); list = List.new(c, list); + if (desugar.hasDefault) { + if (p.curByte == ';') break; // methods after _ + p.req1('}'); + break; + } } - var decl = VstEnum.new(isPrivate, id.name, params, cases.extract(), list); + // 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 = 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, 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 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 members = if(p.curByte == '{', parseEnumCaseMembers(p)); + var c = desugar.synthesizeEnumCaseMember(id.name, args, members); + 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..082ee9bc6 100644 --- a/aeneas/src/vst/Verifier.v3 +++ b/aeneas/src/vst/Verifier.v3 @@ -25,16 +25,20 @@ 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); 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. @@ -47,6 +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(linkEnumCaseMethods); vst.enums.apply(addEnumVals); } if (mainMethod != null) { @@ -86,6 +92,25 @@ 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 = 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(); + 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; + } + 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(); @@ -123,12 +148,34 @@ 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.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()); + 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.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()); - 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]; @@ -278,7 +325,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; @@ -300,7 +347,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; @@ -352,7 +399,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, @@ -394,20 +441,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; @@ -426,7 +459,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). @@ -448,13 +481,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++; } @@ -470,7 +503,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. @@ -484,12 +517,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. @@ -503,10 +536,300 @@ 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: 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). + 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(VstClass.!(cv.compound), m.name()); + if (parentMethod != null) { + m.root = parentMethod.root; + m.index = parentMethod.index; + } + } + } + def verifyEnumQualifiers(decl: VstClass) { + 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 = 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)); + 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: VstClass) -> VstClass { + var root = decl; + if (root.qualifiers != null) { + var tc = prog.typeEnv.lookup(root.qualifiers.head.image); + if (tc != null && tc.kind == Kind.ENUM) { + root = ClassType.!(tc.create0()).classDecl; + } + } + 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: VstClass, target: VstClass) -> 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 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; + 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: 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; + 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). + 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; + } + // 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) { + 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) { + decl.tagType = tagType; + for (l = decl.subtypeOrder; l != null; l = l.tail) { + propagateEnumTagType(l.head, tagType); + } + } + def finalizeEnumType(decl: VstClass) { + if (decl.qualifiers != null) return; // only process from root + var enumType = ClassType.!(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.tagHi >= 0, decl.tagHi + 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: VstClass, 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: VstClass, setType: EnumSetType) { + for (l = decl.subtypeOrder; l != null; l = l.tail) { + ClassType.!(l.head.typeCon.singleton).setType = setType; + propagateEnumSetType(l.head, setType); + } + } def verifyLayout(decl: VstLayout) { var cv = decl.verifier; var size = tryUnboxPositiveInt(decl.size, -1); @@ -607,9 +930,9 @@ 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); } } } @@ -617,7 +940,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 { @@ -667,15 +990,26 @@ 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 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,15 +1019,38 @@ 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"); } + // 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) p.head.vtype = 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; @@ -771,7 +1128,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; } @@ -878,8 +1235,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"); } } @@ -1010,6 +1367,16 @@ class VstCompoundVerifier { } var binding = lookupSuperMember(decl.name()); if (binding == null) { + // Check parent enum chain for subtype enum method overrides. + 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; + verifier.methodOverrideChecks = List.new((this, decl), verifier.methodOverrideChecks); + return; + } + } decl.root = decl; decl.index = compound.numMethods++; return; @@ -1043,9 +1410,11 @@ class VstCompoundVerifier { if (t != null) match (t) { x: IntType => scale = x.packedByteSize; x: FloatType => scale = x.byteSize; - x: EnumType => scale = x.packedByteSize; 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) { @@ -1082,6 +1451,7 @@ class VstCompoundVerifier { } def checkMethodOverride(decl: VstMethod) { var binding = lookupSuperMember(decl.name()); // re-resolve member after return type inference + if (binding == null) return; var superDecl = VstMethod.!(binding.member); var superMethodType = binding.memberType; var methodType = decl.getType(); @@ -1110,6 +1480,15 @@ class VstCompoundVerifier { 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) { + 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; @@ -1185,9 +1564,9 @@ class VstCompoundVerifier { return x.binding = resolveSubtypeWithArgs(x, subDecl, leftType); } } - } else if (EnumType.?(leftType)) { - // Enum.set resolution. - var enumType = EnumType.!(leftType); + } else if (V3.isEnum(leftType)) { + // Enum.set or Enum.SubtypeEnum resolution. + var enumType = ClassType.!(leftType); if (Strings.equal(x.name.image, "set")) { if (x.nested != null) { errAtRange(x.name.range()).TypeParamArityError("enum set type", @@ -1195,6 +1574,34 @@ class VstCompoundVerifier { } return getEnumSetType(x, enumType); } + // Check if name is a subtype enum. + 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", + 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 = ClassType.!(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.classDecl.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)); @@ -1274,10 +1681,10 @@ 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.enumDecl.cases.length); - return getErrorType(); + errAtRange(tref.range()).EnumSetTooLarge(enumType, tref, enumType.classDecl.cases.length); + return tref.binding = getErrorType(); } return tref.binding = enumType.setType; } @@ -1551,7 +1958,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(); } @@ -1804,12 +2211,13 @@ 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; } } 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. @@ -1831,11 +2239,23 @@ 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.classDecl.subtypes != null) { + var subtypeDecl = vt.classDecl.subtypes[name]; + if (subtypeDecl != null) { + pat.subtypeEnumType = ClassType.!(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); + mv.addCasePattern(pat, vc); } def visitEmpty(stmt: EmptyStmt, outer: Type) -> Type { return Void.TYPE; @@ -1961,7 +2381,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); @@ -2833,7 +3253,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; @@ -2850,9 +3270,9 @@ class MatchStmtVerifier(etype: Type, tc: TypeChecker) { } variantType = vt; covered = Array.new(variantType.classDecl.cases.length); - } else if (EnumType.?(etype)) { - enumType = EnumType.!(etype); - covered = Array.new(enumType.enumDecl.cases.length); + } else if (V3.isEnum(etype)) { + enumType = ClassType.!(etype); + covered = Array.new(enumType.classDecl.cases.length); } } def addValue(pat: MatchPattern, val: Val) { @@ -2864,19 +3284,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()); + 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.decl.variantTag); - } - def addEnumPattern(pat: MatchPattern, vc: VstEnumCase) { - if (covered[vc.tag] != null) { - MatchError(pat.expr.range(), "duplicate case", covered[vc.tag].expr.range()); - } - covered[vc.tag] = pat; - pat.val = Int.box(vc.tag); + covered[vc.localTag] = pat; + pat.val = Int.box(vc.runtimeTag()); } def addTypePattern(pat: MatchPattern, t: Type) { var cast = TypeSystem.newTypeCast(etype, t); @@ -2893,6 +3306,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 @@ -2900,7 +3314,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); } } } @@ -2913,6 +3327,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.classDecl.hasDefault) return; for (p in covered) { if (p == null) return; } @@ -2930,15 +3346,23 @@ class MatchStmtVerifier(etype: Type, tc: TypeChecker) { } } } + // If the enum has "case _", the match must have a default _ case. + if (enumType != null && enumType.classDecl.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, - enumType.enumDecl.cases[i].name(), + // Skip the _ case — it cannot be explicitly matched. + var caseName = if(variantType == null, + enumType.classDecl.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..e6f1c6aeb 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() -> bool { return false; } + def isVariant() -> bool { return 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() -> 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; + } + return null; + } } // Parsed "component X { ... }" class VstComponent extends VstCompound { @@ -193,28 +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; - def tagType = V3.getTagType(cases.length); - - new(isPrivate: bool, name: Token, params: VstList, cases, members: List) - super(isPrivate, name, null, params, null, members) { - // XXX: move type creation somewhere else? - 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; @@ -235,15 +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; - 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; @@ -275,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; @@ -305,11 +289,53 @@ 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 synthesizeEnumCaseMember(name: Token, args: 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); + } +} // A member of a class or component class VstMember extends Decl { @@ -328,30 +354,45 @@ 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 { + 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. -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; } } @@ -732,6 +773,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; @@ -984,7 +1027,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 @@ -1075,10 +1118,11 @@ type VstOperator { case FloatFraction; // desugared to (u23|u52).!(u32|u64.view(float|double)) case VariantGetName; // desugared to ArrayGetElem(#names, VariantGetTag(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/aeneas/src/wasm/WasmCodeGen.v3 b/aeneas/src/wasm/WasmCodeGen.v3 index 0072197ee..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) { + 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) { + if (rt == m.receiver || V3.isEnum(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 @@ -409,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; @@ -1635,8 +1643,19 @@ 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 (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 (V3.isEnum(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)) { // get the receiver and cast it @@ -1647,7 +1666,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 +1688,16 @@ 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 (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 (!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)); + } for (i = 1; i < m.ssa.params.length; i++) { b(WasmOp.GET_LOCAL.opcode); w.put_sleb32(i); } @@ -1769,6 +1795,9 @@ 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) { var wht = wasmGc.gcTypeTable.addHeapType(t); if (wht != null) { @@ -1794,7 +1823,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/aeneas/src/wasm/WasmGcTarget.v3 b/aeneas/src/wasm/WasmGcTarget.v3 index 53c710b37..bc3a2dca3 100644 --- a/aeneas/src/wasm/WasmGcTarget.v3 +++ b/aeneas/src/wasm/WasmGcTarget.v3 @@ -137,10 +137,11 @@ 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 { + 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.enumDecl.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); @@ -351,8 +352,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 sigIdx = wasm.addSig(Oop.TYPE, meth.sig); - var fi = FunctionInfo.new(FunctionInfoKind.Indirect, Oop.TYPE, meth.sig, sigIdx, meth, findex); + var adapterRecv = Oop.TYPE; + var adapterSig = meth.sig; + 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); + } + 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 || @@ -410,8 +417,13 @@ 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 = Oop.TYPE; + var adapterSig = m.sig; + 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); + var fi = FunctionInfo.new(FunctionInfoKind.Indirect, adapterRecv, adapterSig, sigIdx, m, findex); table0Infos.put(fi); wasm.indirectAdapterIndexFor[m] = table0Infos.length; wasm.indirectAdapterVector.put(m); @@ -452,7 +464,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(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) var funcInfo: FunctionInfo = if(superIdx < 0, null, functionInfos[superIdx]); @@ -1315,7 +1328,8 @@ class WasmGcProgram extends WasmProgram { } // override def getSigIndex(m: IrMethod) -> u32 { - var sig = getSig(m.receiver, m.sig); + 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); if (index == 0) context.fail1("method signature not found: %q", sig.funcType().render); @@ -1408,7 +1422,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(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); infos.put(fi); diff --git a/aeneas/src/wasm/WasmOp.v3 b/aeneas/src/wasm/WasmOp.v3 index 2b7620a64..b5c49d786 100644 --- a/aeneas/src/wasm/WasmOp.v3 +++ b/aeneas/src/wasm/WasmOp.v3 @@ -198,8 +198,13 @@ enum WasmExtOp(extopcode: u16, text_name: string) { ARRAY_LEN(0xFB0F, "array.len"), REF_TEST(0xFB14, "ref.test"), REF_TEST_NULL(0xFB15, "ref.test_null"), + 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"), 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"), diff --git a/aeneas/src/wasm/WasmTarget.v3 b/aeneas/src/wasm/WasmTarget.v3 index 63f005f26..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.enumDecl.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 @@ -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) && !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,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(V3.isEnum(m.receiver), Void.TYPE, m.ssa.params[0].vtype); + addSig(recv, m.sig); } functions.set(index, (m, kind)); functionCursor = functions.length; 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; 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/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 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..3deb962cb --- /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="-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="-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)" + 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..c81a96a46 --- /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="-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="-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)" + 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." 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." diff --git a/bench/run-wasm-entry.mjs b/bench/run-wasm-entry.mjs new file mode 100644 index 000000000..9aba3e7d9 --- /dev/null +++ b/bench/run-wasm-entry.mjs @@ -0,0 +1,35 @@ +// 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(). +// +// 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 } 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: false, + version: 'preview1', + args: program_args, + env, + preopens: { '.': '.' }, +}); +const importObject = { wasi_snapshot_preview1: wasi.wasiImport }; + +const bytes = readFileSync(wasm_path); +const instance = new WebAssembly.Instance(new WebAssembly.Module(bytes), importObject); + +wasi.initialize(instance); +instance.exports.entry(); 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 diff --git a/claude/todos-extended-enums.md b/claude/todos-extended-enums.md new file mode 100644 index 000000000..da2dc6905 --- /dev/null +++ b/claude/todos-extended-enums.md @@ -0,0 +1,74 @@ +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 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. + - 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). + - 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: + 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 6e6af6471..96b695ed8 100644 --- a/doc/grammar-claude.md +++ b/doc/grammar-claude.md @@ -76,9 +76,75 @@ 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 with optional per-case methods + | '_' ','? // default case: optional, must be last, at most one +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 + | '(' 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 (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. + + +### 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 +201,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..12fd2d969 --- /dev/null +++ b/doc/tutorial/EnumMethods.md @@ -0,0 +1,61 @@ +# 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-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 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 { def speak() -> int { return 2; } }, // per-case override + SNAKE; + + def speak() -> int { return 1; } // subtype-level override +} +``` + +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/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..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); +} 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..fff855048 --- /dev/null +++ b/test/enums/enum_closure08.v3 @@ -0,0 +1,15 @@ +//@execute 0=10; 1=11; 2=20; 3=22 +// 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 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_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; +} 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](); +} 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; +} 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; +} 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_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; +} 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; +} 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/enum_method07.v3 b/test/enums/enum_method07.v3 new file mode 100644 index 000000000..9e6fb7845 --- /dev/null +++ b/test/enums/enum_method07.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_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; +} 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(); +} 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; +} 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_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_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_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_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_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/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; +} diff --git a/test/enums/parser/enum_cmethod00.v3 b/test/enums/parser/enum_cmethod00.v3 new file mode 100644 index 000000000..252debd70 --- /dev/null +++ b/test/enums/parser/enum_cmethod00.v3 @@ -0,0 +1,3 @@ +//@parse +// Per-case method bodies with override +enum E { A { def m() -> int { return 1; } }, B; def m() -> int { return 0; } } 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_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) } 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; +}