Rename grammar's NameRead to PathRead and CheckName to CheckIdentifier, so a name in the engine is only a bound name
This commit is contained in:
@@ -989,7 +989,7 @@ internal/builtinfunc/ the {name()} functions: their checks and draws, checksums,
|
|||||||
internal/datafiles/ the walk of a data tree: its folders, its category files and the rows files beside them
|
internal/datafiles/ the walk of a data tree: its folders, its category files and the rows files beside them
|
||||||
internal/datatype/ the datatype a record column holds; the root's DataType is an alias of it
|
internal/datatype/ the datatype a record column holds; the root's DataType is an alias of it
|
||||||
internal/drawstate/ the seeded randomness and the {seq()} counters a generator draws through
|
internal/drawstate/ the seeded randomness and the {seq()} counters a generator draws through
|
||||||
internal/grammar/ how the template language is written: format tokens, paths and selectors, reference sigils, names, calc syntax, and whether an argument is a template or a path
|
internal/grammar/ how the template language is written: format tokens, paths and selectors, reference sigils, identifiers, calc syntax, and whether an argument is a template or a path
|
||||||
internal/invariant/ the one phrase every package panics with when an invariant breaks
|
internal/invariant/ the one phrase every package panics with when an invariant breaks
|
||||||
internal/proven/ what a proof knows of a value, and the bounds a calc takes from its operands
|
internal/proven/ what a proof knows of a value, and the bounds a calc takes from its operands
|
||||||
internal/rows/ a table's rows: the TSV, the options proved over it, the links between tables, row selection and draws, and the pin set one path or one named pick fixes
|
internal/rows/ a table's rows: the TSV, the options proved over it, the links between tables, row selection and draws, and the pin set one path or one named pick fixes
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ func checkSegments(a arm) error {
|
|||||||
// or a builtin already prepared with its args.
|
// or a builtin already prepared with its args.
|
||||||
type op struct {
|
type op struct {
|
||||||
grammar.Token
|
grammar.Token
|
||||||
arms []arm // grammar.NameRead: the '|' alternatives, split into head and tail once
|
arms []arm // grammar.PathRead: the '|' alternatives, split into head and tail once
|
||||||
call builtinfunc.Call
|
call builtinfunc.Call
|
||||||
// operands are the fields the builtin reads, in the order its operands func
|
// operands are the fields the builtin reads, in the order its operands func
|
||||||
// fixed; expand reads them before the call. nil for a builtin that reads none.
|
// fixed; expand reads them before the call. nil for a builtin that reads none.
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ func checkToken(t grammar.Token, fields map[string]node) ([]unboundRead, error)
|
|||||||
return checkReads(t, fields, true)
|
return checkReads(t, fields, true)
|
||||||
case grammar.NameBind:
|
case grammar.NameBind:
|
||||||
return nil, checkBind(t, fields)
|
return nil, checkBind(t, fields)
|
||||||
case grammar.NameRead:
|
case grammar.PathRead:
|
||||||
unbound, err := checkReads(t, fields, false)
|
unbound, err := checkReads(t, fields, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -113,7 +113,7 @@ func checkBind(t grammar.Token, fields map[string]node) error {
|
|||||||
if isOption(name) {
|
if isOption(name) {
|
||||||
return fmt.Errorf("token {%s}: %q is an option and can never be a name; rename it", t.Body, name)
|
return fmt.Errorf("token {%s}: %q is an option and can never be a name; rename it", t.Body, name)
|
||||||
}
|
}
|
||||||
if err := grammar.CheckName(name); err != nil {
|
if err := grammar.CheckIdentifier(name); err != nil {
|
||||||
return fmt.Errorf("token {%s}: name %w", t.Body, err)
|
return fmt.Errorf("token {%s}: name %w", t.Body, err)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@@ -167,7 +167,7 @@ func checkArm(name string, fields map[string]node, wholeToken bool) (missing boo
|
|||||||
// hintableRef reports whether {/name} is a reference the grammar accepts, so the
|
// hintableRef reports whether {/name} is a reference the grammar accepts, so the
|
||||||
// hint never names a spelling that fails too.
|
// hint never names a spelling that fails too.
|
||||||
func hintableRef(name string) bool {
|
func hintableRef(name string) bool {
|
||||||
return grammar.CheckPathNames(name) == nil
|
return grammar.CheckPathIdentifiers(name) == nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// tokenReads lists the names t reads: a read's arms, or the operands a call's builtin reads.
|
// tokenReads lists the names t reads: a read's arms, or the operands a call's builtin reads.
|
||||||
@@ -214,7 +214,7 @@ func compileFormat(t *template, targets map[*nameBinding]nameTarget) (formatOps,
|
|||||||
case grammar.BuiltinCall:
|
case grammar.BuiltinCall:
|
||||||
o.call = builtinfunc.Prep(tok.Fn, tok.Args)
|
o.call = builtinfunc.Prep(tok.Fn, tok.Args)
|
||||||
o.operands, err = t.compileArms(builtinfunc.Operands(tok.Fn, tok.Args), targets)
|
o.operands, err = t.compileArms(builtinfunc.Operands(tok.Fn, tok.Args), targets)
|
||||||
case grammar.NameRead:
|
case grammar.PathRead:
|
||||||
o.arms, err = t.compileArms(tok.Arms, targets)
|
o.arms, err = t.compileArms(tok.Arms, targets)
|
||||||
case grammar.NameBind:
|
case grammar.NameBind:
|
||||||
continue
|
continue
|
||||||
|
|||||||
@@ -115,7 +115,7 @@ func (s Source) walkFolder(dir []string, compile func(Category) error) (bool, er
|
|||||||
if err != nil || !holds {
|
if err != nil || !holds {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
if err := grammar.CheckName(dir[len(dir)-1]); err != nil {
|
if err := grammar.CheckIdentifier(dir[len(dir)-1]); err != nil {
|
||||||
return false, fmt.Errorf("%s: folder %w", s.labelled(s.dirPath(dir)), err)
|
return false, fmt.Errorf("%s: folder %w", s.labelled(s.dirPath(dir)), err)
|
||||||
}
|
}
|
||||||
return true, nil
|
return true, nil
|
||||||
@@ -140,7 +140,7 @@ func (s Source) compileFile(dir []string, file string, rows *rowsFiles, compile
|
|||||||
}
|
}
|
||||||
full := path.Join(rows.dir, file)
|
full := path.Join(rows.dir, file)
|
||||||
name := strings.TrimSuffix(file, ".json")
|
name := strings.TrimSuffix(file, ".json")
|
||||||
if err := grammar.CheckName(name); err != nil {
|
if err := grammar.CheckIdentifier(name); err != nil {
|
||||||
return false, fmt.Errorf("%s: category %w", s.labelled(full), err)
|
return false, fmt.Errorf("%s: category %w", s.labelled(full), err)
|
||||||
}
|
}
|
||||||
b, err := fs.ReadFile(s.fsys, full)
|
b, err := fs.ReadFile(s.fsys, full)
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ type Token struct {
|
|||||||
Body string // the braces' content, as written
|
Body string // the braces' content, as written
|
||||||
Fn string // BuiltinCall
|
Fn string // BuiltinCall
|
||||||
Args []string // BuiltinCall
|
Args []string // BuiltinCall
|
||||||
Arms []string // NameRead: the '|' alternatives
|
Arms []string // PathRead: the '|' alternatives
|
||||||
// NameBind: the reference it binds, and the name.
|
// NameBind: the reference it binds, and the name.
|
||||||
BoundRef, Bound string
|
BoundRef, Bound string
|
||||||
}
|
}
|
||||||
@@ -77,7 +77,7 @@ const (
|
|||||||
BuiltinCall TokenKind = iota + 1
|
BuiltinCall TokenKind = iota + 1
|
||||||
LiteralRun
|
LiteralRun
|
||||||
NameBind
|
NameBind
|
||||||
NameRead
|
PathRead
|
||||||
)
|
)
|
||||||
|
|
||||||
const AsWord = " as "
|
const AsWord = " as "
|
||||||
@@ -104,7 +104,7 @@ func ParseFormat(format string) ([]Token, error) {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if indexOutside(u.body, '(') < 0 {
|
if indexOutside(u.body, '(') < 0 {
|
||||||
toks = append(toks, Token{Kind: NameRead, Body: u.body, Arms: splitOutside(u.body, '|')})
|
toks = append(toks, Token{Kind: PathRead, Body: u.body, Arms: splitOutside(u.body, '|')})
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
name, args, ok := FuncCall(u.body)
|
name, args, ok := FuncCall(u.body)
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ func TestParseFormatReadsEachTokenKind(t *testing.T) {
|
|||||||
}
|
}
|
||||||
want := []Token{
|
want := []Token{
|
||||||
{Kind: LiteralRun, Lit: "a{"},
|
{Kind: LiteralRun, Lit: "a{"},
|
||||||
{Kind: NameRead, Body: "x|/y.z[a.b]", Arms: []string{"x", "/y.z[a.b]"}},
|
{Kind: PathRead, Body: "x|/y.z[a.b]", Arms: []string{"x", "/y.z[a.b]"}},
|
||||||
{Kind: NameBind, Body: "/p as q", BoundRef: "/p", Bound: "q"},
|
{Kind: NameBind, Body: "/p as q", BoundRef: "/p", Bound: "q"},
|
||||||
{Kind: BuiltinCall, Body: "f(1, n)", Fn: "f", Args: []string{"1", "n"}},
|
{Kind: BuiltinCall, Body: "f(1, n)", Fn: "f", Args: []string{"1", "n"}},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,18 +5,18 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
// reservedInName is what a category, folder or field name may not contain: a dot
|
// reservedInIdentifier is what an identifier may not contain: a dot
|
||||||
// separates the segments of a path, '|' the arms of a token, '(' opens a function
|
// separates the segments of a path, '|' the arms of a token, '(' opens a function
|
||||||
// call, braces delimit the token, '/' starts a reference, and brackets and a quote
|
// call, braces delimit the token, '/' starts a reference, and brackets and a quote
|
||||||
// open a JSON value. A name carrying one is rejected where it is authored rather
|
// open a JSON value. An identifier carrying one is rejected where it is authored rather
|
||||||
// than where it would be unreachable.
|
// than where it would be unreachable.
|
||||||
const reservedInName = ".|({}/[]\""
|
const reservedInIdentifier = ".|({}/[]\""
|
||||||
|
|
||||||
var reservedList = strings.Join(strings.Split(reservedInName, ""), " ")
|
var reservedList = strings.Join(strings.Split(reservedInIdentifier, ""), " ")
|
||||||
|
|
||||||
// CheckName rejects a name the dot path, {token} and JSON grammars cannot spell, or a struct
|
// CheckIdentifier rejects an identifier, the name of a folder, category, field or column, that the
|
||||||
// tag cannot read.
|
// dot path, {token} and JSON grammars cannot spell, or a struct tag cannot read.
|
||||||
func CheckName(name string) error {
|
func CheckIdentifier(name string) error {
|
||||||
if name == "" {
|
if name == "" {
|
||||||
return fmt.Errorf("%q is empty, which is not a path segment, so List never offers it", name)
|
return fmt.Errorf("%q is empty, which is not a path segment, so List never offers it", name)
|
||||||
}
|
}
|
||||||
@@ -26,21 +26,21 @@ func CheckName(name string) error {
|
|||||||
if strings.Contains(name, AsWord) {
|
if strings.Contains(name, AsWord) {
|
||||||
return fmt.Errorf("%q contains %q, which a token reads as binding a name; rename it", name, AsWord)
|
return fmt.Errorf("%q contains %q, which a token reads as binding a name; rename it", name, AsWord)
|
||||||
}
|
}
|
||||||
if i := strings.IndexAny(name, reservedInName); i >= 0 {
|
if i := strings.IndexAny(name, reservedInIdentifier); i >= 0 {
|
||||||
return fmt.Errorf("%q contains %q; a name may not use %s, which the dot path, {token} and JSON grammars reserve",
|
return fmt.Errorf("%q contains %q; a name may not use %s, which the dot path, {token} and JSON grammars reserve",
|
||||||
name, name[i:i+1], reservedList)
|
name, name[i:i+1], reservedList)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// CheckPathNames rejects a dotted path with a segment no name may be.
|
// CheckPathIdentifiers rejects a dotted path with a segment no identifier may be.
|
||||||
func CheckPathNames(path string) error {
|
func CheckPathIdentifiers(path string) error {
|
||||||
segs, err := SplitPath(path)
|
segs, err := SplitPath(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
for _, seg := range NameSegments(segs) {
|
for _, seg := range IdentifierSegments(segs) {
|
||||||
if err := CheckName(seg); err != nil {
|
if err := CheckIdentifier(seg); err != nil {
|
||||||
return fmt.Errorf("path %w", err)
|
return fmt.Errorf("path %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package grammar
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestCheckIdentifierRefusesWhatAGrammarReserves(t *testing.T) {
|
||||||
|
for _, name := range []string{"", "-", "a as b", "a.b", "a|b", "a(b", "a{b", "a}b", "a/b", "a[b", "a]b", `a"b`} {
|
||||||
|
if CheckIdentifier(name) == nil {
|
||||||
|
t.Errorf("CheckIdentifier(%q) = nil, want an error", name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, name := range []string{"a", "postal-code", "é", "a_b"} {
|
||||||
|
if err := CheckIdentifier(name); err != nil {
|
||||||
|
t.Errorf("CheckIdentifier(%q) = %v, want nil", name, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
package grammar
|
|
||||||
|
|
||||||
import "testing"
|
|
||||||
|
|
||||||
func TestCheckNameRefusesWhatAGrammarReserves(t *testing.T) {
|
|
||||||
for _, name := range []string{"", "-", "a as b", "a.b", "a|b", "a(b", "a{b", "a}b", "a/b", "a[b", "a]b", `a"b`} {
|
|
||||||
if CheckName(name) == nil {
|
|
||||||
t.Errorf("CheckName(%q) = nil, want an error", name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for _, name := range []string{"a", "postal-code", "é", "a_b"} {
|
|
||||||
if err := CheckName(name); err != nil {
|
|
||||||
t.Errorf("CheckName(%q) = %v, want nil", name, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -150,8 +150,8 @@ func UnspellableInSelector(s string) string {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
// NameSegments is the segments of a path that are names, its selectors and ".." left out.
|
// IdentifierSegments is the segments of a path that are identifiers, its selectors and ".." left out.
|
||||||
func NameSegments(segs []string) []string {
|
func IdentifierSegments(segs []string) []string {
|
||||||
out := segs[:0:0]
|
out := segs[:0:0]
|
||||||
for _, s := range segs {
|
for _, s := range segs {
|
||||||
if !IsSelector(s) && s != ".." {
|
if !IsSelector(s) && s != ".." {
|
||||||
|
|||||||
@@ -33,9 +33,9 @@ func TestSplitPathRefusesAStraySelectorBracket(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestNameSegmentsDropSelectorsAndStepsUp(t *testing.T) {
|
func TestIdentifierSegmentsDropSelectorsAndStepsUp(t *testing.T) {
|
||||||
if got := NameSegments([]string{"a", "[b]", "..", "c"}); !reflect.DeepEqual(got, []string{"a", "c"}) {
|
if got := IdentifierSegments([]string{"a", "[b]", "..", "c"}); !reflect.DeepEqual(got, []string{"a", "c"}) {
|
||||||
t.Errorf("NameSegments = %q, want [a c]", got)
|
t.Errorf("IdentifierSegments = %q, want [a c]", got)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ func isJSONStart(arg string) bool {
|
|||||||
|
|
||||||
// pathAdvice refuses a spelling of path, naming path to write, or why no name can spell it.
|
// pathAdvice refuses a spelling of path, naming path to write, or why no name can spell it.
|
||||||
func pathAdvice(path, refusal string) error {
|
func pathAdvice(path, refusal string) error {
|
||||||
if err := CheckPathNames(path); err != nil {
|
if err := CheckPathIdentifiers(path); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return fmt.Errorf("%s; write %s", refusal, path)
|
return fmt.Errorf("%s; write %s", refusal, path)
|
||||||
@@ -71,7 +71,7 @@ func loneReference(arg string) (string, bool) {
|
|||||||
|
|
||||||
// loneRef is the reference a format of one reference token and nothing else reads.
|
// loneRef is the reference a format of one reference token and nothing else reads.
|
||||||
func loneRef(toks []Token) (string, bool) {
|
func loneRef(toks []Token) (string, bool) {
|
||||||
if len(toks) != 1 || toks[0].Kind != NameRead || len(toks[0].Arms) != 1 || !IsRef(toks[0].Arms[0]) {
|
if len(toks) != 1 || toks[0].Kind != PathRead || len(toks[0].Arms) != 1 || !IsRef(toks[0].Arms[0]) {
|
||||||
return "", false
|
return "", false
|
||||||
}
|
}
|
||||||
return toks[0].Arms[0], true
|
return toks[0].Arms[0], true
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ func (t *Table[O]) parseRows(data string) error {
|
|||||||
t.header = strings.Split(header, "\t")
|
t.header = strings.Split(header, "\t")
|
||||||
t.col = make(map[string]int, len(t.header))
|
t.col = make(map[string]int, len(t.header))
|
||||||
for i, name := range t.header {
|
for i, name := range t.header {
|
||||||
if err := grammar.CheckName(name); err != nil {
|
if err := grammar.CheckIdentifier(name); err != nil {
|
||||||
return fmt.Errorf("column %w", err)
|
return fmt.Errorf("column %w", err)
|
||||||
}
|
}
|
||||||
if _, dup := t.col[name]; dup {
|
if _, dup := t.col[name]; dup {
|
||||||
|
|||||||
@@ -381,7 +381,7 @@ func compileFields(m map[string]any, pos position) (map[string]node, error) {
|
|||||||
if isOption(k) {
|
if isOption(k) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if err := grammar.CheckName(k); err != nil {
|
if err := grammar.CheckIdentifier(k); err != nil {
|
||||||
return nil, fmt.Errorf("field %w", err)
|
return nil, fmt.Errorf("field %w", err)
|
||||||
}
|
}
|
||||||
n, err := compileAt(m[k], pos)
|
n, err := compileAt(m[k], pos)
|
||||||
|
|||||||
@@ -145,7 +145,7 @@ func (f *Generator) FakeRecord(path string) (*Record, error) {
|
|||||||
}
|
}
|
||||||
record, err := recordOf(n)
|
record, err := recordOf(n)
|
||||||
if errors.Is(err, ErrNoColumns) {
|
if errors.Is(err, ErrNoColumns) {
|
||||||
ns := grammar.NameSegments(segments)
|
ns := grammar.IdentifierSegments(segments)
|
||||||
return nil, fmt.Errorf(`fejkdata: %s %w; render it as a column of one: {"format":"","%s":"{/%s}"}`, path, err, ns[len(ns)-1], path)
|
return nil, fmt.Errorf(`fejkdata: %s %w; render it as a column of one: {"format":"","%s":"{/%s}"}`, path, err, ns[len(ns)-1], path)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -123,7 +123,7 @@ func expand(s *drawstate.State, t *template, env renderEnv) string {
|
|||||||
switch o.Kind {
|
switch o.Kind {
|
||||||
case grammar.LiteralRun:
|
case grammar.LiteralRun:
|
||||||
b.WriteString(o.Lit)
|
b.WriteString(o.Lit)
|
||||||
case grammar.NameRead:
|
case grammar.PathRead:
|
||||||
b.WriteString(readField(s, t, env, o.arms[s.IntN(len(o.arms))]).text)
|
b.WriteString(readField(s, t, env, o.arms[s.IntN(len(o.arms))]).text)
|
||||||
case grammar.BuiltinCall:
|
case grammar.BuiltinCall:
|
||||||
var operands []string
|
var operands []string
|
||||||
|
|||||||
+1
-1
@@ -105,7 +105,7 @@ type columnRead struct {
|
|||||||
// or name, and nil where it does not.
|
// or name, and nil where it does not.
|
||||||
func columnReadOf(t *template, targets map[*nameBinding]nameTarget) *columnRead {
|
func columnReadOf(t *template, targets map[*nameBinding]nameTarget) *columnRead {
|
||||||
ops := t.compiled.ops
|
ops := t.compiled.ops
|
||||||
if t.repeat != 1 || len(ops) != 1 || ops[0].Kind != grammar.NameRead || len(ops[0].arms) != 1 {
|
if t.repeat != 1 || len(ops) != 1 || ops[0].Kind != grammar.PathRead || len(ops[0].arms) != 1 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
a := ops[0].arms[0]
|
a := ops[0].arms[0]
|
||||||
|
|||||||
@@ -231,7 +231,7 @@ func tagValue(sf reflect.StructField, tag string) (any, error) {
|
|||||||
case inline:
|
case inline:
|
||||||
return inputValue(tag)
|
return inputValue(tag)
|
||||||
}
|
}
|
||||||
if err := grammar.CheckPathNames(tag); err != nil {
|
if err := grammar.CheckPathIdentifiers(tag); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return "{/" + tag + "}", nil
|
return "{/" + tag + "}", nil
|
||||||
|
|||||||
@@ -167,7 +167,7 @@ func (t *table) compileRowFormat(format string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
for _, tok := range toks {
|
for _, tok := range toks {
|
||||||
if tok.Kind != grammar.NameRead {
|
if tok.Kind != grammar.PathRead {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
for _, name := range tok.Arms {
|
for _, name := range tok.Arms {
|
||||||
|
|||||||
+1
-1
@@ -167,7 +167,7 @@ func (p *valueProof) proveTemplate(t *template) proven.Facts {
|
|||||||
o := t.compiled.ops[0]
|
o := t.compiled.ops[0]
|
||||||
body, name, args := o.Body, o.Fn, o.Args
|
body, name, args := o.Body, o.Fn, o.Args
|
||||||
switch {
|
switch {
|
||||||
case o.Kind == grammar.NameRead:
|
case o.Kind == grammar.PathRead:
|
||||||
var leaves []node
|
var leaves []node
|
||||||
for _, a := range o.arms {
|
for _, a := range o.arms {
|
||||||
leaves = append(leaves, a.leaves...)
|
leaves = append(leaves, a.leaves...)
|
||||||
|
|||||||
Reference in New Issue
Block a user