4 files changed,
676 insertions(+),
0 deletions(-)
Author:
Oleksandr Smirnov
olexsmir@gmail.com
Committed at:
2026-04-13 17:05:34 +0300
Change ID:
swmxkvulmqlmuqswptzkrtpmxtsntwnk
Parent:
753ee96
jump to
| A | blo/blo.go |
| A | blo/blox.go |
| A | blo/go.mod |
| A | blo/test.blo |
A
blo/blo.go
··· 1 +package main 2 + 3 +import ( 4 + "errors" 5 + "fmt" 6 + "strconv" 7 + "strings" 8 + "unicode" 9 +) 10 + 11 +type TokenType int 12 + 13 +const ( 14 + TokenInvalid TokenType = iota 15 + TokenSym 16 + TokenNum 17 + TokenStr 18 + TokenOParen 19 + TokenCParen 20 + TokenComma 21 +) 22 + 23 +var TokenTypeName = map[TokenType]string{ 24 + TokenInvalid: "TokenInvalid", 25 + TokenSym: "TokenSym", 26 + TokenNum: "TokenNum", 27 + TokenStr: "TokenStr", 28 + TokenOParen: "TokenOParen", 29 + TokenCParen: "TokenCParen", 30 + TokenComma: "TokenComma", 31 +} 32 + 33 +type Token struct { 34 + Type TokenType 35 + Text []rune 36 + Loc Loc 37 +} 38 + 39 +type Loc struct { 40 + Filepath string 41 + Row, Col int 42 +} 43 + 44 +func (l Loc) String() string { return fmt.Sprintf("%s:%d:%d", l.Filepath, l.Row, l.Col) } 45 + 46 +type DiagError struct { 47 + Loc Loc 48 + Err error 49 +} 50 + 51 +func (e *DiagError) Unwrap() error { return e.Err } 52 +func (e *DiagError) Error() string { 53 + return fmt.Sprintf("%s: ERROR: %s", e.Loc, e.Err) 54 +} 55 + 56 +var ( 57 + ErrLexerEOF = errors.New("Lexer: End of file") 58 + ErrLexerUnclosedStr = errors.New("Lexer: Unclosed String") 59 + ErrLexerInvalidEscape = errors.New("Lexer: Invalid Escape Sequence") 60 + ErrLexerInvalidToken = errors.New("Lexer: Invalid Token") 61 +) 62 + 63 +type Lexer struct { 64 + Filepath string 65 + Content []rune // TODO: bytes 66 + Row int 67 + Cur int 68 + Bol int 69 + PeekTok Token 70 + PeekErr error 71 + PeekFull bool 72 +} 73 + 74 +func NewLexer(content []rune, filePath string) Lexer { 75 + return Lexer{ 76 + Filepath: filePath, 77 + Content: content, 78 + } 79 +} 80 + 81 +func (l *Lexer) ChopChar() { 82 + if l.Cur < len(l.Content) { 83 + x := l.Content[l.Cur] 84 + l.Cur += 1 85 + if x == '\n' { 86 + l.Bol = l.Cur 87 + l.Row += 1 88 + } 89 + } 90 +} 91 + 92 +func (l *Lexer) TrimLeft() { 93 + for l.Cur < len(l.Content) && unicode.IsSpace(l.Content[l.Cur]) { 94 + l.ChopChar() 95 + } 96 +} 97 + 98 +func (l Lexer) StartsWith(prefix []rune) bool { 99 + if l.Cur+len(prefix) > len(l.Content) { 100 + return false 101 + } 102 + 103 + for i := range prefix { 104 + if prefix[i] != l.Content[l.Cur+i] { 105 + return false 106 + } 107 + } 108 + 109 + return true 110 +} 111 + 112 +func (l *Lexer) Loc() Loc { 113 + return Loc{ 114 + Filepath: l.Filepath, 115 + Row: l.Row + 1, 116 + Col: l.Cur - l.Bol + 1, 117 + } 118 +} 119 + 120 +func (l *Lexer) ChopWhile(p func(rune) bool) (result []rune) { 121 + for l.Cur < len(l.Content) && p(l.Content[l.Cur]) { 122 + result = append(result, l.Content[l.Cur]) 123 + l.ChopChar() 124 + } 125 + return 126 +} 127 + 128 +func (l *Lexer) ChopToken() (Token, error) { 129 + t := Token{} 130 + 131 + l.TrimLeft() 132 + for l.Cur < len(l.Content) && l.StartsWith([]rune("--")) { 133 + for l.Cur < len(l.Content) && l.Content[l.Cur] != '\n' { 134 + l.ChopChar() 135 + } 136 + l.TrimLeft() 137 + } 138 + 139 + t.Loc = l.Loc() 140 + 141 + if l.Cur >= len(l.Content) { 142 + return t, ErrLexerEOF 143 + } 144 + 145 + first := l.Content[l.Cur] 146 + 147 + ps := []rune("(),") 148 + ts := []TokenType{TokenOParen, TokenCParen, TokenComma} 149 + for i := range ps { 150 + if first == ps[i] { 151 + t.Type = ts[i] 152 + t.Text = []rune{ps[i]} 153 + l.ChopChar() 154 + return t, nil 155 + } 156 + } 157 + 158 + if unicode.IsDigit(first) { 159 + t.Type = TokenNum 160 + t.Text = l.ChopWhile(unicode.IsDigit) 161 + return t, nil 162 + } 163 + 164 + if unicode.IsLetter(first) || first == '_' { 165 + t.Type = TokenSym 166 + t.Text = l.ChopWhile(func(x rune) bool { 167 + return unicode.IsLetter(x) || unicode.IsDigit(x) || x == '_' 168 + }) 169 + return t, nil 170 + } 171 + 172 + if first == '"' { 173 + l.ChopChar() 174 + 175 + t.Type = TokenStr 176 + 177 + for l.Cur < len(l.Content) && l.Content[l.Cur] != '"' { 178 + if l.Content[l.Cur] == '\\' { 179 + l.ChopChar() 180 + if l.Cur >= len(l.Content) { 181 + return t, &DiagError{ 182 + Loc: l.Loc(), 183 + Err: ErrLexerUnclosedStr, 184 + } 185 + } 186 + if l.Content[l.Cur] == '"' { 187 + t.Text = append(t.Text, '"') 188 + l.ChopChar() 189 + } else { 190 + loc := l.Loc() 191 + l.ChopChar() 192 + return t, &DiagError{ 193 + Loc: loc, 194 + Err: fmt.Errorf("%w: %c", ErrLexerInvalidEscape, l.Content[l.Cur]), 195 + } 196 + } 197 + } else { 198 + t.Text = append(t.Text, l.Content[l.Cur]) 199 + l.ChopChar() 200 + } 201 + } 202 + 203 + if l.Cur >= len(l.Content) { 204 + return t, &DiagError{ 205 + Loc: l.Loc(), 206 + Err: ErrLexerUnclosedStr, 207 + } 208 + } 209 + 210 + l.ChopChar() 211 + return t, nil 212 + } 213 + 214 + l.ChopChar() 215 + return t, &DiagError{ 216 + Loc: l.Loc(), 217 + Err: fmt.Errorf("%w: %c", ErrLexerInvalidToken, first), 218 + } 219 +} 220 + 221 +func (l *Lexer) Peek() (Token, error) { 222 + if !l.PeekFull { 223 + l.PeekTok, l.PeekErr = l.ChopToken() 224 + l.PeekFull = true 225 + } 226 + return l.PeekTok, l.PeekErr 227 +} 228 + 229 +func (l *Lexer) Next() (Token, error) { 230 + if l.PeekFull { 231 + l.PeekFull = false 232 + } else { 233 + l.PeekTok, l.PeekErr = l.ChopToken() 234 + } 235 + return l.PeekTok, l.PeekErr 236 +} 237 + 238 +type ExprType int 239 + 240 +const ( 241 + ExprVoid ExprType = iota 242 + ExprInt 243 + ExprStr 244 + ExprVar 245 + ExprFuncall 246 +) 247 + 248 +type Expr struct { 249 + Type ExprType 250 + Loc Loc 251 + 252 + AsInt int 253 + AsStr string 254 + AsVar string 255 + AsFuncall Funcall 256 +} 257 + 258 +type Funcall struct { 259 + Name string 260 + Args []Expr 261 +} 262 + 263 +func (funcall *Funcall) String() string { 264 + var result strings.Builder 265 + fmt.Fprintf(&result, "%s(", funcall.Name) 266 + for i, arg := range funcall.Args { 267 + if i > 0 { 268 + fmt.Fprintf(&result, ", ") 269 + } 270 + fmt.Fprintf(&result, "%s", arg.String()) 271 + } 272 + fmt.Fprintf(&result, ")") 273 + return result.String() 274 +} 275 + 276 +func (expr *Expr) Dump(level int) { 277 + for i := 0; i < level; i += 1 { 278 + fmt.Printf(" ") 279 + } 280 + 281 + switch expr.Type { 282 + case ExprVoid: 283 + fmt.Printf("Void\n") 284 + case ExprInt: 285 + fmt.Printf("Int: %d\n", expr.AsInt) 286 + case ExprStr: 287 + fmt.Printf("Str: \"%s\"\n", expr.AsStr) // TODO: escape strings 288 + case ExprVar: 289 + fmt.Printf("Var: %s\n", expr.AsVar) 290 + case ExprFuncall: 291 + fmt.Printf("Funcall: %s\n", expr.AsFuncall.Name) 292 + for _, arg := range expr.AsFuncall.Args { 293 + arg.Dump(level + 1) 294 + } 295 + } 296 +} 297 + 298 +func (expr *Expr) String() string { 299 + switch expr.Type { 300 + case ExprVoid: 301 + return "" 302 + case ExprInt: 303 + return fmt.Sprintf("%d", expr.AsInt) 304 + case ExprStr: 305 + return fmt.Sprintf("\"%s\"", expr.AsStr) // TODO: escape string 306 + case ExprVar: 307 + return expr.AsVar 308 + case ExprFuncall: 309 + return expr.AsFuncall.String() 310 + default: 311 + panic("unreachable") 312 + } 313 +} 314 + 315 +var ( 316 + ErrParserUnexpectedToken = errors.New("Parser: Unexpected Token") 317 + ErrParserUnclosedFuncall = errors.New("Parser: Unclosed Funcall") 318 +) 319 + 320 +func ParseExpr(l *Lexer) (Expr, error) { 321 + t, err := l.Next() 322 + if err != nil { 323 + return Expr{}, err 324 + } 325 + 326 + switch t.Type { 327 + case TokenSym: 328 + oparen, err := l.Peek() 329 + if err != nil || oparen.Type != TokenOParen { 330 + return Expr{ 331 + Type: ExprVar, 332 + Loc: t.Loc, 333 + AsVar: string(t.Text), 334 + }, nil 335 + } else { 336 + l.Next() 337 + cparen, err := l.Peek() 338 + 339 + if err == nil && cparen.Type == TokenCParen { 340 + l.Next() 341 + return Expr{ 342 + Type: ExprFuncall, 343 + Loc: t.Loc, 344 + AsFuncall: Funcall{Name: string(t.Text)}, 345 + }, nil 346 + } 347 + 348 + arg, err := ParseExpr(l) 349 + if err != nil { 350 + return Expr{}, err 351 + } 352 + 353 + args := []Expr{arg} 354 + 355 + comma, err := l.Next() 356 + for err == nil && comma.Type == TokenComma { 357 + arg, err = ParseExpr(l) 358 + if err != nil { 359 + return Expr{}, err 360 + } 361 + args = append(args, arg) 362 + comma, err = l.Next() 363 + } 364 + 365 + if err == ErrLexerEOF || comma.Type != TokenCParen { 366 + return Expr{}, &DiagError{Loc: comma.Loc, Err: ErrParserUnclosedFuncall} 367 + } 368 + 369 + return Expr{ 370 + Type: ExprFuncall, 371 + Loc: t.Loc, 372 + AsFuncall: Funcall{ 373 + Name: string(t.Text), 374 + Args: args, 375 + }, 376 + }, nil 377 + } 378 + 379 + case TokenNum: 380 + s := string(t.Text) 381 + x, err := strconv.Atoi(s) 382 + if err != nil { 383 + return Expr{}, &DiagError{Loc: t.Loc, Err: err} 384 + } 385 + return Expr{ 386 + Type: ExprInt, 387 + Loc: t.Loc, 388 + AsInt: x, 389 + }, nil 390 + 391 + case TokenStr: 392 + return Expr{ 393 + Type: ExprStr, 394 + Loc: t.Loc, 395 + AsStr: string(t.Text), 396 + }, nil 397 + } 398 + 399 + return Expr{}, &DiagError{ 400 + Loc: t.Loc, 401 + Err: fmt.Errorf("%w: %s", ErrParserUnexpectedToken, TokenTypeName[t.Type]), 402 + } 403 +} 404 + 405 +func ParseExprs(l *Lexer) ([]Expr, error) { 406 + exprs := []Expr{} 407 + for { 408 + expr, err := ParseExpr(l) 409 + if err != nil { 410 + if errors.Is(err, ErrLexerEOF) { 411 + err = nil 412 + } 413 + return exprs, err 414 + } 415 + exprs = append(exprs, expr) 416 + } 417 +} 418 + 419 +type ( 420 + Func = func(context *EvalContext, args []Expr) (Expr, error) 421 + 422 + EvalContext struct{ Scopes []EvalScope } 423 + EvalScope struct { 424 + Vars map[string]Expr 425 + Funcs map[string]Func 426 + } 427 +) 428 + 429 +func (e EvalContext) LookupVar(name string) (Expr, bool) { 430 + scopes := e.Scopes 431 + for len(scopes) > 0 { 432 + n := len(scopes) 433 + varr, ok := scopes[n-1].Vars[name] 434 + if ok { 435 + return varr, true 436 + } 437 + scopes = scopes[:n-1] 438 + } 439 + return Expr{}, false 440 +} 441 + 442 +func (e EvalContext) LookupFunc(name string) (Func, bool) { 443 + scopes := e.Scopes 444 + for len(scopes) > 0 { 445 + n := len(scopes) 446 + fun, ok := scopes[n-1].Funcs[name] 447 + if ok { 448 + return fun, true 449 + } 450 + scopes = scopes[:n-1] 451 + } 452 + return nil, false 453 +} 454 + 455 +func (e *EvalContext) PushScope(scope EvalScope) { e.Scopes = append(e.Scopes, scope) } 456 +func (e *EvalContext) PopScope() { e.Scopes = e.Scopes[0 : len(e.Scopes)-1] } 457 + 458 +func (e *EvalContext) TopScope() *EvalScope { 459 + length := len(e.Scopes) 460 + if length <= 0 { 461 + panic("No scopes found") 462 + } 463 + return &e.Scopes[length-1] 464 +} 465 + 466 +var ( 467 + ErrRuntimeUnknownVar = errors.New("unknown runtime variable") 468 + ErrRuntimeUnknownFun = errors.New("unknown runtime function") 469 +) 470 + 471 +func (e *EvalContext) EvalExpr(expr Expr) (Expr, error) { 472 + switch expr.Type { 473 + case ExprVoid, ExprInt, ExprStr: 474 + return expr, nil 475 + 476 + case ExprVar: 477 + v, ok := e.LookupVar(expr.AsVar) 478 + if !ok { 479 + return Expr{}, fmt.Errorf("%s: ERROR: %w: %s", expr.Loc, ErrRuntimeUnknownVar, expr.AsVar) 480 + } 481 + return e.EvalExpr(v) 482 + 483 + case ExprFuncall: 484 + fn, ok := e.LookupFunc(expr.AsFuncall.Name) 485 + if !ok { 486 + return Expr{}, fmt.Errorf("%s: ERROR: %w: %s", expr.Loc, ErrRuntimeUnknownFun, expr.AsFuncall.Name) 487 + } 488 + return fn(e, expr.AsFuncall.Args) 489 + 490 + default: 491 + panic("unreachable") 492 + } 493 +}
A
blo/blox.go
··· 1 +package main 2 + 3 +import ( 4 + "errors" 5 + "fmt" 6 + "io" 7 + "net/http" 8 + "os" 9 + "strings" 10 +) 11 + 12 +var escope = EvalScope{ 13 + Vars: map[string]Expr{ 14 + "my_name": { 15 + Type: ExprStr, 16 + AsStr: "Olex", 17 + }, 18 + }, 19 + Funcs: map[string]Func{ 20 + "let": func(ctx *EvalContext, args []Expr) (Expr, error) { 21 + if len(args) != 2 { 22 + return Expr{}, errors.New("let() expects two arguments") 23 + } 24 + 25 + if args[0].Type != ExprVar { 26 + return Expr{}, errors.New("First argument of let() has to be variable name") 27 + } 28 + 29 + name := args[0].AsVar 30 + value, err := ctx.EvalExpr(args[1]) 31 + if err != nil { 32 + return Expr{}, err 33 + } 34 + 35 + ctx.TopScope().Vars[name] = value 36 + return Expr{}, nil 37 + }, 38 + 39 + "define": func(ctx *EvalContext, args []Expr) (Expr, error) { 40 + if len(args) < 2 { 41 + return Expr{}, errors.New("define() expects at least 2 arguments") 42 + } 43 + 44 + if args[0].Type != ExprVar { 45 + return Expr{}, errors.New("define(): first argument must be the name of the function") 46 + } 47 + 48 + funName := args[0].AsVar 49 + if args[1].Type != ExprFuncall || args[1].AsFuncall.Name != "args" { 50 + return Expr{}, errors.New("define(): second argument must be the argument list") 51 + } 52 + 53 + funArgs := args[1].AsFuncall.Args 54 + for _, funArg := range funArgs { 55 + if funArg.Type != ExprVar { 56 + return Expr{}, errors.New("define(): argument list must consist of only variable names") 57 + } 58 + } 59 + 60 + ctx.TopScope().Funcs[funName] = func(context *EvalContext, callArgs []Expr) (Expr, error) { 61 + scope := EvalScope{ 62 + Vars: map[string]Expr{}, 63 + Funcs: map[string]Func{}, 64 + } 65 + 66 + if len(callArgs) != len(funArgs) { 67 + return Expr{}, errors.New(fmt.Sprintf("%s(): expected %d arguments but provided %d", funName, len(funArgs), len(args))) 68 + } 69 + 70 + for index := range callArgs { 71 + scope.Vars[funArgs[index].AsVar] = callArgs[index] 72 + } 73 + 74 + context.PushScope(scope) 75 + for _, stmt := range args[2:] { 76 + _, err := context.EvalExpr(stmt) 77 + if err != nil { 78 + return Expr{}, err 79 + } 80 + } 81 + context.PopScope() 82 + 83 + return Expr{}, nil 84 + } 85 + 86 + return Expr{}, nil 87 + }, 88 + 89 + "say": func(context *EvalContext, args []Expr) (Expr, error) { 90 + for _, arg := range args { 91 + val, err := context.EvalExpr(arg) 92 + if err != nil { 93 + return Expr{}, err 94 + } 95 + 96 + switch val.Type { 97 + case ExprStr: 98 + fmt.Printf("%s", val.AsStr) 99 + 100 + case ExprInt: 101 + fmt.Printf("%d", val.AsInt) 102 + default: 103 + return Expr{}, errors.New("say() expects its arguments to be strings or numbers") 104 + } 105 + } 106 + fmt.Printf("\n") 107 + return Expr{}, nil 108 + }, 109 + 110 + "http": func(context *EvalContext, args []Expr) (Expr, error) { 111 + var url strings.Builder 112 + for _, arg := range args { 113 + val, err := context.EvalExpr(arg) 114 + if err != nil { 115 + return Expr{}, err 116 + } 117 + if val.Type != ExprStr { 118 + return Expr{}, errors.New("http() expects its arguments to be strings") 119 + } 120 + fmt.Fprint(&url, val.AsStr) 121 + } 122 + 123 + resp, err := http.Get(url.String()) 124 + if err != nil { 125 + return Expr{}, err 126 + } 127 + defer resp.Body.Close() 128 + 129 + body, err := io.ReadAll(resp.Body) 130 + if err != nil { 131 + return Expr{}, err 132 + } 133 + 134 + return Expr{ 135 + Type: ExprStr, 136 + AsStr: string(body), 137 + }, nil 138 + }, 139 + }, 140 +} 141 + 142 +func main() { 143 + ctx := EvalContext{} 144 + ctx.PushScope(escope) 145 + 146 + if len(os.Args) < 2 { 147 + fmt.Fprintf(os.Stderr, "ERROR: no input is provided\n") 148 + os.Exit(1) 149 + } 150 + 151 + fpath := os.Args[1] 152 + content, err := os.ReadFile(fpath) 153 + if err != nil { 154 + fmt.Fprintf(os.Stderr, "ERROR: could not read file %s: %s\n", fpath, err) 155 + os.Exit(1) 156 + } 157 + 158 + lexer := NewLexer([]rune(string(content)), fpath) 159 + 160 + exprs, err := ParseExprs(&lexer) 161 + if err != nil { 162 + fmt.Fprintln(os.Stderr, err) 163 + os.Exit(1) 164 + } 165 + 166 + for _, expr := range exprs { 167 + if _, err := ctx.EvalExpr(expr); err != nil { 168 + fmt.Fprintln(os.Stderr, err) 169 + os.Exit(1) 170 + } 171 + } 172 +}