mirror of
https://github.com/kubernetes/kubernetes.git
synced 2026-11-06 05:17:08 +00:00
Bump prometheus/common to v0.73.1
Pulls in github.com/pandatix/go-abnf (OpenMetrics decoder in expfmt), pinned to v0.5.5 for the long-line decoding fix (prometheus/common#1028). v0.73.0 is skipped: its go-abnf v0.5.1 requires the blocklisted hashicorp/go-uuid. Signed-off-by: Davanum Srinivas <davanum@gmail.com>
This commit is contained in:
63 files changed
+11590
-72
No files matched your search
+25
@@ -0,0 +1,25 @@
|
||||
= vendor/github.com/pandatix/go-abnf licensed under: =
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2024 Lucas TESSON - PandatiX <lucastesson@protonmail.com>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
= vendor/github.com/pandatix/go-abnf/LICENSE d3af9189e0a449a8402ddf72567f0748
|
||||
@@ -48,7 +48,7 @@ require (
|
||||
github.com/opencontainers/selinux v1.15.1
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2
|
||||
github.com/prometheus/client_model v0.6.3
|
||||
github.com/prometheus/common v0.72.0
|
||||
github.com/prometheus/common v0.73.1
|
||||
github.com/robfig/cron/v3 v3.0.1
|
||||
github.com/spf13/cobra v1.10.2
|
||||
github.com/spf13/pflag v1.0.10
|
||||
@@ -184,6 +184,7 @@ require (
|
||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
github.com/opencontainers/image-spec v1.1.1 // indirect
|
||||
github.com/opencontainers/runtime-spec v1.3.0 // indirect
|
||||
github.com/pandatix/go-abnf v0.5.5 // indirect
|
||||
github.com/peterbourgon/diskv/v3 v3.0.1 // indirect
|
||||
github.com/prometheus/client_golang v1.25.0 // indirect
|
||||
github.com/prometheus/procfs v0.22.0 // indirect
|
||||
|
||||
@@ -245,6 +245,8 @@ github.com/opencontainers/runtime-spec v1.3.0 h1:YZupQUdctfhpZy3TM39nN9Ika5CBWT5
|
||||
github.com/opencontainers/runtime-spec v1.3.0/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=
|
||||
github.com/opencontainers/selinux v1.15.1 h1:ERxeh5caJvCzNAKdI8WQbJmB1LDTn4BuaAg8wihLBpA=
|
||||
github.com/opencontainers/selinux v1.15.1/go.mod h1:LenyElirjUHszfxrjuFqC85HIeXZKumHcKMQtnaDlQQ=
|
||||
github.com/pandatix/go-abnf v0.5.5 h1:EXex5KkRQx4Dm6GnpAcQnIMBs9KiK0uf8p/wq+4t9PM=
|
||||
github.com/pandatix/go-abnf v0.5.5/go.mod h1:AE0KD3/VNoCjGzc1gqetRKshbI4SYErXQ/8kPenR5mc=
|
||||
github.com/peterbourgon/diskv/v3 v3.0.1 h1:x06SQA46+PKIUftmEujdwSEpIx8kR+M9eLYsUxeYveU=
|
||||
github.com/peterbourgon/diskv/v3 v3.0.1/go.mod h1:kJ5Ny7vLdARGU3WUuy6uzO6T0nb/2gWcT1JiBvRmb5o=
|
||||
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8=
|
||||
@@ -255,8 +257,8 @@ github.com/prometheus/client_golang v1.25.0 h1:cDtW1TcFsMi1RJc8khwCKB4VhzwV04aKq
|
||||
github.com/prometheus/client_golang v1.25.0/go.mod h1:j2X/4/kaQHLPnmyEU8zjmkndo1T317VpCIQEml8LTKk=
|
||||
github.com/prometheus/client_model v0.6.3 h1:O0jaTVAYNxTHYInEPFJt5I3+sN8zqBtVMPTB1qyxiEo=
|
||||
github.com/prometheus/client_model v0.6.3/go.mod h1:gpN5P9S7Rr6Yr92PiQ+Ixvhf6JZEkF1dnxsYL2aPBEM=
|
||||
github.com/prometheus/common v0.72.0 h1:tAYsE+sPJxIncDAobm4H5aQjmox9ZxEIIqPbiffa8G4=
|
||||
github.com/prometheus/common v0.72.0/go.mod h1:77NWqAQ2tXT7BIK40qjJdw5Acrsrg1TlHAnsQi3i6mk=
|
||||
github.com/prometheus/common v0.73.1 h1:SZWh/j4AvhCdhX8jvYSnxvPxhWlC9QZkjJ+tOrEr7kU=
|
||||
github.com/prometheus/common v0.73.1/go.mod h1:I9aA/Ukz9s1olOobh8KtCkU7B1mQPQsB9UvQPsUQ1Dk=
|
||||
github.com/prometheus/procfs v0.22.0 h1:6q9+/JL9IKAPbCmBrv9n5O5Ty3NKnciV5X7YGw0oics=
|
||||
github.com/prometheus/procfs v0.22.0/go.mod h1:CvmFr/GVhIjIvWJZW3tgkODBQMRIf0EyWMQLHCHab58=
|
||||
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
||||
|
||||
@@ -17,8 +17,9 @@ require (
|
||||
github.com/go-logr/logr v1.4.4 // indirect
|
||||
github.com/kylelemons/godebug v1.1.0 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/pandatix/go-abnf v0.5.5 // indirect
|
||||
github.com/prometheus/client_model v0.6.3 // indirect
|
||||
github.com/prometheus/common v0.72.0 // indirect
|
||||
github.com/prometheus/common v0.73.1 // indirect
|
||||
github.com/prometheus/procfs v0.22.0 // indirect
|
||||
go.opentelemetry.io/otel v1.47.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.47.0 // indirect
|
||||
|
||||
@@ -14,12 +14,14 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0
|
||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||
github.com/pandatix/go-abnf v0.5.5 h1:EXex5KkRQx4Dm6GnpAcQnIMBs9KiK0uf8p/wq+4t9PM=
|
||||
github.com/pandatix/go-abnf v0.5.5/go.mod h1:AE0KD3/VNoCjGzc1gqetRKshbI4SYErXQ/8kPenR5mc=
|
||||
github.com/prometheus/client_golang v1.25.0 h1:cDtW1TcFsMi1RJc8khwCKB4VhzwV04aKqbYtNOXuiK0=
|
||||
github.com/prometheus/client_golang v1.25.0/go.mod h1:j2X/4/kaQHLPnmyEU8zjmkndo1T317VpCIQEml8LTKk=
|
||||
github.com/prometheus/client_model v0.6.3 h1:O0jaTVAYNxTHYInEPFJt5I3+sN8zqBtVMPTB1qyxiEo=
|
||||
github.com/prometheus/client_model v0.6.3/go.mod h1:gpN5P9S7Rr6Yr92PiQ+Ixvhf6JZEkF1dnxsYL2aPBEM=
|
||||
github.com/prometheus/common v0.72.0 h1:tAYsE+sPJxIncDAobm4H5aQjmox9ZxEIIqPbiffa8G4=
|
||||
github.com/prometheus/common v0.72.0/go.mod h1:77NWqAQ2tXT7BIK40qjJdw5Acrsrg1TlHAnsQi3i6mk=
|
||||
github.com/prometheus/common v0.73.1 h1:SZWh/j4AvhCdhX8jvYSnxvPxhWlC9QZkjJ+tOrEr7kU=
|
||||
github.com/prometheus/common v0.73.1/go.mod h1:I9aA/Ukz9s1olOobh8KtCkU7B1mQPQsB9UvQPsUQ1Dk=
|
||||
github.com/prometheus/procfs v0.22.0 h1:6q9+/JL9IKAPbCmBrv9n5O5Ty3NKnciV5X7YGw0oics=
|
||||
github.com/prometheus/procfs v0.22.0/go.mod h1:CvmFr/GVhIjIvWJZW3tgkODBQMRIf0EyWMQLHCHab58=
|
||||
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
|
||||
|
||||
@@ -74,10 +74,11 @@ require (
|
||||
github.com/moby/spdystream v0.5.1 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect
|
||||
github.com/pandatix/go-abnf v0.5.5 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/prometheus/client_golang v1.25.0 // indirect
|
||||
github.com/prometheus/client_model v0.6.3 // indirect
|
||||
github.com/prometheus/common v0.72.0 // indirect
|
||||
github.com/prometheus/common v0.73.1 // indirect
|
||||
github.com/prometheus/procfs v0.22.0 // indirect
|
||||
github.com/sirupsen/logrus v1.10.2 // indirect
|
||||
github.com/soheilhy/cmux v0.1.5 // indirect
|
||||
|
||||
+4
-2
@@ -127,6 +127,8 @@ github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J
|
||||
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=
|
||||
github.com/onsi/gomega v1.44.0 h1:eAiGl3Pw5jz5GQdDff0BcxYpAX1JxW8xD7mFUuwNfZQ=
|
||||
github.com/onsi/gomega v1.44.0/go.mod h1:e/C2HwaZ1DhvjzXXuFhcR7hY7Sh9pl7MmoWKEjzwcdA=
|
||||
github.com/pandatix/go-abnf v0.5.5 h1:EXex5KkRQx4Dm6GnpAcQnIMBs9KiK0uf8p/wq+4t9PM=
|
||||
github.com/pandatix/go-abnf v0.5.5/go.mod h1:AE0KD3/VNoCjGzc1gqetRKshbI4SYErXQ/8kPenR5mc=
|
||||
github.com/peterbourgon/diskv/v3 v3.0.1/go.mod h1:kJ5Ny7vLdARGU3WUuy6uzO6T0nb/2gWcT1JiBvRmb5o=
|
||||
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
@@ -136,8 +138,8 @@ github.com/prometheus/client_golang v1.25.0 h1:cDtW1TcFsMi1RJc8khwCKB4VhzwV04aKq
|
||||
github.com/prometheus/client_golang v1.25.0/go.mod h1:j2X/4/kaQHLPnmyEU8zjmkndo1T317VpCIQEml8LTKk=
|
||||
github.com/prometheus/client_model v0.6.3 h1:O0jaTVAYNxTHYInEPFJt5I3+sN8zqBtVMPTB1qyxiEo=
|
||||
github.com/prometheus/client_model v0.6.3/go.mod h1:gpN5P9S7Rr6Yr92PiQ+Ixvhf6JZEkF1dnxsYL2aPBEM=
|
||||
github.com/prometheus/common v0.72.0 h1:tAYsE+sPJxIncDAobm4H5aQjmox9ZxEIIqPbiffa8G4=
|
||||
github.com/prometheus/common v0.72.0/go.mod h1:77NWqAQ2tXT7BIK40qjJdw5Acrsrg1TlHAnsQi3i6mk=
|
||||
github.com/prometheus/common v0.73.1 h1:SZWh/j4AvhCdhX8jvYSnxvPxhWlC9QZkjJ+tOrEr7kU=
|
||||
github.com/prometheus/common v0.73.1/go.mod h1:I9aA/Ukz9s1olOobh8KtCkU7B1mQPQsB9UvQPsUQ1Dk=
|
||||
github.com/prometheus/procfs v0.22.0 h1:6q9+/JL9IKAPbCmBrv9n5O5Ty3NKnciV5X7YGw0oics=
|
||||
github.com/prometheus/procfs v0.22.0/go.mod h1:CvmFr/GVhIjIvWJZW3tgkODBQMRIf0EyWMQLHCHab58=
|
||||
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
|
||||
|
||||
@@ -90,10 +90,11 @@ require (
|
||||
github.com/jonboulle/clockwork v0.5.0 // indirect
|
||||
github.com/kylelemons/godebug v1.1.0 // indirect
|
||||
github.com/moby/spdystream v0.5.1 // indirect
|
||||
github.com/pandatix/go-abnf v0.5.5 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/prometheus/client_golang v1.25.0 // indirect
|
||||
github.com/prometheus/client_model v0.6.3 // indirect
|
||||
github.com/prometheus/common v0.72.0 // indirect
|
||||
github.com/prometheus/common v0.73.1 // indirect
|
||||
github.com/prometheus/procfs v0.22.0 // indirect
|
||||
github.com/sirupsen/logrus v1.10.2 // indirect
|
||||
github.com/soheilhy/cmux v0.1.5 // indirect
|
||||
|
||||
Generated
+4
-2
@@ -128,6 +128,8 @@ github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J
|
||||
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=
|
||||
github.com/onsi/gomega v1.44.0 h1:eAiGl3Pw5jz5GQdDff0BcxYpAX1JxW8xD7mFUuwNfZQ=
|
||||
github.com/onsi/gomega v1.44.0/go.mod h1:e/C2HwaZ1DhvjzXXuFhcR7hY7Sh9pl7MmoWKEjzwcdA=
|
||||
github.com/pandatix/go-abnf v0.5.5 h1:EXex5KkRQx4Dm6GnpAcQnIMBs9KiK0uf8p/wq+4t9PM=
|
||||
github.com/pandatix/go-abnf v0.5.5/go.mod h1:AE0KD3/VNoCjGzc1gqetRKshbI4SYErXQ/8kPenR5mc=
|
||||
github.com/peterbourgon/diskv/v3 v3.0.1/go.mod h1:kJ5Ny7vLdARGU3WUuy6uzO6T0nb/2gWcT1JiBvRmb5o=
|
||||
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
@@ -137,8 +139,8 @@ github.com/prometheus/client_golang v1.25.0 h1:cDtW1TcFsMi1RJc8khwCKB4VhzwV04aKq
|
||||
github.com/prometheus/client_golang v1.25.0/go.mod h1:j2X/4/kaQHLPnmyEU8zjmkndo1T317VpCIQEml8LTKk=
|
||||
github.com/prometheus/client_model v0.6.3 h1:O0jaTVAYNxTHYInEPFJt5I3+sN8zqBtVMPTB1qyxiEo=
|
||||
github.com/prometheus/client_model v0.6.3/go.mod h1:gpN5P9S7Rr6Yr92PiQ+Ixvhf6JZEkF1dnxsYL2aPBEM=
|
||||
github.com/prometheus/common v0.72.0 h1:tAYsE+sPJxIncDAobm4H5aQjmox9ZxEIIqPbiffa8G4=
|
||||
github.com/prometheus/common v0.72.0/go.mod h1:77NWqAQ2tXT7BIK40qjJdw5Acrsrg1TlHAnsQi3i6mk=
|
||||
github.com/prometheus/common v0.73.1 h1:SZWh/j4AvhCdhX8jvYSnxvPxhWlC9QZkjJ+tOrEr7kU=
|
||||
github.com/prometheus/common v0.73.1/go.mod h1:I9aA/Ukz9s1olOobh8KtCkU7B1mQPQsB9UvQPsUQ1Dk=
|
||||
github.com/prometheus/procfs v0.22.0 h1:6q9+/JL9IKAPbCmBrv9n5O5Ty3NKnciV5X7YGw0oics=
|
||||
github.com/prometheus/procfs v0.22.0/go.mod h1:CvmFr/GVhIjIvWJZW3tgkODBQMRIf0EyWMQLHCHab58=
|
||||
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
|
||||
|
||||
@@ -58,10 +58,11 @@ require (
|
||||
github.com/kylelemons/godebug v1.1.0 // indirect
|
||||
github.com/moby/term v0.5.2 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/pandatix/go-abnf v0.5.5 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/prometheus/client_golang v1.25.0 // indirect
|
||||
github.com/prometheus/client_model v0.6.3 // indirect
|
||||
github.com/prometheus/common v0.72.0 // indirect
|
||||
github.com/prometheus/common v0.73.1 // indirect
|
||||
github.com/prometheus/procfs v0.22.0 // indirect
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
go.etcd.io/etcd/api/v3 v3.7.3 // indirect
|
||||
|
||||
Generated
+4
-2
@@ -120,6 +120,8 @@ github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRW
|
||||
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=
|
||||
github.com/onsi/gomega v1.44.0 h1:eAiGl3Pw5jz5GQdDff0BcxYpAX1JxW8xD7mFUuwNfZQ=
|
||||
github.com/onsi/gomega v1.44.0/go.mod h1:e/C2HwaZ1DhvjzXXuFhcR7hY7Sh9pl7MmoWKEjzwcdA=
|
||||
github.com/pandatix/go-abnf v0.5.5 h1:EXex5KkRQx4Dm6GnpAcQnIMBs9KiK0uf8p/wq+4t9PM=
|
||||
github.com/pandatix/go-abnf v0.5.5/go.mod h1:AE0KD3/VNoCjGzc1gqetRKshbI4SYErXQ/8kPenR5mc=
|
||||
github.com/peterbourgon/diskv/v3 v3.0.1/go.mod h1:kJ5Ny7vLdARGU3WUuy6uzO6T0nb/2gWcT1JiBvRmb5o=
|
||||
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
@@ -128,8 +130,8 @@ github.com/prometheus/client_golang v1.25.0 h1:cDtW1TcFsMi1RJc8khwCKB4VhzwV04aKq
|
||||
github.com/prometheus/client_golang v1.25.0/go.mod h1:j2X/4/kaQHLPnmyEU8zjmkndo1T317VpCIQEml8LTKk=
|
||||
github.com/prometheus/client_model v0.6.3 h1:O0jaTVAYNxTHYInEPFJt5I3+sN8zqBtVMPTB1qyxiEo=
|
||||
github.com/prometheus/client_model v0.6.3/go.mod h1:gpN5P9S7Rr6Yr92PiQ+Ixvhf6JZEkF1dnxsYL2aPBEM=
|
||||
github.com/prometheus/common v0.72.0 h1:tAYsE+sPJxIncDAobm4H5aQjmox9ZxEIIqPbiffa8G4=
|
||||
github.com/prometheus/common v0.72.0/go.mod h1:77NWqAQ2tXT7BIK40qjJdw5Acrsrg1TlHAnsQi3i6mk=
|
||||
github.com/prometheus/common v0.73.1 h1:SZWh/j4AvhCdhX8jvYSnxvPxhWlC9QZkjJ+tOrEr7kU=
|
||||
github.com/prometheus/common v0.73.1/go.mod h1:I9aA/Ukz9s1olOobh8KtCkU7B1mQPQsB9UvQPsUQ1Dk=
|
||||
github.com/prometheus/procfs v0.22.0 h1:6q9+/JL9IKAPbCmBrv9n5O5Ty3NKnciV5X7YGw0oics=
|
||||
github.com/prometheus/procfs v0.22.0/go.mod h1:CvmFr/GVhIjIvWJZW3tgkODBQMRIf0EyWMQLHCHab58=
|
||||
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
|
||||
|
||||
@@ -14,7 +14,7 @@ require (
|
||||
github.com/moby/term v0.5.2
|
||||
github.com/prometheus/client_golang v1.25.0
|
||||
github.com/prometheus/client_model v0.6.3
|
||||
github.com/prometheus/common v0.72.0
|
||||
github.com/prometheus/common v0.73.1
|
||||
github.com/prometheus/procfs v0.22.0
|
||||
github.com/spf13/cobra v1.10.2
|
||||
github.com/spf13/pflag v1.0.10
|
||||
@@ -56,6 +56,7 @@ require (
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/kylelemons/godebug v1.1.0 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/pandatix/go-abnf v0.5.5 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||
|
||||
Generated
+4
-2
@@ -91,6 +91,8 @@ github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRW
|
||||
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=
|
||||
github.com/onsi/gomega v1.44.0 h1:eAiGl3Pw5jz5GQdDff0BcxYpAX1JxW8xD7mFUuwNfZQ=
|
||||
github.com/onsi/gomega v1.44.0/go.mod h1:e/C2HwaZ1DhvjzXXuFhcR7hY7Sh9pl7MmoWKEjzwcdA=
|
||||
github.com/pandatix/go-abnf v0.5.5 h1:EXex5KkRQx4Dm6GnpAcQnIMBs9KiK0uf8p/wq+4t9PM=
|
||||
github.com/pandatix/go-abnf v0.5.5/go.mod h1:AE0KD3/VNoCjGzc1gqetRKshbI4SYErXQ/8kPenR5mc=
|
||||
github.com/peterbourgon/diskv/v3 v3.0.1/go.mod h1:kJ5Ny7vLdARGU3WUuy6uzO6T0nb/2gWcT1JiBvRmb5o=
|
||||
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
@@ -99,8 +101,8 @@ github.com/prometheus/client_golang v1.25.0 h1:cDtW1TcFsMi1RJc8khwCKB4VhzwV04aKq
|
||||
github.com/prometheus/client_golang v1.25.0/go.mod h1:j2X/4/kaQHLPnmyEU8zjmkndo1T317VpCIQEml8LTKk=
|
||||
github.com/prometheus/client_model v0.6.3 h1:O0jaTVAYNxTHYInEPFJt5I3+sN8zqBtVMPTB1qyxiEo=
|
||||
github.com/prometheus/client_model v0.6.3/go.mod h1:gpN5P9S7Rr6Yr92PiQ+Ixvhf6JZEkF1dnxsYL2aPBEM=
|
||||
github.com/prometheus/common v0.72.0 h1:tAYsE+sPJxIncDAobm4H5aQjmox9ZxEIIqPbiffa8G4=
|
||||
github.com/prometheus/common v0.72.0/go.mod h1:77NWqAQ2tXT7BIK40qjJdw5Acrsrg1TlHAnsQi3i6mk=
|
||||
github.com/prometheus/common v0.73.1 h1:SZWh/j4AvhCdhX8jvYSnxvPxhWlC9QZkjJ+tOrEr7kU=
|
||||
github.com/prometheus/common v0.73.1/go.mod h1:I9aA/Ukz9s1olOobh8KtCkU7B1mQPQsB9UvQPsUQ1Dk=
|
||||
github.com/prometheus/procfs v0.22.0 h1:6q9+/JL9IKAPbCmBrv9n5O5Ty3NKnciV5X7YGw0oics=
|
||||
github.com/prometheus/procfs v0.22.0/go.mod h1:CvmFr/GVhIjIvWJZW3tgkODBQMRIf0EyWMQLHCHab58=
|
||||
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
|
||||
|
||||
@@ -51,10 +51,11 @@ require (
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/kylelemons/godebug v1.1.0 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/pandatix/go-abnf v0.5.5 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/prometheus/client_golang v1.25.0 // indirect
|
||||
github.com/prometheus/client_model v0.6.3 // indirect
|
||||
github.com/prometheus/common v0.72.0 // indirect
|
||||
github.com/prometheus/common v0.73.1 // indirect
|
||||
github.com/prometheus/procfs v0.22.0 // indirect
|
||||
github.com/spf13/cobra v1.10.2 // indirect
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
|
||||
+4
-2
@@ -116,6 +116,8 @@ github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRW
|
||||
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=
|
||||
github.com/onsi/gomega v1.44.0 h1:eAiGl3Pw5jz5GQdDff0BcxYpAX1JxW8xD7mFUuwNfZQ=
|
||||
github.com/onsi/gomega v1.44.0/go.mod h1:e/C2HwaZ1DhvjzXXuFhcR7hY7Sh9pl7MmoWKEjzwcdA=
|
||||
github.com/pandatix/go-abnf v0.5.5 h1:EXex5KkRQx4Dm6GnpAcQnIMBs9KiK0uf8p/wq+4t9PM=
|
||||
github.com/pandatix/go-abnf v0.5.5/go.mod h1:AE0KD3/VNoCjGzc1gqetRKshbI4SYErXQ/8kPenR5mc=
|
||||
github.com/peterbourgon/diskv/v3 v3.0.1/go.mod h1:kJ5Ny7vLdARGU3WUuy6uzO6T0nb/2gWcT1JiBvRmb5o=
|
||||
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
@@ -124,8 +126,8 @@ github.com/prometheus/client_golang v1.25.0 h1:cDtW1TcFsMi1RJc8khwCKB4VhzwV04aKq
|
||||
github.com/prometheus/client_golang v1.25.0/go.mod h1:j2X/4/kaQHLPnmyEU8zjmkndo1T317VpCIQEml8LTKk=
|
||||
github.com/prometheus/client_model v0.6.3 h1:O0jaTVAYNxTHYInEPFJt5I3+sN8zqBtVMPTB1qyxiEo=
|
||||
github.com/prometheus/client_model v0.6.3/go.mod h1:gpN5P9S7Rr6Yr92PiQ+Ixvhf6JZEkF1dnxsYL2aPBEM=
|
||||
github.com/prometheus/common v0.72.0 h1:tAYsE+sPJxIncDAobm4H5aQjmox9ZxEIIqPbiffa8G4=
|
||||
github.com/prometheus/common v0.72.0/go.mod h1:77NWqAQ2tXT7BIK40qjJdw5Acrsrg1TlHAnsQi3i6mk=
|
||||
github.com/prometheus/common v0.73.1 h1:SZWh/j4AvhCdhX8jvYSnxvPxhWlC9QZkjJ+tOrEr7kU=
|
||||
github.com/prometheus/common v0.73.1/go.mod h1:I9aA/Ukz9s1olOobh8KtCkU7B1mQPQsB9UvQPsUQ1Dk=
|
||||
github.com/prometheus/procfs v0.22.0 h1:6q9+/JL9IKAPbCmBrv9n5O5Ty3NKnciV5X7YGw0oics=
|
||||
github.com/prometheus/procfs v0.22.0/go.mod h1:CvmFr/GVhIjIvWJZW3tgkODBQMRIf0EyWMQLHCHab58=
|
||||
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
|
||||
|
||||
Generated
+2
-1
@@ -46,11 +46,12 @@ github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3x
|
||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||
github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||
github.com/pandatix/go-abnf v0.5.5/go.mod h1:AE0KD3/VNoCjGzc1gqetRKshbI4SYErXQ/8kPenR5mc=
|
||||
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_golang v1.25.0/go.mod h1:j2X/4/kaQHLPnmyEU8zjmkndo1T317VpCIQEml8LTKk=
|
||||
github.com/prometheus/client_model v0.6.3/go.mod h1:gpN5P9S7Rr6Yr92PiQ+Ixvhf6JZEkF1dnxsYL2aPBEM=
|
||||
github.com/prometheus/common v0.72.0/go.mod h1:77NWqAQ2tXT7BIK40qjJdw5Acrsrg1TlHAnsQi3i6mk=
|
||||
github.com/prometheus/common v0.73.1/go.mod h1:I9aA/Ukz9s1olOobh8KtCkU7B1mQPQsB9UvQPsUQ1Dk=
|
||||
github.com/prometheus/procfs v0.22.0/go.mod h1:CvmFr/GVhIjIvWJZW3tgkODBQMRIf0EyWMQLHCHab58=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
|
||||
@@ -46,10 +46,11 @@ require (
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/pandatix/go-abnf v0.5.5 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/prometheus/client_golang v1.25.0 // indirect
|
||||
github.com/prometheus/client_model v0.6.3 // indirect
|
||||
github.com/prometheus/common v0.72.0 // indirect
|
||||
github.com/prometheus/common v0.73.1 // indirect
|
||||
github.com/prometheus/procfs v0.22.0 // indirect
|
||||
github.com/spf13/cobra v1.10.2 // indirect
|
||||
github.com/spf13/pflag v1.0.10 // indirect
|
||||
|
||||
+4
-2
@@ -93,6 +93,8 @@ github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRW
|
||||
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=
|
||||
github.com/onsi/gomega v1.44.0 h1:eAiGl3Pw5jz5GQdDff0BcxYpAX1JxW8xD7mFUuwNfZQ=
|
||||
github.com/onsi/gomega v1.44.0/go.mod h1:e/C2HwaZ1DhvjzXXuFhcR7hY7Sh9pl7MmoWKEjzwcdA=
|
||||
github.com/pandatix/go-abnf v0.5.5 h1:EXex5KkRQx4Dm6GnpAcQnIMBs9KiK0uf8p/wq+4t9PM=
|
||||
github.com/pandatix/go-abnf v0.5.5/go.mod h1:AE0KD3/VNoCjGzc1gqetRKshbI4SYErXQ/8kPenR5mc=
|
||||
github.com/peterbourgon/diskv/v3 v3.0.1/go.mod h1:kJ5Ny7vLdARGU3WUuy6uzO6T0nb/2gWcT1JiBvRmb5o=
|
||||
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
@@ -101,8 +103,8 @@ github.com/prometheus/client_golang v1.25.0 h1:cDtW1TcFsMi1RJc8khwCKB4VhzwV04aKq
|
||||
github.com/prometheus/client_golang v1.25.0/go.mod h1:j2X/4/kaQHLPnmyEU8zjmkndo1T317VpCIQEml8LTKk=
|
||||
github.com/prometheus/client_model v0.6.3 h1:O0jaTVAYNxTHYInEPFJt5I3+sN8zqBtVMPTB1qyxiEo=
|
||||
github.com/prometheus/client_model v0.6.3/go.mod h1:gpN5P9S7Rr6Yr92PiQ+Ixvhf6JZEkF1dnxsYL2aPBEM=
|
||||
github.com/prometheus/common v0.72.0 h1:tAYsE+sPJxIncDAobm4H5aQjmox9ZxEIIqPbiffa8G4=
|
||||
github.com/prometheus/common v0.72.0/go.mod h1:77NWqAQ2tXT7BIK40qjJdw5Acrsrg1TlHAnsQi3i6mk=
|
||||
github.com/prometheus/common v0.73.1 h1:SZWh/j4AvhCdhX8jvYSnxvPxhWlC9QZkjJ+tOrEr7kU=
|
||||
github.com/prometheus/common v0.73.1/go.mod h1:I9aA/Ukz9s1olOobh8KtCkU7B1mQPQsB9UvQPsUQ1Dk=
|
||||
github.com/prometheus/procfs v0.22.0 h1:6q9+/JL9IKAPbCmBrv9n5O5Ty3NKnciV5X7YGw0oics=
|
||||
github.com/prometheus/procfs v0.22.0/go.mod h1:CvmFr/GVhIjIvWJZW3tgkODBQMRIf0EyWMQLHCHab58=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
|
||||
@@ -34,10 +34,11 @@ require (
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/kylelemons/godebug v1.1.0 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/pandatix/go-abnf v0.5.5 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/prometheus/client_golang v1.25.0 // indirect
|
||||
github.com/prometheus/client_model v0.6.3 // indirect
|
||||
github.com/prometheus/common v0.72.0 // indirect
|
||||
github.com/prometheus/common v0.73.1 // indirect
|
||||
github.com/prometheus/procfs v0.22.0 // indirect
|
||||
github.com/spf13/pflag v1.0.10 // indirect
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
|
||||
Generated
+4
-2
@@ -66,6 +66,8 @@ github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRW
|
||||
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=
|
||||
github.com/onsi/gomega v1.44.0 h1:eAiGl3Pw5jz5GQdDff0BcxYpAX1JxW8xD7mFUuwNfZQ=
|
||||
github.com/onsi/gomega v1.44.0/go.mod h1:e/C2HwaZ1DhvjzXXuFhcR7hY7Sh9pl7MmoWKEjzwcdA=
|
||||
github.com/pandatix/go-abnf v0.5.5 h1:EXex5KkRQx4Dm6GnpAcQnIMBs9KiK0uf8p/wq+4t9PM=
|
||||
github.com/pandatix/go-abnf v0.5.5/go.mod h1:AE0KD3/VNoCjGzc1gqetRKshbI4SYErXQ/8kPenR5mc=
|
||||
github.com/peterbourgon/diskv/v3 v3.0.1/go.mod h1:kJ5Ny7vLdARGU3WUuy6uzO6T0nb/2gWcT1JiBvRmb5o=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
@@ -73,8 +75,8 @@ github.com/prometheus/client_golang v1.25.0 h1:cDtW1TcFsMi1RJc8khwCKB4VhzwV04aKq
|
||||
github.com/prometheus/client_golang v1.25.0/go.mod h1:j2X/4/kaQHLPnmyEU8zjmkndo1T317VpCIQEml8LTKk=
|
||||
github.com/prometheus/client_model v0.6.3 h1:O0jaTVAYNxTHYInEPFJt5I3+sN8zqBtVMPTB1qyxiEo=
|
||||
github.com/prometheus/client_model v0.6.3/go.mod h1:gpN5P9S7Rr6Yr92PiQ+Ixvhf6JZEkF1dnxsYL2aPBEM=
|
||||
github.com/prometheus/common v0.72.0 h1:tAYsE+sPJxIncDAobm4H5aQjmox9ZxEIIqPbiffa8G4=
|
||||
github.com/prometheus/common v0.72.0/go.mod h1:77NWqAQ2tXT7BIK40qjJdw5Acrsrg1TlHAnsQi3i6mk=
|
||||
github.com/prometheus/common v0.73.1 h1:SZWh/j4AvhCdhX8jvYSnxvPxhWlC9QZkjJ+tOrEr7kU=
|
||||
github.com/prometheus/common v0.73.1/go.mod h1:I9aA/Ukz9s1olOobh8KtCkU7B1mQPQsB9UvQPsUQ1Dk=
|
||||
github.com/prometheus/procfs v0.22.0 h1:6q9+/JL9IKAPbCmBrv9n5O5Ty3NKnciV5X7YGw0oics=
|
||||
github.com/prometheus/procfs v0.22.0/go.mod h1:CvmFr/GVhIjIvWJZW3tgkODBQMRIf0EyWMQLHCHab58=
|
||||
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
|
||||
|
||||
@@ -64,10 +64,11 @@ require (
|
||||
github.com/moby/spdystream v0.5.1 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect
|
||||
github.com/pandatix/go-abnf v0.5.5 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/prometheus/client_golang v1.25.0 // indirect
|
||||
github.com/prometheus/client_model v0.6.3 // indirect
|
||||
github.com/prometheus/common v0.72.0 // indirect
|
||||
github.com/prometheus/common v0.73.1 // indirect
|
||||
github.com/prometheus/procfs v0.22.0 // indirect
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
go.etcd.io/etcd/api/v3 v3.7.3 // indirect
|
||||
|
||||
+4
-2
@@ -120,6 +120,8 @@ github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J
|
||||
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=
|
||||
github.com/onsi/gomega v1.44.0 h1:eAiGl3Pw5jz5GQdDff0BcxYpAX1JxW8xD7mFUuwNfZQ=
|
||||
github.com/onsi/gomega v1.44.0/go.mod h1:e/C2HwaZ1DhvjzXXuFhcR7hY7Sh9pl7MmoWKEjzwcdA=
|
||||
github.com/pandatix/go-abnf v0.5.5 h1:EXex5KkRQx4Dm6GnpAcQnIMBs9KiK0uf8p/wq+4t9PM=
|
||||
github.com/pandatix/go-abnf v0.5.5/go.mod h1:AE0KD3/VNoCjGzc1gqetRKshbI4SYErXQ/8kPenR5mc=
|
||||
github.com/peterbourgon/diskv/v3 v3.0.1/go.mod h1:kJ5Ny7vLdARGU3WUuy6uzO6T0nb/2gWcT1JiBvRmb5o=
|
||||
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
@@ -128,8 +130,8 @@ github.com/prometheus/client_golang v1.25.0 h1:cDtW1TcFsMi1RJc8khwCKB4VhzwV04aKq
|
||||
github.com/prometheus/client_golang v1.25.0/go.mod h1:j2X/4/kaQHLPnmyEU8zjmkndo1T317VpCIQEml8LTKk=
|
||||
github.com/prometheus/client_model v0.6.3 h1:O0jaTVAYNxTHYInEPFJt5I3+sN8zqBtVMPTB1qyxiEo=
|
||||
github.com/prometheus/client_model v0.6.3/go.mod h1:gpN5P9S7Rr6Yr92PiQ+Ixvhf6JZEkF1dnxsYL2aPBEM=
|
||||
github.com/prometheus/common v0.72.0 h1:tAYsE+sPJxIncDAobm4H5aQjmox9ZxEIIqPbiffa8G4=
|
||||
github.com/prometheus/common v0.72.0/go.mod h1:77NWqAQ2tXT7BIK40qjJdw5Acrsrg1TlHAnsQi3i6mk=
|
||||
github.com/prometheus/common v0.73.1 h1:SZWh/j4AvhCdhX8jvYSnxvPxhWlC9QZkjJ+tOrEr7kU=
|
||||
github.com/prometheus/common v0.73.1/go.mod h1:I9aA/Ukz9s1olOobh8KtCkU7B1mQPQsB9UvQPsUQ1Dk=
|
||||
github.com/prometheus/procfs v0.22.0 h1:6q9+/JL9IKAPbCmBrv9n5O5Ty3NKnciV5X7YGw0oics=
|
||||
github.com/prometheus/procfs v0.22.0/go.mod h1:CvmFr/GVhIjIvWJZW3tgkODBQMRIf0EyWMQLHCHab58=
|
||||
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
|
||||
|
||||
+2
-1
@@ -41,11 +41,12 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8m
|
||||
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=
|
||||
github.com/onsi/gomega v1.44.0 h1:eAiGl3Pw5jz5GQdDff0BcxYpAX1JxW8xD7mFUuwNfZQ=
|
||||
github.com/onsi/gomega v1.44.0/go.mod h1:e/C2HwaZ1DhvjzXXuFhcR7hY7Sh9pl7MmoWKEjzwcdA=
|
||||
github.com/pandatix/go-abnf v0.5.5/go.mod h1:AE0KD3/VNoCjGzc1gqetRKshbI4SYErXQ/8kPenR5mc=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_golang v1.25.0/go.mod h1:j2X/4/kaQHLPnmyEU8zjmkndo1T317VpCIQEml8LTKk=
|
||||
github.com/prometheus/client_model v0.6.3/go.mod h1:gpN5P9S7Rr6Yr92PiQ+Ixvhf6JZEkF1dnxsYL2aPBEM=
|
||||
github.com/prometheus/common v0.72.0/go.mod h1:77NWqAQ2tXT7BIK40qjJdw5Acrsrg1TlHAnsQi3i6mk=
|
||||
github.com/prometheus/common v0.73.1/go.mod h1:I9aA/Ukz9s1olOobh8KtCkU7B1mQPQsB9UvQPsUQ1Dk=
|
||||
github.com/prometheus/procfs v0.22.0/go.mod h1:CvmFr/GVhIjIvWJZW3tgkODBQMRIf0EyWMQLHCHab58=
|
||||
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
|
||||
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
|
||||
|
||||
Generated
+4
-2
@@ -38,14 +38,16 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8m
|
||||
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=
|
||||
github.com/onsi/gomega v1.44.0 h1:eAiGl3Pw5jz5GQdDff0BcxYpAX1JxW8xD7mFUuwNfZQ=
|
||||
github.com/onsi/gomega v1.44.0/go.mod h1:e/C2HwaZ1DhvjzXXuFhcR7hY7Sh9pl7MmoWKEjzwcdA=
|
||||
github.com/pandatix/go-abnf v0.5.5 h1:EXex5KkRQx4Dm6GnpAcQnIMBs9KiK0uf8p/wq+4t9PM=
|
||||
github.com/pandatix/go-abnf v0.5.5/go.mod h1:AE0KD3/VNoCjGzc1gqetRKshbI4SYErXQ/8kPenR5mc=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_golang v1.25.0 h1:cDtW1TcFsMi1RJc8khwCKB4VhzwV04aKqbYtNOXuiK0=
|
||||
github.com/prometheus/client_golang v1.25.0/go.mod h1:j2X/4/kaQHLPnmyEU8zjmkndo1T317VpCIQEml8LTKk=
|
||||
github.com/prometheus/client_model v0.6.3 h1:O0jaTVAYNxTHYInEPFJt5I3+sN8zqBtVMPTB1qyxiEo=
|
||||
github.com/prometheus/client_model v0.6.3/go.mod h1:gpN5P9S7Rr6Yr92PiQ+Ixvhf6JZEkF1dnxsYL2aPBEM=
|
||||
github.com/prometheus/common v0.72.0 h1:tAYsE+sPJxIncDAobm4H5aQjmox9ZxEIIqPbiffa8G4=
|
||||
github.com/prometheus/common v0.72.0/go.mod h1:77NWqAQ2tXT7BIK40qjJdw5Acrsrg1TlHAnsQi3i6mk=
|
||||
github.com/prometheus/common v0.73.1 h1:SZWh/j4AvhCdhX8jvYSnxvPxhWlC9QZkjJ+tOrEr7kU=
|
||||
github.com/prometheus/common v0.73.1/go.mod h1:I9aA/Ukz9s1olOobh8KtCkU7B1mQPQsB9UvQPsUQ1Dk=
|
||||
github.com/prometheus/procfs v0.22.0 h1:6q9+/JL9IKAPbCmBrv9n5O5Ty3NKnciV5X7YGw0oics=
|
||||
github.com/prometheus/procfs v0.22.0/go.mod h1:CvmFr/GVhIjIvWJZW3tgkODBQMRIf0EyWMQLHCHab58=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
|
||||
Generated
+2
-1
@@ -55,12 +55,13 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8m
|
||||
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=
|
||||
github.com/onsi/gomega v1.44.0 h1:eAiGl3Pw5jz5GQdDff0BcxYpAX1JxW8xD7mFUuwNfZQ=
|
||||
github.com/onsi/gomega v1.44.0/go.mod h1:e/C2HwaZ1DhvjzXXuFhcR7hY7Sh9pl7MmoWKEjzwcdA=
|
||||
github.com/pandatix/go-abnf v0.5.5/go.mod h1:AE0KD3/VNoCjGzc1gqetRKshbI4SYErXQ/8kPenR5mc=
|
||||
github.com/peterbourgon/diskv/v3 v3.0.1/go.mod h1:kJ5Ny7vLdARGU3WUuy6uzO6T0nb/2gWcT1JiBvRmb5o=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_golang v1.25.0/go.mod h1:j2X/4/kaQHLPnmyEU8zjmkndo1T317VpCIQEml8LTKk=
|
||||
github.com/prometheus/client_model v0.6.3/go.mod h1:gpN5P9S7Rr6Yr92PiQ+Ixvhf6JZEkF1dnxsYL2aPBEM=
|
||||
github.com/prometheus/common v0.72.0/go.mod h1:77NWqAQ2tXT7BIK40qjJdw5Acrsrg1TlHAnsQi3i6mk=
|
||||
github.com/prometheus/common v0.73.1/go.mod h1:I9aA/Ukz9s1olOobh8KtCkU7B1mQPQsB9UvQPsUQ1Dk=
|
||||
github.com/prometheus/procfs v0.22.0/go.mod h1:CvmFr/GVhIjIvWJZW3tgkODBQMRIf0EyWMQLHCHab58=
|
||||
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
|
||||
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
|
||||
|
||||
Generated
+2
-1
@@ -118,6 +118,7 @@ github.com/onsi/gomega v1.44.0 h1:eAiGl3Pw5jz5GQdDff0BcxYpAX1JxW8xD7mFUuwNfZQ=
|
||||
github.com/onsi/gomega v1.44.0/go.mod h1:e/C2HwaZ1DhvjzXXuFhcR7hY7Sh9pl7MmoWKEjzwcdA=
|
||||
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||
github.com/pandatix/go-abnf v0.5.5/go.mod h1:AE0KD3/VNoCjGzc1gqetRKshbI4SYErXQ/8kPenR5mc=
|
||||
github.com/peterbourgon/diskv/v3 v3.0.1 h1:x06SQA46+PKIUftmEujdwSEpIx8kR+M9eLYsUxeYveU=
|
||||
github.com/peterbourgon/diskv/v3 v3.0.1/go.mod h1:kJ5Ny7vLdARGU3WUuy6uzO6T0nb/2gWcT1JiBvRmb5o=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
@@ -125,7 +126,7 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_golang v1.25.0/go.mod h1:j2X/4/kaQHLPnmyEU8zjmkndo1T317VpCIQEml8LTKk=
|
||||
github.com/prometheus/client_model v0.6.3/go.mod h1:gpN5P9S7Rr6Yr92PiQ+Ixvhf6JZEkF1dnxsYL2aPBEM=
|
||||
github.com/prometheus/common v0.72.0/go.mod h1:77NWqAQ2tXT7BIK40qjJdw5Acrsrg1TlHAnsQi3i6mk=
|
||||
github.com/prometheus/common v0.73.1/go.mod h1:I9aA/Ukz9s1olOobh8KtCkU7B1mQPQsB9UvQPsUQ1Dk=
|
||||
github.com/prometheus/procfs v0.22.0/go.mod h1:CvmFr/GVhIjIvWJZW3tgkODBQMRIf0EyWMQLHCHab58=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
|
||||
Generated
+4
-2
@@ -50,6 +50,8 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8m
|
||||
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=
|
||||
github.com/onsi/gomega v1.44.0 h1:eAiGl3Pw5jz5GQdDff0BcxYpAX1JxW8xD7mFUuwNfZQ=
|
||||
github.com/onsi/gomega v1.44.0/go.mod h1:e/C2HwaZ1DhvjzXXuFhcR7hY7Sh9pl7MmoWKEjzwcdA=
|
||||
github.com/pandatix/go-abnf v0.5.5 h1:EXex5KkRQx4Dm6GnpAcQnIMBs9KiK0uf8p/wq+4t9PM=
|
||||
github.com/pandatix/go-abnf v0.5.5/go.mod h1:AE0KD3/VNoCjGzc1gqetRKshbI4SYErXQ/8kPenR5mc=
|
||||
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
@@ -57,8 +59,8 @@ github.com/prometheus/client_golang v1.25.0 h1:cDtW1TcFsMi1RJc8khwCKB4VhzwV04aKq
|
||||
github.com/prometheus/client_golang v1.25.0/go.mod h1:j2X/4/kaQHLPnmyEU8zjmkndo1T317VpCIQEml8LTKk=
|
||||
github.com/prometheus/client_model v0.6.3 h1:O0jaTVAYNxTHYInEPFJt5I3+sN8zqBtVMPTB1qyxiEo=
|
||||
github.com/prometheus/client_model v0.6.3/go.mod h1:gpN5P9S7Rr6Yr92PiQ+Ixvhf6JZEkF1dnxsYL2aPBEM=
|
||||
github.com/prometheus/common v0.72.0 h1:tAYsE+sPJxIncDAobm4H5aQjmox9ZxEIIqPbiffa8G4=
|
||||
github.com/prometheus/common v0.72.0/go.mod h1:77NWqAQ2tXT7BIK40qjJdw5Acrsrg1TlHAnsQi3i6mk=
|
||||
github.com/prometheus/common v0.73.1 h1:SZWh/j4AvhCdhX8jvYSnxvPxhWlC9QZkjJ+tOrEr7kU=
|
||||
github.com/prometheus/common v0.73.1/go.mod h1:I9aA/Ukz9s1olOobh8KtCkU7B1mQPQsB9UvQPsUQ1Dk=
|
||||
github.com/prometheus/procfs v0.22.0 h1:6q9+/JL9IKAPbCmBrv9n5O5Ty3NKnciV5X7YGw0oics=
|
||||
github.com/prometheus/procfs v0.22.0/go.mod h1:CvmFr/GVhIjIvWJZW3tgkODBQMRIf0EyWMQLHCHab58=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
|
||||
@@ -54,10 +54,11 @@ require (
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/kylelemons/godebug v1.1.0 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/pandatix/go-abnf v0.5.5 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/prometheus/client_golang v1.25.0 // indirect
|
||||
github.com/prometheus/client_model v0.6.3 // indirect
|
||||
github.com/prometheus/common v0.72.0 // indirect
|
||||
github.com/prometheus/common v0.73.1 // indirect
|
||||
github.com/prometheus/procfs v0.22.0 // indirect
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
go.etcd.io/etcd/api/v3 v3.7.3 // indirect
|
||||
|
||||
+4
-2
@@ -116,6 +116,8 @@ github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRW
|
||||
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=
|
||||
github.com/onsi/gomega v1.44.0 h1:eAiGl3Pw5jz5GQdDff0BcxYpAX1JxW8xD7mFUuwNfZQ=
|
||||
github.com/onsi/gomega v1.44.0/go.mod h1:e/C2HwaZ1DhvjzXXuFhcR7hY7Sh9pl7MmoWKEjzwcdA=
|
||||
github.com/pandatix/go-abnf v0.5.5 h1:EXex5KkRQx4Dm6GnpAcQnIMBs9KiK0uf8p/wq+4t9PM=
|
||||
github.com/pandatix/go-abnf v0.5.5/go.mod h1:AE0KD3/VNoCjGzc1gqetRKshbI4SYErXQ/8kPenR5mc=
|
||||
github.com/peterbourgon/diskv/v3 v3.0.1/go.mod h1:kJ5Ny7vLdARGU3WUuy6uzO6T0nb/2gWcT1JiBvRmb5o=
|
||||
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
@@ -124,8 +126,8 @@ github.com/prometheus/client_golang v1.25.0 h1:cDtW1TcFsMi1RJc8khwCKB4VhzwV04aKq
|
||||
github.com/prometheus/client_golang v1.25.0/go.mod h1:j2X/4/kaQHLPnmyEU8zjmkndo1T317VpCIQEml8LTKk=
|
||||
github.com/prometheus/client_model v0.6.3 h1:O0jaTVAYNxTHYInEPFJt5I3+sN8zqBtVMPTB1qyxiEo=
|
||||
github.com/prometheus/client_model v0.6.3/go.mod h1:gpN5P9S7Rr6Yr92PiQ+Ixvhf6JZEkF1dnxsYL2aPBEM=
|
||||
github.com/prometheus/common v0.72.0 h1:tAYsE+sPJxIncDAobm4H5aQjmox9ZxEIIqPbiffa8G4=
|
||||
github.com/prometheus/common v0.72.0/go.mod h1:77NWqAQ2tXT7BIK40qjJdw5Acrsrg1TlHAnsQi3i6mk=
|
||||
github.com/prometheus/common v0.73.1 h1:SZWh/j4AvhCdhX8jvYSnxvPxhWlC9QZkjJ+tOrEr7kU=
|
||||
github.com/prometheus/common v0.73.1/go.mod h1:I9aA/Ukz9s1olOobh8KtCkU7B1mQPQsB9UvQPsUQ1Dk=
|
||||
github.com/prometheus/procfs v0.22.0 h1:6q9+/JL9IKAPbCmBrv9n5O5Ty3NKnciV5X7YGw0oics=
|
||||
github.com/prometheus/procfs v0.22.0/go.mod h1:CvmFr/GVhIjIvWJZW3tgkODBQMRIf0EyWMQLHCHab58=
|
||||
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
|
||||
|
||||
@@ -53,10 +53,11 @@ require (
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/kylelemons/godebug v1.1.0 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/pandatix/go-abnf v0.5.5 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/prometheus/client_golang v1.25.0 // indirect
|
||||
github.com/prometheus/client_model v0.6.3 // indirect
|
||||
github.com/prometheus/common v0.72.0 // indirect
|
||||
github.com/prometheus/common v0.73.1 // indirect
|
||||
github.com/prometheus/procfs v0.22.0 // indirect
|
||||
github.com/spf13/pflag v1.0.10 // indirect
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
|
||||
+4
-2
@@ -117,6 +117,8 @@ github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRW
|
||||
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=
|
||||
github.com/onsi/gomega v1.44.0 h1:eAiGl3Pw5jz5GQdDff0BcxYpAX1JxW8xD7mFUuwNfZQ=
|
||||
github.com/onsi/gomega v1.44.0/go.mod h1:e/C2HwaZ1DhvjzXXuFhcR7hY7Sh9pl7MmoWKEjzwcdA=
|
||||
github.com/pandatix/go-abnf v0.5.5 h1:EXex5KkRQx4Dm6GnpAcQnIMBs9KiK0uf8p/wq+4t9PM=
|
||||
github.com/pandatix/go-abnf v0.5.5/go.mod h1:AE0KD3/VNoCjGzc1gqetRKshbI4SYErXQ/8kPenR5mc=
|
||||
github.com/peterbourgon/diskv/v3 v3.0.1/go.mod h1:kJ5Ny7vLdARGU3WUuy6uzO6T0nb/2gWcT1JiBvRmb5o=
|
||||
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
@@ -125,8 +127,8 @@ github.com/prometheus/client_golang v1.25.0 h1:cDtW1TcFsMi1RJc8khwCKB4VhzwV04aKq
|
||||
github.com/prometheus/client_golang v1.25.0/go.mod h1:j2X/4/kaQHLPnmyEU8zjmkndo1T317VpCIQEml8LTKk=
|
||||
github.com/prometheus/client_model v0.6.3 h1:O0jaTVAYNxTHYInEPFJt5I3+sN8zqBtVMPTB1qyxiEo=
|
||||
github.com/prometheus/client_model v0.6.3/go.mod h1:gpN5P9S7Rr6Yr92PiQ+Ixvhf6JZEkF1dnxsYL2aPBEM=
|
||||
github.com/prometheus/common v0.72.0 h1:tAYsE+sPJxIncDAobm4H5aQjmox9ZxEIIqPbiffa8G4=
|
||||
github.com/prometheus/common v0.72.0/go.mod h1:77NWqAQ2tXT7BIK40qjJdw5Acrsrg1TlHAnsQi3i6mk=
|
||||
github.com/prometheus/common v0.73.1 h1:SZWh/j4AvhCdhX8jvYSnxvPxhWlC9QZkjJ+tOrEr7kU=
|
||||
github.com/prometheus/common v0.73.1/go.mod h1:I9aA/Ukz9s1olOobh8KtCkU7B1mQPQsB9UvQPsUQ1Dk=
|
||||
github.com/prometheus/procfs v0.22.0 h1:6q9+/JL9IKAPbCmBrv9n5O5Ty3NKnciV5X7YGw0oics=
|
||||
github.com/prometheus/procfs v0.22.0/go.mod h1:CvmFr/GVhIjIvWJZW3tgkODBQMRIf0EyWMQLHCHab58=
|
||||
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
|
||||
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
# IDE files
|
||||
.vscode/*
|
||||
|
||||
# Benchmark files
|
||||
*.pprof
|
||||
*.golden
|
||||
|
||||
# Fuzzing files
|
||||
testdata/fuzz/*
|
||||
|
||||
# Generated files from pap
|
||||
*.mmd
|
||||
*.svg
|
||||
|
||||
# Documents
|
||||
*.pdf
|
||||
*.bkp
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
before:
|
||||
hooks:
|
||||
- go mod tidy
|
||||
|
||||
gomod:
|
||||
proxy: true
|
||||
|
||||
builds:
|
||||
- id: pap
|
||||
main: ./cmd/pap
|
||||
binary: pap
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
goos:
|
||||
- linux
|
||||
goarch:
|
||||
- amd64
|
||||
|
||||
archives:
|
||||
- id: pap
|
||||
builds:
|
||||
- pap
|
||||
|
||||
changelog:
|
||||
sort: asc
|
||||
filters:
|
||||
exclude:
|
||||
- '^docs:'
|
||||
- '^test:'
|
||||
|
||||
source:
|
||||
enabled: true
|
||||
|
||||
checksum: {}
|
||||
|
||||
sboms:
|
||||
- artifacts: source
|
||||
+128
@@ -0,0 +1,128 @@
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
We as members, contributors, and leaders pledge to make participation in our
|
||||
community a harassment-free experience for everyone, regardless of age, body
|
||||
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||
identity and expression, level of experience, education, socio-economic status,
|
||||
nationality, personal appearance, race, religion, or sexual identity
|
||||
and orientation.
|
||||
|
||||
We pledge to act and interact in ways that contribute to an open, welcoming,
|
||||
diverse, inclusive, and healthy community.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to a positive environment for our
|
||||
community include:
|
||||
|
||||
* Demonstrating empathy and kindness toward other people
|
||||
* Being respectful of differing opinions, viewpoints, and experiences
|
||||
* Giving and gracefully accepting constructive feedback
|
||||
* Accepting responsibility and apologizing to those affected by our mistakes,
|
||||
and learning from the experience
|
||||
* Focusing on what is best not just for us as individuals, but for the
|
||||
overall community
|
||||
|
||||
Examples of unacceptable behavior include:
|
||||
|
||||
* The use of sexualized language or imagery, and sexual attention or
|
||||
advances of any kind
|
||||
* Trolling, insulting or derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or email
|
||||
address, without their explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Enforcement Responsibilities
|
||||
|
||||
Community leaders are responsible for clarifying and enforcing our standards of
|
||||
acceptable behavior and will take appropriate and fair corrective action in
|
||||
response to any behavior that they deem inappropriate, threatening, offensive,
|
||||
or harmful.
|
||||
|
||||
Community leaders have the right and responsibility to remove, edit, or reject
|
||||
comments, commits, code, wiki edits, issues, and other contributions that are
|
||||
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
||||
decisions when appropriate.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies within all community spaces, and also applies when
|
||||
an individual is officially representing the community in public spaces.
|
||||
Examples of representing our community include using an official e-mail address,
|
||||
posting via an official social media account, or acting as an appointed
|
||||
representative at an online or offline event.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported to the community leaders responsible for enforcement at
|
||||
lucastesson@protonmail.com.
|
||||
All complaints will be reviewed and investigated promptly and fairly.
|
||||
|
||||
All community leaders are obligated to respect the privacy and security of the
|
||||
reporter of any incident.
|
||||
|
||||
## Enforcement Guidelines
|
||||
|
||||
Community leaders will follow these Community Impact Guidelines in determining
|
||||
the consequences for any action they deem in violation of this Code of Conduct:
|
||||
|
||||
### 1. Correction
|
||||
|
||||
**Community Impact**: Use of inappropriate language or other behavior deemed
|
||||
unprofessional or unwelcome in the community.
|
||||
|
||||
**Consequence**: A private, written warning from community leaders, providing
|
||||
clarity around the nature of the violation and an explanation of why the
|
||||
behavior was inappropriate. A public apology may be requested.
|
||||
|
||||
### 2. Warning
|
||||
|
||||
**Community Impact**: A violation through a single incident or series
|
||||
of actions.
|
||||
|
||||
**Consequence**: A warning with consequences for continued behavior. No
|
||||
interaction with the people involved, including unsolicited interaction with
|
||||
those enforcing the Code of Conduct, for a specified period of time. This
|
||||
includes avoiding interactions in community spaces as well as external channels
|
||||
like social media. Violating these terms may lead to a temporary or
|
||||
permanent ban.
|
||||
|
||||
### 3. Temporary Ban
|
||||
|
||||
**Community Impact**: A serious violation of community standards, including
|
||||
sustained inappropriate behavior.
|
||||
|
||||
**Consequence**: A temporary ban from any sort of interaction or public
|
||||
communication with the community for a specified period of time. No public or
|
||||
private interaction with the people involved, including unsolicited interaction
|
||||
with those enforcing the Code of Conduct, is allowed during this period.
|
||||
Violating these terms may lead to a permanent ban.
|
||||
|
||||
### 4. Permanent Ban
|
||||
|
||||
**Community Impact**: Demonstrating a pattern of violation of community
|
||||
standards, including sustained inappropriate behavior, harassment of an
|
||||
individual, or aggression toward or disparagement of classes of individuals.
|
||||
|
||||
**Consequence**: A permanent ban from any sort of public interaction within
|
||||
the community.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
||||
version 2.0, available at
|
||||
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
|
||||
|
||||
Community Impact Guidelines were inspired by [Mozilla's code of conduct
|
||||
enforcement ladder](https://github.com/mozilla/diversity).
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
|
||||
For answers to common questions about this code of conduct, see the FAQ at
|
||||
https://www.contributor-covenant.org/faq. Translations are available at
|
||||
https://www.contributor-covenant.org/translations.
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
# Contributing
|
||||
|
||||
Open-Source means we welcome all changes and ideas any time of day and nights, through issues and pull requests.
|
||||
|
||||
## How to contribute
|
||||
|
||||
As the community around this project is small enough, we set the following contributing guidelines:
|
||||
- if necessary, [open an issue](https://github.com/pandatix/go-abnf/issues/new) first
|
||||
- fork the repository
|
||||
- clone, commit, and when feeling confident enough [create a pull request](https://github.com/pandatix/go-abnf/compare) with the issue(s) it solves.
|
||||
- check your notifications for comments, suggestions, and maybe an approval
|
||||
|
||||
## Quality
|
||||
|
||||
This implementation focuses on the IETF RFCs, security and performances.
|
||||
We expect your contribution to provide a minimum quality with unitary tests when applicable.
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2024 Lucas TESSON - PandatiX <lucastesson@protonmail.com>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
+156
@@ -0,0 +1,156 @@
|
||||
<div align="center">
|
||||
<h1>Go-ABNF</h1>
|
||||
<a href="https://pkg.go.dev/github.com/pandatix/go-abnf"><img src="https://shields.io/badge/-reference-blue?logo=go&style=for-the-badge" alt="reference"></a>
|
||||
<a href="https://goreportcard.com/report/github.com/pandatix/go-abnf"><img src="https://goreportcard.com/badge/github.com/pandatix/go-abnf?style=for-the-badge" alt="go report"></a>
|
||||
<a href="https://coveralls.io/github/pandatix/go-abnf?branch=main"><img src="https://img.shields.io/coverallsCoverage/github/pandatix/go-abnf?style=for-the-badge" alt="Coverage Status"></a>
|
||||
<br>
|
||||
<a href=""><img src="https://img.shields.io/github/license/pandatix/go-abnf?style=for-the-badge" alt="License"></a>
|
||||
<a href="https://github.com/pandatix/go-abnf/actions?query=workflow%3Aci+"><img src="https://img.shields.io/github/actions/workflow/status/pandatix/go-abnf/ci.yaml?style=for-the-badge&label=CI" alt="CI"></a>
|
||||
<a href="https://github.com/pandatix/go-abnf/actions/workflows/codeql-analysis.yaml"><img src="https://img.shields.io/github/actions/workflow/status/pandatix/go-abnf/codeql-analysis.yaml?style=for-the-badge&label=CodeQL" alt="CodeQL"></a>
|
||||
<br>
|
||||
<a href="https://securityscorecards.dev/viewer/?uri=github.com/pandatix/go-abnf"><img src="https://img.shields.io/ossf-scorecard/github.com/pandatix/go-abnf?label=openssf%20scorecard&style=for-the-badge" alt="OpenSSF Scoreboard"></a>
|
||||
<a href="https://bestpractices.coreinfrastructure.org/en/projects/7840"><img src="https://img.shields.io/cii/summary/7840?style=for-the-badge&label=openssf%20best%20practices" alt="OpenSSF Best Practices Summary"></a>
|
||||
</div>
|
||||
|
||||
A dependency-free Go module for Augmented Backus-Naur Form (ABNF). It parses an ABNF grammar into a data structure and then lets you *recognise*, *parse*, *generate*, *compile to regex*, *visualise*, and even *generate a standalone Go parser* from it. It implements [RFC 5234](https://www.rfc-editor.org/rfc/rfc5234) and [RFC 7405](https://www.rfc-editor.org/rfc/rfc7405), with Errata [2968](https://errata.rfc-editor.org/eid2968/) and [3076](https://errata.rfc-editor.org/eid3076/).
|
||||
|
||||
> [!NOTE]
|
||||
> **Intended use.** Go-ABNF is built first and foremost as a **testing oracle**.
|
||||
> Because it can turn one grammar into several independent representations (recognizer, parse forest, regex, generator, transition graph), it is well suited to **differential testing and fuzzing**: generate inputs from a grammar, feed them to your own implementation, and cross-check the verdicts against this library - or cross-check the library's own views against each other. It favors correctness and generality (ambiguous and left-recursive grammars, Unicode) over being the fastest production parser.
|
||||
|
||||
## Capabilities
|
||||
|
||||
- Parse ABNF into a manipulable `*Grammar` (with cycle / DAG detection).
|
||||
- Recognize input against a grammar - ambiguous and left-recursive grammars included.
|
||||
- Build a full **parse forest** (SPPF) or **binary-subtree set** (BSR): count trees, detect ambiguity, extract a tree.
|
||||
- Compile a grammar to a **regular expression** (precedence-aware, size-bounded).
|
||||
- **Generate** inputs (random walk), a minimal covering test set, or structured ASTs - for fuzzing.
|
||||
- **Visualize** a grammar as a transition graph (Mermaid).
|
||||
- **Generate a standalone, specialized Go parser** from a grammar (`go generate`).
|
||||
- Unicode code points and 64-bit numeric values; bounded against DoS.
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A["ABNF text"] -->|ParseABNF| G["*Grammar"]
|
||||
G -->|IsValid| R["recognise<br/>(valid?)"]
|
||||
G -->|ParseForest / ParseBSR| F["parse forest<br/>(trees, ambiguity)"]
|
||||
G -->|Regex| X["regular expression"]
|
||||
G -->|Generate / ASTGenerator| S["sample inputs"]
|
||||
G -->|TransitionGraph| T["graph (Mermaid)"]
|
||||
G -->|GenerateGoParser| C["standalone Go parser"]
|
||||
S -. feed .-> SUT["your implementation"]
|
||||
R -. cross-check .- F
|
||||
F -. cross-check .- X
|
||||
```
|
||||
|
||||
## How it works
|
||||
|
||||
ABNF is *self-describing*: its own syntax is written in ABNF. Go-ABNF bootstraps from a handwritten parse/evaluation of that meta-grammar, after which any valid grammar parsed by the library can in turn be used to parse new input - so the engine is generic, not specialized to ABNF.
|
||||
|
||||
<div align="center">
|
||||
<img src="res/grammar.excalidraw.png" width="800px">
|
||||
</div>
|
||||
|
||||
Two parse paths exist by design:
|
||||
- **Recognition** (`Grammar.IsValid`) answers *does this input match?* by tracking the set of reachable input positions. It never materializes a tree, so it is cheap and is the reference verdict for the other paths.
|
||||
- **Parsing** builds a derivation representation using a **GLL** engine [1], which handles arbitrary context-free grammars including ambiguity and (left/right/hidden) recursion. Two representations are available over the same engine: a **Shared Packed Parse Forest** [2] (`ParseForest`) and a **Binary Subtree Set** [3] (`ParseBSR`). Both expose `Valid`, `NumTrees`, `Ambiguous` and `Tree`.
|
||||
|
||||
Repetition (`Min*Max element`) is handled *natively*: it lowers to a single self-recursive nonterminal and the bound is enforced by a counter carried in parser state, rather than unrolled - so a grammar like `0*9999999999 "x"` costs $O(1)$ to set up instead of exhausting memory.
|
||||
|
||||
The generated-parser work targets BSR representation [3], in the spirit of [GoGLL](https://github.com/goccmack/gogll).
|
||||
|
||||
References:
|
||||
- [1] _E. Scott, A. Johnstone. "GLL Parsing," in Electronic Notes in Theoretical Computer Science, vol. 253, no. 7, pp. 177-189, 2010._
|
||||
- [2] _E. Scott, A. Johnstone. "GLL parse-tree generation," in Science of Computer Programming, vol. 78, no. 10, pp. 1828-1844, 2013._
|
||||
- [3] _E. Scott, A. Johnstone, L. van Binsbergen. "Derivation representation using binary subtree sets," in Science of Computer Programming, vol. 175, pp. 63-84, 2019._
|
||||
|
||||
## Usage
|
||||
|
||||
Parse a grammar, then recognize or parse input:
|
||||
|
||||
```go
|
||||
g, err := goabnf.ParseABNF(grammarBytes) // []byte of ABNF, CRLF line endings
|
||||
if err != nil { /* ... */ }
|
||||
|
||||
ok, _ := g.IsValid("rule", input) // boolean recognizer
|
||||
|
||||
f, _ := goabnf.ParseForest(input, g, "rule")
|
||||
fmt.Println(f.Valid(), f.NumTrees(), f.Ambiguous())
|
||||
|
||||
bf, _ := goabnf.ParseBSR(input, g, "rule") // same answers, BSR representation
|
||||
```
|
||||
|
||||
Compile to a regular expression, or render a transition graph:
|
||||
|
||||
```go
|
||||
re, _ := g.Regex("rule", goabnf.WithMaxRegexLen(4096))
|
||||
|
||||
tg, _ := g.TransitionGraph("rule")
|
||||
fmt.Println(tg.ToMermaid())
|
||||
```
|
||||
|
||||
### As a fuzzing oracle
|
||||
|
||||
Generate inputs from a grammar to drive Go's fuzzer, even when the input format is too specific for the engine to discover on its own:
|
||||
```go
|
||||
func FuzzMyParser(f *testing.F) {
|
||||
g, err := goabnf.ParseABNF(myGrammar)
|
||||
if err != nil { f.Fatal(err) }
|
||||
|
||||
f.Fuzz(func(t *testing.T, seed int64) {
|
||||
input, _ := g.Generate(seed, "rule",
|
||||
goabnf.WithRepMax(15), // cap repetitions
|
||||
goabnf.WithThreshold(1024), // cap generated size
|
||||
)
|
||||
MyParser(input) // must not panic; optionally cross-check verdicts
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
More fuzzing helpers are [documented here](examples/fuzzing/).
|
||||
|
||||
### As a differential oracle
|
||||
|
||||
The recognizer and the parse forest are independent code paths over the same grammar; agreement between them (and with your own implementation) is a strong correctness signal:
|
||||
```go
|
||||
ok, _ := g.IsValid("rule", input)
|
||||
f, _ := goabnf.ParseForest(input, g, "rule")
|
||||
if ok != f.Valid() {
|
||||
t.Fatalf("oracle disagreement on %q", input) // a bug in grammar or engine
|
||||
}
|
||||
```
|
||||
|
||||
## Code generation
|
||||
|
||||
Generate a standalone, specialized parser for a grammar - a jump-table GLL/BSR parser with inlined terminals, depending only on the standard library:
|
||||
```go
|
||||
src, _ := goabnf.GenerateGoParser(g, "rule", "myparser")
|
||||
// or, straight from ABNF source:
|
||||
src, _ = goabnf.GenerateGoParserFromABNF(grammarBytes, "rule", "myparser")
|
||||
```
|
||||
|
||||
Wire it into `go generate` with the `cmd/abnf-gen` command:
|
||||
|
||||
```go
|
||||
//go:generate go run github.com/pandatix/go-abnf/cmd/abnf-gen -in grammar.abnf -root rule -pkg myparser -out parser_gen.go
|
||||
```
|
||||
|
||||
The generated file exposes `Parse`, `Result` (`Valid`/`NumTrees`/`Ambiguous`/ `Tree`) and `ParseTree`. Its identifiers are package-scoped, so generate **one parser per package** (e.g. a dedicated `internal/myparser/`). The generated parser is faithful to the grammar you give it: feeding the *raw* RFC 5234 ABNF self-definition produces an ambiguous parser (the defect Erratum 2968 fixes), while the corrected grammar produces an unambiguous one - choose deliberately, and assert the expected tree count in a test.
|
||||
|
||||
## Safety
|
||||
|
||||
The engines are bounded to stay usable on untrusted grammars and inputs: `WithMaxNodes` (transition graph), `WithMaxForestNodes` / `WithMaxSlots` (forest), `WithMaxRegexLen` (regex), and the generated parser's `MaxElements`. Numeric values are accepted up to 64 bits without panicking, and matching is Unicode-aware.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### My ABNF grammar does not work
|
||||
|
||||
**Q**: My ABNF grammar does not work. Do you have any idea why ?
|
||||
|
||||
**A**: There could be many reasons to this. First make sure your grammar ends up by a newline (LF), and especially that the input content has a CR LF. As those appear the same, it is often a source of error.
|
||||
|
||||
### Difference between pap and bap
|
||||
|
||||
**Q**: Is there a difference between pap and [bap](https://github.com/ietf-tools/bap) ?
|
||||
|
||||
**A**: Being written in Go makes `pap` more portable and easier to embed in workflows. Beyond that, the goals differ: `bap` aims to produce meaningful end-user errors, whereas `pap` is built to *challenge* it - and goes further by building transition graphs for fuzzing Go code and by supporting Unicode code points (useful for, e.g., [TOML, which is specified in ABNF](https://github.com/toml-lang/toml/blob/main/toml.abnf)).
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
# Reporting Security Issues
|
||||
|
||||
Please report any security issues you discovered in the API to lucastesson@protonmail.com.
|
||||
|
||||
We will assess the risk, plus make a fix available before we create a GitHub issue.
|
||||
|
||||
Thank you for your contribution.
|
||||
|
||||
## Refering to this repository
|
||||
|
||||
To refer to this repository using a CPE v2.3, please use `cpe:2.3:a:pandatix:go-abnf:*:*:*:*:*:*:*:*` with the `version` set to the tag you are using.
|
||||
+244
@@ -0,0 +1,244 @@
|
||||
package goabnf
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ElemItf defines the interface of all the element alternations:
|
||||
// - ElemRulename
|
||||
// - ElemGroup
|
||||
// - ElemOption
|
||||
// - ElemProseVal
|
||||
// - ElemNumVal
|
||||
// - ElemCharVal
|
||||
//
|
||||
// This is exposed for custom evaluation purposes, please don't use it else.
|
||||
type ElemItf interface {
|
||||
fmt.Stringer
|
||||
}
|
||||
|
||||
// Rule represents an ABNF rule, with its name and underlying alternation.
|
||||
//
|
||||
// This is exposed for custom evaluation purposes, please don't use it else.
|
||||
type Rule struct {
|
||||
// Name is the unique rule name.
|
||||
// Notice it is case-insensitive according to RFC 5234 Section 2.1.
|
||||
Name string
|
||||
|
||||
Alternation Alternation
|
||||
}
|
||||
|
||||
func (rl Rule) String() string {
|
||||
return fmt.Sprintf("%s = %s", rl.Name, rl.Alternation)
|
||||
}
|
||||
|
||||
// Alternation represents an ABNF alternation object.
|
||||
//
|
||||
// This is exposed for custom evaluation purposes, please don't use it else.
|
||||
type Alternation struct {
|
||||
// Concatenations contains the variants that validate the upper
|
||||
// object (i.e. rule, group or option).
|
||||
Concatenations []Concatenation
|
||||
}
|
||||
|
||||
func (alt Alternation) String() string {
|
||||
str := ""
|
||||
for _, concat := range alt.Concatenations {
|
||||
str += fmt.Sprintf("%s / ", concat)
|
||||
}
|
||||
return strings.TrimSuffix(str, " / ")
|
||||
}
|
||||
|
||||
// Concatenation represents an ABNF concatenation object.
|
||||
//
|
||||
// This is exposed for custom evaluation purposes, please don't use it else.
|
||||
type Concatenation struct {
|
||||
// Repetitions contains the following repetitions, order matter.
|
||||
Repetitions []Repetition
|
||||
}
|
||||
|
||||
func (cnt Concatenation) String() string {
|
||||
str := ""
|
||||
for _, rep := range cnt.Repetitions {
|
||||
str += fmt.Sprintf("%s ", rep)
|
||||
}
|
||||
return strings.TrimSuffix(str, " ")
|
||||
}
|
||||
|
||||
// Repetition represents an ABNF repetition object.
|
||||
//
|
||||
// This is exposed for custom evaluation purposes, please don't use it else.
|
||||
type Repetition struct {
|
||||
Min, Max int
|
||||
Element ElemItf
|
||||
}
|
||||
|
||||
func (rep Repetition) String() string {
|
||||
if rep.Min == rep.Max {
|
||||
if rep.Min == 1 {
|
||||
return rep.Element.String()
|
||||
}
|
||||
return strconv.Itoa(rep.Min) + rep.Element.String()
|
||||
}
|
||||
str := ""
|
||||
if rep.Min != 0 {
|
||||
str += strconv.Itoa(rep.Min)
|
||||
}
|
||||
str += "*"
|
||||
if rep.Max != inf {
|
||||
str += strconv.Itoa(rep.Max)
|
||||
}
|
||||
return str + rep.Element.String()
|
||||
}
|
||||
|
||||
// ElemRulename represents an ABNF rulename object.
|
||||
//
|
||||
// This is exposed for custom evaluation purposes, please don't use it else.
|
||||
type ElemRulename struct {
|
||||
// Name is the unique rule name.
|
||||
// Notice it is case-insensitive according to RFC 5234 Section 2.1.
|
||||
Name string
|
||||
}
|
||||
|
||||
func (erln ElemRulename) String() string {
|
||||
return erln.Name
|
||||
}
|
||||
|
||||
var _ ElemItf = (*ElemRulename)(nil)
|
||||
|
||||
// ElemGroup represents an ABNF group object.
|
||||
// It will be straightly passed through with its underlying alternation.
|
||||
//
|
||||
// This is exposed for custom evaluation purposes, please don't use it else.
|
||||
type ElemGroup struct {
|
||||
Alternation Alternation
|
||||
}
|
||||
|
||||
func (egrp ElemGroup) String() string {
|
||||
return "(" + egrp.Alternation.String() + ")"
|
||||
}
|
||||
|
||||
var _ ElemItf = (*ElemGroup)(nil)
|
||||
|
||||
// ElemOption represents an ABNF option object.
|
||||
// It will be straightly converted to a 0*1 repetition.
|
||||
//
|
||||
// This is exposed for custom evaluation purposes, please don't use it else.
|
||||
type ElemOption struct {
|
||||
Alternation Alternation
|
||||
}
|
||||
|
||||
func (eopt ElemOption) String() string {
|
||||
return "[" + eopt.Alternation.String() + "]"
|
||||
}
|
||||
|
||||
var _ ElemItf = (*ElemOption)(nil)
|
||||
|
||||
// ElemCharVal represents an ABNF char-val object.
|
||||
//
|
||||
// This is exposed for custom evaluation purposes, please don't use it else.
|
||||
type ElemCharVal struct {
|
||||
// Sensitive is by default false, support clarrified by RFC 7405 hence
|
||||
// only works on a-z and A-Z.
|
||||
Sensitive bool
|
||||
Values []rune
|
||||
}
|
||||
|
||||
func (ecvl ElemCharVal) String() string {
|
||||
str := ""
|
||||
for _, val := range ecvl.Values {
|
||||
str += string(val)
|
||||
}
|
||||
return `"` + str + `"`
|
||||
}
|
||||
|
||||
var _ ElemItf = (*ElemCharVal)(nil)
|
||||
|
||||
// ElemNumVal represents an ABNF num-val object.
|
||||
//
|
||||
// This is exposed for custom evaluation purposes, please don't use it else.
|
||||
type ElemNumVal struct {
|
||||
Base string
|
||||
// Status could be:
|
||||
// - `statSeries`: `elems` contains all the expected
|
||||
// values in the order of the grammar defined them ;
|
||||
// - `statRange`: `elems` contains the start and end
|
||||
// bounds (so no more than two).
|
||||
Status Status
|
||||
Elems []string
|
||||
}
|
||||
|
||||
func (envl ElemNumVal) String() string {
|
||||
str := "%" + envl.Base
|
||||
spl := "."
|
||||
if envl.Status == StatRange {
|
||||
spl = "-"
|
||||
}
|
||||
for _, val := range envl.Elems {
|
||||
str += val + spl
|
||||
}
|
||||
return strings.TrimSuffix(str, spl)
|
||||
}
|
||||
|
||||
var _ ElemItf = (*ElemNumVal)(nil)
|
||||
|
||||
// ElemProseVal represents an ABNF prose-val object.
|
||||
//
|
||||
// This is exposed for custom evaluation purposes, please don't use it else.
|
||||
type ElemProseVal struct {
|
||||
values []string
|
||||
}
|
||||
|
||||
func (epvl ElemProseVal) String() string {
|
||||
str := ""
|
||||
for _, val := range epvl.values {
|
||||
str += val
|
||||
}
|
||||
return "<" + str + ">"
|
||||
}
|
||||
|
||||
var _ ElemItf = (*ElemProseVal)(nil)
|
||||
|
||||
// Status defines the type of a num-val, rather StatSeries or StatRange.
|
||||
//
|
||||
// This is exposed for custom evaluation purposes, please don't use it else.
|
||||
type Status int
|
||||
|
||||
const (
|
||||
// StatSeries represents a serie of unique byte value.
|
||||
StatSeries Status = iota
|
||||
// StatRange represents an interval of possible bytes.
|
||||
StatRange
|
||||
)
|
||||
|
||||
// ABNF is the manually parsed+evaluated+validated ABNF grammar.
|
||||
var ABNF = &Grammar{
|
||||
Rulemap: map[string]*Rule{
|
||||
abnfRulelist.Name: abnfRulelist,
|
||||
abnfRule.Name: abnfRule,
|
||||
abnfRulename.Name: abnfRulename,
|
||||
abnfDefinedAs.Name: abnfDefinedAs,
|
||||
abnfElements.Name: abnfElements,
|
||||
abnfCWsp.Name: abnfCWsp,
|
||||
abnfCNl.Name: abnfCNl,
|
||||
abnfComment.Name: abnfComment,
|
||||
abnfAlternation.Name: abnfAlternation,
|
||||
abnfConcatenation.Name: abnfConcatenation,
|
||||
abnfRepetition.Name: abnfRepetition,
|
||||
abnfRepeat.Name: abnfRepeat,
|
||||
abnfElement.Name: abnfElement,
|
||||
abnfGroup.Name: abnfGroup,
|
||||
abnfOption.Name: abnfOption,
|
||||
abnfCharVal.Name: abnfCharVal,
|
||||
abnfCaseInsensitiveString.Name: abnfCaseInsensitiveString,
|
||||
abnfCaseSensitiveString.Name: abnfCaseSensitiveString,
|
||||
abnfQuotedString.Name: abnfQuotedString,
|
||||
abnfNumVal.Name: abnfNumVal,
|
||||
abnfBinVal.Name: abnfBinVal,
|
||||
abnfDecVal.Name: abnfDecVal,
|
||||
abnfHexVal.Name: abnfHexVal,
|
||||
abnfProseVal.Name: abnfProseVal,
|
||||
},
|
||||
}
|
||||
+394
@@ -0,0 +1,394 @@
|
||||
package goabnf
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// Generates strings by recursive descent over the grammar AST, so unlike the*
|
||||
// transition-graph Generator (a finite automaton, which can only express
|
||||
// *regular* rules) it handles genuinely recursive rules -- nested objects,
|
||||
// balanced delimiters, expression grammars -- where a rule is reached many times
|
||||
// at many depths.
|
||||
//
|
||||
// The hard part of recursive generation is termination: a recursive rule can
|
||||
// expand forever. This generator guarantees a finite, valid production by
|
||||
// precomputing each rule's minimum expansion cost and, whenever a depth or
|
||||
// length budget is exceeded, steering every choice toward the cheapest-to-finish
|
||||
// expansion. It never truncates mid-derivation, so the output is always a
|
||||
// complete word of the grammar. (It shares the tape/rand entropy `source` with
|
||||
// the transition-graph generator, so it slots into testing.F the same way.)
|
||||
|
||||
const astUnbounded = 1 << 30 // "infinite" min-length sentinel (non-productive)
|
||||
|
||||
// ASTGenerator generates strings admitted by a (possibly recursive) grammar
|
||||
// rule, directly from the AST.
|
||||
type ASTGenerator struct {
|
||||
g *Grammar
|
||||
start *Rule
|
||||
|
||||
maxDepth int // rule-expansion depth before steering to terminate
|
||||
maxLen int // output bytes before steering to terminate
|
||||
maxRepeat int // cap for unbounded ("*") repetitions
|
||||
|
||||
minCost map[string]int // rule name -> minimum expansion cost (productivity)
|
||||
}
|
||||
|
||||
// ASTGenOption configures an ASTGenerator.
|
||||
type ASTGenOption interface {
|
||||
apply(*ASTGenerator)
|
||||
}
|
||||
|
||||
type astGenOptionFunc func(*ASTGenerator)
|
||||
|
||||
func (f astGenOptionFunc) apply(g *ASTGenerator) {
|
||||
f(g)
|
||||
}
|
||||
|
||||
// WithMaxDepth bounds rule-expansion (recursion) depth; past it the generator
|
||||
// steers toward terminating expansions. Defaults to 32.
|
||||
func WithMaxDepth(n int) ASTGenOption {
|
||||
return astGenOptionFunc(func(g *ASTGenerator) {
|
||||
g.maxDepth = n
|
||||
})
|
||||
}
|
||||
|
||||
// WithMaxLen bounds the output length; past it the generator steers toward
|
||||
// terminating expansions. Defaults to 4096. (Mandatory repetitions are always
|
||||
// emitted, so the bound is soft -- a single huge fixed repetition can exceed it.)
|
||||
func WithMaxLen(n int) ASTGenOption {
|
||||
return astGenOptionFunc(func(g *ASTGenerator) {
|
||||
g.maxLen = n
|
||||
})
|
||||
}
|
||||
|
||||
// WithMaxRepeat caps how far an unbounded ("*" / "1*") repetition is unrolled.
|
||||
// Defaults to 16.
|
||||
func WithMaxRepeat(n int) ASTGenOption {
|
||||
return astGenOptionFunc(func(g *ASTGenerator) {
|
||||
g.maxRepeat = n
|
||||
})
|
||||
}
|
||||
|
||||
// NewASTGenerator prepares a generator for rule. It errors if the rule is
|
||||
// undefined, or if any rule reachable from it is undefined or non-productive
|
||||
// (cannot derive any finite string), since neither can be generated. Unlike the
|
||||
// transition-graph generator, recursive rules are fully supported.
|
||||
func NewASTGenerator(g *Grammar, rule string, opts ...ASTGenOption) (*ASTGenerator, error) {
|
||||
start := GetRule(rule, g.Rulemap)
|
||||
if start == nil {
|
||||
return nil, &ErrRuleNotFound{Rulename: rule}
|
||||
}
|
||||
ag := &ASTGenerator{
|
||||
g: g,
|
||||
start: start,
|
||||
maxDepth: 32,
|
||||
maxLen: 4096,
|
||||
maxRepeat: 16,
|
||||
}
|
||||
for _, o := range opts {
|
||||
o.apply(ag)
|
||||
}
|
||||
|
||||
// Least-fixpoint of minimum expansion cost over every rule reachable in
|
||||
// principle -- user rules and the built-in core rules (ALPHA, DIGIT, ...),
|
||||
// which GetRule resolves as a fallback but which are absent from Rulemap.
|
||||
// Keyed by canonical rule name (GetRule is case-insensitive and honors user
|
||||
// overrides of core rules). A rule stuck at the sentinel is non-productive.
|
||||
effective := map[string]*Rule{}
|
||||
for n := range g.Rulemap {
|
||||
if r := GetRule(n, g.Rulemap); r != nil {
|
||||
effective[r.Name] = r
|
||||
}
|
||||
}
|
||||
for n := range coreRules {
|
||||
if r := GetRule(n, g.Rulemap); r != nil {
|
||||
effective[r.Name] = r
|
||||
}
|
||||
}
|
||||
ag.minCost = make(map[string]int, len(effective))
|
||||
for name := range effective {
|
||||
ag.minCost[name] = astUnbounded
|
||||
}
|
||||
for changed := true; changed; {
|
||||
changed = false
|
||||
for name, r := range effective {
|
||||
if c := ag.costAlt(r.Alternation); c < ag.minCost[name] {
|
||||
ag.minCost[name] = c
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Every rule reachable from start must be defined and productive.
|
||||
seen := map[string]bool{start.Name: true}
|
||||
var visitAlt func(Alternation) error
|
||||
visitElem := func(e ElemItf) error {
|
||||
switch x := e.(type) {
|
||||
case ElemRulename:
|
||||
r := GetRule(x.Name, g.Rulemap)
|
||||
if r == nil {
|
||||
return fmt.Errorf("rule %q (reachable from %q) is undefined and cannot be generated", x.Name, rule)
|
||||
}
|
||||
if seen[r.Name] {
|
||||
return nil
|
||||
}
|
||||
seen[r.Name] = true
|
||||
if ag.minCost[r.Name] >= astUnbounded {
|
||||
return fmt.Errorf("rule %q (reachable from %q) is non-productive (cannot derive any finite string)", r.Name, rule)
|
||||
}
|
||||
return visitAlt(r.Alternation)
|
||||
case ElemGroup:
|
||||
return visitAlt(x.Alternation)
|
||||
case ElemOption:
|
||||
return visitAlt(x.Alternation)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
visitAlt = func(a Alternation) error {
|
||||
for _, c := range a.Concatenations {
|
||||
for _, rep := range c.Repetitions {
|
||||
if err := visitElem(rep.Element); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if ag.minCost[start.Name] >= astUnbounded {
|
||||
return nil, fmt.Errorf("rule %q is non-productive (cannot derive any finite string)", rule)
|
||||
}
|
||||
if err := visitAlt(start.Alternation); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ag, nil
|
||||
}
|
||||
|
||||
// Generate maps the tape to a production the grammar admits. Total and
|
||||
// deterministic: any tape (including nil) yields a valid production; an
|
||||
// exhausted tape biases toward the shortest one.
|
||||
func (ag *ASTGenerator) Generate(tape []byte) []byte {
|
||||
return ag.walk(&tapeSource{tape: tape})
|
||||
}
|
||||
|
||||
// GenerateRand draws a production using r as the entropy source.
|
||||
func (ag *ASTGenerator) GenerateRand(r *rand.Rand) []byte {
|
||||
return ag.walk(randSource{r: r})
|
||||
}
|
||||
|
||||
func (ag *ASTGenerator) walk(src source) []byte {
|
||||
out := []byte{}
|
||||
ag.genAlt(src, &out, ag.start.Alternation, 0)
|
||||
return out
|
||||
}
|
||||
|
||||
// over reports whether we should stop taking expansions for their own sake and
|
||||
// steer toward the nearest terminal string.
|
||||
func (ag *ASTGenerator) over(out *[]byte, depth int) bool {
|
||||
return depth >= ag.maxDepth || len(*out) >= ag.maxLen
|
||||
}
|
||||
|
||||
func (ag *ASTGenerator) genAlt(src source, out *[]byte, alt Alternation, depth int) {
|
||||
cs := alt.Concatenations
|
||||
if len(cs) == 0 {
|
||||
return
|
||||
}
|
||||
idx := 0
|
||||
if ag.over(out, depth) {
|
||||
idx = ag.cheapestConcat(cs) // steer: pick the cheapest-to-finish branch
|
||||
} else if len(cs) > 1 {
|
||||
idx = src.intn(len(cs))
|
||||
}
|
||||
ag.genConcat(src, out, cs[idx], depth)
|
||||
}
|
||||
|
||||
func (ag *ASTGenerator) genConcat(src source, out *[]byte, concat Concatenation, depth int) {
|
||||
for _, rep := range concat.Repetitions {
|
||||
ag.genRep(src, out, rep, depth)
|
||||
}
|
||||
}
|
||||
|
||||
func (ag *ASTGenerator) genRep(src source, out *[]byte, rep Repetition, depth int) {
|
||||
lo := rep.Min
|
||||
hi := rep.Max
|
||||
if hi == inf || hi < lo { // unbounded: cap the optional unrolling
|
||||
hi = lo + ag.maxRepeat
|
||||
}
|
||||
n := lo
|
||||
if !ag.over(out, depth) && hi > lo {
|
||||
n = lo + src.intn(hi-lo+1)
|
||||
}
|
||||
for i := 0; i < n; i++ {
|
||||
ag.genElem(src, out, rep.Element, depth)
|
||||
// Stop optional extras once over length, but always emit the mandatory lo.
|
||||
if i+1 >= lo && len(*out) >= ag.maxLen {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (ag *ASTGenerator) genElem(src source, out *[]byte, elem ElemItf, depth int) {
|
||||
switch e := elem.(type) {
|
||||
case ElemRulename:
|
||||
// Guaranteed defined+productive by NewASTGenerator.
|
||||
r := GetRule(e.Name, ag.g.Rulemap)
|
||||
ag.genAlt(src, out, r.Alternation, depth+1)
|
||||
|
||||
case ElemGroup:
|
||||
ag.genAlt(src, out, e.Alternation, depth)
|
||||
|
||||
case ElemOption:
|
||||
if !ag.over(out, depth) && src.intn(2) == 1 {
|
||||
ag.genAlt(src, out, e.Alternation, depth)
|
||||
}
|
||||
|
||||
case ElemCharVal:
|
||||
for _, r := range e.Values {
|
||||
if !e.Sensitive && isASCIILetter(r) && src.intn(2) == 1 {
|
||||
r = flipASCIICase(r)
|
||||
}
|
||||
*out = append(*out, []byte(string(r))...)
|
||||
}
|
||||
|
||||
case ElemNumVal:
|
||||
switch e.Status {
|
||||
case StatSeries:
|
||||
for _, v := range e.Elems {
|
||||
r := numvalToRune(v, e.Base)
|
||||
// Emit only real characters; an out-of-range value cannot appear
|
||||
// in UTF-8 input, so the series would never match anyway.
|
||||
if utf8.ValidRune(r) {
|
||||
*out = append(*out, []byte(string(r))...)
|
||||
}
|
||||
}
|
||||
case StatRange:
|
||||
min, max := numvalToRune(e.Elems[0], e.Base), numvalToRune(e.Elems[1], e.Base)
|
||||
// Emit within the representable (Unicode) subset of the range.
|
||||
if max > utf8.MaxRune {
|
||||
max = utf8.MaxRune
|
||||
}
|
||||
if min > utf8.MaxRune || min > max {
|
||||
break // nothing representable to emit
|
||||
}
|
||||
span := int(max-min) + 1
|
||||
if span < 1 {
|
||||
span = 1
|
||||
}
|
||||
*out = append(*out, []byte(string(min+rune(src.intn(span))))...)
|
||||
}
|
||||
|
||||
case ElemProseVal:
|
||||
// prose-val is informal text; nothing mechanical to emit.
|
||||
}
|
||||
}
|
||||
|
||||
// cheapestConcat returns the index of the concatenation with the smallest
|
||||
// minimum expansion cost -- the fastest way to finish the current alternation.
|
||||
func (ag *ASTGenerator) cheapestConcat(cs []Concatenation) int {
|
||||
best, bestCost := 0, ag.costConcat(cs[0])
|
||||
for i := 1; i < len(cs); i++ {
|
||||
if c := ag.costConcat(cs[i]); c < bestCost {
|
||||
best, bestCost = i, c
|
||||
}
|
||||
}
|
||||
return best
|
||||
}
|
||||
|
||||
// ---- minimum-expansion-cost cost model (for productivity + steering) ----
|
||||
|
||||
func (ag *ASTGenerator) costAlt(alt Alternation) int {
|
||||
if len(alt.Concatenations) == 0 {
|
||||
return 0
|
||||
}
|
||||
best := astUnbounded
|
||||
for _, c := range alt.Concatenations {
|
||||
if v := ag.costConcat(c); v < best {
|
||||
best = v
|
||||
}
|
||||
}
|
||||
return best
|
||||
}
|
||||
|
||||
func (ag *ASTGenerator) costConcat(concat Concatenation) int {
|
||||
total := 0
|
||||
for _, rep := range concat.Repetitions {
|
||||
total = addClamp(total, mulClamp(rep.Min, ag.costElem(rep.Element)))
|
||||
}
|
||||
return total
|
||||
}
|
||||
|
||||
func (ag *ASTGenerator) costElem(elem ElemItf) int {
|
||||
switch e := elem.(type) {
|
||||
case ElemRulename:
|
||||
r := GetRule(e.Name, ag.g.Rulemap)
|
||||
if r == nil {
|
||||
return astUnbounded // undefined rule
|
||||
}
|
||||
if c, ok := ag.minCost[r.Name]; ok {
|
||||
// +1 expansion penalty: referencing a rule always costs strictly
|
||||
// more than emitting a terminal, so a recursive branch can never
|
||||
// tie with (and be chosen over) a terminating one -- even when a
|
||||
// nullable prefix makes their emitted lengths equal.
|
||||
return addClamp(1, c)
|
||||
}
|
||||
return astUnbounded
|
||||
case ElemGroup:
|
||||
return ag.costAlt(e.Alternation)
|
||||
case ElemOption:
|
||||
return 0 // can be skipped
|
||||
case ElemCharVal:
|
||||
return len(string(e.Values))
|
||||
case ElemNumVal:
|
||||
switch e.Status {
|
||||
case StatSeries:
|
||||
n := 0
|
||||
for _, v := range e.Elems {
|
||||
n += len(string(numvalToRune(v, e.Base)))
|
||||
}
|
||||
return n
|
||||
default: // StatRange: one rune, take the lower bound's encoded length
|
||||
return len(string(numvalToRune(e.Elems[0], e.Base)))
|
||||
}
|
||||
case ElemProseVal:
|
||||
return 0
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func addClamp(a, b int) int {
|
||||
if a >= astUnbounded || b >= astUnbounded {
|
||||
return astUnbounded
|
||||
}
|
||||
if s := a + b; s < astUnbounded {
|
||||
return s
|
||||
}
|
||||
return astUnbounded
|
||||
}
|
||||
|
||||
func mulClamp(n, c int) int {
|
||||
if n == 0 {
|
||||
return 0
|
||||
}
|
||||
if c >= astUnbounded {
|
||||
return astUnbounded
|
||||
}
|
||||
if p := n * c; p >= 0 && p < astUnbounded {
|
||||
return p
|
||||
}
|
||||
return astUnbounded
|
||||
}
|
||||
|
||||
func isASCIILetter(r rune) bool {
|
||||
return (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z')
|
||||
}
|
||||
|
||||
func flipASCIICase(r rune) rune {
|
||||
switch {
|
||||
case r >= 'a' && r <= 'z':
|
||||
return r - 'a' + 'A'
|
||||
case r >= 'A' && r <= 'Z':
|
||||
return r - 'A' + 'a'
|
||||
}
|
||||
return r
|
||||
}
|
||||
+604
@@ -0,0 +1,604 @@
|
||||
package goabnf
|
||||
|
||||
import (
|
||||
"math/big"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// Binary Subtree Representation (BSR).
|
||||
//
|
||||
// This is an alternative derivation representation to the SPPF in sppf.go,
|
||||
// following Scott, Johnstone & van Binsbergen, "Derivation representation using
|
||||
// binary subtree sets" (Science of Computer Programming 175, 2019). Instead of
|
||||
// interning symbol/intermediate/packed nodes, the parser records a flat set of
|
||||
// elements (slot, l, k, r): a grammar slot whose consumed prefix derives the
|
||||
// input span [l,r], with the most recently consumed symbol spanning [k,r] and
|
||||
// the earlier prefix spanning [l,k]. The same GLL control (descriptors, GSS,
|
||||
// native counted repetition) drives it; only what gets recorded differs.
|
||||
//
|
||||
// The GSS node's pos field already holds a production's left extent (the call
|
||||
// position), so a BSR element needs no extra threading: on a dot advance the
|
||||
// element is (newSlot, l=frameLeftExtent, k=pivot, r=end).
|
||||
//
|
||||
// The BSR engine is validated to produce identical Valid/NumTrees/Ambiguous
|
||||
// results to the SPPF engine across the differential corpus; it is the intended
|
||||
// foundation for grammar-directed parser generation.
|
||||
|
||||
// bsrElem is one binary subtree element. The slot's dot is "just past" the most
|
||||
// recently consumed symbol; that symbol spans [k,r] and the earlier prefix spans
|
||||
// [l,k]. A complete production (dot == len) records the nonterminal's extent.
|
||||
type bsrElem struct {
|
||||
slot slot
|
||||
l, k, r int
|
||||
}
|
||||
|
||||
// bsrGNode is a GSS node for the BSR engine. Unlike the SPPF GSS, its edges are
|
||||
// unlabelled (no SPPF node is carried): the BSR pivots are recovered from node
|
||||
// positions instead.
|
||||
type bsrGNode struct {
|
||||
ret slot // return slot in the caller's production
|
||||
pos int // call position == the caller production's left extent for callees
|
||||
rc int // repetition count (native counted repetition; 0 otherwise)
|
||||
edges []*bsrGNode
|
||||
edgeSet map[*bsrGNode]bool
|
||||
}
|
||||
|
||||
type bsrDesc struct {
|
||||
L slot
|
||||
u *bsrGNode
|
||||
i int
|
||||
rc int
|
||||
}
|
||||
|
||||
type bsrParser struct {
|
||||
sg *slotGrammar
|
||||
input []byte
|
||||
|
||||
set map[bsrElem]bool
|
||||
gss map[gssKey]*bsrGNode
|
||||
u0 *bsrGNode
|
||||
seen map[bsrDesc]bool
|
||||
work []bsrDesc
|
||||
popped map[*bsrGNode]map[int]bool
|
||||
|
||||
maxElems int
|
||||
aborted bool
|
||||
}
|
||||
|
||||
// addAlt adds the descriptor starting alternate L.alt of L.nt at i when the
|
||||
// lookahead test admits it.
|
||||
func (p *bsrParser) addAlt(L slot, u *bsrGNode, i, rc int) {
|
||||
if p.sg.la.alt(L.nt, L.alt).admits(p.input, i) {
|
||||
p.add(L, u, i, rc)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *bsrParser) add(L slot, u *bsrGNode, i, rc int) {
|
||||
d := bsrDesc{L, u, i, rc}
|
||||
if p.seen[d] {
|
||||
return
|
||||
}
|
||||
p.seen[d] = true
|
||||
p.work = append(p.work, d)
|
||||
}
|
||||
|
||||
func (p *bsrParser) gssNodeFor(ret slot, pos, rc int) *bsrGNode {
|
||||
k := gssKey{ret, pos, rc}
|
||||
if v, ok := p.gss[k]; ok {
|
||||
return v
|
||||
}
|
||||
v := &bsrGNode{ret: ret, pos: pos, rc: rc, edgeSet: map[*bsrGNode]bool{}}
|
||||
p.gss[k] = v
|
||||
return v
|
||||
}
|
||||
|
||||
// record adds the BSR element for advancing into slot L over [l,r] with the last
|
||||
// symbol pivoting at k. Per the binary-subtree scheme an element is kept only
|
||||
// when the consumed prefix has length >= 2 (an intermediate extent) or the
|
||||
// production is complete (dot == len). A length-1 prefix is degenerate (l == k)
|
||||
// and is reconstructed directly from the single symbol, so it is not stored.
|
||||
func (p *bsrParser) record(L slot, l, k, r int) {
|
||||
prod := p.sg.nts[L.nt].alts[L.alt]
|
||||
if L.dot >= 2 || L.dot == len(prod) {
|
||||
if p.maxElems > 0 && len(p.set) >= p.maxElems {
|
||||
p.aborted = true
|
||||
return
|
||||
}
|
||||
p.set[bsrElem{L, l, k, r}] = true
|
||||
}
|
||||
}
|
||||
|
||||
func (p *bsrParser) create(ret slot, u *bsrGNode, i, rc int) *bsrGNode {
|
||||
v := p.gssNodeFor(ret, i, rc)
|
||||
if !v.edgeSet[u] {
|
||||
v.edgeSet[u] = true
|
||||
v.edges = append(v.edges, u)
|
||||
for j := range p.popped[v] {
|
||||
p.record(ret, u.pos, i, j)
|
||||
p.add(ret, u, j, rc)
|
||||
}
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
func (p *bsrParser) pop(u *bsrGNode, j int) {
|
||||
if u == p.u0 {
|
||||
return
|
||||
}
|
||||
if p.popped[u] == nil {
|
||||
p.popped[u] = map[int]bool{}
|
||||
}
|
||||
p.popped[u][j] = true
|
||||
for _, v := range u.edges {
|
||||
p.record(u.ret, v.pos, u.pos, j)
|
||||
p.add(u.ret, v, j, u.rc)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *bsrParser) addRepAlts(ntID int, v *bsrGNode, i, rc int) {
|
||||
nt := p.sg.nts[ntID]
|
||||
if rc >= nt.repMin {
|
||||
p.addAlt(slot{ntID, 0, 0}, v, i, rc)
|
||||
}
|
||||
if nt.repMax == inf || rc < nt.repMax {
|
||||
p.addAlt(slot{ntID, 1, 0}, v, i, rc)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *bsrParser) matchTerm(s ssym, i int) int {
|
||||
if s.kind == symEps {
|
||||
return i
|
||||
}
|
||||
switch v := s.term.(type) {
|
||||
case ElemCharVal:
|
||||
idx := i
|
||||
for k := 0; k < len(v.Values); k++ {
|
||||
if idx >= len(p.input) {
|
||||
return -1
|
||||
}
|
||||
r, size := utf8.DecodeRune(p.input[idx:])
|
||||
if r == utf8.RuneError && size == 1 {
|
||||
return -1
|
||||
}
|
||||
if !sensequal(v.Values[k], r, v.Sensitive) {
|
||||
return -1
|
||||
}
|
||||
idx += size
|
||||
}
|
||||
return idx
|
||||
case ElemNumVal:
|
||||
switch v.Status {
|
||||
case StatRange:
|
||||
if i >= len(p.input) {
|
||||
return -1
|
||||
}
|
||||
min := numvalToRune(v.Elems[0], v.Base)
|
||||
max := numvalToRune(v.Elems[1], v.Base)
|
||||
r, size := utf8.DecodeRune(p.input[i:])
|
||||
if r == utf8.RuneError && size == 1 {
|
||||
return -1
|
||||
}
|
||||
if min <= r && r <= max {
|
||||
return i + size
|
||||
}
|
||||
return -1
|
||||
case StatSeries:
|
||||
idx := i
|
||||
for k := 0; k < len(v.Elems); k++ {
|
||||
ru := numvalToRune(v.Elems[k], v.Base)
|
||||
if !utf8.ValidRune(ru) {
|
||||
return -1
|
||||
}
|
||||
str := string(ru)
|
||||
sz := len(str)
|
||||
if idx+sz > len(p.input) || str != string(p.input[idx:idx+sz]) {
|
||||
return -1
|
||||
}
|
||||
idx += sz
|
||||
}
|
||||
return idx
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
func (p *bsrParser) parse() {
|
||||
startNT := p.sg.start
|
||||
p.u0 = &bsrGNode{ret: slot{-1, -1, -1}, pos: 0, edgeSet: map[*bsrGNode]bool{}}
|
||||
for ai := range p.sg.nts[startNT].alts {
|
||||
p.addAlt(slot{startNT, ai, 0}, p.u0, 0, 0)
|
||||
}
|
||||
for len(p.work) > 0 {
|
||||
if p.aborted {
|
||||
return
|
||||
}
|
||||
d := p.work[len(p.work)-1]
|
||||
p.work = p.work[:len(p.work)-1]
|
||||
p.process(d)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *bsrParser) process(d bsrDesc) {
|
||||
L, u, i, rc := d.L, d.u, d.i, d.rc
|
||||
for {
|
||||
prod := p.sg.nts[L.nt].alts[L.alt]
|
||||
if L.dot == len(prod) {
|
||||
p.pop(u, i)
|
||||
return
|
||||
}
|
||||
s := prod[L.dot]
|
||||
if s.kind == symNonterm {
|
||||
if !p.sg.la.callAt(L).admits(p.input, i) {
|
||||
return
|
||||
}
|
||||
ret := slot{L.nt, L.alt, L.dot + 1}
|
||||
v := p.create(ret, u, i, rc)
|
||||
if p.sg.nts[s.nt].isRep {
|
||||
childRC := 0
|
||||
if s.nt == L.nt {
|
||||
childRC = capRC(rc, p.sg.nts[s.nt])
|
||||
}
|
||||
p.addRepAlts(s.nt, v, i, childRC)
|
||||
} else {
|
||||
for ai := range p.sg.nts[s.nt].alts {
|
||||
p.addAlt(slot{s.nt, ai, 0}, v, i, 0)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
j := p.matchTerm(s, i)
|
||||
if j < 0 {
|
||||
return
|
||||
}
|
||||
next := slot{L.nt, L.alt, L.dot + 1}
|
||||
p.record(next, u.pos, i, j)
|
||||
i = j
|
||||
L = next
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Queries over the BSR set
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// BSRForest is the BSR analogue of Forest: a set of binary subtree elements plus
|
||||
// an index for reconstruction. Valid/NumTrees/Ambiguous/Tree mirror Forest.
|
||||
type BSRForest struct {
|
||||
sg *slotGrammar
|
||||
input []byte
|
||||
rulename string
|
||||
set map[bsrElem]bool
|
||||
index map[bsrKey][]int // (slot,l,r) -> pivots k
|
||||
start int
|
||||
n int
|
||||
}
|
||||
|
||||
type bsrKey struct {
|
||||
slot slot
|
||||
l, r int
|
||||
}
|
||||
|
||||
func (f *BSRForest) buildIndex() {
|
||||
f.index = make(map[bsrKey][]int, len(f.set))
|
||||
for e := range f.set {
|
||||
k := bsrKey{e.slot, e.l, e.r}
|
||||
f.index[k] = append(f.index[k], e.k)
|
||||
}
|
||||
}
|
||||
|
||||
// startElems returns, for each production of the start rule, the pivots of the
|
||||
// complete-production elements spanning the whole input.
|
||||
func (f *BSRForest) startElems() (slot, []int, bool) {
|
||||
for ai, prod := range f.sg.nts[f.start].alts {
|
||||
s := slot{f.start, ai, len(prod)}
|
||||
if ks := f.index[bsrKey{s, 0, f.n}]; len(ks) > 0 {
|
||||
return s, ks, true
|
||||
}
|
||||
}
|
||||
return slot{}, nil, false
|
||||
}
|
||||
|
||||
// Valid reports whether the whole input is derivable by the root rule.
|
||||
func (f *BSRForest) Valid() bool {
|
||||
for ai, prod := range f.sg.nts[f.start].alts {
|
||||
s := slot{f.start, ai, len(prod)}
|
||||
if len(f.index[bsrKey{s, 0, f.n}]) > 0 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Nodes returns the number of BSR elements (the representation's size).
|
||||
func (f *BSRForest) Nodes() int { return len(f.set) }
|
||||
|
||||
// symNode/interNode identities for memoisation and cycle detection.
|
||||
type bsrNodeID struct {
|
||||
inter bool
|
||||
slot slot // for intermediate nodes
|
||||
nt int // for symbol nodes
|
||||
l, r int
|
||||
}
|
||||
|
||||
// NumTrees returns the number of distinct parse trees represented, or -1 for
|
||||
// infinitely many (a cycle, i.e. an infinitely-ambiguous grammar). It mirrors
|
||||
// Forest.NumTrees but counts over BSR extents instead of SPPF nodes.
|
||||
func (f *BSRForest) NumTrees() *big.Int {
|
||||
if !f.Valid() {
|
||||
return big.NewInt(0)
|
||||
}
|
||||
memo := map[bsrNodeID]*big.Int{}
|
||||
onStack := map[bsrNodeID]bool{}
|
||||
infinite := false
|
||||
|
||||
var symWays func(s ssym, a, b int) *big.Int
|
||||
var countSym func(nt, l, r int) *big.Int
|
||||
var countInter func(sl slot, l, r int) *big.Int
|
||||
var countElem func(sl slot, l, k, r int) *big.Int
|
||||
|
||||
symWays = func(s ssym, a, b int) *big.Int {
|
||||
if s.kind == symNonterm {
|
||||
return countSym(s.nt, a, b)
|
||||
}
|
||||
return big.NewInt(1) // terminal / eps leaf
|
||||
}
|
||||
|
||||
countElem = func(sl slot, l, k, r int) *big.Int {
|
||||
prod := f.sg.nts[sl.nt].alts[sl.alt]
|
||||
last := symWays(prod[sl.dot-1], k, r)
|
||||
var pre *big.Int
|
||||
switch sl.dot {
|
||||
case 1: // empty prefix (l == k)
|
||||
pre = big.NewInt(1)
|
||||
case 2: // single-symbol prefix
|
||||
pre = symWays(prod[0], l, k)
|
||||
default: // intermediate prefix
|
||||
pre = countInter(slot{sl.nt, sl.alt, sl.dot - 1}, l, k)
|
||||
}
|
||||
return new(big.Int).Mul(pre, last)
|
||||
}
|
||||
|
||||
countInter = func(sl slot, l, r int) *big.Int {
|
||||
id := bsrNodeID{inter: true, slot: sl, l: l, r: r}
|
||||
if v, ok := memo[id]; ok {
|
||||
return v
|
||||
}
|
||||
if onStack[id] {
|
||||
infinite = true
|
||||
return big.NewInt(1)
|
||||
}
|
||||
onStack[id] = true
|
||||
total := big.NewInt(0)
|
||||
for _, k := range f.index[bsrKey{sl, l, r}] {
|
||||
total.Add(total, countElem(sl, l, k, r))
|
||||
}
|
||||
onStack[id] = false
|
||||
memo[id] = total
|
||||
return total
|
||||
}
|
||||
|
||||
countSym = func(nt, l, r int) *big.Int {
|
||||
id := bsrNodeID{nt: nt, l: l, r: r}
|
||||
if v, ok := memo[id]; ok {
|
||||
return v
|
||||
}
|
||||
if onStack[id] {
|
||||
infinite = true
|
||||
return big.NewInt(1)
|
||||
}
|
||||
onStack[id] = true
|
||||
total := big.NewInt(0)
|
||||
for ai, prod := range f.sg.nts[nt].alts {
|
||||
sl := slot{nt, ai, len(prod)}
|
||||
for _, k := range f.index[bsrKey{sl, l, r}] {
|
||||
total.Add(total, countElem(sl, l, k, r))
|
||||
}
|
||||
}
|
||||
onStack[id] = false
|
||||
memo[id] = total
|
||||
return total
|
||||
}
|
||||
|
||||
res := countSym(f.start, 0, f.n)
|
||||
if infinite {
|
||||
return big.NewInt(-1)
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
// Ambiguous reports whether the input has more than one distinct parse tree.
|
||||
func (f *BSRForest) Ambiguous() bool {
|
||||
if !f.Valid() {
|
||||
return false
|
||||
}
|
||||
visited := map[bsrNodeID]bool{}
|
||||
|
||||
var symAmb func(nt, l, r int) bool
|
||||
var interAmb func(sl slot, l, r int) bool
|
||||
var elemAmb func(sl slot, l, k, r int) bool
|
||||
|
||||
symOf := func(s ssym, a, b int) bool {
|
||||
if s.kind == symNonterm {
|
||||
return symAmb(s.nt, a, b)
|
||||
}
|
||||
return false
|
||||
}
|
||||
elemAmb = func(sl slot, l, k, r int) bool {
|
||||
prod := f.sg.nts[sl.nt].alts[sl.alt]
|
||||
if symOf(prod[sl.dot-1], k, r) {
|
||||
return true
|
||||
}
|
||||
switch sl.dot {
|
||||
case 1:
|
||||
return false
|
||||
case 2:
|
||||
return symOf(prod[0], l, k)
|
||||
default:
|
||||
return interAmb(slot{sl.nt, sl.alt, sl.dot - 1}, l, k)
|
||||
}
|
||||
}
|
||||
interAmb = func(sl slot, l, r int) bool {
|
||||
id := bsrNodeID{inter: true, slot: sl, l: l, r: r}
|
||||
if visited[id] {
|
||||
return false
|
||||
}
|
||||
visited[id] = true
|
||||
ks := f.index[bsrKey{sl, l, r}]
|
||||
if len(ks) > 1 {
|
||||
return true
|
||||
}
|
||||
for _, k := range ks {
|
||||
if elemAmb(sl, l, k, r) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
symAmb = func(nt, l, r int) bool {
|
||||
id := bsrNodeID{nt: nt, l: l, r: r}
|
||||
if visited[id] {
|
||||
return false
|
||||
}
|
||||
visited[id] = true
|
||||
packs := 0
|
||||
for ai, prod := range f.sg.nts[nt].alts {
|
||||
sl := slot{nt, ai, len(prod)}
|
||||
ks := f.index[bsrKey{sl, l, r}]
|
||||
packs += len(ks)
|
||||
}
|
||||
if packs > 1 {
|
||||
return true
|
||||
}
|
||||
for ai, prod := range f.sg.nts[nt].alts {
|
||||
sl := slot{nt, ai, len(prod)}
|
||||
for _, k := range f.index[bsrKey{sl, l, r}] {
|
||||
if elemAmb(sl, l, k, r) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
return symAmb(f.start, 0, f.n)
|
||||
}
|
||||
|
||||
// Tree extracts a single parse tree (first element at each extent), or nil if
|
||||
// the input is invalid. Synthetic nonterminals (groups, options, repetitions)
|
||||
// are collapsed so only original rule names appear, matching Forest.Tree.
|
||||
func (f *BSRForest) Tree() *ParseTree {
|
||||
if !f.Valid() {
|
||||
return nil
|
||||
}
|
||||
sl, ks, ok := f.startElems()
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
visited := map[bsrNodeID]bool{}
|
||||
return f.emitSym(f.start, sl, ks[0], 0, f.n, visited)
|
||||
}
|
||||
|
||||
// emitSym builds a ParseTree node for nonterminal nt over [l,r], chosen via the
|
||||
// complete-production slot sl with pivot k.
|
||||
func (f *BSRForest) emitSym(nt int, sl slot, k, l, r int, visited map[bsrNodeID]bool) *ParseTree {
|
||||
var kids []*ParseTree
|
||||
f.collectElem(sl, l, k, r, &kids, visited)
|
||||
info := f.sg.nts[nt]
|
||||
if !info.isRule {
|
||||
// synthetic (group/option/rep): splice children into the parent
|
||||
return &ParseTree{Start: l, End: r, Children: kids} // caller flattens
|
||||
}
|
||||
return &ParseTree{Rule: info.ruleName, Start: l, End: r, Children: kids}
|
||||
}
|
||||
|
||||
// collectElem appends the children of element (sl,l,k,r) into out, flattening
|
||||
// intermediate extents and synthetic nonterminals.
|
||||
func (f *BSRForest) collectElem(sl slot, l, k, r int, out *[]*ParseTree, visited map[bsrNodeID]bool) {
|
||||
prod := f.sg.nts[sl.nt].alts[sl.alt]
|
||||
// prefix x_1..x_{dot-1} over [l,k]
|
||||
switch sl.dot {
|
||||
case 1:
|
||||
// empty
|
||||
case 2:
|
||||
f.collectChild(prod[0], l, k, out, visited)
|
||||
default:
|
||||
isl := slot{sl.nt, sl.alt, sl.dot - 1}
|
||||
if ks := f.index[bsrKey{isl, l, k}]; len(ks) > 0 {
|
||||
f.collectElem(isl, l, ks[0], k, out, visited)
|
||||
}
|
||||
}
|
||||
// last symbol x_{dot} over [k,r]
|
||||
f.collectChild(prod[sl.dot-1], k, r, out, visited)
|
||||
}
|
||||
|
||||
func (f *BSRForest) collectChild(s ssym, a, b int, out *[]*ParseTree, visited map[bsrNodeID]bool) {
|
||||
if s.kind != symNonterm {
|
||||
if s.kind == symEps {
|
||||
return // epsilon contributes no leaf
|
||||
}
|
||||
*out = append(*out, &ParseTree{Start: a, End: b})
|
||||
return
|
||||
}
|
||||
id := bsrNodeID{nt: s.nt, l: a, r: b}
|
||||
if visited[id] {
|
||||
return
|
||||
}
|
||||
visited[id] = true
|
||||
// pick the first complete-production element for this nonterminal extent
|
||||
for ai, prod := range f.sg.nts[s.nt].alts {
|
||||
csl := slot{s.nt, ai, len(prod)}
|
||||
if ks := f.index[bsrKey{csl, a, b}]; len(ks) > 0 {
|
||||
info := f.sg.nts[s.nt]
|
||||
if info.isRule {
|
||||
var kids []*ParseTree
|
||||
f.collectElem(csl, a, ks[0], b, &kids, visited)
|
||||
*out = append(*out, &ParseTree{Rule: info.ruleName, Start: a, End: b, Children: kids})
|
||||
} else {
|
||||
// synthetic: splice its children directly into the parent
|
||||
f.collectElem(csl, a, ks[0], b, out, visited)
|
||||
}
|
||||
visited[id] = false
|
||||
return
|
||||
}
|
||||
}
|
||||
visited[id] = false
|
||||
}
|
||||
|
||||
func newBSRParser(sg *slotGrammar, input []byte, maxElems int) *bsrParser {
|
||||
return &bsrParser{
|
||||
sg: sg,
|
||||
input: input,
|
||||
set: map[bsrElem]bool{},
|
||||
gss: map[gssKey]*bsrGNode{},
|
||||
seen: map[bsrDesc]bool{},
|
||||
popped: map[*bsrGNode]map[int]bool{},
|
||||
maxElems: maxElems,
|
||||
}
|
||||
}
|
||||
|
||||
// ParseBSR parses input under rootRulename using the BSR engine. It accepts the
|
||||
// same options as ParseForest (WithMaxForestNodes bounds the BSR set size).
|
||||
func ParseBSR(input []byte, grammar *Grammar, rootRulename string, opts ...ForestOption) (*BSRForest, error) {
|
||||
cfg := forestConfig{maxSlots: defaultMaxSlots}
|
||||
for _, o := range opts {
|
||||
o(&cfg)
|
||||
}
|
||||
sg, err := compileSlots(grammar, rootRulename, cfg.maxSlots)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
p := newBSRParser(sg, input, cfg.maxNodes)
|
||||
p.parse()
|
||||
if p.aborted {
|
||||
return nil, &ErrForestTooLarge{Max: cfg.maxNodes}
|
||||
}
|
||||
f := &BSRForest{
|
||||
sg: sg,
|
||||
input: input,
|
||||
rulename: rootRulename,
|
||||
set: p.set,
|
||||
start: sg.start,
|
||||
n: len(input),
|
||||
}
|
||||
f.buildIndex()
|
||||
return f, nil
|
||||
}
|
||||
+903
@@ -0,0 +1,903 @@
|
||||
package goabnf
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"go/format"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// GenerateGoParserFromABNF is a convenience wrapper that parses ABNF grammar
|
||||
// source and generates a specialised parser for it in one step. root is the rule
|
||||
// the generated parser starts from; pkg is the package name of the emitted file.
|
||||
// It is the natural entry point for a code-generation command (e.g. driven by
|
||||
// `go generate`).
|
||||
func GenerateGoParserFromABNF(abnfSource []byte, root, pkg string, opts ...ABNFOption) ([]byte, error) {
|
||||
g, err := ParseABNF(abnfSource, opts...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parse ABNF source: %w", err)
|
||||
}
|
||||
return GenerateGoParser(g, root, pkg)
|
||||
}
|
||||
|
||||
// GenerateGoParser emits a standalone Go source file containing a parser
|
||||
// SPECIALISED to grammar g rooted at root. Unlike a table-driven interpreter,
|
||||
// the grammar is compiled into control flow:
|
||||
//
|
||||
// - the parse loop is a jump-table switch over slot IDs; each case is the
|
||||
// straight-line code for that one slot (its symbol, its successor, its
|
||||
// completion), so there is no per-step table lookup and no symbol-kind
|
||||
// dispatch at runtime;
|
||||
// - terminals are inlined as byte comparisons / rune decodes against baked
|
||||
// literals (no ElemItf, no closure table);
|
||||
// - nonterminal calls, completions, and the {min,max} repetition gating are
|
||||
// baked as constants at each site;
|
||||
// - only the genuinely grammar-independent machinery (GSS, descriptor
|
||||
// worklist, BSR set) remains as fixed runtime, plus a compact metadata table
|
||||
// used solely by the reconstruction queries (NumTrees/Ambiguous/Tree), which
|
||||
// inherently need grammar structure.
|
||||
//
|
||||
// The output depends only on the standard library, is panic-free (input decoded
|
||||
// via utf8, no manual indexing) and bounded (MaxElements aborts pathological
|
||||
// inputs with ErrTooLarge). It is validated against ParseBSR via the differential
|
||||
// harness. pkg is the package name for the emitted file.
|
||||
func GenerateGoParser(g *Grammar, root, pkg string) ([]byte, error) {
|
||||
sg, err := compileSlots(g, root, defaultMaxSlots)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
gen := &codegen{sg: sg, root: root, pkg: pkg}
|
||||
gen.assignSlots()
|
||||
src := gen.emit()
|
||||
formatted, ferr := format.Source([]byte(src))
|
||||
if ferr != nil {
|
||||
return []byte(src), fmt.Errorf("generated source failed to format: %w", ferr)
|
||||
}
|
||||
return formatted, nil
|
||||
}
|
||||
|
||||
type sInfo struct {
|
||||
nt, alt, dot, plen int
|
||||
}
|
||||
|
||||
type codegen struct {
|
||||
sg *slotGrammar
|
||||
root string
|
||||
pkg string
|
||||
|
||||
slots []sInfo // slot id -> (nt,alt,dot,prodLen)
|
||||
idOf [][]int // nt,alt -> base slot id (dot 0); dot d == base+d
|
||||
series []string
|
||||
seriesIdx map[string]int
|
||||
usesUTF8 bool // whether any emitted matcher references unicode/utf8
|
||||
}
|
||||
|
||||
func (c *codegen) startID(nt, alt int) int { return c.idOf[nt][alt] }
|
||||
func (c *codegen) compID(nt, alt int) int { return c.idOf[nt][alt] + len(c.sg.nts[nt].alts[alt]) }
|
||||
|
||||
// assignSlots numbers every slot (nt,alt,dot) with dots within a production
|
||||
// consecutive, so the slot at dot d-1 is (id-1).
|
||||
func (c *codegen) assignSlots() {
|
||||
c.seriesIdx = map[string]int{}
|
||||
c.idOf = make([][]int, len(c.sg.nts))
|
||||
for nt, info := range c.sg.nts {
|
||||
c.idOf[nt] = make([]int, len(info.alts))
|
||||
for alt, prod := range info.alts {
|
||||
c.idOf[nt][alt] = len(c.slots)
|
||||
for dot := 0; dot <= len(prod); dot++ {
|
||||
c.slots = append(c.slots, sInfo{nt, alt, dot, len(prod)})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *codegen) emit() string {
|
||||
c.scanUTF8()
|
||||
var b strings.Builder
|
||||
fmt.Fprintf(&b, "// Code generated by go-abnf for root %q. DO NOT EDIT.\n\n", c.root)
|
||||
fmt.Fprintf(&b, "package %s\n\n", c.pkg)
|
||||
c.emitImports(&b)
|
||||
b.WriteString(headerBoilerplate)
|
||||
c.emitProcess(&b)
|
||||
c.emitSeed(&b)
|
||||
c.emitMetadata(&b)
|
||||
b.WriteString(queryBoilerplate)
|
||||
c.emitSeriesVars(&b)
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// scanUTF8 sets usesUTF8 if any terminal compiles to code that decodes runes
|
||||
// (a num-val range, or a char-val with a non-ASCII rune).
|
||||
func (c *codegen) scanUTF8() {
|
||||
for _, nt := range c.sg.nts {
|
||||
for _, prod := range nt.alts {
|
||||
for _, s := range prod {
|
||||
if s.kind != symTerm {
|
||||
continue
|
||||
}
|
||||
switch v := s.term.(type) {
|
||||
case ElemNumVal:
|
||||
if v.Status == StatRange {
|
||||
c.usesUTF8 = true
|
||||
}
|
||||
case ElemCharVal:
|
||||
for _, r := range v.Values {
|
||||
if r > 0x7f {
|
||||
c.usesUTF8 = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *codegen) emitImports(b *strings.Builder) {
|
||||
b.WriteString("import (\n\t\"errors\"\n\t\"math/big\"\n")
|
||||
if c.usesUTF8 {
|
||||
b.WriteString("\t\"unicode/utf8\"\n")
|
||||
}
|
||||
b.WriteString(")\n")
|
||||
}
|
||||
|
||||
// emitProcess generates the specialised parse loop: a jump-table switch over
|
||||
// slot ids, one case per slot.
|
||||
func (c *codegen) emitProcess(b *strings.Builder) {
|
||||
b.WriteString("\nfunc (p *parser) process(d desc) {\n")
|
||||
b.WriteString("\tL, u, i, rc := d.L, d.u, d.i, d.rc\n")
|
||||
b.WriteString("\t_ = rc\n")
|
||||
b.WriteString("\tfor {\n\t\tswitch L {\n")
|
||||
for id, s := range c.slots {
|
||||
info := c.sg.nts[s.nt]
|
||||
fmt.Fprintf(b, "\t\tcase %d: // %s\n", id, c.slotComment(s))
|
||||
if s.dot == s.plen {
|
||||
b.WriteString("\t\t\tp.pop(u, i)\n\t\t\treturn\n")
|
||||
continue
|
||||
}
|
||||
sym := info.alts[s.alt][s.dot]
|
||||
nextID := id + 1
|
||||
switch sym.kind {
|
||||
case symNonterm:
|
||||
c.emitCall(b, s, sym.nt, nextID)
|
||||
default:
|
||||
c.emitTerminal(b, sym, nextID)
|
||||
}
|
||||
}
|
||||
b.WriteString("\t\tdefault:\n\t\t\treturn\n")
|
||||
b.WriteString("\t\t}\n\t}\n}\n")
|
||||
}
|
||||
|
||||
func (c *codegen) emitCall(b *strings.Builder, s sInfo, callee, retID int) {
|
||||
fmt.Fprintf(b, "\t\t\tv := p.create(%d, u, i, rc)\n", retID)
|
||||
ci := c.sg.nts[callee]
|
||||
if ci.isRep {
|
||||
if callee == s.nt {
|
||||
// self-recursion inside the rep's own loop: continue the chain.
|
||||
if ci.repMax == inf {
|
||||
fmt.Fprintf(b, "\t\t\tcrc := rc + 1\n\t\t\tif crc > %d {\n\t\t\t\tcrc = %d\n\t\t\t}\n", ci.repMin, ci.repMin)
|
||||
} else {
|
||||
b.WriteString("\t\t\tcrc := rc + 1\n")
|
||||
}
|
||||
fmt.Fprintf(b, "\t\t\tif crc >= %d {\n\t\t\t\tp.add(%d, v, i, crc)\n\t\t\t}\n", ci.repMin, c.startID(callee, 0))
|
||||
if ci.repMax == inf {
|
||||
fmt.Fprintf(b, "\t\t\tp.add(%d, v, i, crc)\n", c.startID(callee, 1))
|
||||
} else {
|
||||
fmt.Fprintf(b, "\t\t\tif crc < %d {\n\t\t\t\tp.add(%d, v, i, crc)\n\t\t\t}\n", ci.repMax, c.startID(callee, 1))
|
||||
}
|
||||
} else {
|
||||
// external call: fresh chain at count 0; gating decided at gen time.
|
||||
if ci.repMin == 0 {
|
||||
fmt.Fprintf(b, "\t\t\tp.add(%d, v, i, 0)\n", c.startID(callee, 0))
|
||||
}
|
||||
if ci.repMax == inf || 0 < ci.repMax {
|
||||
fmt.Fprintf(b, "\t\t\tp.add(%d, v, i, 0)\n", c.startID(callee, 1))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for alt := range ci.alts {
|
||||
fmt.Fprintf(b, "\t\t\tp.add(%d, v, i, 0)\n", c.startID(callee, alt))
|
||||
}
|
||||
}
|
||||
b.WriteString("\t\t\treturn\n")
|
||||
}
|
||||
|
||||
func (c *codegen) emitTerminal(b *strings.Builder, sym ssym, nextID int) {
|
||||
if sym.kind == symEps {
|
||||
fmt.Fprintf(b, "\t\t\tp.record(%d, u.pos, i, i)\n", nextID)
|
||||
fmt.Fprintf(b, "\t\t\tL = %d\n\t\t\tcontinue\n", nextID)
|
||||
return
|
||||
}
|
||||
if !c.emitMatch(b, sym) {
|
||||
// The terminal can never match (a prose-val, or a series carrying a
|
||||
// non-Unicode value): emitMatch has emitted `return` and defined no j,
|
||||
// so the success tail below would be unreachable and reference an
|
||||
// undefined j ; we should stop here.
|
||||
return
|
||||
}
|
||||
fmt.Fprintf(b, "\t\t\tp.record(%d, u.pos, i, j)\n", nextID)
|
||||
fmt.Fprintf(b, "\t\t\ti = j\n\t\t\tL = %d\n\t\t\tcontinue\n", nextID)
|
||||
}
|
||||
|
||||
// emitMatch emits inline code setting j to the end index on success or return on
|
||||
// failure. It reports whether the terminal can match at all: a false result
|
||||
// means it emitted only `return` and defined no j (prose-val, or a series with a
|
||||
// non-Unicode value), so the caller must not emit the j-dependent success tail.
|
||||
func (c *codegen) emitMatch(b *strings.Builder, s ssym) bool {
|
||||
switch v := s.term.(type) {
|
||||
case ElemCharVal:
|
||||
c.emitCharVal(b, v)
|
||||
return true
|
||||
case ElemNumVal:
|
||||
switch v.Status {
|
||||
case StatRange:
|
||||
lo := numvalToRune(v.Elems[0], v.Base)
|
||||
hi := numvalToRune(v.Elems[1], v.Base)
|
||||
b.WriteString("\t\t\tif i >= len(p.input) {\n\t\t\t\treturn\n\t\t\t}\n")
|
||||
b.WriteString("\t\t\trv, rs := utf8.DecodeRune(p.input[i:])\n")
|
||||
b.WriteString("\t\t\tif rv == utf8.RuneError && rs == 1 {\n\t\t\t\treturn\n\t\t\t}\n")
|
||||
fmt.Fprintf(b, "\t\t\tif rv < %d || rv > %d {\n\t\t\t\treturn\n\t\t\t}\n", lo, hi)
|
||||
b.WriteString("\t\t\tj := i + rs\n")
|
||||
return true
|
||||
case StatSeries:
|
||||
return c.emitSeries(b, v)
|
||||
default:
|
||||
b.WriteString("\t\t\treturn\n")
|
||||
return false
|
||||
}
|
||||
default: // prose-val: never matches
|
||||
b.WriteString("\t\t\treturn\n")
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func (c *codegen) emitCharVal(b *strings.Builder, v ElemCharVal) {
|
||||
allASCII := true
|
||||
for _, r := range v.Values {
|
||||
if r > 0x7f {
|
||||
allASCII = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if allASCII {
|
||||
fmt.Fprintf(b, "\t\t\tif i+%d > len(p.input) {\n\t\t\t\treturn\n\t\t\t}\n", len(v.Values))
|
||||
for off, r := range v.Values {
|
||||
bb := byte(r)
|
||||
if v.Sensitive {
|
||||
fmt.Fprintf(b, "\t\t\tif p.input[i+%d] != %d {\n\t\t\t\treturn\n\t\t\t}\n", off, bb)
|
||||
} else {
|
||||
fmt.Fprintf(b, "\t\t\tif foldB(p.input[i+%d]) != %d {\n\t\t\t\treturn\n\t\t\t}\n", off, foldByte(bb))
|
||||
}
|
||||
}
|
||||
fmt.Fprintf(b, "\t\t\tj := i + %d\n", len(v.Values))
|
||||
return
|
||||
}
|
||||
// rare non-ASCII char-val: decode rune-by-rune.
|
||||
b.WriteString("\t\t\tj := i\n")
|
||||
for _, r := range v.Values {
|
||||
b.WriteString("\t\t\t{\n")
|
||||
b.WriteString("\t\t\t\tif j >= len(p.input) {\n\t\t\t\t\treturn\n\t\t\t\t}\n")
|
||||
b.WriteString("\t\t\t\trv, rs := utf8.DecodeRune(p.input[j:])\n")
|
||||
b.WriteString("\t\t\t\tif rv == utf8.RuneError && rs == 1 {\n\t\t\t\t\treturn\n\t\t\t\t}\n")
|
||||
if v.Sensitive {
|
||||
fmt.Fprintf(b, "\t\t\t\tif rv != %d {\n\t\t\t\t\treturn\n\t\t\t\t}\n", r)
|
||||
} else {
|
||||
fmt.Fprintf(b, "\t\t\t\tif foldR(rv) != %d {\n\t\t\t\t\treturn\n\t\t\t\t}\n", foldRune(r))
|
||||
}
|
||||
b.WriteString("\t\t\t\tj += rs\n\t\t\t}\n")
|
||||
}
|
||||
}
|
||||
|
||||
func (c *codegen) emitSeries(b *strings.Builder, v ElemNumVal) bool {
|
||||
var sb strings.Builder
|
||||
for _, e := range v.Elems {
|
||||
r := numvalToRune(e, v.Base)
|
||||
if !utf8.ValidRune(r) {
|
||||
b.WriteString("\t\t\treturn // series contains a non-Unicode value\n")
|
||||
return false
|
||||
}
|
||||
sb.WriteRune(r)
|
||||
}
|
||||
idx := c.seriesVar([]byte(sb.String()))
|
||||
fmt.Fprintf(b, "\t\t\tif i+len(series%d) > len(p.input) || !bytesEqual(p.input[i:i+len(series%d)], series%d) {\n\t\t\t\treturn\n\t\t\t}\n", idx, idx, idx)
|
||||
fmt.Fprintf(b, "\t\t\tj := i + len(series%d)\n", idx)
|
||||
return true
|
||||
}
|
||||
|
||||
func (c *codegen) seriesVar(bs []byte) int {
|
||||
key := string(bs)
|
||||
if i, ok := c.seriesIdx[key]; ok {
|
||||
return i
|
||||
}
|
||||
i := len(c.series)
|
||||
c.seriesIdx[key] = i
|
||||
parts := make([]string, len(bs))
|
||||
for k, x := range bs {
|
||||
parts[k] = strconv.Itoa(int(x))
|
||||
}
|
||||
c.series = append(c.series, "[]byte{"+strings.Join(parts, ", ")+"}")
|
||||
return i
|
||||
}
|
||||
|
||||
func (c *codegen) emitSeed(b *strings.Builder) {
|
||||
b.WriteString("\nfunc (p *parser) seed() {\n")
|
||||
for alt := range c.sg.nts[c.sg.start].alts {
|
||||
fmt.Fprintf(b, "\tp.add(%d, p.u0, 0, 0)\n", c.startID(c.sg.start, alt))
|
||||
}
|
||||
b.WriteString("}\n")
|
||||
}
|
||||
|
||||
func (c *codegen) emitMetadata(b *strings.Builder) {
|
||||
b.WriteString("\n// ---- baked metadata (reconstruction queries only) ----\n\n")
|
||||
fmt.Fprintf(b, "const gRootName = %q\n", c.root)
|
||||
fmt.Fprintf(b, "const gStart = %d\n\n", c.sg.start)
|
||||
|
||||
b.WriteString("var slotInfo = []sInfo{\n")
|
||||
for _, s := range c.slots {
|
||||
fmt.Fprintf(b, "\t{%d, %d, %d, %d},\n", s.nt, s.alt, s.dot, s.plen)
|
||||
}
|
||||
b.WriteString("}\n\n")
|
||||
|
||||
b.WriteString("var ntComplete = [][]int{\n")
|
||||
for nt := range c.sg.nts {
|
||||
b.WriteString("\t{")
|
||||
for alt := range c.sg.nts[nt].alts {
|
||||
if alt > 0 {
|
||||
b.WriteString(", ")
|
||||
}
|
||||
fmt.Fprintf(b, "%d", c.compID(nt, alt))
|
||||
}
|
||||
b.WriteString("},\n")
|
||||
}
|
||||
b.WriteString("}\n\n")
|
||||
|
||||
b.WriteString("var qNTs = []qNT{\n")
|
||||
for _, nt := range c.sg.nts {
|
||||
fmt.Fprintf(b, "\t{isRule: %v, ruleName: %q, alts: ", nt.isRule, nt.ruleName)
|
||||
if len(nt.alts) == 0 {
|
||||
b.WriteString("nil},\n")
|
||||
continue
|
||||
}
|
||||
b.WriteString("[][]qSym{")
|
||||
for ai, prod := range nt.alts {
|
||||
if ai > 0 {
|
||||
b.WriteString(", ")
|
||||
}
|
||||
b.WriteString("{")
|
||||
for si, sym := range prod {
|
||||
if si > 0 {
|
||||
b.WriteString(", ")
|
||||
}
|
||||
switch sym.kind {
|
||||
case symNonterm:
|
||||
fmt.Fprintf(b, "{kind: kNT, nt: %d}", sym.nt)
|
||||
case symEps:
|
||||
b.WriteString("{kind: kEps}")
|
||||
default:
|
||||
b.WriteString("{kind: kTerm}")
|
||||
}
|
||||
}
|
||||
b.WriteString("}")
|
||||
}
|
||||
b.WriteString("}},\n")
|
||||
}
|
||||
b.WriteString("}\n")
|
||||
}
|
||||
|
||||
func (c *codegen) emitSeriesVars(b *strings.Builder) {
|
||||
if len(c.series) == 0 {
|
||||
return
|
||||
}
|
||||
b.WriteString("\n// ---- baked series literals ----\n")
|
||||
for i, s := range c.series {
|
||||
fmt.Fprintf(b, "var series%d = %s\n", i, s)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *codegen) slotComment(s sInfo) string {
|
||||
nt := c.sg.nts[s.nt]
|
||||
name := nt.ruleName
|
||||
if name == "" {
|
||||
name = nt.label
|
||||
}
|
||||
prod := nt.alts[s.alt]
|
||||
parts := make([]string, 0, len(prod)+1)
|
||||
for d, sym := range prod {
|
||||
if d == s.dot {
|
||||
parts = append(parts, ".")
|
||||
}
|
||||
parts = append(parts, symStr(c.sg, sym))
|
||||
}
|
||||
if s.dot == len(prod) {
|
||||
parts = append(parts, ".")
|
||||
}
|
||||
return strings.TrimSpace(name+" ::= "+strings.Join(parts, " ")) +
|
||||
fmt.Sprintf(" (%d,%d,%d)", s.nt, s.alt, s.dot)
|
||||
}
|
||||
|
||||
func symStr(sg *slotGrammar, s ssym) string {
|
||||
switch s.kind {
|
||||
case symNonterm:
|
||||
n := sg.nts[s.nt]
|
||||
if n.ruleName != "" {
|
||||
return n.ruleName
|
||||
}
|
||||
if n.label != "" {
|
||||
return n.label
|
||||
}
|
||||
return "nt" + strconv.Itoa(s.nt)
|
||||
case symEps:
|
||||
return "eps"
|
||||
default:
|
||||
if s.term != nil {
|
||||
return sanitizeComment(s.term.String())
|
||||
}
|
||||
return "?"
|
||||
}
|
||||
}
|
||||
|
||||
func sanitizeComment(s string) string {
|
||||
s = strings.ReplaceAll(s, "*/", "* /")
|
||||
s = strings.ReplaceAll(s, "\r", " ")
|
||||
s = strings.ReplaceAll(s, "\n", " ")
|
||||
return s
|
||||
}
|
||||
|
||||
func foldByte(b byte) byte {
|
||||
if b >= 'A' && b <= 'Z' {
|
||||
return b - 'A' + 'a'
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func foldRune(r rune) rune {
|
||||
if r >= 'A' && r <= 'Z' {
|
||||
return r - 'A' + 'a'
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
// headerBoilerplate: runtime types, GSS/BSR engine state. Grammar-independent.
|
||||
// (Imports are emitted separately so unicode/utf8 is only pulled in when a
|
||||
// terminal actually decodes runes.) The generated process()/seed() and metadata
|
||||
// complete the file.
|
||||
const headerBoilerplate = `
|
||||
// MaxElements bounds the BSR set; parsing aborts with ErrTooLarge beyond it.
|
||||
// Set to 0 to disable the bound.
|
||||
var MaxElements = 1 << 22
|
||||
|
||||
// ErrTooLarge is returned when the BSR set exceeds MaxElements.
|
||||
var ErrTooLarge = errors.New("parse: BSR set exceeded MaxElements")
|
||||
|
||||
const inf = -1
|
||||
|
||||
const (
|
||||
kNT = 0
|
||||
kTerm = 1
|
||||
kEps = 2
|
||||
)
|
||||
|
||||
// sInfo decodes a slot id into (nonterminal, alternate, dot, production length).
|
||||
type sInfo struct{ nt, alt, dot, plen int }
|
||||
|
||||
// qSym is a query-only symbol descriptor (kind, and nt for nonterminals).
|
||||
type qSym struct{ kind, nt int }
|
||||
|
||||
// qNT is query-only nonterminal metadata.
|
||||
type qNT struct {
|
||||
isRule bool
|
||||
ruleName string
|
||||
alts [][]qSym
|
||||
}
|
||||
|
||||
type bsrElem struct {
|
||||
sl int
|
||||
l, k, r int
|
||||
}
|
||||
|
||||
type gssKey struct {
|
||||
ret int
|
||||
pos int
|
||||
rc int
|
||||
}
|
||||
|
||||
type gnode struct {
|
||||
ret int
|
||||
pos int
|
||||
rc int
|
||||
edges []*gnode
|
||||
edgeSet map[*gnode]bool
|
||||
}
|
||||
|
||||
type desc struct {
|
||||
L, i, rc int
|
||||
u *gnode
|
||||
}
|
||||
|
||||
type parser struct {
|
||||
input []byte
|
||||
set map[bsrElem]bool
|
||||
gss map[gssKey]*gnode
|
||||
u0 *gnode
|
||||
seen map[desc]bool
|
||||
work []desc
|
||||
popped map[*gnode]map[int]bool
|
||||
aborted bool
|
||||
}
|
||||
|
||||
func foldB(b byte) byte {
|
||||
if b >= 'A' && b <= 'Z' {
|
||||
return b - 'A' + 'a'
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func foldR(r rune) rune {
|
||||
if r >= 'A' && r <= 'Z' {
|
||||
return r - 'A' + 'a'
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
func bytesEqual(a, b []byte) bool {
|
||||
if len(a) != len(b) {
|
||||
return false
|
||||
}
|
||||
for i := range a {
|
||||
if a[i] != b[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (p *parser) add(L int, u *gnode, i, rc int) {
|
||||
d := desc{L: L, i: i, rc: rc, u: u}
|
||||
if p.seen[d] {
|
||||
return
|
||||
}
|
||||
p.seen[d] = true
|
||||
p.work = append(p.work, d)
|
||||
}
|
||||
|
||||
func (p *parser) gssNodeFor(ret, pos, rc int) *gnode {
|
||||
k := gssKey{ret, pos, rc}
|
||||
if v, ok := p.gss[k]; ok {
|
||||
return v
|
||||
}
|
||||
v := &gnode{ret: ret, pos: pos, rc: rc, edgeSet: map[*gnode]bool{}}
|
||||
p.gss[k] = v
|
||||
return v
|
||||
}
|
||||
|
||||
// record inserts a BSR element, keeping only intermediate (prefix length >= 2)
|
||||
// or complete (dot == len) extents.
|
||||
func (p *parser) record(L, l, k, r int) {
|
||||
si := slotInfo[L]
|
||||
if si.dot >= 2 || si.dot == si.plen {
|
||||
if MaxElements > 0 && len(p.set) >= MaxElements {
|
||||
p.aborted = true
|
||||
return
|
||||
}
|
||||
p.set[bsrElem{L, l, k, r}] = true
|
||||
}
|
||||
}
|
||||
|
||||
func (p *parser) create(ret int, u *gnode, i, rc int) *gnode {
|
||||
v := p.gssNodeFor(ret, i, rc)
|
||||
if !v.edgeSet[u] {
|
||||
v.edgeSet[u] = true
|
||||
v.edges = append(v.edges, u)
|
||||
for j := range p.popped[v] {
|
||||
p.record(ret, u.pos, i, j)
|
||||
p.add(ret, u, j, rc)
|
||||
}
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
func (p *parser) pop(u *gnode, j int) {
|
||||
if u == p.u0 {
|
||||
return
|
||||
}
|
||||
if p.popped[u] == nil {
|
||||
p.popped[u] = map[int]bool{}
|
||||
}
|
||||
p.popped[u][j] = true
|
||||
for _, v := range u.edges {
|
||||
p.record(u.ret, v.pos, u.pos, j)
|
||||
p.add(u.ret, v, j, u.rc)
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
// queryBoilerplate: Result, reconstruction queries (over slotInfo/ntComplete/
|
||||
// qNTs), and the public Parse. Grammar-independent.
|
||||
const queryBoilerplate = `
|
||||
// ---- result + reconstruction queries ----
|
||||
|
||||
type ParseTree struct {
|
||||
Rule string
|
||||
Start, End int
|
||||
Children []*ParseTree
|
||||
}
|
||||
|
||||
type bsrKey struct {
|
||||
sl int
|
||||
l, r int
|
||||
}
|
||||
|
||||
type nodeID struct {
|
||||
sl int // slot id for intermediate nodes; -1 for symbol nodes
|
||||
nt int // nonterminal id for symbol nodes
|
||||
l, r int
|
||||
}
|
||||
|
||||
// Result holds the BSR set for a parse and answers queries over it.
|
||||
type Result struct {
|
||||
input []byte
|
||||
set map[bsrElem]bool
|
||||
index map[bsrKey][]int
|
||||
n int
|
||||
}
|
||||
|
||||
func (r *Result) buildIndex() {
|
||||
if r.index != nil {
|
||||
return
|
||||
}
|
||||
r.index = make(map[bsrKey][]int, len(r.set))
|
||||
for e := range r.set {
|
||||
bk := bsrKey{e.sl, e.l, e.r}
|
||||
r.index[bk] = append(r.index[bk], e.k)
|
||||
}
|
||||
}
|
||||
|
||||
// Valid reports whether the whole input is derivable by the root rule.
|
||||
func (r *Result) Valid() bool {
|
||||
r.buildIndex()
|
||||
for _, sl := range ntComplete[gStart] {
|
||||
if len(r.index[bsrKey{sl, 0, r.n}]) > 0 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Elements returns the number of BSR elements (representation size).
|
||||
func (r *Result) Elements() int { return len(r.set) }
|
||||
|
||||
// NumTrees returns the number of distinct parse trees, or -1 for infinitely many.
|
||||
func (r *Result) NumTrees() *big.Int {
|
||||
if !r.Valid() {
|
||||
return big.NewInt(0)
|
||||
}
|
||||
memo := map[nodeID]*big.Int{}
|
||||
onStack := map[nodeID]bool{}
|
||||
infinite := false
|
||||
|
||||
var symWays func(s qSym, a, b int) *big.Int
|
||||
var countSym func(nt, l, rr int) *big.Int
|
||||
var countInter func(sl, l, rr int) *big.Int
|
||||
var countElem func(sl, l, k, rr int) *big.Int
|
||||
|
||||
symWays = func(s qSym, a, b int) *big.Int {
|
||||
if s.kind == kNT {
|
||||
return countSym(s.nt, a, b)
|
||||
}
|
||||
return big.NewInt(1)
|
||||
}
|
||||
countElem = func(sl, l, k, rr int) *big.Int {
|
||||
si := slotInfo[sl]
|
||||
prod := qNTs[si.nt].alts[si.alt]
|
||||
last := symWays(prod[si.dot-1], k, rr)
|
||||
var pre *big.Int
|
||||
switch {
|
||||
case si.dot == 1:
|
||||
pre = big.NewInt(1)
|
||||
case si.dot == 2:
|
||||
pre = symWays(prod[0], l, k)
|
||||
default:
|
||||
pre = countInter(sl-1, l, k)
|
||||
}
|
||||
return new(big.Int).Mul(pre, last)
|
||||
}
|
||||
countInter = func(sl, l, rr int) *big.Int {
|
||||
id := nodeID{sl: sl, nt: -1, l: l, r: rr}
|
||||
if v, ok := memo[id]; ok {
|
||||
return v
|
||||
}
|
||||
if onStack[id] {
|
||||
infinite = true
|
||||
return big.NewInt(1)
|
||||
}
|
||||
onStack[id] = true
|
||||
total := big.NewInt(0)
|
||||
for _, k := range r.index[bsrKey{sl, l, rr}] {
|
||||
total.Add(total, countElem(sl, l, k, rr))
|
||||
}
|
||||
onStack[id] = false
|
||||
memo[id] = total
|
||||
return total
|
||||
}
|
||||
countSym = func(nt, l, rr int) *big.Int {
|
||||
id := nodeID{sl: -1, nt: nt, l: l, r: rr}
|
||||
if v, ok := memo[id]; ok {
|
||||
return v
|
||||
}
|
||||
if onStack[id] {
|
||||
infinite = true
|
||||
return big.NewInt(1)
|
||||
}
|
||||
onStack[id] = true
|
||||
total := big.NewInt(0)
|
||||
for _, sl := range ntComplete[nt] {
|
||||
for _, k := range r.index[bsrKey{sl, l, rr}] {
|
||||
total.Add(total, countElem(sl, l, k, rr))
|
||||
}
|
||||
}
|
||||
onStack[id] = false
|
||||
memo[id] = total
|
||||
return total
|
||||
}
|
||||
res := countSym(gStart, 0, r.n)
|
||||
if infinite {
|
||||
return big.NewInt(-1)
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
// Ambiguous reports whether the input has more than one distinct parse tree.
|
||||
func (r *Result) Ambiguous() bool {
|
||||
if !r.Valid() {
|
||||
return false
|
||||
}
|
||||
visited := map[nodeID]bool{}
|
||||
var symAmb func(nt, l, rr int) bool
|
||||
var interAmb func(sl, l, rr int) bool
|
||||
var elemAmb func(sl, l, k, rr int) bool
|
||||
var symOf func(s qSym, a, b int) bool
|
||||
symOf = func(s qSym, a, b int) bool {
|
||||
if s.kind == kNT {
|
||||
return symAmb(s.nt, a, b)
|
||||
}
|
||||
return false
|
||||
}
|
||||
elemAmb = func(sl, l, k, rr int) bool {
|
||||
si := slotInfo[sl]
|
||||
prod := qNTs[si.nt].alts[si.alt]
|
||||
if symOf(prod[si.dot-1], k, rr) {
|
||||
return true
|
||||
}
|
||||
switch {
|
||||
case si.dot == 1:
|
||||
return false
|
||||
case si.dot == 2:
|
||||
return symOf(prod[0], l, k)
|
||||
default:
|
||||
return interAmb(sl-1, l, k)
|
||||
}
|
||||
}
|
||||
interAmb = func(sl, l, rr int) bool {
|
||||
id := nodeID{sl: sl, nt: -1, l: l, r: rr}
|
||||
if visited[id] {
|
||||
return false
|
||||
}
|
||||
visited[id] = true
|
||||
ks := r.index[bsrKey{sl, l, rr}]
|
||||
if len(ks) > 1 {
|
||||
return true
|
||||
}
|
||||
for _, k := range ks {
|
||||
if elemAmb(sl, l, k, rr) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
symAmb = func(nt, l, rr int) bool {
|
||||
id := nodeID{sl: -1, nt: nt, l: l, r: rr}
|
||||
if visited[id] {
|
||||
return false
|
||||
}
|
||||
visited[id] = true
|
||||
packs := 0
|
||||
for _, sl := range ntComplete[nt] {
|
||||
packs += len(r.index[bsrKey{sl, l, rr}])
|
||||
}
|
||||
if packs > 1 {
|
||||
return true
|
||||
}
|
||||
for _, sl := range ntComplete[nt] {
|
||||
for _, k := range r.index[bsrKey{sl, l, rr}] {
|
||||
if elemAmb(sl, l, k, rr) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
return symAmb(gStart, 0, r.n)
|
||||
}
|
||||
|
||||
// Tree extracts a single parse tree (first element at each extent), or nil if
|
||||
// the input is invalid. Synthetic nonterminals are collapsed to rule names.
|
||||
func (r *Result) Tree() *ParseTree {
|
||||
if !r.Valid() {
|
||||
return nil
|
||||
}
|
||||
for _, sl := range ntComplete[gStart] {
|
||||
if ks := r.index[bsrKey{sl, 0, r.n}]; len(ks) > 0 {
|
||||
var kids []*ParseTree
|
||||
r.collectElem(sl, 0, ks[0], r.n, &kids, map[nodeID]bool{})
|
||||
return &ParseTree{Rule: qNTs[gStart].ruleName, Start: 0, End: r.n, Children: kids}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Result) collectElem(sl, l, k, rr int, out *[]*ParseTree, visited map[nodeID]bool) {
|
||||
si := slotInfo[sl]
|
||||
prod := qNTs[si.nt].alts[si.alt]
|
||||
switch {
|
||||
case si.dot == 1:
|
||||
case si.dot == 2:
|
||||
r.collectChild(prod[0], l, k, out, visited)
|
||||
default:
|
||||
if ks := r.index[bsrKey{sl - 1, l, k}]; len(ks) > 0 {
|
||||
r.collectElem(sl-1, l, ks[0], k, out, visited)
|
||||
}
|
||||
}
|
||||
r.collectChild(prod[si.dot-1], k, rr, out, visited)
|
||||
}
|
||||
|
||||
func (r *Result) collectChild(s qSym, a, b int, out *[]*ParseTree, visited map[nodeID]bool) {
|
||||
if s.kind != kNT {
|
||||
if s.kind == kEps {
|
||||
return
|
||||
}
|
||||
*out = append(*out, &ParseTree{Start: a, End: b})
|
||||
return
|
||||
}
|
||||
id := nodeID{sl: -1, nt: s.nt, l: a, r: b}
|
||||
if visited[id] {
|
||||
return
|
||||
}
|
||||
visited[id] = true
|
||||
for _, sl := range ntComplete[s.nt] {
|
||||
if ks := r.index[bsrKey{sl, a, b}]; len(ks) > 0 {
|
||||
if qNTs[s.nt].isRule {
|
||||
var kids []*ParseTree
|
||||
r.collectElem(sl, a, ks[0], b, &kids, visited)
|
||||
*out = append(*out, &ParseTree{Rule: qNTs[s.nt].ruleName, Start: a, End: b, Children: kids})
|
||||
} else {
|
||||
r.collectElem(sl, a, ks[0], b, out, visited)
|
||||
}
|
||||
visited[id] = false
|
||||
return
|
||||
}
|
||||
}
|
||||
visited[id] = false
|
||||
}
|
||||
|
||||
// Parse parses input under the baked root rule using the specialised engine.
|
||||
func Parse(input []byte) (*Result, error) {
|
||||
p := &parser{
|
||||
input: input,
|
||||
set: map[bsrElem]bool{},
|
||||
gss: map[gssKey]*gnode{},
|
||||
seen: map[desc]bool{},
|
||||
popped: map[*gnode]map[int]bool{},
|
||||
}
|
||||
p.u0 = &gnode{ret: -1, edgeSet: map[*gnode]bool{}}
|
||||
p.seed()
|
||||
for len(p.work) > 0 {
|
||||
if p.aborted {
|
||||
return nil, ErrTooLarge
|
||||
}
|
||||
d := p.work[len(p.work)-1]
|
||||
p.work = p.work[:len(p.work)-1]
|
||||
p.process(d)
|
||||
}
|
||||
return &Result{input: input, set: p.set, n: len(input)}, nil
|
||||
}
|
||||
`
|
||||
+237
@@ -0,0 +1,237 @@
|
||||
package goabnf
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type node struct {
|
||||
Rulename string
|
||||
index, lowlink int
|
||||
onStack bool
|
||||
Dependencies []string
|
||||
}
|
||||
|
||||
// Depgraph contains for each rule the rules it depends on.
|
||||
// Notice it does not differentiate between a mandatory dependency
|
||||
// or an avoidable one.
|
||||
type Depgraph map[string]*node
|
||||
|
||||
// DependencyGraph creates a dependency graph for a whole grammar.
|
||||
// It includes core rules only if necessary.
|
||||
func (g *Grammar) DependencyGraph() Depgraph {
|
||||
graph := Depgraph{}
|
||||
for _, corerule := range coreRules {
|
||||
graph[strings.ToLower(corerule.Name)] = &node{
|
||||
Rulename: corerule.Name,
|
||||
Dependencies: getDependencies(corerule.Alternation),
|
||||
}
|
||||
}
|
||||
for _, rule := range g.Rulemap {
|
||||
graph[strings.ToLower(rule.Name)] = &node{
|
||||
Rulename: rule.Name,
|
||||
Dependencies: getDependencies(rule.Alternation),
|
||||
}
|
||||
}
|
||||
return graph
|
||||
}
|
||||
|
||||
func getDependencies(alt Alternation) []string {
|
||||
deps := []string{}
|
||||
for _, conc := range alt.Concatenations {
|
||||
for _, rep := range conc.Repetitions {
|
||||
switch v := rep.Element.(type) {
|
||||
case ElemGroup:
|
||||
deps = appendDeps(deps, getDependencies(v.Alternation)...)
|
||||
case ElemOption:
|
||||
deps = appendDeps(deps, getDependencies(v.Alternation)...)
|
||||
case ElemRulename:
|
||||
deps = appendDeps(deps, strings.ToLower(v.Name))
|
||||
}
|
||||
}
|
||||
}
|
||||
return deps
|
||||
}
|
||||
|
||||
func appendDeps(deps []string, ndeps ...string) []string {
|
||||
for _, ndep := range ndeps {
|
||||
if !slices.Contains(deps, ndep) {
|
||||
deps = append(deps, ndep)
|
||||
}
|
||||
}
|
||||
return deps
|
||||
}
|
||||
|
||||
// Mermaid returns a flowchart of the dependency graph.
|
||||
func (dg Depgraph) Mermaid() string {
|
||||
var out strings.Builder
|
||||
out.WriteString("flowchart TD\n")
|
||||
for _, node := range dg {
|
||||
for _, dep := range node.Dependencies {
|
||||
fmt.Fprintf(&out, "\t%s --> %s\n", node.Rulename, dep)
|
||||
}
|
||||
out.WriteString("\n")
|
||||
}
|
||||
return out.String()
|
||||
}
|
||||
|
||||
// IsDag find Strongly Connected Components using Tarjan's algorithm
|
||||
// and returns whether it contains cycle or not.
|
||||
// Could be improved by Nuutila's or Pearce's algorithms, or replaced
|
||||
// by Kosaraju's algorithm.
|
||||
func (g *Grammar) IsDAG() bool {
|
||||
scc := &cycle{
|
||||
index: 0,
|
||||
stack: []*node{},
|
||||
dg: g.DependencyGraph(),
|
||||
}
|
||||
scc.find()
|
||||
|
||||
for _, sccs := range scc.sccs {
|
||||
if len(sccs) > 1 {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// RuleContainsCycle returns whether the rule contains a cycle or not.
|
||||
// It travels through the whole rule dependency graph, such that
|
||||
// it checks if the rule is cyclic AND if one of its dependency is too.
|
||||
func (g *Grammar) RuleContainsCycle(rulename string) (bool, error) {
|
||||
// Check the rule exists
|
||||
rule := GetRule(rulename, g.Rulemap)
|
||||
if rule == nil {
|
||||
return false, &ErrRuleNotFound{
|
||||
Rulename: rulename,
|
||||
}
|
||||
}
|
||||
|
||||
// Get all SCCs
|
||||
scc := &cycle{
|
||||
index: 0,
|
||||
stack: []*node{},
|
||||
dg: g.DependencyGraph(),
|
||||
}
|
||||
scc.find()
|
||||
|
||||
return ruleContainsCycle(scc.sccs, rulename), nil
|
||||
}
|
||||
|
||||
func ruleContainsCycle(sccs [][]*node, rulename string) bool {
|
||||
// Index nodes by lowercase name and flag those sitting in a non-trivial SCC
|
||||
// (size > 1 == a cycle). Precomputing this removes the O(V) SCC scan that
|
||||
// the previous implementation did on every recursive step.
|
||||
nodeOf := make(map[string]*node, len(sccs))
|
||||
inCycle := make(map[string]bool, len(sccs))
|
||||
|
||||
for _, s := range sccs {
|
||||
nontrivial := len(s) > 1
|
||||
for _, nd := range s {
|
||||
key := strings.ToLower(nd.Rulename)
|
||||
nodeOf[key] = nd
|
||||
inCycle[key] = nontrivial
|
||||
}
|
||||
}
|
||||
return ruleCycleVisit(strings.ToLower(rulename), nodeOf, inCycle, map[string]bool{})
|
||||
}
|
||||
|
||||
// ruleCycleVisit reports whether the rule (or any transitive dependency) is
|
||||
// cyclic. The visited set keeps each rule explored at most once: without it a
|
||||
// diamond-shaped dependency DAG is walked along every root-to-leaf path, which
|
||||
// is exponential. Marking on entry is safe because a cyclic rule returns true
|
||||
// here before it can ever be marked-and-skipped, so a memoized rule is always
|
||||
// genuinely acyclic.
|
||||
func ruleCycleVisit(key string, nodeOf map[string]*node, inCycle, visited map[string]bool) bool {
|
||||
if visited[key] {
|
||||
return false
|
||||
}
|
||||
visited[key] = true
|
||||
nd := nodeOf[key]
|
||||
if nd == nil {
|
||||
// Unknown rule: treat as acyclic (matches the previous behaviour).
|
||||
return false
|
||||
}
|
||||
|
||||
// Self-loop (a size-1 SCC with a self-edge is not flagged by inCycle).
|
||||
for _, dep := range nd.Dependencies {
|
||||
if strings.ToLower(dep) == key {
|
||||
return true
|
||||
}
|
||||
}
|
||||
if inCycle[key] {
|
||||
// If it depends on itself or is part of an SCC, then is cylic
|
||||
return true
|
||||
}
|
||||
|
||||
for _, dep := range nd.Dependencies {
|
||||
dk := strings.ToLower(dep)
|
||||
if dk == key {
|
||||
continue
|
||||
}
|
||||
if ruleCycleVisit(dk, nodeOf, inCycle, visited) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type cycle struct {
|
||||
index int
|
||||
stack []*node
|
||||
sccs [][]*node
|
||||
|
||||
dg Depgraph
|
||||
}
|
||||
|
||||
func (c *cycle) find() {
|
||||
for _, v := range c.dg {
|
||||
if v.index == 0 {
|
||||
c.strongconnect(v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *cycle) strongconnect(v *node) {
|
||||
// Set the depth index for v to the smallest unused index
|
||||
v.index = c.index
|
||||
v.lowlink = c.index
|
||||
c.index++
|
||||
c.stack = append(c.stack, v)
|
||||
v.onStack = true
|
||||
|
||||
// Consider successors of v
|
||||
for _, dep := range v.Dependencies {
|
||||
w, ok := c.dg[dep]
|
||||
if !ok {
|
||||
// core rules, as we know they won't have a cycle thus
|
||||
// no SCC, we don't need to recurse.
|
||||
continue
|
||||
}
|
||||
if w.index == 0 {
|
||||
// Successor w has not yet been visited; recurse on it
|
||||
c.strongconnect(w)
|
||||
v.lowlink = min(v.lowlink, w.lowlink)
|
||||
} else {
|
||||
if w.onStack {
|
||||
// Successor w is in stack S and hence in the current SCC
|
||||
// If w is not on stack, then (v, w) is an edge pointing
|
||||
// to an SCC already found and must be ignored.
|
||||
v.lowlink = min(v.lowlink, w.index)
|
||||
}
|
||||
}
|
||||
}
|
||||
// If v is a root node, pop the stack and generate an SCC
|
||||
if v.lowlink == v.index {
|
||||
scc := []*node{}
|
||||
w := (*node)(nil)
|
||||
for w == nil || !strings.EqualFold(v.Rulename, w.Rulename) {
|
||||
w = c.stack[len(c.stack)-1]
|
||||
c.stack = c.stack[:len(c.stack)-1]
|
||||
w.onStack = false
|
||||
scc = append(scc, w)
|
||||
}
|
||||
c.sccs = append(c.sccs, scc)
|
||||
}
|
||||
}
|
||||
+245
@@ -0,0 +1,245 @@
|
||||
package goabnf
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrNoSolutionFound is an error returned when parsing an ABNF
|
||||
// grammar and no solution has been found.
|
||||
//
|
||||
// On the ABNF parsing path it is wrapped by a [*ParseError] carrying the
|
||||
// position at which parsing got stuck; errors.Is(err, ErrNoSolutionFound)
|
||||
// keeps matching.
|
||||
ErrNoSolutionFound = errors.New("no solution found, input ABNF grammar may be invalid")
|
||||
|
||||
// ErrHandlingProseVal is an error returned when an operation tried
|
||||
// to produce something on a prose-val, but it can't be handled properly.
|
||||
ErrHandlingProseVal = errors.New("can't handle prose-val descriptions")
|
||||
|
||||
// ErrProseValInTransitionGraph is returned when building a transition graph
|
||||
// from a grammar that contains a prose-val, which has no defined expansion.
|
||||
ErrProseValInTransitionGraph = errors.New("prose value is not supported in transition graphs")
|
||||
|
||||
// ErrRepetitionThreshold is returned when expanding a repetition whose bound
|
||||
// exceeds the threshold set with WithRepetitionThreshold.
|
||||
ErrRepetitionThreshold = errors.New("repetition threshold reached")
|
||||
|
||||
// ErrRepetitionMinNotOne is returned when a mandatory (max == 1) repetition
|
||||
// has a minimum other than 1.
|
||||
ErrRepetitionMinNotOne = errors.New("repetition minimum must be 1 when maximum is 1")
|
||||
|
||||
// ErrRepetitionMinGtMax is returned when a repetition has a minimum greater
|
||||
// than its maximum.
|
||||
ErrRepetitionMinGtMax = errors.New("repetition minimum is greater than maximum")
|
||||
)
|
||||
|
||||
// ParseError reports where parsing input against a grammar got stuck. It is
|
||||
// returned (wrapped around [ErrNoSolutionFound]) when an input does not parse,
|
||||
// to help locate the offending construct -- in particular when writing or
|
||||
// debugging an ABNF grammar.
|
||||
type ParseError struct {
|
||||
// Offset is the byte offset into the input of the furthest position the
|
||||
// parser was able to reach before failing.
|
||||
Offset int
|
||||
// Line and Col are the 1-based line and column of Offset.
|
||||
Line, Col int
|
||||
// Expected lists the terminals the parser could have consumed at Offset, if
|
||||
// that information was available. It may be empty.
|
||||
Expected []string
|
||||
// Found describes the input byte at Offset (e.g. `%x0A (LF)`), or "EOF" when
|
||||
// Offset is at the end of input. It may be empty.
|
||||
Found string
|
||||
// Hint, when set, gives a plain-language diagnosis of a common mistake (e.g.
|
||||
// bare LF line endings where ABNF requires CR LF).
|
||||
Hint string
|
||||
}
|
||||
|
||||
var _ error = (*ParseError)(nil)
|
||||
|
||||
func (e *ParseError) Error() string {
|
||||
var b strings.Builder
|
||||
fmt.Fprintf(&b, "parse error at line %d, column %d (offset %d)", e.Line, e.Col, e.Offset)
|
||||
switch {
|
||||
case len(e.Expected) > 0:
|
||||
fmt.Fprintf(&b, ": expected %s", strings.Join(e.Expected, ", "))
|
||||
if e.Found != "" {
|
||||
fmt.Fprintf(&b, ", found %s", e.Found)
|
||||
}
|
||||
case e.Found != "":
|
||||
fmt.Fprintf(&b, ": unexpected %s", e.Found)
|
||||
default:
|
||||
fmt.Fprintf(&b, ": %s", ErrNoSolutionFound)
|
||||
}
|
||||
if e.Hint != "" {
|
||||
fmt.Fprintf(&b, " (hint: %s)", e.Hint)
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// Unwrap reports ErrNoSolutionFound so that callers matching the historical
|
||||
// sentinel with errors.Is keep working.
|
||||
func (e *ParseError) Unwrap() error { return ErrNoSolutionFound }
|
||||
|
||||
// describeByte renders an input byte the way ABNF terminals are written, naming
|
||||
// the control characters that matter most when debugging a grammar.
|
||||
func describeByte(b byte) string {
|
||||
switch b {
|
||||
case '\r':
|
||||
return "%x0D (CR)"
|
||||
case '\n':
|
||||
return "%x0A (LF)"
|
||||
case '\t':
|
||||
return "%x09 (HTAB)"
|
||||
case ' ':
|
||||
return "%x20 (SP)"
|
||||
}
|
||||
if b >= 0x20 && b < 0x7f {
|
||||
return fmt.Sprintf("%q", string(b))
|
||||
}
|
||||
return fmt.Sprintf("%%x%02X", b)
|
||||
}
|
||||
|
||||
// newParseError builds a ParseError from a byte offset into input, computing the
|
||||
// 1-based line and column, the byte found there, and a hint for the bare-LF
|
||||
// line-ending mistake (the most common ABNF authoring error).
|
||||
func newParseError(input []byte, offset int, expected []string) *ParseError {
|
||||
if offset < 0 {
|
||||
offset = 0
|
||||
}
|
||||
if offset > len(input) {
|
||||
offset = len(input)
|
||||
}
|
||||
line, col := 1, 1
|
||||
for i := 0; i < offset; i++ {
|
||||
if input[i] == '\n' {
|
||||
line++
|
||||
col = 1
|
||||
} else {
|
||||
col++
|
||||
}
|
||||
}
|
||||
exp := append([]string(nil), expected...)
|
||||
sort.Strings(exp)
|
||||
pe := &ParseError{Offset: offset, Line: line, Col: col, Expected: exp}
|
||||
if offset < len(input) {
|
||||
pe.Found = describeByte(input[offset])
|
||||
// Bare LF where the previous byte is not CR: ABNF lines end with CR LF,
|
||||
// so a lone LF is almost always the cause.
|
||||
if input[offset] == '\n' && (offset == 0 || input[offset-1] != '\r') {
|
||||
pe.Hint = `input uses bare LF line endings, but ABNF requires CR LF ("\r\n")`
|
||||
}
|
||||
} else {
|
||||
pe.Found = "EOF"
|
||||
}
|
||||
return pe
|
||||
}
|
||||
|
||||
// ErrMultipleSolutionsFound is an error returned when a parser found
|
||||
// multiple solutions when none or one were expected.
|
||||
type ErrMultipleSolutionsFound struct{}
|
||||
|
||||
var _ error = (*ErrMultipleSolutionsFound)(nil)
|
||||
|
||||
func (err ErrMultipleSolutionsFound) Error() string {
|
||||
return "multiple solutions found, this should not happen. Please open an issue. This could eventually need an Erratum from IETF tracking"
|
||||
}
|
||||
|
||||
// ErrRuleNotFound is an error returned when the rule was not found
|
||||
// as part of the grammar.
|
||||
type ErrRuleNotFound struct {
|
||||
Rulename string
|
||||
}
|
||||
|
||||
var _ error = (*ErrRuleNotFound)(nil)
|
||||
|
||||
func (err ErrRuleNotFound) Error() string {
|
||||
return fmt.Sprintf("rule %s was not found in grammar", err.Rulename)
|
||||
}
|
||||
|
||||
// ErrCoreRuleModify is an error returned when an incremental alternative
|
||||
// for a core rule.
|
||||
type ErrCoreRuleModify struct {
|
||||
CoreRulename string
|
||||
}
|
||||
|
||||
var _ error = (*ErrCoreRuleModify)(nil)
|
||||
|
||||
func (err ErrCoreRuleModify) Error() string {
|
||||
return fmt.Sprintf("core rule %s can't be modified", err.CoreRulename)
|
||||
}
|
||||
|
||||
// ErrDependencyNotFound is an error returned during ABNF grammar
|
||||
// semantic vaildation, if a rule depends on an unexisting rule.
|
||||
type ErrDependencyNotFound struct {
|
||||
Rulename string
|
||||
}
|
||||
|
||||
var _ error = (*ErrDependencyNotFound)(nil)
|
||||
|
||||
func (err ErrDependencyNotFound) Error() string {
|
||||
return fmt.Sprintf("unsatisfied dependency (rule) %s", err.Rulename)
|
||||
}
|
||||
|
||||
// ErrSemanticRepetition is an error returned during ABNF grammar
|
||||
// semantic validation, if a repetition has min < max.
|
||||
type ErrSemanticRepetition struct {
|
||||
Repetition Repetition
|
||||
}
|
||||
|
||||
var _ error = (*ErrSemanticRepetition)(nil)
|
||||
|
||||
func (err ErrSemanticRepetition) Error() string {
|
||||
return fmt.Sprintf("invalid semantic of input ABNF grammar for repetition %s", err.Repetition)
|
||||
}
|
||||
|
||||
// ErrTooLargeNumeral is an error returned when the numeral value
|
||||
// provided to parse cannot be handled as a 7-bit US-ASCII valid value.
|
||||
type ErrTooLargeNumeral struct {
|
||||
Base, Value string
|
||||
}
|
||||
|
||||
var _ error = (*ErrTooLargeNumeral)(nil)
|
||||
|
||||
func (err ErrTooLargeNumeral) Error() string {
|
||||
return fmt.Sprintf("too large numeral value %s for base %s", err.Value, err.Base)
|
||||
}
|
||||
|
||||
// ErrDuplicatedRule is an error returned when the rule already
|
||||
// exist as part of the grammar.
|
||||
type ErrDuplicatedRule struct {
|
||||
Rulename string
|
||||
}
|
||||
|
||||
var _ error = (*ErrDuplicatedRule)(nil)
|
||||
|
||||
func (err ErrDuplicatedRule) Error() string {
|
||||
return fmt.Sprintf("rule %s was already defined in grammar", err.Rulename)
|
||||
}
|
||||
|
||||
// ErrCyclicRule is an error returned when can't work due to an
|
||||
// unavoidable cyclic rule.
|
||||
type ErrCyclicRule struct {
|
||||
Rulename string
|
||||
}
|
||||
|
||||
var _ error = (*ErrCyclicRule)(nil)
|
||||
|
||||
func (err ErrCyclicRule) Error() string {
|
||||
return fmt.Sprintf("can't generate a content as the rule %s involves an unavoidable cycle", err.Rulename)
|
||||
}
|
||||
|
||||
// ErrMaxNodesExceeded is returned by TransitionGraph when construction would
|
||||
// allocate more nodes than the budget set with WithMaxNodes.
|
||||
type ErrMaxNodesExceeded struct {
|
||||
Max int
|
||||
}
|
||||
|
||||
var _ error = (*ErrMaxNodesExceeded)(nil)
|
||||
|
||||
func (err ErrMaxNodesExceeded) Error() string {
|
||||
return fmt.Sprintf("transition graph node budget of %d exceeded", err.Max)
|
||||
}
|
||||
+775
@@ -0,0 +1,775 @@
|
||||
package goabnf
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// fuzz.go turns a (fully expanded) transition graph into a total
|
||||
// function from arbitrary bytes to a string the grammar admits. It is the core
|
||||
// primitive for structure-aware fuzzing with testing.F: because every input
|
||||
// maps to a valid production, the fuzzing engine's mutations explore the
|
||||
// grammar's structure instead of bouncing off the target's first syntax check.
|
||||
//
|
||||
// The walk is deliberately different from a TransitionGraphReader: the reader
|
||||
// enumerates entrypoint->endpoint trails (each directed edge at most once) for
|
||||
// finite coverage, whereas the generator takes a free walk that MAY repeat
|
||||
// edges -- so it can unroll a "*" repetition into "a", "aa", "aaa", ... -- and
|
||||
// is kept finite by explicit budgets (max length, per-edge repetitions, total
|
||||
// steps) rather than by the no-repeat rule. When a budget is hit the walk does
|
||||
// not stop dead (that could leave it on a non-accepting node); it steers along
|
||||
// shortest paths to the nearest endpoint, so the output is always a complete,
|
||||
// valid production.
|
||||
|
||||
// source supplies the decisions that drive a walk. Two implementations exist: a
|
||||
// byte tape (for testing.F, where mutations of the tape are the fuzzing signal)
|
||||
// and a *rand.Rand (for property tests and corpus generation). intn(n) returns
|
||||
// a value in [0, n); when a tape is exhausted it returns 0, which makes
|
||||
// generation total and biases an empty/short tape toward the shortest
|
||||
// production.
|
||||
type source interface {
|
||||
intn(n int) int
|
||||
}
|
||||
|
||||
type tapeSource struct {
|
||||
tape []byte
|
||||
pos int
|
||||
}
|
||||
|
||||
func (s *tapeSource) intn(n int) int {
|
||||
if n <= 1 {
|
||||
return 0
|
||||
}
|
||||
// Read just enough bytes to address n, big-endian. One byte per decision
|
||||
// when n <= 256 keeps locality high: a single-byte mutation flips at most
|
||||
// one decision (until the number of decisions changes).
|
||||
width := 1
|
||||
for max := n; max > 256; max = (max + 255) / 256 {
|
||||
width++
|
||||
}
|
||||
v := 0
|
||||
for i := 0; i < width; i++ {
|
||||
b := 0
|
||||
if s.pos < len(s.tape) {
|
||||
b = int(s.tape[s.pos])
|
||||
s.pos++
|
||||
}
|
||||
v = v<<8 | b
|
||||
if v < 0 { // overflow guard on absurd widths
|
||||
v &= 0x7fffffff
|
||||
}
|
||||
}
|
||||
return v % n
|
||||
}
|
||||
|
||||
type randSource struct{ r *rand.Rand }
|
||||
|
||||
func (s randSource) intn(n int) int {
|
||||
if n <= 1 {
|
||||
return 0
|
||||
}
|
||||
return s.r.Intn(n)
|
||||
}
|
||||
|
||||
// indexedSource addresses the tape by decision number rather than a moving
|
||||
// cursor: the i-th decision always reads the fixed-width slot [i*stride, ...],
|
||||
// regardless of how many bytes earlier decisions "needed". This keeps a tape
|
||||
// edit local -- it can only perturb the one decision whose slot it lands in --
|
||||
// so an upstream structural change no longer reframes the bytes every later
|
||||
// decision reads (the residual non-locality is purely semantic: a forked walk
|
||||
// reaches different nodes, where the same slot byte means a different choice).
|
||||
// stride 4 addresses any choice count up to 2^32, well above the widest
|
||||
// (full-Unicode) num-val range.
|
||||
type indexedSource struct {
|
||||
tape []byte
|
||||
i int
|
||||
}
|
||||
|
||||
const indexedStride = 4
|
||||
|
||||
func (s *indexedSource) intn(n int) int {
|
||||
if n <= 1 {
|
||||
return 0
|
||||
}
|
||||
off := s.i * indexedStride
|
||||
s.i++
|
||||
v := 0
|
||||
for k := range indexedStride {
|
||||
b := 0
|
||||
if off+k < len(s.tape) {
|
||||
b = int(s.tape[off+k])
|
||||
}
|
||||
v = v<<8 | b
|
||||
if v < 0 {
|
||||
v &= 0x7fffffff
|
||||
}
|
||||
}
|
||||
return v % n
|
||||
}
|
||||
|
||||
// Generator produces strings admitted by a transition graph.
|
||||
type Generator struct {
|
||||
tg *TransitionGraph
|
||||
|
||||
maxLength int
|
||||
maxReps int
|
||||
maxSteps int
|
||||
|
||||
stableAddr bool
|
||||
|
||||
endSet map[string]bool // node ID -> is an endpoint ("" == emptyNode)
|
||||
canReach map[string]bool // node ID -> can still reach some endpoint
|
||||
distToEnd map[string]int // node ID -> fewest steps to an endpoint
|
||||
entries []*Node // entrypoints that can reach an endpoint
|
||||
|
||||
emitCache map[string][]emission // node ID -> octet emissions (lazy, for ExpectedNext)
|
||||
emitOK map[string]bool // node ID -> emissions analyzable
|
||||
}
|
||||
|
||||
// GenOption configures a Generator.
|
||||
type GenOption interface {
|
||||
apply(*Generator)
|
||||
}
|
||||
|
||||
type genOptionFunc func(*Generator)
|
||||
|
||||
func (f genOptionFunc) apply(g *Generator) {
|
||||
f(g)
|
||||
}
|
||||
|
||||
// WithMaxLength bounds the number of bytes a generated production may contain.
|
||||
// Once reached the walk steers to the nearest endpoint. Defaults to 4096.
|
||||
func WithMaxLength(n int) GenOption {
|
||||
return genOptionFunc(func(g *Generator) {
|
||||
g.maxLength = n
|
||||
})
|
||||
}
|
||||
|
||||
// WithMaxReps bounds how many times a single edge may be traversed, which caps
|
||||
// how far repetitions ("*" / "1*" / "n*m") are unrolled. Defaults to 32.
|
||||
func WithMaxReps(n int) GenOption {
|
||||
return genOptionFunc(func(g *Generator) {
|
||||
g.maxReps = n
|
||||
})
|
||||
}
|
||||
|
||||
// WithMaxSteps is a hard backstop on the total number of walk steps, defending
|
||||
// against pathological graphs. Defaults to 100000.
|
||||
func WithMaxSteps(n int) GenOption {
|
||||
return genOptionFunc(func(g *Generator) {
|
||||
g.maxSteps = n
|
||||
})
|
||||
}
|
||||
|
||||
// WithStableAddressing makes Generate address the tape by decision number
|
||||
// (fixed-width slots) instead of a moving cursor. This improves locality for a
|
||||
// coverage-guided engine -- a one-byte tape edit perturbs a single decision
|
||||
// rather than reframing every decision downstream of it -- at the cost of
|
||||
// consuming the tape less densely. Recommended for use under testing.F.
|
||||
func WithStableAddressing() GenOption {
|
||||
return genOptionFunc(func(g *Generator) {
|
||||
g.stableAddr = true
|
||||
})
|
||||
}
|
||||
|
||||
// NewGenerator prepares a Generator from a transition graph. The graph must be
|
||||
// fully expanded -- build it with WithDeflateRules(true) when the target rule
|
||||
// references other rules -- because an unexpanded rule node carries no bytes and
|
||||
// could not be turned into a valid production. It returns an error naming the
|
||||
// first such node otherwise. (Recursive rules cannot be expanded at all; in that
|
||||
// case TransitionGraph itself returns ErrCyclicRule before reaching here.)
|
||||
func NewGenerator(tg *TransitionGraph, opts ...GenOption) (*Generator, error) {
|
||||
g := &Generator{
|
||||
tg: tg,
|
||||
maxLength: 4096,
|
||||
maxReps: 32,
|
||||
maxSteps: 100_000,
|
||||
}
|
||||
for _, o := range opts {
|
||||
o.apply(g)
|
||||
}
|
||||
|
||||
// Collect every reachable node and the reverse adjacency, and verify the
|
||||
// graph is fully expanded (only producing leaves and the empty node).
|
||||
revAdj := map[string][]*Node{}
|
||||
nodes := map[string]*Node{}
|
||||
var stack []*Node
|
||||
for _, e := range tg.Entrypoints {
|
||||
if e != emptyNode {
|
||||
stack = append(stack, e)
|
||||
}
|
||||
}
|
||||
for len(stack) > 0 {
|
||||
n := stack[len(stack)-1]
|
||||
stack = stack[:len(stack)-1]
|
||||
id := tgNodeID(n)
|
||||
if _, ok := nodes[id]; ok {
|
||||
continue
|
||||
}
|
||||
nodes[id] = n
|
||||
if !producing(n) {
|
||||
return nil, fmt.Errorf("transition graph contains an unexpanded rule node %q; build it with WithDeflateRules(true)", n.Elem.String())
|
||||
}
|
||||
for _, sc := range n.Nexts {
|
||||
if sc == emptyNode {
|
||||
continue
|
||||
}
|
||||
revAdj[tgNodeID(sc)] = append(revAdj[tgNodeID(sc)], n)
|
||||
stack = append(stack, sc)
|
||||
}
|
||||
}
|
||||
|
||||
// endSet, then backward BFS from endpoints for canReach + distToEnd.
|
||||
g.endSet = map[string]bool{}
|
||||
g.canReach = map[string]bool{}
|
||||
g.distToEnd = map[string]int{}
|
||||
var q []string
|
||||
for _, e := range tg.Endpoints {
|
||||
id := tgNodeID(e)
|
||||
g.endSet[id] = true
|
||||
if !g.canReach[id] {
|
||||
g.canReach[id] = true
|
||||
g.distToEnd[id] = 0
|
||||
q = append(q, id)
|
||||
}
|
||||
}
|
||||
for len(q) > 0 {
|
||||
id := q[0]
|
||||
q = q[1:]
|
||||
for _, p := range revAdj[id] {
|
||||
pid := tgNodeID(p)
|
||||
if g.canReach[pid] {
|
||||
continue
|
||||
}
|
||||
g.canReach[pid] = true
|
||||
g.distToEnd[pid] = g.distToEnd[id] + 1
|
||||
q = append(q, pid)
|
||||
}
|
||||
}
|
||||
|
||||
for _, e := range tg.Entrypoints {
|
||||
if g.canReach[tgNodeID(e)] {
|
||||
g.entries = append(g.entries, e)
|
||||
}
|
||||
}
|
||||
return g, nil
|
||||
}
|
||||
|
||||
// Generate maps the tape to a production the grammar admits. It is total: any
|
||||
// tape (including nil) yields a valid production; an exhausted tape biases
|
||||
// toward the shortest completion.
|
||||
func (g *Generator) Generate(tape []byte) []byte {
|
||||
if g.stableAddr {
|
||||
return g.walk(&indexedSource{tape: tape}, nil)
|
||||
}
|
||||
return g.walk(&tapeSource{tape: tape}, nil)
|
||||
}
|
||||
|
||||
// GenerateRand draws a production using r as the entropy source.
|
||||
func (g *Generator) GenerateRand(r *rand.Rand) []byte {
|
||||
return g.walk(randSource{r: r}, nil)
|
||||
}
|
||||
|
||||
// traceSeg records that out[off:off+length] was emitted by a node carrying elem.
|
||||
type traceSeg struct {
|
||||
off, length int
|
||||
elem ElemItf
|
||||
}
|
||||
|
||||
func (g *Generator) walk(src source, trace *[]traceSeg) []byte {
|
||||
out := []byte{}
|
||||
if len(g.entries) == 0 {
|
||||
return out
|
||||
}
|
||||
node := g.entries[src.intn(len(g.entries))]
|
||||
|
||||
visits := map[string]int{}
|
||||
steps := 0
|
||||
for {
|
||||
if node != emptyNode && producing(node) {
|
||||
_, vt := nodeEmit(node, 0)
|
||||
if vt < 1 {
|
||||
vt = 1
|
||||
}
|
||||
var idx int32
|
||||
if vt > 1 {
|
||||
idx = int32(src.intn(int(vt)))
|
||||
}
|
||||
b, _ := nodeEmit(node, idx)
|
||||
if trace != nil && len(b) > 0 {
|
||||
*trace = append(*trace, traceSeg{off: len(out), length: len(b), elem: node.Elem})
|
||||
}
|
||||
out = append(out, b...)
|
||||
}
|
||||
|
||||
// The empty node is a terminal entry/endpoint sentinel with no
|
||||
// successors; reaching it as an entrypoint means the whole production is
|
||||
// nullable, so stopping here yields the (valid) empty production.
|
||||
if node == emptyNode {
|
||||
break
|
||||
}
|
||||
|
||||
id := tgNodeID(node)
|
||||
atEnd := g.endSet[id]
|
||||
|
||||
// liveAll: successors that can still reach an endpoint (used for
|
||||
// termination steering). liveCapped: those not yet at the per-edge cap
|
||||
// (used for ordinary random choice).
|
||||
var liveAll, liveCapped []*Node
|
||||
for _, sc := range node.Nexts {
|
||||
if sc == emptyNode || !g.canReach[tgNodeID(sc)] {
|
||||
continue
|
||||
}
|
||||
liveAll = append(liveAll, sc)
|
||||
if visits[id+">"+tgNodeID(sc)] < g.maxReps {
|
||||
liveCapped = append(liveCapped, sc)
|
||||
}
|
||||
}
|
||||
if len(liveAll) == 0 {
|
||||
break // terminal; canReach guarantees this node is an endpoint
|
||||
}
|
||||
|
||||
over := len(out) >= g.maxLength || steps >= g.maxSteps
|
||||
terminating := over || len(liveCapped) == 0
|
||||
|
||||
var next *Node
|
||||
if atEnd {
|
||||
if terminating {
|
||||
break
|
||||
}
|
||||
// One decision: 0 == stop here, otherwise continue to a successor.
|
||||
c := src.intn(len(liveCapped) + 1)
|
||||
if c == 0 {
|
||||
break
|
||||
}
|
||||
next = liveCapped[c-1]
|
||||
} else if terminating {
|
||||
next = g.nearestToEnd(liveAll)
|
||||
} else {
|
||||
next = liveCapped[src.intn(len(liveCapped))]
|
||||
}
|
||||
|
||||
visits[id+">"+tgNodeID(next)]++
|
||||
steps++
|
||||
node = next
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// nearestToEnd returns the candidate with the fewest steps to an endpoint.
|
||||
// Following it repeatedly walks a shortest path to an endpoint (distToEnd
|
||||
// strictly decreases), so the termination phase never repeats an edge.
|
||||
func (g *Generator) nearestToEnd(cands []*Node) *Node {
|
||||
best := cands[0]
|
||||
bestD := g.distToEnd[tgNodeID(best)]
|
||||
for _, c := range cands[1:] {
|
||||
if d := g.distToEnd[tgNodeID(c)]; d < bestD {
|
||||
best, bestD = c, d
|
||||
}
|
||||
}
|
||||
return best
|
||||
}
|
||||
|
||||
// From here, generate *negative* test inputs: strings that lie just outside the
|
||||
// grammar, each paired with a label describing the violation. They are the
|
||||
// complement of Generate's valid productions -- a correct target MUST reject them
|
||||
// -- and they reach the error-handling paths (truncation, boundary overruns,
|
||||
// trailing input) where many parser bugs live and which a valid-only generator
|
||||
// can never exercise.
|
||||
//
|
||||
// A perturbation is NOT assumed invalid: grammars can be ambiguous or redundant
|
||||
// (flipping "a" to "b" in `"a" / "b"` is still valid), so every candidate is
|
||||
// checked against the grammar and only genuinely-rejected ones are returned.
|
||||
// This makes the "target must reject" oracle sound.
|
||||
|
||||
// NearMiss returns a string just outside the grammar together with a label
|
||||
// describing how it violates the grammar, or ok=false if no minimal
|
||||
// perturbation within the attempt budget produced a string the grammar rejects.
|
||||
// It is deterministic in tape and total.
|
||||
func (g *Generator) NearMiss(tape []byte) (production []byte, label string, ok bool) {
|
||||
src := &tapeSource{tape: tape}
|
||||
var trace []traceSeg
|
||||
base := g.walk(src, &trace)
|
||||
|
||||
for _, c := range g.nearMissCandidates(src, base, trace) {
|
||||
if c.bytes == nil || string(c.bytes) == string(base) {
|
||||
continue
|
||||
}
|
||||
valid, err := g.tg.grammar.IsValid(g.tg.rulename, c.bytes)
|
||||
if err == nil && !valid {
|
||||
return c.bytes, c.label, true
|
||||
}
|
||||
}
|
||||
return nil, "", false
|
||||
}
|
||||
|
||||
// NearMissRand is the *rand.Rand-driven variant of NearMiss.
|
||||
func (g *Generator) NearMissRand(r *rand.Rand) (production []byte, label string, ok bool) {
|
||||
// Draw a tape and reuse the deterministic path so behaviour is identical.
|
||||
tape := make([]byte, 64)
|
||||
r.Read(tape)
|
||||
return g.NearMiss(tape)
|
||||
}
|
||||
|
||||
type candidate struct {
|
||||
bytes []byte
|
||||
label string
|
||||
}
|
||||
|
||||
func cloneBytes(b []byte) []byte {
|
||||
c := make([]byte, len(b))
|
||||
copy(c, b)
|
||||
return c
|
||||
}
|
||||
|
||||
// nearMissCandidates proposes minimal perturbations of base, ordered with a
|
||||
// little entropy from src so different tapes explore different operators first.
|
||||
// Correctness does not depend on the order: NearMiss verifies each against the
|
||||
// grammar.
|
||||
func (g *Generator) nearMissCandidates(src source, base []byte, trace []traceSeg) []candidate {
|
||||
var cs []candidate
|
||||
|
||||
// (1) Boundary violations: take a num-val range segment one step past its
|
||||
// edge. These are the high-value off-by-one cases.
|
||||
for _, seg := range trace {
|
||||
nv, isRange := numvalRange(seg.elem)
|
||||
if !isRange {
|
||||
continue
|
||||
}
|
||||
for _, b := range []struct {
|
||||
r rune
|
||||
what string
|
||||
}{
|
||||
{rune(nv.min - 1), "below"},
|
||||
{rune(nv.max + 1), "above"},
|
||||
} {
|
||||
if b.r < 0 || b.r > 0x10FFFF {
|
||||
continue
|
||||
}
|
||||
cand := cloneBytes(base)
|
||||
repl := []byte(string(b.r))
|
||||
out := append(append(cloneBytes(cand[:seg.off]), repl...), cand[seg.off+seg.length:]...)
|
||||
cs = append(cs, candidate{out, fmt.Sprintf("byte at offset %d is one %s num-val range %s", seg.off, b.what, nv.label)})
|
||||
}
|
||||
}
|
||||
|
||||
// (2) Off-alphabet substitution at a chosen position: replace one byte with
|
||||
// a "wild" value. Verified, so even an unlucky pick is safe.
|
||||
if len(base) > 0 {
|
||||
pos := src.intn(len(base))
|
||||
for _, nb := range []byte{0x00, 0xFF, base[pos] ^ 0x20, base[pos] + 1, '!'} {
|
||||
if nb == base[pos] {
|
||||
continue
|
||||
}
|
||||
cand := cloneBytes(base)
|
||||
cand[pos] = nb
|
||||
cs = append(cs, candidate{cand, fmt.Sprintf("byte at offset %d substituted with 0x%02x", pos, nb)})
|
||||
}
|
||||
}
|
||||
|
||||
// (3) Truncation: drop a suffix (incomplete input -- exercises overread /
|
||||
// "premature end" handling).
|
||||
for _, n := range []int{1, 2, len(base) / 2, len(base) - 1} {
|
||||
if n <= 0 || n > len(base) {
|
||||
continue
|
||||
}
|
||||
cs = append(cs, candidate{cloneBytes(base[:len(base)-n]), fmt.Sprintf("truncated by %d byte(s) (len %d)", n, len(base)-n)})
|
||||
}
|
||||
|
||||
// (4) Trailing input: append a byte (exercises "ignores trailing garbage").
|
||||
for _, nb := range []byte{0x00, '!', 0xFF, 'A'} {
|
||||
out := append(cloneBytes(base), nb)
|
||||
cs = append(cs, candidate{out, fmt.Sprintf("trailing byte 0x%02x appended", nb)})
|
||||
}
|
||||
|
||||
// (5) Deletion of a single interior byte.
|
||||
if len(base) >= 2 {
|
||||
pos := src.intn(len(base))
|
||||
out := append(cloneBytes(base[:pos]), base[pos+1:]...)
|
||||
cs = append(cs, candidate{out, fmt.Sprintf("byte at offset %d deleted", pos)})
|
||||
}
|
||||
|
||||
// Light entropy-driven rotation so the first verified hit varies by tape.
|
||||
if len(cs) > 1 {
|
||||
k := src.intn(len(cs))
|
||||
cs = append(cs[k:], cs[:k]...)
|
||||
}
|
||||
return cs
|
||||
}
|
||||
|
||||
type numvalInfo struct {
|
||||
min, max int32
|
||||
label string
|
||||
}
|
||||
|
||||
// numvalRange reports whether elem is a num-val range and, if so, its bounds.
|
||||
func numvalRange(elem ElemItf) (numvalInfo, bool) {
|
||||
nv, ok := elem.(ElemNumVal)
|
||||
if !ok || nv.Status != StatRange {
|
||||
return numvalInfo{}, false
|
||||
}
|
||||
min, max := numvalToInt32(nv.Elems[0], nv.Base), numvalToInt32(nv.Elems[1], nv.Base)
|
||||
// Clamp the upper bound to the Unicode range so the "just above" boundary
|
||||
// probe (max+1) stays a meaningful rune rather than overflowing.
|
||||
if max > utf8.MaxRune {
|
||||
max = utf8.MaxRune
|
||||
}
|
||||
return numvalInfo{min: min, max: max, label: elem.String()}, true
|
||||
}
|
||||
|
||||
// From here answers for a given prefix "which octets may legally come next, and
|
||||
// is the prefix itself a complete word?" by simulating the transition-graph
|
||||
// automaton over the prefix and reading the frontier. The graph is not a plain
|
||||
// byte DFA -- each node emits a multi-octet string with per-position choices
|
||||
// (letter casings, num-val ranges) -- so this is an NFA-style frontier simulation,
|
||||
// not a single-state walk.
|
||||
//
|
||||
// Its headline use is sharpening NearMiss: appending an octet that is NOT in the
|
||||
// expected-next set yields a string that is provably not a prefix of any word in
|
||||
// the language, i.e. guaranteed invalid, with a precise label. It also enables
|
||||
// error-position testing ("the target must fail at byte N").
|
||||
|
||||
// octetSet is a 256-bit set of octets.
|
||||
type octetSet [4]uint64
|
||||
|
||||
func (s *octetSet) add(b byte) {
|
||||
s[b>>6] |= 1 << (b & 63)
|
||||
}
|
||||
|
||||
func (s octetSet) has(b byte) bool {
|
||||
return s[b>>6]&(1<<(b&63)) != 0
|
||||
}
|
||||
|
||||
func (s octetSet) union(o octetSet) octetSet {
|
||||
return octetSet{s[0] | o[0], s[1] | o[1], s[2] | o[2], s[3] | o[3]}
|
||||
}
|
||||
|
||||
func (s octetSet) bytes() []byte {
|
||||
var out []byte
|
||||
for b := range 256 {
|
||||
if s.has(byte(b)) {
|
||||
out = append(out, byte(b))
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// emission is one fixed-length alternative of a node's output: pos[j] is the set
|
||||
// of octets the node may emit at byte offset j. A node usually has exactly one
|
||||
// emission; a num-val range that spans several UTF-8 lengths has one per length.
|
||||
type emission struct {
|
||||
pos []octetSet
|
||||
}
|
||||
|
||||
// expectedNextRangeCap bounds how many runes a multi-byte num-val range is
|
||||
// expanded into for analysis; beyond it ExpectedNext reports ok=false rather
|
||||
// than doing unbounded work. ASCII ranges are always handled (<=128 octets).
|
||||
const expectedNextRangeCap = 1 << 16
|
||||
|
||||
func (g *Generator) emissions(n *Node) ([]emission, bool) {
|
||||
id := tgNodeID(n)
|
||||
if g.emitCache == nil {
|
||||
g.emitCache = map[string][]emission{}
|
||||
g.emitOK = map[string]bool{}
|
||||
}
|
||||
if e, seen := g.emitCache[id]; seen {
|
||||
return e, g.emitOK[id]
|
||||
}
|
||||
e, ok := nodeEmissions(n)
|
||||
g.emitCache[id] = e
|
||||
g.emitOK[id] = ok
|
||||
return e, ok
|
||||
}
|
||||
|
||||
func nodeEmissions(n *Node) ([]emission, bool) {
|
||||
switch v := n.Elem.(type) {
|
||||
case ElemCharVal:
|
||||
e := emission{}
|
||||
for _, r := range v.Values {
|
||||
if !v.Sensitive && ((r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z')) {
|
||||
lo, up := r, r
|
||||
if lo >= 'A' && lo <= 'Z' {
|
||||
lo = lo - 'A' + 'a'
|
||||
}
|
||||
if up >= 'a' && up <= 'z' {
|
||||
up = up - 'a' + 'A'
|
||||
}
|
||||
var s octetSet
|
||||
s.add(byte(lo))
|
||||
s.add(byte(up))
|
||||
e.pos = append(e.pos, s)
|
||||
} else {
|
||||
for _, b := range []byte(string(r)) {
|
||||
var s octetSet
|
||||
s.add(b)
|
||||
e.pos = append(e.pos, s)
|
||||
}
|
||||
}
|
||||
}
|
||||
return []emission{e}, true
|
||||
|
||||
case ElemNumVal:
|
||||
switch v.Status {
|
||||
case StatSeries:
|
||||
e := emission{}
|
||||
for _, el := range v.Elems {
|
||||
r := numvalToRune(el, v.Base)
|
||||
if !utf8.ValidRune(r) {
|
||||
// An out-of-range element can never appear in UTF-8 input.
|
||||
return nil, false
|
||||
}
|
||||
for _, b := range []byte(string(r)) {
|
||||
var s octetSet
|
||||
s.add(b)
|
||||
e.pos = append(e.pos, s)
|
||||
}
|
||||
}
|
||||
return []emission{e}, true
|
||||
case StatRange:
|
||||
min, max := numvalToInt32(v.Elems[0], v.Base), numvalToInt32(v.Elems[1], v.Base)
|
||||
// Clamp to the Unicode range first: otherwise an out-of-range bound
|
||||
// near MaxInt32 overflows the int32 count below, bypasses the cap
|
||||
// guard, and turns the enumeration loop into a DoS. A range wholly
|
||||
// above U+10FFFF emits nothing.
|
||||
if max > utf8.MaxRune {
|
||||
max = utf8.MaxRune
|
||||
}
|
||||
if min > utf8.MaxRune || min > max {
|
||||
return nil, false
|
||||
}
|
||||
if max <= 0x7f { // ASCII: a single one-octet emission
|
||||
var s octetSet
|
||||
for b := min; b <= max && b <= 0x7f; b++ { // explicit ASCII bound for the conversion
|
||||
s.add(byte(b))
|
||||
}
|
||||
return []emission{{pos: []octetSet{s}}}, true
|
||||
}
|
||||
if max-min+1 > expectedNextRangeCap {
|
||||
return nil, false
|
||||
}
|
||||
byLen := map[int]*emission{}
|
||||
for r := min; r <= max; r++ {
|
||||
bs := []byte(string(rune(r)))
|
||||
e := byLen[len(bs)]
|
||||
if e == nil {
|
||||
e = &emission{pos: make([]octetSet, len(bs))}
|
||||
byLen[len(bs)] = e
|
||||
}
|
||||
for j, b := range bs {
|
||||
e.pos[j].add(b)
|
||||
}
|
||||
}
|
||||
out := make([]emission, 0, len(byLen))
|
||||
for _, e := range byLen {
|
||||
out = append(out, *e)
|
||||
}
|
||||
return out, true
|
||||
}
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// ExpectedNext simulates the automaton over prefix and reports the octets that
|
||||
// may legally follow it, whether prefix is itself a complete word, and whether
|
||||
// the analysis succeeded (ok is false only when the graph uses a feature this
|
||||
// analyzer declines to expand, e.g. an enormous multi-byte num-val range). When
|
||||
// prefix is not a viable prefix of any word, next is empty and accepts is false.
|
||||
func (g *Generator) ExpectedNext(prefix []byte) (next []byte, accepts bool, ok bool) {
|
||||
type state struct {
|
||||
n *Node
|
||||
e, p int
|
||||
}
|
||||
ready := []state{}
|
||||
seen := map[state]bool{}
|
||||
push := func(s state) {
|
||||
if !seen[s] {
|
||||
seen[s] = true
|
||||
ready = append(ready, s)
|
||||
}
|
||||
}
|
||||
start := func(n *Node) bool {
|
||||
ems, o := g.emissions(n)
|
||||
if !o {
|
||||
return false
|
||||
}
|
||||
for ei, em := range ems {
|
||||
if len(em.pos) > 0 {
|
||||
push(state{n, ei, 0})
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
for _, e := range g.tg.Entrypoints {
|
||||
if e == emptyNode {
|
||||
continue
|
||||
}
|
||||
if !start(e) {
|
||||
return nil, false, false
|
||||
}
|
||||
}
|
||||
|
||||
lastCompletedEndpoint := false
|
||||
consumedAll := true
|
||||
for idx, b := range prefix {
|
||||
next2 := []state{}
|
||||
seen2 := map[state]bool{}
|
||||
push2 := func(s state) {
|
||||
if !seen2[s] {
|
||||
seen2[s] = true
|
||||
next2 = append(next2, s)
|
||||
}
|
||||
}
|
||||
lastCompletedEndpoint = false
|
||||
for _, s := range ready {
|
||||
ems, _ := g.emissions(s.n)
|
||||
em := ems[s.e]
|
||||
if !em.pos[s.p].has(b) {
|
||||
continue
|
||||
}
|
||||
if s.p+1 == len(em.pos) { // node s.n completes on this octet
|
||||
if g.endSet[tgNodeID(s.n)] {
|
||||
lastCompletedEndpoint = true
|
||||
}
|
||||
for _, m := range s.n.Nexts {
|
||||
if m == emptyNode {
|
||||
continue
|
||||
}
|
||||
ems2, o := g.emissions(m)
|
||||
if !o {
|
||||
return nil, false, false
|
||||
}
|
||||
for ei, em2 := range ems2 {
|
||||
if len(em2.pos) > 0 {
|
||||
push2(state{m, ei, 0})
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
push2(state{s.n, s.e, s.p + 1})
|
||||
}
|
||||
}
|
||||
ready, seen = next2, seen2
|
||||
if len(ready) == 0 {
|
||||
if idx != len(prefix)-1 {
|
||||
consumedAll = false // dead before the end: prefix not viable
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !consumedAll {
|
||||
return nil, false, true
|
||||
}
|
||||
if len(prefix) == 0 {
|
||||
accepts = g.endSet[""] // empty word accepted iff emptyNode is an endpoint
|
||||
} else {
|
||||
accepts = lastCompletedEndpoint
|
||||
}
|
||||
var acc octetSet
|
||||
for _, s := range ready {
|
||||
ems, _ := g.emissions(s.n)
|
||||
acc = acc.union(ems[s.e].pos[s.p])
|
||||
}
|
||||
return acc.bytes(), accepts, true
|
||||
}
|
||||
+253
@@ -0,0 +1,253 @@
|
||||
package goabnf
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"maps"
|
||||
"math/rand"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// Generate is an experimental feature that consumes a seed for
|
||||
// a pseudo-random number generator, used to randomly travel through
|
||||
// the grammar given a rulename to work on.
|
||||
// With this, it provides reproducibility, usefull for fuzz crashers.
|
||||
//
|
||||
// You can leverage its capacities (maximum repetitions, length
|
||||
// threshold, etc.) with the functional options.
|
||||
//
|
||||
// It is a good capability for testing and fuzzing parsers during
|
||||
// testing, compliance, fuzzing or optimization.
|
||||
func (g *Grammar) Generate(seed int64, rulename string, opts ...GenerateOption) ([]byte, error) {
|
||||
if err := checkCanGenerateSafely(g, rulename); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Use a pseudo-random number generator
|
||||
rand := rand.NewSource(seed)
|
||||
|
||||
// Define conditions from options
|
||||
options := &genOpts{
|
||||
repMax: 16,
|
||||
threshold: 256,
|
||||
}
|
||||
for _, opt := range opts {
|
||||
opt.apply(options)
|
||||
}
|
||||
|
||||
// Generate actual content (rule exists, checked first)
|
||||
rule := GetRule(rulename, g.Rulemap)
|
||||
out := []byte{}
|
||||
generateAlt(rand, g, &out, rule.Alternation, options)
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func generateAlt(rand rand.Source, g *Grammar, out *[]byte, alt Alternation, options *genOpts) {
|
||||
// Select any possible path in the alternation
|
||||
cnt := alt.Concatenations[int(rand.Int63())%len(alt.Concatenations)]
|
||||
|
||||
// Travel through all the repetitions
|
||||
for _, rep := range cnt.Repetitions {
|
||||
repmax := rep.Max
|
||||
if repmax == inf {
|
||||
repmax = options.repMax
|
||||
}
|
||||
torep := rep.Min + int(rand.Int63())%(repmax-rep.Min+1)
|
||||
for i := 0; generateKeepGoing(i, torep, rep.Min, len(*out), options.threshold); i++ {
|
||||
switch elem := rep.Element.(type) {
|
||||
case ElemRulename:
|
||||
rule := GetRule(elem.Name, g.Rulemap)
|
||||
generateAlt(rand, g, out, rule.Alternation, options)
|
||||
|
||||
case ElemOption:
|
||||
if (rand.Int63() % 2) == 0 {
|
||||
generateAlt(rand, g, out, elem.Alternation, options)
|
||||
}
|
||||
|
||||
case ElemGroup:
|
||||
generateAlt(rand, g, out, elem.Alternation, options)
|
||||
|
||||
case ElemNumVal:
|
||||
switch elem.Status {
|
||||
case StatRange:
|
||||
min, max := numvalToRune(elem.Elems[0], elem.Base), numvalToRune(elem.Elems[1], elem.Base)
|
||||
// Generation emits UTF-8 text, so clamp to the representable
|
||||
// (Unicode) subset; a range wholly above U+10FFFF (or with
|
||||
// max < min) yields no character.
|
||||
if max > utf8.MaxRune {
|
||||
max = utf8.MaxRune
|
||||
}
|
||||
if min <= utf8.MaxRune && min <= max {
|
||||
r := min + (rune(rand.Int63()&0x0F) % (max - min + 1)) // mask 0x0F to keep the 32-bit part (rune size)
|
||||
appendPtr(out, r)
|
||||
}
|
||||
|
||||
case StatSeries:
|
||||
for _, v := range elem.Elems {
|
||||
r := numvalToRune(v, elem.Base)
|
||||
// Skip values that are not real characters; they cannot be
|
||||
// emitted as UTF-8 text.
|
||||
if utf8.ValidRune(r) {
|
||||
appendPtr(out, r)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
case ElemCharVal:
|
||||
for _, val := range elem.Values {
|
||||
if !elem.Sensitive && (int(rand.Int63())%2) == 0 {
|
||||
val = runeMax(val)
|
||||
}
|
||||
appendPtr(out, val)
|
||||
}
|
||||
}
|
||||
|
||||
// Prose-val is not covered, checked before to avoid generation
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func appendPtr(slc *[]byte, v ...rune) {
|
||||
b := *slc
|
||||
b = append(b, []byte(string(v))...)
|
||||
*slc = b
|
||||
}
|
||||
|
||||
func generateKeepGoing(i, torep, minrep, lenout, threshold int) bool {
|
||||
// If can do no run and reached threshold, skip
|
||||
if i == 0 && minrep == 0 && lenout >= threshold {
|
||||
return false
|
||||
}
|
||||
return i < torep
|
||||
}
|
||||
|
||||
// GenerateOption is an option for the *Grammar.Generate method.
|
||||
type GenerateOption interface {
|
||||
apply(*genOpts)
|
||||
}
|
||||
|
||||
type genOpts struct {
|
||||
repMax int
|
||||
threshold int
|
||||
}
|
||||
|
||||
type repMaxOption int
|
||||
|
||||
// WithRepMax defines the maximum repetition to stop generating at.
|
||||
func WithRepMax(repMax int) repMaxOption {
|
||||
return repMaxOption(repMax)
|
||||
}
|
||||
|
||||
func (opt repMaxOption) apply(opts *genOpts) {
|
||||
opts.repMax = int(opt)
|
||||
}
|
||||
|
||||
type thresholdOption int
|
||||
|
||||
// WithThreshold defines the length threshold to stop generating at.
|
||||
func WithThreshold(threshold int) thresholdOption {
|
||||
return thresholdOption(threshold)
|
||||
}
|
||||
|
||||
func (opt thresholdOption) apply(opts *genOpts) {
|
||||
opts.threshold = int(opt)
|
||||
}
|
||||
|
||||
// checkCanGenerateSafely returns no error if the rule can be generated
|
||||
// safely i.e. if the rule can exist without infinite recursion, AND if it
|
||||
// does not contain a prose-val as it could not generate from it.
|
||||
// Factually, it checks if all involved rules have no path v such that it
|
||||
// produces a cycle (v:rule-*->rule) AND that this path is mandatory
|
||||
// (no option, no repetition with a minimum of zero).
|
||||
func checkCanGenerateSafely(g *Grammar, rulename string) error {
|
||||
rule := GetRule(rulename, g.Rulemap)
|
||||
if rule == nil {
|
||||
return &ErrRuleNotFound{
|
||||
Rulename: rulename,
|
||||
}
|
||||
}
|
||||
knownRules := map[string]struct{}{
|
||||
rulename: {},
|
||||
}
|
||||
if err := checkCanGenerateSafelyAlt(g, knownRules, rule.Alternation); err != nil {
|
||||
if err, ok := err.(*ErrCyclicRule); ok {
|
||||
return err
|
||||
}
|
||||
return &ErrCyclicRule{
|
||||
Rulename: rulename,
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkCanGenerateSafelyAlt(g *Grammar, knownRules map[string]struct{}, alt Alternation) error {
|
||||
errs := make([]error, len(alt.Concatenations))
|
||||
for alti, concat := range alt.Concatenations {
|
||||
errs[alti] = checkCanGenerateSafelyConcat(g, knownRules, concat)
|
||||
}
|
||||
allErrors := true
|
||||
for i := 0; i < len(errs) && allErrors; i++ {
|
||||
if errs[i] == nil {
|
||||
allErrors = false
|
||||
}
|
||||
}
|
||||
if allErrors {
|
||||
return errors.Join(errs...)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkCanGenerateSafelyConcat(g *Grammar, knownRules map[string]struct{}, concat Concatenation) error {
|
||||
for _, rep := range concat.Repetitions {
|
||||
// If the repetition is not mandatory, we can escape so can
|
||||
// generate safely.
|
||||
if rep.Min == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
// Deal with the repetition itself then.
|
||||
switch elem := rep.Element.(type) {
|
||||
case ElemRulename:
|
||||
// Copy rules to only focus on rules that made use come here.
|
||||
// If shared with others, the dependency graph can lead to the same rule
|
||||
// from another path without it being a cycle, thus must be handled.
|
||||
scopeRules := duplicateMap(knownRules)
|
||||
for known := range scopeRules {
|
||||
if strings.EqualFold(elem.Name, known) {
|
||||
return &ErrCyclicRule{
|
||||
Rulename: elem.Name,
|
||||
}
|
||||
}
|
||||
}
|
||||
rule := GetRule(elem.Name, g.Rulemap)
|
||||
scopeRules[elem.Name] = struct{}{}
|
||||
if err := checkCanGenerateSafelyAlt(g, scopeRules, rule.Alternation); err != nil {
|
||||
if err, ok := err.(*ErrCyclicRule); ok {
|
||||
return err
|
||||
}
|
||||
return &ErrCyclicRule{
|
||||
Rulename: elem.Name,
|
||||
}
|
||||
}
|
||||
|
||||
case ElemGroup:
|
||||
if err := checkCanGenerateSafelyAlt(g, knownRules, elem.Alternation); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
case ElemProseVal:
|
||||
return ErrHandlingProseVal
|
||||
|
||||
// Other types are not considered for the following reasons:
|
||||
// - option: equivalent to rep.min==0, escapable path even if could be cyclic
|
||||
// - num-val, char-val: termination paths, can't be cyclic
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func duplicateMap[T comparable, V any](m map[T]V) map[T]V {
|
||||
n := make(map[T]V, len(m))
|
||||
maps.Copy(n, m)
|
||||
return n
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
go 1.26.0
|
||||
|
||||
use (
|
||||
.
|
||||
cmd/pap
|
||||
)
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
|
||||
github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
|
||||
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0=
|
||||
golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc=
|
||||
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
+443
@@ -0,0 +1,443 @@
|
||||
package goabnf
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Grammar represents an ABNF grammar as defined by RFC 5234.
|
||||
// It is constituted of a set of rules with a unique name.
|
||||
type Grammar struct {
|
||||
Rulemap map[string]*Rule
|
||||
}
|
||||
|
||||
// IsValid checks there exist at least a path that completly consumes
|
||||
// input, hence is valid given this grammar and especially one of its
|
||||
// rule.
|
||||
func (g *Grammar) IsValid(rulename string, input []byte) (bool, error) {
|
||||
rule := GetRule(rulename, g.Rulemap)
|
||||
if rule == nil {
|
||||
return false, &ErrRuleNotFound{Rulename: rulename}
|
||||
}
|
||||
// Validity only needs to know whether some derivation consumes the whole
|
||||
// input, never the (possibly exponentially many) derivations themselves.
|
||||
// We therefore propagate the set of reachable end-positions per
|
||||
// (element, index) instead of enumerating paths, which keeps this
|
||||
// polynomial. Left-recursive rules are resolved by seed-growing rather than
|
||||
// refused; see recognize.go and leftrec.go.
|
||||
r := &recognizer{
|
||||
g: g,
|
||||
input: input,
|
||||
memo: map[string]map[int]bool{},
|
||||
inProgress: map[string]bool{},
|
||||
leftRec: g.leftRecursiveSCCs(),
|
||||
growing: map[string]map[int]bool{},
|
||||
}
|
||||
ends := r.reachElem(ElemRulename{Name: rulename}, 0)
|
||||
return ends[len(input)], nil
|
||||
}
|
||||
|
||||
// String returns the representation of the grammar that is valid
|
||||
// according to the ABNF specifications/RFCs.
|
||||
// This notably imply the use of CRLF instead of LF, and does not
|
||||
// preserve the initial order nor pretty print it.
|
||||
func (g *Grammar) String() string {
|
||||
str := ""
|
||||
for _, rule := range g.Rulemap {
|
||||
str += rule.String() + "\r\n"
|
||||
}
|
||||
return str
|
||||
}
|
||||
|
||||
// PrettyPrint returns a prettified string that represents the grammar.
|
||||
func (g *Grammar) PrettyPrint() string {
|
||||
// Determine maximum rulename length
|
||||
rulenameLength := 0
|
||||
for rulename := range g.Rulemap {
|
||||
if len(rulename) > rulenameLength {
|
||||
rulenameLength = len(rulename)
|
||||
}
|
||||
}
|
||||
|
||||
// Construct output
|
||||
out := ""
|
||||
for rulename, rl := range g.Rulemap {
|
||||
spaces := ""
|
||||
for i := 0; i < rulenameLength-len(rulename); i++ {
|
||||
spaces += " "
|
||||
}
|
||||
|
||||
out += fmt.Sprintf("%s%s = %s\r\n", rulename, spaces, rl.Alternation)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// ParseABNF is a helper facilitating the call to Parse using the
|
||||
// pre-computed ABNF grammar and evaluated the resulting to produce
|
||||
// a ready-to-use grammar.
|
||||
func ParseABNF(input []byte, opts ...ABNFOption) (*Grammar, error) {
|
||||
o := process(opts...)
|
||||
|
||||
// Parse the ABNF source using the ABNF meta-grammar with the GLL engine.
|
||||
f, err := Parse(input, ABNF, "rulelist")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !f.Valid() {
|
||||
return nil, f.ParseError()
|
||||
}
|
||||
if f.Ambiguous() {
|
||||
return nil, &ErrMultipleSolutionsFound{}
|
||||
}
|
||||
|
||||
g, err := evalForest(input, f, o)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if o.validate {
|
||||
if err := SemvalABNF(g); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return g, nil
|
||||
}
|
||||
|
||||
// Parse parses input against grammar starting at rootRulename using the GLL
|
||||
// engine, returning the shared packed parse forest. Unlike the previous
|
||||
// backtracking parser it accepts ANY grammar -- left, right and mutual
|
||||
// recursion and ambiguity included -- in worst-case cubic time, so it never
|
||||
// stack-overflows on left recursion nor blows up exponentially. Inspect the
|
||||
// result with the Forest methods (Valid, Ambiguous, NumTrees, Tree).
|
||||
func Parse(input []byte, grammar *Grammar, rootRulename string) (*Forest, error) {
|
||||
return ParseForest(input, grammar, rootRulename)
|
||||
}
|
||||
|
||||
// EvaluateABNF evaluates a parse forest -- produced by parsing an ABNF source
|
||||
// against the ABNF meta-grammar -- into a ready-to-use *Grammar.
|
||||
func EvaluateABNF(input []byte, f *Forest, opts ...ABNFOption) (*Grammar, error) {
|
||||
return evalForest(input, f, process(opts...))
|
||||
}
|
||||
|
||||
func evalForest(input []byte, f *Forest, o *abnfOptions) (*Grammar, error) {
|
||||
tree := f.Tree()
|
||||
if tree == nil {
|
||||
return nil, ErrNoSolutionFound
|
||||
}
|
||||
ev := &feval{input: input, o: o}
|
||||
return ev.rulelist(tree)
|
||||
}
|
||||
|
||||
// feval evaluates the rule-keyed ParseTree of an ABNF source into a *Grammar.
|
||||
// It dispatches on rule names and reads byte spans, so it is decoupled from the
|
||||
// exact (scaffolding-collapsed) shape of the tree.
|
||||
type feval struct {
|
||||
input []byte
|
||||
o *abnfOptions
|
||||
}
|
||||
|
||||
func ptChildren(t *ParseTree, name string) []*ParseTree {
|
||||
var out []*ParseTree
|
||||
for _, c := range t.Children {
|
||||
if c.Rule == name {
|
||||
out = append(out, c)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func ptFirst(t *ParseTree, names ...string) *ParseTree {
|
||||
for _, c := range t.Children {
|
||||
if slices.Contains(names, c.Rule) {
|
||||
return c
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *feval) span(t *ParseTree) string { return string(e.input[t.Start:t.End]) }
|
||||
|
||||
func (e *feval) rulelist(t *ParseTree) (*Grammar, error) {
|
||||
mp := map[string]*Rule{}
|
||||
for _, rc := range ptChildren(t, "rule") {
|
||||
rl, definedAs, err := e.rule(rc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
switch definedAs {
|
||||
case "=":
|
||||
isCoreRule := GetRule(rl.Name, nil) != nil
|
||||
if isCoreRule {
|
||||
if !e.o.redefineCore {
|
||||
return nil, &ErrCoreRuleModify{CoreRulename: rl.Name}
|
||||
}
|
||||
} else if GetRule(rl.Name, mp) != nil {
|
||||
return nil, &ErrDuplicatedRule{Rulename: rl.Name}
|
||||
}
|
||||
mp[rl.Name] = rl
|
||||
case "=/":
|
||||
isCoreRule := GetRule(rl.Name, nil) != nil
|
||||
if !e.o.redefineCore && isCoreRule {
|
||||
return nil, &ErrCoreRuleModify{CoreRulename: rl.Name}
|
||||
}
|
||||
rule := GetRule(rl.Name, mp)
|
||||
if rule == nil {
|
||||
return nil, &ErrRuleNotFound{Rulename: rl.Name}
|
||||
}
|
||||
rule.Alternation.Concatenations = append(rule.Alternation.Concatenations, rl.Alternation.Concatenations...)
|
||||
mp[rule.Name] = rule
|
||||
}
|
||||
}
|
||||
return &Grammar{Rulemap: mp}, nil
|
||||
}
|
||||
|
||||
func (e *feval) rule(t *ParseTree) (*Rule, string, error) {
|
||||
nameNode := ptFirst(t, "rulename")
|
||||
defNode := ptFirst(t, "defined-as")
|
||||
elemsNode := ptFirst(t, "elements")
|
||||
if nameNode == nil || defNode == nil || elemsNode == nil {
|
||||
return nil, "", ErrNoSolutionFound
|
||||
}
|
||||
definedAs := "="
|
||||
if strings.Contains(e.span(defNode), "=/") {
|
||||
definedAs = "=/"
|
||||
}
|
||||
altNode := ptFirst(elemsNode, "alternation")
|
||||
if altNode == nil {
|
||||
return nil, "", ErrNoSolutionFound
|
||||
}
|
||||
alt, err := e.alternation(altNode)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
return &Rule{Name: e.span(nameNode), Alternation: alt}, definedAs, nil
|
||||
}
|
||||
|
||||
func (e *feval) alternation(t *ParseTree) (Alternation, error) {
|
||||
var cs []Concatenation
|
||||
for _, cc := range ptChildren(t, "concatenation") {
|
||||
c, err := e.concatenation(cc)
|
||||
if err != nil {
|
||||
return Alternation{}, err
|
||||
}
|
||||
cs = append(cs, c)
|
||||
}
|
||||
return Alternation{Concatenations: cs}, nil
|
||||
}
|
||||
|
||||
func (e *feval) concatenation(t *ParseTree) (Concatenation, error) {
|
||||
var rs []Repetition
|
||||
for _, rc := range ptChildren(t, "repetition") {
|
||||
r, err := e.repetition(rc)
|
||||
if err != nil {
|
||||
return Concatenation{}, err
|
||||
}
|
||||
rs = append(rs, r)
|
||||
}
|
||||
return Concatenation{Repetitions: rs}, nil
|
||||
}
|
||||
|
||||
func (e *feval) repetition(t *ParseTree) (Repetition, error) {
|
||||
min, max := 1, 1
|
||||
if rep := ptFirst(t, "repeat"); rep != nil {
|
||||
min, max = e.parseRepeat(rep)
|
||||
}
|
||||
elNode := ptFirst(t, "element")
|
||||
if elNode == nil {
|
||||
return Repetition{}, ErrNoSolutionFound
|
||||
}
|
||||
elem, err := e.element(elNode)
|
||||
if err != nil {
|
||||
return Repetition{}, err
|
||||
}
|
||||
return Repetition{Min: min, Max: max, Element: elem}, nil
|
||||
}
|
||||
|
||||
func (e *feval) parseRepeat(t *ParseTree) (int, int) {
|
||||
s := e.span(t)
|
||||
before, after, ok := strings.Cut(s, "*")
|
||||
if !ok {
|
||||
d, _ := strconv.Atoi(s)
|
||||
return d, d
|
||||
}
|
||||
min, max := 0, inf
|
||||
if pre := before; pre != "" {
|
||||
min, _ = strconv.Atoi(pre)
|
||||
}
|
||||
if post := after; post != "" {
|
||||
max, _ = strconv.Atoi(post)
|
||||
}
|
||||
return min, max
|
||||
}
|
||||
|
||||
func (e *feval) element(t *ParseTree) (ElemItf, error) {
|
||||
c := ptFirst(t, "rulename", "group", "option", "char-val", "num-val", "prose-val")
|
||||
if c == nil {
|
||||
return nil, ErrNoSolutionFound
|
||||
}
|
||||
switch c.Rule {
|
||||
case "rulename":
|
||||
return ElemRulename{Name: e.span(c)}, nil
|
||||
case "group":
|
||||
alt, err := e.alternation(ptFirst(c, "alternation"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ElemGroup{Alternation: alt}, nil
|
||||
case "option":
|
||||
alt, err := e.alternation(ptFirst(c, "alternation"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ElemOption{Alternation: alt}, nil
|
||||
case "char-val":
|
||||
return e.charVal(c), nil
|
||||
case "num-val":
|
||||
return e.numVal(c), nil
|
||||
case "prose-val":
|
||||
return e.proseVal(c), nil
|
||||
}
|
||||
return nil, ErrNoSolutionFound
|
||||
}
|
||||
|
||||
func (e *feval) charVal(t *ParseTree) ElemCharVal {
|
||||
s := e.span(t)
|
||||
sensitive := false
|
||||
if len(s) >= 2 && s[0] == '%' {
|
||||
if s[1] == 's' || s[1] == 'S' {
|
||||
sensitive = true
|
||||
}
|
||||
s = s[2:]
|
||||
}
|
||||
first := strings.IndexByte(s, '"')
|
||||
last := strings.LastIndexByte(s, '"')
|
||||
value := []rune{}
|
||||
if first >= 0 && last > first {
|
||||
value = []rune(s[first+1 : last])
|
||||
}
|
||||
return ElemCharVal{Sensitive: sensitive, Values: value}
|
||||
}
|
||||
|
||||
func (e *feval) numVal(t *ParseTree) ElemNumVal {
|
||||
s := e.span(t)
|
||||
base := "d"
|
||||
rest := ""
|
||||
if len(s) >= 2 && s[0] == '%' {
|
||||
switch s[1] {
|
||||
case 'b', 'B':
|
||||
base = "b"
|
||||
case 'd', 'D':
|
||||
base = "d"
|
||||
case 'x', 'X':
|
||||
base = "x"
|
||||
}
|
||||
rest = s[2:]
|
||||
}
|
||||
status := StatSeries
|
||||
var elems []string
|
||||
if strings.ContainsRune(rest, '-') {
|
||||
status = StatRange
|
||||
elems = strings.Split(rest, "-")
|
||||
} else {
|
||||
elems = strings.Split(rest, ".")
|
||||
}
|
||||
// A num-val outside the Unicode range is still a well-formed numeric
|
||||
// encoding (legitimate for non-textual / on-wire grammars), so it is kept
|
||||
// as-is here. Strict semantic validation (SemvalABNF) rejects it for textual
|
||||
// use; consumers that match against decoded runes treat an unrepresentable
|
||||
// value as non-matching rather than panicking.
|
||||
return ElemNumVal{Base: base, Status: status, Elems: elems}
|
||||
}
|
||||
|
||||
func (e *feval) proseVal(t *ParseTree) ElemProseVal {
|
||||
values := []string{}
|
||||
for i := t.Start + 1; i < t.End-1; i++ {
|
||||
values = append(values, string(e.input[i]))
|
||||
}
|
||||
return ElemProseVal{values: values}
|
||||
}
|
||||
|
||||
func sensequal(target, actual rune, sensitive bool) bool {
|
||||
if !sensitive {
|
||||
target, actual = runeMin(target), runeMin(actual)
|
||||
}
|
||||
return target == actual
|
||||
}
|
||||
|
||||
func runeMin(r rune) rune {
|
||||
if r >= 'A' && r <= 'Z' {
|
||||
return r - 'A' + 'a'
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
func runeMax(r rune) rune {
|
||||
if r >= 'a' && r <= 'z' {
|
||||
return r - 'a' + 'A'
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
// SemvalABNF proceed to semantic validations of an ABNF grammar.
|
||||
// It currently support the following checks:
|
||||
// - for all rules, its dependencies (rules) exist
|
||||
// - for repetition, min <= max
|
||||
// - for num-val, that the value fits in 7-bits (US-ASCII encoded)
|
||||
// To update this list, please open an issue.
|
||||
func SemvalABNF(g *Grammar) error {
|
||||
// Check all dependencies exist
|
||||
for _, rule := range g.Rulemap {
|
||||
deps := getDependencies(rule.Alternation)
|
||||
for _, dep := range deps {
|
||||
r := GetRule(dep, g.Rulemap)
|
||||
if r == nil {
|
||||
return &ErrDependencyNotFound{
|
||||
Rulename: dep,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, rule := range g.Rulemap {
|
||||
if err := semvalAlternation(rule.Alternation); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func semvalAlternation(alt Alternation) error {
|
||||
for _, concat := range alt.Concatenations {
|
||||
for _, rep := range concat.Repetitions {
|
||||
// min <= max
|
||||
if rep.Max != inf && rep.Min > rep.Max {
|
||||
return &ErrSemanticRepetition{
|
||||
Repetition: rep,
|
||||
}
|
||||
}
|
||||
switch elem := rep.Element.(type) {
|
||||
// num-val base
|
||||
case ElemNumVal:
|
||||
for _, val := range elem.Elems {
|
||||
if err := checkBounds(val, elem.Base); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// propagate recursion
|
||||
case ElemGroup:
|
||||
if err := semvalAlternation(elem.Alternation); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
case ElemOption:
|
||||
if err := semvalAlternation(elem.Alternation); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
+159
@@ -0,0 +1,159 @@
|
||||
package goabnf
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Identifies left-recursive rules so the recognizer can handle them by seed-growing
|
||||
// instead of refusing them. A rule is left-recursive when it can re-derive itself
|
||||
// without consuming input -- i.e. it appears on a cycle of the "left-corner"
|
||||
// relation: A left-corners B if B can begin a derivation of A at the same input
|
||||
// position. Crucially this skips over prefixes that can vanish (zero-min repetitions,
|
||||
// options, and nullable elements), so it also catches *hidden* left recursion such as
|
||||
// `a = b a / "x"` where `b` is nullable -- which a purely positional check misses.
|
||||
|
||||
// nullableRules computes, by least fixpoint over user and core rules, which rules can
|
||||
// derive the empty string.
|
||||
func (g *Grammar) nullableRules() map[string]bool {
|
||||
rules := map[string]*Rule{}
|
||||
for n := range g.Rulemap {
|
||||
if r := GetRule(n, g.Rulemap); r != nil {
|
||||
rules[strings.ToLower(r.Name)] = r
|
||||
}
|
||||
}
|
||||
for n := range coreRules {
|
||||
if r := GetRule(n, g.Rulemap); r != nil {
|
||||
rules[strings.ToLower(r.Name)] = r
|
||||
}
|
||||
}
|
||||
null := map[string]bool{}
|
||||
for changed := true; changed; {
|
||||
changed = false
|
||||
for name, r := range rules {
|
||||
if !null[name] && altNullable(g, null, r.Alternation) {
|
||||
null[name] = true
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
func altNullable(g *Grammar, null map[string]bool, alt Alternation) bool {
|
||||
for _, c := range alt.Concatenations {
|
||||
nullable := true
|
||||
for _, rep := range c.Repetitions {
|
||||
if !repNullable(g, null, rep) {
|
||||
nullable = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if nullable {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func repNullable(g *Grammar, null map[string]bool, rep Repetition) bool {
|
||||
if rep.Min == 0 {
|
||||
return true
|
||||
}
|
||||
return elemNullable(g, null, rep.Element)
|
||||
}
|
||||
|
||||
func elemNullable(g *Grammar, null map[string]bool, elem ElemItf) bool {
|
||||
switch v := elem.(type) {
|
||||
case ElemOption:
|
||||
return true
|
||||
case ElemGroup:
|
||||
return altNullable(g, null, v.Alternation)
|
||||
case ElemRulename:
|
||||
return null[strings.ToLower(v.Name)]
|
||||
case ElemCharVal:
|
||||
return len(v.Values) == 0
|
||||
}
|
||||
// num-val always consumes; prose-val is treated as unmatchable.
|
||||
return false
|
||||
}
|
||||
|
||||
// leftRecursiveSCCs returns, for every left-recursive rule (keyed by lowercase
|
||||
// name), the set of rule names in its left-corner strongly-connected component
|
||||
// (mutually left-recursive rules grown together). Non-left-recursive rules are
|
||||
// absent from the map.
|
||||
func (g *Grammar) leftRecursiveSCCs() map[string][]string {
|
||||
null := g.nullableRules()
|
||||
|
||||
// Build the left-corner dependency graph and reuse Tarjan's SCC finder.
|
||||
lc := Depgraph{}
|
||||
add := func(r *Rule) {
|
||||
name := strings.ToLower(r.Name)
|
||||
if _, ok := lc[name]; ok {
|
||||
return
|
||||
}
|
||||
lc[name] = &node{Rulename: r.Name, Dependencies: leftCornerDeps(g, null, r.Alternation)}
|
||||
}
|
||||
for n := range g.Rulemap {
|
||||
if r := GetRule(n, g.Rulemap); r != nil {
|
||||
add(r)
|
||||
}
|
||||
}
|
||||
for n := range coreRules {
|
||||
if r := GetRule(n, g.Rulemap); r != nil {
|
||||
add(r)
|
||||
}
|
||||
}
|
||||
|
||||
finder := &cycle{index: 0, stack: []*node{}, dg: lc}
|
||||
finder.find()
|
||||
|
||||
out := map[string][]string{}
|
||||
for _, scc := range finder.sccs {
|
||||
members := make([]string, len(scc))
|
||||
for i, nd := range scc {
|
||||
members[i] = strings.ToLower(nd.Rulename)
|
||||
}
|
||||
leftRecursive := len(scc) > 1
|
||||
if !leftRecursive { // size 1: left-recursive only with a self left-corner
|
||||
leftRecursive = slices.Contains(scc[0].Dependencies, members[0])
|
||||
}
|
||||
if leftRecursive {
|
||||
for _, m := range members {
|
||||
out[m] = members
|
||||
}
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// leftCornerDeps collects the rules reachable as a left corner of alt: walk each
|
||||
// concatenation left to right, collecting rulename references, and stop at the
|
||||
// first repetition that cannot vanish.
|
||||
func leftCornerDeps(g *Grammar, null map[string]bool, alt Alternation) []string {
|
||||
deps := []string{}
|
||||
var addAlt func(Alternation)
|
||||
addElem := func(e ElemItf) {
|
||||
switch v := e.(type) {
|
||||
case ElemRulename:
|
||||
deps = appendDeps(deps, strings.ToLower(v.Name))
|
||||
case ElemGroup:
|
||||
addAlt(v.Alternation)
|
||||
case ElemOption:
|
||||
addAlt(v.Alternation)
|
||||
}
|
||||
}
|
||||
addAlt = func(a Alternation) {
|
||||
for _, c := range a.Concatenations {
|
||||
for _, rep := range c.Repetitions {
|
||||
addElem(rep.Element)
|
||||
if rep.Min == 0 || elemNullable(g, null, rep.Element) {
|
||||
continue // this position can be empty; the next is also a left corner
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
addAlt(alt)
|
||||
return deps
|
||||
}
|
||||
+298
@@ -0,0 +1,298 @@
|
||||
package goabnf
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// One-symbol lookahead for the GLL engines.
|
||||
//
|
||||
// GLL as published guards parsing steps with a selection test test(x, A, γ):
|
||||
// a sequence γ in a production of A is only explored at input position i when
|
||||
// the next input symbol x is in FIRST(γ), or γ is nullable and x is in
|
||||
// FOLLOW(A) (Scott & Johnstone, "GLL parse-tree generation", Science of
|
||||
// Computer Programming 78(10), 2013, §3.3). It guards the descriptor started
|
||||
// for each alternate α of A (§4.3), and the call of each nonterminal instance
|
||||
// A ::= α·Xβ with γ = Xβ (§4.1), so no GSS node is created for a call that
|
||||
// cannot consume the next symbol.
|
||||
//
|
||||
// The guard prunes sub-parses whose continuation cannot consume the next input
|
||||
// symbol, such as a repetition 1*ALPHA stopping inside a run of letters. This
|
||||
// keeps the forest proportional to the derivations that can still complete,
|
||||
// e.g. linear in the run length for 1*ALPHA followed by a delimiter.
|
||||
//
|
||||
// A sequence rejected by the guard would have failed on a terminal at the same
|
||||
// position, so its FIRST (and, when nullable, FOLLOW) terminals feed the
|
||||
// furthest-failure diagnostics in its place.
|
||||
//
|
||||
// Symbols are runes (ABNF terminals are matched on decoded UTF-8), so sets are
|
||||
// rune intervals plus an end-of-input marker. FIRST/FOLLOW are computed on the
|
||||
// context-free reading of the slot grammar, ignoring repetition bounds: the
|
||||
// bounded language is a subset of it, so the sets over-approximate and the test
|
||||
// never rejects a viable sequence.
|
||||
|
||||
type runeRange struct{ lo, hi rune }
|
||||
|
||||
// laSet is a lookahead set: rune intervals, the end-of-input marker, and the
|
||||
// terminals contributing to it (for furthest-failure diagnostics).
|
||||
type laSet struct {
|
||||
ranges []runeRange // sorted, non-overlapping, non-adjacent
|
||||
ascii [2]uint64 // bitmap of ranges below utf8.RuneSelf
|
||||
eof bool
|
||||
terms []ssym
|
||||
descs map[string]bool
|
||||
}
|
||||
|
||||
func (s *laSet) addRange(lo, hi rune) bool {
|
||||
if lo > hi {
|
||||
return false
|
||||
}
|
||||
if s.coversRange(lo, hi) {
|
||||
return false
|
||||
}
|
||||
s.ranges = append(s.ranges, runeRange{lo, hi})
|
||||
sort.Slice(s.ranges, func(i, j int) bool { return s.ranges[i].lo < s.ranges[j].lo })
|
||||
merged := s.ranges[:1]
|
||||
for _, r := range s.ranges[1:] {
|
||||
last := &merged[len(merged)-1]
|
||||
if r.lo <= last.hi+1 {
|
||||
if r.hi > last.hi {
|
||||
last.hi = r.hi
|
||||
}
|
||||
continue
|
||||
}
|
||||
merged = append(merged, r)
|
||||
}
|
||||
s.ranges = merged
|
||||
return true
|
||||
}
|
||||
|
||||
func (s *laSet) coversRange(lo, hi rune) bool {
|
||||
for _, r := range s.ranges {
|
||||
if r.lo <= lo && hi <= r.hi {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (s *laSet) addTerm(t ssym) bool {
|
||||
desc := "?"
|
||||
if str, ok := t.term.(fmt.Stringer); ok {
|
||||
desc = str.String()
|
||||
}
|
||||
if s.descs == nil {
|
||||
s.descs = map[string]bool{}
|
||||
}
|
||||
if s.descs[desc] {
|
||||
return false
|
||||
}
|
||||
s.descs[desc] = true
|
||||
s.terms = append(s.terms, t)
|
||||
return true
|
||||
}
|
||||
|
||||
// union adds o to s (optionally including o's end-of-input marker) and
|
||||
// reports whether s changed.
|
||||
func (s *laSet) union(o *laSet, withEOF bool) bool {
|
||||
changed := false
|
||||
for _, r := range o.ranges {
|
||||
if s.addRange(r.lo, r.hi) {
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
if withEOF && o.eof && !s.eof {
|
||||
s.eof = true
|
||||
changed = true
|
||||
}
|
||||
for _, t := range o.terms {
|
||||
if s.addTerm(t) {
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
return changed
|
||||
}
|
||||
|
||||
func (s *laSet) seal() {
|
||||
for _, r := range s.ranges {
|
||||
for c := r.lo; c <= r.hi && c < utf8.RuneSelf; c++ {
|
||||
if c >= 0 {
|
||||
s.ascii[c/64] |= 1 << (c % 64)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *laSet) contains(c rune) bool {
|
||||
if c >= 0 && c < utf8.RuneSelf {
|
||||
return s.ascii[c/64]&(1<<(c%64)) != 0
|
||||
}
|
||||
i := sort.Search(len(s.ranges), func(i int) bool { return s.ranges[i].hi >= c })
|
||||
return i < len(s.ranges) && s.ranges[i].lo <= c
|
||||
}
|
||||
|
||||
// termFirst returns the FIRST set of a terminal and whether it matches the
|
||||
// empty string, mirroring matchTerm.
|
||||
func termFirst(t ssym) (laSet, bool) {
|
||||
var s laSet
|
||||
if t.kind == symEps {
|
||||
return s, true
|
||||
}
|
||||
switch v := t.term.(type) {
|
||||
case ElemCharVal:
|
||||
if len(v.Values) == 0 {
|
||||
return s, true
|
||||
}
|
||||
r := v.Values[0]
|
||||
if v.Sensitive {
|
||||
s.addRange(r, r)
|
||||
} else {
|
||||
lo := runeMin(r)
|
||||
s.addRange(lo, lo)
|
||||
up := runeMax(lo)
|
||||
s.addRange(up, up)
|
||||
}
|
||||
|
||||
case ElemNumVal:
|
||||
switch v.Status {
|
||||
case StatRange:
|
||||
s.addRange(numvalToRune(v.Elems[0], v.Base), numvalToRune(v.Elems[1], v.Base))
|
||||
|
||||
case StatSeries:
|
||||
if len(v.Elems) == 0 {
|
||||
return s, true
|
||||
}
|
||||
if r := numvalToRune(v.Elems[0], v.Base); utf8.ValidRune(r) {
|
||||
s.addRange(r, r)
|
||||
}
|
||||
}
|
||||
}
|
||||
// A terminal that never matches (prose-val, out-of-range series) has an
|
||||
// empty rune set but is still an attempted terminal for diagnostics.
|
||||
s.addTerm(t)
|
||||
return s, false
|
||||
}
|
||||
|
||||
// lookahead holds the selection sets FIRST(γ) ∪ (FOLLOW(A) if γ is nullable)
|
||||
// of the slot grammar: sel for every alternate γ = α of A, call for every
|
||||
// nonterminal instance A ::= α·Xβ with γ = Xβ.
|
||||
type lookahead struct {
|
||||
sel [][]laSet // [nt][alt]
|
||||
call [][][]laSet // [nt][alt][dot], set only where the symbol at dot is a nonterminal
|
||||
}
|
||||
|
||||
func computeLookahead(sg *slotGrammar) *lookahead {
|
||||
n := len(sg.nts)
|
||||
nullable := make([]bool, n)
|
||||
first := make([]laSet, n)
|
||||
follow := make([]laSet, n)
|
||||
|
||||
symNullable := func(s ssym) bool {
|
||||
switch s.kind {
|
||||
case symNonterm:
|
||||
return nullable[s.nt]
|
||||
|
||||
default:
|
||||
_, null := termFirst(s)
|
||||
return null
|
||||
}
|
||||
}
|
||||
// firstSeq returns FIRST(seq) and whether seq is nullable.
|
||||
firstSeq := func(seq []ssym) (laSet, bool) {
|
||||
var out laSet
|
||||
for _, s := range seq {
|
||||
if s.kind == symNonterm {
|
||||
out.union(&first[s.nt], false)
|
||||
} else {
|
||||
f, _ := termFirst(s)
|
||||
out.union(&f, false)
|
||||
}
|
||||
if !symNullable(s) {
|
||||
return out, false
|
||||
}
|
||||
}
|
||||
return out, true
|
||||
}
|
||||
|
||||
for changed := true; changed; {
|
||||
changed = false
|
||||
for a, nt := range sg.nts {
|
||||
for _, alt := range nt.alts {
|
||||
f, null := firstSeq(alt)
|
||||
if first[a].union(&f, false) {
|
||||
changed = true
|
||||
}
|
||||
if null && !nullable[a] {
|
||||
nullable[a] = true
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
follow[sg.start].eof = true
|
||||
for changed := true; changed; {
|
||||
changed = false
|
||||
for a, nt := range sg.nts {
|
||||
for _, alt := range nt.alts {
|
||||
for k, s := range alt {
|
||||
if s.kind != symNonterm {
|
||||
continue
|
||||
}
|
||||
f, null := firstSeq(alt[k+1:])
|
||||
if follow[s.nt].union(&f, false) {
|
||||
changed = true
|
||||
}
|
||||
if null && follow[s.nt].union(&follow[a], true) {
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
selection := func(a int, seq []ssym) laSet {
|
||||
f, null := firstSeq(seq)
|
||||
if null {
|
||||
f.union(&follow[a], true)
|
||||
}
|
||||
f.seal()
|
||||
return f
|
||||
}
|
||||
la := &lookahead{sel: make([][]laSet, n), call: make([][][]laSet, n)}
|
||||
for a, nt := range sg.nts {
|
||||
la.sel[a] = make([]laSet, len(nt.alts))
|
||||
la.call[a] = make([][]laSet, len(nt.alts))
|
||||
for ai, alt := range nt.alts {
|
||||
la.sel[a][ai] = selection(a, alt)
|
||||
la.call[a][ai] = make([]laSet, len(alt))
|
||||
for k, sym := range alt {
|
||||
if sym.kind == symNonterm {
|
||||
la.call[a][ai][k] = selection(a, alt[k:])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return la
|
||||
}
|
||||
|
||||
// alt returns the selection set guarding alternate alt of nonterminal nt.
|
||||
func (la *lookahead) alt(nt, alt int) *laSet { return &la.sel[nt][alt] }
|
||||
|
||||
// callAt returns the selection set guarding the nonterminal call at slot L.
|
||||
func (la *lookahead) callAt(L slot) *laSet { return &la.call[L.nt][L.alt][L.dot] }
|
||||
|
||||
// admits reports whether the next input symbol at i is in s, i.e. whether the
|
||||
// guarded sequence may derive a prefix of input[i:] in some context.
|
||||
func (s *laSet) admits(input []byte, i int) bool {
|
||||
if i >= len(input) {
|
||||
return s.eof
|
||||
}
|
||||
r, size := utf8.DecodeRune(input[i:])
|
||||
if r == utf8.RuneError && size == 1 {
|
||||
// No terminal matches an invalid UTF-8 sequence (see matchTerm).
|
||||
return false
|
||||
}
|
||||
return s.contains(r)
|
||||
}
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
package goabnf
|
||||
|
||||
const (
|
||||
defaultValidate = true
|
||||
defaultRedefine = false
|
||||
)
|
||||
|
||||
func process(opts ...ABNFOption) *abnfOptions {
|
||||
o := &abnfOptions{
|
||||
validate: defaultValidate,
|
||||
redefineCore: defaultRedefine,
|
||||
}
|
||||
for _, opt := range opts {
|
||||
opt.apply(o)
|
||||
}
|
||||
return o
|
||||
}
|
||||
|
||||
// ABNFOption defines the interface for parsing ABNF functional options.
|
||||
type ABNFOption interface {
|
||||
apply(*abnfOptions)
|
||||
}
|
||||
|
||||
type abnfOptions struct {
|
||||
validate bool
|
||||
redefineCore bool
|
||||
}
|
||||
|
||||
// Defines if proceed to semantic validation.
|
||||
type validateOption bool
|
||||
|
||||
var _ ABNFOption = (*validateOption)(nil)
|
||||
|
||||
func (o validateOption) apply(opts *abnfOptions) {
|
||||
opts.validate = bool(o)
|
||||
}
|
||||
|
||||
// WithValidation returns a functional option to proceed to
|
||||
// validation or not.
|
||||
// Default is true.
|
||||
func WithValidation(validate bool) ABNFOption {
|
||||
return validateOption(validate)
|
||||
}
|
||||
|
||||
// Defines if an ABNF grammar could redefine a core rule.
|
||||
type redefineCoreOption bool
|
||||
|
||||
var _ ABNFOption = (*redefineCoreOption)(nil)
|
||||
|
||||
func (o redefineCoreOption) apply(opts *abnfOptions) {
|
||||
opts.redefineCore = bool(o)
|
||||
}
|
||||
|
||||
// WithRedefineCoreRules returns a functional option to
|
||||
// enable redefining a core rule.
|
||||
// Default is false.
|
||||
//
|
||||
// WARNING: use with caution, as we left to the user the responsibility
|
||||
// to ensure the redefinition keeps the ABNF grammar coherent (i.e.
|
||||
// there is an isomorphism between the core rule and the redefinition,
|
||||
// not especially the same textual representation).
|
||||
func WithRedefineCoreRules(redefine bool) ABNFOption {
|
||||
return redefineCoreOption(redefine)
|
||||
}
|
||||
+320
@@ -0,0 +1,320 @@
|
||||
package goabnf
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// recognize.go holds the engine behind (*Grammar).IsValid.
|
||||
//
|
||||
// Validity only needs the SET of input positions each element can reach, never
|
||||
// the (possibly exponential) set of distinct parse trees that reach them. We
|
||||
// compute, for each (element, start-index), the set of reachable end-indices,
|
||||
// deduplicating positions at every step and memoizing per (element, index).
|
||||
// Positions live in [0, len(input)], so the work is polynomial rather than the
|
||||
// exponential path enumeration that Parse performs.
|
||||
//
|
||||
// IsValid enforces left-termination before calling in, so the in-progress
|
||||
// guard below is defense-in-depth: it can never fire for a left-terminating
|
||||
// grammar (a same-index re-entry implies the rule is not left terminating).
|
||||
|
||||
type recognizer struct {
|
||||
g *Grammar
|
||||
input []byte
|
||||
memo map[string]map[int]bool // element.String()+"@"+index -> reachable end set
|
||||
inProgress map[string]bool
|
||||
|
||||
// Left-recursion support. leftRec maps each left-recursive rule (lowercase
|
||||
// name) to its left-corner SCC, grown jointly. growing holds the live seed
|
||||
// of each rule currently being grown (key "rule@index"); a re-entrant call
|
||||
// returns the seed instead of recursing.
|
||||
leftRec map[string][]string
|
||||
growing map[string]map[int]bool
|
||||
}
|
||||
|
||||
func cloneSet(s map[int]bool) map[int]bool {
|
||||
out := make(map[int]bool, len(s))
|
||||
for k := range s {
|
||||
out[k] = true
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func (r *recognizer) reachAlt(alt Alternation, index int) map[int]bool {
|
||||
out := map[int]bool{}
|
||||
for _, c := range alt.Concatenations {
|
||||
for e := range r.reachConcat(c, index) {
|
||||
out[e] = true
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func (r *recognizer) reachConcat(c Concatenation, index int) map[int]bool {
|
||||
cur := map[int]bool{index: true}
|
||||
for _, rep := range c.Repetitions {
|
||||
next := map[int]bool{}
|
||||
for p := range cur {
|
||||
for e := range r.reachRep(rep, p) {
|
||||
next[e] = true
|
||||
}
|
||||
}
|
||||
cur = next
|
||||
if len(cur) == 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
return cur
|
||||
}
|
||||
|
||||
func (r *recognizer) reachRep(rep Repetition, index int) map[int]bool {
|
||||
ends := map[int]bool{}
|
||||
if rep.Min == 0 {
|
||||
ends[index] = true
|
||||
}
|
||||
|
||||
// level = positions reachable after exactly c occurrences.
|
||||
level := map[int]bool{index: true}
|
||||
// seen = every position reached at any count so far. reachElem only moves
|
||||
// forward (an end position is >= its start), so once a step introduces no
|
||||
// position outside seen, the reachable set has closed and no later
|
||||
// repetition can add a new end position. Breaking there keeps an unbounded
|
||||
// repetition over an element with large reach-sets (e.g. *(1*ALPHA)) at
|
||||
// O(n^2); the previous len(next)==len(level) test never tripped because
|
||||
// level shrank by one each step ({1..n}, {2..n}, ...), giving O(n^3).
|
||||
seen := map[int]bool{index: true}
|
||||
|
||||
// Levels are stationary after at most len(input)+1 counts: reachElem only
|
||||
// moves forward, so whether position x is in a level depends on positions
|
||||
// below x plus x reaching itself, and each position settles one count after
|
||||
// the ones below it. The stationary check below therefore fires within cap.
|
||||
cap := len(r.input) + 2
|
||||
for c := 1; c <= cap; c++ {
|
||||
if rep.Max != inf && c > rep.Max {
|
||||
break
|
||||
}
|
||||
next := map[int]bool{}
|
||||
for p := range level {
|
||||
for e := range r.reachElem(rep.Element, p) {
|
||||
next[e] = true
|
||||
}
|
||||
}
|
||||
if len(next) == 0 {
|
||||
break
|
||||
}
|
||||
if c >= rep.Min {
|
||||
for e := range next {
|
||||
ends[e] = true
|
||||
}
|
||||
}
|
||||
grew := false
|
||||
for e := range next {
|
||||
if !seen[e] {
|
||||
seen[e] = true
|
||||
grew = true
|
||||
}
|
||||
}
|
||||
// Stationary level: every further count reaches the same positions, so
|
||||
// a minimum not yet met is met by that same set. This happens with a
|
||||
// nullable element, e.g. 3"" on an empty input, where the count must
|
||||
// go past the input length to reach the minimum.
|
||||
if c < rep.Min && sameSet(next, level) {
|
||||
if rep.Max == inf || rep.Min <= rep.Max {
|
||||
for e := range next {
|
||||
ends[e] = true
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
level = next
|
||||
// Reachable set closed and the minimum-count requirement met.
|
||||
if !grew && c >= rep.Min {
|
||||
break
|
||||
}
|
||||
}
|
||||
return ends
|
||||
}
|
||||
|
||||
func (r *recognizer) reachElem(elem ElemItf, index int) map[int]bool {
|
||||
// Left-recursive rulenames are resolved by seed-growing rather than the
|
||||
// flat memoized recursion below (which would cut the recursion to empty and
|
||||
// under-accept).
|
||||
if rn, ok := elem.(ElemRulename); ok {
|
||||
if scc, isLR := r.leftRec[strings.ToLower(rn.Name)]; isLR {
|
||||
return r.reachLeftRec(strings.ToLower(rn.Name), scc, index)
|
||||
}
|
||||
}
|
||||
|
||||
key := elem.String() + "@" + strconv.Itoa(index)
|
||||
if cached, ok := r.memo[key]; ok {
|
||||
return cached
|
||||
}
|
||||
if r.inProgress[key] {
|
||||
// Re-entry at the same position == non-progressing recursion in a rule
|
||||
// not flagged left-recursive. Return empty (defense-in-depth) instead of
|
||||
// looping forever.
|
||||
return map[int]bool{}
|
||||
}
|
||||
r.inProgress[key] = true
|
||||
out := r.computeElem(elem, index)
|
||||
delete(r.inProgress, key)
|
||||
// Safe to cache: a rule reached here either is independent of any actively
|
||||
// growing seed, or is itself a growing SCC member (handled by reachLeftRec
|
||||
// via r.growing, not this path). A lower left-corner SCC cannot reference
|
||||
// the SCC growing above it -- they would be one SCC -- so its result is
|
||||
// stable. Caching nested SCCs is what keeps left recursion polynomial.
|
||||
r.memo[key] = cloneSet(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// reachLeftRec computes the reachable end-set of a left-recursive rule at index
|
||||
// by growing the whole left-corner SCC from the empty seed to its least
|
||||
// fixpoint. Each growth step adds end-positions (bounded by len(input)), so it
|
||||
// terminates; a re-entrant call to any SCC member at the same index returns the
|
||||
// current seed. Pure (base-less) left recursion correctly settles at the empty
|
||||
// set, i.e. "no match".
|
||||
func (r *recognizer) reachLeftRec(name string, scc []string, index int) map[int]bool {
|
||||
sidx := "@" + strconv.Itoa(index)
|
||||
key := name + sidx
|
||||
if cached, ok := r.memo[key]; ok {
|
||||
return cached
|
||||
}
|
||||
if seed, ok := r.growing[key]; ok {
|
||||
return seed // re-entrant: return the current seed
|
||||
}
|
||||
|
||||
keys := make([]string, len(scc))
|
||||
for i, x := range scc {
|
||||
keys[i] = x + sidx
|
||||
r.growing[keys[i]] = map[int]bool{}
|
||||
}
|
||||
for changed := true; changed; {
|
||||
changed = false
|
||||
for i, x := range scc {
|
||||
rule := GetRule(x, r.g.Rulemap)
|
||||
if rule == nil {
|
||||
continue
|
||||
}
|
||||
ns := r.reachAlt(rule.Alternation, index)
|
||||
cur := r.growing[keys[i]]
|
||||
for e := range ns {
|
||||
if !cur[e] {
|
||||
cur[e] = true
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The fixpoint converged. These end-sets are final and independent of any
|
||||
// outer growing seed (the SCC only re-references its own members, held in
|
||||
// r.growing, and strictly lower SCCs, which cannot reference it back), so
|
||||
// they can be memoized unconditionally.
|
||||
var result map[int]bool
|
||||
for i, x := range scc {
|
||||
v := r.growing[keys[i]]
|
||||
delete(r.growing, keys[i])
|
||||
r.memo[keys[i]] = cloneSet(v)
|
||||
if x == name {
|
||||
result = cloneSet(v)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (r *recognizer) computeElem(elem ElemItf, index int) map[int]bool {
|
||||
out := map[int]bool{}
|
||||
switch v := elem.(type) {
|
||||
case ElemRulename:
|
||||
rule := GetRule(v.Name, r.g.Rulemap)
|
||||
if rule == nil {
|
||||
return out
|
||||
}
|
||||
return r.reachAlt(rule.Alternation, index)
|
||||
|
||||
case ElemGroup:
|
||||
return r.reachAlt(v.Alternation, index)
|
||||
|
||||
case ElemOption:
|
||||
out[index] = true // 0 occurrences
|
||||
for e := range r.reachAlt(v.Alternation, index) {
|
||||
out[e] = true
|
||||
}
|
||||
return out
|
||||
|
||||
case ElemProseVal:
|
||||
// prose-val can't be matched (same as the original parser)
|
||||
return out
|
||||
|
||||
case ElemNumVal:
|
||||
switch v.Status {
|
||||
case StatRange:
|
||||
if index >= len(r.input) {
|
||||
return out
|
||||
}
|
||||
// Bounds are numeric: a range straddling the Unicode ceiling still
|
||||
// matches its representable (rune) subset, one wholly above it matches
|
||||
// nothing. numvalToRune never panics (saturates out-of-int32 bounds).
|
||||
min := numvalToRune(v.Elems[0], v.Base)
|
||||
max := numvalToRune(v.Elems[1], v.Base)
|
||||
ru, size := utf8.DecodeRune(r.input[index:])
|
||||
if ru == utf8.RuneError && size == 1 {
|
||||
return out
|
||||
}
|
||||
if min <= ru && ru <= max {
|
||||
out[index+size] = true
|
||||
}
|
||||
case StatSeries:
|
||||
idx := index
|
||||
for i := 0; i < len(v.Elems); i++ {
|
||||
ru := numvalToRune(v.Elems[i], v.Base)
|
||||
// A series element must denote a real character; an out-of-range
|
||||
// value can never appear in UTF-8 input, so the series matches
|
||||
// nothing (and we avoid a spurious U+FFFD coincidence).
|
||||
if !utf8.ValidRune(ru) {
|
||||
return out
|
||||
}
|
||||
s := string(ru)
|
||||
sz := len([]byte(s))
|
||||
if idx+sz > len(r.input) || s != string(r.input[idx:idx+sz]) {
|
||||
return out
|
||||
}
|
||||
idx += sz
|
||||
}
|
||||
out[idx] = true
|
||||
}
|
||||
return out
|
||||
|
||||
case ElemCharVal:
|
||||
idx := index
|
||||
for i := 0; i < len(v.Values); i++ {
|
||||
if idx >= len(r.input) {
|
||||
return out
|
||||
}
|
||||
ru, size := utf8.DecodeRune(r.input[idx:])
|
||||
if ru == utf8.RuneError && size == 1 {
|
||||
return out
|
||||
}
|
||||
if !sensequal(v.Values[i], ru, v.Sensitive) {
|
||||
return out
|
||||
}
|
||||
idx += size
|
||||
}
|
||||
out[idx] = true
|
||||
return out
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func sameSet(a, b map[int]bool) bool {
|
||||
if len(a) != len(b) {
|
||||
return false
|
||||
}
|
||||
for k := range a {
|
||||
if !b[k] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
+468
@@ -0,0 +1,468 @@
|
||||
package goabnf
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// Regex builds a regular expression that recognises the given rulename.
|
||||
//
|
||||
// The rule must not contain a cycle in its dependency graph: a regular
|
||||
// expression (RE2, no subroutines) cannot express recursion.
|
||||
//
|
||||
// Because RE2 has no subroutine/backreference, a rule referenced N times is
|
||||
// inlined N times, so the output can grow exponentially with reference-nesting
|
||||
// depth for diamond-shaped grammars. Pass WithMaxRegexLen to fail fast with
|
||||
// *ErrRegexTooLarge instead of exhausting memory. The output is simplified
|
||||
// (character classes merged, redundant groups and {1} quantifiers dropped) but
|
||||
// is not guaranteed minimal.
|
||||
func (g *Grammar) Regex(rulename string, opts ...RegexOption) (string, error) {
|
||||
o := ®exOptions{}
|
||||
for _, op := range opts {
|
||||
op.applyRegex(o)
|
||||
}
|
||||
|
||||
isCyclic, err := g.RuleContainsCycle(rulename)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if isCyclic {
|
||||
return "", &ErrCyclicRule{Rulename: rulename}
|
||||
}
|
||||
rule := GetRule(rulename, g.Rulemap)
|
||||
if rule == nil {
|
||||
return "", &ErrRuleNotFound{Rulename: rulename}
|
||||
}
|
||||
|
||||
b := &reBuilder{g: g, memo: map[string]reNode{}}
|
||||
node := b.alt(rule.Alternation)
|
||||
if b.err != nil {
|
||||
return "", b.err
|
||||
}
|
||||
node = simplify(node, map[reNode]reNode{})
|
||||
|
||||
r := &reRenderer{max: o.maxLen}
|
||||
var sb strings.Builder
|
||||
r.render(&sb, node, precAlt)
|
||||
if r.err != nil {
|
||||
return "", r.err
|
||||
}
|
||||
return sb.String(), nil
|
||||
}
|
||||
|
||||
// ---- options & error ----
|
||||
|
||||
type regexOptions struct{ maxLen int }
|
||||
|
||||
// RegexOption configures Regex.
|
||||
type RegexOption interface{ applyRegex(*regexOptions) }
|
||||
|
||||
type regexOptionFunc func(*regexOptions)
|
||||
|
||||
func (f regexOptionFunc) applyRegex(o *regexOptions) { f(o) }
|
||||
|
||||
// WithMaxRegexLen bounds the produced regex length in bytes. When the budget is
|
||||
// exceeded, Regex returns *ErrRegexTooLarge. Zero (the default) is unbounded.
|
||||
func WithMaxRegexLen(n int) RegexOption {
|
||||
return regexOptionFunc(func(o *regexOptions) { o.maxLen = n })
|
||||
}
|
||||
|
||||
// ErrRegexTooLarge is returned by Regex when the output would exceed the budget
|
||||
// set with WithMaxRegexLen.
|
||||
type ErrRegexTooLarge struct{ Max int }
|
||||
|
||||
var _ error = (*ErrRegexTooLarge)(nil)
|
||||
|
||||
func (e *ErrRegexTooLarge) Error() string {
|
||||
return fmt.Sprintf("regex exceeds the configured %d-byte budget", e.Max)
|
||||
}
|
||||
|
||||
// ---- intermediate representation ----
|
||||
|
||||
const (
|
||||
precAlt = 1 // alternation binds loosest
|
||||
precConcat = 2
|
||||
precRepeat = 3
|
||||
precAtom = 4 // a class or single char binds tightest
|
||||
)
|
||||
|
||||
type reNode interface{ prec() int }
|
||||
|
||||
type reRange struct{ lo, hi rune }
|
||||
|
||||
type reClass struct{ ranges []reRange } // a single [c,c] renders as a bare char
|
||||
type reConcat struct{ parts []reNode }
|
||||
type reAlt struct{ opts []reNode }
|
||||
type reRepeat struct {
|
||||
sub reNode
|
||||
min, max int
|
||||
}
|
||||
type reEmpty struct{}
|
||||
|
||||
// reNever matches nothing. It represents a num-val a RE2/Unicode pattern cannot
|
||||
// express (a value entirely above U+10FFFF), keeping Regex consistent with the
|
||||
// recognizer, which also matches nothing in that case.
|
||||
type reNever struct{}
|
||||
|
||||
func (*reClass) prec() int { return precAtom }
|
||||
func (*reConcat) prec() int { return precConcat }
|
||||
func (*reAlt) prec() int { return precAlt }
|
||||
func (*reRepeat) prec() int { return precRepeat }
|
||||
func (*reEmpty) prec() int { return precAtom }
|
||||
func (*reNever) prec() int { return precAtom }
|
||||
|
||||
var reEmptyV reNode = &reEmpty{}
|
||||
var reNeverV reNode = &reNever{}
|
||||
|
||||
func isNever(n reNode) bool { _, ok := n.(*reNever); return ok }
|
||||
|
||||
// ---- build (memoised per rule => linear over the grammar DAG) ----
|
||||
|
||||
type reBuilder struct {
|
||||
g *Grammar
|
||||
memo map[string]reNode
|
||||
err error
|
||||
}
|
||||
|
||||
func (b *reBuilder) rule(name string) reNode {
|
||||
key := strings.ToLower(name)
|
||||
if n, ok := b.memo[key]; ok {
|
||||
return n
|
||||
}
|
||||
r := GetRule(name, b.g.Rulemap)
|
||||
if r == nil {
|
||||
return reEmptyV
|
||||
}
|
||||
n := b.alt(r.Alternation)
|
||||
b.memo[key] = n
|
||||
return n
|
||||
}
|
||||
|
||||
func (b *reBuilder) alt(a Alternation) reNode {
|
||||
opts := make([]reNode, 0, len(a.Concatenations))
|
||||
for _, c := range a.Concatenations {
|
||||
opts = append(opts, b.concat(c))
|
||||
}
|
||||
return &reAlt{opts: opts}
|
||||
}
|
||||
|
||||
func (b *reBuilder) concat(c Concatenation) reNode {
|
||||
parts := make([]reNode, 0, len(c.Repetitions))
|
||||
for _, rep := range c.Repetitions {
|
||||
parts = append(parts, &reRepeat{sub: b.elem(rep.Element), min: rep.Min, max: rep.Max})
|
||||
}
|
||||
return &reConcat{parts: parts}
|
||||
}
|
||||
|
||||
func (b *reBuilder) elem(e ElemItf) reNode {
|
||||
switch v := e.(type) {
|
||||
case ElemRulename:
|
||||
return b.rule(v.Name)
|
||||
case ElemGroup:
|
||||
return b.alt(v.Alternation)
|
||||
case ElemOption:
|
||||
return &reRepeat{sub: b.alt(v.Alternation), min: 0, max: 1}
|
||||
case ElemNumVal:
|
||||
switch v.Status {
|
||||
case StatRange:
|
||||
lo := numvalToRune(v.Elems[0], v.Base)
|
||||
hi := min(
|
||||
// RE2 patterns are Unicode (max U+10FFFF). A range straddling that
|
||||
// ceiling is clamped to its representable subset; one wholly above it
|
||||
// matches nothing -- mirroring the recognizer.
|
||||
numvalToRune(v.Elems[1], v.Base), utf8.MaxRune)
|
||||
if lo > utf8.MaxRune || lo > hi {
|
||||
return reNeverV
|
||||
}
|
||||
return &reClass{ranges: []reRange{{lo, hi}}}
|
||||
default: // StatSeries is a CONCATENATION of values (a fixed sequence).
|
||||
parts := make([]reNode, 0, len(v.Elems))
|
||||
for _, s := range v.Elems {
|
||||
c := numvalToRune(s, v.Base)
|
||||
// A series element must be a real character; if it is not, the
|
||||
// whole sequence can never match any (Unicode) input.
|
||||
if !utf8.ValidRune(c) {
|
||||
return reNeverV
|
||||
}
|
||||
parts = append(parts, &reClass{ranges: []reRange{{c, c}}})
|
||||
}
|
||||
return &reConcat{parts: parts}
|
||||
}
|
||||
case ElemCharVal:
|
||||
// A char-val is case-insensitive unless Sensitive (RFC 7405); the
|
||||
// recognizer folds ASCII A-Z only (see runeMin/sensequal), so we match
|
||||
// that exactly here.
|
||||
parts := make([]reNode, 0, len(v.Values))
|
||||
for _, c := range v.Values {
|
||||
parts = append(parts, charClass(c, v.Sensitive))
|
||||
}
|
||||
return &reConcat{parts: parts}
|
||||
case ElemProseVal:
|
||||
if b.err == nil {
|
||||
b.err = ErrHandlingProseVal
|
||||
}
|
||||
return reEmptyV
|
||||
}
|
||||
return reEmptyV
|
||||
}
|
||||
|
||||
func charClass(c rune, sensitive bool) reNode {
|
||||
if !sensitive {
|
||||
switch {
|
||||
case c >= 'A' && c <= 'Z':
|
||||
lo := c - 'A' + 'a'
|
||||
return &reClass{ranges: []reRange{{c, c}, {lo, lo}}}
|
||||
case c >= 'a' && c <= 'z':
|
||||
up := c - 'a' + 'A'
|
||||
return &reClass{ranges: []reRange{{up, up}, {c, c}}}
|
||||
}
|
||||
}
|
||||
return &reClass{ranges: []reRange{{c, c}}}
|
||||
}
|
||||
|
||||
// ---- simplification (pointer-memoised => linear over the DAG) ----
|
||||
|
||||
func simplify(n reNode, memo map[reNode]reNode) reNode {
|
||||
if n == nil {
|
||||
return reEmptyV
|
||||
}
|
||||
if s, ok := memo[n]; ok {
|
||||
return s
|
||||
}
|
||||
var res reNode
|
||||
switch v := n.(type) {
|
||||
case *reClass:
|
||||
res = &reClass{ranges: normalizeRanges(v.ranges)}
|
||||
case *reEmpty:
|
||||
res = reEmptyV
|
||||
case *reNever:
|
||||
res = reNeverV
|
||||
case *reConcat:
|
||||
parts := make([]reNode, 0, len(v.parts))
|
||||
never := false
|
||||
for _, p := range v.parts {
|
||||
sp := simplify(p, memo)
|
||||
switch x := sp.(type) {
|
||||
case *reEmpty: // drop: empty in a concatenation contributes nothing
|
||||
case *reNever: // one un-matchable part makes the whole sequence un-matchable
|
||||
never = true
|
||||
case *reConcat:
|
||||
parts = append(parts, x.parts...) // flatten
|
||||
default:
|
||||
parts = append(parts, sp)
|
||||
}
|
||||
}
|
||||
switch {
|
||||
case never:
|
||||
res = reNeverV
|
||||
case len(parts) == 0:
|
||||
res = reEmptyV
|
||||
case len(parts) == 1:
|
||||
res = parts[0]
|
||||
default:
|
||||
res = &reConcat{parts: parts}
|
||||
}
|
||||
case *reAlt:
|
||||
opts := make([]reNode, 0, len(v.opts))
|
||||
for _, o := range v.opts {
|
||||
so := simplify(o, memo)
|
||||
if isNever(so) { // a never-matching branch is dead; drop it
|
||||
continue
|
||||
}
|
||||
if x, ok := so.(*reAlt); ok {
|
||||
opts = append(opts, x.opts...) // flatten
|
||||
} else {
|
||||
opts = append(opts, so)
|
||||
}
|
||||
}
|
||||
// Fold every class option into a single merged class: turns an
|
||||
// alternation of single characters / ranges into one [...].
|
||||
var ranges []reRange
|
||||
classes := 0
|
||||
others := make([]reNode, 0, len(opts))
|
||||
for _, o := range opts {
|
||||
if c, ok := o.(*reClass); ok {
|
||||
ranges = append(ranges, c.ranges...)
|
||||
classes++
|
||||
} else {
|
||||
others = append(others, o)
|
||||
}
|
||||
}
|
||||
merged := make([]reNode, 0, len(others)+1)
|
||||
if classes > 0 {
|
||||
merged = append(merged, &reClass{ranges: normalizeRanges(ranges)})
|
||||
}
|
||||
merged = append(merged, others...)
|
||||
switch len(merged) {
|
||||
case 0:
|
||||
// Every branch was un-matchable: the alternation matches nothing.
|
||||
res = reNeverV
|
||||
case 1:
|
||||
res = merged[0]
|
||||
default:
|
||||
res = &reAlt{opts: merged}
|
||||
}
|
||||
case *reRepeat:
|
||||
sub := simplify(v.sub, memo)
|
||||
switch {
|
||||
case isNever(sub):
|
||||
// Zero occurrences are allowed iff min == 0 (then it matches empty);
|
||||
// otherwise the repetition is itself un-matchable.
|
||||
if v.min == 0 {
|
||||
res = reEmptyV
|
||||
} else {
|
||||
res = reNeverV
|
||||
}
|
||||
case isEmpty(sub):
|
||||
res = reEmptyV
|
||||
case v.min == 1 && v.max == 1:
|
||||
res = sub
|
||||
default:
|
||||
res = &reRepeat{sub: sub, min: v.min, max: v.max}
|
||||
}
|
||||
default:
|
||||
res = n
|
||||
}
|
||||
memo[n] = res
|
||||
return res
|
||||
}
|
||||
|
||||
func isEmpty(n reNode) bool { _, ok := n.(*reEmpty); return ok }
|
||||
|
||||
// normalizeRanges sorts and coalesces overlapping/adjacent ranges.
|
||||
func normalizeRanges(rs []reRange) []reRange {
|
||||
if len(rs) <= 1 {
|
||||
return rs
|
||||
}
|
||||
cp := append([]reRange(nil), rs...)
|
||||
sort.Slice(cp, func(i, j int) bool {
|
||||
return cp[i].lo < cp[j].lo || (cp[i].lo == cp[j].lo && cp[i].hi < cp[j].hi)
|
||||
})
|
||||
out := []reRange{cp[0]}
|
||||
for _, r := range cp[1:] {
|
||||
last := &out[len(out)-1]
|
||||
if r.lo <= last.hi+1 { // overlapping or adjacent
|
||||
if r.hi > last.hi {
|
||||
last.hi = r.hi
|
||||
}
|
||||
} else {
|
||||
out = append(out, r)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// ---- render (minimal grouping, length-budgeted) ----
|
||||
|
||||
type reRenderer struct {
|
||||
max int
|
||||
n int
|
||||
err error
|
||||
}
|
||||
|
||||
func (rr *reRenderer) write(sb *strings.Builder, s string) {
|
||||
if rr.err != nil {
|
||||
return
|
||||
}
|
||||
rr.n += len(s)
|
||||
if rr.max > 0 && rr.n > rr.max {
|
||||
rr.err = &ErrRegexTooLarge{Max: rr.max}
|
||||
return
|
||||
}
|
||||
sb.WriteString(s)
|
||||
}
|
||||
|
||||
// render emits node; it self-groups with (?:...) when its precedence is looser
|
||||
// than the surrounding context requires.
|
||||
func (rr *reRenderer) render(sb *strings.Builder, n reNode, ctx int) {
|
||||
if rr.err != nil {
|
||||
return
|
||||
}
|
||||
grp := n.prec() < ctx
|
||||
if grp {
|
||||
rr.write(sb, "(?:")
|
||||
}
|
||||
switch v := n.(type) {
|
||||
case *reEmpty:
|
||||
// (?: ) already opened if grouped; otherwise emit an explicit empty.
|
||||
if !grp {
|
||||
rr.write(sb, "(?:)")
|
||||
}
|
||||
case *reNever:
|
||||
// Negated full-Unicode class: matches no character, hence nothing.
|
||||
rr.write(sb, "[^\\x00-\\x{10ffff}]")
|
||||
case *reClass:
|
||||
rr.write(sb, renderClass(v))
|
||||
case *reConcat:
|
||||
for _, p := range v.parts {
|
||||
rr.render(sb, p, precConcat)
|
||||
}
|
||||
case *reAlt:
|
||||
for i, o := range v.opts {
|
||||
if i > 0 {
|
||||
rr.write(sb, "|")
|
||||
}
|
||||
rr.render(sb, o, precAlt)
|
||||
}
|
||||
case *reRepeat:
|
||||
rr.render(sb, v.sub, precAtom) // operand of a quantifier must be an atom
|
||||
rr.write(sb, quantifier(v.min, v.max))
|
||||
}
|
||||
if grp {
|
||||
rr.write(sb, ")")
|
||||
}
|
||||
}
|
||||
|
||||
func renderClass(c *reClass) string {
|
||||
if len(c.ranges) == 1 && c.ranges[0].lo == c.ranges[0].hi {
|
||||
return regescape(c.ranges[0].lo) // bare single character
|
||||
}
|
||||
var b strings.Builder
|
||||
b.WriteByte('[')
|
||||
for _, r := range c.ranges {
|
||||
if r.lo == r.hi {
|
||||
b.WriteString(classEscape(r.lo))
|
||||
} else {
|
||||
b.WriteString(classEscape(r.lo))
|
||||
b.WriteByte('-')
|
||||
b.WriteString(classEscape(r.hi))
|
||||
}
|
||||
}
|
||||
b.WriteByte(']')
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func quantifier(min, max int) string {
|
||||
switch {
|
||||
case min == 0 && max == 1:
|
||||
return "?"
|
||||
case min == 0 && max == inf:
|
||||
return "*"
|
||||
case min == 1 && max == inf:
|
||||
return "+"
|
||||
case max == inf:
|
||||
return fmt.Sprintf("{%d,}", min)
|
||||
case min == max:
|
||||
return fmt.Sprintf("{%d}", min)
|
||||
case min == 0:
|
||||
return fmt.Sprintf("{0,%d}", max)
|
||||
default:
|
||||
return fmt.Sprintf("{%d,%d}", min, max)
|
||||
}
|
||||
}
|
||||
|
||||
// regescape quotes a single rune for use outside a character class.
|
||||
func regescape(r rune) string { return regexp.QuoteMeta(string(r)) }
|
||||
|
||||
// classEscape renders a rune as a character-class member. Alphanumerics are
|
||||
// never class metacharacters; everything else is emitted as \x{...} to avoid
|
||||
// the class-specific escaping pitfalls of ], ^, - and \.
|
||||
func classEscape(r rune) string {
|
||||
if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') {
|
||||
return string(r)
|
||||
}
|
||||
return fmt.Sprintf("\\x{%x}", r)
|
||||
}
|
||||
+1867
File diff suppressed because it is too large.
Load diff
+902
@@ -0,0 +1,902 @@
|
||||
package goabnf
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/big"
|
||||
"strconv"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// This file implements a GLL parser (Scott & Johnstone) producing a binarized
|
||||
// Shared Packed Parse Forest (SPPF) for ABNF grammars.
|
||||
//
|
||||
// GLL parses ANY context-free grammar -- including left, right, hidden and
|
||||
// mutual recursion, and arbitrary ambiguity -- in worst-case O(n^3) time and
|
||||
// space, using a graph-structured stack (GSS) and a descriptor worklist whose
|
||||
// "seen" set makes recursion terminate. The forest shares every sub-derivation
|
||||
// and packs ambiguity, so it stays polynomial even when it represents
|
||||
// exponentially or infinitely many trees.
|
||||
//
|
||||
// Pipeline: lower the ABNF AST into a flat slotted grammar (synthetic
|
||||
// nonterminals for groups, options and repetitions), then run GLL over the
|
||||
// slots. Recognition, ambiguity, tree counting and extraction are polynomial
|
||||
// queries over the resulting forest.
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Slotted grammar
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
type symKind uint8
|
||||
|
||||
const (
|
||||
symNonterm symKind = iota
|
||||
symTerm // a char-val / num-val / prose-val matcher
|
||||
symEps // matches the empty string
|
||||
)
|
||||
|
||||
type ssym struct {
|
||||
kind symKind
|
||||
nt int // nonterminal id, for symNonterm
|
||||
term ElemItf // terminal element, for symTerm
|
||||
}
|
||||
|
||||
type sgNT struct {
|
||||
label string // diagnostic label
|
||||
isRule bool // corresponds to an original ABNF rule (kept in the tree)
|
||||
ruleName string // original rule name when isRule
|
||||
alts [][]ssym // alternates; every alternate has >= 1 symbol
|
||||
|
||||
// Native counted repetition. When isRep, the nonterminal stands for
|
||||
// `repElem{repMin,repMax}` and its two alternates are the self-recursive
|
||||
// encoding `R ::= eps | repElem R`. The [repMin,repMax] bound is enforced at
|
||||
// parse time by a count carried in the GLL state (see addRepAlts), so the
|
||||
// slot grammar stays O(1) in the repetition bound instead of unrolling it.
|
||||
isRep bool
|
||||
repMin int
|
||||
repMax int // inf (== -1) means unbounded
|
||||
}
|
||||
|
||||
type slotGrammar struct {
|
||||
nts []*sgNT
|
||||
index map[string]int // dedup key -> nt id
|
||||
start int
|
||||
la *lookahead // selection sets guarding descriptor creation
|
||||
}
|
||||
|
||||
func (sg *slotGrammar) reserve(key, label string) (int, bool) {
|
||||
if id, ok := sg.index[key]; ok {
|
||||
return id, true
|
||||
}
|
||||
id := len(sg.nts)
|
||||
sg.nts = append(sg.nts, &sgNT{label: label})
|
||||
sg.index[key] = id
|
||||
return id, false
|
||||
}
|
||||
|
||||
// compileSlots lowers grammar starting at rootRulename into a slotted grammar.
|
||||
const defaultMaxSlots = 1 << 16
|
||||
|
||||
func compileSlots(g *Grammar, rootRulename string, maxSlots int) (*slotGrammar, error) {
|
||||
if GetRule(rootRulename, g.Rulemap) == nil {
|
||||
return nil, &ErrRuleNotFound{Rulename: rootRulename}
|
||||
}
|
||||
sg := &slotGrammar{index: map[string]int{}}
|
||||
c := &lowerer{g: g, sg: sg, max: maxSlots}
|
||||
sg.start = c.rule(rootRulename)
|
||||
if c.err != nil {
|
||||
return nil, c.err
|
||||
}
|
||||
if c.over {
|
||||
return nil, &ErrGrammarTooLarge{Max: maxSlots}
|
||||
}
|
||||
sg.la = computeLookahead(sg)
|
||||
return sg, nil
|
||||
}
|
||||
|
||||
type lowerer struct {
|
||||
g *Grammar
|
||||
sg *slotGrammar
|
||||
err error
|
||||
max int // max nonterminals; 0 == unbounded
|
||||
over bool // budget exceeded
|
||||
}
|
||||
|
||||
func (c *lowerer) budgetExceeded() bool {
|
||||
if c.max > 0 && len(c.sg.nts) >= c.max {
|
||||
c.over = true
|
||||
}
|
||||
return c.over
|
||||
}
|
||||
|
||||
func (c *lowerer) rule(name string) int {
|
||||
key := "rule:" + canon(name)
|
||||
id, existed := c.sg.reserve(key, name)
|
||||
if existed {
|
||||
return id
|
||||
}
|
||||
if c.budgetExceeded() {
|
||||
return id
|
||||
}
|
||||
rule := GetRule(name, c.g.Rulemap)
|
||||
if rule == nil {
|
||||
// Unknown rule: a nonterminal with no alternates derives nothing.
|
||||
c.sg.nts[id].label = name
|
||||
return id
|
||||
}
|
||||
c.sg.nts[id].isRule = true
|
||||
c.sg.nts[id].ruleName = rule.Name
|
||||
c.sg.nts[id].alts = c.alts(rule.Alternation)
|
||||
return id
|
||||
}
|
||||
|
||||
func (c *lowerer) alts(a Alternation) [][]ssym {
|
||||
out := make([][]ssym, 0, len(a.Concatenations))
|
||||
for _, cc := range a.Concatenations {
|
||||
seq := make([]ssym, 0, len(cc.Repetitions))
|
||||
for _, rep := range cc.Repetitions {
|
||||
seq = append(seq, c.rep(rep))
|
||||
}
|
||||
if len(seq) == 0 {
|
||||
seq = []ssym{{kind: symEps}}
|
||||
}
|
||||
out = append(out, seq)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func (c *lowerer) rep(rep Repetition) ssym {
|
||||
if rep.Min == 1 && rep.Max == 1 {
|
||||
return c.elem(rep.Element)
|
||||
}
|
||||
key := "rep:" + rep.String()
|
||||
id, existed := c.sg.reserve(key, rep.String())
|
||||
if existed {
|
||||
return ssym{kind: symNonterm, nt: id}
|
||||
}
|
||||
if c.budgetExceeded() {
|
||||
return ssym{kind: symNonterm, nt: id}
|
||||
}
|
||||
// A malformed bound where max < min accepts nothing: leave the rep
|
||||
// nonterminal with no alternates so it derives the empty language (it is
|
||||
// unsatisfiable, NOT unbounded). This matches the recognizer.
|
||||
if rep.Max != inf && rep.Max < rep.Min {
|
||||
return ssym{kind: symNonterm, nt: id}
|
||||
}
|
||||
// Lower to the self-recursive encoding R ::= eps | E R, a single nonterminal
|
||||
// regardless of the bound. The [Min,Max] count is enforced at parse time
|
||||
// (addRepAlts), so lowering is O(1) in the bound and there is no per-count
|
||||
// nonterminal explosion (the old optTail/reqOne unrolling, which scaled with
|
||||
// Max-Min and Min respectively).
|
||||
esym := c.elem(rep.Element)
|
||||
self := ssym{kind: symNonterm, nt: id}
|
||||
c.sg.nts[id].alts = [][]ssym{
|
||||
{{kind: symEps}}, // alt 0: stop
|
||||
{esym, self}, // alt 1: consume one element, then loop
|
||||
}
|
||||
c.sg.nts[id].label = rep.String()
|
||||
c.sg.nts[id].isRep = true
|
||||
c.sg.nts[id].repMin = rep.Min
|
||||
c.sg.nts[id].repMax = rep.Max
|
||||
return self
|
||||
}
|
||||
|
||||
func (c *lowerer) elem(e ElemItf) ssym {
|
||||
switch v := e.(type) {
|
||||
case ElemRulename:
|
||||
return ssym{kind: symNonterm, nt: c.rule(v.Name)}
|
||||
case ElemGroup:
|
||||
return ssym{kind: symNonterm, nt: c.group(v)}
|
||||
case ElemOption:
|
||||
return ssym{kind: symNonterm, nt: c.option(v)}
|
||||
case ElemCharVal, ElemNumVal, ElemProseVal:
|
||||
return ssym{kind: symTerm, term: e}
|
||||
}
|
||||
return ssym{kind: symTerm, term: e}
|
||||
}
|
||||
|
||||
func (c *lowerer) group(v ElemGroup) int {
|
||||
key := "grp:" + v.String()
|
||||
id, existed := c.sg.reserve(key, v.String())
|
||||
if existed {
|
||||
return id
|
||||
}
|
||||
if c.budgetExceeded() {
|
||||
return id
|
||||
}
|
||||
c.sg.nts[id].alts = c.alts(v.Alternation)
|
||||
return id
|
||||
}
|
||||
|
||||
func (c *lowerer) option(v ElemOption) int {
|
||||
key := "opt:" + v.String()
|
||||
id, existed := c.sg.reserve(key, v.String())
|
||||
if existed {
|
||||
return id
|
||||
}
|
||||
if c.budgetExceeded() {
|
||||
return id
|
||||
}
|
||||
alts := [][]ssym{{{kind: symEps}}}
|
||||
alts = append(alts, c.alts(v.Alternation)...)
|
||||
c.sg.nts[id].alts = alts
|
||||
return id
|
||||
}
|
||||
|
||||
func canon(name string) string {
|
||||
b := []byte(name)
|
||||
for i := range b {
|
||||
if b[i] >= 'A' && b[i] <= 'Z' {
|
||||
b[i] += 'a' - 'A'
|
||||
}
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// SPPF nodes
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
type gnodeKind uint8
|
||||
|
||||
const (
|
||||
gSymbol gnodeKind = iota // a nonterminal node (Start..End)
|
||||
gInter // a binarisation intermediate node
|
||||
gTerm // a terminal / epsilon node
|
||||
)
|
||||
|
||||
type gnode struct {
|
||||
kind gnodeKind
|
||||
nt int // symbol nonterminal id, for gSymbol
|
||||
Start, End int
|
||||
packs []pack // >1 pack == ambiguity
|
||||
}
|
||||
|
||||
// pack is a packed node. It is labelled by the grammar slot it completes and
|
||||
// its pivot (Scott & Johnstone, "GLL parse-tree generation", Science of
|
||||
// Computer Programming 78(10), 2013, §3.1); the pivot is the left extent of the
|
||||
// last child. Two alternates deriving the same span through the same children
|
||||
// are distinct derivations, hence distinct packed nodes.
|
||||
type pack struct {
|
||||
L slot
|
||||
kids []*gnode // 1 or 2 children
|
||||
}
|
||||
|
||||
func (n *gnode) addPack(L slot, children []*gnode) {
|
||||
for _, p := range n.packs {
|
||||
if p.L != L || len(p.kids) != len(children) {
|
||||
continue
|
||||
}
|
||||
same := true
|
||||
for i := range p.kids {
|
||||
if p.kids[i] != children[i] {
|
||||
same = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if same {
|
||||
return
|
||||
}
|
||||
}
|
||||
n.packs = append(n.packs, pack{L: L, kids: children})
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// GLL machinery
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
type slot struct{ nt, alt, dot int }
|
||||
|
||||
type gssNode struct {
|
||||
ret slot // return slot
|
||||
pos int
|
||||
rc int // repetition count carried for native counted repetition (0 otherwise)
|
||||
edges []gssEdge
|
||||
edgeSet map[gssEdge]bool
|
||||
}
|
||||
|
||||
type gssEdge struct {
|
||||
w *gnode
|
||||
to *gssNode
|
||||
}
|
||||
|
||||
type descriptor struct {
|
||||
L slot
|
||||
u *gssNode
|
||||
i int
|
||||
w *gnode
|
||||
rc int // repetition count (0 outside a counted repetition)
|
||||
}
|
||||
|
||||
type gllParser struct {
|
||||
sg *slotGrammar
|
||||
input []byte
|
||||
|
||||
nodes map[nodeKey]*gnode
|
||||
gss map[gssKey]*gssNode
|
||||
u0 *gssNode
|
||||
seen map[descriptor]bool
|
||||
work []descriptor
|
||||
popped map[*gssNode]map[*gnode]bool
|
||||
|
||||
maxNodes int
|
||||
aborted bool
|
||||
|
||||
// Furthest-failure diagnostics: maxPos is the deepest input offset at which
|
||||
// a terminal match was attempted and failed; expected collects the terminals
|
||||
// that could have been consumed there (deduplicated via expectedSeen). They
|
||||
// drive ParseError when the parse has no solution.
|
||||
maxPos int
|
||||
expected []string
|
||||
expectedSeen map[string]bool
|
||||
}
|
||||
|
||||
// nodeKey interns SPPF nodes without allocating strings. Distinct kinds never
|
||||
// collide: symbol nodes key on (kind,nt,start,end); intermediate nodes on
|
||||
// (kind,L,start,end); terminal nodes on (kind,start,end).
|
||||
type nodeKey struct {
|
||||
kind gnodeKind
|
||||
nt int
|
||||
L slot
|
||||
start, end int
|
||||
}
|
||||
|
||||
type gssKey struct {
|
||||
ret slot
|
||||
pos int
|
||||
rc int
|
||||
}
|
||||
|
||||
// addAlt adds the descriptor starting alternate L.alt of L.nt at i when the
|
||||
// lookahead test admits it. A rejected alternate would fail on its first
|
||||
// terminal at i (or, when nullable, its continuation would), so its terminals
|
||||
// are reported to the furthest-failure diagnostics in its place.
|
||||
func (p *gllParser) addAlt(L slot, u *gssNode, i int, rc int) {
|
||||
if p.admits(p.sg.la.alt(L.nt, L.alt), i) {
|
||||
p.add(L, u, i, nil, rc)
|
||||
}
|
||||
}
|
||||
|
||||
// admits applies a lookahead test at i. A rejected sequence would have failed
|
||||
// on one of the set's terminals at i, so those feed the furthest-failure
|
||||
// diagnostics in its place.
|
||||
func (p *gllParser) admits(s *laSet, i int) bool {
|
||||
if s.admits(p.input, i) {
|
||||
return true
|
||||
}
|
||||
if i >= p.maxPos {
|
||||
for _, t := range s.terms {
|
||||
p.noteFail(i, t)
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (p *gllParser) add(L slot, u *gssNode, i int, w *gnode, rc int) {
|
||||
d := descriptor{L, u, i, w, rc}
|
||||
if p.seen[d] {
|
||||
return
|
||||
}
|
||||
p.seen[d] = true
|
||||
p.work = append(p.work, d)
|
||||
}
|
||||
|
||||
func (p *gllParser) gssNodeFor(ret slot, pos, rc int) *gssNode {
|
||||
k := gssKey{ret, pos, rc}
|
||||
if v, ok := p.gss[k]; ok {
|
||||
return v
|
||||
}
|
||||
v := &gssNode{ret: ret, pos: pos, rc: rc, edgeSet: map[gssEdge]bool{}}
|
||||
p.gss[k] = v
|
||||
return v
|
||||
}
|
||||
|
||||
func (p *gllParser) create(L slot, u *gssNode, i int, w *gnode, rc int) *gssNode {
|
||||
v := p.gssNodeFor(L, i, rc)
|
||||
e := gssEdge{w: w, to: u}
|
||||
if !v.edgeSet[e] {
|
||||
v.edgeSet[e] = true
|
||||
v.edges = append(v.edges, e)
|
||||
for z := range p.popped[v] {
|
||||
y := p.getNodeP(L, w, z)
|
||||
p.add(L, u, z.End, y, rc)
|
||||
}
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
func (p *gllParser) pop(u *gssNode, i int, z *gnode) {
|
||||
if u == p.u0 {
|
||||
return
|
||||
}
|
||||
if p.popped[u] == nil {
|
||||
p.popped[u] = map[*gnode]bool{}
|
||||
}
|
||||
p.popped[u][z] = true
|
||||
for _, e := range u.edges {
|
||||
y := p.getNodeP(u.ret, e.w, z)
|
||||
p.add(u.ret, e.to, i, y, u.rc)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *gllParser) findNode(k nodeKey) *gnode {
|
||||
if v, ok := p.nodes[k]; ok {
|
||||
return v
|
||||
}
|
||||
if p.maxNodes > 0 && len(p.nodes) >= p.maxNodes {
|
||||
p.aborted = true
|
||||
}
|
||||
v := &gnode{kind: k.kind, nt: k.nt, Start: k.start, End: k.end}
|
||||
p.nodes[k] = v
|
||||
return v
|
||||
}
|
||||
|
||||
func (p *gllParser) getNodeT(start, end int) *gnode {
|
||||
return p.findNode(nodeKey{kind: gTerm, nt: -1, start: start, end: end})
|
||||
}
|
||||
|
||||
// getNodeP combines left node w (may be nil == $) and right node z under slot L.
|
||||
func (p *gllParser) getNodeP(L slot, w, z *gnode) *gnode {
|
||||
prod := p.sg.nts[L.nt].alts[L.alt]
|
||||
betaEmpty := L.dot == len(prod)
|
||||
if L.dot == 1 && !betaEmpty {
|
||||
return z // single symbol consumed, more to come: pass straight up
|
||||
}
|
||||
start := z.Start
|
||||
if w != nil {
|
||||
start = w.Start
|
||||
}
|
||||
var k nodeKey
|
||||
if betaEmpty {
|
||||
k = nodeKey{kind: gSymbol, nt: L.nt, start: start, end: z.End}
|
||||
} else {
|
||||
k = nodeKey{kind: gInter, nt: -1, L: L, start: start, end: z.End}
|
||||
}
|
||||
y := p.findNode(k)
|
||||
if w != nil {
|
||||
y.addPack(L, []*gnode{w, z})
|
||||
} else {
|
||||
y.addPack(L, []*gnode{z})
|
||||
}
|
||||
return y
|
||||
}
|
||||
|
||||
// matchTerm returns the end index after matching the terminal at i, or -1.
|
||||
func (p *gllParser) matchTerm(s ssym, i int) int {
|
||||
if s.kind == symEps {
|
||||
return i
|
||||
}
|
||||
switch v := s.term.(type) {
|
||||
case ElemCharVal:
|
||||
idx := i
|
||||
for k := 0; k < len(v.Values); k++ {
|
||||
if idx >= len(p.input) {
|
||||
return -1
|
||||
}
|
||||
r, size := utf8.DecodeRune(p.input[idx:])
|
||||
if r == utf8.RuneError && size == 1 {
|
||||
return -1
|
||||
}
|
||||
if !sensequal(v.Values[k], r, v.Sensitive) {
|
||||
return -1
|
||||
}
|
||||
idx += size
|
||||
}
|
||||
return idx
|
||||
case ElemNumVal:
|
||||
switch v.Status {
|
||||
case StatRange:
|
||||
if i >= len(p.input) {
|
||||
return -1
|
||||
}
|
||||
// Numeric comparison: a range straddling U+10FFFF matches its rune
|
||||
// subset; numvalToRune never panics (saturates out-of-int32 bounds).
|
||||
min := numvalToRune(v.Elems[0], v.Base)
|
||||
max := numvalToRune(v.Elems[1], v.Base)
|
||||
r, size := utf8.DecodeRune(p.input[i:])
|
||||
if r == utf8.RuneError && size == 1 {
|
||||
return -1
|
||||
}
|
||||
if min <= r && r <= max {
|
||||
return i + size
|
||||
}
|
||||
return -1
|
||||
case StatSeries:
|
||||
idx := i
|
||||
for k := 0; k < len(v.Elems); k++ {
|
||||
ru := numvalToRune(v.Elems[k], v.Base)
|
||||
// Series elements must be real characters: an out-of-range value
|
||||
// matches no UTF-8 input (and avoids a spurious U+FFFD match).
|
||||
if !utf8.ValidRune(ru) {
|
||||
return -1
|
||||
}
|
||||
str := string(ru)
|
||||
sz := len(str)
|
||||
if idx+sz > len(p.input) || str != string(p.input[idx:idx+sz]) {
|
||||
return -1
|
||||
}
|
||||
idx += sz
|
||||
}
|
||||
return idx
|
||||
}
|
||||
}
|
||||
return -1 // prose-val and anything else never matches
|
||||
}
|
||||
|
||||
// parse runs the GLL loop and returns the symbol node (start, 0, n) or nil.
|
||||
func (p *gllParser) parse() *gnode {
|
||||
startNT := p.sg.start
|
||||
p.u0 = &gssNode{ret: slot{-1, -1, -1}, pos: 0}
|
||||
for ai := range p.sg.nts[startNT].alts {
|
||||
p.addAlt(slot{startNT, ai, 0}, p.u0, 0, 0)
|
||||
}
|
||||
for len(p.work) > 0 {
|
||||
if p.aborted {
|
||||
return nil
|
||||
}
|
||||
d := p.work[len(p.work)-1]
|
||||
p.work = p.work[:len(p.work)-1]
|
||||
p.process(d)
|
||||
}
|
||||
key := nodeKey{kind: gSymbol, nt: startNT, start: 0, end: len(p.input)}
|
||||
return p.nodes[key]
|
||||
}
|
||||
|
||||
func (p *gllParser) process(d descriptor) {
|
||||
L, u, i, w, rc := d.L, d.u, d.i, d.w, d.rc
|
||||
for {
|
||||
prod := p.sg.nts[L.nt].alts[L.alt]
|
||||
if L.dot == len(prod) {
|
||||
z := w
|
||||
if z == nil {
|
||||
z = p.getNodeT(i, i)
|
||||
}
|
||||
p.pop(u, i, z)
|
||||
return
|
||||
}
|
||||
s := prod[L.dot]
|
||||
if s.kind == symNonterm {
|
||||
if !p.admits(p.sg.la.callAt(L), i) {
|
||||
return
|
||||
}
|
||||
ret := slot{L.nt, L.alt, L.dot + 1}
|
||||
// Preserve the current repetition frame count across the sub-parse so
|
||||
// that when it returns we resume the loop with the right count.
|
||||
v := p.create(ret, u, i, w, rc)
|
||||
if p.sg.nts[s.nt].isRep {
|
||||
// Native counted repetition: a self-reference inside the rep's own
|
||||
// loop alternate continues the chain (count+1); any other reference
|
||||
// starts a fresh chain at count 0.
|
||||
childRC := 0
|
||||
if s.nt == L.nt {
|
||||
childRC = capRC(rc, p.sg.nts[s.nt])
|
||||
}
|
||||
p.addRepAlts(s.nt, v, i, childRC)
|
||||
} else {
|
||||
for ai := range p.sg.nts[s.nt].alts {
|
||||
p.addAlt(slot{s.nt, ai, 0}, v, i, 0)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
j := p.matchTerm(s, i)
|
||||
if j < 0 {
|
||||
p.noteFail(i, s)
|
||||
return
|
||||
}
|
||||
cR := p.getNodeT(i, j)
|
||||
next := slot{L.nt, L.alt, L.dot + 1}
|
||||
w = p.getNodeP(next, w, cR)
|
||||
i = j
|
||||
L = next
|
||||
}
|
||||
}
|
||||
|
||||
// noteFail records that a terminal match failed at input offset i. It maintains
|
||||
// the furthest such offset and, for that offset only, the set of terminals that
|
||||
// could have been consumed -- the classic "furthest-failure" position used to
|
||||
// report where parsing got stuck.
|
||||
func (p *gllParser) noteFail(i int, s ssym) {
|
||||
if i > p.maxPos {
|
||||
p.maxPos = i
|
||||
p.expected = p.expected[:0]
|
||||
p.expectedSeen = nil
|
||||
}
|
||||
if i != p.maxPos {
|
||||
return
|
||||
}
|
||||
desc := "?"
|
||||
if str, ok := s.term.(fmt.Stringer); ok {
|
||||
desc = str.String()
|
||||
}
|
||||
if p.expectedSeen == nil {
|
||||
p.expectedSeen = map[string]bool{}
|
||||
}
|
||||
if !p.expectedSeen[desc] {
|
||||
p.expectedSeen[desc] = true
|
||||
p.expected = append(p.expected, desc)
|
||||
}
|
||||
}
|
||||
|
||||
// capRC returns the count after consuming one more element of a repetition R
|
||||
// currently at count rc. For an unbounded repetition it saturates at repMin
|
||||
// (once the minimum is met the exact count no longer matters), which keeps the
|
||||
// reachable counts -- and hence the GLL state -- finite even when the element is
|
||||
// nullable.
|
||||
func capRC(rc int, nt *sgNT) int {
|
||||
next := rc + 1
|
||||
if nt.repMax == inf && next > nt.repMin {
|
||||
next = nt.repMin
|
||||
}
|
||||
return next
|
||||
}
|
||||
|
||||
// addRepAlts seeds the two alternates of a repetition nonterminal, gated by the
|
||||
// current count rc: it may stop (alt 0) only once the minimum is met, and may
|
||||
// consume another element (alt 1) only while under the maximum. This enforces
|
||||
// the {repMin,repMax} bound in parser state instead of unrolling it into the
|
||||
// grammar, mirroring the recognizer's c>=min / c<max gates. rc is carried in the
|
||||
// GSS key so two derivations that reach the same point with different committed
|
||||
// counts are kept distinct (no over-/under-counting via shared continuations).
|
||||
func (p *gllParser) addRepAlts(ntID int, v *gssNode, i, rc int) {
|
||||
nt := p.sg.nts[ntID]
|
||||
if rc >= nt.repMin {
|
||||
p.addAlt(slot{ntID, 0, 0}, v, i, rc) // stop: alt 0 == [eps]
|
||||
}
|
||||
if nt.repMax == inf || rc < nt.repMax {
|
||||
p.addAlt(slot{ntID, 1, 0}, v, i, rc) // loop: alt 1 == [element, self]
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Public API
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// Forest is a shared packed parse forest produced by ParseForest.
|
||||
type Forest struct {
|
||||
sg *slotGrammar
|
||||
input []byte
|
||||
rulename string
|
||||
root *gnode
|
||||
built int
|
||||
|
||||
// maxPos / expected carry the furthest-failure diagnostics from the parse,
|
||||
// surfaced through ParseError when the forest is invalid.
|
||||
maxPos int
|
||||
expected []string
|
||||
}
|
||||
|
||||
// ForestOption configures ParseForest.
|
||||
type ForestOption func(*forestConfig)
|
||||
|
||||
type forestConfig struct {
|
||||
maxNodes int
|
||||
maxSlots int
|
||||
}
|
||||
|
||||
// WithMaxForestNodes bounds the number of SPPF nodes the forest may allocate.
|
||||
// Because a complete SPPF is worst-case cubic in the input length, this caps the
|
||||
// time and memory ParseForest can spend on adversarial input. 0 means unbounded.
|
||||
func WithMaxForestNodes(n int) ForestOption { return func(c *forestConfig) { c.maxNodes = n } }
|
||||
|
||||
// WithMaxSlots bounds the number of nonterminals produced while lowering the
|
||||
// grammar. A finite repetition m*n lowers to ~n nonterminals, so this caps the
|
||||
// cost of an adversarial grammar such as a = 9999999999"x". 0 means unbounded;
|
||||
// the default is 65536.
|
||||
func WithMaxSlots(n int) ForestOption { return func(c *forestConfig) { c.maxSlots = n } }
|
||||
|
||||
// ErrForestTooLarge is returned by ParseForest when the SPPF exceeds the
|
||||
// configured WithMaxForestNodes budget.
|
||||
type ErrForestTooLarge struct{ Max int }
|
||||
|
||||
func (e *ErrForestTooLarge) Error() string {
|
||||
return "abnf: parse forest exceeded " + strconv.Itoa(e.Max) + " nodes"
|
||||
}
|
||||
|
||||
// ErrGrammarTooLarge is returned by ParseForest when lowering the grammar would
|
||||
// exceed the WithMaxSlots budget, typically because of an absurd repetition
|
||||
// bound (e.g. a = 9999999999"x").
|
||||
type ErrGrammarTooLarge struct{ Max int }
|
||||
|
||||
func (e *ErrGrammarTooLarge) Error() string {
|
||||
return "abnf: grammar lowering exceeded " + strconv.Itoa(e.Max) + " nonterminals"
|
||||
}
|
||||
|
||||
func newGLLParser(sg *slotGrammar, input []byte, maxNodes int) *gllParser {
|
||||
return &gllParser{
|
||||
sg: sg,
|
||||
input: input,
|
||||
nodes: map[nodeKey]*gnode{},
|
||||
gss: map[gssKey]*gssNode{},
|
||||
seen: map[descriptor]bool{},
|
||||
popped: map[*gssNode]map[*gnode]bool{},
|
||||
maxNodes: maxNodes,
|
||||
}
|
||||
}
|
||||
|
||||
// ParseForest parses input with grammar starting at rootRulename using GLL,
|
||||
// returning the shared packed parse forest. It runs in worst-case O(n^3) time
|
||||
// and space for ANY grammar -- left/right/mutual recursion and ambiguity
|
||||
// included -- so it is safe on untrusted or malformed grammars.
|
||||
func ParseForest(input []byte, grammar *Grammar, rootRulename string, opts ...ForestOption) (*Forest, error) {
|
||||
cfg := forestConfig{maxSlots: defaultMaxSlots}
|
||||
for _, o := range opts {
|
||||
o(&cfg)
|
||||
}
|
||||
sg, err := compileSlots(grammar, rootRulename, cfg.maxSlots)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
p := newGLLParser(sg, input, cfg.maxNodes)
|
||||
root := p.parse()
|
||||
if p.aborted {
|
||||
return nil, &ErrForestTooLarge{Max: cfg.maxNodes}
|
||||
}
|
||||
return &Forest{sg: sg, input: input, rulename: rootRulename, root: root, built: len(p.nodes), maxPos: p.maxPos, expected: p.expected}, nil
|
||||
}
|
||||
|
||||
// Valid reports whether the whole input is derivable by the root rule.
|
||||
func (f *Forest) Valid() bool { return f.root != nil }
|
||||
|
||||
// ParseError returns the furthest-failure diagnostic for an invalid forest:
|
||||
// where parsing got stuck and which terminals were expected there. It returns
|
||||
// nil when the forest is valid. The returned error unwraps to
|
||||
// [ErrNoSolutionFound].
|
||||
func (f *Forest) ParseError() *ParseError {
|
||||
if f.Valid() {
|
||||
return nil
|
||||
}
|
||||
return newParseError(f.input, f.maxPos, f.expected)
|
||||
}
|
||||
|
||||
// Nodes returns the number of forest nodes reachable from the root.
|
||||
func (f *Forest) Nodes() int {
|
||||
if f.root == nil {
|
||||
return 0
|
||||
}
|
||||
seen := map[*gnode]bool{}
|
||||
var walk func(n *gnode)
|
||||
walk = func(n *gnode) {
|
||||
if seen[n] {
|
||||
return
|
||||
}
|
||||
seen[n] = true
|
||||
for _, pk := range n.packs {
|
||||
for _, c := range pk.kids {
|
||||
walk(c)
|
||||
}
|
||||
}
|
||||
}
|
||||
walk(f.root)
|
||||
return len(seen)
|
||||
}
|
||||
|
||||
// Ambiguous reports whether the input has more than one distinct parse tree.
|
||||
func (f *Forest) Ambiguous() bool {
|
||||
if f.root == nil {
|
||||
return false
|
||||
}
|
||||
seen := map[*gnode]bool{}
|
||||
var walk func(n *gnode) bool
|
||||
walk = func(n *gnode) bool {
|
||||
if seen[n] {
|
||||
return false
|
||||
}
|
||||
seen[n] = true
|
||||
if len(n.packs) > 1 {
|
||||
return true
|
||||
}
|
||||
for _, pk := range n.packs {
|
||||
for _, c := range pk.kids {
|
||||
if walk(c) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
return walk(f.root)
|
||||
}
|
||||
|
||||
// NumTrees returns the number of distinct parse trees the forest represents.
|
||||
// A result of -1 means infinitely many (a cycle in the forest, i.e. an
|
||||
// infinitely-ambiguous grammar such as A = A / "x").
|
||||
func (f *Forest) NumTrees() *big.Int {
|
||||
if f.root == nil {
|
||||
return big.NewInt(0)
|
||||
}
|
||||
memo := map[*gnode]*big.Int{}
|
||||
onStack := map[*gnode]bool{}
|
||||
infinite := false
|
||||
var count func(n *gnode) *big.Int
|
||||
count = func(n *gnode) *big.Int {
|
||||
if v, ok := memo[n]; ok {
|
||||
return v
|
||||
}
|
||||
if onStack[n] {
|
||||
infinite = true
|
||||
return big.NewInt(1)
|
||||
}
|
||||
onStack[n] = true
|
||||
total := big.NewInt(0)
|
||||
if len(n.packs) == 0 {
|
||||
total = big.NewInt(1)
|
||||
}
|
||||
for _, pk := range n.packs {
|
||||
prod := big.NewInt(1)
|
||||
for _, c := range pk.kids {
|
||||
prod.Mul(prod, count(c))
|
||||
}
|
||||
total.Add(total, prod)
|
||||
}
|
||||
onStack[n] = false
|
||||
memo[n] = total
|
||||
return total
|
||||
}
|
||||
res := count(f.root)
|
||||
if infinite {
|
||||
return big.NewInt(-1)
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
// ParseTree is a concrete syntax tree keyed by grammar rules: rule nodes carry
|
||||
// the rule name, terminals are leaves with empty Rule.
|
||||
type ParseTree struct {
|
||||
Rule string
|
||||
Start, End int
|
||||
Children []*ParseTree
|
||||
}
|
||||
|
||||
// Tree extracts a single parse tree (first packing at each node), or nil if the
|
||||
// input is invalid. A visited guard keeps extraction finite on cyclic forests.
|
||||
func (f *Forest) Tree() *ParseTree {
|
||||
if f.root == nil {
|
||||
return nil
|
||||
}
|
||||
return f.emitSymbol(f.root, map[*gnode]bool{})
|
||||
}
|
||||
|
||||
func (f *Forest) emitSymbol(n *gnode, onStack map[*gnode]bool) *ParseTree {
|
||||
t := &ParseTree{Start: n.Start, End: n.End}
|
||||
if n.kind == gSymbol && n.nt >= 0 && f.sg.nts[n.nt].isRule {
|
||||
t.Rule = f.sg.nts[n.nt].ruleName
|
||||
}
|
||||
onStack[n] = true
|
||||
f.collect(n, &t.Children, onStack)
|
||||
delete(onStack, n)
|
||||
return t
|
||||
}
|
||||
|
||||
func (f *Forest) collect(n *gnode, into *[]*ParseTree, onStack map[*gnode]bool) {
|
||||
if len(n.packs) == 0 {
|
||||
if n.kind == gTerm && n.End > n.Start {
|
||||
*into = append(*into, &ParseTree{Start: n.Start, End: n.End})
|
||||
}
|
||||
return
|
||||
}
|
||||
for _, c := range n.packs[0].kids {
|
||||
f.collectChild(c, into, onStack)
|
||||
}
|
||||
}
|
||||
|
||||
func (f *Forest) collectChild(c *gnode, into *[]*ParseTree, onStack map[*gnode]bool) {
|
||||
switch {
|
||||
case c.kind == gTerm:
|
||||
if c.End > c.Start {
|
||||
*into = append(*into, &ParseTree{Start: c.Start, End: c.End})
|
||||
}
|
||||
case c.kind == gSymbol && c.nt >= 0 && f.sg.nts[c.nt].isRule:
|
||||
if onStack[c] {
|
||||
*into = append(*into, &ParseTree{Rule: f.sg.nts[c.nt].ruleName, Start: c.Start, End: c.End})
|
||||
return
|
||||
}
|
||||
*into = append(*into, f.emitSymbol(c, onStack))
|
||||
default:
|
||||
if onStack[c] {
|
||||
return
|
||||
}
|
||||
onStack[c] = true
|
||||
f.collect(c, into, onStack)
|
||||
delete(onStack, c)
|
||||
}
|
||||
}
|
||||
+1546
File diff suppressed because it is too large.
Load diff
+152
@@ -0,0 +1,152 @@
|
||||
package goabnf
|
||||
|
||||
import (
|
||||
"math"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// int32ToNumval converts an integer value into its numeric-string representation
|
||||
// in the given base. It is the inverse of numvalToInt32 and produces the form
|
||||
// stored in an ElemNumVal's Elems (e.g. 97 in base "x" -> "61").
|
||||
func int32ToNumval(v int32, base string) string {
|
||||
switch base {
|
||||
case "B", "b":
|
||||
return strconv.FormatInt(int64(v), 2)
|
||||
case "D", "d":
|
||||
return strconv.FormatInt(int64(v), 10)
|
||||
case "X", "x":
|
||||
return strings.ToUpper(strconv.FormatInt(int64(v), 16))
|
||||
}
|
||||
return strconv.FormatInt(int64(v), 10)
|
||||
}
|
||||
|
||||
// baseRadix maps an ABNF num-val base marker (bin/dec/hex) to its integer radix.
|
||||
func baseRadix(base string) int {
|
||||
switch base {
|
||||
case "B", "b":
|
||||
return 2
|
||||
case "D", "d":
|
||||
return 10
|
||||
case "X", "x":
|
||||
return 16
|
||||
}
|
||||
return 10
|
||||
}
|
||||
|
||||
// numvalToUint64 parses a num-val literal in the given base into its numeric
|
||||
// value. ok is false only when the literal does not fit in 64 bits (or is
|
||||
// malformed), which lets callers degrade gracefully instead of overflowing
|
||||
// silently. This is the single overflow-safe entry point every other conversion
|
||||
// builds on.
|
||||
//
|
||||
// A num-val is a plain non-negative integer, not intrinsically a Unicode scalar:
|
||||
// values beyond U+10FFFF are legitimate (e.g. for non-textual / on-wire
|
||||
// grammars). Unicode is handled as the common case at match time; this layer is
|
||||
// no longer Unicode-bounded.
|
||||
func numvalToUint64(str, base string) (uint64, bool) {
|
||||
v, err := strconv.ParseUint(str, baseRadix(base), 64)
|
||||
if err != nil {
|
||||
return 0, false
|
||||
}
|
||||
return v, true
|
||||
}
|
||||
|
||||
// numvalToInt32 converts a num-val into an int32 codepoint. It never panics: a
|
||||
// value that does not fit a positive int32 is saturated to math.MaxInt32, which
|
||||
// is above the maximum Unicode scalar (U+10FFFF) and therefore matches no decoded
|
||||
// rune. A range bound that merely straddles the Unicode ceiling still covers its
|
||||
// representable (Unicode) subset, because the saturated upper bound stays above
|
||||
// every rune.
|
||||
func numvalToInt32(str, base string) int32 {
|
||||
v, ok := numvalToUint64(str, base)
|
||||
if !ok || v > math.MaxInt32 {
|
||||
return math.MaxInt32
|
||||
}
|
||||
return int32(v)
|
||||
}
|
||||
|
||||
// numvalToRune converts a num-val into the corresponding rune. It never panics;
|
||||
// a value beyond the Unicode range yields a rune that no UTF-8 input can produce,
|
||||
// so callers matching against decoded runes simply find no match. Callers that
|
||||
// require text validity (e.g. a num-val series, which must denote real
|
||||
// characters) should additionally check utf8.ValidRune.
|
||||
func numvalToRune(str, base string) rune {
|
||||
return rune(numvalToInt32(str, base))
|
||||
}
|
||||
|
||||
// checkBounds enforces the Unicode-range invariant (value <= U+10FFFF). It is
|
||||
// the strict-validation path used by SemvalABNF: with validation enabled (the
|
||||
// default, and the common textual case) a num-val outside the Unicode range is
|
||||
// rejected. The conversion helpers above are deliberately broader -- a num-val is
|
||||
// a plain integer and larger values are legitimate for non-textual grammars -- so
|
||||
// without validation such values are accepted and handled (matched as no rune).
|
||||
func checkBounds(str, base string) error {
|
||||
str = strings.TrimLeft(str, "0")
|
||||
switch base {
|
||||
// Whatever the base, the higher value we arbitrary decide to support is
|
||||
// the maximal Unicode character i.e. U+10FFFF.
|
||||
//
|
||||
// Every value (represented as integer) is then in interval [0;1_114_111].
|
||||
// For each base, we can compute the number of characters of an acceptable
|
||||
// num-val as ceil(log(base,1_114_111)).
|
||||
//
|
||||
// We also have to ensure that it does not go over the boundaries even if
|
||||
// of an acceptable length, e.g., 0xFFFFFF is not.
|
||||
|
||||
case "B", "b":
|
||||
// 21 = ceil(log(2, max_unicode))
|
||||
// U+10FFFF = Ob100001111111111111111
|
||||
if len(str) > 21 ||
|
||||
(len(str) == 21 && (str[1] != '0' || str[2] != '0' || str[3] != '0' || str[4] != '0')) {
|
||||
return &ErrTooLargeNumeral{
|
||||
Base: base,
|
||||
Value: str,
|
||||
}
|
||||
}
|
||||
|
||||
case "D", "d":
|
||||
// 7 = ceil(log(10, max_unicode))
|
||||
// U+10FFFF = 0d1114111
|
||||
if len(str) > 7 ||
|
||||
(len(str) == 7 && (str[0] > '1' || (str[0] == '1' && (str[1] > '1' || (str[1] == '1' && (str[2] > '1' || (str[2] == '1' && (str[3] > '4' || (str[3] == '4' && (str[4] > '1' || (str[4] == '1' && (str[5] > '1' || (str[5] == '1' && str[6] > '1'))))))))))))) {
|
||||
return &ErrTooLargeNumeral{
|
||||
Base: base,
|
||||
Value: str,
|
||||
}
|
||||
}
|
||||
|
||||
case "X", "x":
|
||||
// 6 = ceil(log(16, max_unicode))
|
||||
// U+10FFFF = 0x10FFFF
|
||||
if len(str) > 6 ||
|
||||
(len(str) == 6 && (str[0] > '1' || (str[0] == '1' && (str[1] > '0')))) {
|
||||
return &ErrTooLargeNumeral{
|
||||
Base: base,
|
||||
Value: str,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetRule returns the rule by the given rulename, whether
|
||||
// it is a core rule or present in the grammar, or nil if not found.
|
||||
// It validates the RFC 5234 Section 2.1 "rule names are case insensitive".
|
||||
func GetRule(rulename string, rulemap map[string]*Rule) *Rule {
|
||||
// First look in the newly defined rules, as we could override definitions
|
||||
if rule := getRuleIn(rulename, rulemap); rule != nil {
|
||||
return rule
|
||||
}
|
||||
return getRuleIn(rulename, coreRules)
|
||||
}
|
||||
|
||||
func getRuleIn(rulename string, rulemap map[string]*Rule) *Rule {
|
||||
for _, rule := range rulemap {
|
||||
if strings.EqualFold(rulename, rule.Name) {
|
||||
return rule
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
+41
-9
@@ -65,6 +65,16 @@ func ResponseFormat(h http.Header) Format {
|
||||
return FmtUnknown
|
||||
}
|
||||
return FmtText
|
||||
|
||||
case OpenMetricsType:
|
||||
switch params["version"] {
|
||||
case "", OpenMetricsVersion_0_0_1:
|
||||
return FmtOpenMetrics_0_0_1
|
||||
case OpenMetricsVersion_1_0_0:
|
||||
return FmtOpenMetrics_1_0_0
|
||||
default:
|
||||
return FmtUnknown
|
||||
}
|
||||
}
|
||||
|
||||
return FmtUnknown
|
||||
@@ -73,14 +83,12 @@ func ResponseFormat(h http.Header) Format {
|
||||
// NewDecoder returns a new decoder based on the given input format. Metric
|
||||
// names are validated based on the provided Format -- if the format requires
|
||||
// escaping, traditional Prometheus validity checking is used. Otherwise, names
|
||||
// are checked for UTF-8 validity. Supported formats include delimited protobuf
|
||||
// and Prometheus text format. For historical reasons, this decoder falls back
|
||||
// to classic text decoding for other legacy formats, but returns an error for
|
||||
// unsupported formats such as OpenMetrics 2.0. This decoder does not fully
|
||||
// support OpenMetrics although it may often succeed for OpenMetrics 1.0 due to
|
||||
// the similarities between the formats. This decoder may not support the latest
|
||||
// features of Prometheus text format and is not intended for high-performance
|
||||
// applications.
|
||||
// are checked for UTF-8 validity. Supported formats include delimited protobuf,
|
||||
// Prometheus text format, and OpenMetrics 0.0.1 / 1.0.0. For historical
|
||||
// reasons, this decoder falls back to classic text decoding for other legacy
|
||||
// formats, but returns an error for unsupported formats such as OpenMetrics 2.0.
|
||||
// This decoder may not support the latest features of Prometheus text format
|
||||
// and is not intended for high-performance applications.
|
||||
// See: https://github.com/prometheus/common/issues/812
|
||||
func NewDecoder(r io.Reader, format Format) Decoder {
|
||||
scheme := model.LegacyValidation
|
||||
@@ -94,7 +102,13 @@ func NewDecoder(r io.Reader, format Format) Decoder {
|
||||
return &errDecoder{err: fmt.Errorf("format %s not supported for decoding", format)}
|
||||
case TypeOpenMetrics:
|
||||
_, params, err := mime.ParseMediaType(string(format))
|
||||
if err == nil && params["version"] == OpenMetricsVersion_2_0_0 {
|
||||
if err != nil {
|
||||
return &errDecoder{err: fmt.Errorf("format %s not supported for decoding", format)}
|
||||
}
|
||||
switch params["version"] {
|
||||
case "", OpenMetricsVersion_0_0_1, OpenMetricsVersion_1_0_0:
|
||||
return &openMetricsDecoder{p: newOpenMetricsParser(r, scheme)}
|
||||
default:
|
||||
return &errDecoder{err: fmt.Errorf("format %s not supported for decoding", format)}
|
||||
}
|
||||
}
|
||||
@@ -146,6 +160,24 @@ func (d *errDecoder) Decode(*dto.MetricFamily) error {
|
||||
return d.err
|
||||
}
|
||||
|
||||
// openMetricsDecoder implements the Decoder interface for the OpenMetrics 1.0 protocol.
|
||||
type openMetricsDecoder struct {
|
||||
p *openMetricsParser
|
||||
err error
|
||||
}
|
||||
|
||||
// Decode implements the Decoder interface.
|
||||
func (d *openMetricsDecoder) Decode(v *dto.MetricFamily) error {
|
||||
if d.err != nil {
|
||||
return d.err
|
||||
}
|
||||
if err := d.p.next(v); err != nil {
|
||||
d.err = err
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// textDecoder implements the Decoder interface for the text protocol.
|
||||
type textDecoder struct {
|
||||
r io.Reader
|
||||
|
||||
+19
-11
@@ -37,6 +37,7 @@ func init() {
|
||||
FmtProtoText,
|
||||
FmtProtoCompact,
|
||||
FmtOpenMetrics_1_0_0,
|
||||
FmtOpenMetrics_2_0_0_rc,
|
||||
FmtOpenMetrics_2_0_0,
|
||||
FmtOpenMetrics_0_0_1,
|
||||
} {
|
||||
@@ -130,21 +131,14 @@ func NegotiateIncludingOpenMetrics(h http.Header) Format {
|
||||
// If no accepted format matches the Accept header, it falls back to the text
|
||||
// format if present in the accepted list, or the first accepted format (or FmtText
|
||||
// if accepted is empty).
|
||||
// The escaping scheme is taken from the matching Accept clause. Without one,
|
||||
// OpenMetrics 2.0 gets allow-utf-8 and all other formats model.NameEscapingScheme.
|
||||
func NegotiateAccept(h http.Header, accepted ...Format) Format {
|
||||
escapingScheme := Format("; escaping=" + model.NameEscapingScheme.String())
|
||||
for _, ac := range goautoneg.ParseAccept(h.Get(hdrAccept)) {
|
||||
if escapeParam := ac.Params[model.EscapingKey]; escapeParam != "" {
|
||||
switch Format(escapeParam) {
|
||||
case model.AllowUTF8, model.EscapeUnderscores, model.EscapeDots, model.EscapeValues:
|
||||
escapingScheme = Format("; escaping=" + escapeParam)
|
||||
default:
|
||||
// If the escaping parameter is unknown, ignore it.
|
||||
}
|
||||
}
|
||||
|
||||
for _, f := range accepted {
|
||||
if matchFormat(ac, f) {
|
||||
return f + escapingScheme
|
||||
return f + escapingFor(ac, f)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -159,6 +153,20 @@ func NegotiateAccept(h http.Header, accepted ...Format) Format {
|
||||
return FmtText + escapingScheme
|
||||
}
|
||||
|
||||
// escapingFor returns the escaping term for the Accept clause that matched f.
|
||||
func escapingFor(ac goautoneg.Accept, f Format) Format {
|
||||
switch p := ac.Params[model.EscapingKey]; p {
|
||||
case model.AllowUTF8, model.EscapeUnderscores, model.EscapeDots, model.EscapeValues:
|
||||
// An explicit request always wins, also for OpenMetrics 2.0.
|
||||
return Format("; escaping=" + p)
|
||||
}
|
||||
if f.FormatType() == TypeOpenMetrics && isOpenMetrics20Version(f.Version()) {
|
||||
// OpenMetrics 2.0 is UTF-8 native, so asking for it implies UTF-8 name support.
|
||||
return Format("; escaping=" + model.AllowUTF8)
|
||||
}
|
||||
return Format("; escaping=" + model.NameEscapingScheme.String())
|
||||
}
|
||||
|
||||
// matchFormat checks if a parsed accept clause matches a given Format.
|
||||
func matchFormat(ac goautoneg.Accept, f Format) bool {
|
||||
target, ok := formatToAccept[f]
|
||||
@@ -255,7 +263,7 @@ func NewEncoder(w io.Writer, format Format, options ...EncoderOption) Encoder {
|
||||
}
|
||||
case TypeOpenMetrics:
|
||||
_, params, err := mime.ParseMediaType(string(format))
|
||||
if err == nil && params["version"] == OpenMetricsVersion_2_0_0 {
|
||||
if err == nil && isOpenMetrics20Version(params["version"]) {
|
||||
return encoderCloser{
|
||||
encode: func(v *dto.MetricFamily) error {
|
||||
_, err := MetricFamilyToOpenMetrics20(w, model.EscapeMetricFamily(v, escapingScheme), options...)
|
||||
|
||||
+18
-7
@@ -36,6 +36,8 @@ const (
|
||||
//nolint:revive // Allow for underscores.
|
||||
OpenMetricsVersion_1_0_0 = "1.0.0"
|
||||
//nolint:revive // Allow for underscores.
|
||||
OpenMetricsVersion_2_0_0_rc = "2.0.0-rc"
|
||||
//nolint:revive // Allow for underscores.
|
||||
OpenMetricsVersion_2_0_0 = "2.0.0"
|
||||
)
|
||||
|
||||
@@ -57,6 +59,8 @@ const (
|
||||
//nolint:revive // Allow for underscores.
|
||||
FmtOpenMetrics_2_0_0 Format = OpenMetricsType + `; version=` + OpenMetricsVersion_2_0_0 + `; charset=utf-8`
|
||||
//nolint:revive // Allow for underscores.
|
||||
FmtOpenMetrics_2_0_0_rc Format = OpenMetricsType + `; version=` + OpenMetricsVersion_2_0_0_rc + `; charset=utf-8`
|
||||
//nolint:revive // Allow for underscores.
|
||||
FmtOpenMetrics_1_0_0 Format = OpenMetricsType + `; version=` + OpenMetricsVersion_1_0_0 + `; charset=utf-8`
|
||||
//nolint:revive // Allow for underscores.
|
||||
FmtOpenMetrics_0_0_1 Format = OpenMetricsType + `; version=` + OpenMetricsVersion_0_0_1 + `; charset=utf-8`
|
||||
@@ -106,20 +110,27 @@ func NewFormat(t FormatType) Format {
|
||||
// NewOpenMetricsFormat generates a new OpenMetrics format matching the
|
||||
// specified version number.
|
||||
//
|
||||
// Note: OpenMetrics version 2.0.0 is experimental and encode-only (supporting
|
||||
// Note: OpenMetrics versions 2.0.0-rc and 2.0.0 are experimental and encode-only (supporting
|
||||
// counter, gauge, summary, untyped, histogram, and gaugehistogram metric types).
|
||||
func NewOpenMetricsFormat(version string) (Format, error) {
|
||||
if version == OpenMetricsVersion_0_0_1 {
|
||||
switch version {
|
||||
case OpenMetricsVersion_0_0_1:
|
||||
return FmtOpenMetrics_0_0_1, nil
|
||||
}
|
||||
if version == OpenMetricsVersion_1_0_0 {
|
||||
case OpenMetricsVersion_1_0_0:
|
||||
return FmtOpenMetrics_1_0_0, nil
|
||||
}
|
||||
if version == OpenMetricsVersion_2_0_0 {
|
||||
case OpenMetricsVersion_2_0_0_rc:
|
||||
// OpenMetrics 2.0.0-rc is experimental and encode-only.
|
||||
return FmtOpenMetrics_2_0_0_rc, nil
|
||||
case OpenMetricsVersion_2_0_0:
|
||||
// OpenMetrics 2.0.0 is experimental and encode-only.
|
||||
return FmtOpenMetrics_2_0_0, nil
|
||||
default:
|
||||
return FmtUnknown, errors.New("unknown open metrics version string")
|
||||
}
|
||||
return FmtUnknown, errors.New("unknown open metrics version string")
|
||||
}
|
||||
|
||||
func isOpenMetrics20Version(v string) bool {
|
||||
return v == OpenMetricsVersion_2_0_0 || v == OpenMetricsVersion_2_0_0_rc
|
||||
}
|
||||
|
||||
// Version returns the version parameter without validating whether it is supported.
|
||||
|
||||
+1118
File diff suppressed because it is too large.
Load diff
Vendored
+4
-1
@@ -472,6 +472,9 @@ github.com/opencontainers/runtime-spec/specs-go
|
||||
github.com/opencontainers/selinux/go-selinux
|
||||
github.com/opencontainers/selinux/go-selinux/label
|
||||
github.com/opencontainers/selinux/pkg/pwalkdir
|
||||
# github.com/pandatix/go-abnf v0.5.5
|
||||
## explicit; go 1.26.0
|
||||
github.com/pandatix/go-abnf
|
||||
# github.com/peterbourgon/diskv/v3 v3.0.1
|
||||
## explicit; go 1.12
|
||||
github.com/peterbourgon/diskv/v3
|
||||
@@ -493,7 +496,7 @@ github.com/prometheus/client_golang/prometheus/testutil/promlint/validations
|
||||
# github.com/prometheus/client_model v0.6.3
|
||||
## explicit; go 1.23
|
||||
github.com/prometheus/client_model/go
|
||||
# github.com/prometheus/common v0.72.0
|
||||
# github.com/prometheus/common v0.73.1
|
||||
## explicit; go 1.26.0
|
||||
github.com/prometheus/common/expfmt
|
||||
github.com/prometheus/common/model
|
||||
|
||||
Reference in new issue
Block a user