Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[stdlib] Add repr() function and Representable trait #2361

Closed
Closed
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
8 changes: 8 additions & 0 deletions stdlib/src/builtin/int.mojo
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,14 @@ struct Int(Intable, Stringable, KeyElement, Boolable, Formattable):
# Keep buf alive until we've finished with the StringRef
_ = buf^

fn __repr__(self) -> String:
"""Get the integer as a string. Returns the same `String` as `__str__`.

Returns:
A string representation.
"""
return str(self)

@always_inline("nodebug")
fn __mlir_index__(self) -> __mlir_type.index:
"""Convert to index.
Expand Down
88 changes: 88 additions & 0 deletions stdlib/src/builtin/repr.mojo
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# ===----------------------------------------------------------------------=== #
# Copyright (c) 2024, Modular Inc. All rights reserved.
#
# Licensed under the Apache License v2.0 with LLVM Exceptions:
# https://llvm.org/LICENSE.txt
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ===----------------------------------------------------------------------=== #
"""Provide the `repr` function.

The functions and traits provided here are built-ins, so you don't need to import them.
"""


trait Representable:
"""A trait that describes a type that has a String representation.

Any type that conforms to the `Representable` trait can be used with the
`repr` function. Any conforming type must also implement the `__repr__` method.
Here is an example:

```mojo
@value
struct Dog(Representable):
var name: String
var age: Int

fn __repr__(self) -> String:
return "Dog(name=" + repr(self.name) + ", age=" + repr(self.age) + ")"

var dog = Dog("Rex", 5)
print(repr(dog))
# Dog(name='Rex', age=5)
```

The method `__repr__` should compute the "official" string representation of a type.

If at all possible, this should look like a valid Mojo expression
that could be used to recreate a struct instance with the same
value (given an appropriate environment).
So a returned String of the form `module_name.SomeStruct(arg1=value1, arg2=value2)` is advised.
If this is not possible, a string of the form `<...some useful description...>`
should be returned.

The return value must be a `String` instance.
This is typically used for debugging, so it is important that the representation is information-rich and unambiguous.

Note that when computing the string representation of a collection (`Dict`, `List`, `Set`, etc...),
the `repr` function is called on each element, not the `str()` function.
"""

fn __repr__(self) -> String:
"""Get the string representation of the type instance, if possible, compatible with Mojo syntax.

Returns:
The string representation of the instance.
"""
pass


fn repr[T: Representable](value: T) -> String:
"""
Args:
JoeLoser marked this conversation as resolved.
Show resolved Hide resolved
value: The value to get the string representation of.

Parameters:
T: The type of `value`. Must implement the `Representable` trait.

Returns:
The string representation of the given value.
"""
return value.__repr__()


fn repr(value: None) -> String:
"""Returns the string representation of `None`.

Args:
value: A `None` value.

Returns:
The string representation of `None`.
"""
return "None"
7 changes: 7 additions & 0 deletions stdlib/src/builtin/str.mojo
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,13 @@ trait Stringable:
**Note:** If the `__str__()` method might raise an error, use the
[`StringableRaising`](/mojo/stdlib/builtin/str/stringableraising)
trait, instead.

About the difference between `__repr__()` and `__str__()`:
The method `__repr__` compute the compute the "official" string representation of an object
while `__str__` computes the "informal" or nicely printable string representation of an object.

This method differs from `__repr__()` in that there is no expectation that `__str__()`
return a valid Mojo expression: a more convenient or concise representation can be used.
"""

fn __str__(self) -> String:
Expand Down
11 changes: 11 additions & 0 deletions stdlib/src/builtin/string.mojo
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,17 @@ struct String(
fn __str__(self) -> String:
return self

@always_inline
fn __repr__(self) -> String:
"""Return a Mojo-compatible representation of the `String` instance.

You don't need to call this method directly, use `repr(my_string)` instead.
"""
if "'" in self:
return '"' + self + "'"
else:
return "'" + self + "'"

# ===------------------------------------------------------------------===#
# Initializers
# ===------------------------------------------------------------------===#
Expand Down
12 changes: 12 additions & 0 deletions stdlib/src/builtin/value.mojo
Original file line number Diff line number Diff line change
Expand Up @@ -121,3 +121,15 @@ trait StringableCollectionElement(CollectionElement, Stringable):
"""

pass


trait RepresentableCollectionElement(CollectionElement, Representable):
"""The RepresentableCollectionElement trait denotes a trait composition
of the `CollectionElement` and `Representable` traits.

This is useful to have as a named entity since Mojo does not
currently support anonymous trait compositions to constrain
on `CollectionElement & Representable` in the parameter.
"""

pass
13 changes: 8 additions & 5 deletions stdlib/src/collections/dict.mojo
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ trait KeyElement(CollectionElement, Hashable, EqualityComparable):
pass


trait StringableKeyElement(KeyElement, Stringable):
trait RepresentableKeyElement(KeyElement, Representable):
"""A trait composition for types which implement all requirements of
dictionary keys and Stringable."""

Expand Down Expand Up @@ -534,7 +534,7 @@ struct Dict[K: KeyElement, V: CollectionElement](

@staticmethod
fn __str__[
T: StringableKeyElement, U: StringableCollectionElement
T: RepresentableKeyElement, U: RepresentableCollectionElement
](self: Dict[T, U]) -> String:
"""Returns a string representation of a `Dict`.

Expand All @@ -553,14 +553,17 @@ struct Dict[K: KeyElement, V: CollectionElement](
When the compiler supports conditional methods, then a simple `str(my_dict)` will
be enough.

Note that both they keys and values' types must implement the `__repr__()` method
for this to work. See the `Representable` trait for more information.

Args:
self: The Dict to represent as a string.

Parameters:
T: The type of the keys in the Dict. Must implement the
traits `Stringable` and `KeyElement`.
traits `Representable` and `KeyElement`.
U: The type of the values in the Dict. Must implement the
traits `Stringable` and `CollectionElement`.
traits `Representable` and `CollectionElement`.

Returns:
A string representation of the Dict.
Expand All @@ -571,7 +574,7 @@ struct Dict[K: KeyElement, V: CollectionElement](

var i = 0
for key_value in self.items():
result += str(key_value[].key) + ": " + str(key_value[].value)
result += repr(key_value[].key) + ": " + repr(key_value[].value)
if i < len(self) - 1:
result += ", "
i += 1
Expand Down
8 changes: 5 additions & 3 deletions stdlib/src/collections/list.mojo
Original file line number Diff line number Diff line change
Expand Up @@ -565,7 +565,7 @@ struct List[T: CollectionElement](CollectionElement, Sized, Boolable):
return _ListIter[T, mutability, self_life, False](len(ref[]), ref)

@staticmethod
fn __str__[U: StringableCollectionElement](self: List[U]) -> String:
fn __str__[U: RepresentableCollectionElement](self: List[U]) -> String:
"""Returns a string representation of a `List`.

Note that since we can't condition methods on a trait yet,
Expand All @@ -579,12 +579,14 @@ struct List[T: CollectionElement](CollectionElement, Sized, Boolable):
When the compiler supports conditional methods, then a simple `str(my_list)` will
be enough.

The elements' type must implement the `__repr__()` for this to work.

Args:
self: The list to represent as a string.

Parameters:
U: The type of the elements in the list. Must implement the
traits `Stringable` and `CollectionElement`.
traits `Representable` and `CollectionElement`.

Returns:
A string representation of the list.
Expand All @@ -599,7 +601,7 @@ struct List[T: CollectionElement](CollectionElement, Sized, Boolable):
var result = String(List[Int8](capacity=minimum_capacity))
result += "["
for i in range(len(self)):
result += str(self[i])
result += repr(self[i])
if i < len(self) - 1:
result += ", "
result += "]"
Expand Down
18 changes: 18 additions & 0 deletions stdlib/test/builtin/test_int.mojo
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,22 @@ def test_mod():
assert_equal(1, Int(-3) % Int(2))


def test_string_conversion():
assert_equal(str(Int(3)), "3")
assert_equal(str(Int(-3)), "-3")
assert_equal(str(Int(0)), "0")
assert_equal(str(Int(100)), "100")
assert_equal(str(Int(-100)), "-100")


def test_int_representation():
assert_equal(repr(Int(3)), "3")
assert_equal(repr(Int(-3)), "-3")
assert_equal(repr(Int(0)), "0")
assert_equal(repr(Int(100)), "100")
assert_equal(repr(Int(-100)), "-100")


def main():
test_constructors()
test_properties()
Expand All @@ -77,3 +93,5 @@ def main():
test_pow()
test_floordiv()
test_mod()
test_string_conversion()
test_int_representation()
53 changes: 53 additions & 0 deletions stdlib/test/builtin/test_repr.mojo
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# ===----------------------------------------------------------------------=== #
# Copyright (c) 2024, Modular Inc. All rights reserved.
#
# Licensed under the Apache License v2.0 with LLVM Exceptions:
# https://llvm.org/LICENSE.txt
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ===----------------------------------------------------------------------=== #
# RUN: %mojo %s

from testing import assert_equal


@value
struct Dog(Representable):
var name: String
var age: Int

fn __repr__(self) -> String:
return "Dog(name=" + repr(self.name) + ", age=" + repr(self.age) + ")"


def test_explicit_conformance():
dog = Dog(name="Fido", age=3)
assert_equal(repr(dog), "Dog(name='Fido', age=3)")


@value
struct Cat:
var name: String
var age: Int

fn __repr__(self) -> String:
return "Cat(name=" + repr(self.name) + ", age=" + repr(self.age) + ")"


def test_implicit_conformance():
cat = Cat(name="Whiskers", age=2)
assert_equal(repr(cat), "Cat(name='Whiskers', age=2)")


def test_none_representation():
assert_equal(repr(None), "None")


def main():
test_explicit_conformance()
test_implicit_conformance()
test_none_representation()
7 changes: 7 additions & 0 deletions stdlib/test/builtin/test_string.mojo
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,12 @@ fn test_stringable() raises:
assert_equal("a string", str(String(AString())))


fn test_representable() raises:
assert_equal(repr(String("hello")), "'hello'")
assert_equal(repr(String(0)), "'0'")
assert_equal(repr(String("Some' quotes' ")), "'Some\\' quotes\\' '")


fn test_constructors() raises:
# Default construction
assert_equal(0, len(String()))
Expand Down Expand Up @@ -623,6 +629,7 @@ def main():
test_equality_operators()
test_add()
test_stringable()
test_representable()
test_string_join()
test_stringref()
test_stringref_from_dtypepointer()
Expand Down
2 changes: 1 addition & 1 deletion stdlib/test/collections/test_dict.mojo
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ def test_dict_string_representation_string_int():
some_dict._minimum_size_of_string_representation()
<= len(dict_as_string)
)
assert_equal(dict_as_string, "{a: 1, b: 2}")
assert_equal(dict_as_string, "{'a': 1, 'b': 2}")


def test_dict_string_representation_int_int():
Expand Down
9 changes: 3 additions & 6 deletions stdlib/test/collections/test_list.mojo
Original file line number Diff line number Diff line change
Expand Up @@ -580,13 +580,10 @@ def test_converting_list_to_string():
var my_list = List[Int](1, 2, 3)
assert_equal(__type_of(my_list).__str__(my_list), "[1, 2, 3]")

var my_list2 = List[SIMD[DType.int8, 2]](
SIMD[DType.int8, 2](1, 2), SIMD[DType.int8, 2](3, 4)
var my_list4 = List[String]("a", "b", "c", "foo")
assert_equal(
__type_of(my_list4).__str__(my_list4), "['a', 'b', 'c', 'foo']"
)
assert_equal(__type_of(my_list2).__str__(my_list2), "[[1, 2], [3, 4]]")

var my_list3 = List[Float64](1.0, 2.0, 3.0)
assert_equal(__type_of(my_list3).__str__(my_list3), "[1.0, 2.0, 3.0]")


def main():
Expand Down