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 }