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:
Davanum Srinivas committed 2026-11-02 16:06:23 -05:00
1 parent 2fa0dff0d5
commit 787dd27af4
63 files changed
+11590 -72

No files matched your search

+25
View File
@@ -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
+2 -1
View File
@@ -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
+4 -2
View File
@@ -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=
+2 -1
View File
@@ -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
+4 -2
View File
@@ -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
View File
@@ -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=
+2 -1
View File
@@ -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
+4 -2
View File
@@ -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=
+2 -1
View File
@@ -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
+4 -2
View File
@@ -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=
+2 -1
View File
@@ -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
+4 -2
View File
@@ -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=
+2 -1
View File
@@ -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
View File
@@ -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=
+2 -1
View File
@@ -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
View File
@@ -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=
+2 -1
View File
@@ -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
+4 -2
View File
@@ -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=
+2 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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=
+4 -2
View File
@@ -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=
+2 -1
View File
@@ -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=
+2 -1
View File
@@ -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=
+4 -2
View File
@@ -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
View File
@@ -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=
+2 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -0,0 +1,6 @@
go 1.26.0
use (
.
cmd/pap
)
+11
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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 := &regexOptions{}
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
View File
File diff suppressed because it is too large. Load diff
+902
View File
@@ -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)
}
}
File diff suppressed because it is too large. Load diff
+152
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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.
File diff suppressed because it is too large. Load diff
+4 -1
View File
@@ -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