From fd030be086f6ab872d34fb422ea77ec2d7d4b8db Mon Sep 17 00:00:00 2001 From: IgorCielniak Date: Sat, 14 Mar 2026 12:44:34 +0100 Subject: [PATCH] added a snake game example and fixed a small bug in the compiler --- examples/snake.sl | 624 ++++++++++++++++++++++++++ main.py | 49 +- tests/with_else_if_shorthand.expected | 4 + tests/with_else_if_shorthand.sl | 23 + 4 files changed, 687 insertions(+), 13 deletions(-) create mode 100644 examples/snake.sl create mode 100644 tests/with_else_if_shorthand.expected create mode 100644 tests/with_else_if_shorthand.sl diff --git a/examples/snake.sl b/examples/snake.sl new file mode 100644 index 0000000..90b0f4c --- /dev/null +++ b/examples/snake.sl @@ -0,0 +1,624 @@ +# Terminal Snake (classic real-time: WASD steer, q quit) + +import stdlib/stdlib.sl +import stdlib/linux.sl + +macro WIDTH 0 20 ; +macro HEIGHT 0 12 ; +macro CELLS 0 WIDTH HEIGHT * ; + +macro CH_W 0 119 ; +macro CH_A 0 97 ; +macro CH_S 0 115 ; +macro CH_D 0 100 ; +macro CH_Q 0 113 ; +macro CH_w 0 87 ; +macro CH_a 0 65 ; +macro CH_s 0 83 ; +macro CH_d 0 68 ; +macro CH_q 0 81 ; +macro FRAME_DELAY_NS 0 350000000 ; + +macro TCGETS 0 21505 ; +macro TCSETS 0 21506 ; +macro LFLAG_OFF 0 12 ; +macro ECHO 0 8 ; +macro ICANON 0 2 ; + +# state layout (qwords) +macro ST_DIR 0 0 ; +macro ST_LEN 0 8 ; +macro ST_FOOD_X 0 16 ; +macro ST_FOOD_Y 0 24 ; +macro ST_GAME_OVER 0 32 ; +macro ST_QUIT 0 40 ; +macro ST_WIN 0 48 ; + +# direction constants +macro DIR_RIGHT 0 0 ; +macro DIR_DOWN 0 1 ; +macro DIR_LEFT 0 2 ; +macro DIR_UP 0 3 ; + +#arr_get [*, arr | idx] -> [* | value] +word arr_get + 8 * + @ +end + +#arr_set [*, arr, idx | value] -> [*] +word arr_set + >r + 8 * + + r> ! +end + +#xy_idx [*, x | y] -> [* | idx] +word xy_idx + WIDTH * + +end + +#board_get [*, board, x | y] -> [* | value] +word board_get + xy_idx + arr_get +end + +#board_set [*, board, x, y | value] -> [*] +word board_set + >r + xy_idx + r> arr_set +end + +#state_dir@ [* | state] -> [* | dir] +word state_dir@ + ST_DIR + @ +end + +#state_dir! [*, state | dir] -> [*] +word state_dir! + swap ST_DIR + swap ! +end + +#state_len@ [* | state] -> [* | len] +word state_len@ + ST_LEN + @ +end + +#state_len! [*, state | len] -> [*] +word state_len! + swap ST_LEN + swap ! +end + +#state_food_x@ [* | state] -> [* | x] +word state_food_x@ + ST_FOOD_X + @ +end + +#state_food_x! [*, state | x] -> [*] +word state_food_x! + swap ST_FOOD_X + swap ! +end + +#state_food_y@ [* | state] -> [* | y] +word state_food_y@ + ST_FOOD_Y + @ +end + +#state_food_y! [*, state | y] -> [*] +word state_food_y! + swap ST_FOOD_Y + swap ! +end + +#state_game_over@ [* | state] -> [* | flag] +word state_game_over@ + ST_GAME_OVER + @ +end + +#state_game_over! [*, state | flag] -> [*] +word state_game_over! + swap ST_GAME_OVER + swap ! +end + +#state_quit@ [* | state] -> [* | flag] +word state_quit@ + ST_QUIT + @ +end + +#state_quit! [*, state | flag] -> [*] +word state_quit! + swap ST_QUIT + swap ! +end + +#state_win@ [* | state] -> [* | flag] +word state_win@ + ST_WIN + @ +end + +#state_win! [*, state | flag] -> [*] +word state_win! + swap ST_WIN + swap ! +end + +#term_enter [*] -> [*] +word term_enter + # Enter alternate screen: ESC[?1049h + 27 putc 91 putc 63 putc 49 putc 48 putc 52 putc 57 putc 104 putc + # Hide cursor: ESC[?25l + 27 putc 91 putc 63 putc 50 putc 53 putc 108 putc +end + +#term_raw_on [*, orig | work] -> [*] +:asm term_raw_on { + ; stack: orig (NOS), work (TOS) + mov r14, [r12] ; work + mov r15, [r12 + 8] ; orig + add r12, 16 + + ; ioctl(0, TCGETS, orig) + mov rax, 16 + mov rdi, 0 + mov rsi, 21505 + mov rdx, r15 + syscall + + ; copy 64 bytes orig -> work + mov rcx, 8 + mov rsi, r15 + mov rdi, r14 +.copy_loop: + mov rbx, [rsi] + mov [rdi], rbx + add rsi, 8 + add rdi, 8 + loop .copy_loop + + ; clear ECHO | ICANON in c_lflag (offset 12) + mov eax, [r14 + 12] + and eax, 0xFFFFFFF5 + mov [r14 + 12], eax + + ; c_cc[VTIME]=0 (offset 17+5), c_cc[VMIN]=0 (offset 17+6) + mov byte [r14 + 22], 0 + mov byte [r14 + 23], 0 + + ; ioctl(0, TCSETS, work) + mov rax, 16 + mov rdi, 0 + mov rsi, 21506 + mov rdx, r14 + syscall +} +; + +#stdin_nonblock_on [* | old_flags_ptr] -> [*] +:asm stdin_nonblock_on { + mov r14, [r12] + add r12, 8 + + ; old_flags = fcntl(0, F_GETFL, 0) + mov rax, 72 + mov rdi, 0 + mov rsi, 3 + xor rdx, rdx + syscall + mov [r14], rax + + ; fcntl(0, F_SETFL, old_flags | O_NONBLOCK) + mov rbx, rax + or rbx, 2048 + mov rax, 72 + mov rdi, 0 + mov rsi, 4 + mov rdx, rbx + syscall +} +; + +#stdin_nonblock_off [* | old_flags_ptr] -> [*] +:asm stdin_nonblock_off { + mov r14, [r12] + add r12, 8 + + mov rax, 72 + mov rdi, 0 + mov rsi, 4 + mov rdx, [r14] + syscall +} +; + +#sleep_tick [* | ts_ptr] -> [*] +:asm sleep_tick { + mov r14, [r12] + add r12, 8 + + ; nanosleep(ts_ptr, NULL) + mov rax, 35 + mov rdi, r14 + xor rsi, rsi + syscall +} +; + +#wait [* | ts_ptr] -> [*] +word wait + sleep_tick +end + +#term_raw_off [* | orig] -> [*] +:asm term_raw_off { + mov r14, [r12] + add r12, 8 + + mov rax, 16 + mov rdi, 0 + mov rsi, 21506 + mov rdx, r14 + syscall +} +; + +#term_leave [*] -> [*] +word term_leave + # Show cursor: ESC[?25h + 27 putc 91 putc 63 putc 50 putc 53 putc 104 putc + # Leave alternate screen: ESC[?1049l + 27 putc 91 putc 63 putc 49 putc 48 putc 52 putc 57 putc 108 putc +end + +#clear_screen_home [*] -> [*] +word clear_screen_home + # Clear full screen: ESC[2J + 27 putc 91 putc 50 putc 74 putc + # Move cursor home: ESC[H + 27 putc 91 putc 72 putc +end + +#clear_board [* | board] -> [*] +word clear_board + 0 + while dup CELLS < do + over over 8 * + 0 ! + 1 + + end + drop + drop +end + +#init_state [* | state] -> [*] +word init_state + dup DIR_RIGHT state_dir! + dup 3 state_len! + dup 0 state_food_x! + dup 0 state_food_y! + dup 0 state_game_over! + dup 0 state_quit! + dup 0 state_win! + drop +end + +#init_snake [*, board, xs | ys] -> [*] +word init_snake + with b xs ys in + WIDTH 2 / + HEIGHT 2 / + with cx cy in + xs 0 cx arr_set + ys 0 cy arr_set + b cx cy 1 board_set + + xs 1 cx 1 - arr_set + ys 1 cy arr_set + b cx 1 - cy 1 board_set + + xs 2 cx 2 - arr_set + ys 2 cy arr_set + b cx 2 - cy 1 board_set + end + end +end + +#spawn_food [*, board | state] -> [*] +word spawn_food + with b s in + rand syscall.getpid + CELLS % + 0 + 0 + with start tried found in + while tried CELLS < do + start tried + CELLS % + dup b swap arr_get 0 == if + dup WIDTH % s swap state_food_x! + dup WIDTH / s swap state_food_y! + drop + 1 found ! + CELLS tried ! + else + drop + tried 1 + tried ! + end + end + + found 0 == if + s 1 state_win! + end + end + end +end + +#draw_game [*, board, xs, ys | state] -> [*] +word draw_game + with b xs ys s in + "Snake (WASD to steer, q to quit)" puts + "Score: " puts + s state_len@ 3 - puti + 10 putc + + xs drop + ys drop + + 0 + while dup HEIGHT < do + 0 + while dup WIDTH < do + over s state_food_y@ == if + dup s state_food_x@ == if + 42 putc + else + over WIDTH * over + + b swap arr_get + if 111 putc else 46 putc end + end + else + over WIDTH * over + + b swap arr_get + if 111 putc else 46 putc end + end + 1 + + end + drop + 10 putc + 1 + + end + drop + + s state_game_over@ if + "Game over!" puts + end + s state_win@ if + "You win!" puts + end + end +end + +#read_input [*, input_buf | state] -> [*] +word read_input + with ibuf s in + FD_STDIN ibuf 8 syscall.read + dup 0 <= if + drop + else + drop + ibuf c@ + + dup CH_Q == if + drop + s 1 state_quit! + else dup CH_q == if + drop + s 1 state_quit! + else dup CH_W == if + drop + s state_dir@ DIR_DOWN != if + s DIR_UP state_dir! + end + else dup CH_w == if + drop + s state_dir@ DIR_DOWN != if + s DIR_UP state_dir! + end + else dup CH_S == if + drop + s state_dir@ DIR_UP != if + s DIR_DOWN state_dir! + end + else dup CH_s == if + drop + s state_dir@ DIR_UP != if + s DIR_DOWN state_dir! + end + else dup CH_A == if + drop + s state_dir@ DIR_RIGHT != if + s DIR_LEFT state_dir! + end + else dup CH_a == if + drop + s state_dir@ DIR_RIGHT != if + s DIR_LEFT state_dir! + end + else dup CH_D == if + drop + s state_dir@ DIR_LEFT != if + s DIR_RIGHT state_dir! + end + else dup CH_d == if + drop + s state_dir@ DIR_LEFT != if + s DIR_RIGHT state_dir! + end + else + drop + end + end + end +end + +#step_game [*, board, xs, ys | state] -> [*] +word step_game + with b xs ys s in + xs 0 arr_get + ys 0 arr_get + with hx hy in + hx + hy + # Compute next head from direction. + s state_dir@ DIR_RIGHT == if + drop + hx 1 + + hy + else s state_dir@ DIR_DOWN == if + drop + hx + hy 1 + + else s state_dir@ DIR_LEFT == if + drop + hx 1 - + hy + else + drop + hx + hy 1 - + end + + with nx ny in + # dead flag from wall collision + 0 + nx 0 < if drop 1 end + nx WIDTH >= if drop 1 end + ny 0 < if drop 1 end + ny HEIGHT >= if drop 1 end + + with dead in + dead if + s 1 state_game_over! + else + # grow flag + 0 + nx s state_food_x@ == if + ny s state_food_y@ == if + drop 1 + end + end + + with grow in + # when not growing, remove tail before collision check + grow 0 == if + s state_len@ 1 - + with ti in + xs ti arr_get + ys ti arr_get + with tx ty in + b tx ty 0 board_set + end + end + end + + # self collision + b nx ny board_get if + s 1 state_game_over! + else + # shift body + s state_len@ + grow if + # start at len for growth + else + 1 - + end + while dup 0 > do + dup >r + xs r@ xs r@ 1 - arr_get arr_set + ys r@ ys r@ 1 - arr_get arr_set + rdrop + 1 - + end + drop + + # write new head + xs 0 nx arr_set + ys 0 ny arr_set + b nx ny 1 board_set + + grow if + s state_len@ 1 + s swap state_len! + b s spawn_food + end + end + end + end + end + end + end + end +end + +word main + CELLS 8 * alloc + CELLS 8 * alloc + CELLS 8 * alloc + 56 alloc + 8 alloc + 64 alloc + 64 alloc + 8 alloc + 16 alloc + + with board xs ys state input term_orig term_work fd_flags sleep_ts in + board clear_board + state init_state + board xs ys init_snake + board state spawn_food + + sleep_ts 0 ! + sleep_ts 8 + FRAME_DELAY_NS ! + + term_orig term_work term_raw_on + fd_flags stdin_nonblock_on + term_enter + + 1 + while dup do + drop + clear_screen_home + board xs ys state draw_game + + state state_game_over@ if + 0 + else state state_win@ if + 0 + else state state_quit@ if + 0 + else + input state read_input + state state_quit@ if + 0 + else + board xs ys state step_game + sleep_ts wait + 1 + end + end + end + drop + + clear_screen_home + board xs ys state draw_game + + fd_flags stdin_nonblock_off + term_orig term_raw_off + term_leave + + sleep_ts 16 free + fd_flags 8 free + term_work 64 free + term_orig 64 free + input 8 free + state 56 free + ys CELLS 8 * free + xs CELLS 8 * free + board CELLS 8 * free + end + + 0 +end diff --git a/main.py b/main.py index fcc272a..7eb8b2e 100644 --- a/main.py +++ b/main.py @@ -6943,19 +6943,30 @@ def macro_with(ctx: MacroContext) -> Optional[List[Op]]: raise ParseError("'with' requires at least one variable name") body: List[Token] = [] + else_line: Optional[int] = None depth = 0 while True: if parser._eof(): raise ParseError("unterminated 'with' block (missing 'end')") tok = parser.next_token() + if else_line is not None and tok.line != else_line: + else_line = None if tok.lexeme == "end": if depth == 0: break depth -= 1 body.append(tok) continue - if tok.lexeme in ("with", "if", "for", "while", "begin", "word"): + if tok.lexeme in ("with", "for", "while", "begin", "word"): depth += 1 + elif tok.lexeme == "if": + # Support shorthand elif form `else if` inside with-blocks. + # This inline `if` shares the same closing `end` as the preceding + # branch and therefore must not increment nesting depth. + if else_line != tok.line: + depth += 1 + elif tok.lexeme == "else": + else_line = tok.line body.append(tok) helper_for: Dict[str, str] = {} @@ -6963,14 +6974,26 @@ def macro_with(ctx: MacroContext) -> Optional[List[Op]]: _, helper = parser.allocate_variable(name) helper_for[name] = helper - emitted: List[str] = [] + emitted_tokens: List[Token] = [] + + def _emit_lex(lex: str, src_tok: Optional[Token] = None) -> None: + base = src_tok or template or Token(lexeme="", line=0, column=0, start=0, end=0) + emitted_tokens.append( + Token( + lexeme=lex, + line=base.line, + column=base.column, + start=base.start, + end=base.end, + ) + ) # Initialize variables by storing current stack values into their buffers for name in reversed(names): helper = helper_for[name] - emitted.append(helper) - emitted.append("swap") - emitted.append("!") + _emit_lex(helper, template) + _emit_lex("swap", template) + _emit_lex("!", template) i = 0 while i < len(body): @@ -6980,23 +7003,23 @@ def macro_with(ctx: MacroContext) -> Optional[List[Op]]: if helper is not None: next_tok = body[i + 1] if i + 1 < len(body) else None if next_tok is not None and next_tok.lexeme == "!": - emitted.append(helper) - emitted.append("swap") - emitted.append("!") + _emit_lex(helper, tok) + _emit_lex("swap", tok) + _emit_lex("!", tok) i += 2 continue if next_tok is not None and next_tok.lexeme == "@": - emitted.append(helper) + _emit_lex(helper, tok) i += 1 continue - emitted.append(helper) - emitted.append("@") + _emit_lex(helper, tok) + _emit_lex("@", tok) i += 1 continue - emitted.append(tok.lexeme) + _emit_lex(tok.lexeme, tok) i += 1 - ctx.inject_tokens(emitted, template=template) + ctx.inject_token_objects(emitted_tokens) return None diff --git a/tests/with_else_if_shorthand.expected b/tests/with_else_if_shorthand.expected new file mode 100644 index 0000000..eabb171 --- /dev/null +++ b/tests/with_else_if_shorthand.expected @@ -0,0 +1,4 @@ +neg +zero +small +big diff --git a/tests/with_else_if_shorthand.sl b/tests/with_else_if_shorthand.sl new file mode 100644 index 0000000..3204686 --- /dev/null +++ b/tests/with_else_if_shorthand.sl @@ -0,0 +1,23 @@ +import stdlib/stdlib.sl + +word classify + with n in + n 0 < if + "neg" puts + else n 0 == if + "zero" puts + else n 10 < if + "small" puts + else + "big" puts + end + end +end + +word main + -1 classify + 0 classify + 3 classify + 20 classify + 0 +end