// Copyright (c) 2016, Daniel Martí <mvdan@mvdan.cc>
// See LICENSE for licensing information

package syntax

import (
	
	
)

// Node represents a syntax tree node.
type Node interface {
	// Pos returns the position of the first character of the node. Comments
	// are ignored, except if the node is a *File.
	Pos() Pos
	// End returns the position of the character immediately after the node.
	// If the character is a newline, the line number won't cross into the
	// next line. Comments are ignored, except if the node is a *File.
	End() Pos
}

// File represents a shell source file.
type File struct {
	Name string

	Stmts []*Stmt
	Last  []Comment
}

func ( *File) () Pos { return stmtsPos(.Stmts, .Last) }
func ( *File) () Pos { return stmtsEnd(.Stmts, .Last) }

func stmtsPos( []*Stmt,  []Comment) Pos {
	if len() > 0 {
		 := [0]
		 := .Pos()
		if len(.Comments) > 0 {
			if  := .Comments[0].Pos(); .After() {
				return 
			}
		}
		return 
	}
	if len() > 0 {
		return [0].Pos()
	}
	return Pos{}
}

func stmtsEnd( []*Stmt,  []Comment) Pos {
	if len() > 0 {
		return [len()-1].End()
	}
	if len() > 0 {
		 := [len()-1]
		 := .End()
		if len(.Comments) > 0 {
			if  := .Comments[0].End(); .After() {
				return 
			}
		}
		return 
	}
	return Pos{}
}

// Pos is a position within a shell source file.
type Pos struct {
	offs, lineCol uint32
}

// We used to split line and column numbers evenly in 16 bits, but line numbers
// are significantly more important in practice. Use more bits for them.
const (
	lineBitSize = 18
	lineMax     = (1 << lineBitSize) - 1

	colBitSize = 32 - lineBitSize
	colMax     = (1 << colBitSize) - 1
	colBitMask = colMax
)

// TODO(v4): consider using uint32 for Offset/Line/Col to better represent bit sizes.
// Or go with int64, which more closely resembles portable "sizes" elsewhere.
// The latter is probably nicest, as then we can change the number of internal
// bits later, and we can also do overflow checks for the user in NewPos.

// NewPos creates a position with the given offset, line, and column.
//
// Note that Pos uses a limited number of bits to store these numbers.
// If line or column overflow their allocated space, they are replaced with 0.
func (, ,  uint) Pos {
	if  > lineMax {
		 = 0 // protect against overflows; rendered as "?"
	}
	if  > colMax {
		 = 0 // protect against overflows; rendered as "?"
	}
	return Pos{
		offs:    uint32(),
		lineCol: (uint32() << colBitSize) | uint32(),
	}
}

// Offset returns the byte offset of the position in the original source file.
// Byte offsets start at 0.
//
// Note that Offset is not protected against overflows;
// if an input is larger than 4GiB, the offset will wrap around to 0.
func ( Pos) () uint { return uint(.offs) }

// Line returns the line number of the position, starting at 1.
//
// Line is protected against overflows; if an input has too many lines, extra
// lines will have a line number of 0, rendered as "?" by [Pos.String].
func ( Pos) () uint { return uint(.lineCol >> colBitSize) }

// Col returns the column number of the position, starting at 1. It counts in
// bytes.
//
// Col is protected against overflows; if an input line has too many columns,
// extra columns will have a column number of 0, rendered as "?" by [Pos.String].
func ( Pos) () uint { return uint(.lineCol & colBitMask) }

func ( Pos) () string {
	var  strings.Builder
	if  := .Line();  > 0 {
		.WriteString(strconv.FormatUint(uint64(), 10))
	} else {
		.WriteByte('?')
	}
	.WriteByte(':')
	if  := .Col();  > 0 {
		.WriteString(strconv.FormatUint(uint64(), 10))
	} else {
		.WriteByte('?')
	}
	return .String()
}

// IsValid reports whether the position contains useful position information.
// Some positions returned via [Parse] may be invalid: for example, [Stmt.Semicolon]
// will only be valid if a statement contained a closing token such as ';'.
func ( Pos) () bool { return  != Pos{} }

// After reports whether the position p is after p2. It is a more expressive
// version of p.Offset() > p2.Offset().
func ( Pos) ( Pos) bool { return .offs > .offs }

func posAddCol( Pos,  int) Pos {
	// TODO: guard against overflows
	.lineCol += uint32()
	.offs += uint32()
	return 
}

func posMax(,  Pos) Pos {
	if .After() {
		return 
	}
	return 
}

// Comment represents a single comment on a single line.
type Comment struct {
	Hash Pos
	Text string
}

func ( *Comment) () Pos { return .Hash }
func ( *Comment) () Pos { return posAddCol(.Hash, 1+len(.Text)) }

// Stmt represents a statement, also known as a "complete command". It is
// compromised of a command and other components that may come before or after
// it.
type Stmt struct {
	Comments   []Comment
	Cmd        Command
	Position   Pos
	Semicolon  Pos  // position of ';', '&', or '|&', if any
	Negated    bool // ! stmt
	Background bool // stmt &
	Coprocess  bool // mksh's |&

	Redirs []*Redirect // stmt >a <b
}

func ( *Stmt) () Pos { return .Position }
func ( *Stmt) () Pos {
	if .Semicolon.IsValid() {
		 := posAddCol(.Semicolon, 1) // ';' or '&'
		if .Coprocess {
			 = posAddCol(, 1) // '|&'
		}
		return 
	}
	 := .Position
	if .Negated {
		 = posAddCol(, 1)
	}
	if .Cmd != nil {
		 = .Cmd.End()
	}
	if len(.Redirs) > 0 {
		 = posMax(, .Redirs[len(.Redirs)-1].End())
	}
	return 
}

// Command represents all nodes that are simple or compound commands, including
// function declarations.
//
// These are *CallExpr, *IfClause, *WhileClause, *ForClause, *CaseClause,
// *Block, *Subshell, *BinaryCmd, *FuncDecl, *ArithmCmd, *TestClause,
// *DeclClause, *LetClause, *TimeClause, and *CoprocClause.
type Command interface {
	Node
	commandNode()
}

func (*CallExpr) ()     {}
func (*IfClause) ()     {}
func (*WhileClause) ()  {}
func (*ForClause) ()    {}
func (*CaseClause) ()   {}
func (*Block) ()        {}
func (*Subshell) ()     {}
func (*BinaryCmd) ()    {}
func (*FuncDecl) ()     {}
func (*ArithmCmd) ()    {}
func (*TestClause) ()   {}
func (*DeclClause) ()   {}
func (*LetClause) ()    {}
func (*TimeClause) ()   {}
func (*CoprocClause) () {}
func (*TestDecl) ()     {}

// Assign represents an assignment to a variable.
//
// Here and elsewhere, Index can mean either an index expression into an indexed
// array, or a string key into an associative array.
//
// If Index is non-nil, the value will be a word and not an array as nested
// arrays are not allowed.
//
// If Naked is true and Name is nil, the assignment is part of a DeclClause and
// the argument (in the Value field) will be evaluated at run-time. This
// includes parameter expansions, which may expand to assignments or options.
type Assign struct {
	Append bool       // +=
	Naked  bool       // without '='
	Name   *Lit       // must be a valid name
	Index  ArithmExpr // [i], ["k"]
	Value  *Word      // =val
	Array  *ArrayExpr // =(arr)
}

func ( *Assign) () Pos {
	if .Name == nil {
		return .Value.Pos()
	}
	return .Name.Pos()
}

func ( *Assign) () Pos {
	if .Value != nil {
		return .Value.End()
	}
	if .Array != nil {
		return .Array.End()
	}
	if .Index != nil {
		return posAddCol(.Index.End(), 2)
	}
	if .Naked {
		return .Name.End()
	}
	return posAddCol(.Name.End(), 1)
}

// Redirect represents an input/output redirection.
type Redirect struct {
	OpPos Pos
	Op    RedirOperator
	N     *Lit  // fd>, or {varname}> in Bash
	Word  *Word // >word
	Hdoc  *Word // here-document body
}

func ( *Redirect) () Pos {
	if .N != nil {
		return .N.Pos()
	}
	return .OpPos
}

func ( *Redirect) () Pos {
	if .Hdoc != nil {
		return .Hdoc.End()
	}
	return .Word.End()
}

// CallExpr represents a command execution or function call, otherwise known as
// a "simple command".
//
// If Args is empty, Assigns apply to the shell environment. Otherwise, they are
// variables that cannot be arrays and which only apply to the call.
type CallExpr struct {
	Assigns []*Assign // a=x b=y args
	Args    []*Word
}

func ( *CallExpr) () Pos {
	if len(.Assigns) > 0 {
		return .Assigns[0].Pos()
	}
	return .Args[0].Pos()
}

func ( *CallExpr) () Pos {
	if len(.Args) == 0 {
		return .Assigns[len(.Assigns)-1].End()
	}
	return .Args[len(.Args)-1].End()
}

// Subshell represents a series of commands that should be executed in a nested
// shell environment.
type Subshell struct {
	Lparen, Rparen Pos

	Stmts []*Stmt
	Last  []Comment
}

func ( *Subshell) () Pos { return .Lparen }
func ( *Subshell) () Pos { return posAddCol(.Rparen, 1) }

// Block represents a series of commands that should be executed in a nested
// scope. It is essentially a list of statements within curly braces.
type Block struct {
	Lbrace, Rbrace Pos

	Stmts []*Stmt
	Last  []Comment
}

func ( *Block) () Pos { return .Lbrace }
func ( *Block) () Pos { return posAddCol(.Rbrace, 1) }

// IfClause represents an if statement.
type IfClause struct {
	Position Pos // position of the starting "if", "elif", or "else" token
	ThenPos  Pos // position of "then", empty if this is an "else"
	FiPos    Pos // position of "fi", shared with .Else if non-nil

	Cond     []*Stmt
	CondLast []Comment
	Then     []*Stmt
	ThenLast []Comment

	Else *IfClause // if non-nil, an "elif" or an "else"

	Last []Comment // comments on the first "elif", "else", or "fi"
}

func ( *IfClause) () Pos { return .Position }
func ( *IfClause) () Pos { return posAddCol(.FiPos, 2) }

// WhileClause represents a while or an until clause.
type WhileClause struct {
	WhilePos, DoPos, DonePos Pos
	Until                    bool

	Cond     []*Stmt
	CondLast []Comment
	Do       []*Stmt
	DoLast   []Comment
}

func ( *WhileClause) () Pos { return .WhilePos }
func ( *WhileClause) () Pos { return posAddCol(.DonePos, 4) }

// ForClause represents a for or a select clause. The latter is only present in
// Bash.
type ForClause struct {
	ForPos, DoPos, DonePos Pos
	Select                 bool
	Braces                 bool // deprecated form with { } instead of do/done
	Loop                   Loop

	Do     []*Stmt
	DoLast []Comment
}

func ( *ForClause) () Pos { return .ForPos }
func ( *ForClause) () Pos { return posAddCol(.DonePos, 4) }

// Loop holds either *WordIter or *CStyleLoop.
type Loop interface {
	Node
	loopNode()
}

func (*WordIter) ()   {}
func (*CStyleLoop) () {}

// WordIter represents the iteration of a variable over a series of words in a
// for clause. If InPos is an invalid position, the "in" token was missing, so
// the iteration is over the shell's positional parameters.
type WordIter struct {
	Name  *Lit
	InPos Pos // position of "in"
	Items []*Word
}

func ( *WordIter) () Pos { return .Name.Pos() }
func ( *WordIter) () Pos {
	if len(.Items) > 0 {
		return wordLastEnd(.Items)
	}
	return posMax(.Name.End(), posAddCol(.InPos, 2))
}

// CStyleLoop represents the behavior of a for clause similar to the C
// language.
//
// This node will only appear with LangBash.
type CStyleLoop struct {
	Lparen, Rparen Pos
	// Init, Cond, Post can each be nil, if the for loop construct omits it.
	Init, Cond, Post ArithmExpr
}

func ( *CStyleLoop) () Pos { return .Lparen }
func ( *CStyleLoop) () Pos { return posAddCol(.Rparen, 2) }

// BinaryCmd represents a binary expression between two statements.
type BinaryCmd struct {
	OpPos Pos
	Op    BinCmdOperator
	X, Y  *Stmt
}

func ( *BinaryCmd) () Pos { return .X.Pos() }
func ( *BinaryCmd) () Pos { return .Y.End() }

// FuncDecl represents the declaration of a function.
type FuncDecl struct {
	Position Pos
	RsrvWord bool // non-posix "function f" style
	Parens   bool // with () parentheses, only meaningful with RsrvWord=true
	Name     *Lit
	Body     *Stmt
}

func ( *FuncDecl) () Pos { return .Position }
func ( *FuncDecl) () Pos { return .Body.End() }

// Word represents a shell word, containing one or more word parts contiguous to
// each other. The word is delimited by word boundaries, such as spaces,
// newlines, semicolons, or parentheses.
type Word struct {
	Parts []WordPart
}

func ( *Word) () Pos { return .Parts[0].Pos() }
func ( *Word) () Pos { return .Parts[len(.Parts)-1].End() }

// Lit returns the word as a literal value, if the word consists of *Lit nodes
// only. An empty string is returned otherwise. Words with multiple literals,
// which can appear in some edge cases, are handled properly.
//
// For example, the word "foo" will return "foo", but the word "foo${bar}" will
// return "".
func ( *Word) () string {
	// In the usual case, we'll have either a single part that's a literal,
	// or one of the parts being a non-literal. Using strings.Join instead
	// of a strings.Builder avoids extra work in these cases, since a single
	// part is a shortcut, and many parts don't incur string copies.
	 := make([]string, 0, 1)
	for ,  := range .Parts {
		,  := .(*Lit)
		if ! {
			return ""
		}
		 = append(, .Value)
	}
	return strings.Join(, "")
}

// WordPart represents all nodes that can form part of a word.
//
// These are *Lit, *SglQuoted, *DblQuoted, *ParamExp, *CmdSubst, *ArithmExp,
// *ProcSubst, and *ExtGlob.
type WordPart interface {
	Node
	wordPartNode()
}

func (*Lit) ()       {}
func (*SglQuoted) () {}
func (*DblQuoted) () {}
func (*ParamExp) ()  {}
func (*CmdSubst) ()  {}
func (*ArithmExp) () {}
func (*ProcSubst) () {}
func (*ExtGlob) ()   {}
func (*BraceExp) ()  {}

// Lit represents a string literal.
//
// Note that a parsed string literal may not appear as-is in the original source
// code, as it is possible to split literals by escaping newlines. The splitting
// is lost, but the end position is not.
type Lit struct {
	ValuePos, ValueEnd Pos
	Value              string
}

func ( *Lit) () Pos { return .ValuePos }
func ( *Lit) () Pos { return .ValueEnd }

// SglQuoted represents a string within single quotes.
type SglQuoted struct {
	Left, Right Pos
	Dollar      bool // $''
	Value       string
}

func ( *SglQuoted) () Pos { return .Left }
func ( *SglQuoted) () Pos { return posAddCol(.Right, 1) }

// DblQuoted represents a list of nodes within double quotes.
type DblQuoted struct {
	Left, Right Pos
	Dollar      bool // $""
	Parts       []WordPart
}

func ( *DblQuoted) () Pos { return .Left }
func ( *DblQuoted) () Pos { return posAddCol(.Right, 1) }

// CmdSubst represents a command substitution.
type CmdSubst struct {
	Left, Right Pos

	Stmts []*Stmt
	Last  []Comment

	Backquotes bool // deprecated `foo`
	TempFile   bool // mksh's ${ foo;}
	ReplyVar   bool // mksh's ${|foo;}
}

func ( *CmdSubst) () Pos { return .Left }
func ( *CmdSubst) () Pos { return posAddCol(.Right, 1) }

// ParamExp represents a parameter expansion.
type ParamExp struct {
	Dollar, Rbrace Pos

	Short  bool // $a instead of ${a}
	Excl   bool // ${!a}
	Length bool // ${#a}
	Width  bool // ${%a}
	Param  *Lit
	Index  ArithmExpr       // ${a[i]}, ${a["k"]}
	Slice  *Slice           // ${a:x:y}
	Repl   *Replace         // ${a/x/y}
	Names  ParNamesOperator // ${!prefix*} or ${!prefix@}
	Exp    *Expansion       // ${a:-b}, ${a#b}, etc
}

func ( *ParamExp) () Pos { return .Dollar }
func ( *ParamExp) () Pos {
	if !.Short {
		return posAddCol(.Rbrace, 1)
	}
	if .Index != nil {
		return posAddCol(.Index.End(), 1)
	}
	return .Param.End()
}

func ( *ParamExp) () bool {
	return .Short && .Index != nil
}

// Slice represents a character slicing expression inside a ParamExp.
//
// This node will only appear in LangBash and LangMirBSDKorn.
type Slice struct {
	Offset, Length ArithmExpr
}

// Replace represents a search and replace expression inside a ParamExp.
type Replace struct {
	All        bool
	Orig, With *Word
}

// Expansion represents string manipulation in a ParamExp other than those
// covered by Replace.
type Expansion struct {
	Op   ParExpOperator
	Word *Word
}

// ArithmExp represents an arithmetic expansion.
type ArithmExp struct {
	Left, Right Pos
	Bracket     bool // deprecated $[expr] form
	Unsigned    bool // mksh's $((# expr))

	X ArithmExpr
}

func ( *ArithmExp) () Pos { return .Left }
func ( *ArithmExp) () Pos {
	if .Bracket {
		return posAddCol(.Right, 1)
	}
	return posAddCol(.Right, 2)
}

// ArithmCmd represents an arithmetic command.
//
// This node will only appear in LangBash and LangMirBSDKorn.
type ArithmCmd struct {
	Left, Right Pos
	Unsigned    bool // mksh's ((# expr))

	X ArithmExpr
}

func ( *ArithmCmd) () Pos { return .Left }
func ( *ArithmCmd) () Pos { return posAddCol(.Right, 2) }

// ArithmExpr represents all nodes that form arithmetic expressions.
//
// These are *BinaryArithm, *UnaryArithm, *ParenArithm, and *Word.
type ArithmExpr interface {
	Node
	arithmExprNode()
}

func (*BinaryArithm) () {}
func (*UnaryArithm) ()  {}
func (*ParenArithm) ()  {}
func (*Word) ()         {}

// BinaryArithm represents a binary arithmetic expression.
//
// If Op is any assign operator, X will be a word with a single *Lit whose value
// is a valid name.
//
// Ternary operators like "a ? b : c" are fit into this structure. Thus, if
// Op==TernQuest, Y will be a *BinaryArithm with Op==TernColon. Op can only be
// TernColon in that scenario.
type BinaryArithm struct {
	OpPos Pos
	Op    BinAritOperator
	X, Y  ArithmExpr
}

func ( *BinaryArithm) () Pos { return .X.Pos() }
func ( *BinaryArithm) () Pos { return .Y.End() }

// UnaryArithm represents an unary arithmetic expression. The unary operator
// may come before or after the sub-expression.
//
// If Op is Inc or Dec, X will be a word with a single *Lit whose value is a
// valid name.
type UnaryArithm struct {
	OpPos Pos
	Op    UnAritOperator
	Post  bool
	X     ArithmExpr
}

func ( *UnaryArithm) () Pos {
	if .Post {
		return .X.Pos()
	}
	return .OpPos
}

func ( *UnaryArithm) () Pos {
	if .Post {
		return posAddCol(.OpPos, 2)
	}
	return .X.End()
}

// ParenArithm represents an arithmetic expression within parentheses.
type ParenArithm struct {
	Lparen, Rparen Pos

	X ArithmExpr
}

func ( *ParenArithm) () Pos { return .Lparen }
func ( *ParenArithm) () Pos { return posAddCol(.Rparen, 1) }

// CaseClause represents a case (switch) clause.
type CaseClause struct {
	Case, In, Esac Pos
	Braces         bool // deprecated mksh form with braces instead of in/esac

	Word  *Word
	Items []*CaseItem
	Last  []Comment
}

func ( *CaseClause) () Pos { return .Case }
func ( *CaseClause) () Pos { return posAddCol(.Esac, 4) }

// CaseItem represents a pattern list (case) within a CaseClause.
type CaseItem struct {
	Op       CaseOperator
	OpPos    Pos // unset if it was finished by "esac"
	Comments []Comment
	Patterns []*Word

	Stmts []*Stmt
	Last  []Comment
}

func ( *CaseItem) () Pos { return .Patterns[0].Pos() }
func ( *CaseItem) () Pos {
	if .OpPos.IsValid() {
		return posAddCol(.OpPos, len(.Op.String()))
	}
	return stmtsEnd(.Stmts, .Last)
}

// TestClause represents a Bash extended test clause.
//
// This node will only appear in LangBash and LangMirBSDKorn.
type TestClause struct {
	Left, Right Pos

	X TestExpr
}

func ( *TestClause) () Pos { return .Left }
func ( *TestClause) () Pos { return posAddCol(.Right, 2) }

// TestExpr represents all nodes that form test expressions.
//
// These are *BinaryTest, *UnaryTest, *ParenTest, and *Word.
type TestExpr interface {
	Node
	testExprNode()
}

func (*BinaryTest) () {}
func (*UnaryTest) ()  {}
func (*ParenTest) ()  {}
func (*Word) ()       {}

// BinaryTest represents a binary test expression.
type BinaryTest struct {
	OpPos Pos
	Op    BinTestOperator
	X, Y  TestExpr
}

func ( *BinaryTest) () Pos { return .X.Pos() }
func ( *BinaryTest) () Pos { return .Y.End() }

// UnaryTest represents a unary test expression. The unary operator may come
// before or after the sub-expression.
type UnaryTest struct {
	OpPos Pos
	Op    UnTestOperator
	X     TestExpr
}

func ( *UnaryTest) () Pos { return .OpPos }
func ( *UnaryTest) () Pos { return .X.End() }

// ParenTest represents a test expression within parentheses.
type ParenTest struct {
	Lparen, Rparen Pos

	X TestExpr
}

func ( *ParenTest) () Pos { return .Lparen }
func ( *ParenTest) () Pos { return posAddCol(.Rparen, 1) }

// DeclClause represents a Bash declare clause.
//
// Args can contain a mix of regular and naked assignments. The naked
// assignments can represent either options or variable names.
//
// This node will only appear with LangBash.
type DeclClause struct {
	// Variant is one of "declare", "local", "export", "readonly",
	// "typeset", or "nameref".
	Variant *Lit
	Args    []*Assign
}

func ( *DeclClause) () Pos { return .Variant.Pos() }
func ( *DeclClause) () Pos {
	if len(.Args) > 0 {
		return .Args[len(.Args)-1].End()
	}
	return .Variant.End()
}

// ArrayExpr represents a Bash array expression.
//
// This node will only appear with LangBash.
type ArrayExpr struct {
	Lparen, Rparen Pos

	Elems []*ArrayElem
	Last  []Comment
}

func ( *ArrayExpr) () Pos { return .Lparen }
func ( *ArrayExpr) () Pos { return posAddCol(.Rparen, 1) }

// ArrayElem represents a Bash array element.
//
// Index can be nil; for example, declare -a x=(value).
// Value can be nil; for example, declare -A x=([index]=).
// Finally, neither can be nil; for example, declare -A x=([index]=value)
type ArrayElem struct {
	Index    ArithmExpr
	Value    *Word
	Comments []Comment
}

func ( *ArrayElem) () Pos {
	if .Index != nil {
		return .Index.Pos()
	}
	return .Value.Pos()
}

func ( *ArrayElem) () Pos {
	if .Value != nil {
		return .Value.End()
	}
	return posAddCol(.Index.Pos(), 1)
}

// ExtGlob represents a Bash extended globbing expression. Note that these are
// parsed independently of whether shopt has been called or not.
//
// This node will only appear in LangBash and LangMirBSDKorn.
type ExtGlob struct {
	OpPos   Pos
	Op      GlobOperator
	Pattern *Lit
}

func ( *ExtGlob) () Pos { return .OpPos }
func ( *ExtGlob) () Pos { return posAddCol(.Pattern.End(), 1) }

// ProcSubst represents a Bash process substitution.
//
// This node will only appear with LangBash.
type ProcSubst struct {
	OpPos, Rparen Pos
	Op            ProcOperator

	Stmts []*Stmt
	Last  []Comment
}

func ( *ProcSubst) () Pos { return .OpPos }
func ( *ProcSubst) () Pos { return posAddCol(.Rparen, 1) }

// TimeClause represents a Bash time clause. PosixFormat corresponds to the -p
// flag.
//
// This node will only appear in LangBash and LangMirBSDKorn.
type TimeClause struct {
	Time        Pos
	PosixFormat bool
	Stmt        *Stmt
}

func ( *TimeClause) () Pos { return .Time }
func ( *TimeClause) () Pos {
	if .Stmt == nil {
		return posAddCol(.Time, 4)
	}
	return .Stmt.End()
}

// CoprocClause represents a Bash coproc clause.
//
// This node will only appear with LangBash.
type CoprocClause struct {
	Coproc Pos
	Name   *Word
	Stmt   *Stmt
}

func ( *CoprocClause) () Pos { return .Coproc }
func ( *CoprocClause) () Pos { return .Stmt.End() }

// LetClause represents a Bash let clause.
//
// This node will only appear in LangBash and LangMirBSDKorn.
type LetClause struct {
	Let   Pos
	Exprs []ArithmExpr
}

func ( *LetClause) () Pos { return .Let }
func ( *LetClause) () Pos { return .Exprs[len(.Exprs)-1].End() }

// BraceExp represents a Bash brace expression, such as "{a,f}" or "{1..10}".
//
// This node will only appear as a result of SplitBraces.
type BraceExp struct {
	Sequence bool // {x..y[..incr]} instead of {x,y[,...]}
	Elems    []*Word
}

func ( *BraceExp) () Pos {
	return posAddCol(.Elems[0].Pos(), -1)
}

func ( *BraceExp) () Pos {
	return posAddCol(wordLastEnd(.Elems), 1)
}

// TestDecl represents the declaration of a Bats test function.
type TestDecl struct {
	Position    Pos
	Description *Word
	Body        *Stmt
}

func ( *TestDecl) () Pos { return .Position }
func ( *TestDecl) () Pos { return .Body.End() }

func wordLastEnd( []*Word) Pos {
	if len() == 0 {
		return Pos{}
	}
	return [len()-1].End()
}