diff --git a/examples/features.rizz b/examples/features.rizz index 9731f67..0886f17 100644 --- a/examples/features.rizz +++ b/examples/features.rizz @@ -17,10 +17,17 @@ fun appendSignalToNeverAndAdd1 s a = const (a |> s) fun is0 x = x == 0 +let nestedList = [[1, 2], [3, 4], [5]] +let nestedStringList = [["Hello", "world"], ["Rizz", "is"], ["cool"]] + fun entry x = let my_console_sig = mk_sig (wait console) in - let my_console = console_out_signal ("" :: my_console_sig) in - let q = quit_at (my_console_sig) in - let nestedList = [[1, 2], [3, 4], [5]] in - let nestedStringList = [["Hello", "world"], ["Rizz", "is"], ["cool"]] in + let port_sig = port_input 7777 in + let my_console_sig = map_l (fun x -> "console: " + x) my_console_sig in + let port_sig = map (fun x -> "port: " + x) port_sig in + let port_with_random = map (fun x -> x + "; Random number:" + string_of_int (random_int 100)) port_sig in + let _ = port_out_signal 7777 ("" :: my_console_sig) in + let _ = console_out_signal port_with_random in + let quit_sig = filterL (fun x -> string_contains x "quit") (tail port_with_random) in + let q = quit_at (quit_sig) in start_event_loop () diff --git a/examples/snake.rizz b/examples/snake.rizz index f5b9443..3afc0e5 100644 --- a/examples/snake.rizz +++ b/examples/snake.rizz @@ -1,294 +1,199 @@ - -let board_width = 20 -let board_height = 20 -let board_cells = board_width * board_height - -fun repeat_text text count : String -> Int -> String = - let is_done = count <= 0 in - match is_done with - | true -> "" - | false -> text + repeat_text text (count - 1) - -fun make_border unit : Unit -> String = - "+" + repeat_text "-" board_width + "+" - -fun point_eq left right : (Int * Int) -> (Int * Int) -> Bool = - let pair = (left, right) in - match pair with - | ((x1, y1), (x2, y2)) -> - let same_x = x1 == x2 in - match same_x with - | true -> y1 == y2 - | false -> false - -fun join_segments parts : List String -> String = - match parts with - | [] -> "" - | part :: rest -> - let rest_empty = list_is_empty rest in - match rest_empty with - | true -> part - | false -> part + ";" + join_segments rest - -fun first_segment parts : List String -> String = - match parts with - | [] -> "" - | part :: rest -> part - -fun rest_segments parts : List String -> String = - match parts with - | [] -> "" - | part :: rest -> join_segments rest - -fun without_last_segment parts : List String -> String = - match parts with - | [] -> "" - | part :: rest -> - let rest_empty = list_is_empty rest in - match rest_empty with - | true -> "" - | false -> - let rest_has_one = list_length rest == 1 in - match rest_has_one with - | true -> part - | false -> part + ";" + without_last_segment rest - -fun point_to_token point : (Int * Int) -> String = - match point with - | (x, y) -> string_of_int x + "_" + string_of_int y - -fun token_to_point token : String -> (Int * Int) = - let parts = string_split token "_" in - match parts with - | x_text :: rest -> - let rest_empty = list_is_empty rest in - match rest_empty with - | true -> (0, 0) - | false -> - let y_text = list_head rest in - let parsed_x = parse_int x_text in - match parsed_x with - | Some (x) -> - let parsed_y = parse_int y_text in - match parsed_y with - | Some (y) -> (x, y) - | _ -> (0, 0) - | _ -> (0, 0) - | _ -> (0, 0) - -fun snake_head snake : String -> (Int * Int) = - token_to_point (first_segment (string_split snake ";")) - -fun snake_body snake : String -> String = - rest_segments (string_split snake ";") - -fun snake_without_tail snake : String -> String = - without_last_segment (string_split snake ";") - -fun prepend_token token snake : String -> String -> String = - let snake_empty = snake == "" in - match snake_empty with - | true -> token - | false -> token + ";" + snake - -fun snake_contains point snake : (Int * Int) -> String -> Bool = - let token = point_to_token point in - string_contains (";" + snake + ";") (";" + token + ";") - -fun move_point direction point : String -> (Int * Int) -> (Int * Int) = - let step = (direction, point) in - match step with - | ("up", (x, y)) -> (x, y - 1) - | ("down", (x, y)) -> (x, y + 1) - | ("left", (x, y)) -> (x - 1, y) - | ("right", (x, y)) -> (x + 1, y) - | (_, (x, y)) -> (x, y) - -fun is_inside point : (Int * Int) -> Bool = - match point with - | (x, y) -> - let x_negative = x < 0 in - match x_negative with - | true -> false - | false -> - let y_negative = y < 0 in - match y_negative with - | true -> false - | false -> - let x_too_large = x >= board_width in - match x_too_large with - | true -> false - | false -> y < board_height - -fun opposite_direction direction : String -> String = - match direction with - | "up" -> "down" - | "down" -> "up" - | "left" -> "right" - | "right" -> "left" - | _ -> "" - -fun normalize_command input : String -> String = - match input with - | "w" -> "up" - | "W" -> "up" - | "up" -> "up" - | "a" -> "left" - | "A" -> "left" - | "left" -> "left" - | "s" -> "down" - | "S" -> "down" - | "down" -> "down" - | "d" -> "right" - | "D" -> "right" - | "right" -> "right" - | "q" -> "quit" - | "Q" -> "quit" - | "quit" -> "quit" - | _ -> "" - -fun choose_direction current_direction input : String -> String -> String = - let command = normalize_command input in - let command_empty = command == "" in - match command_empty with - | true -> current_direction - | false -> - let command_quit = command == "quit" in - match command_quit with - | true -> current_direction - | false -> - let opposite = opposite_direction current_direction in - let reversing = command == opposite in - match reversing with - | true -> current_direction - | false -> command - -fun point_of_index index : Int -> (Int * Int) = - (mod index board_width, div index board_width) - -fun find_food snake start remaining : String -> Int -> Int -> (Int * Int) = - let no_remaining = remaining <= 0 in - match no_remaining with - | true -> (0, 0) - | false -> - let candidate = point_of_index (mod start board_cells) in - let occupied = snake_contains candidate snake in - match occupied with - | true -> find_food snake (start + 1) (remaining - 1) - | false -> candidate - -fun render_cell x y snake food : Int -> Int -> String -> (Int * Int) -> String = - let point = (x, y) in - let head = snake_head snake in - let body = snake_body snake in - let on_head = point_eq point head in - match on_head with - | true -> "@" - | false -> - let on_body = snake_contains point body in - match on_body with - | true -> "o" - | false -> - let on_food = point_eq point food in - match on_food with - | true -> "*" - | false -> " " - -fun render_row_cells x y snake food : Int -> Int -> String -> (Int * Int) -> String = - let row_done = x >= board_width in - match row_done with - | true -> "" - | false -> render_cell x y snake food + render_row_cells (x + 1) y snake food - -fun render_rows y snake food : Int -> String -> (Int * Int) -> String = - let board_done = y >= board_height in - match board_done with - | true -> "" - | false -> "|" + render_row_cells 0 y snake food + "|\n" + render_rows (y + 1) snake food - -fun render_state state : (String * ((Int * Int) * (String * (Int * String)))) -> String = - match state with - | (snake, (food, (direction, (score, status)))) -> - let border = make_border () in - let board = border + "\n" + render_rows 0 snake food + border in - let hud = "Score: " + string_of_int score + " Direction: " + direction + "\n" in - let help = "Controls: w/a/s/d or up/left/down/right. Enter q to quit.\n" in - let still_running = status == "" in - match still_running with - | true -> hud + help + board - | false -> hud + help + board + "\n" + status - -fun is_finished state : (String * ((Int * Int) * (String * (Int * String)))) -> Bool = - match state with - | (_snake, (_food, (_direction, (_score, status)))) -> - let still_running = status == "" in - match still_running with - | true -> false - | false -> true - -fun update_state state input : (String * ((Int * Int) * (String * (Int * String)))) -> String -> (String * ((Int * Int) * (String * (Int * String)))) = - match state with - | (snake, (food, (direction, (score, status)))) -> - let still_running = status == "" in - match still_running with - | true -> - let command = normalize_command input in - let should_quit = command == "quit" in - match should_quit with - | true -> - (snake, (food, (direction, (score, "Thanks for playing.")))) - | false -> - let head = snake_head snake in - let next_direction = choose_direction direction input in - let next_head = move_point next_direction head in - let next_body = - let ate_now = point_eq next_head food in - match ate_now with - | true -> snake - | false -> snake_without_tail snake - in - let hit_wall = - let inside = is_inside next_head in - match inside with - | true -> false - | false -> true - in - let hit_self = snake_contains next_head next_body in - match hit_wall with - | true -> - (snake, (food, (next_direction, (score, "Game over: you hit the wall.")))) - | false -> - match hit_self with - | true -> - (snake, (food, (next_direction, (score, "Game over: you ran into yourself.")))) - | false -> - let ate_food = point_eq next_head food in - let next_snake = prepend_token (point_to_token next_head) next_body in - let next_score = - let gained_score = ate_food in - match gained_score with - | true -> score + 1 - | false -> score - in - let next_food = - let needs_food = ate_food in - match needs_food with - | true -> find_food next_snake (score * 7 + board_width + board_height) board_cells - | false -> food - in - (next_snake, (next_food, (next_direction, (next_score, "")))) - | false -> state - -fun entry x = - let console_sig = "" :: mk_sig (wait console) in - let initial_snake = "3_3;2_3;1_3" in - let initial_food = find_food initial_snake 5 board_cells in - let initial_state = (initial_snake, (initial_food, ("right", (0, "")))) in - let game_clock = clock 300 in - let input_sig = map (snd) (sample game_clock console_sig) in - let game_states = scan update_state initial_state input_sig in - let rendered_states = map render_state game_states in - let _out = console_out_signal rendered_states in - let _quit = quit_at (filter is_finished game_states) in - let _x = console_out_signal game_clock in - start_event_loop () +let board_width = 20 +let board_height = 20 +let board_cells = board_width * board_height +let tick_ms = 250 +let clear_home = "\u{1B}[2J\u{1B}[H" + +type SnakeEvent = +| Start +| Tick (String) +| Command (String) + +fun repeat_text text count : String -> Int -> String = + if count <= 0 then "" else text + repeat_text text (count - 1) + +fun make_border unit : Unit -> String = + "+" + repeat_text "-" board_width + "+" + +fun point_eq left right : (Int * Int) -> (Int * Int) -> Bool = + match (left, right) with + | ((x1, y1), (x2, y2)) -> if x1 == x2 then y1 == y2 else false + +fun snake_head snake : List (Int * Int) -> (Int * Int) = + match snake with + | head :: _rest -> head + | [] -> (0, 0) + +fun snake_body snake : List (Int * Int) -> List (Int * Int) = + match snake with + | _head :: rest -> rest + | [] -> [] + +fun snake_without_tail snake : List (Int * Int) -> List (Int * Int) = + match snake with + | [] -> [] + | head :: rest -> + if list_is_empty rest then [] + else list_append [head] (snake_without_tail rest) + +fun snake_contains point snake : (Int * Int) -> List (Int * Int) -> Bool = + match snake with + | [] -> false + | head :: rest -> + if point_eq point head then true else snake_contains point rest + +fun move_point direction point : String -> (Int * Int) -> (Int * Int) = + match (direction, point) with + | ("up", (x, y)) -> (x, y - 1) + | ("down", (x, y)) -> (x, y + 1) + | ("left", (x, y)) -> (x - 1, y) + | ("right", (x, y)) -> (x + 1, y) + | (_, (x, y)) -> (x, y) + +fun is_inside point : (Int * Int) -> Bool = + match point with + | (x, y) -> + if x < 0 then false + else if y < 0 then false + else if x >= board_width then false + else y < board_height + +fun opposite_direction direction : String -> String = + match direction with + | "up" -> "down" + | "down" -> "up" + | "left" -> "right" + | "right" -> "left" + | _ -> "" + +fun normalize_command input : String -> String = + match input with + | "w" -> "up" + | "W" -> "up" + | "ArrowUp" -> "up" + | "a" -> "left" + | "A" -> "left" + | "ArrowLeft" -> "left" + | "s" -> "down" + | "S" -> "down" + | "ArrowDown" -> "down" + | "d" -> "right" + | "D" -> "right" + | "ArrowRight" -> "right" + | "q" -> "quit" + | "Q" -> "quit" + | "Escape" -> "quit" + | _ -> "" + +fun is_direction command : String -> Bool = + if command == "up" then true + else if command == "down" then true + else if command == "left" then true + else command == "right" + +fun choose_direction current_direction command : String -> String -> String = + if is_direction command then + if command == opposite_direction current_direction then current_direction else command + else current_direction + +fun queue_direction current_direction queued_direction command : String -> String -> String -> String = + if is_direction command then + if command == opposite_direction current_direction then queued_direction else command + else queued_direction + +fun point_of_index index : Int -> (Int * Int) = + (mod index board_width, div index board_width) + +fun find_food snake start remaining : List (Int * Int) -> Int -> Int -> (Int * Int) = + if remaining <= 0 then (0, 0) + else + let candidate = point_of_index (mod start board_cells) in + if snake_contains candidate snake + then find_food snake (start + 1) (remaining - 1) + else candidate + +fun render_cell x y snake food : Int -> Int -> List (Int * Int) -> (Int * Int) -> String = + let point = (x, y) in + if point_eq point (snake_head snake) then "@" + else if snake_contains point (snake_body snake) then "o" + else if point_eq point food then "*" + else " " + +fun render_row_cells x y snake food : Int -> Int -> List (Int * Int) -> (Int * Int) -> String = + if x >= board_width then "" + else render_cell x y snake food + render_row_cells (x + 1) y snake food + +fun render_rows y snake food : Int -> List (Int * Int) -> (Int * Int) -> String = + if y >= board_height then "" + else "|" + render_row_cells 0 y snake food + "|\n" + render_rows (y + 1) snake food + +fun render_state state : (List (Int * Int) * ((Int * Int) * (String * (String * (Int * String))))) -> String = + match state with + | (snake, (food, (direction, (_queued_direction, (score, status))))) -> + let border = make_border () in + let board = border + "\n" + render_rows 0 snake food + border in + let hud = "Score: " + string_of_int score + " Direction: " + direction + "\n" in + let help = "Controls: w/a/s/d, arrow keys, q, or Escape.\n" in + clear_home + (if status == "" then hud + help + board else hud + help + board + "\n" + status) + +fun is_finished state : (List (Int * Int) * ((Int * Int) * (String * (String * (Int * String))))) -> Bool = + match state with + | (_snake, (_food, (_direction, (_queued_direction, (_score, status))))) -> not (status == "") + +fun move_snake state command : (List (Int * Int) * ((Int * Int) * (String * (String * (Int * String))))) -> String -> (List (Int * Int) * ((Int * Int) * (String * (String * (Int * String))))) = + match state with + | (snake, (food, (direction, (queued_direction, (score, status))))) -> + if not (status == "") then state + else + let next_direction = queue_direction direction queued_direction command in + let head = snake_head snake in + let next_head = move_point next_direction head in + let ate_food = point_eq next_head food in + let next_body = if ate_food then snake else snake_without_tail snake in + if not (is_inside next_head) then + (snake, (food, (direction, (queued_direction, (score, "Game over: you hit the wall."))))) + else if snake_contains next_head next_body then + (snake, (food, (direction, (queued_direction, (score, "Game over: you ran into yourself."))))) + else + let next_snake = next_head :: next_body in + let next_score = if ate_food then score + 1 else score in + let next_food = + if ate_food + then find_food next_snake (random_int board_cells) board_cells + else food + in + (next_snake, (next_food, (next_direction, (next_direction, (next_score, ""))))) + +fun update_state state event : (List (Int * Int) * ((Int * Int) * (String * (String * (Int * String))))) -> SnakeEvent -> (List (Int * Int) * ((Int * Int) * (String * (String * (Int * String))))) = + match state with + | (snake, (food, (direction, (queued_direction, (score, status))))) -> + match event with + | Start -> state + | Command (command) -> + (snake, (food, (direction, (queue_direction direction queued_direction command, (score, status))))) + | Tick (command) -> move_snake state command + +fun to_snake_event event : Sync (Int, String) -> SnakeEvent = + match event with + | Left (_tick) -> Tick ("") + | Right (command) -> Command (command) + | Both (_tick, command) -> Tick (command) + +fun entry x = + let _clear = clear_screen () in + let _hide_cursor = hide_cursor () in + let key_sig = "" :: mk_sig (wait keyboard) in + let command_sig = map normalize_command key_sig in + let initial_snake = [(3, 3), (2, 3), (1, 3)] in + let initial_food = find_food initial_snake (random_int board_cells) board_cells in + let initial_state = (initial_snake, (initial_food, ("right", ("right", (0, ""))))) in + let game_clock = clock tick_ms in + let events = merge_events to_snake_event Start game_clock command_sig in + let game_states = scan update_state initial_state events in + let rendered_states = map render_state game_states in + let _out = console_out_signal rendered_states in + let _quit_key = quit_at (filter (fun command -> command == "quit") command_sig) in + let _quit_game = quit_at (filter is_finished game_states) in + let result = start_event_loop () in + let _show_cursor = show_cursor () in + result diff --git a/package.json b/package.json index fd45794..37e7ea0 100644 --- a/package.json +++ b/package.json @@ -10,8 +10,8 @@ "ext:install": "npm run build && npm run ext:install:nobuild", "ext:install:deps": "cd vscode-rizzo-lsp && npm install", "release:package:toolchain": "node ./scripts/package-toolchain.mjs", - "transpile": "gcc -I./src/runtime output.c -o output || clang -I./src/runtime output.c -o output", + "transpile": "gcc -I./src/runtime output.c -o output -lm || clang -I./src/runtime output.c -o output.exe -lws2_32", "rizzo": "npm run transpile && ./output", "rizz": "npm run rizzo" } -} \ No newline at end of file +} diff --git a/src/lib/backend_c.ml b/src/lib/backend_c.ml index df9d965..2108ab4 100644 --- a/src/lib/backend_c.ml +++ b/src/lib/backend_c.ml @@ -3,6 +3,26 @@ open Refcount_core let standard_indent = 4 +let c_string_literal s = + let buffer = Buffer.create (String.length s + 2) in + Buffer.add_char buffer '"'; + String.iter + (fun ch -> + match ch with + | '"' -> Buffer.add_string buffer "\\\"" + | '\\' -> Buffer.add_string buffer "\\\\" + | '\n' -> Buffer.add_string buffer "\\n" + | '\r' -> Buffer.add_string buffer "\\r" + | '\t' -> Buffer.add_string buffer "\\t" + | c -> + let code = Char.code c in + if code < 32 || code > 126 + then Buffer.add_string buffer (Printf.sprintf "\\%03o" code) + else Buffer.add_char buffer c) + s; + Buffer.add_char buffer '"'; + Buffer.contents buffer + let make_fun_decl ?ending:(e = " {\n") name = Printf.sprintf "rz_box_t %s(size_t _, rz_box_t* args)%s" name e (* TODO: see if we can remove the num_args (the first param) from the method signature *) @@ -74,10 +94,11 @@ let emit_c_code (RefProg{functions; _} as p:program) (filename:string) = write "#include \"rizzo.h\"\n"; write "\n"; - write (Printf.sprintf "static rz_box_t %s;\n\n" (mangle "console")); + write (Printf.sprintf "static rz_box_t %s;\n" (mangle "console")); + write (Printf.sprintf "static rz_box_t %s;\n\n" (mangle "keyboard")); string_consts - |> List.iter (fun ((str_lit, name)) -> write @@ Printf.sprintf "static char* %s = %S;\n" name str_lit); + |> List.iter (fun ((str_lit, name)) -> write @@ Printf.sprintf "static char* %s = %s;\n" name (c_string_literal str_lit)); write "\n"; (* forward declare functions *) @@ -93,6 +114,7 @@ let emit_c_code (RefProg{functions; _} as p:program) (filename:string) = write ("int main() {\n" ^ " rz_init_rizzo();\n"); write (Printf.sprintf "%s = rz_make_int(RZ_CHANNEL_CONSOLE_IN);\n" (mangle "console")) ~indent:standard_indent; + write (Printf.sprintf "%s = rz_make_int(RZ_CHANNEL_KEYBOARD_IN);\n" (mangle "keyboard")) ~indent:standard_indent; init_globals globals; @@ -231,4 +253,4 @@ let emit_c_code (RefProg{functions; _} as p:program) (filename:string) = Fun.protect ~finally:(fun () -> close_out out_file) (fun () -> emit_program p; flush out_file) - \ No newline at end of file + diff --git a/src/lib/rizzo_builtins.ml b/src/lib/rizzo_builtins.ml index c4fb4ce..7196a41 100644 --- a/src/lib/rizzo_builtins.ml +++ b/src/lib/rizzo_builtins.ml @@ -20,15 +20,22 @@ let mk name ?(ownership = None) ?(proj_idx = None) ?(public = true) (t: Ast.typ) open Ast.Factory -let output_builtins = [ - mk "console_out_signal" ~ownership:(Some [Refcount_core.Owned]) (TFun (Cons1(TSignal (TParam "'a"), []), TUnit)) (); - mk "console_out_signal_l" ~ownership:(Some [Refcount_core.Owned]) (TFun (Cons1(TLater(TSignal (TParam "'a")), []), TUnit)) (); - mk "quit_at" ~ownership:(Some [Refcount_core.Owned]) (TFun (Cons1(TLater (TParam "'a"), []), TUnit)) (); -] +let output_builtins = [ + mk "console_out_signal" ~ownership:(Some [Refcount_core.Owned]) (TFun (Cons1(TSignal (TParam "'a"), []), TUnit)) (); + mk "console_out_signal_l" ~ownership:(Some [Refcount_core.Owned]) (TFun (Cons1(TLater(TSignal (TParam "'a")), []), TUnit)) (); + mk "port_out_signal" ~ownership:(Some [Refcount_core.Borrowed; Refcount_core.Owned]) (TFun (Cons1(TInt, [TSignal TString]), TUnit)) (); + mk "quit_at" ~ownership:(Some [Refcount_core.Owned]) (TFun (Cons1(TLater (TParam "'a"), []), TUnit)) (); + mk "clear_screen" ~ownership:(Some [Refcount_core.Borrowed]) (TFun(Cons1(TUnit, []), TUnit)) (); + mk "hide_cursor" ~ownership:(Some [Refcount_core.Borrowed]) (TFun(Cons1(TUnit, []), TUnit)) (); + mk "show_cursor" ~ownership:(Some [Refcount_core.Borrowed]) (TFun(Cons1(TUnit, []), TUnit)) (); + mk "move_cursor" ~ownership:(Some [Refcount_core.Borrowed; Refcount_core.Borrowed]) (TFun(Cons1(TInt, [TInt]), TUnit)) (); +] let builtins = [ mk "start_event_loop" ~ownership:(Some [Refcount_core.Borrowed]) (TFun(Cons1(TUnit, []), TUnit)) (); mk "clock" ~ownership:(Some [Refcount_core.Borrowed]) (TFun (Cons1(TInt, []), TSignal TInt)) (); + mk "random_int" ~ownership:(Some [Refcount_core.Borrowed]) (TFun (Cons1(TInt, []), TInt)) (); + mk "port_input" ~ownership:(Some [Refcount_core.Borrowed]) (TFun (Cons1(TInt, []), TSignal TString)) (); mk "parse_int" ~ownership:(Some [Refcount_core.Borrowed]) (TFun (Cons1(TString, []), TApp (TName "Option", [TInt]))) (); mk "not" ~ownership:(Some [Refcount_core.Borrowed]) (TFun (Cons1(TBool, []), TBool)) (); mk "mod" ~ownership:(Some [Refcount_core.Borrowed; Refcount_core.Borrowed]) (TFun (Cons1(TInt, [TInt]), TInt)) (); @@ -66,7 +73,7 @@ let builtins = [ mk "snd" ~proj_idx: (Some 1) (typ_fun1 (typ_tuple (typ_param "'a") (typ_param "'b")) (typ_param "'b")) (); mk "console" (TChan TString) (); - + mk "keyboard" (TChan TString) (); ] @ output_builtins let builtins_map = List.map (fun b -> b.name, b) builtins |> M.of_list diff --git a/src/lib/rizzoc.ml b/src/lib/rizzoc.ml index c6df4fc..78a6152 100644 --- a/src/lib/rizzoc.ml +++ b/src/lib/rizzoc.ml @@ -170,10 +170,10 @@ let generated_c_compiler_invocation ?compiler ?(runtime_include = "src/runtime") @ (if debug_malloc then ["-D__RZ_DEBUG_MALLOC"] else []) @ (if debug_info then ["-D__RZ_DEBUG_INFO"] else []) @ (if heap_info then ["-D__RZ_HEAP_INFO"] else []) - @ (if debug_info || debug_malloc || heap_info then ["-g"] else []) - @ ["-I"; runtime_include; input_file; "-o"; output_file] - @ (if is_windows then [] else ["-lm"]) - in + @ (if debug_info || debug_malloc || heap_info then ["-g"] else []) + @ ["-I"; runtime_include; input_file; "-o"; output_file] + @ (if is_windows then ["-lws2_32"] else ["-lm"]) + in { compiler; arguments } let to_shell_command { compiler; arguments } = diff --git a/src/runtime/builtins.h b/src/runtime/builtins.h index ba10008..3866748 100644 --- a/src/runtime/builtins.h +++ b/src/runtime/builtins.h @@ -10,10 +10,11 @@ #include "later.h" #include "timer.h" -static rz_box_t rz_start_event_loop(); -static inline rz_box_t rz_register_output_signal(size_t num_args, rz_box_t *args); -static inline rz_box_t rz_register_output_signal_deferred(size_t num_args, rz_box_t *args); -static inline rz_box_t rz_eq(rz_box_t a, rz_box_t b); +static rz_box_t rz_start_event_loop(); +static inline rz_box_t rz_register_output_signal(size_t num_args, rz_box_t *args); +static inline rz_box_t rz_register_output_signal_deferred(size_t num_args, rz_box_t *args); +static inline rz_box_t rz_register_port_output_signal(size_t num_args, rz_box_t *args); +static inline rz_box_t rz_eq(rz_box_t a, rz_box_t b); enum { @@ -111,35 +112,35 @@ static inline rz_box_t rz_builtin_make_just(rz_box_t value) return rz_make_ptr(rz_ctor_var(1, 1, value)); } -static inline rz_box_t rz_builtin_make_wait_later(rz_channel_t chan) -{ - return rz_make_ptr(rz_ctor_var(RZ_TAG_LATER_WAIT, 1, rz_make_channel(chan))); -} - -static inline rz_box_t rz_builtin_clock_step(size_t num_args, rz_box_t *args); - -static inline rz_box_t rz_builtin_make_clock_signal(rz_channel_t chan, rz_box_t head) -{ - rz_box_t delayed_step; - rz_box_t wait_later; - rz_box_t tail; - delayed_step = rz_make_ptr(rz_ctor_var( - RZ_TAG_DELAY, - 1, - rz_lift_c_fun(rz_builtin_clock_step, 3, (rz_box_t[]){rz_make_channel(chan)}, 1))); - wait_later = rz_builtin_make_wait_later(chan); - tail = rz_make_ptr(rz_ctor_var(RZ_TAG_LATER_APP, 2, delayed_step, wait_later)); - return rz_make_ptr_sig(rz_signal_ctor(head, tail)); -} - -static inline rz_box_t rz_builtin_clock_step(size_t num_args, rz_box_t *args) -{ - rz_channel_t chan; - rz_builtin_expect_arity("clock_step", 3, num_args); - chan = rz_builtin_expect_int("clock_step", 0, args[0]); - rz_builtin_expect_unit("clock_step", 1, args[1]); - return rz_builtin_make_clock_signal(chan, args[2]); -} +static inline rz_box_t rz_builtin_make_wait_later(rz_channel_t chan) +{ + return rz_make_ptr(rz_ctor_var(RZ_TAG_LATER_WAIT, 1, rz_make_channel(chan))); +} + +static inline rz_box_t rz_builtin_channel_signal_step(size_t num_args, rz_box_t *args); + +static inline rz_box_t rz_builtin_make_channel_signal(rz_channel_t chan, rz_box_t head) +{ + rz_box_t delayed_step; + rz_box_t wait_later; + rz_box_t tail; + delayed_step = rz_make_ptr(rz_ctor_var( + RZ_TAG_DELAY, + 1, + rz_lift_c_fun(rz_builtin_channel_signal_step, 3, (rz_box_t[]){rz_make_channel(chan)}, 1))); + wait_later = rz_builtin_make_wait_later(chan); + tail = rz_make_ptr(rz_ctor_var(RZ_TAG_LATER_APP, 2, delayed_step, wait_later)); + return rz_make_ptr_sig(rz_signal_ctor(head, tail)); +} + +static inline rz_box_t rz_builtin_channel_signal_step(size_t num_args, rz_box_t *args) +{ + rz_channel_t chan; + rz_builtin_expect_arity("channel_signal_step", 3, num_args); + chan = rz_builtin_expect_int("channel_signal_step", 0, args[0]); + rz_builtin_expect_unit("channel_signal_step", 1, args[1]); + return rz_builtin_make_channel_signal(chan, args[2]); +} static inline rz_box_t rz_builtin_start_event_loop(size_t num_args, rz_box_t *args) { @@ -148,14 +149,41 @@ static inline rz_box_t rz_builtin_start_event_loop(size_t num_args, rz_box_t *ar return rz_start_event_loop(); } -static inline rz_box_t rz_builtin_string_of_int(size_t num_args, rz_box_t *args) +static inline rz_box_t rz_builtin_string_of_int(size_t num_args, rz_box_t *args) { rz_builtin_expect_arity("string_of_int", 1, num_args); int64_t value = rz_builtin_expect_int("string_of_int", 0, args[0]); char buffer[21]; // enough to hold -2^63 and null terminator snprintf(buffer, sizeof(buffer), "%" PRId64, value); return rz_make_string_len(buffer, strlen(buffer)); -} +} + +static uint64_t rz_random_state = 0; + +static inline uint64_t rz_random_next_u64(void) +{ + if (rz_random_state == 0) + { + double now = rz_timer_now_seconds(); + rz_random_state = ((uint64_t)(now * 1000000000.0)) ^ UINT64_C(0x9E3779B97F4A7C15); + } + rz_random_state ^= rz_random_state << 13; + rz_random_state ^= rz_random_state >> 7; + rz_random_state ^= rz_random_state << 17; + return rz_random_state; +} + +static inline rz_box_t rz_builtin_random_int(size_t num_args, rz_box_t *args) +{ + rz_builtin_expect_arity("random_int", 1, num_args); + int64_t upper_bound = rz_builtin_expect_int("random_int", 0, args[0]); + if (upper_bound <= 0) + { + fprintf(stderr, "Runtime error: builtin 'random_int' expected a positive upper bound, got %" PRId64 "\n", upper_bound); + exit(1); + } + return rz_make_int((int64_t)(rz_random_next_u64() % (uint64_t)upper_bound)); +} static inline rz_box_t rz_builtin_mod(size_t num_args, rz_box_t *args) { @@ -230,11 +258,18 @@ static inline rz_box_t rz_builtin_clamp(size_t num_args, rz_box_t *args) return rz_make_int(value); } -static inline rz_box_t rz_builtin_console_out_signal(size_t num_args, rz_box_t *args) -{ - rz_builtin_expect_arity("console_out_signal", 1, num_args); - return rz_register_output_signal(num_args, args); -} +static inline rz_box_t rz_builtin_console_out_signal(size_t num_args, rz_box_t *args) +{ + rz_builtin_expect_arity("console_out_signal", 1, num_args); + return rz_register_output_signal(num_args, args); +} + +static inline rz_box_t rz_builtin_port_out_signal(size_t num_args, rz_box_t *args) +{ + rz_builtin_expect_arity("port_out_signal", 2, num_args); + rz_builtin_expect_int("port_out_signal", 0, args[0]); + return rz_register_port_output_signal(num_args, args); +} static inline rz_box_t rz_builtin_console_out_signal_l_step(size_t num_args, rz_box_t *args) { @@ -244,8 +279,8 @@ static inline rz_box_t rz_builtin_console_out_signal_l_step(size_t num_args, rz_ return rz_make_ptr_sig(rz_signal_ctor(RZ_UNIT, RZ_NEVER)); } -static inline rz_box_t rz_builtin_console_out_signal_l(size_t num_args, rz_box_t *args) -{ +static inline rz_box_t rz_builtin_console_out_signal_l(size_t num_args, rz_box_t *args) +{ rz_builtin_expect_arity("console_out_signal_l", 1, num_args); rz_box_t later = args[0]; if (later.kind != RZ_BOX_PTR) @@ -257,11 +292,55 @@ static inline rz_box_t rz_builtin_console_out_signal_l(size_t num_args, rz_box_t rz_box_t lifted_register = rz_lift_c_fun(rz_builtin_console_out_signal_l_step, 2, NULL, 0); rz_box_t delayed_register = rz_make_ptr(rz_ctor_var(RZ_TAG_DELAY, 1, lifted_register)); rz_signal_ctor(rz_make_int(0), rz_make_ptr(rz_ctor(RZ_TAG_LATER_APP, 2, (rz_box_t[]){ delayed_register, later }))); - return RZ_UNIT; -} - -static inline rz_box_t rz_builtin_string_contains(size_t num_args, rz_box_t *args) -{ + return RZ_UNIT; +} + +static inline rz_box_t rz_builtin_clear_screen(size_t num_args, rz_box_t *args) +{ + rz_builtin_expect_arity("clear_screen", 1, num_args); + rz_builtin_expect_unit("clear_screen", 0, args[0]); + printf("\033[2J\033[H"); + fflush(stdout); + return RZ_UNIT; +} + +static inline rz_box_t rz_builtin_hide_cursor(size_t num_args, rz_box_t *args) +{ + rz_builtin_expect_arity("hide_cursor", 1, num_args); + rz_builtin_expect_unit("hide_cursor", 0, args[0]); + printf("\033[?25l"); + fflush(stdout); + return RZ_UNIT; +} + +static inline rz_box_t rz_builtin_show_cursor(size_t num_args, rz_box_t *args) +{ + rz_builtin_expect_arity("show_cursor", 1, num_args); + rz_builtin_expect_unit("show_cursor", 0, args[0]); + printf("\033[?25h"); + fflush(stdout); + return RZ_UNIT; +} + +static inline rz_box_t rz_builtin_move_cursor(size_t num_args, rz_box_t *args) +{ + int64_t row; + int64_t column; + rz_builtin_expect_arity("move_cursor", 2, num_args); + row = rz_builtin_expect_int("move_cursor", 0, args[0]); + column = rz_builtin_expect_int("move_cursor", 1, args[1]); + if (row <= 0 || column <= 0) + { + fprintf(stderr, "Runtime error: builtin 'move_cursor' expected positive row and column, got %" PRId64 ", %" PRId64 "\n", row, column); + exit(1); + } + printf("\033[%" PRId64 ";%" PRId64 "H", row, column); + fflush(stdout); + return RZ_UNIT; +} + +static inline rz_box_t rz_builtin_string_contains(size_t num_args, rz_box_t *args) +{ size_t text_len; size_t needle_len; const char *text_bytes; @@ -423,10 +502,20 @@ static inline rz_box_t rz_builtin_clock(size_t num_args, rz_box_t *args) fprintf(stderr, "Runtime error: builtin 'clock' expected a positive interval in milliseconds, got %"PRId64"\n", interval_ms); exit(1); } - chan = rz_timer_register(interval_ms); - head = rz_make_int(0); - return rz_builtin_make_clock_signal(chan, head); -} + chan = rz_timer_register(interval_ms); + head = rz_make_int(0); + return rz_builtin_make_channel_signal(chan, head); +} + +static inline rz_box_t rz_builtin_port_input(size_t num_args, rz_box_t *args) +{ + int64_t port; + rz_channel_t chan; + rz_builtin_expect_arity("port_input", 1, num_args); + port = rz_builtin_expect_int("port_input", 0, args[0]); + chan = rz_tcp_input_register(port); + return rz_builtin_make_channel_signal(chan, rz_make_string_len("", 0)); +} static inline rz_box_t rz_quit(size_t num_args, rz_box_t *args) { diff --git a/src/runtime/channel.h b/src/runtime/channel.h index 1936d20..5983758 100644 --- a/src/runtime/channel.h +++ b/src/runtime/channel.h @@ -3,15 +3,30 @@ #include "stdint.h" #include "core.h" -typedef int64_t rz_channel_t; +typedef int64_t rz_channel_t; + +#define RZ_CHANNEL_CONSOLE_IN ((rz_channel_t)0) +#define RZ_CHANNEL_KEYBOARD_IN ((rz_channel_t)1) +#define RZ_CHANNEL_DYNAMIC_BASE ((rz_channel_t)2) + +static rz_channel_t rz_next_dynamic_channel = RZ_CHANNEL_DYNAMIC_BASE; + +static inline void rz_channel_reset_dynamic(void) { + rz_next_dynamic_channel = RZ_CHANNEL_DYNAMIC_BASE; +} + +static inline rz_channel_t rz_channel_alloc(void) { + return rz_next_dynamic_channel++; +} + +static inline rz_box_t rz_make_channel(rz_channel_t chan) { + return rz_make_int(chan); +} -#define RZ_CHANNEL_CONSOLE_IN ((rz_channel_t)0) -#define RZ_CHANNEL_TIMER_BASE ((rz_channel_t)1) - -static inline rz_box_t rz_make_channel(rz_channel_t chan) { - return rz_make_int(chan); -} - -static inline rz_box_t rz_channel_console_get() { - return rz_make_channel(RZ_CHANNEL_CONSOLE_IN); -} +static inline rz_box_t rz_channel_console_get() { + return rz_make_channel(RZ_CHANNEL_CONSOLE_IN); +} + +static inline rz_box_t rz_channel_keyboard_get() { + return rz_make_channel(RZ_CHANNEL_KEYBOARD_IN); +} diff --git a/src/runtime/dune b/src/runtime/dune index dd378e9..f07d641 100644 --- a/src/runtime/dune +++ b/src/runtime/dune @@ -7,6 +7,7 @@ (core.h as rizzoc/runtime/core.h) (heap.h as rizzoc/runtime/heap.h) (later.h as rizzoc/runtime/later.h) - (os.h as rizzoc/runtime/os.h) - (timer.h as rizzoc/runtime/timer.h) - (rizzo.h as rizzoc/runtime/rizzo.h))) + (os.h as rizzoc/runtime/os.h) + (tcp.h as rizzoc/runtime/tcp.h) + (timer.h as rizzoc/runtime/timer.h) + (rizzo.h as rizzoc/runtime/rizzo.h))) diff --git a/src/runtime/os.h b/src/runtime/os.h index b34a6eb..666f726 100644 --- a/src/runtime/os.h +++ b/src/runtime/os.h @@ -5,27 +5,140 @@ #include #include #include + +#ifdef _WIN32 +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#include +#else +#include +#include +#include +#include +#include +#include +#endif -#ifdef _WIN32 -#include -#else -#include -#include -#include -#endif - -typedef enum { - RZ_OK = 0, - RZ_NO_INPUT = 1, - RZ_INPUT_TOO_LONG = 2, - RZ_TIMEOUT = 3, -} rz_os_result_t; - -#ifdef _WIN32 -typedef struct { - char buffer[4096]; - size_t length; - bool truncated; +typedef enum { + RZ_OK = 0, + RZ_NO_INPUT = 1, + RZ_INPUT_TOO_LONG = 2, + RZ_TIMEOUT = 3, +} rz_os_result_t; + +#define RZ_KEYBOARD_QUEUE_CAPACITY 128 +#define RZ_KEYBOARD_EVENT_SIZE 32 + +typedef struct { + char events[RZ_KEYBOARD_QUEUE_CAPACITY][RZ_KEYBOARD_EVENT_SIZE]; + size_t head; + size_t count; +} rz_keyboard_queue_t; + +static rz_keyboard_queue_t rz_keyboard_queue = {{{0}}, 0, 0}; + +static inline void rz_keyboard_queue_reset(void) { + rz_keyboard_queue.head = 0; + rz_keyboard_queue.count = 0; +} + +static inline void rz_keyboard_queue_push_name(const char* name) { + size_t index; + if (name == NULL || name[0] == '\0') { + return; + } + if (rz_keyboard_queue.count == RZ_KEYBOARD_QUEUE_CAPACITY) { + rz_keyboard_queue.head = (rz_keyboard_queue.head + 1) % RZ_KEYBOARD_QUEUE_CAPACITY; + rz_keyboard_queue.count--; + } + index = (rz_keyboard_queue.head + rz_keyboard_queue.count) % RZ_KEYBOARD_QUEUE_CAPACITY; + snprintf(rz_keyboard_queue.events[index], RZ_KEYBOARD_EVENT_SIZE, "%s", name); + rz_keyboard_queue.count++; +} + +static inline void rz_keyboard_queue_push_bytes(const char* bytes, size_t length) { + char event[RZ_KEYBOARD_EVENT_SIZE]; + if (bytes == NULL || length == 0 || length >= RZ_KEYBOARD_EVENT_SIZE) { + return; + } + memcpy(event, bytes, length); + event[length] = '\0'; + rz_keyboard_queue_push_name(event); +} + +static inline void rz_keyboard_queue_push_char(char c) { + switch (c) { + case '\r': + case '\n': + rz_keyboard_queue_push_name("Enter"); + break; + case '\b': + case 127: + rz_keyboard_queue_push_name("Backspace"); + break; + case '\t': + rz_keyboard_queue_push_name("Tab"); + break; + case 27: + rz_keyboard_queue_push_name("Escape"); + break; + default: + if ((unsigned char)c >= ' ') { + rz_keyboard_queue_push_bytes(&c, 1); + } + break; + } +} + +static inline void rz_keyboard_queue_push_input_bytes(const char* bytes, size_t length) { + for (size_t i = 0; i < length; i++) { + if (bytes[i] == '\r' && i + 1 < length && bytes[i + 1] == '\n') { + rz_keyboard_queue_push_name("Enter"); + i++; + continue; + } + if ((unsigned char)bytes[i] == 27 && i + 2 < length && bytes[i + 1] == '[') { + switch (bytes[i + 2]) { + case 'A': + rz_keyboard_queue_push_name("ArrowUp"); + i += 2; + continue; + case 'B': + rz_keyboard_queue_push_name("ArrowDown"); + i += 2; + continue; + case 'C': + rz_keyboard_queue_push_name("ArrowRight"); + i += 2; + continue; + case 'D': + rz_keyboard_queue_push_name("ArrowLeft"); + i += 2; + continue; + default: + break; + } + } + rz_keyboard_queue_push_char(bytes[i]); + } +} + +static inline bool rz_keyboard_take_event(char* buffer, size_t size) { + if (rz_keyboard_queue.count == 0 || size == 0) { + return false; + } + snprintf(buffer, size, "%s", rz_keyboard_queue.events[rz_keyboard_queue.head]); + rz_keyboard_queue.head = (rz_keyboard_queue.head + 1) % RZ_KEYBOARD_QUEUE_CAPACITY; + rz_keyboard_queue.count--; + return true; +} + +#ifdef _WIN32 +typedef struct { + char buffer[4096]; + size_t length; + bool truncated; } rz_console_line_state_t; static rz_console_line_state_t rz_console_line_state = {{0}, 0, false}; @@ -82,19 +195,39 @@ static inline rz_os_result_t rz_console_line_finish(char* buffer, size_t size) { return result; } -static inline bool rz_console_handle_key_event(const KEY_EVENT_RECORD* key_event, char* buffer, size_t size, rz_os_result_t* result_out) { - char c; - if (!key_event->bKeyDown) { - return false; - } - - c = key_event->uChar.AsciiChar; - if (c == '\0') { - return false; - } - if (c == '\r' || c == '\n') { - rz_console_echo_bytes("\r\n", 2); - *result_out = rz_console_line_finish(buffer, size); +static inline bool rz_console_handle_key_event(const KEY_EVENT_RECORD* key_event, char* buffer, size_t size, rz_os_result_t* result_out) { + char c; + if (!key_event->bKeyDown) { + return false; + } + + c = key_event->uChar.AsciiChar; + if (c == '\0') { + switch (key_event->wVirtualKeyCode) { + case VK_UP: + rz_keyboard_queue_push_name("ArrowUp"); + break; + case VK_DOWN: + rz_keyboard_queue_push_name("ArrowDown"); + break; + case VK_LEFT: + rz_keyboard_queue_push_name("ArrowLeft"); + break; + case VK_RIGHT: + rz_keyboard_queue_push_name("ArrowRight"); + break; + case VK_ESCAPE: + rz_keyboard_queue_push_name("Escape"); + break; + default: + break; + } + return false; + } + rz_keyboard_queue_push_char(c); + if (c == '\r' || c == '\n') { + rz_console_echo_bytes("\r\n", 2); + *result_out = rz_console_line_finish(buffer, size); return true; } if (c == '\b') { @@ -112,7 +245,7 @@ static inline bool rz_console_handle_key_event(const KEY_EVENT_RECORD* key_event return false; } -static rz_os_result_t rz_readline_timeout_console(char* buffer, size_t size, uint32_t timeout_ms) { +static rz_os_result_t rz_readline_timeout_console(char* buffer, size_t size, uint32_t timeout_ms) { HANDLE stdin_handle = GetStdHandle(STD_INPUT_HANDLE); ULONGLONG deadline = 0; @@ -165,29 +298,182 @@ static rz_os_result_t rz_readline_timeout_console(char* buffer, size_t size, uin } } } -} -#endif +} +#else +typedef struct { + char buffer[4096]; + size_t length; + bool truncated; +} rz_posix_console_line_state_t; + +static rz_posix_console_line_state_t rz_posix_console_line_state = {{0}, 0, false}; +static struct termios rz_posix_original_termios; +static bool rz_posix_raw_console_enabled = false; + +static inline void rz_posix_console_restore(void) { + if (rz_posix_raw_console_enabled) { + tcsetattr(STDIN_FILENO, TCSAFLUSH, &rz_posix_original_termios); + rz_posix_raw_console_enabled = false; + } +} + +static inline bool rz_posix_console_enable_raw(void) { + struct termios raw; + if (rz_posix_raw_console_enabled) { + return true; + } + if (!isatty(STDIN_FILENO)) { + return false; + } + if (tcgetattr(STDIN_FILENO, &rz_posix_original_termios) != 0) { + return false; + } + raw = rz_posix_original_termios; + raw.c_lflag &= (tcflag_t)~(ECHO | ICANON); + raw.c_iflag &= (tcflag_t)~(IXON | ICRNL); + raw.c_cc[VMIN] = 0; + raw.c_cc[VTIME] = 0; + if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &raw) != 0) { + return false; + } + rz_posix_raw_console_enabled = true; + atexit(rz_posix_console_restore); + return true; +} + +static inline void rz_posix_console_echo_bytes(const char* text, size_t length) { + if (length > 0) { + (void)write(STDOUT_FILENO, text, length); + } +} + +static inline void rz_posix_console_line_reset(void) { + rz_posix_console_line_state.length = 0; + rz_posix_console_line_state.truncated = false; +} + +static inline void rz_posix_console_line_push_char(char c) { + if (rz_posix_console_line_state.length + 1 < sizeof(rz_posix_console_line_state.buffer)) { + rz_posix_console_line_state.buffer[rz_posix_console_line_state.length++] = c; + rz_posix_console_line_state.buffer[rz_posix_console_line_state.length] = '\0'; + } else { + rz_posix_console_line_state.truncated = true; + } +} + +static inline rz_os_result_t rz_posix_console_line_finish(char* buffer, size_t size) { + size_t copy_len = 0; + if (size > 0) { + copy_len = rz_posix_console_line_state.length; + if (copy_len >= size) { + copy_len = size - 1; + } + memcpy(buffer, rz_posix_console_line_state.buffer, copy_len); + buffer[copy_len] = '\0'; + } + + rz_os_result_t result = + (rz_posix_console_line_state.truncated || rz_posix_console_line_state.length >= size) + ? RZ_INPUT_TOO_LONG + : RZ_OK; + rz_posix_console_line_reset(); + return result; +} + +static inline bool rz_posix_console_handle_input_bytes(const char* bytes, size_t length, char* buffer, size_t size, rz_os_result_t* result_out) { + rz_keyboard_queue_push_input_bytes(bytes, length); + for (size_t i = 0; i < length; i++) { + char c = bytes[i]; + if (c == 27 && i + 2 < length && bytes[i + 1] == '[') { + i += 2; + continue; + } + if (c == '\r' || c == '\n') { + rz_posix_console_echo_bytes("\r\n", 2); + *result_out = rz_posix_console_line_finish(buffer, size); + return true; + } + if (c == '\b' || c == 127) { + if (rz_posix_console_line_state.length > 0) { + rz_posix_console_line_state.length--; + rz_posix_console_line_state.buffer[rz_posix_console_line_state.length] = '\0'; + rz_posix_console_echo_bytes("\b \b", 3); + } + continue; + } + if ((unsigned char)c >= ' ' || c == '\t') { + rz_posix_console_line_push_char(c); + rz_posix_console_echo_bytes(&c, 1); + } + } + return false; +} + +static rz_os_result_t rz_readline_timeout_terminal(char* buffer, size_t size, uint32_t timeout_ms) { + int stdin_fd = fileno(stdin); + fd_set read_fds; + int ready; + char input_buffer[16]; + ssize_t bytes_read; + rz_os_result_t line_result = RZ_TIMEOUT; + + if (!rz_posix_console_enable_raw()) { + return RZ_NO_INPUT; + } + + FD_ZERO(&read_fds); + FD_SET(stdin_fd, &read_fds); + if (timeout_ms == UINT32_MAX) { + ready = select(stdin_fd + 1, &read_fds, NULL, NULL, NULL); + } else { + struct timeval timeout = { + .tv_sec = (time_t)(timeout_ms / 1000), + .tv_usec = (suseconds_t)((timeout_ms % 1000) * 1000) + }; + ready = select(stdin_fd + 1, &read_fds, NULL, NULL, &timeout); + } + if (ready == 0) { + return RZ_TIMEOUT; + } + if (ready < 0) { + return RZ_NO_INPUT; + } + + bytes_read = read(stdin_fd, input_buffer, sizeof(input_buffer)); + if (bytes_read <= 0) { + if (bytes_read < 0 && (errno == EAGAIN || errno == EWOULDBLOCK)) { + return RZ_TIMEOUT; + } + return RZ_NO_INPUT; + } + if (rz_posix_console_handle_input_bytes(input_buffer, (size_t)bytes_read, buffer, size, &line_result)) { + return line_result; + } + return RZ_TIMEOUT; +} +#endif /* https://stackoverflow.com/a/4023921 */ -static rz_os_result_t rz_readline(char* buffer, size_t size) { - if (fgets(buffer, size, stdin) == NULL) { - return RZ_NO_INPUT; - } - - size_t len = strlen(buffer); - bool had_line_ending = false; - if(len > 0 && buffer[len - 1] == '\n') { - buffer[len - 1] = '\0'; - len--; - had_line_ending = true; - } - if (len > 0 && buffer[len - 1] == '\r') { - buffer[len - 1] = '\0'; - had_line_ending = true; - } - if (had_line_ending) { - return RZ_OK; +static rz_os_result_t rz_readline(char* buffer, size_t size) { + if (fgets(buffer, size, stdin) == NULL) { + return RZ_NO_INPUT; + } + + size_t len = strlen(buffer); + bool had_line_ending = false; + rz_keyboard_queue_push_input_bytes(buffer, len); + if(len > 0 && buffer[len - 1] == '\n') { + buffer[len - 1] = '\0'; + len--; + had_line_ending = true; } + if (len > 0 && buffer[len - 1] == '\r') { + buffer[len - 1] = '\0'; + had_line_ending = true; + } + if (had_line_ending) { + return RZ_OK; + } /* empty the line */ int ch; @@ -245,11 +531,14 @@ static rz_os_result_t rz_readline_timeout(char* buffer, size_t size, uint32_t ti rz_sleep_ms(remaining_ms < 10 ? remaining_ms : 10); } } -#else - int stdin_fd = fileno(stdin); - fd_set read_fds; - int ready; - FD_ZERO(&read_fds); +#else + int stdin_fd = fileno(stdin); + fd_set read_fds; + int ready; + if (isatty(stdin_fd)) { + return rz_readline_timeout_terminal(buffer, size, timeout_ms); + } + FD_ZERO(&read_fds); FD_SET(stdin_fd, &read_fds); if (timeout_ms == UINT32_MAX) { ready = select(stdin_fd + 1, &read_fds, NULL, NULL, NULL); diff --git a/src/runtime/rizzo.h b/src/runtime/rizzo.h index a888660..9a3fe2d 100644 --- a/src/runtime/rizzo.h +++ b/src/runtime/rizzo.h @@ -7,11 +7,12 @@ #define __RZ_INPUT_BUFFER_SIZE 512 #endif -#include "core.h" -#include "heap.h" -#include "later.h" -#include "channel.h" -#include "timer.h" +#include "core.h" +#include "heap.h" +#include "later.h" +#include "channel.h" +#include "tcp.h" +#include "timer.h" static bool rz_should_quit = false; @@ -19,26 +20,49 @@ static bool rz_should_quit = false; #include "os.h" #include "stdlib.h" -typedef struct rz_signal_list -{ - size_t count, capacity; - rz_signal_t *signals[]; -} rz_signal_list_t; -static rz_signal_list_t *rz_signal_list_create(); -static void rz_signal_list_add(rz_signal_list_t **list, rz_object_t *signal); -static void rz_print_registered_outputs(); -static void rz_print_registered_output_head(rz_signal_t *sig, bool force); - -rz_signal_list_t *rz_global_output_signals = NULL; +typedef struct rz_signal_list +{ + size_t count, capacity; + rz_signal_t *signals[]; +} rz_signal_list_t; + +typedef struct rz_port_output +{ + rz_signal_t *signal; + rz_socket_t socket; +} rz_port_output_t; + +typedef struct rz_port_output_list +{ + size_t count, capacity; + rz_port_output_t outputs[]; +} rz_port_output_list_t; + +static rz_signal_list_t *rz_signal_list_create(); +static void rz_signal_list_add(rz_signal_list_t **list, rz_object_t *signal); +static rz_port_output_list_t *rz_port_output_list_create(); +static void rz_port_output_list_add(rz_port_output_list_t **list, rz_object_t *signal, rz_socket_t socket); +static void rz_print_registered_outputs(); +static void rz_print_registered_output_head(rz_signal_t *sig, bool force); +static void rz_send_registered_port_output_head(rz_port_output_t *output, bool force); +static void rz_drain_keyboard_events(); + +rz_signal_list_t *rz_global_output_signals = NULL; +rz_port_output_list_t *rz_global_port_output_signals = NULL; /* initializes the Rizzo runtime. */ -static void rz_init_rizzo() -{ - rz_heap_init(); - rz_global_output_signals = rz_signal_list_create(); - rz_should_quit = false; - rz_timer_reset(); -} +static void rz_init_rizzo() +{ + rz_channel_reset_dynamic(); + rz_heap_init(); + rz_global_output_signals = rz_signal_list_create(); + rz_global_port_output_signals = rz_port_output_list_create(); + rz_should_quit = false; + rz_timer_reset(); + rz_tcp_reset(); + rz_keyboard_queue_reset(); + rz_random_state = 0; +} #if defined(__RZ_DEBUG_INFO) || defined(__RZ_HEAP_INFO) static uint64_t rz_debug_heap_step_count = 0; @@ -70,49 +94,73 @@ static inline void rz_step(rz_channel_t chan, rz_box_t v) rz_debug_heap_step_count++; #endif -#ifdef __RZ_DEBUG_INFO - printf("step %.4" PRIu64 ", channel %" PRIu64 ", Sig index %" PRIu64 ", ", rz_debug_heap_step_count, chan, rz_debug_signal_next_index); - rz_debug_print_heap(); -#endif -} - -/* Starts the Rizzo event loop: +#ifdef __RZ_DEBUG_INFO + printf("step %.4" PRIu64 ", channel %" PRIu64 ", Sig index %" PRIu64 ", ", rz_debug_heap_step_count, chan, rz_debug_signal_next_index); + rz_debug_print_heap(); +#endif +} + +static void rz_drain_keyboard_events() +{ + char key_buffer[RZ_KEYBOARD_EVENT_SIZE]; + while (!rz_should_quit && rz_keyboard_take_event(key_buffer, sizeof(key_buffer))) + { + rz_step(RZ_CHANNEL_KEYBOARD_IN, rz_make_string_len(key_buffer, strlen(key_buffer))); + } +} + +/* Starts the Rizzo event loop: - Listens to input on channels (currently only console input) - Then produces a time step by calling `rz_step` (which updates the heap) */ static rz_box_t rz_start_event_loop() { - char buffer[__RZ_INPUT_BUFFER_SIZE]; - rz_channel_t timer_channel; - rz_box_t timer_value; + char buffer[__RZ_INPUT_BUFFER_SIZE]; + rz_channel_t timer_channel; + rz_box_t timer_value; + rz_channel_t tcp_channel; + rz_box_t tcp_value; #ifdef __RZ_DEBUG_INFO printf("After initialization, Sig index %" PRIu64 ", ", rz_debug_signal_next_index); rz_debug_print_heap(); #endif - while (!rz_should_quit) - { - double now = rz_timer_now_seconds(); - uint32_t timeout_ms = UINT32_MAX; - bool has_timers = rz_timer_next_timeout_ms(now, &timeout_ms); - rz_os_result_t status = has_timers - ? rz_readline_timeout(buffer, sizeof(buffer), timeout_ms) - : rz_readline(buffer, sizeof(buffer)); - now = rz_timer_now_seconds(); - while (rz_timer_take_due(now, &timer_channel, &timer_value)) - { - rz_step(timer_channel, timer_value); - } - if (status == RZ_OK) - { - rz_step(RZ_CHANNEL_CONSOLE_IN, rz_make_string_len(buffer, strlen(buffer))); - } - else if (status == RZ_NO_INPUT) - { - if (!rz_timer_has_registered_channels()) - { - break; - } + while (!rz_should_quit) + { + rz_drain_keyboard_events(); + double now = rz_timer_now_seconds(); + uint32_t timeout_ms = UINT32_MAX; + bool has_timers = rz_timer_next_timeout_ms(now, &timeout_ms); + bool has_tcp_inputs = rz_tcp_has_registered_inputs(); + if (has_tcp_inputs && timeout_ms > 10) + { + timeout_ms = 10; + } + rz_os_result_t status = has_timers + ? rz_readline_timeout(buffer, sizeof(buffer), timeout_ms) + : (has_tcp_inputs + ? rz_readline_timeout(buffer, sizeof(buffer), timeout_ms) + : rz_readline(buffer, sizeof(buffer))); + now = rz_timer_now_seconds(); + while (rz_timer_take_due(now, &timer_channel, &timer_value)) + { + rz_step(timer_channel, timer_value); + } + while (rz_tcp_take_input(&tcp_channel, &tcp_value)) + { + rz_step(tcp_channel, tcp_value); + } + rz_drain_keyboard_events(); + if (status == RZ_OK) + { + rz_step(RZ_CHANNEL_CONSOLE_IN, rz_make_string_len(buffer, strlen(buffer))); + } + else if (status == RZ_NO_INPUT) + { + if (!rz_timer_has_registered_channels() && !rz_tcp_has_registered_inputs()) + { + break; + } if (!rz_should_quit && timeout_ms > 0 && timeout_ms != UINT32_MAX) { rz_sleep_ms(timeout_ms); @@ -147,8 +195,29 @@ static inline rz_box_t rz_register_output_signal(size_t num_args, rz_box_t *args /* we've just read a signal, which has a head value in the current time tick - output that */ rz_print_registered_output_head((rz_signal_t *)rz_unbox_ptr(sig), true); rz_signal_list_add(&rz_global_output_signals, rz_unbox_ptr(sig)); - return RZ_UNIT; -} + return RZ_UNIT; +} + +static inline rz_box_t rz_register_port_output_signal(size_t num_args, rz_box_t *args) +{ + (void)num_args; + rz_box_t port = args[0]; + rz_box_t sig = args[1]; + if (sig.kind != RZ_BOX_PTR || rz_object_get_type(rz_unbox_ptr(sig)) != RZ_SIGNAL) + { + rz_debug_print_box(sig); + fprintf(stderr, "Runtime error: rz_register_port_output_signal got a non-signal value (%d)\n", sig.kind); + exit(1); + } + rz_socket_t socket = rz_tcp_connect_localhost(rz_unbox_int(port)); + rz_port_output_t output = { + .signal = (rz_signal_t *)rz_unbox_ptr(sig), + .socket = socket, + }; + rz_send_registered_port_output_head(&output, true); + rz_port_output_list_add(&rz_global_port_output_signals, rz_unbox_ptr(sig), socket); + return RZ_UNIT; +} /* Registers a signal after heap update has already started for the current tick. Signals that already updated this tick are printed by the normal output pass. */ @@ -186,13 +255,30 @@ static inline void rz_print_registered_output_head(rz_signal_t *sig, bool force) } } -static inline void rz_print_registered_outputs() -{ - for (size_t i = 0; i < rz_global_output_signals->count; i++) - { - rz_print_registered_output_head(rz_global_output_signals->signals[i], false); - } -} +static inline void rz_print_registered_outputs() +{ + for (size_t i = 0; i < rz_global_output_signals->count; i++) + { + rz_print_registered_output_head(rz_global_output_signals->signals[i], false); + } + for (size_t i = 0; i < rz_global_port_output_signals->count; i++) + { + rz_send_registered_port_output_head(&rz_global_port_output_signals->outputs[i], false); + } +} + +static inline void rz_send_registered_port_output_head(rz_port_output_t *output, bool force) +{ + if (rz_unbox_int(output->signal->updated) || force) + { + if (!rz_box_is_string(output->signal->head)) + { + fprintf(stderr, "Runtime error: port_out_signal expected Signal String\n"); + exit(1); + } + rz_tcp_send_line(output->socket, output->signal->head); + } +} static rz_signal_list_t *rz_signal_list_create() { @@ -212,6 +298,30 @@ static void rz_signal_list_add(rz_signal_list_t **list_ref, rz_object_t *signal) list = (rz_signal_list_t *)realloc(list, sizeof(rz_signal_list_t) + new_capacity * sizeof(rz_signal_t *)); list->capacity = new_capacity; *list_ref = list; - } - list->signals[list->count++] = (rz_signal_t *)signal; -} + } + list->signals[list->count++] = (rz_signal_t *)signal; +} + +static rz_port_output_list_t *rz_port_output_list_create() +{ + size_t initial_capacity = 10; + rz_port_output_list_t *list = (rz_port_output_list_t *)malloc(sizeof(rz_port_output_list_t) + initial_capacity * sizeof(rz_port_output_t)); + list->count = 0; + list->capacity = initial_capacity; + return list; +} + +static void rz_port_output_list_add(rz_port_output_list_t **list_ref, rz_object_t *signal, rz_socket_t socket) +{ + rz_port_output_list_t *list = *list_ref; + if (list->count == list->capacity) + { + size_t new_capacity = list->capacity * 2; + list = (rz_port_output_list_t *)realloc(list, sizeof(rz_port_output_list_t) + new_capacity * sizeof(rz_port_output_t)); + list->capacity = new_capacity; + *list_ref = list; + } + list->outputs[list->count].signal = (rz_signal_t *)signal; + list->outputs[list->count].socket = socket; + list->count++; +} diff --git a/src/runtime/tcp.h b/src/runtime/tcp.h new file mode 100644 index 0000000..a1115b0 --- /dev/null +++ b/src/runtime/tcp.h @@ -0,0 +1,345 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#ifndef __RZ_INPUT_BUFFER_SIZE +#define __RZ_INPUT_BUFFER_SIZE 512 +#endif + +#include "allocation.h" +#include "channel.h" +#include "core.h" + +#ifdef _WIN32 +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#include +#include +typedef SOCKET rz_socket_t; +#define RZ_INVALID_SOCKET INVALID_SOCKET +#define rz_socket_close closesocket +#else +#include +#include +#include +#include +#include +typedef int rz_socket_t; +#define RZ_INVALID_SOCKET (-1) +#define rz_socket_close close +#endif + +typedef struct rz_tcp_client +{ + rz_socket_t socket; + char buffer[__RZ_INPUT_BUFFER_SIZE]; + size_t length; + struct rz_tcp_client *next; +} rz_tcp_client_t; + +typedef struct rz_tcp_input +{ + int64_t port; + rz_channel_t channel; + rz_socket_t listen_socket; + rz_tcp_client_t *clients; + struct rz_tcp_input *next; +} rz_tcp_input_t; + +static rz_tcp_input_t *rz_tcp_inputs = NULL; +static bool rz_tcp_initialized = false; + +static inline void rz_tcp_ensure_initialized(void) +{ + if (rz_tcp_initialized) + { + return; + } +#ifdef _WIN32 + WSADATA data; + if (WSAStartup(MAKEWORD(2, 2), &data) != 0) + { + fprintf(stderr, "Runtime error: failed to initialize Winsock\n"); + exit(1); + } +#endif + rz_tcp_initialized = true; +} + +static inline bool rz_tcp_would_block(void) +{ +#ifdef _WIN32 + int err = WSAGetLastError(); + return err == WSAEWOULDBLOCK; +#else + return errno == EAGAIN || errno == EWOULDBLOCK; +#endif +} + +static inline void rz_tcp_set_nonblocking(rz_socket_t socket) +{ +#ifdef _WIN32 + u_long mode = 1; + if (ioctlsocket(socket, FIONBIO, &mode) != 0) + { + fprintf(stderr, "Runtime error: failed to make TCP socket nonblocking\n"); + exit(1); + } +#else + int flags = fcntl(socket, F_GETFL, 0); + if (flags < 0 || fcntl(socket, F_SETFL, flags | O_NONBLOCK) < 0) + { + fprintf(stderr, "Runtime error: failed to make TCP socket nonblocking\n"); + exit(1); + } +#endif +} + +static inline void rz_tcp_validate_port(const char *name, int64_t port) +{ + if (port <= 0 || port > 65535) + { + fprintf(stderr, "Runtime error: builtin '%s' expected port in range 1..65535, got %" PRId64 "\n", name, port); + exit(1); + } +} + +static inline struct sockaddr_in rz_tcp_loopback_addr(int64_t port) +{ + struct sockaddr_in addr; + memset(&addr, 0, sizeof(addr)); + addr.sin_family = AF_INET; + addr.sin_port = htons((uint16_t)port); + addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); + return addr; +} + +static inline rz_socket_t rz_tcp_create_socket(void) +{ + rz_socket_t sock; + rz_tcp_ensure_initialized(); + sock = (rz_socket_t)socket(AF_INET, SOCK_STREAM, 0); + if (sock == RZ_INVALID_SOCKET) + { + fprintf(stderr, "Runtime error: failed to create TCP socket\n"); + exit(1); + } + return sock; +} + +static inline void rz_tcp_client_free(rz_tcp_client_t *client) +{ + if (client) + { + rz_socket_close(client->socket); + rz_free(client); + } +} + +static inline void rz_tcp_reset(void) +{ + rz_tcp_input_t *input = rz_tcp_inputs; + while (input) + { + rz_tcp_input_t *next_input = input->next; + rz_tcp_client_t *client = input->clients; + while (client) + { + rz_tcp_client_t *next_client = client->next; + rz_tcp_client_free(client); + client = next_client; + } + rz_socket_close(input->listen_socket); + rz_free(input); + input = next_input; + } + rz_tcp_inputs = NULL; +} + +static inline bool rz_tcp_has_registered_inputs(void) +{ + return rz_tcp_inputs != NULL; +} + +static inline rz_channel_t rz_tcp_input_register(int64_t port) +{ + rz_tcp_input_t *existing = rz_tcp_inputs; + while (existing) + { + if (existing->port == port) + { + return existing->channel; + } + existing = existing->next; + } + + rz_tcp_validate_port("port_input", port); + rz_socket_t listen_socket = rz_tcp_create_socket(); + struct sockaddr_in addr = rz_tcp_loopback_addr(port); + int reuse = 1; + if (setsockopt(listen_socket, SOL_SOCKET, SO_REUSEADDR, (const char *)&reuse, sizeof(reuse)) != 0) + { + rz_socket_close(listen_socket); + fprintf(stderr, "Runtime error: failed to configure TCP port %" PRId64 "\n", port); + exit(1); + } + if (bind(listen_socket, (struct sockaddr *)&addr, sizeof(addr)) != 0) + { + rz_socket_close(listen_socket); + fprintf(stderr, "Runtime error: failed to bind TCP input port %" PRId64 "\n", port); + exit(1); + } + if (listen(listen_socket, 5) != 0) + { + rz_socket_close(listen_socket); + fprintf(stderr, "Runtime error: failed to listen on TCP input port %" PRId64 "\n", port); + exit(1); + } + rz_tcp_set_nonblocking(listen_socket); + + rz_tcp_input_t *input = (rz_tcp_input_t *)rz_malloc(sizeof(rz_tcp_input_t)); + input->port = port; + input->channel = rz_channel_alloc(); + input->listen_socket = listen_socket; + input->clients = NULL; + input->next = rz_tcp_inputs; + rz_tcp_inputs = input; + return input->channel; +} + +static inline void rz_tcp_accept_pending(rz_tcp_input_t *input) +{ + while (true) + { + rz_socket_t client_socket = accept(input->listen_socket, NULL, NULL); + if (client_socket == RZ_INVALID_SOCKET) + { + if (rz_tcp_would_block()) + { + return; + } + return; + } + rz_tcp_set_nonblocking(client_socket); + rz_tcp_client_t *client = (rz_tcp_client_t *)rz_malloc(sizeof(rz_tcp_client_t)); + client->socket = client_socket; + client->length = 0; + client->next = input->clients; + input->clients = client; + } +} + +static inline bool rz_tcp_client_take_line(rz_tcp_client_t *client, rz_box_t *value_out, bool *closed_out) +{ + char c; + *closed_out = false; + while (true) + { + int received = recv(client->socket, &c, 1, 0); + if (received == 0) + { + *closed_out = true; + return false; + } + if (received < 0) + { + if (rz_tcp_would_block()) + { + return false; + } + *closed_out = true; + return false; + } + if (c == '\n') + { + *value_out = rz_make_string_len(client->buffer, client->length); + client->length = 0; + return true; + } + if (c == '\r') + { + continue; + } + if (client->length + 1 >= sizeof(client->buffer)) + { + fprintf(stderr, "Runtime error: TCP input line exceeded %d bytes\n", __RZ_INPUT_BUFFER_SIZE); + client->length = 0; + continue; + } + client->buffer[client->length++] = c; + } +} + +static inline bool rz_tcp_take_input(rz_channel_t *channel_out, rz_box_t *value_out) +{ + rz_tcp_input_t *input = rz_tcp_inputs; + while (input) + { + rz_tcp_accept_pending(input); + rz_tcp_client_t **client_ref = &input->clients; + while (*client_ref) + { + rz_tcp_client_t *client = *client_ref; + bool closed = false; + if (rz_tcp_client_take_line(client, value_out, &closed)) + { + *channel_out = input->channel; + return true; + } + if (closed) + { + *client_ref = client->next; + rz_tcp_client_free(client); + } + else + { + client_ref = &client->next; + } + } + input = input->next; + } + return false; +} + +static inline rz_socket_t rz_tcp_connect_localhost(int64_t port) +{ + rz_tcp_validate_port("port_out_signal", port); + rz_socket_t socket = rz_tcp_create_socket(); + struct sockaddr_in addr = rz_tcp_loopback_addr(port); + if (connect(socket, (struct sockaddr *)&addr, sizeof(addr)) != 0) + { + rz_socket_close(socket); + fprintf(stderr, "Runtime error: failed to connect to TCP output port %" PRId64 "\n", port); + exit(1); + } + return socket; +} + +static inline void rz_tcp_send_all(rz_socket_t socket, const char *bytes, size_t len) +{ + size_t sent_total = 0; + while (sent_total < len) + { + int sent = send(socket, bytes + sent_total, (int)(len - sent_total), 0); + if (sent <= 0) + { + fprintf(stderr, "Runtime error: failed to send TCP output\n"); + exit(1); + } + sent_total += (size_t)sent; + } +} + +static inline void rz_tcp_send_line(rz_socket_t socket, rz_box_t value) +{ + const char *bytes = rz_string_data(value); + size_t len = rz_string_byte_length(value); + rz_tcp_send_all(socket, bytes, len); + rz_tcp_send_all(socket, "\n", 1); +} diff --git a/src/runtime/timer.h b/src/runtime/timer.h index 985cdb1..d370d7e 100644 --- a/src/runtime/timer.h +++ b/src/runtime/timer.h @@ -2,13 +2,16 @@ #include #include -#include - -#ifdef _WIN32 -#include -#else -#include -#endif +#include + +#ifdef _WIN32 +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#include +#else +#include +#endif #include "allocation.h" #include "channel.h" @@ -23,8 +26,7 @@ typedef struct rz_timer struct rz_timer *next; } rz_timer_t; -static rz_timer_t *rz_timer_list = NULL; -static rz_channel_t rz_next_timer_channel = RZ_CHANNEL_TIMER_BASE; +static rz_timer_t *rz_timer_list = NULL; static inline double rz_timer_now_seconds(void) { @@ -49,16 +51,15 @@ static inline void rz_timer_reset(void) rz_timer_t *next = timer->next; rz_free(timer); timer = next; - } - rz_timer_list = NULL; - rz_next_timer_channel = RZ_CHANNEL_TIMER_BASE; -} + } + rz_timer_list = NULL; +} static inline rz_channel_t rz_timer_register(int64_t interval_ms) { rz_timer_t *timer = (rz_timer_t *)rz_malloc(sizeof(rz_timer_t)); double now_seconds = rz_timer_now_seconds(); - timer->channel = rz_next_timer_channel++; + timer->channel = rz_channel_alloc(); timer->interval_seconds = (double)interval_ms / 1000.0; timer->start_seconds = now_seconds; timer->next_fire_seconds = now_seconds + timer->interval_seconds; diff --git a/src/stdlib/signal.rizz b/src/stdlib/signal.rizz index 6ba2991..26b9848 100644 --- a/src/stdlib/signal.rizz +++ b/src/stdlib/signal.rizz @@ -158,3 +158,15 @@ fun trigger f s1 s2 = fun trigger_l f s1 s2 = (fun s1' -> trigger f s1' s2) |> s1 + +fun merge_events_from f xs ys changed : (Sync ('a, 'b) -> 'c) -> Signal 'a -> Signal 'b -> Sync (Signal 'a, Signal 'b) -> Signal 'c = + match changed with + | Left (xs_) -> + f (Left (head xs_)) :: (merge_events_from f xs_ ys |> sync (tail xs_) (tail ys)) + | Right (ys_) -> + f (Right (head ys_)) :: (merge_events_from f xs ys_ |> sync (tail xs) (tail ys_)) + | Both (xs_, ys_) -> + f (Both (head xs_, head ys_)) :: (merge_events_from f xs_ ys_ |> sync (tail xs_) (tail ys_)) + +fun merge_events f initial xs ys : (Sync ('a, 'b) -> 'c) -> 'c -> Signal 'a -> Signal 'b -> Signal 'c = + initial :: (merge_events_from f xs ys |> sync (tail xs) (tail ys)) diff --git a/src/test/test_builtins.ml b/src/test/test_builtins.ml index 717f600..439d5d1 100644 --- a/src/test/test_builtins.ml +++ b/src/test/test_builtins.ml @@ -11,12 +11,19 @@ let parse_and_typecheck input = Alcotest.(check int) "type errors" 0 (List.length errors); typed -let test_console_is_string_channel () = - let typed = parse_and_typecheck "let use_console = console\n" in - match typed with - | [TopLet (_, EVar ("console", Ann_typed (_, console_t)), _)] -> - Alcotest.(check bool) "console is Chan String" true (Ast.eq_typ console_t (TChan TString)) - | _ -> Alcotest.fail "unexpected typed AST shape for console builtin" +let test_console_is_string_channel () = + let typed = parse_and_typecheck "let use_console = console\n" in + match typed with + | [TopLet (_, EVar ("console", Ann_typed (_, console_t)), _)] -> + Alcotest.(check bool) "console is Chan String" true (Ast.eq_typ console_t (TChan TString)) + | _ -> Alcotest.fail "unexpected typed AST shape for console builtin" + +let test_keyboard_is_string_channel () = + let typed = parse_and_typecheck "let use_keyboard = keyboard\n" in + match typed with + | [TopLet (_, EVar ("keyboard", Ann_typed (_, keyboard_t)), _)] -> + Alcotest.(check bool) "keyboard is Chan String" true (Ast.eq_typ keyboard_t (TChan TString)) + | _ -> Alcotest.fail "unexpected typed AST shape for keyboard builtin" let test_parse_int_has_expected_type () = let typed = parse_and_typecheck "let maybe_num = parse_int(\"42\")\n" in @@ -30,8 +37,8 @@ let test_parse_int_has_expected_type () = (Ast.eq_typ result_t (TApp (TName "Option", [TInt]))) | _ -> Alcotest.fail "unexpected typed AST shape for parse_int builtin" -let test_clock_has_expected_type () = - let typed = parse_and_typecheck "let ticks = clock(100)\n" in +let test_clock_has_expected_type () = + let typed = parse_and_typecheck "let ticks = clock(100)\n" in match typed with | [TopLet (_, EApp (EVar ("clock", Ann_typed (_, builtin_t)), [_], Ann_typed (_, result_t)), _)] -> Alcotest.(check bool) "clock builtin type" @@ -40,9 +47,57 @@ let test_clock_has_expected_type () = Alcotest.(check bool) "clock return type" true (Ast.eq_typ result_t (TSignal TInt)) - | _ -> Alcotest.fail "unexpected typed AST shape for clock builtin" - -let test_new_builtins_have_expected_types () = + | _ -> Alcotest.fail "unexpected typed AST shape for clock builtin" + +let test_random_and_port_builtins_have_expected_types () = + let typed = + parse_and_typecheck + ("let random_value = random_int 10\n" + ^ "let port_values = port_input 9000\n" + ^ "let _port_out = port_out_signal 9001 port_values\n") + in + match typed with + | [ TopLet (_, EApp (EVar ("random_int", Ann_typed (_, random_t)), [_], Ann_typed (_, random_result_t)), _); + TopLet (_, EApp (EVar ("port_input", Ann_typed (_, port_input_t)), [_], Ann_typed (_, port_input_result_t)), _); + TopLet (_, EApp (EVar ("port_out_signal", Ann_typed (_, port_out_t)), [_; _], Ann_typed (_, port_out_result_t)), _) ] -> + Alcotest.(check bool) "random_int builtin type" + true + (Ast.eq_typ random_t (TFun (Cons1 (TInt, []), TInt))); + Alcotest.(check bool) "random_int result type" true (Ast.eq_typ random_result_t TInt); + Alcotest.(check bool) "port_input builtin type" + true + (Ast.eq_typ port_input_t (TFun (Cons1 (TInt, []), TSignal TString))); + Alcotest.(check bool) "port_input result type" true (Ast.eq_typ port_input_result_t (TSignal TString)); + Alcotest.(check bool) "port_out_signal builtin type" + true + (Ast.eq_typ port_out_t (TFun (Cons1 (TInt, [TSignal TString]), TUnit))); + Alcotest.(check bool) "port_out_signal result type" true (Ast.eq_typ port_out_result_t TUnit) + | _ -> Alcotest.fail "unexpected typed AST shape for random/port builtins" + +let test_terminal_builtins_have_expected_types () = + let typed = + parse_and_typecheck + ("let _clear = clear_screen ()\n" + ^ "let _hide = hide_cursor ()\n" + ^ "let _move = move_cursor 1 2\n" + ^ "let _show = show_cursor ()\n") + in + match typed with + | [ TopLet (_, EApp (EVar ("clear_screen", Ann_typed (_, clear_t)), [_], Ann_typed (_, clear_result_t)), _); + TopLet (_, EApp (EVar ("hide_cursor", Ann_typed (_, hide_t)), [_], Ann_typed (_, hide_result_t)), _); + TopLet (_, EApp (EVar ("move_cursor", Ann_typed (_, move_t)), [_; _], Ann_typed (_, move_result_t)), _); + TopLet (_, EApp (EVar ("show_cursor", Ann_typed (_, show_t)), [_], Ann_typed (_, show_result_t)), _) ] -> + Alcotest.(check bool) "clear_screen builtin type" true (Ast.eq_typ clear_t (TFun (Cons1 (TUnit, []), TUnit))); + Alcotest.(check bool) "clear_screen result type" true (Ast.eq_typ clear_result_t TUnit); + Alcotest.(check bool) "hide_cursor builtin type" true (Ast.eq_typ hide_t (TFun (Cons1 (TUnit, []), TUnit))); + Alcotest.(check bool) "hide_cursor result type" true (Ast.eq_typ hide_result_t TUnit); + Alcotest.(check bool) "move_cursor builtin type" true (Ast.eq_typ move_t (TFun (Cons1 (TInt, [TInt]), TUnit))); + Alcotest.(check bool) "move_cursor result type" true (Ast.eq_typ move_result_t TUnit); + Alcotest.(check bool) "show_cursor builtin type" true (Ast.eq_typ show_t (TFun (Cons1 (TUnit, []), TUnit))); + Alcotest.(check bool) "show_cursor result type" true (Ast.eq_typ show_result_t TUnit) + | _ -> Alcotest.fail "unexpected typed AST shape for terminal builtins" + +let test_new_builtins_have_expected_types () = let typed = parse_and_typecheck ("let mod_result = mod 7 3\n" @@ -155,8 +210,11 @@ let test_contextual_function_literals_allow_curried_shapes () = let builtin_tests = [ "console is a string channel", `Quick, test_console_is_string_channel; + "keyboard is a string channel", `Quick, test_keyboard_is_string_channel; "parse_int returns option int", `Quick, test_parse_int_has_expected_type; "clock returns signal int", `Quick, test_clock_has_expected_type; + "random and port builtins have expected types", `Quick, test_random_and_port_builtins_have_expected_types; + "terminal builtins have expected types", `Quick, test_terminal_builtins_have_expected_types; "new builtins have expected types", `Quick, test_new_builtins_have_expected_types; "list constructors and builtins have expected types", `Quick, test_list_constructors_and_projection_builtins_have_expected_types; "list support builtins have expected types", `Quick, test_list_supporting_builtins_have_expected_types; diff --git a/src/test/test_end_to_end.ml b/src/test/test_end_to_end.ml index c6676f1..7001424 100644 --- a/src/test/test_end_to_end.ml +++ b/src/test/test_end_to_end.ml @@ -103,17 +103,158 @@ let run_console_program ?(delay_s = 0.) ?(debug_malloc = false) ~program ~input with exn -> Alcotest.failf "Compilation failed with exception: %s" (Printexc.to_string exn) -let contains_substring ~text ~substring = +let contains_substring ~text ~substring = let text_length = String.length text in let substring_length = String.length substring in let rec loop index = if index + substring_length > text_length then false else if String.sub text index substring_length = substring then true else loop (index + 1) - in - substring_length = 0 || loop 0 - -let test_console_signal_input_string_is_freed () = + in + substring_length = 0 || loop 0 + +let compile_program_binary ~prefix ~program = + let output_file = Filename.temp_file prefix ".c" in + let binary_file = Filename.temp_file prefix ".exe" in + let original_cwd = Sys.getcwd () in + let cleanup () = + List.iter (fun path -> if Sys.file_exists path then Sys.remove path) [output_file; binary_file] + in + try + Rizzoc.compile_from_string program output_file; + Sys.chdir "../../../.."; + let command = + Rizzoc.to_shell_command + (Rizzoc.generated_c_compiler_invocation ~input_file:output_file + ~output_file:binary_file ()) + in + let status = Sys.command command in + Sys.chdir original_cwd; + if status <> 0 + then ( + cleanup (); + Alcotest.failf "C compile failed with status %d. Command: %s" status command); + binary_file, cleanup + with exn -> + Sys.chdir original_cwd; + cleanup (); + Alcotest.failf "Compilation failed with exception: %s" (Printexc.to_string exn) + +let read_available_lines ?(timeout_s = 2.0) in_chan = + let fd = Unix.descr_of_in_channel in_chan in + let deadline = Unix.gettimeofday () +. timeout_s in + let normalize_line line = + if String.ends_with ~suffix:"\r" line + then String.sub line 0 (String.length line - 1) + else line + in + let rec go acc = + let remaining = deadline -. Unix.gettimeofday () in + if remaining <= 0. then List.rev acc + else + let ready, _, _ = Unix.select [fd] [] [] remaining in + if ready = [] then List.rev acc + else + match input_line in_chan with + | line -> go (normalize_line line :: acc) + | exception End_of_file -> List.rev acc + in + go [] + +let get_free_tcp_port () = + let sock = Unix.socket Unix.PF_INET Unix.SOCK_STREAM 0 in + Fun.protect + ~finally:(fun () -> Unix.close sock) + (fun () -> + Unix.setsockopt sock Unix.SO_REUSEADDR true; + Unix.bind sock (Unix.ADDR_INET (Unix.inet_addr_loopback, 0)); + match Unix.getsockname sock with + | Unix.ADDR_INET (_, port) -> port + | _ -> Alcotest.fail "expected inet socket") + +let test_random_int_outputs_value_in_range () = + let program = + {| + fun entry x = + let n = random_int 10 in + let _o = console_out_signal (string_of_int n :: never) in + start_event_loop () + |} + in + let outputs, process_status = run_console_program ~program ~input:"trigger" () in + let maybe_value = + List.find_map + (fun line -> + match int_of_string_opt line with + | Some n when n >= 0 && n < 10 -> Some n + | _ -> None) + outputs + in + Alcotest.(check bool) "random output in range" true (Option.is_some maybe_value); + Alcotest.(check int) "process exit code" 0 + (match process_status with + | Unix.WEXITED code -> code + | Unix.WSIGNALED signal -> Alcotest.failf "Process was terminated by signal %d" signal + | Unix.WSTOPPED signal -> Alcotest.failf "Process was stopped by signal %d" signal) + +let test_random_int_rejects_non_positive_bound () = + if Sys.win32 then () + else + let program = + {| + fun entry x = + let n = random_int 0 in + let _o = console_out_signal (string_of_int n :: never) in + start_event_loop () + |} + in + let binary_file, cleanup = compile_program_binary ~prefix:"random-invalid" ~program in + Fun.protect + ~finally:cleanup + (fun () -> + let in_chan, out_chan, err_chan = + Unix.open_process_args_full binary_file [| binary_file |] (Unix.environment ()) + in + close_out out_chan; + let _outputs = read_available_lines ~timeout_s:0.1 in_chan in + let errors = read_available_lines err_chan in + let process_status = Unix.close_process_full (in_chan, out_chan, err_chan) in + Alcotest.(check bool) "error mentions positive upper bound" true + (List.exists (fun line -> contains_substring ~text:line ~substring:"positive upper bound") errors); + match process_status with + | Unix.WEXITED code -> Alcotest.(check bool) "process exits nonzero" true (code <> 0) + | Unix.WSIGNALED _ -> () + | Unix.WSTOPPED signal -> Alcotest.failf "Process was stopped by signal %d" signal) + +let test_port_output_loops_into_port_input () = + let port = get_free_tcp_port () in + let program = + Printf.sprintf + {| + fun entry x = + let port_sig = port_input %d in + let displayed = map (fun x -> "port: " + x + "; Random number:" + string_of_int (random_int 100)) port_sig in + let _send = port_out_signal %d ("hello from tcp" :: never) in + let _out = console_out_signal displayed in + let _q = quit_at (tail port_sig) in + start_event_loop () + |} + port + port + in + let outputs, process_status = run_console_program ~delay_s:1.0 ~program ~input:"" () in + Alcotest.(check bool) "tcp loopback output appears" true + (List.exists + (fun line -> + contains_substring ~text:line ~substring:"port: hello from tcp; Random number:") + outputs); + Alcotest.(check int) "process exit code" 0 + (match process_status with + | Unix.WEXITED code -> code + | Unix.WSIGNALED signal -> Alcotest.failf "Process was terminated by signal %d" signal + | Unix.WSTOPPED signal -> Alcotest.failf "Process was stopped by signal %d" signal) + +let test_console_signal_input_string_is_freed () = let program = {| fun entry x = @@ -137,7 +278,7 @@ let test_console_signal_input_string_is_freed () = | Unix.WSIGNALED signal -> Alcotest.failf "Process was terminated by signal %d" signal | Unix.WSTOPPED signal -> Alcotest.failf "Process was stopped by signal %d" signal) -let test_simple_console_identity () = +let test_simple_console_identity () = let program = {| fun entry x = @@ -155,9 +296,78 @@ let test_simple_console_identity () = (match process_status with | Unix.WEXITED code -> code | Unix.WSIGNALED signal -> Alcotest.failf "Process was terminated by signal %d" signal - | Unix.WSTOPPED signal -> Alcotest.failf "Process was stopped by signal %d" signal) - -let test_runtime_mod_and_string_helpers () = + | Unix.WSTOPPED signal -> Alcotest.failf "Process was stopped by signal %d" signal) + +let test_keyboard_signal_receives_key_press () = + let program = + {| + fun entry x = + let keys = mk_sig (wait keyboard) in + let _out = console_out_signal ("" :: keys) in + let _quit = quit_at (filter_l (fun key -> key == "q") keys) in + start_event_loop () + |} + in + let outputs, process_status = run_console_program ~delay_s:1.0 ~program ~input:"q" () in + Alcotest.(check bool) "keyboard output appears" true (List.mem "q" outputs); + Alcotest.(check int) "process exit code" 0 + (match process_status with + | Unix.WEXITED code -> code + | Unix.WSIGNALED signal -> Alcotest.failf "Process was terminated by signal %d" signal + | Unix.WSTOPPED signal -> Alcotest.failf "Process was stopped by signal %d" signal) + +let test_terminal_builtins_emit_escape_sequences_and_continue () = + let program = + {| + fun entry x = + let _clear = clear_screen () in + let _hide = hide_cursor () in + let _move = move_cursor 1 1 in + let _show = show_cursor () in + let _out = console_out_signal ("ok" :: never) in + start_event_loop () + |} + in + let outputs, process_status = run_console_program ~program ~input:"trigger" () in + Alcotest.(check bool) "program output appears after terminal controls" true + (List.exists (fun line -> contains_substring ~text:line ~substring:"ok") outputs); + Alcotest.(check int) "process exit code" 0 + (match process_status with + | Unix.WEXITED code -> code + | Unix.WSIGNALED signal -> Alcotest.failf "Process was terminated by signal %d" signal + | Unix.WSTOPPED signal -> Alcotest.failf "Process was stopped by signal %d" signal) + +let test_merge_events_combines_signal_updates () = + let program = + {| + fun label event = + match event with + | Left (_fast) -> "fast" + | Right (_slow) -> "slow" + | Both (_fast, _slow) -> "both" + + fun entry x = + let fast = clock 20 in + let slow = clock 40 in + let events = merge_events label "start" fast slow in + let _out = console_out_signal events in + let _quit = quit_at (tail slow) in + start_event_loop () + |} + in + let outputs, process_status = run_console_program ~delay_s:1.0 ~program ~input:"" () in + Alcotest.(check bool) "initial merge event appears" true (List.mem "start" outputs); + Alcotest.(check bool) "clock merge event appears" true + (List.exists + (fun line -> line = "fast" || line = "slow" || line = "both") + outputs); + Alcotest.(check int) "process exit code" 0 + (match process_status with + | Unix.WEXITED code -> code + | Unix.WSIGNALED signal -> Alcotest.failf "Process was terminated by signal %d" signal + | Unix.WSTOPPED signal -> Alcotest.failf "Process was stopped by signal %d" signal) + +let test_runtime_mod_and_string_helpers () = let program = {| fun entry x = @@ -379,8 +589,14 @@ let test_list_pattern_match_outputs_head () = | Unix.WSTOPPED signal -> Alcotest.failf "Process was stopped by signal %d" signal) let end_to_end_tests = [ - "Inputing on the consile outputs the same thing", `Quick, test_simple_console_identity; - "Issue filterL variant outputs quit", `Quick, test_issue_filterl_variant_outputs_quit; + "Inputing on the consile outputs the same thing", `Quick, test_simple_console_identity; + "random_int outputs value in range", `Quick, test_random_int_outputs_value_in_range; + "random_int rejects non-positive bound", `Quick, test_random_int_rejects_non_positive_bound; + "port output loops into port input", `Quick, test_port_output_loops_into_port_input; + "Keyboard signal receives key press", `Quick, test_keyboard_signal_receives_key_press; + "terminal builtins emit escape sequences and continue", `Quick, test_terminal_builtins_emit_escape_sequences_and_continue; + "merge_events combines signal updates", `Quick, test_merge_events_combines_signal_updates; + "Issue filterL variant outputs quit", `Quick, test_issue_filterl_variant_outputs_quit; "Issue filterL variant outputs quit after non-match", `Quick, test_issue_filterl_variant_outputs_quit_after_non_match; "Issue map_l variant outputs Hello world", `Quick, test_issue_map_l_variant_outputs_hello_world; "ostar constructor maps console signal", `Quick, test_ostar_constructor_maps_console_signal; diff --git a/src/test/test_runtime_windows.ml b/src/test/test_runtime_windows.ml index 71c072d..6b0547c 100644 --- a/src/test/test_runtime_windows.ml +++ b/src/test/test_runtime_windows.ml @@ -53,11 +53,13 @@ static int fail_at(int line) { int main(void) { char buffer[8]; + char key_buffer[32]; rz_os_result_t result = RZ_TIMEOUT; bool done = false; KEY_EVENT_RECORD key_event; size_t index; + rz_keyboard_queue_reset(); rz_console_line_reset(); ZeroMemory(&key_event, sizeof(key_event)); key_event.bKeyDown = TRUE; @@ -69,6 +71,9 @@ int main(void) { if (rz_console_line_state.length != 1 || rz_console_line_state.buffer[0] != 'a') { return fail_at(__LINE__); } + if (!rz_keyboard_take_event(key_buffer, sizeof(key_buffer)) || strcmp(key_buffer, "a") != 0) { + return fail_at(__LINE__); + } ZeroMemory(&key_event, sizeof(key_event)); key_event.bKeyDown = TRUE; @@ -77,6 +82,21 @@ int main(void) { if (!done || result != RZ_OK || strcmp(buffer, "a") != 0) { return fail_at(__LINE__); } + if (!rz_keyboard_take_event(key_buffer, sizeof(key_buffer)) || strcmp(key_buffer, "Enter") != 0) { + return fail_at(__LINE__); + } + + ZeroMemory(&key_event, sizeof(key_event)); + key_event.bKeyDown = TRUE; + key_event.uChar.AsciiChar = '\0'; + key_event.wVirtualKeyCode = VK_UP; + done = rz_console_handle_key_event(&key_event, buffer, sizeof(buffer), &result); + if (done) { + return fail_at(__LINE__); + } + if (!rz_keyboard_take_event(key_buffer, sizeof(key_buffer)) || strcmp(key_buffer, "ArrowUp") != 0) { + return fail_at(__LINE__); + } rz_console_line_reset(); for (index = 0; index < 9; index++) { @@ -104,4 +124,4 @@ int main(void) { let windows_runtime_tests = [ "Windows console timeout reader waits for completed line", `Quick, test_console_timeout_reader_waits_for_completed_line; -] \ No newline at end of file +]