Files
mgmt/lang/parser.y
James Shubin 96dccca475 lang: Add module imports and more
This enables imports in mcl code, and is one of last remaining blockers
to using mgmt. Now we can start writing standalone modules, and adding
standard library functions as needed. There's still lots to do, but this
was a big missing piece. It was much harder to get right than I had
expected, but I think it's solid!

This unfortunately large commit is the result of some wild hacking I've
been doing for the past little while. It's the result of a rebase that
broke many "wip" commits that tracked my private progress, into
something that's not gratuitously messy for our git logs. Since this was
a learning and discovery process for me, I've "erased" the confusing git
history that wouldn't have helped. I'm happy to discuss the dead-ends,
and a small portion of that code was even left in for possible future
use.

This patch includes:

* A change to the cli interface:
You now specify the front-end explicitly, instead of leaving it up to
the front-end to decide when to "activate". For example, instead of:

mgmt run --lang code.mcl

we now do:

mgmt run lang --lang code.mcl

We might rename the --lang flag in the future to avoid the awkward word
repetition. Suggestions welcome, but I'm considering "input". One
side-effect of this change, is that flags which are "engine" specific
now must be specified with "run" before the front-end name. Eg:

mgmt run --tmp-prefix lang --lang code.mcl

instead of putting --tmp-prefix at the end. We also changed the GAPI
slightly, but I've patched all code that used it. This also makes things
consistent with the "deploy" command.

* The deploys are more robust and let you deploy after a run
This has been vastly improved and let's mgmt really run as a smart
engine that can handle different workloads. If you don't want to deploy
when you've started with `run` or if one comes in, you can use the
--no-watch-deploy option to block new deploys.

* The import statement exists and works!
We now have a working `import` statement. Read the docs, and try it out.
I think it's quite elegant how it fits in with `SetScope`. Have a look.
As a result, we now have some built-in functions available in modules.
This also adds the metadata.yaml entry-point for all modules. Have a
look at the examples or the tests. The bulk of the patch is to support
this.

* Improved lang input parsing code:
I re-wrote the parsing that determined what ran when we passed different
things to --lang. Deciding between running an mcl file or raw code is
now handled in a more intelligent, and re-usable way. See the inputs.go
file if you want to have a look. One casualty is that you can't stream
code from stdin *directly* to the front-end, it's encapsulated into a
deploy first. You can still use stdin though! I doubt anyone will notice
this change.

* The scope was extended to include functions and classes:
Go forth and import lovely code. All these exist in scopes now, and can
be re-used!

* Function calls actually use the scope now. Glad I got this sorted out.

* There is import cycle detection for modules!
Yes, this is another dag. I think that's #4. I guess they're useful.

* A ton of tests and new test infra was added!
This should make it much easier to add new tests that run mcl code. Have
a look at TestAstFunc1 to see how to add more of these.

As usual, I'll try to keep these commits smaller in the future!
2018-12-21 06:22:12 -05:00

1244 lines
27 KiB
Plaintext

// Mgmt
// Copyright (C) 2013-2018+ James Shubin and the project contributors
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
%{
package lang
import (
"fmt"
"strings"
"github.com/purpleidea/mgmt/lang/interfaces"
"github.com/purpleidea/mgmt/lang/types"
)
const (
errstrParseAdditionalEquals = "additional equals in bind statement"
errstrParseExpectingComma = "expecting trailing comma"
)
func init() {
yyErrorVerbose = true // set the global that enables showing full errors
}
%}
%union {
row int
col int
//err error // TODO: if we ever match ERROR in the parser
bool bool
str string
int int64 // this is the .int as seen in lexer.nex
float float64
strSlice []string
typ *types.Type
stmts []interfaces.Stmt
stmt interfaces.Stmt
exprs []interfaces.Expr
expr interfaces.Expr
mapKVs []*ExprMapKV
mapKV *ExprMapKV
structFields []*ExprStructField
structField *ExprStructField
args []*Arg
arg *Arg
resContents []StmtResContents // interface
resField *StmtResField
resEdge *StmtResEdge
edgeHalfList []*StmtEdgeHalf
edgeHalf *StmtEdgeHalf
}
%token OPEN_CURLY CLOSE_CURLY
%token OPEN_PAREN CLOSE_PAREN
%token OPEN_BRACK CLOSE_BRACK
%token IF ELSE
%token STRING BOOL INTEGER FLOAT
%token EQUALS DOLLAR
%token COMMA COLON SEMICOLON
%token ELVIS ROCKET ARROW DOT
%token STR_IDENTIFIER BOOL_IDENTIFIER INT_IDENTIFIER FLOAT_IDENTIFIER
%token MAP_IDENTIFIER STRUCT_IDENTIFIER VARIANT_IDENTIFIER VAR_IDENTIFIER
%token VAR_IDENTIFIER_HX
%token RES_IDENTIFIER CAPITALIZED_RES_IDENTIFIER
%token IDENTIFIER CAPITALIZED_IDENTIFIER
%token FUNC_IDENTIFIER
%token CLASS_IDENTIFIER INCLUDE_IDENTIFIER
%token IMPORT_IDENTIFIER AS_IDENTIFIER
%token COMMENT ERROR
// precedence table
// "Operator precedence is determined by the line ordering of the declarations;
// the higher the line number of the declaration (lower on the page or screen),
// the higher the precedence."
// From: https://www.gnu.org/software/bison/manual/html_node/Infix-Calc.html
// FIXME: a yacc specialist should check the precedence and add more tests!
%left AND OR
%nonassoc LT GT LTE GTE EQ NEQ // TODO: is %nonassoc correct for all of these?
%left PLUS MINUS
%left MULTIPLY DIVIDE
%right NOT
//%right EXP // exponentiation
%nonassoc IN // TODO: is %nonassoc correct for this?
%error IDENTIFIER STRING OPEN_CURLY IDENTIFIER ROCKET BOOL CLOSE_CURLY: errstrParseExpectingComma
%error IDENTIFIER STRING OPEN_CURLY IDENTIFIER ROCKET STRING CLOSE_CURLY: errstrParseExpectingComma
%error IDENTIFIER STRING OPEN_CURLY IDENTIFIER ROCKET INTEGER CLOSE_CURLY: errstrParseExpectingComma
%error IDENTIFIER STRING OPEN_CURLY IDENTIFIER ROCKET FLOAT CLOSE_CURLY: errstrParseExpectingComma
%error VAR_IDENTIFIER EQ BOOL: errstrParseAdditionalEquals
%error VAR_IDENTIFIER EQ STRING: errstrParseAdditionalEquals
%error VAR_IDENTIFIER EQ INTEGER: errstrParseAdditionalEquals
%error VAR_IDENTIFIER EQ FLOAT: errstrParseAdditionalEquals
%%
top:
prog
{
posLast(yylex, yyDollar) // our pos
// store the AST in the struct that we previously passed in
lp := cast(yylex)
lp.ast = $1.stmt
// this is equivalent to:
//lp := yylex.(*Lexer).parseResult
//lp.(*lexParseAST).ast = $1.stmt
}
;
prog:
/* end of list */
{
posLast(yylex, yyDollar) // our pos
$$.stmt = &StmtProg{
Prog: []interfaces.Stmt{},
}
}
| prog stmt
{
posLast(yylex, yyDollar) // our pos
// TODO: should we just skip comments for now?
//if _, ok := $2.stmt.(*StmtComment); !ok {
//}
if stmt, ok := $1.stmt.(*StmtProg); ok {
stmts := stmt.Prog
stmts = append(stmts, $2.stmt)
$$.stmt = &StmtProg{
Prog: stmts,
}
}
}
;
stmt:
COMMENT
{
posLast(yylex, yyDollar) // our pos
$$.stmt = &StmtComment{
Value: $1.str,
}
}
| bind
{
posLast(yylex, yyDollar) // our pos
$$.stmt = $1.stmt
}
| resource
{
posLast(yylex, yyDollar) // our pos
$$.stmt = $1.stmt
}
| edge
{
posLast(yylex, yyDollar) // our pos
$$.stmt = $1.stmt
}
| IF expr OPEN_CURLY prog CLOSE_CURLY
{
posLast(yylex, yyDollar) // our pos
$$.stmt = &StmtIf{
Condition: $2.expr,
ThenBranch: $4.stmt,
//ElseBranch: nil,
}
}
| IF expr OPEN_CURLY prog CLOSE_CURLY ELSE OPEN_CURLY prog CLOSE_CURLY
{
posLast(yylex, yyDollar) // our pos
$$.stmt = &StmtIf{
Condition: $2.expr,
ThenBranch: $4.stmt,
ElseBranch: $8.stmt,
}
}
// this is the named version, iow, a user-defined function (statement)
// `func name() { <expr> }`
// `func name(<arg>) { <expr> }`
// `func name(<arg>, <arg>) { <expr> }`
| FUNC_IDENTIFIER IDENTIFIER OPEN_PAREN args CLOSE_PAREN OPEN_CURLY expr CLOSE_CURLY
{
posLast(yylex, yyDollar) // our pos
$$.stmt = &StmtFunc{
Name: $2.str,
Func: &ExprFunc{
Args: $4.args,
//Return: nil,
Body: $7.expr,
},
}
}
// `func name(...) <type> { <expr> }`
| FUNC_IDENTIFIER IDENTIFIER OPEN_PAREN args CLOSE_PAREN type OPEN_CURLY expr CLOSE_CURLY
{
posLast(yylex, yyDollar) // our pos
fn := &ExprFunc{
Args: $4.args,
Return: $6.typ, // return type is known
Body: $8.expr,
}
isFullyTyped := $6.typ != nil // true if set
m := make(map[string]*types.Type)
ord := []string{}
for _, a := range $4.args {
if a.Type == nil {
// at least one is unknown, can't run SetType...
isFullyTyped = false
break
}
m[a.Name] = a.Type
ord = append(ord, a.Name)
}
if isFullyTyped {
typ := &types.Type{
Kind: types.KindFunc,
Map: m,
Ord: ord,
Out: $6.typ,
}
if err := fn.SetType(typ); err != nil {
// this will ultimately cause a parser error to occur...
yylex.Error(fmt.Sprintf("%s: %+v", ErrParseSetType, err))
}
}
$$.stmt = &StmtFunc{
Name: $2.str,
Func: fn,
}
}
// `class name { <prog> }`
| CLASS_IDENTIFIER IDENTIFIER OPEN_CURLY prog CLOSE_CURLY
{
posLast(yylex, yyDollar) // our pos
$$.stmt = &StmtClass{
Name: $2.str,
Args: nil,
Body: $4.stmt,
}
}
// `class name(<arg>) { <prog> }`
// `class name(<arg>, <arg>) { <prog> }`
| CLASS_IDENTIFIER IDENTIFIER OPEN_PAREN args CLOSE_PAREN OPEN_CURLY prog CLOSE_CURLY
{
posLast(yylex, yyDollar) // our pos
$$.stmt = &StmtClass{
Name: $2.str,
Args: $4.args,
Body: $7.stmt,
}
}
// `include name`
| INCLUDE_IDENTIFIER dotted_identifier
{
posLast(yylex, yyDollar) // our pos
$$.stmt = &StmtInclude{
Name: $2.str,
}
}
// `include name(...)`
| INCLUDE_IDENTIFIER dotted_identifier OPEN_PAREN call_args CLOSE_PAREN
{
posLast(yylex, yyDollar) // our pos
$$.stmt = &StmtInclude{
Name: $2.str,
Args: $4.exprs,
}
}
// `import "name"`
| IMPORT_IDENTIFIER STRING
{
posLast(yylex, yyDollar) // our pos
$$.stmt = &StmtImport{
Name: $2.str,
//Alias: "",
}
}
// `import "name" as alias`
| IMPORT_IDENTIFIER STRING AS_IDENTIFIER IDENTIFIER
{
posLast(yylex, yyDollar) // our pos
$$.stmt = &StmtImport{
Name: $2.str,
Alias: $4.str,
}
}
// `import "name" as *`
| IMPORT_IDENTIFIER STRING AS_IDENTIFIER MULTIPLY
{
posLast(yylex, yyDollar) // our pos
$$.stmt = &StmtImport{
Name: $2.str,
Alias: $4.str,
}
}
/*
// resource bind
| rbind
{
posLast(yylex, yyDollar) // our pos
$$.stmt = $1.stmt
}
*/
;
expr:
BOOL
{
posLast(yylex, yyDollar) // our pos
$$.expr = &ExprBool{
V: $1.bool,
}
}
| STRING
{
posLast(yylex, yyDollar) // our pos
$$.expr = &ExprStr{
V: $1.str,
}
}
| INTEGER
{
posLast(yylex, yyDollar) // our pos
$$.expr = &ExprInt{
V: $1.int,
}
}
| FLOAT
{
posLast(yylex, yyDollar) // our pos
$$.expr = &ExprFloat{
V: $1.float,
}
}
| list
{
posLast(yylex, yyDollar) // our pos
// TODO: list could be squashed in here directly...
$$.expr = $1.expr
}
| map
{
posLast(yylex, yyDollar) // our pos
// TODO: map could be squashed in here directly...
$$.expr = $1.expr
}
| struct
{
posLast(yylex, yyDollar) // our pos
// TODO: struct could be squashed in here directly...
$$.expr = $1.expr
}
| call
{
posLast(yylex, yyDollar) // our pos
// TODO: call could be squashed in here directly...
$$.expr = $1.expr
}
| var
{
posLast(yylex, yyDollar) // our pos
// TODO: var could be squashed in here directly...
$$.expr = $1.expr
}
| func
{
posLast(yylex, yyDollar) // our pos
// TODO: var could be squashed in here directly...
$$.expr = $1.expr
}
| IF expr OPEN_CURLY expr CLOSE_CURLY ELSE OPEN_CURLY expr CLOSE_CURLY
{
posLast(yylex, yyDollar) // our pos
$$.expr = &ExprIf{
Condition: $2.expr,
ThenBranch: $4.expr,
ElseBranch: $8.expr,
}
}
// parenthesis wrap an expression for precedence
| OPEN_PAREN expr CLOSE_PAREN
{
posLast(yylex, yyDollar) // our pos
$$.expr = $2.expr
}
;
list:
// `[42, 0, -13]`
OPEN_BRACK list_elements CLOSE_BRACK
{
posLast(yylex, yyDollar) // our pos
$$.expr = &ExprList{
Elements: $2.exprs,
}
}
;
list_elements:
/* end of list */
{
posLast(yylex, yyDollar) // our pos
$$.exprs = []interfaces.Expr{}
}
| list_elements list_element
{
posLast(yylex, yyDollar) // our pos
$$.exprs = append($1.exprs, $2.expr)
}
;
list_element:
expr COMMA
{
posLast(yylex, yyDollar) // our pos
$$.expr = $1.expr
}
;
map:
// `{"hello" => "there", "world" => "big",}`
OPEN_CURLY map_kvs CLOSE_CURLY
{
posLast(yylex, yyDollar) // our pos
$$.expr = &ExprMap{
KVs: $2.mapKVs,
}
}
;
map_kvs:
/* end of list */
{
posLast(yylex, yyDollar) // our pos
$$.mapKVs = []*ExprMapKV{}
}
| map_kvs map_kv
{
posLast(yylex, yyDollar) // our pos
$$.mapKVs = append($1.mapKVs, $2.mapKV)
}
;
map_kv:
expr ROCKET expr COMMA
{
posLast(yylex, yyDollar) // our pos
$$.mapKV = &ExprMapKV{
Key: $1.expr,
Val: $3.expr,
}
}
;
struct:
// `struct{answer => 0, truth => false, hello => "world",}`
STRUCT_IDENTIFIER OPEN_CURLY struct_fields CLOSE_CURLY
{
posLast(yylex, yyDollar) // our pos
$$.expr = &ExprStruct{
Fields: $3.structFields,
}
}
;
struct_fields:
/* end of list */
{
posLast(yylex, yyDollar) // our pos
$$.structFields = []*ExprStructField{}
}
| struct_fields struct_field
{
posLast(yylex, yyDollar) // our pos
$$.structFields = append($1.structFields, $2.structField)
}
;
struct_field:
IDENTIFIER ROCKET expr COMMA
{
posLast(yylex, yyDollar) // our pos
$$.structField = &ExprStructField{
Name: $1.str,
Value: $3.expr,
}
}
;
call:
// fmt.printf(...)
dotted_identifier OPEN_PAREN call_args CLOSE_PAREN
{
posLast(yylex, yyDollar) // our pos
$$.expr = &ExprCall{
Name: $1.str,
Args: $3.exprs,
//Var: false, // default
}
}
// calling a function that's stored in a variable (a lambda)
// `$foo(4, "hey")` # call function value
| VAR_IDENTIFIER OPEN_PAREN call_args CLOSE_PAREN
{
posLast(yylex, yyDollar) // our pos
$$.expr = &ExprCall{
Name: $1.str,
Args: $3.exprs,
// XXX: this Var option isn't implemented yet
//Var: true, // lambda
}
}
| expr PLUS expr
{
posLast(yylex, yyDollar) // our pos
$$.expr = &ExprCall{
Name: operatorFuncName,
Args: []interfaces.Expr{
&ExprStr{ // operator first
V: $2.str, // for PLUS this is a `+` character
},
$1.expr,
$3.expr,
},
}
}
| expr MINUS expr
{
posLast(yylex, yyDollar) // our pos
$$.expr = &ExprCall{
Name: operatorFuncName,
Args: []interfaces.Expr{
&ExprStr{ // operator first
V: $2.str,
},
$1.expr,
$3.expr,
},
}
}
| expr MULTIPLY expr
{
posLast(yylex, yyDollar) // our pos
$$.expr = &ExprCall{
Name: operatorFuncName,
Args: []interfaces.Expr{
&ExprStr{ // operator first
V: $2.str,
},
$1.expr,
$3.expr,
},
}
}
| expr DIVIDE expr
{
posLast(yylex, yyDollar) // our pos
$$.expr = &ExprCall{
Name: operatorFuncName,
Args: []interfaces.Expr{
&ExprStr{ // operator first
V: $2.str,
},
$1.expr,
$3.expr,
},
}
}
| expr EQ expr
{
posLast(yylex, yyDollar) // our pos
$$.expr = &ExprCall{
Name: operatorFuncName,
Args: []interfaces.Expr{
&ExprStr{ // operator first
V: $2.str,
},
$1.expr,
$3.expr,
},
}
}
| expr NEQ expr
{
posLast(yylex, yyDollar) // our pos
$$.expr = &ExprCall{
Name: operatorFuncName,
Args: []interfaces.Expr{
&ExprStr{ // operator first
V: $2.str,
},
$1.expr,
$3.expr,
},
}
}
| expr LT expr
{
posLast(yylex, yyDollar) // our pos
$$.expr = &ExprCall{
Name: operatorFuncName,
Args: []interfaces.Expr{
&ExprStr{ // operator first
V: $2.str,
},
$1.expr,
$3.expr,
},
}
}
| expr GT expr
{
posLast(yylex, yyDollar) // our pos
$$.expr = &ExprCall{
Name: operatorFuncName,
Args: []interfaces.Expr{
&ExprStr{ // operator first
V: $2.str,
},
$1.expr,
$3.expr,
},
}
}
| expr LTE expr
{
posLast(yylex, yyDollar) // our pos
$$.expr = &ExprCall{
Name: operatorFuncName,
Args: []interfaces.Expr{
&ExprStr{ // operator first
V: $2.str,
},
$1.expr,
$3.expr,
},
}
}
| expr GTE expr
{
posLast(yylex, yyDollar) // our pos
$$.expr = &ExprCall{
Name: operatorFuncName,
Args: []interfaces.Expr{
&ExprStr{ // operator first
V: $2.str,
},
$1.expr,
$3.expr,
},
}
}
| expr AND expr
{
posLast(yylex, yyDollar) // our pos
$$.expr = &ExprCall{
Name: operatorFuncName,
Args: []interfaces.Expr{
&ExprStr{ // operator first
V: $2.str,
},
$1.expr,
$3.expr,
},
}
}
| expr OR expr
{
posLast(yylex, yyDollar) // our pos
$$.expr = &ExprCall{
Name: operatorFuncName,
Args: []interfaces.Expr{
&ExprStr{ // operator first
V: $2.str,
},
$1.expr,
$3.expr,
},
}
}
| NOT expr
{
posLast(yylex, yyDollar) // our pos
$$.expr = &ExprCall{
Name: operatorFuncName,
Args: []interfaces.Expr{
&ExprStr{ // operator first
V: $1.str,
},
$2.expr,
},
}
}
| VAR_IDENTIFIER_HX
// get the N-th historical value, eg: $foo{3} is equivalent to: history($foo, 3)
{
posLast(yylex, yyDollar) // our pos
$$.expr = &ExprCall{
Name: historyFuncName,
Args: []interfaces.Expr{
&ExprVar{
Name: $1.str,
},
&ExprInt{
V: $1.int,
},
},
}
}
// TODO: fix conflicts with this method, and replace the above VAR_IDENTIFIER_HX
// TODO: allow $pkg.ns.foo{4}, instead of $foo = $pkg.ns.foo ; $foo{4}
// TODO: use this: dotted_var_identifier OPEN_BRACK INTEGER CLOSE_BRACK instead?
//| dotted_var_identifier OPEN_CURLY INTEGER CLOSE_CURLY
// {
// posLast(yylex, yyDollar) // our pos
// $$.expr = &ExprCall{
// Name: historyFuncName,
// Args: []interfaces.Expr{
// &ExprVar{
// Name: $1.str,
// },
// &ExprInt{
// V: $3.int,
// },
// },
// }
// }
| expr IN expr
{
posLast(yylex, yyDollar) // our pos
$$.expr = &ExprCall{
Name: containsFuncName,
Args: []interfaces.Expr{
$1.expr,
$3.expr,
},
}
}
;
// list order gets us the position of the arg, but named params would work too!
// this is also used by the include statement when the called class uses args!
call_args:
/* end of list */
{
posLast(yylex, yyDollar) // our pos
$$.exprs = []interfaces.Expr{}
}
// seems that "left recursion" works here... thanks parser generator!
| call_args COMMA expr
{
posLast(yylex, yyDollar) // our pos
$$.exprs = append($1.exprs, $3.expr)
}
| expr
{
posLast(yylex, yyDollar) // our pos
$$.exprs = append([]interfaces.Expr{}, $1.expr)
}
;
var:
dotted_var_identifier
{
posLast(yylex, yyDollar) // our pos
$$.expr = &ExprVar{
Name: $1.str,
}
}
;
func:
// this is the lambda version, iow, a function as a value (expression)
// `func() { <expr> }`
// `func(<arg>) { <expr> }`
// `func(<arg>, <arg>) { <expr> }`
FUNC_IDENTIFIER OPEN_PAREN args CLOSE_PAREN OPEN_CURLY expr CLOSE_CURLY
{
posLast(yylex, yyDollar) // our pos
$$.expr = &ExprFunc{
Args: $3.args,
//Return: nil,
Body: $6.expr,
}
}
// `func(...) <type> { <expr> }`
| FUNC_IDENTIFIER OPEN_PAREN args CLOSE_PAREN type OPEN_CURLY expr CLOSE_CURLY
{
posLast(yylex, yyDollar) // our pos
$$.expr = &ExprFunc{
Args: $3.args,
Return: $5.typ, // return type is known
Body: $7.expr,
}
isFullyTyped := $5.typ != nil // true if set
m := make(map[string]*types.Type)
ord := []string{}
for _, a := range $3.args {
if a.Type == nil {
// at least one is unknown, can't run SetType...
isFullyTyped = false
break
}
m[a.Name] = a.Type
ord = append(ord, a.Name)
}
if isFullyTyped {
typ := &types.Type{
Kind: types.KindFunc,
Map: m,
Ord: ord,
Out: $5.typ,
}
if err := $$.expr.SetType(typ); err != nil {
// this will ultimately cause a parser error to occur...
yylex.Error(fmt.Sprintf("%s: %+v", ErrParseSetType, err))
}
}
}
;
args:
/* end of list */
{
posLast(yylex, yyDollar) // our pos
$$.args = []*Arg{}
}
| args COMMA arg
{
posLast(yylex, yyDollar) // our pos
$$.args = append($1.args, $3.arg)
}
| arg
{
posLast(yylex, yyDollar) // our pos
$$.args = append([]*Arg{}, $1.arg)
}
;
arg:
// `$x`
VAR_IDENTIFIER
{
$$.arg = &Arg{
Name: $1.str,
}
}
// `$x <type>`
| VAR_IDENTIFIER type
{
$$.arg = &Arg{
Name: $1.str,
Type: $2.typ,
}
}
;
bind:
VAR_IDENTIFIER EQUALS expr
{
posLast(yylex, yyDollar) // our pos
$$.stmt = &StmtBind{
Ident: $1.str,
Value: $3.expr,
}
}
| VAR_IDENTIFIER type EQUALS expr
{
posLast(yylex, yyDollar) // our pos
var expr interfaces.Expr = $4.expr
if err := expr.SetType($2.typ); err != nil {
// this will ultimately cause a parser error to occur...
yylex.Error(fmt.Sprintf("%s: %+v", ErrParseSetType, err))
}
$$.stmt = &StmtBind{
Ident: $1.str,
Value: expr,
}
}
;
/* TODO: do we want to include this?
// resource bind
rbind:
VAR_IDENTIFIER EQUALS resource
{
posLast(yylex, yyDollar) // our pos
// XXX: this kind of bind is different than the others, because
// it can only really be used for send->recv stuff, eg:
// foo.SomeString -> bar.SomeOtherString
$$.expr = &StmtBind{
Ident: $1.str,
Value: $3.stmt,
}
}
;
*/
resource:
// `file "/tmp/hello" { ... }` or `aws:ec2 "/tmp/hello" { ... }`
RES_IDENTIFIER expr OPEN_CURLY resource_body CLOSE_CURLY
{
posLast(yylex, yyDollar) // our pos
$$.stmt = &StmtRes{
Kind: $1.str,
Name: $2.expr,
Contents: $4.resContents,
}
}
// note: this is a simplified version of the above if the lexer picks it
// note: must not include underscores, but that is checked after parsing
| IDENTIFIER expr OPEN_CURLY resource_body CLOSE_CURLY
{
posLast(yylex, yyDollar) // our pos
$$.stmt = &StmtRes{
Kind: $1.str,
Name: $2.expr,
Contents: $4.resContents,
}
}
;
resource_body:
/* end of list */
{
posLast(yylex, yyDollar) // our pos
$$.resContents = []StmtResContents{}
}
| resource_body resource_field
{
posLast(yylex, yyDollar) // our pos
$$.resContents = append($1.resContents, $2.resField)
}
| resource_body conditional_resource_field
{
posLast(yylex, yyDollar) // our pos
$$.resContents = append($1.resContents, $2.resField)
}
| resource_body resource_edge
{
posLast(yylex, yyDollar) // our pos
$$.resContents = append($1.resContents, $2.resEdge)
}
| resource_body conditional_resource_edge
{
posLast(yylex, yyDollar) // our pos
$$.resContents = append($1.resContents, $2.resEdge)
}
;
resource_field:
IDENTIFIER ROCKET expr COMMA
{
posLast(yylex, yyDollar) // our pos
$$.resField = &StmtResField{
Field: $1.str,
Value: $3.expr,
}
}
;
conditional_resource_field:
// content => $present ?: "hello",
IDENTIFIER ROCKET expr ELVIS expr COMMA
{
posLast(yylex, yyDollar) // our pos
$$.resField = &StmtResField{
Field: $1.str,
Value: $5.expr,
Condition: $3.expr,
}
}
;
resource_edge:
// Before => Test["t1"],
CAPITALIZED_IDENTIFIER ROCKET edge_half COMMA
{
posLast(yylex, yyDollar) // our pos
$$.resEdge = &StmtResEdge{
Property: $1.str,
EdgeHalf: $3.edgeHalf,
}
}
;
conditional_resource_edge:
// Before => $present ?: Test["t1"],
CAPITALIZED_IDENTIFIER ROCKET expr ELVIS edge_half COMMA
{
posLast(yylex, yyDollar) // our pos
$$.resEdge = &StmtResEdge{
Property: $1.str,
EdgeHalf: $5.edgeHalf,
Condition: $3.expr,
}
}
;
edge:
// TODO: we could technically prevent single edge_half pieces from being
// parsed, but it's probably more work than is necessary...
// Test["t1"] -> Test["t2"] -> Test["t3"] # chain or pair
edge_half_list
{
posLast(yylex, yyDollar) // our pos
$$.stmt = &StmtEdge{
EdgeHalfList: $1.edgeHalfList,
//Notify: false, // unused here
}
}
// Test["t1"].foo_send -> Test["t2"].blah_recv # send/recv
| edge_half_sendrecv ARROW edge_half_sendrecv
{
posLast(yylex, yyDollar) // our pos
$$.stmt = &StmtEdge{
EdgeHalfList: []*StmtEdgeHalf{
$1.edgeHalf,
$3.edgeHalf,
},
//Notify: false, // unused here, it is implied (i think)
}
}
;
edge_half_list:
edge_half
{
posLast(yylex, yyDollar) // our pos
$$.edgeHalfList = []*StmtEdgeHalf{$1.edgeHalf}
}
| edge_half_list ARROW edge_half
{
posLast(yylex, yyDollar) // our pos
$$.edgeHalfList = append($1.edgeHalfList, $3.edgeHalf)
}
;
edge_half:
// eg: Test["t1"]
CAPITALIZED_RES_IDENTIFIER OPEN_BRACK expr CLOSE_BRACK
{
posLast(yylex, yyDollar) // our pos
$$.edgeHalf = &StmtEdgeHalf{
Kind: $1.str,
Name: $3.expr,
//SendRecv: "", // unused
}
}
// note: this is a simplified version of the above if the lexer picks it
// note: must not include underscores, but that is checked after parsing
| CAPITALIZED_IDENTIFIER OPEN_BRACK expr CLOSE_BRACK
{
posLast(yylex, yyDollar) // our pos
$$.edgeHalf = &StmtEdgeHalf{
Kind: $1.str,
Name: $3.expr,
//SendRecv: "", // unused
}
}
;
edge_half_sendrecv:
// eg: Test["t1"].foo_send
CAPITALIZED_RES_IDENTIFIER OPEN_BRACK expr CLOSE_BRACK DOT IDENTIFIER
{
posLast(yylex, yyDollar) // our pos
$$.edgeHalf = &StmtEdgeHalf{
Kind: $1.str,
Name: $3.expr,
SendRecv: $6.str,
}
}
// note: this is a simplified version of the above if the lexer picks it
// note: must not include underscores, but that is checked after parsing
| CAPITALIZED_IDENTIFIER OPEN_BRACK expr CLOSE_BRACK DOT IDENTIFIER
{
posLast(yylex, yyDollar) // our pos
$$.edgeHalf = &StmtEdgeHalf{
Kind: $1.str,
Name: $3.expr,
SendRecv: $6.str,
}
}
;
type:
BOOL_IDENTIFIER
{
posLast(yylex, yyDollar) // our pos
$$.typ = types.NewType($1.str) // "bool"
}
| STR_IDENTIFIER
{
posLast(yylex, yyDollar) // our pos
$$.typ = types.NewType($1.str) // "str"
}
| INT_IDENTIFIER
{
posLast(yylex, yyDollar) // our pos
$$.typ = types.NewType($1.str) // "int"
}
| FLOAT_IDENTIFIER
{
posLast(yylex, yyDollar) // our pos
$$.typ = types.NewType($1.str) // "float"
}
| OPEN_BRACK CLOSE_BRACK type
// list: []int or [][]str (with recursion)
{
posLast(yylex, yyDollar) // our pos
$$.typ = types.NewType("[]" + $3.typ.String())
}
| MAP_IDENTIFIER OPEN_CURLY type COLON type CLOSE_CURLY
// map: map{str: int} or map{str: []int}
{
posLast(yylex, yyDollar) // our pos
$$.typ = types.NewType(fmt.Sprintf("map{%s: %s}", $3.typ.String(), $5.typ.String()))
}
| STRUCT_IDENTIFIER OPEN_CURLY type_struct_fields CLOSE_CURLY
// struct: struct{} or struct{a bool} or struct{a bool; bb int}
{
posLast(yylex, yyDollar) // our pos
$$.typ = types.NewType(fmt.Sprintf("%s{%s}", $1.str, strings.Join($3.strSlice, "; ")))
}
| VARIANT_IDENTIFIER
{
posLast(yylex, yyDollar) // our pos
$$.typ = types.NewType($1.str) // "variant"
}
;
type_struct_fields:
/* end of list */
{
posLast(yylex, yyDollar) // our pos
$$.strSlice = []string{}
}
| type_struct_fields SEMICOLON type_struct_field
{
posLast(yylex, yyDollar) // our pos
$$.strSlice = append($1.strSlice, $3.str)
}
| type_struct_field
{
posLast(yylex, yyDollar) // our pos
$$.strSlice = []string{$1.str}
}
;
type_struct_field:
IDENTIFIER type
{
posLast(yylex, yyDollar) // our pos
$$.str = fmt.Sprintf("%s %s", $1.str, $2.typ.String())
}
;
dotted_identifier:
IDENTIFIER
{
posLast(yylex, yyDollar) // our pos
$$.str = $1.str
}
| dotted_identifier DOT IDENTIFIER
{
posLast(yylex, yyDollar) // our pos
$$.str = $1.str + interfaces.ModuleSep + $3.str
}
;
// there are different ways the lexer/parser might choose to represent this...
dotted_var_identifier:
// eg: $foo (no dots)
VAR_IDENTIFIER
{
posLast(yylex, yyDollar) // our pos
$$.str = $1.str
}
// eg: $foo . bar.baz (identifier + dotted identifier)
| VAR_IDENTIFIER DOT dotted_identifier
{
posLast(yylex, yyDollar) // our pos
$$.str = $1.str + interfaces.ModuleSep + $3.str
}
// eg: $ foo.bar.baz (dollar prefix + dotted identifier)
| DOLLAR dotted_identifier
{
posLast(yylex, yyDollar) // our pos
$$.str = $2.str
}
;
%%
// pos is a helper function used to track the position in the parser.
func pos(y yyLexer, dollar yySymType) {
lp := cast(y)
lp.row = dollar.row
lp.col = dollar.col
// FIXME: in some cases the before last value is most meaningful...
//lp.row = append(lp.row, dollar.row)
//lp.col = append(lp.col, dollar.col)
//log.Printf("parse: %d x %d", lp.row, lp.col)
return
}
// cast is used to pull out the parser run-specific struct we store our AST in.
// this is usually called in the parser.
func cast(y yyLexer) *lexParseAST {
x := y.(*Lexer).parseResult
return x.(*lexParseAST)
}
// postLast pulls out the "last token" and does a pos with that. This is a hack!
func posLast(y yyLexer, dollars []yySymType) {
// pick the last token in the set matched by the parser
pos(y, dollars[len(dollars)-1]) // our pos
}
// cast is used to pull out the parser run-specific struct we store our AST in.
// this is usually called in the lexer.
func (yylex *Lexer) cast() *lexParseAST {
return yylex.parseResult.(*lexParseAST)
}
// pos is a helper function used to track the position in the lexer.
func (yylex *Lexer) pos(lval *yySymType) {
lval.row = yylex.Line()
lval.col = yylex.Column()
// TODO: we could use: `s := yylex.Text()` to calculate a delta length!
//log.Printf("lexer: %d x %d", lval.row, lval.col)
}
// Error is the error handler which gets called on a parsing error.
func (yylex *Lexer) Error(str string) {
lp := yylex.cast()
if str != "" {
// This error came from the parser. It is usually also set when
// the lexer fails, because it ends up generating ERROR tokens,
// which most parsers usually don't match and store in the AST.
err := ErrParseError // TODO: add more specific types...
if strings.HasSuffix(str, ErrParseAdditionalEquals.Error()) {
err = ErrParseAdditionalEquals
} else if strings.HasSuffix(str, ErrParseExpectingComma.Error()) {
err = ErrParseExpectingComma
} else if strings.HasPrefix(str, ErrParseSetType.Error()) {
err = ErrParseSetType
}
lp.parseErr = &LexParseErr{
Err: err,
Str: str,
// FIXME: get these values, by tracking pos in parser...
// FIXME: currently, the values we get are mostly wrong!
Row: lp.row, //lp.row[len(lp.row)-1],
Col: lp.col, //lp.col[len(lp.col)-1],
}
}
}