krom_script 1.0.3
krom_script: ^1.0.3 copied to clipboard
A lightweight, embeddable scripting language for Dart applications.
Changelog #
1.0.3 #
Les erreurs d'exécution disent où (#24) #
Le lexer connaît la ligne de chaque jeton depuis toujours ; l'interprète, lui,
levait des Exception nues. undefined variable: total arrivait donc sans
la moindre position, et l'auteur n'avait qu'à relire son fichier. Seules les
erreurs de syntaxe étaient situées.
C'est pire pour une mini-app : le script exécuté est une concaténation, et c'est la position qui permet à l'hôte de retrouver le fichier d'origine. Sans elle, toute la chaîne de correspondance en aval reste sans objet.
Deux mécanismes, complémentaires :
- Les sites qui connaissent le nœud fautif le nomment exactement —
identifiant inconnu, propriété sur
null, affectation impossible, indexation, division par zéro, opérateur inconnu, arguments demap/filter/reduce/find/findIndex. Ligne et colonne. - Chaque statement sert de filet pour le reste : ce qu'aucun site ne nomme — une native qui lève, un appel sur ce qui n'est pas une fonction — reçoit au moins la ligne du statement en cours. L'imbrication est sans danger : la position la plus interne est posée en premier, les cadres extérieurs laissent passer une erreur déjà située.
Deux choses restent intactes : un KromResourceError (budget, délai) garde son
type, parce que ce n'est pas une faute du code exécuté ; et une erreur qui
portait déjà une position la conserve.
Statement et Expression déclarent désormais Token get token — les
sous-classes stockaient déjà ce jeton, l'exposer suffisait.
Visible pour l'appelant : le type change pour les erreurs qui étaient des
Exception nues, et devient KromRuntimeError. Un hôte qui filtrait sur le
texte du message n'est pas affecté — le message est inchangé, la position s'y
ajoute en suffixe.
1.0.2 #
Six correctifs de justesse et de sûreté (#10 à #15). Le numéro est un correctif, mais plusieurs changements sont visibles : lire la section Migration avant de monter de version.
Analyse — sources tronquées rejetées (#12) #
Deux sources tronquées passaient la validation à l'analyse :
- Chaîne non terminée : le lexeur signale désormais
unterminated string literal, à la ligne et à la colonne du guillemet ouvrant, pour les deux styles de guillemets ("et'). Un guillemet échappé (\") ne ferme plus la chaîne par accident. - Bloc non fermé : un bloc qui atteint la fin du fichier sans son
}produitexpected TokenType.rbrace, got TokenType.eof, exactement comme le font déjà(et[.
Les erreurs lexicales remontent par la liste Parser.errors() existante — pas
de seconde API d'erreurs : KromScript.run, KromEngine.load et tout appel à
Parser(Lexer(src)).parseProgram() les voient sans changement côté appelant.
Impact — des sources jusqu'ici acceptées puis exécutées sont maintenant rejetées à l'analyse. C'est l'intention : elles ne pouvaient s'exécuter qu'avec une sémantique différente de celle écrite.
Ordre — plus de coercition des opérandes non numériques (#15) #
-
Opérateurs d'ordre (
<,>,<=,>=) — ils ne coercent plus les opérandes non numériques vers0.0(#15).null < 5répondaittrue: dans un moteur de règles,age < 18sur un champ non rempli déclenchait une validation qui devait rester dormante.L'ordre n'est désormais défini que sur les nombres et les chaînes qui se parsent comme des nombres. Tout autre opérande —
null, booléen, map, liste, fonction, chaîne non numérique — rend la comparaison indéfinie, et une comparaison indéfinie vautfalse, des deux côtés et pour les quatre opérateurs. Sur une donnée absente,age < 18etage >= 18sont donc tous les deux faux : aucune règle d'ordre ne se déclenche sur une valeur qu'on n'a pas.Une chaîne numérique continue de se comparer numériquement (
"10" > 5vaut toujourstrue). L'égalité (==/!=), l'arithmétique etsort()sont inchangées.
Résolution de propriété — null et absent lus comme null (#10, #11) #
-
Propriété nulle ou absente —
m.videetm.absentvalent désormaisnullau lieu de levercannot access property … : object must implement KromBindable(#10). La résolution répondait sur la valeur : unnulllégitime était indistinguable d'une propriété introuvable et repartait vers le chemin réflexif, qui lève sur une map. Elle répond maintenant sur la présence —KromRuntimeType.hasProperty, que le handler des maps résout parcontainsKey. Comme ailleurs dans le langage, une propriété absente vautnull: fausse en condition, rattrapable par?:. -
Objets liés (
KromBindable) — lire une propriété déclarée qui vautnullrenvoyait unNativeFunctionValue, l'emballage de méthode. Cet objet est vrai en condition et se retrouvait tel quel dans les données de l'hôte — une valeur calculée a écrit une fonction dans un corps JSON envoyé à un serveur (#10). L'emballage n'est plus construit qu'en position d'appel (p.methode()) ; une lecture simple vautnull.Obs(null).valuevaut doncnull, etRxList().firstsur une liste vide aussi. -
?.ne résolvait que sur les maps (#11) :l?.lengthvalaitnulllà oùl.lengthvalait3, silencieusement.?.effectue désormais exactement la même résolution que.— maps, listes, chaînes, objets liés, méthodes comprises — et ne court-circuite que sur un receveurnull.a?.beta.bne diffèrent plus que sur ce point.
Garde d'exécution — appliquée par défaut (#13) #
- La garde d'exécution s'applique enfin à
KromScript.ExecutionLimitsse documentait « sûr par défaut » mais n'était câblée que dansKSEngine:KromScript.eval,KromScript.runetKromScript.builder(...).execute()s'exécutaient sans budget d'opérations ni deadline, si bien quewhile (true) { }figeait l'hôte. Ces trois points d'entrée tournent désormais sousExecutionLimits()(10 000 000 opérations, 1 s). - Nouveau
KromScriptBuilder.withLimits(ExecutionLimits)et paramètre nommélimitssurKromScript.run/KromScript.eval: le type documenté est maintenant atteignable depuis l'API publique. withMaxOperations()etwithTimeout()restent valables — ils surchargent désormais la borne correspondante au lieu d'être le seul moyen d'en avoir une, et réarment la garde si elle avait été désactivée.- Une seule implémentation de l'application des bornes
(
ExecutionLimits.applyTo), partagée parKromScriptetKSEngine, pour que les deux chemins ne puissent plus diverger.
Migration : un script qui bouclait sans fin échoue maintenant avec
KromResourceError au lieu de tourner indéfiniment. Un script légitime reste
très en deçà des bornes. Pour du code de confiance qui doit tourner sans
limite, l'option est explicite : withLimits(ExecutionLimits.unlimited) (ou
limits: ExecutionLimits.unlimited).
Représentation numérique — une seule règle, jusqu'au JSON (#14) #
-
Une seule représentation numérique à la frontière hôte (#14). Le type Dart d'un nombre ne dépend plus du chemin par lequel le script l'a atteint : un nombre entier est un
int, un nombre fractionnaire undouble. Avant,m.netm["n"]rendaient3(donnée hôte intacte) alors que1 + 1etl.lengthrendaient2.0et3.0(chemin calculé / réflexif) — le même champ se sérialisait donc en3ou en3.0selon qu'un script l'avait touché ou non.La règle est énoncée une seule fois, dans
lib/src/runtime/numbers.dart(kromCanonicalNumber/kromCanonicalValue, tous deux exportés), et appliquée à chaque passage : variables hôte entrantes,ScriptResult.value,KSEngineResult.value,KSEngine.invokeSync/getVariable/reactiveState, et les deux sens deKromBindable.kromDisplayrend désormais les nombres via cette même règle.L'arithmétique est inchangée : l'interpréteur calcule toujours en
double, la division, le modulo et les débordements se comportent à l'identique. La sortie deprint()et l'interpolation sont inchangées elles aussi (print(1 + 1)affiche toujours2). -
jsonStringifysuit la même règle —jsonStringify({ n: 3 })rend désormais{"n":3}au lieu de{"n":3.0}, récursivement dans les listes et les maps. Cela révoque la décision de 1.0.0 qui gardait l'encodage JSON de Dart au nom du « format réseau préservé ».Motif : ce moteur est l'un de trois jumeaux (Go côté serveur, TypeScript côté web, Dart côté mobile) qui doivent produire des corps JSON identiques octet pour octet. Go et TypeScript sérialisent
2, pas2.0: le format réseau que le canon spécifie est bien2, et c'était donc la divergence, non la préservation.jsonParseest déjà canonique (jsonDecoderend unintpour un littéral entier), doncjsonStringify(jsonParse(s))fait maintenant un aller-retour stable.
Migration #
#15 — opérateurs d'ordre. Visible pour un script qui s'appuyait sur l'ancienne coercition :
null < 5,"abc" < 5,true < 5,[…] < 5,{…} < 5passent detrueàfalse(et de même dans le sens miroir,5 > null, etc.).null <= nullet"abc" <= "abc"passent detrueàfalse: deux opérandes non ordonnables ne sont pas « égaux » au sens de l'ordre. Utiliser==pour tester l'égalité.- L'identité
!(a < b) == (a >= b)n'est plus garantie quand un opérande n'est pas ordonnable — comme pourNaNen IEEE-754 et en JavaScript.
Un script qui doit distinguer « absent » de « comparé » teste explicitement
x == null (ou x ?: valeurParDéfaut) avant la comparaison.
#10 / #11 — résolution de propriété.
- Un code qui s'appuyait sur la levée d'exception pour détecter une propriété
manquante reçoit maintenant
null. C'est l'objet du correctif, mais le changement est visible : testerx == null, oux ?: défaut. ?.sur un receveur qui n'expose rien (nombre, booléen, objet hôte nonKromBindable) lève désormais comme., au lieu de valoir silencieusementnull. Seul un receveurnullcourt-circuite.- Une méthode d'objet lié lue comme valeur (
p.methodesans appel, pour la passer en callback) vautnull: l'appeler directement, ou l'envelopper dans une lambdafn(x) { p.methode(x) }. - Le contournement qui réécrivait
bloc.champenbloc?.champpour obtenir unnulln'a plus lieu d'être.
#14 — représentation numérique.
- Un appelant qui écrivait
result.value as doubledoit écrire(result.value as num).toDouble()— de même pour les arguments reçus par unKromBindable.callMethod, qui arrivent maintenant sous forme canonique. Les comparaisons de valeur (result.value == 2.0) restent vraies : en Dart2 == 2.0. - Qui analyse la sortie de
jsonStringifyen attendant2.0doit accepter2. Un consommateur JSON standard n'est pas concerné :2et2.0s'y décodent sur le même nombre.
1.0.1 #
Qualité du paquet (aucun changement d'API ni de comportement) :
- Code entièrement conforme à
dart format. - Suppression du code mort signalé par l'analyseur (imports, champs et
variables locales inutilisés) dans
lib/,bin/ettest/. pathdéclaré endev_dependency(utilisé par les tests).
1.0.0 #
Première version stable. API figée, sûreté par défaut et un langage nettement plus expressif — sur Dart, Kotlin, Go et TypeScript.
Langage #
- Chaînes
else if— plus besoin deifimbriqués. - Affectations composées :
+=,-=,*=,/=. - Opérateur ternaire :
cond ? a : b. for-insur les maps — itère sur les clés.- Natif
range()pour les boucles numériques. - Commentaires bloc
/* ... */. - Affichage des entiers sans
.0:4au lieu de4.0, appliqué àtoString, l'interpolation,+,print,joinet aux clés de map (jsonStringifyreste inchangé — format réseau préservé).
Sûreté #
- Garde d'exécution activée par défaut : budget d'opérations + deadline,
pour qu'un script tiers ne puisse pas figer l'hôte.
ExecutionLimits.unlimitedreste disponible pour du code de confiance. print()est aussi routé vers la sortie standard, en plus dedeveloper.log.
Optimiseur & introspection hôte #
- Correction de plusieurs bugs de fausse suppression / transformation
(propagation de constantes dans les boucles et les closures, inlining
capturant un paramètre, round-trip du
elseaprès optimisation). - Les hooks de cycle de vie (
onInit/onShow/onHide/onDispose) sont préservés au tree-shaking. - Introspection réactive :
reactiveState()/setReactiveValue()pour lire et piloter l'état,lastOpsUsed/maxOperationspour instrumenter le budget d'opérations.
Migration #
Rien à migrer depuis 0.2.0 : ces ajouts sont rétrocompatibles.
0.2.0 #
Breaking Changes #
- Binding API: Objects passed to
.bind()must now implementKromBindableinterface- This enables support for iOS, Android, and Web platforms
- Removed dependency on
dart:mirrorswhich is not available on these platforms
Migration Guide #
Before (0.1.x):
// Automatic reflection - no longer supported
class User {
final String name;
User(this.name);
String greet() => "Hello, $name!";
}
final result = KromScript.builder('user.greet()').bind('user', User('Alice')).execute();
After (0.2.0):
class User implements KromBindable {
final String name;
User(this.name);
String greet() => "Hello, $name!";
@override
Object? getProperty(String name) => name == 'name' ? this.name : null;
@override
Object? callMethod(String name, List<Object?> args) {
if (name == 'greet') return greet();
return null;
}
}
final result = KromScript.builder('user.greet()').bind('user', User('Alice')).execute();
0.1.1 #
- Fixed string template parsing for complex expressions
- Improved error messages for undefined variables
- Performance optimizations for repeated script execution
0.1.0 #
- Added
for...inloop support for iterating over arrays - Added
whileloop support for conditional iteration - Added user-defined functions with
fnkeyword - Enhanced array operations:
map,filter,reduce,find,findIndex - Added operation limit and timeout for script execution
- Performance improvements via AST caching
0.0.1 #
- 🎉 Initial release
- Variables and expressions
- Null-safety operators (
?.,?:) - Control flow (if/else)
- Native functions:
- String manipulation
- Math operations
- Crypto (MD5, SHA1, SHA256)
- JSON parsing/stringify
- Base64/URL encoding
- Array operations
- Custom function registration
- Builder pattern API