From 0aec1286fa3537031b2c2b9a20b5fe67fc7d6e1f Mon Sep 17 00:00:00 2001 From: Steven de Oliveira Date: Tue, 23 Dec 2025 11:55:24 +0100 Subject: [PATCH 1/9] Re-adding file used in README --- doc/architecture.png | Bin 0 -> 63846 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 doc/architecture.png diff --git a/doc/architecture.png b/doc/architecture.png new file mode 100644 index 0000000000000000000000000000000000000000..20c96e2f8e8f49731791c1ec313df875206ec756 GIT binary patch literal 63846 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 literal 0 HcmV?d00001 From 738f0bfd1850b179ddbacb817c32ed1a799c0bd2 Mon Sep 17 00:00:00 2001 From: Steven de Oliveira Date: Tue, 23 Dec 2025 13:22:49 +0100 Subject: [PATCH 2/9] More details --- doc/mlang.md | 76 +++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 72 insertions(+), 4 deletions(-) diff --git a/doc/mlang.md b/doc/mlang.md index bdf599e54..3e8b796be 100644 --- a/doc/mlang.md +++ b/doc/mlang.md @@ -2,6 +2,63 @@ # Le compilateur MLang +## Installer + +Mlang est implanté en OCaml. L'utilisation du gestionnaire de paquets OCaml `opam` +est fortement recommandée. +Vous pouvez l'installer via votre gestionnaire de paquet préféré s'il distribue +`opam`, ou bien en vous referant à la [documentation d'opam](https://opam.ocaml.org/doc/Install.html). +Mlang a également quelques autres dépendances, dont une vers la librairie de calcul +de flotants MPFR. Si vous êtes sous Debian, vous pouvez simplement utiliser la commande +suivante pour installer toutes les dépendances externes à OCaml : + +``` +$ sudo apt install \ + libgmp-dev \ + libmpfr-dev \ + git \ + patch \ + unzip \ + bubblewrap \ + bzip2 \ + opam +``` + +Si vous n'avez jamais utilisé `opam`, commencez par lancer : + +``` +$ opam init +$ opam update +``` + +Enfin, vous pouvre initialiser le projet `mlang` avec + +``` +$ make init +``` + +**Note pour les utilisateurs d'opam confirmés** : la commande `make init` crée +un switch local où seront installées les dépendances OCaml de mlang. + +Cette commande initialise le dossier `ir-calcul` dans lequel sont poussés +le code de calcul primitif de l'impot sur le revenu. + +Si besoin, la commande +``` +$ make deps +``` +réinstallera les dépendances OCaml et mettra à jour le dossier `ir-calcul`. + +Une fois compilé, vous pouvez soit appeler mlang via la commande : +``` +$ opam exec -- mlang +``` +tant que vous êtes dans le dossier depuis lequel vous avez compilé, soit +l'installer localement avec la commande : +``` +opam install ./mlang.opam +``` + ## Utiliser MLang Le binaire `mlang` prend en argument le fichier *M* à exécuter. @@ -10,8 +67,9 @@ traitement sera équivalent au traitement d'un seul et même fichier dans lequel serait concatené le contenu de chaque fichier. %% -Les deux options principales sont : +Les options principales sont : * `-A`: le nom de l'application à traiter; +* `-b`: le mode d'utilisation, ou `backend`; * `--mpp_function`: le nom de la fonction principale à traiter. ### Mode interpreteur @@ -49,7 +107,8 @@ NB: le dossier `output` doit avoir été créé en amont. ### Options DGFiP -Les options DGFiP sont à usage interne. +Les options DGFiP sont à usage interne. Elles sont spécifiées dans +l'option `--dgfip_options`. ``` -b VAL @@ -94,11 +153,17 @@ Les options DGFiP sont à usage interne. -Z Colored output in chainings ``` -## Comportement de mlang +## Comportement de Mlang + +Le compilateur Mlang effectue son traitement en quatre étapes : +* la traduction dans un format abstrait interne; +* un pré-traitement pour le simplifier; +* une vérification pour analyser la cohérence du code; +* le traitement du code M, que ce soit son interprétation ou sa compilation. ### Traduction -% A faire : traduction du M en M_AST +Le langage M est parsé selon les règles spécifiées dans {ref}`syntax`. ### Pré-traitement @@ -293,6 +358,9 @@ BZ = BZ + B10; ### Vérification de cohérence +De nombreuses constructions sont valides à la traduction, mais brisent +certains invariants nécessaires à la bonne exécution du code. + % A faire : documentation de la verification ### Traitement From 2d081d2e890a6b4e97868c11b48fb0e8120ca832 Mon Sep 17 00:00:00 2001 From: Steven de Oliveira Date: Tue, 23 Dec 2025 13:42:23 +0100 Subject: [PATCH 3/9] More details on doc --- doc/mlang.md | 31 ++++++++++++++++++++++++------- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/doc/mlang.md b/doc/mlang.md index 3e8b796be..cb481d5f5 100644 --- a/doc/mlang.md +++ b/doc/mlang.md @@ -155,20 +155,27 @@ l'option `--dgfip_options`. ## Comportement de Mlang +% A faire : des modules sont référencés plus bas. +% Ca serait bien d'avoir des liens vers les docs de ces modules. + Le compilateur Mlang effectue son traitement en quatre étapes : * la traduction dans un format abstrait interne; * un pré-traitement pour le simplifier; * une vérification pour analyser la cohérence du code; * le traitement du code M, que ce soit son interprétation ou sa compilation. +Le module `Driver` (et plus précisément la fonction `Driver.main`) correspond au +point d'entrée de mlang. + ### Traduction Le langage M est parsé selon les règles spécifiées dans {ref}`syntax`. +Elle est effecuée par les modules `Mparser`, `Mlexer` et `Parse_utils`. ### Pré-traitement -Le prétraitement est une opération purement syntaxique. -Son but est triple : +Le prétraitement est une opération purement syntaxique. Elle est effectuée par +les modules `Expander` et `Mir`. Son but est triple : - éliminer les constructions relatives aux applications non-sélectionnées ; - remplacer les constantes par leur valeur numérique ; - remplacer les expressions numériques débutant par `somme` avec des @@ -354,21 +361,31 @@ BX = BX + B09; BX = BX + B10; BZ = BZ + B09; BZ = BZ + B10; -```` +``` ### Vérification de cohérence De nombreuses constructions sont valides à la traduction, mais brisent -certains invariants nécessaires à la bonne exécution du code. +certains invariants nécessaires à la bonne exécution du code : double +déclaration d'attributs, nom de variable déjà utilisé, variable mal +typée... +L'ensemble de ces vérifications est accessible dans le module +`Validator` du frontend. Le sous module `Validator.Err` définit l'ensemble +des erreurs levées par cette étape de vérification. -% A faire : documentation de la verification +**NB** : seule la première erreur rencontrée par le validateur est levée. ### Traitement #### Interpreteur -Lecture du fichier IRJ et interpretation du code. +L'interpréteur utilise la représentation interne du code M +pour lancer le calcul à partir d'un fichier IRJ +(voir {ref}`syntax_irj`). +L'interprétation est effecuée par le module `Test_interpreter`. #### Transpilation -Ecriture du code C équivalent au code M. +La transpilation traduit le code dans le langage spécifié (en 2025, seul le C +est transpilable). La transpilation est effecutée dans le module +`Bir_to_dgfip_c`. From 2a39eee8e6544539bb22d71cd192de18ef4110a1 Mon Sep 17 00:00:00 2001 From: Steven de Oliveira Date: Tue, 23 Dec 2025 16:04:57 +0100 Subject: [PATCH 4/9] Rework code documentation --- src/mlang/index.mld | 138 +++++++++++++++++++++----------------------- src/mlang/mlang.ml | 22 +++++++ 2 files changed, 87 insertions(+), 73 deletions(-) create mode 100644 src/mlang/mlang.ml diff --git a/src/mlang/index.mld b/src/mlang/index.mld index 2c2be98ac..8d409e935 100644 --- a/src/mlang/index.mld +++ b/src/mlang/index.mld @@ -3,96 +3,88 @@ The Mlang compiler has a traditionnal architecture consisting of various intermediate representations going from the source code to the target backend. -{1 M Frontend} +{1 Frontend} -First, the source code is parsed according to the Menhir grammar specified in -{!module: Mlang.Mparser}. The grammar is not exactly LR(1) so we rely on {!module: Mlang.Parse_utils} -to backtrack, especially on symbol parsing. The target intermediate representation -is {!module: Mlang.Mast}, which is very close to the concrete syntax and can be -printed using {!module: Mlang.Format_mast}. +First, the source code is parsed according to the Menhir grammar specified in {!module: Mlang.M_frontend.Mparser}. +The grammar is not exactly LR(1) so we rely on {!module: Mlang.M_frontend.Parse_utils} to backtrack, especially on symbol parsing. The target intermediate representation is {!module: Mlang.M_frontend.Mast}, which is very close to the concrete syntax and can be printed using {!module: Mlang.M_frontend.Format_mast}. +The frontend also handles ast expansion with {!module: Mlang.M_frontend.Expander} and validation with {!module: Mlang.M_frontend.Validator}. -{!modules: Mlang.Mast Mlang.Format_mast Mlang.Mparser Mlang.Parse_utils } +{!modules: + Mlang.M_frontend.Expander + Mlang.M_frontend.Mast + Mlang.M_frontend.Mlexer + Mlang.M_frontend.Mparser + Mlang.M_frontend.Parse_utils + Mlang.M_frontend.Validator } -{1 M Intermediate representation} +{1 Intermediate Representation} The M language has a lot of weird syntactic sugars and constructs linked to its -usage inside multiple DGFiP applications. {!module: Mlang.Mast_to_mir } extracts from the -AST the computational core corresponding to a DGFiP application into the M Variable -Graph ({!module: Mlang.Mir}), which consists basically of a flat map of all the definitions of +usage inside multiple DGFiP applications. {!module: Mlang.M_frontend.Mast_to_mir} extracts from the AST of {!module: Mlang.M_frontend.Mast} the computational core corresponding to a DGFiP application into the M Variable Graph ({!module: Mlang.M_ir.Mir}), which consists basically of a flat map of all the definitions of the variables used in the application. The type system of M is very primitive, -and basically all programs typecheck ; however {!module: Mlang.Mir_typechecker} provides a top-down typechecking algorithm to split simple variables from tables. +and basically all programs typecheck ; however {!module: Mlang.M_frontend.Validator} provides a top-down typechecking algorithm to split simple variables from tables. +{!modules: + Mlang.M_ir.Com + Mlang.M_ir.Format_mir + Mlang.M_ir.Mir + Mlang.M_ir.Mir_interpreter + Mlang.M_ir.Mir_number + Mlang.M_ir.Mir_roundops } -At this point, the {!module: Mlang.Mir_dependency_graph} modules interprets the MIR as a first-class -graph and computes various reachability and cycle analysis in order to determine -the computational flow inside the program. This dependency order is encapsulated -with the program in {!module: Mlang.Mir_interface}. -Some typechecking and macro expansion is performed -by {!module: Mlang.Mir_typechecker}. +{1 Testing} -{!modules: Mlang.Mir Mlang.Mast_to_mir Mlang.Format_mir Mlang.Mir_typechecker Mlang.Mir_interface Mlang.Mir_dependency_graph } - -{1 M++ Frontend } - -The M code can be called several times through a sort of driver that saves -some variables between calls. This driver is encoded in the M++ domain-specific -language that Mlang handles together with the M. M++ is parsed with -{!module: Mlang.Mpp_parser} - -{!modules: Mlang.Mpp_parser Mlang.Mpp_ast} - -{1 M++ Intermediate representation } - -From the M++ AST, {!module: Mlang.Mpp_frontend} translates to {!module: Mlang.Mpp_ir} -by eliminating some syntactic sugars. - -{!modules: Mlang.Mpp_frontend Mlang.Mpp_ir Mlang.Mpp_format} - -{1 M/M++ Common backend intermediate representation} - -The module {!module: Mlang.Mpp_ir_to_bir} performs the inlining of all the M -calls in the M++ programs and yields a single program in a new common intermediate -representation, {!module: Mlang.Bir}. Inputs and outputs for this representation -can be specified using {!module: Mlang.Bir_interface}. - -The BIR representation is equipped with a fully fledged interpreter, -{!module: Mlang.Bir_interpreter}. The interpreter is instrumented for code coverage -via {!module: Mlang.Bir_instrumentation}, and can be parametrized via multiple -sorts of custom floating point values for precision testing ({!module: Mlang.Bir_number}). - -{!modules: Mlang.Bir_instrumentation Mlang.Bir_interface Mlang.Bir_interpreter Mlang.Bir_number Mlang.Bir Mlang.Format_bir} - -{1 Optimizations } - -While BIR is an AST-based representation, {!module: Mlang.Oir} defines a dual -of BIR in a control-flow-graph (CFG) for. You can go back and forth between -those two implementations using {!module: Mpp.Bir_to_oir}. - -OIR is the right place to perform some basic program optimizations. {!module: Mlang.Inlining} -expands some of the small rules into their full expressions, while -{!module: Mlang.Partial_evaluation} and {!module: Mlang.Dead_code_removal} simplify the -program depending on the inputs and outputs. The main optimizing loop is -implemented in {!module: Mlang.Oir_optimizations}. - -{!modules: Mlang.Oir_optimizations Mlang.Inlining Mlang.Partial_evaluation Mlang.Dead_code_removal} - -{1 Testing M and M++ programs } - -Mlang comes with a testing framework for M and M++ programs that is based on +Mlang comes with a testing framework for M programs that is based on the test format used by the DGFiP. The test files are parsed with -{!module: Mlang.Test_parser} into {!module: Mlang.Test_ast}. Then, single -or batch testing can be performed using {!module: Mlang.Test_interpreter}. +{!module: Mlang.Irj_utils.Irj_file}. Then, single +or batch testing can be performed using {!module: Mlang.Irj_utils.Test_interpreter}. -{!modules: Mlang.Test_ast Mlang.Test_interpreter Mlang.Test_parser} +{!modules: + Mlang.Irj_utils.Irj_ast + Mlang.Irj_utils.Irj_file + Mlang.Irj_utils.Irj_lexer + Mlang.Irj_utils.Irj_parser + Mlang.Irj_utils.ParserMessages + Mlang.Irj_utils.Test_interpreter } -{1 Compiling M and M++ programs} +{1 Compiling} M/M++ programs can be compiled to other programming languages using several backends that take BIR and produce source code files in their respective languages. -{!modules: Mlang.Bir_to_python Mlang.Bir_to_c Mlang.Bir_to_dgfip_c} +{!modules: + Mlang.Backend_compilers.Bir_to_dgfip_c + Mlang.Backend_compilers.DecoupledExpr + Mlang.Backend_compilers.Dgfip_compir_files + Mlang.Backend_compilers.Dgfip_gen_files + Mlang.Backend_compilers.Dgfip_varid + Mlang.Backend_compilers.Prelude + } {1 Utils } -{!modules: Mlang.Cli Mlang.Errors Mlang.Pos} +{!modules: + Mlang.Utils.CharMap + Mlang.Utils.Cli + Mlang.Utils.Config + Mlang.Utils.Dgfip_m + Mlang.Utils.Dgfip_options + Mlang.Utils.Dict + Mlang.Utils.Errors + Mlang.Utils.IntMap + Mlang.Utils.IntSet + Mlang.Utils.IntSetMap + Mlang.Utils.MapExt + Mlang.Utils.Pos + Mlang.Utils.Pp + Mlang.Utils.SetExt + Mlang.Utils.SetSetExt + Mlang.Utils.Sorting + Mlang.Utils.StrMap + Mlang.Utils.StrSet + Mlang.Utils.StrSetMap + Mlang.Utils.StrSetSet + Mlang.Utils.Strings + Mlang.Utils.TopologicalSorting + } diff --git a/src/mlang/mlang.ml b/src/mlang/mlang.ml new file mode 100644 index 000000000..44622282d --- /dev/null +++ b/src/mlang/mlang.ml @@ -0,0 +1,22 @@ +(* Copyright (C) 2019-2021 Inria, contributor: Denis Merigoux + + + This program is free software: you can redistribute it and/or modify it under + the terms of the GNU General Public License as published by the Free Software + Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT + ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public License along with + this program. If not, see . *) + +module Backend_compilers = Backend_compilers +module Driver = Driver +module Irj_utils = Irj_utils +module M_ir = M_ir +module M_frontend = M_frontend +module Utils = Utils From 47efc33477d4f125317e149639333df1fd1069ca Mon Sep 17 00:00:00 2001 From: Steven de Oliveira Date: Tue, 6 Jan 2026 13:27:28 +0100 Subject: [PATCH 5/9] =?UTF-8?q?R=C3=A9f=C3=A9rencement=20de=20la=20documen?= =?UTF-8?q?tation=20d=C3=A9veloppeur=20dans=20la=20documentation=20mlang?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Makefile | 15 +-------------- doc/conf.py | 5 ++++- doc/index.md | 3 ++- makefiles/mlang.mk | 29 +++++++++++++++++++++++++++-- 4 files changed, 34 insertions(+), 18 deletions(-) diff --git a/Makefile b/Makefile index 52a0a137b..3adc2622e 100644 --- a/Makefile +++ b/Makefile @@ -37,18 +37,5 @@ all: FORCE quick_test tests test_dgfip_c_backend clean: FORCE remise_a_zero_versionnage $(call make_in,$(DGFIP_DIR),clean_backend_all) rm -f doc/doc.html + rm -rf examples/doc dune clean - -doc-deps: FORCE - python3 -m venv .venv - .venv/bin/pip install sphinx myst-parser - -sphinx-doc: FORCE - @command -v .venv/bin/sphinx-build >/dev/null 2>&1 || \ - { echo "Pour construire la documentation, vous avez besoin de sphinx-build avec \ - l'extension 'myst-parser'. Lancez `make doc-deps`."; exit 1; } - rm -rf _build/default/doc/* - cp -rf doc/* _build/default/doc/ - mkdir -p examples/doc - .venv/bin/sphinx-build -M html _build/default/doc/ examples/doc - .venv/bin/sphinx-build -M latexpdf _build/default/doc/ examples/doc diff --git a/doc/conf.py b/doc/conf.py index 5b231de11..ef7f41d8f 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -24,4 +24,7 @@ # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output html_theme = 'alabaster' -html_static_path = ['_static'] +html_static_path = ['_static','_static/dev'] + +# When building, we move the dev documentation in this 'dev' folder. +html_extra_path = ['_static/dev'] diff --git a/doc/index.md b/doc/index.md index d3cd2fb4b..f0c371bc8 100644 --- a/doc/index.md +++ b/doc/index.md @@ -34,5 +34,6 @@ exemples/valeurs exemples/fonctions ``` +## Documentation développeur - +- Index diff --git a/makefiles/mlang.mk b/makefiles/mlang.mk index 26cd49d47..a1277b852 100644 --- a/makefiles/mlang.mk +++ b/makefiles/mlang.mk @@ -124,11 +124,36 @@ test_irj: FORCE build-dev # Doc ################################################## -doc: FORCE build +TARGET_DIR_SPHINX_DOC_SRC:= _build/default/sphinx-doc-src +TARGET_DIR_DOC_BUILD:= _build/default/full-doc + +doc-deps: FORCE + python3 -m venv .venv + .venv/bin/pip install sphinx myst-parser + +sphinx-doc: FORCE build dev-doc + @command -v .venv/bin/sphinx-build >/dev/null 2>&1 || \ + { echo "Pour construire la documentation, vous avez besoin de sphinx-build avec \ + l'extension 'myst-parser'. Lancez `make doc-deps`."; exit 1; } + rm -rf $(TARGET_DIR_SPHINX_DOC_SRC)/* + cp -r doc $(TARGET_DIR_SPHINX_DOC_SRC) + mkdir -p $(TARGET_DIR_DOC_BUILD)/html/_static/dev + cp -r $(shell pwd)/_build/default/_doc/_html $(TARGET_DIR_SPHINX_DOC_SRC)/_static/dev + .venv/bin/sphinx-build -M html $(TARGET_DIR_SPHINX_DOC_SRC) $(TARGET_DIR_DOC_BUILD) + .venv/bin/sphinx-build -M latexpdf $(TARGET_DIR_SPHINX_DOC_SRC) $(TARGET_DIR_DOC_BUILD) + +dev-doc: FORCE build ifeq ($(call is_in,),) $(call make_in,,$@) else dune build @doc - ln -fs $(shell pwd)/_build/default/_doc/_html/index.html doc/doc.html endif +doc: FORCE build dev-doc sphinx-doc +ifeq ($(call is_in,),) + $(call make_in,,$@) +else + rm -rf examples/doc + mkdir -p examples/doc + cp -r $(TARGET_DIR_DOC_BUILD)/* examples/doc +endif From fa2985841fabc6c9f3a4384154ff599e00fb83e8 Mon Sep 17 00:00:00 2001 From: Steven de Oliveira Date: Tue, 6 Jan 2026 13:28:56 +0100 Subject: [PATCH 6/9] =?UTF-8?q?D=C3=A9ploiement=20documentation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/publish_doc.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/publish_doc.yml b/.github/workflows/publish_doc.yml index 716fcc165..40702611c 100644 --- a/.github/workflows/publish_doc.yml +++ b/.github/workflows/publish_doc.yml @@ -44,11 +44,11 @@ jobs: - name: Build run: | eval $(opam env) - make build-doc + make doc - name: Deploy uses: peaceiris/actions-gh-pages@v3 # if: ${{ github.ref == 'refs/heads/main' }} with: github_token: ${{ secrets.GITHUB_TOKEN }} - publish_dir: _build/default/_doc/_html/ + publish_dir: _build/default/full-doc/html/ From a71cb2b87377d1d2b405413f489397b59d16827b Mon Sep 17 00:00:00 2001 From: Steven de Oliveira Date: Tue, 6 Jan 2026 14:17:06 +0100 Subject: [PATCH 7/9] Doc minimale pour irj_checker --- src/irj_checker/dune | 4 ++++ src/irj_checker/index.mld | 31 +++++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+) create mode 100644 src/irj_checker/index.mld diff --git a/src/irj_checker/dune b/src/irj_checker/dune index dd5165adc..10b783929 100644 --- a/src/irj_checker/dune +++ b/src/irj_checker/dune @@ -10,3 +10,7 @@ (package irj_checker) (public_name irj_checker) (libraries mlang cmdliner dune-build-info)) + +(documentation + (package irj_checker) + (mld_files ("index"))) diff --git a/src/irj_checker/index.mld b/src/irj_checker/index.mld new file mode 100644 index 000000000..a38df7146 --- /dev/null +++ b/src/irj_checker/index.mld @@ -0,0 +1,31 @@ +{0 Irj checker} + +The Irj_checker Module is a simple entry point to use the Mlang IRJ file +parser in order to perform syntactic checks on test files or produce other +IR test formats. + +{1 Usage} + irj_checker \[--message-format=VAL\] \[--validation-mode=VAL\] \[OPTION\]… + \[FILE\] \[TARGET\] + +{1 Arguments} + - FILE : Test file (usually with the .irj extension) + + - TARGET (absent=none) : + Transformation target, among the following list: none (only checks + test syntax), pasp (API PAS-CALC for primitive computation + resources), pasc (API PAS-CALC for corrective computation + resources). + +{1 Options} + - -m VAL, --message-format=VAL (absent=human) : + Selects the format of error and warning messages emitted by the + compiler. If set to human, the messages will be nicely displayed + and meant to be read by a human. If set to gnu, the messages will + be rendered according to the GNU coding standards. + + - -v VAL, --validation-mode=VAL (absent=strict) : + Select the validation criteria. If set to strict, the whole grammar + is applied. If set to corrective or primitive, only the + corresponding files are accepted, for instance primitive file in + corrective mode will raise an error. \ No newline at end of file From d8adff491794a1848101b547b7006050ecff6467 Mon Sep 17 00:00:00 2001 From: Steven de Oliveira Date: Thu, 8 Jan 2026 11:37:19 +0100 Subject: [PATCH 8/9] Mast doc --- src/mlang/m_frontend/expander.mli | 6 ++ src/mlang/m_frontend/mast.ml | 100 +++++++++++++++++++++--------- src/mlang/m_frontend/mparser.mly | 4 ++ src/mlang/utils/config.mli | 6 +- 4 files changed, 85 insertions(+), 31 deletions(-) diff --git a/src/mlang/m_frontend/expander.mli b/src/mlang/m_frontend/expander.mli index 0dc7e5e8c..890dc8580 100644 --- a/src/mlang/m_frontend/expander.mli +++ b/src/mlang/m_frontend/expander.mli @@ -12,3 +12,9 @@ this program. If not, see . *) val proceed : Mast.program -> Mast.program +(** Expands the program by: + - keeping elements that only belong to the selected applications defined in + {!Utils.Config.application_names}; + - inlining constant's values and removes their definition; + - unrolling loops on variable names (such as "sum(i=05,06,07:Xi)" that + becomes "sum(x05, X06, X07)". *) diff --git a/src/mlang/m_frontend/mast.ml b/src/mlang/m_frontend/mast.ml index a8fec03fa..9c2316346 100644 --- a/src/mlang/m_frontend/mast.ml +++ b/src/mlang/m_frontend/mast.ml @@ -25,8 +25,7 @@ (**{2 Names}*) type application = string -(** Applications are rule annotations. The 3 main DGFiP applications seem to be: - +(** Applications are rule annotations. The 3 main DGFiP applications are: - [batch]: deprecated, used to compute the income tax but not anymore; - [bareme]: seems to compute the income tax; - [iliad]: usage unkown, much bigger than [bareme]. *) @@ -42,12 +41,17 @@ type error_name = string (**{2 Literals}*) -type table_size = LiteralSize of int | SymbolSize of string +type table_size = + | LiteralSize of int (** The table size is an integer literal. *) + | SymbolSize of string (** The table size is a named constant. *) +(** Returns the literal value of a table size. This function expects to take a + LiteralSize value. *) let get_table_size = function | LiteralSize i -> i | SymbolSize _ -> assert false +(** Same as [get_table_size] on an optional size. *) let get_table_size_opt = function | Some (Pos.Mark (LiteralSize i, pos)) -> Some (Pos.mark i pos) | None -> None @@ -75,14 +79,23 @@ type instruction = (Com.m_var_name, error_name) Com.instruction type m_instruction = instruction Pos.marked type rule = { - rule_number : int Pos.marked; + rule_number : int Pos.marked; (** The rule's unique identifier. *) rule_tag_names : string Pos.marked list Pos.marked; + (** The rule's domains. *) rule_apps : application Pos.marked StrMap.t; + (** The applications of this rule. The key and the marked bound value seem + to be equal. *) + (* TODO: simplify map *) rule_chainings : chaining Pos.marked StrMap.t; + (** The chainings of this rule. The key and the marked bound value seem to + be equal. *) + (* TODO: simplify map *) rule_tmp_vars : (string Pos.marked * table_size Pos.marked option) list; - rule_formulaes : instruction Pos.marked list; - (** A rule can contain many variable definitions *) + (** The temporary variables of a rule. *) + rule_formulaes : instruction Pos.marked list; (** The rule's instructions. *) } +(** A rule is a list of instruction associated to one or several domains and to + a list of applications. *) type target = { target_name : string Pos.marked; @@ -93,6 +106,8 @@ type target = { target_tmp_vars : (string Pos.marked * table_size Pos.marked option) list; target_prog : m_instruction list; } +(** A target is a meta rule that can call rules. Targets are entrypoints of the + M program. *) type 'a domain_decl = { dom_names : string Pos.marked list Pos.marked list; @@ -100,6 +115,9 @@ type 'a domain_decl = { dom_by_default : bool; dom_data : 'a; } +(** The declaration of a generic domain. It will either be a rule domain (with + 'a = {!rule_domain_data}) or a verification domain ('a = + {!verif_domain_data}). *) type rule_domain_data = { rdom_computable : bool } @@ -116,28 +134,46 @@ type rule_domain_decl = rule_domain_data domain_decl (**{3 Input variables}*) type variable_attribute = string Pos.marked * int Pos.marked +(** Variables are defined in families (saisie, calculee, ...) which all define + attributes. Each variable must assign a value for each of its attributes. *) type input_variable = { - input_name : string Pos.marked; + input_name : string Pos.marked; (** The unique identifier of the variable. *) input_category : string Pos.marked list; - input_attributes : variable_attribute list; - input_alias : string Pos.marked; (** Unused for now *) - input_is_givenback : bool; + (** The variable's categories (which define the variable's attributes). *) + input_attributes : variable_attribute list; (** The variable's attributes. *) + input_alias : string Pos.marked; + (** The variable can be used by its name or its alias. *) + input_is_givenback : bool; (** Whether it is "restituee" or not. *) input_description : string Pos.marked; + (** The documentation of the variable. *) input_typ : Com.value_typ Pos.marked option; + (** An optional type for the variable. By default, it will be treated as a + real. *) } +(** An input variable (variables from the family "saisie", which is a keyword). +*) type computed_variable = { - comp_name : string Pos.marked; + comp_name : string Pos.marked; (** The unique identifier for the variable. *) comp_table : table_size Pos.marked option; - (** size of the table, [None] for non-table variables *) - comp_attributes : variable_attribute list; + (** Size of the table if the variable is a table, [None] for non-table + variables *) + comp_attributes : variable_attribute list; (** The variable's attributes. *) comp_category : string Pos.marked list; + (** The variable's categories (which define the variable's attributes). *) comp_typ : Com.value_typ Pos.marked option; - comp_is_givenback : bool; + (** An optional type for the variable. By default, it will be treated as a + real. *) + comp_is_givenback : bool; (** Whether it is "restituee" or not. *) comp_description : string Pos.marked; + (** The documentation of the variable. *) } +(** A computed variable (variables from the family "calculee", which is a + keyword. They basically are all the variables that are not from the "saisie" + family. *) +(** A variable declaration. *) type variable_decl = | ComputedVar of computed_variable Pos.marked | ConstVar of string Pos.marked * Com.m_var_name Com.atom Pos.marked @@ -147,29 +183,35 @@ type variable_decl = type var_type = Input | Computed type var_category_decl = { - var_type : var_type; - var_category : string Pos.marked list; + var_type : var_type; (** Are variables inputs or computed? *) + var_category : string Pos.marked list; (** The subcategories. *) var_attributes : string Pos.marked list; + (** The attributes for variables of this category. *) } +(** A variable category (and subcategory) declaration. *) + +(** {3 Standard categories} *) -(* standard categories *) +(** The category for {!Input} variables. *) let input_category = "saisie" +(** The category for {!Computed} variables. *) let computed_category = "calculee" let base_category = "base" +(** The category for given back variables. *) let givenback_category = "restituee" (**{2 Verification clauses}*) (** These clauses are expression refering to the variables of the program. They - seem to be dynamically checked and trigger errors when false. *) + are dynamically checked and trigger errors when false. *) type verification_condition = { verif_cond_expr : expression Pos.marked; verif_cond_error : error_name Pos.marked * string Pos.marked option; - (** A verification condition error can ba associated to a variable *) + (** A verification condition error can be associated to a variable *) } type verification = { @@ -198,19 +240,21 @@ type error_ = { type source_file_item = | Application of application Pos.marked (** Declares an application *) | Chaining of chaining Pos.marked * application Pos.marked list - | VariableDecl of variable_decl - | EventDecl of Com.event_field list - | Function of target - | Rule of rule - | Target of target - | Verification of verification + (** Declares a chaining with its applications *) + | VariableDecl of variable_decl (** Declares a variable *) + | EventDecl of Com.event_field list (** Declares an event *) + | Function of target (** Defines a function *) + | Rule of rule (** Defines a rule *) + | Target of target (** Defines a target *) + | Verification of verification (** Defines a verification *) | Error of error_ (** Declares an error *) | Output of string Pos.marked (** Declares an output variable *) | Func (** Declares a function, unused *) | VarCatDecl of var_category_decl Pos.marked - | RuleDomDecl of rule_domain_decl - | VerifDomDecl of verif_domain_decl - | VariableSpaceDecl of Com.variable_space + (** Defines a variable category *) + | RuleDomDecl of rule_domain_decl (** Declares a rule domain *) + | VerifDomDecl of verif_domain_decl (** Declares a verification domain *) + | VariableSpaceDecl of Com.variable_space (** Declares a variable space *) (* TODO: parse something here *) diff --git a/src/mlang/m_frontend/mparser.mly b/src/mlang/m_frontend/mparser.mly index 51f38dbe8..91fa7e7b4 100644 --- a/src/mlang/m_frontend/mparser.mly +++ b/src/mlang/m_frontend/mparser.mly @@ -484,6 +484,10 @@ rule_etc: in aux [] begPos uname in + List.iter (fun (Pos.Mark (i, _)) -> + Format.printf "Tag %S@." i; + ) + (Pos.unmark rule_tag_names); let rule_number = try Pos.map int_of_string num with _ -> diff --git a/src/mlang/utils/config.mli b/src/mlang/utils/config.mli index 42a1b38bf..a4399afc8 100644 --- a/src/mlang/utils/config.mli +++ b/src/mlang/utils/config.mli @@ -10,7 +10,7 @@ type value_sort = (** Rounding operations to use in the interpreter. They correspond to the rounding operations used by the DGFiP calculator in different execution contexts. - + - RODefault: rounding operations used in the PC/single-thread context - ROMulti: rouding operations used in the PC/multi-thread context - ROMainframe rounding operations used in the mainframe context *) @@ -47,8 +47,8 @@ val var_info_flag : bool ref (** Print infomation about variables declared, defined ou used incorrectly *) val var_info_debug : string list ref -(** Prints even more information but only about some variables members of a - list *) +(** Prints even more information but only about some variables members of a list +*) val warning_flag : bool ref (** Print warning info *) From 1ef91046f21fbd7ac367e5d57a3c007876503d10 Mon Sep 17 00:00:00 2001 From: Steven de Oliveira Date: Thu, 8 Jan 2026 18:03:22 +0100 Subject: [PATCH 9/9] Some rework on doc + partial doc of Com --- doc/exemples/fonctions.md | 184 ++++++++++++++++++--------- doc/fonctions.md | 5 + src/mlang/m_frontend/mast.ml | 2 +- src/mlang/m_ir/com.ml | 4 +- src/mlang/m_ir/com.mli | 240 ++++++++++++++++++++++++++++------- 5 files changed, 326 insertions(+), 109 deletions(-) diff --git a/doc/exemples/fonctions.md b/doc/exemples/fonctions.md index a2a7f7795..9400809dc 100644 --- a/doc/exemples/fonctions.md +++ b/doc/exemples/fonctions.md @@ -14,8 +14,9 @@ evenement : valeur ev_val : variable ev_var; -X : calculee mon_attribut = 0 : ""; -TAB : tableau[10] calculee mon_attribut = 2 : ""; +X : calculee mon_attribut = 0 : "" type REEL; +Y : calculee mon_attribut = 1 : ""; +TAB : tableau[10] calculee mon_attribut = 2 : "" type ENTIER; cible init_tab: application : mon_application; @@ -32,45 +33,54 @@ iterer : variable I : entre 0..(taille(TAB) - 1) increment 1 : dans ( cible test_abs: application : mon_application; afficher "\n__ABS__"; -afficher "\n abs(indefini) = "; -afficher (abs(indefini)); -afficher "\n abs(1) = "; +afficher indenter(2); +afficher "\nabs(indefini) = "; +afficher (abs(indefini)); +afficher "\nabs(1) = "; afficher (abs(1)); -afficher "\n abs(-1) = "; +afficher "\nabs(-1) = "; afficher (abs(-1)); afficher "\n"; +afficher indenter(-2); cible test_arr: application : mon_application; afficher "\n__ARR__"; -afficher "\n arr(indefini) = "; +afficher indenter(2); +afficher "\narr(indefini) = "; afficher (arr(indefini)); -afficher "\n arr(1.8) = "; +afficher "\narr(1.8) = "; afficher (arr(1.8)); -afficher "\n arr(-1.7) = "; +afficher "\ arr(-1.7) = "; afficher (arr(-1.7)); afficher "\n"; +afficher indenter(-2); cible test_attribut: application : mon_application; afficher "\n__ATTRIBUT__"; -afficher "\n attribut(X, mon_attribut) = "; +afficher indenter(2); +afficher "\nattribut(X, mon_attribut) = "; afficher (attribut(X, mon_attribut)); -afficher "\n attribut(TAB, mon_attribut) = "; +afficher "\nattribut(TAB, mon_attribut) = "; afficher (attribut(TAB, mon_attribut)); afficher "\n"; +afficher indenter(-2); cible test_champ_evenement_base: application : mon_application; -afficher "\n champ_evenement(0, ev_val) = "; +afficher indenter(2); +afficher "\nchamp_evenement(0, ev_val) = "; afficher (champ_evenement (0,ev_val)); -afficher "\n champ_evenement(0, ev_var) = "; +afficher "\nchamp_evenement(0, ev_var) = "; afficher (champ_evenement (0,ev_var)); afficher "\n"; +afficher indenter(-2); cible test_champ_evenement: application : mon_application; afficher "\n__CHAMP_EVENEMENT__"; +afficher indenter(2); arranger_evenements : ajouter 1 : dans ( @@ -79,75 +89,87 @@ arranger_evenements champ_evenement (0,ev_var) reference X; champ_evenement (0,ev_val) = 42; X = 2; - afficher "\n Après avoir initialisé les champs de l'événement:"; + afficher "\nAprès avoir initialisé les champs de l'événement:"; calculer cible test_champ_evenement_base; X = indefini; ) +afficher indenter(-2); cible test_inf: application : mon_application; afficher "\n__INF__"; -afficher "\n inf(indefini) = "; +afficher indenter(2); +afficher "\ninf(indefini) = "; afficher (inf(indefini)); -afficher "\n inf(1.8) = "; +afficher "\ninf(1.8) = "; afficher (inf(1.8)); -afficher "\n inf(-1.7) = "; +afficher "\ninf(-1.7) = "; afficher (inf(-1.7)); afficher "\n"; +afficher indenter(-2); cible test_max: application : mon_application; afficher "\n__MAX__"; -afficher "\n max(indefini, indefini) = "; +afficher indenter(2); +afficher "\nmax(indefini, indefini) = "; afficher (max(indefini, indefini)); -afficher "\n max(-1, indefini) = "; +afficher "\nmax(-1, indefini) = "; afficher (max(-1, indefini)); -afficher "\n max(indefini, -1) = "; +afficher "\nmax(indefini, -1) = "; afficher (max(indefini, -1)); -afficher "\n max(1, indefini) = "; +afficher "\nmax(1, indefini) = "; afficher (max(1, indefini)); afficher "\n"; +afficher indenter(-2); cible test_meme_variable: application : mon_application; afficher "\n__MEME_VARIABLE__"; -afficher "\n meme_variable(X,X) = "; +afficher indenter(2); +afficher "\nmeme_variable(X,X) = "; afficher (meme_variable(X,X)); -afficher "\n meme_variable(X,TAB) = "; +afficher "\nmeme_variable(X,TAB) = "; afficher (meme_variable(X,TAB)); -afficher "\n meme_variable(TAB[0],TAB) = "; +afficher "\nmeme_variable(TAB[0],TAB) = "; afficher (meme_variable(TAB[0],TAB)); afficher "\n"; +afficher indenter(-2); cible test_min: application : mon_application; afficher "\n__MIN__"; -afficher "\n min(indefini, indefini) = "; +afficher indenter(2); +afficher "\nmin(indefini, indefini) = "; afficher (min(indefini, indefini)); -afficher "\n min(1, indefini) = "; +afficher "\nmin(1, indefini) = "; afficher (min(1, indefini)); -afficher "\n min(indefini, 1) = "; +afficher "\nmin(indefini, 1) = "; afficher (min(indefini, 1)); -afficher "\n min(-1, indefini) = "; +afficher "\nmin(-1, indefini) = "; afficher (min(-1, indefini)); afficher "\n"; +afficher indenter(-2); cible test_multimax_base: application : mon_application; -afficher "\n multimax(indefini, TAB) = "; +afficher indenter(2); +afficher "\nmultimax(indefini, TAB) = "; afficher (multimax(indefini, TAB)); -afficher "\n multimax(7, TAB) = "; +afficher "\nmultimax(7, TAB) = "; afficher (multimax(7, TAB)); -afficher "\n multimax(taille(TAB) + 1, TAB) = "; +afficher "\nmultimax(taille(TAB) + 1, TAB) = "; afficher (multimax(taille(TAB) + 1, TAB)); -afficher "\n multimax(0, TAB) = "; +afficher "\nmultimax(0, TAB) = "; afficher (multimax(0, TAB)); -afficher "\n multimax(-1, TAB) = "; +afficher "\nmultimax(-1, TAB) = "; afficher (multimax(-1, TAB)); +afficher indenter(-2); cible test_multimax: application : mon_application; afficher "\n__MULTIMAX__"; +afficher indenter(2); afficher "\nAvant initialisation du tableau :"; calculer cible test_multimax_base; calculer cible init_tab; @@ -155,16 +177,20 @@ afficher "\nAprès initialisation du tableau :"; calculer cible test_multimax_base; calculer cible reinit_tab; afficher "\n"; +afficher indenter(-2); cible test_nb_evenements_base: application : mon_application; -afficher "\n nb_evenements() = "; +afficher indenter(2); +afficher "\nnb_evenements() = "; afficher (nb_evenements ()); afficher "\n"; +afficher indenter(-2); cible test_nb_evenements: application : mon_application; afficher "\n__NB_EVENEMENTS__"; +afficher indenter(2); afficher "\nAvant la définition d'un événement :"; calculer cible test_nb_evenements_base; arranger_evenements @@ -175,89 +201,129 @@ arranger_evenements ) afficher "\nAprès la définition d'un événement :"; calculer cible test_nb_evenements_base; +afficher indenter(-2); cible test_null: application : mon_application; afficher "\n__NULL__"; -afficher "\n null(indefini) = "; +afficher indenter(2); +afficher "\nnull(indefini) = "; afficher (null(indefini)); -afficher "\n null(0) = "; +afficher "\nnull(0) = "; afficher (null(0)); -afficher "\n null(1) = "; +afficher "\nnull(1) = "; afficher (null(1)); afficher "\n"; +afficher indenter(-2); cible test_positif: application : mon_application; afficher "\n__POSITIF__"; -afficher "\n positif(indefini) = "; +afficher indenter(2); +afficher "\npositif(indefini) = "; afficher (positif(indefini)); -afficher "\n positif(0) = "; +afficher "\npositif(0) = "; afficher (positif(0)); -afficher "\n positif(1) = "; +afficher "\npositif(1) = "; afficher (positif(1)); -afficher "\n positif(-1) = "; +afficher "\npositif(-1) = "; afficher (positif(-1)); afficher "\n"; +afficher indenter(-2); cible test_positif_ou_nul: application : mon_application; afficher "\n__POSITIF OU NUL__"; -afficher "\n positif_ou_nul(indefini) = "; +afficher indenter(2); +afficher "\npositif_ou_nul(indefini) = "; afficher (positif_ou_nul(indefini)); -afficher "\n positif_ou_nul(0) = "; +afficher "\npositif_ou_nul(0) = "; afficher (positif_ou_nul(0)); -afficher "\n positif_ou_nul(1) = "; +afficher "\npositif_ou_nul(1) = "; afficher (positif_ou_nul(1)); -afficher "\n positif_ou_nul(-1) = "; +afficher "\npositif_ou_nul(-1) = "; afficher (positif_ou_nul(-1)); afficher "\n"; +afficher indenter(-2); cible test_present: application : mon_application; afficher "\n__PRESENT__"; -afficher "\n present(indefini) = "; +afficher indenter(2); +afficher "\npresent(indefini) = "; afficher (present(indefini)); -afficher "\n present(0) = "; +afficher "\npresent(0) = "; afficher (present(0)); -afficher "\n present(1) = "; +afficher "\npresent(1) = "; afficher (present(1)); afficher "\n"; +afficher indenter(-2); cible test_somme: application : mon_application; afficher "\n__SOMME__"; -afficher "\n somme() = "; +afficher indenter(2); +afficher "\nsomme() = "; afficher (somme()); -afficher "\n somme(indefini) = "; +afficher "\nsomme(indefini) = "; afficher (somme(indefini)); -afficher "\n somme(1, indefini) = "; +afficher "\nsomme(1, indefini) = "; afficher (somme(1, indefini)); -afficher "\n somme(1, 2, 3, 4, 5) = "; +afficher "\nsomme(1, 2, 3, 4, 5) = "; afficher (somme(1, 2, 3, 4, 5)); afficher "\n"; +afficher indenter(-2); cible test_supzero: application : mon_application; afficher "\n__SUPZERO__"; -afficher "\n supzero(indefini) = "; +afficher indenter(2); +afficher "\nsupzero(indefini) = "; afficher (supzero(indefini)); -afficher "\n supzero(42) = "; +afficher "\nsupzero(42) = "; afficher (supzero(42)); -afficher "\n supzero(-1) = "; +afficher "\nsupzero(-1) = "; afficher (supzero(-1)); -afficher "\n supzero(0) = "; +afficher "\nsupzero(0) = "; afficher (supzero(0)); afficher "\n"; +afficher indenter(-2); cible test_taille: application : mon_application; afficher "\n__TAILLE__"; -afficher "\n taille(TAB) = "; +afficher indenter(2); +afficher "\ntaille(TAB) = "; afficher (taille(TAB)); -afficher "\n taille(X) = "; +afficher "\ntaille(X) = "; afficher (taille(X)); afficher "\n"; +afficher indenter(-2); + +cible test_type: +application : mon_application; +afficher "\n__TYPE__"; +afficher indenter(2); +afficher "\ntype(X, REEL) = "; +afficher (type(X, REEL)); +afficher "\ntype(X, ENTIER) = "; +afficher (type(X, ENTIER)); +afficher "\ntype(TAB, ENTIER) = "; +afficher (type(TAB, ENTIER)); +afficher "\ntype(Y, ENTIER) = "; +afficher (type(Y, ENTIER)); +afficher "\ntype(Y, REEL) = "; +afficher (type(Y, REEL)); +afficher "\ntype(Y, BOOLEEN) = "; +afficher (type(Y, BOOLEEN)); +afficher "\ntype(Y, DATE_AAAA) = "; +afficher (type(Y, DATE_AAAA)); +afficher "\ntype(Y, DATE_JJMMAAAA) = "; +afficher (type(Y, DATE_JJMMAAAA)); +afficher "\ntype(Y, DATE_MM) = "; +afficher (type(Y, DATE_MM)); +afficher "\n"; +afficher indenter(-2); cible fun_test: application : mon_application; @@ -295,6 +361,8 @@ calculer cible test_somme; calculer cible test_supzero; # Taille calculer cible test_taille; +# Type +calculer cible test_type; ``` ## Fichier irj : test.irj diff --git a/doc/fonctions.md b/doc/fonctions.md index cda4e8ea1..41950f6e6 100644 --- a/doc/fonctions.md +++ b/doc/fonctions.md @@ -140,6 +140,11 @@ S'il s'agit d'une variable, `taille` renvoie `1`, sinon elle renvoie la taille d tableau. Elle échoue si l'argument est une constante ou une valeur. +## type(V, type) + +Cette fonction prend en argument une variable et un type. Elle +renvoie 1 si V est du type donné, 0 sinon. + % TODO % ## numero_compil() diff --git a/src/mlang/m_frontend/mast.ml b/src/mlang/m_frontend/mast.ml index 9c2316346..aa458d56c 100644 --- a/src/mlang/m_frontend/mast.ml +++ b/src/mlang/m_frontend/mast.ml @@ -164,7 +164,7 @@ type computed_variable = { (** The variable's categories (which define the variable's attributes). *) comp_typ : Com.value_typ Pos.marked option; (** An optional type for the variable. By default, it will be treated as a - real. *) + real, but tests on types will always result to 0. *) comp_is_givenback : bool; (** Whether it is "restituee" or not. *) comp_description : string Pos.marked; (** The documentation of the variable. *) diff --git a/src/mlang/m_ir/com.ml b/src/mlang/m_ir/com.ml index 5e3db3e24..fd47c7f92 100644 --- a/src/mlang/m_ir/com.ml +++ b/src/mlang/m_ir/com.ml @@ -53,11 +53,11 @@ module CatVar = struct let from_string_list = function | Pos.Mark ([ Pos.Mark ("*", _) ], id_pos) -> - one (Input (StrSet.one "*")) id_pos + one all_inputs id_pos |> add (Computed { is_base = false }) id_pos |> add (Computed { is_base = true }) id_pos | Pos.Mark ([ Pos.Mark ("saisie", _); Pos.Mark ("*", _) ], id_pos) -> - one (Input (StrSet.one "*")) id_pos + one all_inputs id_pos | Pos.Mark (Pos.Mark ("saisie", _) :: id, id_pos) -> one (Input (StrSet.from_marked_list id)) id_pos | Pos.Mark (Pos.Mark ("calculee", _) :: id, id_pos) -> ( diff --git a/src/mlang/m_ir/com.mli b/src/mlang/m_ir/com.mli index 25ba1f587..81c19012e 100644 --- a/src/mlang/m_ir/com.mli +++ b/src/mlang/m_ir/com.mli @@ -1,13 +1,42 @@ +(*This program is free software: you can redistribute it and/or modify it under + the terms of the GNU General Public License as published by the Free Software + Foundation, either version 3 of the License, or (at your option) any later + version. + + This program is distributed in the hope that it will be useful, but WITHOUT + ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + this program. If not, see . *) + +(** {2 Categories} *) + +(** High level representation of categories of variable. + In practice, there only are two types of categories: + * "calculee" (Input) variables, with several sub categories; + * "saisie" (Computed) variable, that can either be base or non-base. + + Such representation allows to manipulate sets of variable (for example + in iterations). *) module CatVar : sig - type t = Input of StrSet.t | Computed of { is_base : bool } + type t = + | Input of StrSet.t + (** An input variable category with its sub categories *) + | Computed of { is_base : bool } + (** A computed var with a flag marking if it is a base varible. *) val all_inputs : t + (** A category containing all inputs. *) val is_input : t -> bool + (** Returns [true] if the category in argument is an input variable category. *) val is_computed : t -> bool + (** Returns [true] if the category in argument is a computed variable category. *) val pp : Format.formatter -> t -> unit + (** Pretty printer. *) val compare : t -> t -> int @@ -17,6 +46,13 @@ module CatVar : sig include MapExt.T with type key = t val from_string_list : string Pos.marked list Pos.marked -> Pos.t t + (** [from_string_list l] + + From a category identifier [l] seen as a list of strings (for example + [\["saisie"; "revenu"; "corrective"\]] for the category + ["saisie revenu corrective"]), + returns a map binding all [CatVar.t] values refered by [l] to the location + of [l]. *) end type loc = LocComputed | LocBase | LocInput @@ -28,17 +64,17 @@ module CatVar : sig module LocMap : MapExt.T with type key = loc type data = { - id : t; - id_str : string; - id_int : int; - loc : loc; - pos : Pos.t; - attributs : Pos.t StrMap.t; + id : t; (** The category *) + id_str : string; (** A unique string identifier for the category *) + id_int : int; (** A unique int identifier *) + loc : loc; (** Quick access to the category kind *) + pos : Pos.t; (** Variable category declaration *) + attributs : Pos.t StrMap.t; (** Attributes of the category *) } + (** Data for a given category. Defined in the validator. *) end -(** Here are all the types a value can have. Date types don't seem to be used at - all though. *) +(** Here are all the types a value can have. Used for the M function [type]. *) type value_typ = | Boolean | DateYear @@ -47,78 +83,123 @@ type value_typ = | Integer | Real +(** {2 Variables} *) + +(** Note: TGV stands for "Tableau Général des Variables". + This is where the variables are declared. *) + type loc_tgv = { - loc_cat : CatVar.loc; - loc_idx : int; - loc_tab_idx : int; - loc_cat_id : CatVar.t; - loc_cat_str : string; - loc_cat_idx : int; + loc_cat : CatVar.loc; (** Its category kind *) + loc_idx : int; (** TODO *) + loc_tab_idx : int; (** TODO *) + loc_cat_id : CatVar.t; (** Full details on its category *) + loc_cat_str : string; (** String representation of its category *) + loc_cat_idx : int; (** The variable's identifier in its category. *) } +(** Location of a TGV variable in the compiler's context. *) -type loc_tmp = { loc_idx : int; loc_tab_idx : int; loc_cat_idx : int } +type loc_tmp = { + loc_idx : int; (** TODO *) + loc_tab_idx : int; (** TODO *) + loc_cat_idx : int; (** The variable's identifier in its category *) +} +(** Location of a temporary variable in the compiler's context. *) +(** The different kinds of variables; the 'string' is the variable's name. *) type loc = - | LocTgv of string * loc_tgv - | LocTmp of string * loc_tmp - | LocRef of string * int + | LocTgv of string * loc_tgv (** A TGV variable *) + | LocTmp of string * loc_tmp (** A temporary variable *) + | LocRef of string * int (** A reference (the integer is the 'loc_idx') *) module Var : sig type id = int + (** Data on a TGV variable. *) type tgv = { table : t Array.t option; + (** The array of cells if the variable is a table. *) alias : string Pos.marked option; (** Input variable have an alias *) descr : string Pos.marked; (** Description taken from the variable declaration *) attrs : int Pos.marked StrMap.t; - cat : CatVar.t; - is_given_back : bool; - typ : value_typ option; + (** Variable's attributes and their values *) + cat : CatVar.t; (** Category *) + is_given_back : bool; (** Is the variable 'restituee'? *) + typ : value_typ option; (** Optional variable type *) } + (** Exhaustive data on a TGV variable. *) - and scope = Tgv of tgv | Temp of t Array.t option | Ref + (** Where can the variable be found? *) + and scope = + | Tgv of tgv (** This variable belongs to the TGV. *) + | Temp of t Array.t option + (** This variable is temporary, maybe an array. *) + | Ref (** This references another variable. *) and t = { name : string Pos.marked; (** The position is the variable declaration *) id : id; + (** A unique identifier, enforced by [Var.new_{tgv,temp,ref,arg,res}] *) loc : loc; - scope : scope; + scope : scope; (** Data on the variable. *) } + (** {2 Getters and setters} *) + val tgv : t -> tgv + (** Returns the tgv data of a variable; fails if it is not a TGV variable. *) val name : t -> string Pos.marked + (** Returns a markzs variable name. *) val name_str : t -> string + (** Same as [name] without the mark. *) val get_table : t -> t Array.t option + (** Returns the table represented by the variable, if relevant. + Returns [None] on references. *) val is_table : t -> bool + (** Returns true if the variable represents a table. *) val set_table : t -> t Array.t option -> t + (** Sets a table to the given variable. *) val cat_var_loc : t -> CatVar.loc + (** Returns the category of a TGV variable; fails if it is not a TGV variable. *) val size : t -> int + (** Returns the size of a variable: the size of the array if it is a table; 1 + otherwise. *) val alias : t -> string Pos.marked option + (** Returns the variable's alias if any, otherwise returns None. + Aliases only are set for input variables. *) val alias_str : t -> string + (** Same as [alias] without the mark, or "" if no alias was given. *) val descr : t -> string Pos.marked + (** Returns the description of the variable; fails if it is not a TGV variable. *) val descr_str : t -> string + (** Same as [descr] without the mark. *) val attrs : t -> int Pos.marked StrMap.t + (** Returns the attributes of a TGV variable; fails if it is not a TGV variable. *) val cat : t -> CatVar.t + (** Returns the category of a TGV variable; fails if it is not a TGV variable. *) val typ : t -> value_typ option + (** Returns the type of a TGV variable; fails if it is not a TGV variable. *) val is_given_back : t -> bool + (** Returns [true] if TGV variable in argument is 'restituee'; fails if it is + not a TGV variable. *) val loc_tgv : t -> loc_tgv + (** Returns the loc data of a TGV variable; fails if it not a TGV variable. *) val loc_cat_idx : t -> int @@ -135,12 +216,13 @@ module Var : sig val set_loc_tab_idx : t -> int -> t val is_tgv : t -> bool + (** Returns true if it is a TGV variable. *) val is_temp : t -> bool + (** Returns true if it is a temporary variable. *) val is_ref : t -> bool - - val init_loc : CatVar.t -> loc_tgv + (** Returns true if it is a reference. *) val new_tgv : name:string Pos.marked -> @@ -152,16 +234,22 @@ module Var : sig cat:CatVar.t -> typ:value_typ option -> t + (** Creates a new tgv variable with a unique id. *) val new_temp : name:string Pos.marked -> table:t Array.t option -> t + (** Creates a new temporary variable with a unique id. *) val new_ref : name:string Pos.marked -> t + (** Creates a new reference with a unique id. *) val new_arg : name:string Pos.marked -> t + (** Creates a new temporary variable for a function's argument. *) val new_res : name:string Pos.marked -> t + (** Creates a new temporary variable for a function's result. *) val pp : Format.formatter -> t -> unit + (** Pretty printer. *) val compare : t -> t -> int @@ -176,9 +264,16 @@ module Var : sig val compare_name : string -> string -> int*) end -type event_field = { name : string Pos.marked; index : int; is_var : bool } +type event_field = { + name : string Pos.marked; (** The field name *) + index : int; (** Position of the event field. Set in the validator. *) + is_var : bool; (** Is a reference to a variable or a value? *) +} +(** Data on an event field. *) -type ('n, 'v) event_value = Numeric of 'n | RefVar of 'v +type ('n, 'v) event_value = + | Numeric of 'n + | RefVar of 'v (** The values of an event. *) module DomainId : StrSet.T @@ -216,19 +311,37 @@ type variable_space = { type verif_domain = verif_domain_data domain +(** {2 Values and expressions} *) + +(* Careful, the link may be unstable! *) +(** For a complete documentation on values and simple expressions semantics, + check {{:../../../../../syntax.html#valeurs}the full documentation}. *) + +(** A literal can either be a float value or undefined. *) type literal = Float of float | Undefined +(** A case for switches (aiguillages). *) type case = Default | Value of literal (** Unary operators *) type unop = Not | Minus -(** Binary operators *) +(** Binary operators. *) type binop = And | Or | Add | Sub | Mul | Div | Mod -(** Comparison operators *) +(** Comparison operators. + Comparison always return 0 or 1 when the compared values are both + defined; otherwise, it returns 'undefined'. + + Note that this is even true when comparing undefined with undefined: + the proper way to test if a value is undefined is through the function + 'PresentFunc' (present). *) type comp_op = Gt | Gte | Lt | Lte | Eq | Neq +(** Functions callable in M. + There is only a subset of the whole M functions; some are translated + into dedicated expressions at parsing (champ_evenement for example + is directly transformed into an {!access} to a variable). *) type func = | SumFunc (** Sums the arguments *) | AbsFunc (** Absolute value *) @@ -236,23 +349,23 @@ type func = | MaxFunc (** Maximum of a list of values *) | GtzFunc (** Greater than zero (strict) ? *) | GtezFunc (** Greater or equal than zero ? *) - | NullFunc (** Equal to zero ? *) + | NullFunc (** Equal to zero *) | ArrFunc (** Round to nearest integer *) | InfFunc (** Truncate to integer *) - | PresentFunc (** Different than zero ? *) - | Multimax (** ??? *) - | Supzero (** ??? *) - | VerifNumber - | ComplNumber - | NbEvents - | Func of string + | PresentFunc (** Different than undefined *) + | Multimax (** Max of a subset of a table *) + | Supzero (** Identity if > 0, undefined otherwise *) + | VerifNumber (** -- unsupported -- *) + | ComplNumber (** -- unsupported -- *) + | NbEvents (** Total number of events *) + | Func of string (** A M function *) (** The M language has an extremely odd way to specify looping. Rather than having first-class local mutable variables whose value change at each loop iteration, the M language prefers to use the changing loop parameter to instantiate the variable names inside the loop. For instance, - {v somme(i=1..10:Xi) v} + {v somme(i=1..9:Xi) v} should evaluate to the sum of variables [X1], [X2], etc. Parameters can be number or characters and there can be multiple of them. We have to store all @@ -267,11 +380,16 @@ type var_name = Normal of string | Generic of var_name_generic type m_var_name = var_name Pos.marked type var_space = (m_var_name * int) option +(** The prefix of a variable that defines its space. + No space is equivalent to the default space. *) +(** A generic representation of an access to a variable, read or write. *) type 'v access = - | VarAccess of var_space * 'v + | VarAccess of var_space * 'v (** Simple variable occurence *) | TabAccess of var_space * 'v * 'v m_expression + (** Access to a cell of a table *) | FieldAccess of var_space * 'v m_expression * string Pos.marked * int + (** Call to 'champ_evenement' *) and 'v m_access = 'v access Pos.marked @@ -293,11 +411,13 @@ and 'v loop_variables = | ValueSets of 'v loop_variable list | Ranges of 'v loop_variable list +(** A set of values. *) and 'v set_value = - | FloatValue of float Pos.marked - | VarValue of 'v m_access - | IntervalValue of int Pos.marked * int Pos.marked + | FloatValue of float Pos.marked (** A literal singleton *) + | VarValue of 'v m_access (** A variable singleton *) + | IntervalValue of int Pos.marked * int Pos.marked (** A literal interval *) +(** Basic M expressions. *) and 'v expression = | TestInSet of bool * 'v m_expression * 'v set_value list (** Test if an expression is in a set of value (or not in the set if the @@ -326,6 +446,7 @@ and 'v expression = and 'v m_expression = 'v expression Pos.marked +(** Handling of errors. *) module Error : sig type typ = Anomaly | Discordance | Information @@ -354,15 +475,26 @@ module Error : sig end end +(** {2 Printing in M} *) + +(** The print function is treated as a dedicated instruction. *) + +(** Where to print *) type print_std = StdOut | StdErr +(** How to print a variable *) type print_info = Name | Alias +(** Something to print *) type 'v print_arg = | PrintString of string | PrintAccess of print_info * 'v m_access | PrintIndent of 'v m_expression + (** Evaluates the expression and indents as much *) | PrintExpr of 'v m_expression * int * int + (** Prints an expression with a minimal and maximal precision. *) + +(** {2 Loops} *) (** In the M language, you can define multiple variables at once. This is the way they do looping since the definition can depend on the loop variable @@ -378,13 +510,17 @@ type 'v formula = | SingleFormula of 'v formula_decl | MultipleFormulaes of 'v formula_loop * 'v formula_decl +(** {2 Stopping} *) + type stop_kind = - | SKApplication (* Leave the whole application *) - | SKTarget (* Leave the current target *) - | SKFun (* Leave the current function *) + | SKApplication (** Leave the whole application *) + | SKTarget (** Leave the current target *) + | SKFun (** Leave the current function *) | SKId of string option -(* Leave the iterator with the selected var - (or the current if [None]) *) + (** Leave the iterator with the selected var + (or the current if [None]) *) + +(** {2 Instructions} *) type ('v, 'e) instruction = | Affectation of 'v formula Pos.marked @@ -432,6 +568,8 @@ type ('v, 'e) instruction = and ('v, 'e) m_instruction = ('v, 'e) instruction Pos.marked +(** A target is a list of instructions. They are very similar to rules, + except targets are entrypoints of the M program. *) type ('v, 'e) target = { target_name : string Pos.marked; target_file : string option; @@ -444,6 +582,10 @@ type ('v, 'e) target = { target_nb_refs : int; target_prog : ('v, 'e) m_instruction list; } +(* TODO: target doc *) + +(** {2 Utils} *) +(* TODO: doc *) val target_is_function : ('v, 'e) target -> bool @@ -484,6 +626,8 @@ val get_var_name : var_name -> string val get_normal_var : var_name -> string +(** {2 Pretty printing functions} *) + val format_value_typ : Pp.t -> value_typ -> unit val format_literal : Pp.t -> literal -> unit