Pourquoi je n'arrive pas à utiliser les flux standards en Rust

Le problème exposé dans ce sujet a été résolu.

Bonjour, Comme hobbyist, j’ai commencé à apprendre la programmation et depuis peu Rust, et pour ce faire, j’ai commencé l’implémentation du langage de programmation Monkey du livre Writing an interpreter in Go.

J’ai essayé d’implémenter le code du livre en Rust, donc je ne pense pas que ce j’ai fait soit vraiment idiomatique (et avoir utilisé du code unsafe n’a sûrement pas aidé). Comme ça a passé les tests, j’ai cherché à faire la prochaine étape : tester le programme avec les flux standards.

Bien que ça compilait, soit ça finissait en disant main thread panicked, non-valid utf-8, depuis WSL et Powershell, soit ça ne fonctionnait pas et je n’obtenais pas les sorties espérées, de fait je n’en obtenais pas du tout ! (les macros print! et println! ne semblent pas fonctionner).

J’obtiens les mêmes comportements sur le site web repl.it (excellent site par ailleurs).

Bien que je pensais avoir suivi la documentation (qui est très bien faite), il semblerait qu’il y ait une erreur, je me tourne donc vers vous pour m’aider à la trouver.

GitHub repo

repl.it result

J’insisterais sur le fait que je ne cherche pas une solution "prête à l’emploi".

Vous remerciant de votre lecture et de votre considération.

+0 -0

Bonjour,

Je n’ai pas eu le temps de regarder ton code source. Néanmoins, il faut noter que Rust a un type String qui vérifie systématiquement que les chaînes sont de l’UTF8 valide. Est-ce que ton découpage de l’entrée en token ne créerait pas des String dont le contenu utf8 est invalide (couper la chaîne au milieu des octets d’un codepoint Unicode ?).

Salut,

Je ne suis pas sûr de comprendre quel est ton problème. Je n’ai pas d’erreur sur le commit ce2e6a012d89c2a95b3af77cf29362874d49d763. Tu veux dire par exemple que print!("{}", PROMPT) n’affiche pas immédiatement le prompt ? C’est normal, comme précisé dans la doc. L’affichage sur la sortie standard est géré par un buffer, pour forcer l’affichage à un point donné du programme il faut utiliser io::stdout().flush()

En ajoutant cet appel, j’ai bien le prompt qui apparaît. Le programme semble tourner dans le vide si je presse Enter, mais comme il n’est pas fini c’est peut être normal ?

EDIT :

main thread panicked, non-valid utf-8, depuis WSL et Powershell

Comme précisé par mon voisin du dessus, Rust s’attend à lire de l’UTF8. Si ton shell est mal configuré, il envoie peut être du texte dans un encodage différent. Je ne connais pas Windows suffisamment pour t’aider plus.

+0 -0

@adri1 D’accord je vais rajouter le io::stdout().flush(). Et la configuration du terminal semble être donc un problème. @victorlevasseur Par contre pour le découpage de caractères, je ne savais pas, mais cela paraît possible. Après comme les tests ont passé et que je n’ai testé qu’avec des caractères ASCII (si j’ai bien compris l’UTF-8, ça ne fait qu’un octet le caractère), je ne comprendrais pas pourquoi. Il faudra que je vérifie.

Salut,

En jetant un œil rapide à ton code, j’ai l’impression que tu devrais lire la doc du type String et en particulier regarder les méthodes qui viennent de l’implémentation de Deref<Target=str>. Typiquement, il y a plusieurs endroits (notamment lexer.rs) où tu traites ton input comme des suites de bytes avec une syntaxe à la C qui prend un pointer vers des u8 pour ensuite recomposer des str avec

from_utf8(unsafe {from_raw_parts(input.0, input.1)})

Je ne sais pas quelle est la façon idiomatique de procéder en Go, mais ce n’est pas certainement pas une façon propre de procéder en Rust. Tu as intérêt à manipuler ton entrée comme des chaines UTF8 directement. Les seules bonnes raisons d’utiliser from_utf8 et/ou from_raw_parts est si tu écris une libraire de gestion d’encodages ou si tu dois t’interfacer avec du code qui est incapable de donner autre chose que des bytes…

Utiliser String et str te permet d’utiliser toutes les abstractions qui existent pour les manipuler et implémenter ton lexer. Si tout ce que tu veux traiter est une entrée en UTF8, il est absurde de passer par des tableaux de bytes. Les bons types à utiliser sont String et str. De manière générale, si tu utilises unsafe sans être vraiment sûr d’en avoir besoin (typiquement pour contourner une limitation du système de types de Rust), ça peut être un signe que tu n’utilises pas les bonnes abstractions. Surtout pour écrire un simple lexer…

EDIT : et si tu ne l’as pas encore fait, je te suggère de lire le Rust book. La documentation de la librairie standard est très bien faite mais difficile d’accès sans une certaine vue d’ensemble sur le langage. Le book est parfait pour acquérir cette vue d’ensemble !

+2 -0

J’ai eu un autre commentaire de ce genre sur Stack Overflow. J’avais du mal avec le borrow checker, et la passation de String à &str et inversement n’arrangeait rien. J’avais lu que la représentation des &str était composé de \*const u8 et d’un usize.

J’ai donc utilisé des pointeurs sur u8 pour passer les arguments aux fonctions. Je sais maintenant que c’est non idiomatique, et comme m’a été conseillé, je vais retravailler les fonctions pour accepter les Rc ou Arc (quelle est la différence ?).

Par rapport au Rust book, j’avais commencé à le lire, mais de peur de ne faire que lire et de ne rien à faire avec Rust, j’ai commencé un projet qui m’intéressait, alors évidemment il y a des couacs. Pour l’instant je me suis dépatouillé avec les héritages d’autres langages, et la doc, qui me semble très bonne (bien que je découvre encore la bonne façon d’utiliser une documentation).

Pour la façon idiomatique de procéder en Go, ça a l’air plus simple, mais comme il s’agit d’un langage de haut niveau, cela semble logique.

+0 -0

et comme m’a été conseillé, je vais retravailler les fonctions pour accepter les Rc ou Arc (quelle est la différence ?).

Je doute fortement que tu ais besoin ni de l’un ni de l’autre mais soit. Les Rc sont des pointeurs intelligents permettant de partager l'ownership (alors qu’une valeur "normale" en Rust n’a qu’un seul owner). C’est une façon assez cheap de pouvoir partager une référence entre plusieurs structures sans lier les lifetimes de ces structures. Une référence n’a pas le droit de vivre plus longtemps que la donnée référencée en Rust, donc si tu veux que plusieurs objets pointent vers la même donnée, tu dois t’assurer que la donnée vit au moins aussi longtemps que les objets ayant une référence. Si tu as un seul owner de la donnée en question, ça signifie que tous les objets ayant une référence vers cette donnée ont une durée de vie contrainte par celle de cet owner. Rc permet de s’affranchir de cette limite en permettant à plusieurs objets de posséder la donnée, et garantir que la donnée vivra tant qu’elle a au moins un owner encore en vie.

Un Arc est essentiellement la même chose, avec en plus la possibilité de partager l'ownership entre différent threads. Partout où tu peux utiliser un Rc, tu peux utiliser un Arc à la place. Ça a bien sûr un coût au runtime, et donc si tu ne comptes pas utiliser des threads, autant utiliser Rc.

Cela dit, vu ce que tu cherches à implémenter, je pense que tu peux t’en sortir simplement avec des String et des &str.

Par rapport au Rust book, j’avais commencé à le lire, mais de peur de ne faire que lire et de ne rien à faire avec Rust, j’ai commencé un projet qui m’intéressait, alors évidemment il y a des couacs.

Je pense que tu mets un peu la charrue avant les bœufs. Plutôt que t’attaquer directement à un projet aussi complexe, tu devrais plutôt essayer de faire des exercices plus simples. On ne peut pas se leurrer, Rust a un coût d’entrée plutôt important et se lancer tête baissée dans un projet relativement conséquent risque plus de te faire prendre de mauvaises habitudes.

Pour la façon idiomatique de procéder en Go, ça a l’air plus simple, mais comme il s’agit d’un langage de haut niveau, cela semble logique.

Ce n’est pas vraiment une question de "haut" vs "bas" niveau. Je dirais plutôt que Rust offre de nombreux niveaux d’abstractions qui n’existent pas en Go. Go est plus simple de part son modèle mémoire (avec GC) et des choix de design qui visent à rendre le langage minimal. Rust fait un peu le choix inverse en exposant des abstractions très diverses à l’utilisateur et un modèle mémoire (à ownership et lifetimes) moins coûteux au runtime mais plus contraignants pour le développeur. Ça a un coût d’entrée.

+0 -0

Je pense que tu mets un peu la charrue avant les bœufs. Plutôt que t’attaquer directement à un projet aussi complexe, tu devrais plutôt essayer de faire des exercices plus simples. On ne peut pas se leurrer, Rust a un coût d’entrée plutôt important et se lancer tête baissée dans un projet relativement conséquent risque plus de te faire prendre de mauvaises habitudes.

Ok, ça me semble être une bonne morale de l’histoire, je vais donc me concentrer sur l’apprentissage du langage pendant un temps, et quand j’en saurai assez me remettre sur le projet. Je tiens à vous remercier pour l’aide que vous m’avez fourni

Plop, juste une petite précision puisque je vois qu’il est question de chaînes de caractères en Go d’une part, et de livres qui utilisent Go comme support d’autre part.

D’abord à propos des conversions bytes vs. utf-8 : non, c’est pas idiomatique de traiter une entrée textuelle d’abord comme des bytes avant de faire des conversions soi-même en strings. Go pousse à mort l’utilisation d’Unicode (ce qui n’est pas surprenant quand on sait que le créateur d’Unicode est aussi derrière le design de Go…) : ces manipulations de bytes y sont douteuses, notamment parce qu’il propose nativement de manipuler des caractères unicode (runes), et que ses string sont des chaînes utf-8 immuables.

J’ai donc été voir ce qui était montré dans les deux bouquins, et bien que je trouve ceux-ci super, il faut avouer qu’il y a pas mal de choses à redire sur l’usage qui y est fait de Go : l’auteur a écrit son code en Go comme il aurait codé en Javascript. Sous prétexte qu’il ne "vise pas les meilleures performances possibles", il a défini des tas de trucs de façon relativement beotienne en Go. Par exemple, l’usage de chaînes de caractères à la place d’énumérations alors qu’il va faire des switch sur ces valeurs est un gâchis inexplicable des ressources, et quand on a un minimum l’habitude de Go, de C ou de C++, c’est un truc qui jure vraiment dans son code : ça sent un peu l’assemblage de carton et de scotch pour "faire le job". De la même manière, il aurait gagné à dire, tout simplement, que son langage acceptait une entrée en Unicode et travailler directement avec des rune Unicode : c’est ce qui est fait un peu partout en Go, y compris dans la bibliothèque standard, et ça me scie un peu qu’il ait estimé ne pas avoir le temps d’aller simplement jeter un oeil au code-source des packages qu’il utilise dans un soucis de cohérence alors qu’il a visiblement passé un paquet de temps à se renseigner sur l’écriture de tests, pour s’attarder dans ses livres sur une API dont une partie est devenue obsolète depuis…

Bref, c’est dommage parce qu’en dehors de ça, ces livres font un effort de vulgarisation et de pédagogie remarquable.

Maintenant, c’est typiquement le genre de code qui ne se traduit pas du tout bien de façon littérale de Go vers Rust. Et je suis d’accord avec @adri1 : ce projet me semble vachement ambitieux pour s’attaquer à Rust.

+1 -0
Connectez-vous pour pouvoir poster un message.
Connexion

Pas encore membre ?

Créez un compte en une minute pour profiter pleinement de toutes les fonctionnalités de Zeste de Savoir. Ici, tout est gratuit et sans publicité.
Créer un compte