github.com/lmorg/murex@v0.0.0-20240217211045-e081c89cd4ef/lang/functions.go (about)

     1  package lang
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"strings"
     7  	"sync"
     8  
     9  	"github.com/lmorg/murex/lang/ref"
    10  	"github.com/lmorg/murex/lang/types"
    11  	"github.com/lmorg/murex/utils/readline"
    12  )
    13  
    14  // MurexFuncs is a table of murex functions
    15  type MurexFuncs struct {
    16  	mutex sync.Mutex
    17  	fn    map[string]*murexFuncDetails
    18  }
    19  
    20  // MurexFuncDetails is the properties for any given murex function
    21  type murexFuncDetails struct {
    22  	Block      []rune
    23  	Summary    string
    24  	Parameters []MxFunctionParams
    25  	FileRef    *ref.File
    26  }
    27  
    28  type MxFunctionParams struct {
    29  	Name        string
    30  	DataType    string
    31  	Description string
    32  	Default     string
    33  }
    34  
    35  // NewMurexFuncs creates a new table of murex functions
    36  func NewMurexFuncs() *MurexFuncs {
    37  	mf := new(MurexFuncs)
    38  	mf.fn = make(map[string]*murexFuncDetails)
    39  
    40  	return mf
    41  }
    42  
    43  func funcSummary(block []rune) string {
    44  	var (
    45  		line1   bool
    46  		comment bool
    47  		summary []rune
    48  	)
    49  
    50  	for _, r := range block {
    51  		switch {
    52  		case r == '\r':
    53  			continue
    54  
    55  		case r == '\n' && !line1:
    56  			line1 = true
    57  
    58  		case r == '\n' && (line1 || comment):
    59  			goto exitParser
    60  
    61  		case r == '#':
    62  			comment = true
    63  			line1 = true
    64  
    65  		case !line1 && (r == '{' || r == ' ' || r == '\t'):
    66  			continue
    67  
    68  		case comment && r == '\t':
    69  			summary = append(summary, ' ', ' ', ' ', ' ')
    70  
    71  		case comment:
    72  			summary = append(summary, r)
    73  
    74  		case line1 && (r == ' ' || r == '\t'):
    75  			continue
    76  
    77  		default:
    78  			return ""
    79  		}
    80  	}
    81  
    82  exitParser:
    83  	return strings.TrimSpace(string(summary))
    84  }
    85  
    86  const ( // function parameter error messages
    87  	fpeUnexpectedWhiteSpace    = "unexpected whitespace character (chr %d) at %d (%d,%d)"
    88  	fpeUnexpectedNewLine       = "unexpected new line at %d (%d,%d)"
    89  	fpeUnexpectedComma         = "unexpected comma at %d (%d,%d)"
    90  	fpeUnexpectedCharacter     = "unexpected character '%s' (chr %d) at %d (%d,%d)"
    91  	fpeUnexpectedColon         = "unexpected colon ':' (chr %d) at %d (%d,%d)"
    92  	fpeUnexpectedQuotationMark = "unexpected quotation mark '\"' (chr %d) at %d (%d,%d)"
    93  	fpeUnexpectedEndSquare     = "unexpected closing square bracket ']' (chr %d) at %d (%d,%d)"
    94  	fpeEofNameStart            = "missing variable name at %d (%d,%d)"
    95  	fpeEofNameRead             = "variable name not terminated with a colon %d (%d,%d)"
    96  	fpeEofTypeStart            = "missing data type %d (%d,%d)"
    97  	fpeEofDescRead             = "missing closing quotation mark on description %d (%d,%d)"
    98  	fpeEofDefaultRead          = "missing closing square bracket on default %d (%d,%d)"
    99  	fpeParameterNoName         = "parameter %d is missing a name"
   100  	fpeParameterNoDataType     = "parameter %d is missing a data type"
   101  )
   102  
   103  const ( // function parameter contexts
   104  	fpcNameStart = 0
   105  	fpcNameRead  = iota
   106  	fpcTypeStart
   107  	fpcTypeRead
   108  	fpcDescStart
   109  	fpcDescRead
   110  	fpcDescEnd
   111  	fpcDefaultRead
   112  	fpcDefaultEnd
   113  )
   114  
   115  // Parse the function parameter and data type block
   116  func ParseMxFunctionParameters(parameters string) ([]MxFunctionParams, error) {
   117  	/* function example (
   118  		name: str [Bob] "User name",
   119  		age:  num [100] "How old are you?"
   120  	   ) {}*/
   121  
   122  	var (
   123  		context int
   124  		counter int
   125  		x, y    = 0, 1
   126  	)
   127  
   128  	mfp := make([]MxFunctionParams, 1)
   129  
   130  	for i, r := range parameters {
   131  		x++
   132  
   133  		switch r {
   134  		case '\r':
   135  			// do nothing
   136  
   137  		case '\n':
   138  			switch context {
   139  			case fpcNameStart, fpcDescEnd, fpcDefaultEnd:
   140  				y++
   141  				x = 1
   142  			default:
   143  				return nil, fmt.Errorf(fpeUnexpectedNewLine, i+1, y, x)
   144  			}
   145  
   146  		case ' ', '\t':
   147  			switch context {
   148  			case fpcNameRead:
   149  				return nil, fmt.Errorf(fpeUnexpectedWhiteSpace, r, i+1, y, x)
   150  			case fpcTypeRead:
   151  				context++
   152  			case fpcDescRead:
   153  				mfp[counter].Description += " "
   154  			case fpcDefaultRead:
   155  				mfp[counter].Default += " "
   156  			default:
   157  				// do nothing
   158  				continue
   159  			}
   160  
   161  		case ':':
   162  			switch context {
   163  			case fpcNameRead:
   164  				context++
   165  			case fpcDescRead:
   166  				mfp[counter].Description += ":"
   167  			case fpcDefaultRead:
   168  				mfp[counter].Default += ":"
   169  			default:
   170  				return nil, fmt.Errorf(fpeUnexpectedColon, r, i+1, y, x)
   171  			}
   172  
   173  		case '"':
   174  			switch context {
   175  			case fpcDefaultRead:
   176  				mfp[counter].Default += "\""
   177  			case fpcDescStart, fpcDescRead:
   178  				context++
   179  			case fpcDefaultEnd:
   180  				context = fpcDescRead
   181  			default:
   182  				return nil, fmt.Errorf(fpeUnexpectedQuotationMark, r, i+1, y, x)
   183  			}
   184  
   185  		case '[':
   186  			switch context {
   187  			case fpcDescRead:
   188  				mfp[counter].Description += "["
   189  			case fpcDefaultRead:
   190  				mfp[counter].Default += "["
   191  			case fpcDescStart, fpcDescEnd:
   192  				context = fpcDefaultRead
   193  			}
   194  
   195  		case ']':
   196  			switch context {
   197  			case fpcDescRead:
   198  				mfp[counter].Description += "]"
   199  			case fpcDefaultRead:
   200  				context++
   201  			default:
   202  				return nil, fmt.Errorf(fpeUnexpectedEndSquare, r, i+1, y, x)
   203  			}
   204  
   205  		case ',':
   206  			switch context {
   207  			case fpcDescRead:
   208  				mfp[counter].Description += ","
   209  			case fpcDefaultRead:
   210  				mfp[counter].Default += ","
   211  			case fpcNameRead:
   212  				mfp[counter].DataType = types.String
   213  				mfp = append(mfp, MxFunctionParams{})
   214  				counter++
   215  				context = fpcNameStart
   216  			case fpcTypeRead, fpcDescEnd, fpcDefaultEnd:
   217  				mfp = append(mfp, MxFunctionParams{})
   218  				counter++
   219  				context = fpcNameStart
   220  			default:
   221  				return nil, fmt.Errorf(fpeUnexpectedComma, i+1, y, x)
   222  			}
   223  
   224  		default:
   225  			if (r >= 'a' && 'z' >= r) ||
   226  				(r >= 'A' && 'Z' >= r) ||
   227  				(r >= '0' && '9' >= r) ||
   228  				r == '_' || r == '-' {
   229  
   230  				switch context {
   231  				case fpcNameStart:
   232  					context++
   233  					fallthrough
   234  				case fpcNameRead:
   235  					mfp[counter].Name += string([]rune{r})
   236  					continue
   237  				case fpcTypeStart:
   238  					context++
   239  					fallthrough
   240  				case fpcTypeRead:
   241  					mfp[counter].DataType += string([]rune{r})
   242  					continue
   243  				case fpcDescRead:
   244  					mfp[counter].Description += string([]rune{r})
   245  					continue
   246  				case fpcDefaultRead:
   247  					mfp[counter].Default += string([]rune{r})
   248  					continue
   249  				}
   250  			}
   251  
   252  			switch context {
   253  			case fpcDescRead:
   254  				mfp[counter].Description += string([]rune{r})
   255  			case fpcDefaultRead:
   256  				mfp[counter].Default += string([]rune{r})
   257  			default:
   258  				return nil, fmt.Errorf(fpeUnexpectedCharacter, string([]rune{r}), r, i+1, y, x)
   259  			}
   260  		}
   261  	}
   262  
   263  	switch context {
   264  	case fpcNameStart:
   265  		return nil, fmt.Errorf(fpeEofNameStart, len(parameters), y, x)
   266  	case fpcNameRead:
   267  		//return nil, fmt.Errorf(fpeEofNameRead, len(parameters), y, x)
   268  		mfp[counter].DataType = types.String
   269  	case fpcTypeStart:
   270  		return nil, fmt.Errorf(fpeEofTypeStart, len(parameters), y, x)
   271  	case fpcDescRead:
   272  		return nil, fmt.Errorf(fpeEofDescRead, len(parameters), y, x)
   273  	case fpcDefaultRead:
   274  		return nil, fmt.Errorf(fpeEofDefaultRead, len(parameters), y, x)
   275  	}
   276  
   277  	for i := range mfp {
   278  		if mfp[i].Name == "" {
   279  			return nil, fmt.Errorf(fpeParameterNoName, i+1)
   280  		}
   281  		if mfp[i].DataType == "" {
   282  			return nil, fmt.Errorf(fpeParameterNoDataType, i+1)
   283  		}
   284  	}
   285  
   286  	return mfp, nil
   287  }
   288  
   289  func (mfd *murexFuncDetails) castParameters(p *Process) error {
   290  	for i := range mfd.Parameters {
   291  		s, err := p.Parameters.String(i)
   292  		if err != nil {
   293  			if p.Background.Get() {
   294  				return fmt.Errorf("cannot prompt for parameters when a function is run in the background: %s", err.Error())
   295  			}
   296  
   297  			prompt := mfd.Parameters[i].Description
   298  			if prompt == "" {
   299  				prompt = "Please enter a value for '" + mfd.Parameters[i].Name + "'"
   300  			}
   301  			if len(mfd.Parameters[i].Default) > 0 {
   302  				prompt += " [" + mfd.Parameters[i].Default + "]"
   303  			}
   304  			rl := readline.NewInstance()
   305  			rl.SetPrompt(prompt + ": ")
   306  			rl.History = new(readline.NullHistory)
   307  
   308  			s, err = rl.Readline()
   309  			if err != nil {
   310  				return err
   311  			}
   312  
   313  			if s == "" {
   314  				s = mfd.Parameters[i].Default
   315  			}
   316  		}
   317  
   318  		v, err := types.ConvertGoType(s, mfd.Parameters[i].DataType)
   319  		if err != nil {
   320  			return fmt.Errorf("cannot convert parameter %d '%s' to data type '%s'", i+1, s, mfd.Parameters[i].DataType)
   321  		}
   322  		err = p.Variables.Set(p, mfd.Parameters[i].Name, v, mfd.Parameters[i].DataType)
   323  		if err != nil {
   324  			return fmt.Errorf("cannot set function variable: %s", err.Error())
   325  		}
   326  	}
   327  
   328  	return nil
   329  }
   330  
   331  // Define creates a function
   332  func (mf *MurexFuncs) Define(name string, parameters []MxFunctionParams, block []rune, fileRef *ref.File) {
   333  	summary := funcSummary(block)
   334  
   335  	mf.mutex.Lock()
   336  	mf.fn[name] = &murexFuncDetails{
   337  		Block:      block,
   338  		Parameters: parameters,
   339  		FileRef:    fileRef,
   340  		Summary:    summary,
   341  	}
   342  
   343  	mf.mutex.Unlock()
   344  }
   345  
   346  // get returns the function's details
   347  func (mf *MurexFuncs) get(name string) *murexFuncDetails {
   348  	mf.mutex.Lock()
   349  	fn := mf.fn[name]
   350  	mf.mutex.Unlock()
   351  	return fn
   352  }
   353  
   354  // Exists checks if function already created
   355  func (mf *MurexFuncs) Exists(name string) bool {
   356  	mf.mutex.Lock()
   357  	exists := mf.fn[name] != nil
   358  	mf.mutex.Unlock()
   359  	return exists
   360  }
   361  
   362  // Block returns function code
   363  func (mf *MurexFuncs) Block(name string) ([]rune, error) {
   364  	mf.mutex.Lock()
   365  	fn := mf.fn[name]
   366  	mf.mutex.Unlock()
   367  
   368  	if fn == nil {
   369  		return nil, errors.New("cannot locate function named `" + name + "`")
   370  	}
   371  
   372  	return fn.Block, nil
   373  }
   374  
   375  // Summary returns functions summary
   376  func (mf *MurexFuncs) Summary(name string) (string, error) {
   377  	mf.mutex.Lock()
   378  	fn := mf.fn[name]
   379  	mf.mutex.Unlock()
   380  
   381  	if fn == nil {
   382  		return "", errors.New("cannot locate function named `" + name + "`")
   383  	}
   384  
   385  	return fn.Summary, nil
   386  }
   387  
   388  // Undefine deletes function from table
   389  func (mf *MurexFuncs) Undefine(name string) error {
   390  	mf.mutex.Lock()
   391  
   392  	if mf.fn[name] == nil {
   393  		mf.mutex.Unlock()
   394  		return errors.New("cannot locate function named `" + name + "`")
   395  	}
   396  
   397  	delete(mf.fn, name)
   398  	mf.mutex.Unlock()
   399  	return nil
   400  }
   401  
   402  // Dump list all murex functions in table
   403  func (mf *MurexFuncs) Dump() interface{} {
   404  	type funcs struct {
   405  		Summary    string
   406  		Parameters []MxFunctionParams
   407  		Block      string
   408  		FileRef    *ref.File
   409  	}
   410  
   411  	dump := make(map[string]funcs)
   412  
   413  	mf.mutex.Lock()
   414  	for name, fn := range mf.fn {
   415  		dump[name] = funcs{
   416  			Summary:    fn.Summary,
   417  			Parameters: fn.Parameters,
   418  			Block:      string(fn.Block),
   419  			FileRef:    fn.FileRef,
   420  		}
   421  	}
   422  	mf.mutex.Unlock()
   423  
   424  	return dump
   425  }
   426  
   427  // UpdateMap is used for auto-completions. It takes an existing map and updates it's values rather than copying data
   428  func (mf *MurexFuncs) UpdateMap(m map[string]bool) {
   429  	for name := range mf.fn {
   430  		m[name] = true
   431  	}
   432  }