func (p *Parser) literal() (node *tp.Instruction) { token := p.pop() switch token.Lexeme { case STRING: node = tp.MakeText(token.Value, token.LineNumber) case REGEXP: node = tp.MakeFunctionCall("regexp", tp.ListInstructions(tp.MakeText(token.Value, token.LineNumber), tp.MakeText(token.ExtraValue, token.LineNumber)), nil, token.LineNumber) case POS: node = tp.MakePosition(token.Value, token.LineNumber) } return node }
func (p *Parser) read() (node *tp.Instruction) { p.pop() // pop the "read" keyword readLineNo := p.peek().LineNumber if p.peek().Lexeme != LPAREN { p.error("argument list expected for read") } p.pop() // pop the lparen if p.peek().Lexeme != STRING { p.error("`read` requires a literal string argument") } readPath := p.pop().Value readDir := "" if p.peek().Lexeme == COMMA { p.pop() if p.peek().Lexeme != STRING { p.error("second argument to `read` must be a literal string") } readDir = p.pop().Value if filepath.IsAbs(readDir) { panic(fmt.Sprintf("%s:%d -- second argument to `read` must be a relative path", p.FileName, readLineNo)) } } if p.peek().Lexeme != RPAREN { p.error("unterminated argument list in read") } p.pop() // pop the rparen // make sure we're not trying to read outside the project folder fullReadPath := "" if len(readDir) > 0 { fullReadPath = filepath.Clean(filepath.Join(p.ProjectPath, readDir, readPath)) } else { fullReadPath = filepath.Clean(filepath.Join(p.ProjectPath, p.ScriptPath, readPath)) } absReadPath, err := filepath.Abs(fullReadPath) if err != nil { msg := fmt.Sprintf("%s:%d -- `read` could not resolve the full path to %s", p.FileName, readLineNo, readPath) panic(msg) } if !strings.HasPrefix(absReadPath, filepath.Join(p.ProjectPath)) { msg := fmt.Sprintf("%s:%d -- `read` cannot open files outside the project folder", p.FileName, readLineNo) panic(msg) } contents, err := ioutil.ReadFile(absReadPath) if err != nil { // can't use p.error because it's not a syntax error var msgPath string if len(readDir) == 0 { msgPath = filepath.Join(p.ScriptPath, readPath) } else { msgPath = filepath.Join(readDir, readPath) } msg := fmt.Sprintf("%s:%d -- `read` could not open %s", p.FileName, readLineNo, msgPath) panic(msg) } node = tp.MakeText(string(contents), readLineNo) return node }
func (p *Parser) variable(ns string) (node *tp.Instruction) { // ns = strings.Split(ns, ",")[0] // only use the first namespace -- can't efficiently search namespaces for global vars token := p.pop() lexeme, name, lineNo := token.Lexeme, token.Value, token.LineNumber sigil := "$" if lexeme == LVAR { sigil = "%" } var val *tp.Instruction var block []*tp.Instruction if p.peek().Lexeme == EQUAL { p.pop() // pop the equal sign switch p.peek().Lexeme { case STRING, REGEXP, POS, READ, ID, TYPE, GVAR, LVAR, LPAREN: val = p.expression() default: p.error("invalid expression in assignment to " + sigil + name) } } if p.peek().Lexeme == LBRACE { block = p.block() } if lexeme == LVAR { node = tp.MakeLocalVar(name, val, block, lineNo) } else { fullVarName := name // if the namespace is 'tritium', leave it off -- necessary to avoid breaking all that global capture stuff if ns != "tritium" { fullVarName = fmt.Sprintf("%s.%s", ns, name) } args := tp.ListInstructions(tp.MakeText(fullVarName, lineNo)) if val != nil { args = append(args, val) } node = tp.MakeFunctionCall("var", args, block, lineNo) } return node }
func (p *Parser) expression() (node *tp.Instruction) { terms := tp.ListInstructions(p.term()) for p.peek().Lexeme == PLUS { p.pop() // pop the plus sign switch p.peek().Lexeme { case STRING, REGEXP, POS, READ, ID, TYPE, GVAR, LVAR, LPAREN: rhs := p.term() last := len(terms) - 1 if terms[last].GetType() == constants.Instruction_TEXT && rhs.GetType() == constants.Instruction_TEXT { terms[last] = tp.MakeText(terms[last].GetValue()+rhs.GetValue(), terms[last].GetLineNumber()) } else { terms = append(terms, rhs) } default: p.error("argument to `+` must be a self-contained expression") } } if len(terms) > 1 { node = tp.FoldLeft("concat", terms[0], terms[1:len(terms)]) } else { node = terms[0] } return node }
func (p *Parser) call(funcName *Token) (node *tp.Instruction) { funcNameStr := funcName.Value // grab the function name funcLineNo := funcName.LineNumber if p.peek().Lexeme != LPAREN { p.error("parenthesized argument list expected in call to " + funcNameStr) } p.pop() // pop the lparen ords, kwdnames, kwdvals := p.arguments(funcNameStr) // gather the arguments numArgs := len(ords) // this will never happen because p.arguments() only returns when it encounters an rparen if p.peek().Lexeme != RPAREN { p.error("unterminated argument list in call to " + funcNameStr) } p.pop() // pop the rparen var block []*tp.Instruction if p.peek().Lexeme == LBRACE { block = p.block() } // Expand keyword args if kwdnames != nil && kwdvals != nil { kwdToGensym := make(map[string]string, len(kwdnames)) outer := tp.ListInstructions() for i, k := range kwdnames { tempname := p.gensym() tempvar := tp.MakeFunctionCall("var", tp.ListInstructions(tp.MakeText(tempname, funcLineNo), kwdvals[i]), nil, funcLineNo) outer = append(outer, tempvar) kwdToGensym[k] = tempname } inner := tp.ListInstructions() for _, k := range kwdnames { getter := tp.MakeFunctionCall("var", tp.ListInstructions(tp.MakeText(kwdToGensym[k], funcLineNo)), nil, funcLineNo) setter := tp.MakeFunctionCall("set", tp.ListInstructions(tp.MakeText(k, funcLineNo), getter), nil, funcLineNo) inner = append(inner, setter) } if block != nil { for _, v := range block { inner = append(inner, v) } } theCall := tp.MakeFunctionCall(funcNameStr, ords, inner, funcLineNo) outer = append(outer, theCall) node = tp.MakeBlock(outer, funcLineNo) } else if funcNameStr == "concat" && numArgs > 2 { // expand variadic concat into nested binary concats lhs := tp.FoldLeft("concat", ords[0], ords[1:numArgs-1]) rhs := ords[numArgs-1] node = tp.MakeFunctionCall("concat", tp.ListInstructions(lhs, rhs), block, funcLineNo) } else if funcNameStr == "log" && numArgs > 1 { // expand variadic log into composition of log and concat cats := tp.FoldLeft("concat", ords[0], ords[1:]) node = tp.MakeFunctionCall("log", tp.ListInstructions(cats), block, funcLineNo) } else { node = tp.MakeFunctionCall(funcNameStr, ords, block, funcLineNo) } // if it's not a root file, we can assume that it's a user-called function if !p.CompilingMixer /* p.RootFile == false && IncludeSelectorInfo == true */ { node.IsUserCalled = proto.Bool(true) } return node }
func (p *Parser) statement() (node *tp.Instruction) { switch p.peek().Lexeme { case IMPORT, OPTIONAL: optional := false if p.peek().Lexeme == OPTIONAL { optional = true } if p.inFunc { panic(fmt.Sprintf("|%s:%d -- imports not allowed inside function definitions", p.FileName, p.peek().LineNumber)) } token := p.pop() // pop the "@import" or "@optional" token (includes importee) importPath := token.Value layered := false appliedLayers := p.AppliedLayers if strings.Index(importPath, "@") != -1 { if len(p.Layers) == 0 { if !optional { panic(fmt.Sprintf("%s:%d -- required layer not provided; please make sure you've specified all necessary layers in the start-up options", p.FileName, token.LineNumber)) } else { // make a no-op if the import is optional and no layer has been provided node = tp.MakeText("", token.LineNumber) return } } if len(appliedLayers) > 0 { tmpSlice := make([]string, 2) tmpSlice[0] = appliedLayers tmpSlice[1] = p.Layers[0] appliedLayers = strings.Join(tmpSlice, "/") } else { appliedLayers = p.Layers[0] } importPath = strings.Replace(importPath, "@", p.Layers[0], -1) layered = true } scriptLocationInProject := filepath.Clean(filepath.Join(p.ScriptPath, importPath)) // extract the root script folder from the relative path of the importee // (would be easier if filepath.FromSlash worked as advertised) dir, base := filepath.Split(p.ScriptPath) if len(dir) == 0 { dir = base base = "" } if dir[len(dir)-1] == os.PathSeparator { dir = dir[0 : len(dir)-1] } for len(base) > 0 { dir, base = filepath.Split(dir) if len(dir) == 0 { dir = base base = "" } if dir[len(dir)-1] == os.PathSeparator { dir = dir[0 : len(dir)-1] } } // make sure that the importee is under the right subfolder if !strings.HasPrefix(scriptLocationInProject, dir) { msg := fmt.Sprintf("%s:%d -- imported file must exist under the `%s` folder", p.FileName, token.LineNumber, dir) panic(msg) } // now make sure the file exists and give a helpful message if it's a layer file exists, exErr := fileutil.Exists(filepath.Join(p.ProjectPath, scriptLocationInProject)) if !exists || exErr != nil { if optional { // make a no-op if the import is optional node = tp.MakeText("", token.LineNumber) return } else if layered { panic(fmt.Sprintf("%s:%d -- required layer (%s) has no corresponding Tritium file (want %s)", p.FileName, p.LineNumber, appliedLayers, scriptLocationInProject)) } else { panic(fmt.Sprintf("%s:%d -- file to import not found (%s)", p.FileName, p.LineNumber, scriptLocationInProject)) } } node = tp.MakeImport(scriptLocationInProject, token.LineNumber) if layered { node.Namespace = proto.String(appliedLayers) // re-use this slot to specify which layer the import is targeting } case STRING, REGEXP, POS, READ, ID, TYPE, GVAR, LVAR, LPAREN: node = p.expression() case NAMESPACE: p.error("`@namespace` directive must occur at the top of a file or code block") default: p.error("statement must consist of import or expression") } return node }