Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,8 @@ export namespace CommonNames {
export const ASC_VERSION_MAJOR = "ASC_VERSION_MAJOR";
export const ASC_VERSION_MINOR = "ASC_VERSION_MINOR";
export const ASC_VERSION_PATCH = "ASC_VERSION_PATCH";
// enums
export const EnumToString = "__enum_to_string";
// classes
export const I8 = "I8";
export const I16 = "I16";
Expand Down
55 changes: 54 additions & 1 deletion src/compiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1522,6 +1522,49 @@ export class Compiler extends DiagnosticEmitter {
return true;
}

private ensureEnumToString(enumElement: Enum, reportNode: Node): string | null {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you add a couple blank lines in here? It's a little hard to read

if (enumElement.toStringFunctionName) return enumElement.toStringFunctionName;

if (!this.compileEnum(enumElement)) return null;
if (enumElement.is(CommonFlags.Const)) {
this.errorRelated(
DiagnosticCode.A_const_enum_member_can_only_be_accessed_using_a_string_literal,
reportNode.range, enumElement.identifierNode.range
);
return null;
}

let members = enumElement.members;
if (!members) return null;

let module = this.module;
const isInline = enumElement.hasDecorator(DecoratorFlags.Inline);

const functionName = `${enumElement.internalName}#${CommonNames.EnumToString}`;
enumElement.toStringFunctionName = functionName;

let exprs = new Array<ExpressionRef>();
// when the values are the same, TS returns the last enum value name that appears
for (let _keys = Map_keys(members), _values = Map_values(members), i = 1, k = _keys.length; i <= k; ++i) {
let enumValueName = unchecked(_keys[k - i]);
let member = unchecked(_values[k - i]);
Comment on lines +1548 to +1550
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
for (let _keys = Map_keys(members), _values = Map_values(members), i = 1, k = _keys.length; i <= k; ++i) {
let enumValueName = unchecked(_keys[k - i]);
let member = unchecked(_values[k - i]);
for (let _keys = Map_keys(members), _values = Map_values(members), i = _keys.length - 1; i >= 0; --i) {
let enumValueName = unchecked(_keys[i]);
let member = unchecked(_values[i]);

Doesn't this work?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if i is signed, then it works. The original version works for unsigned and signed to avoid unexpected endless loop

if (member.kind != ElementKind.EnumValue) continue;
let enumValue = <EnumValue>member;
const enumValueExpr = isInline
? module.i32(i64_low(enumValue.constantIntegerValue))
: module.global_get(enumValue.internalName, TypeRef.I32);
let expr = module.if(
module.binary(BinaryOp.EqI32, enumValueExpr, module.local_get(0, TypeRef.I32)),
module.return(this.ensureStaticString(enumValueName))
);
exprs.push(expr);
}
exprs.push(module.unreachable());
module.addFunction(functionName, TypeRef.I32, TypeRef.I32, null, module.block(null, exprs, TypeRef.I32));

return functionName;
}

// === Functions ================================================================================

/** Compiles a priorly resolved function. */
Expand Down Expand Up @@ -7092,7 +7135,17 @@ export class Compiler extends DiagnosticEmitter {
): ExpressionRef {
let module = this.module;
let targetExpression = expression.expression;
let targetType = this.resolver.resolveExpression(targetExpression, this.currentFlow); // reports
let resolver = this.resolver;
let targetElement = resolver.lookupExpression(targetExpression, this.currentFlow, Type.auto, ReportMode.Swallow);
if (targetElement && targetElement.kind == ElementKind.Enum) {
const elementExpr = this.compileExpression(expression.elementExpression, Type.i32, Constraints.ConvImplicit);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like we're allowing Enum[Enum.X] but not Enum["X"] (only Enum.X, the property access expression, is allowed). This is likely a good thing to have, unless users really want to get enum members with computed string values (which should probably be discouraged regardless).

(It also seems like we don't have a good error for someObject["property"] and we use TS2329 instead, so a good error for Enum["X"] isn't necessary for now.)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, since the current runtime only support to get enum value from compilation time known string (aka string literal). Enum.X is always better then Enum["X"].

const toStringFunctionName = this.ensureEnumToString(<Enum>targetElement, expression);
this.currentType = this.program.stringInstance.type;
if (toStringFunctionName == null) return module.unreachable();
return module.call(toStringFunctionName, [ elementExpr ], TypeRef.I32);
}

let targetType = resolver.resolveExpression(targetExpression, this.currentFlow);
if (targetType) {
let classReference = targetType.getClassOrWrapper(this.program);
if (classReference) {
Expand Down
1 change: 1 addition & 0 deletions src/diagnosticMessages.json
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,7 @@
"Type '{0}' has no property '{1}'.": 2460,
"The '{0}' operator cannot be applied to type '{1}'.": 2469,
"In 'const' enum declarations member initializer must be constant expression.": 2474,
"A const enum member can only be accessed using a string literal.": 2476,
"Export declaration conflicts with exported declaration of '{0}'.": 2484,
"'{0}' is referenced directly or indirectly in its own base expression.": 2506,
"Cannot create an instance of an abstract class.": 2511,
Expand Down
2 changes: 1 addition & 1 deletion src/program.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3449,7 +3449,7 @@ export class Namespace extends DeclaredElement {

/** An enum. */
export class Enum extends TypedElement {

toStringFunctionName: string | null = null;
/** Constructs a new enum. */
constructor(
/** Simple name. */
Expand Down
7 changes: 7 additions & 0 deletions tests/compiler/enum-to-string-error.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"asc_flags": [],
"stderr": [
"TS2476: A const enum member can only be accessed using a string literal.",
"EOF"
]
}
9 changes: 9 additions & 0 deletions tests/compiler/enum-to-string-error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
const enum CE {
CE0,
CE1,
CE2,
}

assert(CE[CE.CE0] === "CE0");

ERROR("EOF");
Loading