Skip to content

Commit

Permalink
Improve JavaScript handling of Map/Object
Browse files Browse the repository at this point in the history
Switch to Map by default and introduce two new advanced types to make
the distinction between Map/Object when needed. The new types are
"dictionary" (since "map" is already in use) for the datastructure and
"object" for the JavaScript-specific "Object" type, which no other
language natively supports at the moment.

All existing languages have been updated to support the "dictionary"
advanced type if they supported the basic "map" type, while the advanced
type for "object" is reduced, except in JavaScript, which supports both.

As part of this, the way to indicate type limits on certain elements
of collections has been reworked to be more flexible. It is now possible
to specify limits for all types separately. At the moment, we still only
recognize limits on map keys and set elements, so nothing changes there.
  • Loading branch information
niknetniko committed Apr 12, 2024
1 parent 106446b commit dcf9668
Show file tree
Hide file tree
Showing 20 changed files with 239 additions and 80 deletions.
3 changes: 2 additions & 1 deletion tested/datatypes/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from tested.datatypes.advanced import (
AdvancedNothingTypes,
AdvancedNumericTypes,
AdvancedObjectTypes,
AdvancedSequenceTypes,
AdvancedStringTypes,
AdvancedTypes,
Expand All @@ -36,7 +37,7 @@
BooleanTypes = BasicBooleanTypes
NothingTypes = BasicNothingTypes | AdvancedNothingTypes
SequenceTypes = BasicSequenceTypes | AdvancedSequenceTypes
ObjectTypes = BasicObjectTypes
ObjectTypes = BasicObjectTypes | AdvancedObjectTypes

SimpleTypes = NumericTypes | StringTypes | BooleanTypes | NothingTypes
ComplexTypes = SequenceTypes | ObjectTypes
Expand Down
13 changes: 13 additions & 0 deletions tested/datatypes/advanced.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from tested.datatypes.basic import (
BasicNothingTypes,
BasicNumericTypes,
BasicObjectTypes,
BasicSequenceTypes,
BasicStringTypes,
BasicTypes,
Expand Down Expand Up @@ -110,9 +111,21 @@ class AdvancedNothingTypes(_AdvancedDataType):
"""


class AdvancedObjectTypes(_AdvancedDataType):
DICTIONARY = "dictionary", BasicObjectTypes.MAP
"""
A proper map/dictionary/associative array data structure.
"""
OBJECT = "object", BasicObjectTypes.MAP
"""
An object like in JavaScript.
"""


AdvancedTypes = Union[
AdvancedNumericTypes,
AdvancedSequenceTypes,
AdvancedStringTypes,
AdvancedNothingTypes,
AdvancedObjectTypes,
]
35 changes: 25 additions & 10 deletions tested/features.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,14 @@

from attrs import define

from tested.datatypes import AllTypes, BasicObjectTypes, BasicSequenceTypes, NestedTypes
from tested.datatypes import (
AllTypes,
BasicObjectTypes,
BasicSequenceTypes,
ComplexExpressionTypes,
NestedTypes,
resolve_to_basic,
)

if TYPE_CHECKING:
from tested.languages.config import Language
Expand Down Expand Up @@ -166,20 +173,28 @@ def is_supported(language: "Language") -> bool:
return False
nested_types = []
for key, value_types in required.nested_types:
if key in (BasicSequenceTypes.SET, BasicObjectTypes.MAP):
# Skip these
if isinstance(key, ComplexExpressionTypes):
continue
basic_key = resolve_to_basic(key)
if basic_key in (BasicSequenceTypes.SET, BasicObjectTypes.MAP):
nested_types.append((key, value_types))

restricted = {
BasicSequenceTypes.SET: language.set_type_restrictions(),
BasicObjectTypes.MAP: language.map_type_restrictions(),
}
collection_restrictions = language.collection_restrictions()

for key, value_types in nested_types:
if not (value_types <= restricted[key]):
basic_key = resolve_to_basic(key)
restrictions = collection_restrictions.get(
key, collection_restrictions.get(basic_key)
)
# If None, skip as there are no restrictions.
if restrictions is None:
continue
if not (value_types <= restrictions):
_logger.warning("This test suite is not compatible!")
_logger.warning(f"Required {key} types are {value_types}.")
_logger.warning(f"The language supports {restricted[key]}.")
missing = (value_types ^ restricted[key]) & value_types
_logger.warning(f"For {key}, used types are {value_types}.")
_logger.warning(f"Language restrictions are {restrictions}.")
missing = (value_types ^ restrictions) & value_types
_logger.warning(f"Missing types are: {missing}.")
return False

Expand Down
29 changes: 12 additions & 17 deletions tested/languages/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -243,29 +243,24 @@ def supported_constructs(self) -> set[Construct]:
"""
return set()

def map_type_restrictions(self) -> set[ExpressionTypes] | None:
def collection_restrictions(self) -> dict[AllTypes, set[ExpressionTypes]]:
"""
Get type restrictions that apply to map types in this language.
Get type restrictions for other types.
If you return None, all data types are assumed to be usable as the key in
a map data type, such as a dictionary or hashmap. Otherwise, you must return
a whitelist of the allowed types.
The exact interpretation varies by the type. Only some restrictions are
currently recognized:
:return: The whitelist of allowed types, or everything is allowed.
"""
return None

def set_type_restrictions(self) -> set[ExpressionTypes] | None:
"""
Get type restrictions that apply to the set types in this language.
Restrictions on `BasicObjectTypes.Map` (or related advanced types) are
interpreted as restrictions on the type of the keys. The provided types
are a whitelist: the only allowed types.
If you return None, all data types are assumed to be usable as the key in
a set data type, such as a HashSet. Otherwise, you must return a whitelist
of the allowed types.
Restrictions on `BasicSetTypes.SET` (or related advanced types) are
interpreted as restrictions on the type of the elements. The provided
types are a whitelist: the only allowed types.
:return: The whitelist of allowed types, or everything is allowed.
:return: Mapping of types to their restrictions. Unmapped types are unrestricted.
"""
return None
return dict()

def datatype_support(self) -> dict[AllTypes, TypeSupport]:
"""
Expand Down
2 changes: 2 additions & 0 deletions tested/languages/csharp/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@ def datatype_support(self) -> dict[AllTypes, TypeSupport]:
"sequence": "supported",
"set": "supported",
"map": "supported",
"dictionary": "supported",
"object": "reduced",
"nothing": "supported",
"undefined": "reduced",
"null": "reduced",
Expand Down
2 changes: 1 addition & 1 deletion tested/languages/csharp/templates/Values.cs
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ class Values
type = "set";
data = encodeSequence((IEnumerable) value);
} else if (value is IDictionary) {
type = "map";
type = "dictionary";
List<DictionaryEntry> entries = new List<DictionaryEntry>();
foreach (DictionaryEntry entry in (IDictionary) value)
{
Expand Down
22 changes: 16 additions & 6 deletions tested/languages/java/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,12 @@
from pathlib import Path
from typing import TYPE_CHECKING

from tested.datatypes import AllTypes, ExpressionTypes
from tested.datatypes import (
AllTypes,
BasicObjectTypes,
BasicSequenceTypes,
ExpressionTypes,
)
from tested.dodona import AnnotateCode, Message
from tested.features import Construct, TypeSupport
from tested.languages.config import (
Expand Down Expand Up @@ -71,6 +76,8 @@ def datatype_support(self) -> dict[AllTypes, TypeSupport]:
"sequence": "supported",
"set": "supported",
"map": "supported",
"dictionary": "supported",
"object": "reduced",
"nothing": "supported",
"undefined": "reduced",
"null": "reduced",
Expand All @@ -91,8 +98,8 @@ def datatype_support(self) -> dict[AllTypes, TypeSupport]:
"list": "supported",
}

def map_type_restrictions(self) -> set[ExpressionTypes] | None:
return { # type: ignore
def collection_restrictions(self) -> dict[AllTypes, set[ExpressionTypes]]:
restrictions = {
"integer",
"real",
"char",
Expand All @@ -113,9 +120,10 @@ def map_type_restrictions(self) -> set[ExpressionTypes] | None:
"function_calls",
"identifiers",
}

def set_type_restrictions(self) -> set[ExpressionTypes] | None:
return self.map_type_restrictions()
return {
BasicObjectTypes.MAP: restrictions, # type: ignore
BasicSequenceTypes.SET: restrictions,
}

def compilation(self, files: list[str]) -> CallbackResult:
def file_filter(file: Path) -> bool:
Expand Down Expand Up @@ -171,6 +179,8 @@ def get_declaration_metadata(self) -> TypeDeclarationMetadata:
"sequence": "List",
"set": "Set",
"map": "Map",
"dictionary": "Map",
"object": "Map",
"nothing": "Void",
"undefined": "Void",
"int8": "byte",
Expand Down
8 changes: 4 additions & 4 deletions tested/languages/java/templates/Values.java
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ private static List<String> internalEncode(Object value) {
type = "set";
data = encodeSequence((Iterable<?>) value);
} else if (value instanceof Map) {
type = "map";
type = "dictionary";
var elements = new ArrayList<String>();
for (Map.Entry<?, ?> entry : ((Map<?, ?>) value).entrySet()) {
elements.add("{ \"key\":" + encode(entry.getKey()) + ",\"value\": " + encode(entry.getValue()) + "}");
Expand Down Expand Up @@ -163,7 +163,7 @@ private static String convertMessage(EvaluationResult.Message message) {
var description = asJson(message.description);
var format = asJson(message.format);
var permission = asJson(message.permission);

return """
{
"description": %s,
Expand All @@ -172,7 +172,7 @@ private static String convertMessage(EvaluationResult.Message message) {
}
""".formatted(description, format, permission);
}

private static String asJson(String value) {
if (value == null) {
return "null";
Expand All @@ -188,7 +188,7 @@ public static void sendEvaluated(PrintWriter writer, EvaluationResult r) {
var dslExpected = asJson(r.dslExpected);
var dslActual = asJson(r.dslActual);
var messages = String.join(", ", converted);

String result = """
{
"result": %b,
Expand Down
26 changes: 10 additions & 16 deletions tested/languages/javascript/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,12 @@
from pathlib import Path
from typing import TYPE_CHECKING

from tested.datatypes import AllTypes, BasicStringTypes, ExpressionTypes
from tested.datatypes import (
AdvancedObjectTypes,
AllTypes,
BasicStringTypes,
ExpressionTypes,
)
from tested.dodona import AnnotateCode, Message
from tested.features import Construct, TypeSupport
from tested.languages.config import (
Expand Down Expand Up @@ -72,6 +77,8 @@ def datatype_support(self) -> dict[AllTypes, TypeSupport]:
"sequence": "supported",
"set": "supported",
"map": "supported",
"dictionary": "supported",
"object": "supported",
"nothing": "supported",
"undefined": "supported",
"null": "supported",
Expand All @@ -92,21 +99,8 @@ def datatype_support(self) -> dict[AllTypes, TypeSupport]:
"tuple": "reduced",
}

def map_type_restrictions(self) -> set[ExpressionTypes] | None:
return {BasicStringTypes.TEXT}

def set_type_restrictions(self) -> set[ExpressionTypes] | None:
return { # type: ignore
"integer",
"real",
"text",
"boolean",
"sequence",
"set",
"map",
"function_calls",
"identifiers",
}
def collection_restrictions(self) -> dict[AllTypes, set[ExpressionTypes]]:
return {AdvancedObjectTypes.OBJECT: {BasicStringTypes.TEXT}}

def compilation(self, files: list[str]) -> CallbackResult:
submission = submission_file(self)
Expand Down
20 changes: 17 additions & 3 deletions tested/languages/javascript/generators.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
BasicSequenceTypes,
BasicStringTypes,
)
from tested.datatypes.advanced import AdvancedObjectTypes
from tested.languages.conventionalize import submission_file
from tested.languages.preparation import (
PreparedContext,
Expand Down Expand Up @@ -48,6 +49,17 @@ def convert_value(value: Value) -> str:
raise AssertionError("Double extended values are not supported in js.")
elif value.type == AdvancedNumericTypes.FIXED_PRECISION:
raise AssertionError("Fixed precision values are not supported in js.")
elif value.type == AdvancedObjectTypes.OBJECT:
assert isinstance(value, ObjectType)
result = "{"
for i, pair in enumerate(value.data):
result += convert_statement(pair.key, True)
result += ": "
result += convert_statement(pair.value, True)
if i != len(value.data) - 1:
result += ", "
result += "}"
return result
elif value.type in (
AdvancedNumericTypes.INT_64,
AdvancedNumericTypes.U_INT_64,
Expand Down Expand Up @@ -79,14 +91,16 @@ def convert_value(value: Value) -> str:
return f"new Set([{convert_arguments(value.data)}])"
elif value.type == BasicObjectTypes.MAP:
assert isinstance(value, ObjectType)
result = "{"
result = "new Map(["
for i, pair in enumerate(value.data):
result += "["
result += convert_statement(pair.key, True)
result += ": "
result += ", "
result += convert_statement(pair.value, True)
result += "]"
if i != len(value.data) - 1:
result += ", "
result += "}"
result += "])"
return result
elif value.type == BasicStringTypes.UNKNOWN:
assert isinstance(value, StringType)
Expand Down
4 changes: 2 additions & 2 deletions tested/languages/javascript/templates/values.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ function encode(value) {
type = "set";
value = Array.from(value).map(encode);
} else if (value instanceof Map) {
type = "map";
type = "dictionary";
value = Array
.from(value)
.map(([key, value]) => {
Expand All @@ -65,7 +65,7 @@ function encode(value) {
);
} else if (value?.constructor === Object) {
// Plain objects
type = "map";
type = "object";
// Process the elements of the object.
value = Object.entries(value).map(([key, value]) => {
return {
Expand Down
Loading

0 comments on commit dcf9668

Please sign in to comment.