diff --git a/.llm/BETA_6_NETWORKING_FOUNDATION.md b/.llm/BETA_6_NETWORKING_FOUNDATION.md new file mode 100644 index 0000000..b84d7b5 --- /dev/null +++ b/.llm/BETA_6_NETWORKING_FOUNDATION.md @@ -0,0 +1,66 @@ +# 1.0.0-beta.6 Networking Foundation Target + +Status: released as `1.0.0-beta.6` on 2026-05-22. + +`1.0.0-beta.6` targets a deliberately narrow networking foundation after the +resource-handle and host-error policy introduced in `1.0.0-beta.2`. The goal +is blocking loopback TCP only, enough for local client/server fixtures and +small request/response examples without committing to a full networking stack. + +## Slovo Source Surface + +The staged source facade is `lib/std/net.slo`, importable explicitly as +`std.net`. + +Exported helpers: + +- `tcp_connect_loopback_result : (i32) -> (result i32 i32)` +- `tcp_listen_loopback_result : (i32) -> (result i32 i32)` +- `tcp_bound_port_result : (i32) -> (result i32 i32)` +- `tcp_accept_result : (i32) -> (result i32 i32)` +- `tcp_read_all_result : (i32) -> (result string i32)` +- `tcp_write_text_result : (i32, string) -> (result i32 i32)` +- `tcp_close_result : (i32) -> (result i32 i32)` +- `tcp_write_text_ok : (i32, string) -> bool` +- `tcp_close_ok : (i32) -> bool` + +The `i32` values returned by successful connect/listen/accept operations are +opaque process-local handles. They are not host file descriptors, stable ABI +values, transferable capabilities, or ownership-checked affine resources. + +## Runtime Calls + +The facade wraps these compiler-known runtime calls: + +- `std.net.tcp_connect_loopback_result(port i32) -> (result i32 i32)` +- `std.net.tcp_listen_loopback_result(port i32) -> (result i32 i32)` +- `std.net.tcp_bound_port_result(handle i32) -> (result i32 i32)` +- `std.net.tcp_accept_result(listener i32) -> (result i32 i32)` +- `std.net.tcp_read_all_result(handle i32) -> (result string i32)` +- `std.net.tcp_write_text_result(handle i32, text string) -> (result i32 i32)` +- `std.net.tcp_close_result(handle i32) -> (result i32 i32)` + +Ordinary host failures return `err 1`. Successful status-returning operations +return `ok 0`. Successful handle-returning operations return `ok handle`. +Successful `tcp_bound_port_result` returns the bound loopback TCP port. + +## Fixtures + +- `examples/projects/std-import-net/` exercises explicit `std.net` source + import. +- `examples/projects/std-layout-local-net/` mirrors the facade as a local + module fixture and keeps the source-search contract explicit. + +The source-side fixtures use invalid ports and handles for deterministic +result-shape checks. Positive loopback client/server behavior is covered by +the matching compiler/runtime tests when the local sandbox allows loopback +sockets. + +## Deferrals + +This scope does not add DNS, TLS, UDP, Unix-domain sockets, non-loopback +binding, async IO, event loops, readiness polling, timeouts, buffering policy, +HTTP frameworks, socket options beyond the implementation minimum, +platform-specific error codes, rich host-error ADTs, stable runtime helper +symbols, stable ABI/layout/ownership guarantees, automatic cleanup, or a +stable standard-library API freeze. diff --git a/README.md b/README.md index 38614c5..32681fe 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ This repository is the canonical public monorepo for the language design, standard library source, compiler, runtime, examples, benchmarks, and technical documents. -Current release: `1.0.0-beta.5`. +Current release: `1.0.0-beta.6`. ## Repository Layout @@ -24,13 +24,13 @@ scripts/ local release and document tooling ## Beta Scope -`1.0.0-beta.5` keeps the `1.0.0-beta` language baseline, includes the +`1.0.0-beta.6` keeps the `1.0.0-beta` language baseline, includes the `1.0.0-beta.1` tooling/install hardening slice, the `1.0.0-beta.2` runtime/resource foundation bundle, the `1.0.0-beta.3` standard-library stabilization bundle, the `1.0.0-beta.4` language-usability diagnostics -bundle, and the `1.0.0-beta.5` local package/workspace discipline bundle. The -language baseline supports practical local command-line programs and libraries -with: +bundle, the `1.0.0-beta.5` local package/workspace discipline bundle, and the +`1.0.0-beta.6` loopback networking foundation. The language baseline supports +practical local command-line programs and libraries with: - modules, explicit imports, packages, and local workspaces - `new`, `check`, `fmt`, `test`, `doc`, and `build` @@ -39,11 +39,12 @@ with: current `match` - explicit `std/*.slo` imports from `lib/std`, installed `share/slovo/std`, or `SLOVO_STD_PATH` +- beta-scoped loopback TCP handles through `std.net` - hosted native builds through LLVM IR, Clang, and `runtime/runtime.c` Still deferred before stable: generics, maps/sets, broad package registry -semantics, networking/async, LSP/watch/debug-adapter guarantees, stable ABI and -layout, and a stable standard-library compatibility freeze. +semantics, DNS/TLS/async networking, LSP/watch/debug-adapter guarantees, +stable ABI and layout, and a stable standard-library compatibility freeze. ## Build And Test @@ -163,6 +164,19 @@ package/dependency summary, new workspace templates declare local-package rules. Remote registries, lockfiles, semantic-version solving, package publishing, and stable package ABI/layout remain deferred. +## 1.0.0-beta.6 Networking Foundation + +The `1.0.0-beta.6` release adds a narrow blocking loopback TCP foundation: + +- compiler-known `std.net.tcp_*_result` calls for connect, listen, + bound-port lookup, accept, read-all, write-text, and close +- `lib/std/net.slo` source facades and explicit std/local example projects +- opaque beta-scoped `i32` socket handles with concrete `result` values + +This is not a general networking stack. DNS, TLS, UDP, non-loopback binding, +async IO, HTTP frameworks, rich host-error ADTs, stable socket ABI/layout, and +automatic resource ownership remain deferred. + ## Documentation - [Language Manifest](docs/language/MANIFEST.md) diff --git a/compiler/Cargo.lock b/compiler/Cargo.lock index 7498867..0549084 100644 --- a/compiler/Cargo.lock +++ b/compiler/Cargo.lock @@ -4,4 +4,4 @@ version = 3 [[package]] name = "glagol" -version = "1.0.0-beta.5" +version = "1.0.0-beta.6" diff --git a/compiler/Cargo.toml b/compiler/Cargo.toml index 96bc271..3274b80 100644 --- a/compiler/Cargo.toml +++ b/compiler/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "glagol" -version = "1.0.0-beta.5" +version = "1.0.0-beta.6" edition = "2021" description = "Glagol, the first compiler for the Slovo language" license = "MIT OR Apache-2.0" diff --git a/compiler/src/llvm.rs b/compiler/src/llvm.rs index 365c65c..b7edf49 100644 --- a/compiler/src/llvm.rs +++ b/compiler/src/llvm.rs @@ -57,6 +57,13 @@ pub fn emit(_file: &str, program: &CheckedProgram) -> Result { | "__glagol_env_get_result" | "__glagol_fs_read_text_result" | "__glagol_fs_read_open_text_result" + | "__glagol_net_tcp_read_all_result" | "__glagol_io_read_stdin_result" ) { return self.emit_string_result_host_call(expr, callee, &arg_values); @@ -1670,11 +1678,17 @@ impl FunctionGen<'_> { || callee == "__glagol_fs_remove_file_result" || callee == "__glagol_fs_create_dir_result" || callee == "__glagol_fs_close_result" + || callee == "__glagol_net_tcp_write_text_result" + || callee == "__glagol_net_tcp_close_result" { return self.emit_i32_result_status_call(expr, callee, &arg_values); } if callee == "__glagol_fs_open_text_read_result" + || callee == "__glagol_net_tcp_connect_loopback_result" + || callee == "__glagol_net_tcp_listen_loopback_result" + || callee == "__glagol_net_tcp_bound_port_result" + || callee == "__glagol_net_tcp_accept_result" || callee == "__glagol_string_parse_i32_result" { return self.emit_i32_result_encoded_i64_call(expr, callee, &arg_values); diff --git a/compiler/src/std_runtime.rs b/compiler/src/std_runtime.rs index 2b0a09e..0852531 100644 --- a/compiler/src/std_runtime.rs +++ b/compiler/src/std_runtime.rs @@ -68,6 +68,7 @@ const F64_PARAM: &[RuntimeType] = &[RuntimeType::F64]; const BOOL_PARAM: &[RuntimeType] = &[RuntimeType::Bool]; const STRING_PARAM: &[RuntimeType] = &[RuntimeType::String]; const STRING_STRING_PARAMS: &[RuntimeType] = &[RuntimeType::String, RuntimeType::String]; +const I32_STRING_PARAMS: &[RuntimeType] = &[RuntimeType::I32, RuntimeType::String]; const VEC_I32_PARAM: &[RuntimeType] = &[RuntimeType::VecI32]; const VEC_I32_I32_PARAMS: &[RuntimeType] = &[RuntimeType::VecI32, RuntimeType::I32]; const VEC_I64_PARAM: &[RuntimeType] = &[RuntimeType::VecI64]; @@ -350,6 +351,55 @@ pub const FUNCTIONS: &[RuntimeFunction] = &[ return_type: RuntimeType::ResultI32I32, promoted: true, }, + RuntimeFunction { + source_name: "std.net.tcp_connect_loopback_result", + runtime_symbol: "__glagol_net_tcp_connect_loopback_result", + params: I32_PARAM, + return_type: RuntimeType::ResultI32I32, + promoted: true, + }, + RuntimeFunction { + source_name: "std.net.tcp_listen_loopback_result", + runtime_symbol: "__glagol_net_tcp_listen_loopback_result", + params: I32_PARAM, + return_type: RuntimeType::ResultI32I32, + promoted: true, + }, + RuntimeFunction { + source_name: "std.net.tcp_bound_port_result", + runtime_symbol: "__glagol_net_tcp_bound_port_result", + params: I32_PARAM, + return_type: RuntimeType::ResultI32I32, + promoted: true, + }, + RuntimeFunction { + source_name: "std.net.tcp_accept_result", + runtime_symbol: "__glagol_net_tcp_accept_result", + params: I32_PARAM, + return_type: RuntimeType::ResultI32I32, + promoted: true, + }, + RuntimeFunction { + source_name: "std.net.tcp_read_all_result", + runtime_symbol: "__glagol_net_tcp_read_all_result", + params: I32_PARAM, + return_type: RuntimeType::ResultStringI32, + promoted: true, + }, + RuntimeFunction { + source_name: "std.net.tcp_write_text_result", + runtime_symbol: "__glagol_net_tcp_write_text_result", + params: I32_STRING_PARAMS, + return_type: RuntimeType::ResultI32I32, + promoted: true, + }, + RuntimeFunction { + source_name: "std.net.tcp_close_result", + runtime_symbol: "__glagol_net_tcp_close_result", + params: I32_PARAM, + return_type: RuntimeType::ResultI32I32, + promoted: true, + }, RuntimeFunction { source_name: "std.vec.i32.empty", runtime_symbol: "__glagol_vec_i32_empty", @@ -665,6 +715,13 @@ const RESERVED_HELPER_SYMBOLS: &[&str] = &[ "__glagol_fs_open_text_read_result", "__glagol_fs_read_open_text_result", "__glagol_fs_close_result", + "__glagol_net_tcp_connect_loopback_result", + "__glagol_net_tcp_listen_loopback_result", + "__glagol_net_tcp_bound_port_result", + "__glagol_net_tcp_accept_result", + "__glagol_net_tcp_read_all_result", + "__glagol_net_tcp_write_text_result", + "__glagol_net_tcp_close_result", "__glagol_vec_i32_eq", "__glagol_vec_i32_allocation_trap", "__glagol_vec_i32_index_trap", diff --git a/compiler/src/test_runner.rs b/compiler/src/test_runner.rs index d8f76ae..03ac37c 100644 --- a/compiler/src/test_runner.rs +++ b/compiler/src/test_runner.rs @@ -2672,6 +2672,170 @@ fn eval_expr( payload: if was_open { 0 } else { 1 }, }); } + if runtime_symbol == "__glagol_net_tcp_connect_loopback_result" { + let Some(port) = args.first() else { + return Err(unsupported_test_expr( + file, + expr, + "malformed `std.net.tcp_connect_loopback_result` calls", + )); + }; + let port = eval_expr(file, port, locals, functions, foreign_imports, depth)?; + let Some(_) = port.as_i32() else { + return Err(unsupported_test_expr( + file, + expr, + "`std.net.tcp_connect_loopback_result` port on non-i32 values", + )); + }; + return Ok(Value::ResultI32 { + is_ok: false, + payload: 1, + }); + } + if runtime_symbol == "__glagol_net_tcp_listen_loopback_result" { + let Some(port) = args.first() else { + return Err(unsupported_test_expr( + file, + expr, + "malformed `std.net.tcp_listen_loopback_result` calls", + )); + }; + let port = eval_expr(file, port, locals, functions, foreign_imports, depth)?; + let Some(_) = port.as_i32() else { + return Err(unsupported_test_expr( + file, + expr, + "`std.net.tcp_listen_loopback_result` port on non-i32 values", + )); + }; + return Ok(Value::ResultI32 { + is_ok: false, + payload: 1, + }); + } + if runtime_symbol == "__glagol_net_tcp_bound_port_result" { + let Some(handle) = args.first() else { + return Err(unsupported_test_expr( + file, + expr, + "malformed `std.net.tcp_bound_port_result` calls", + )); + }; + let handle = eval_expr(file, handle, locals, functions, foreign_imports, depth)?; + let Some(_) = handle.as_i32() else { + return Err(unsupported_test_expr( + file, + expr, + "`std.net.tcp_bound_port_result` handle on non-i32 values", + )); + }; + return Ok(Value::ResultI32 { + is_ok: false, + payload: 1, + }); + } + if runtime_symbol == "__glagol_net_tcp_accept_result" { + let Some(listener) = args.first() else { + return Err(unsupported_test_expr( + file, + expr, + "malformed `std.net.tcp_accept_result` calls", + )); + }; + let listener = + eval_expr(file, listener, locals, functions, foreign_imports, depth)?; + let Some(_) = listener.as_i32() else { + return Err(unsupported_test_expr( + file, + expr, + "`std.net.tcp_accept_result` listener on non-i32 values", + )); + }; + return Ok(Value::ResultI32 { + is_ok: false, + payload: 1, + }); + } + if runtime_symbol == "__glagol_net_tcp_read_all_result" { + let Some(handle) = args.first() else { + return Err(unsupported_test_expr( + file, + expr, + "malformed `std.net.tcp_read_all_result` calls", + )); + }; + let handle = eval_expr(file, handle, locals, functions, foreign_imports, depth)?; + let Some(_) = handle.as_i32() else { + return Err(unsupported_test_expr( + file, + expr, + "`std.net.tcp_read_all_result` handle on non-i32 values", + )); + }; + return Ok(Value::ResultStringI32 { + is_ok: false, + ok_payload: String::new(), + err_payload: 1, + }); + } + if runtime_symbol == "__glagol_net_tcp_write_text_result" { + let Some(handle) = args.first() else { + return Err(unsupported_test_expr( + file, + expr, + "malformed `std.net.tcp_write_text_result` calls", + )); + }; + let Some(text) = args.get(1) else { + return Err(unsupported_test_expr( + file, + expr, + "malformed `std.net.tcp_write_text_result` calls", + )); + }; + let handle = eval_expr(file, handle, locals, functions, foreign_imports, depth)?; + let text = eval_expr(file, text, locals, functions, foreign_imports, depth)?; + let Some(_) = handle.as_i32() else { + return Err(unsupported_test_expr( + file, + expr, + "`std.net.tcp_write_text_result` handle on non-i32 values", + )); + }; + let Some(_) = text.as_string() else { + return Err(unsupported_test_expr( + file, + expr, + "`std.net.tcp_write_text_result` text on non-string values", + )); + }; + return Ok(Value::ResultI32 { + is_ok: false, + payload: 1, + }); + } + if runtime_symbol == "__glagol_net_tcp_close_result" { + let Some(handle) = args.first() else { + return Err(unsupported_test_expr( + file, + expr, + "malformed `std.net.tcp_close_result` calls", + )); + }; + let handle = eval_expr(file, handle, locals, functions, foreign_imports, depth)?; + let Some(_) = handle.as_i32() else { + return Err(unsupported_test_expr( + file, + expr, + "`std.net.tcp_close_result` handle on non-i32 values", + )); + }; + return Ok(Value::ResultI32 { + is_ok: false, + payload: 1, + }); + } if runtime_symbol == "__glagol_vec_i32_empty" { return Ok(Value::VecI32(Vec::new())); } diff --git a/compiler/tests/promotion_gate.rs b/compiler/tests/promotion_gate.rs index bc40c9d..cfae030 100644 --- a/compiler/tests/promotion_gate.rs +++ b/compiler/tests/promotion_gate.rs @@ -1141,6 +1141,28 @@ const STANDARD_FS_SOURCE_FACADE_ALPHA: &[&str] = &[ "read_bool_or", ]; +const STANDARD_NET_SOURCE_FACADE_ALPHA: &[&str] = &[ + "tcp_connect_loopback_result", + "tcp_listen_loopback_result", + "tcp_bound_port_result", + "tcp_accept_result", + "tcp_read_all_result", + "tcp_write_text_result", + "tcp_close_result", + "tcp_write_text_ok", + "tcp_close_ok", +]; + +const STANDARD_NET_RUNTIME_NAMES: &[&str] = &[ + "std.net.tcp_connect_loopback_result", + "std.net.tcp_listen_loopback_result", + "std.net.tcp_bound_port_result", + "std.net.tcp_accept_result", + "std.net.tcp_read_all_result", + "std.net.tcp_write_text_result", + "std.net.tcp_close_result", +]; + const STANDARD_PROCESS_SOURCE_FACADE_ALPHA: &[&str] = &[ "argc", "arg", @@ -1715,6 +1737,7 @@ fn promotion_gate_artifacts_are_aligned() { repo.join("examples/projects/std-layout-local-random"); let glagol_project_std_layout_local_env = repo.join("examples/projects/std-layout-local-env"); let glagol_project_std_layout_local_fs = repo.join("examples/projects/std-layout-local-fs"); + let glagol_project_std_layout_local_net = repo.join("examples/projects/std-layout-local-net"); let glagol_project_std_layout_local_io = repo.join("examples/projects/std-layout-local-io"); let glagol_project_std_layout_local_cli = repo.join("examples/projects/std-layout-local-cli"); let glagol_project_std_layout_local_vec_i32 = @@ -1734,6 +1757,7 @@ fn promotion_gate_artifacts_are_aligned() { let glagol_project_std_import_random = repo.join("examples/projects/std-import-random"); let glagol_project_std_import_env = repo.join("examples/projects/std-import-env"); let glagol_project_std_import_fs = repo.join("examples/projects/std-import-fs"); + let glagol_project_std_import_net = repo.join("examples/projects/std-import-net"); let glagol_project_std_import_process = repo.join("examples/projects/std-import-process"); let glagol_project_std_import_string = repo.join("examples/projects/std-import-string"); let glagol_project_std_import_num = repo.join("examples/projects/std-import-num"); @@ -2085,6 +2109,7 @@ fn promotion_gate_artifacts_are_aligned() { assert_project_std_import_random_tooling_matches_fixture(&glagol_project_std_import_random); assert_project_std_import_env_tooling_matches_fixture(&glagol_project_std_import_env); assert_project_std_import_fs_tooling_matches_fixture(&glagol_project_std_import_fs); + assert_project_std_import_net_tooling_matches_fixture(&glagol_project_std_import_net); assert_project_std_import_process_tooling_matches_fixture(&glagol_project_std_import_process); assert_project_std_import_string_tooling_matches_fixture(&glagol_project_std_import_string); assert_project_std_import_num_tooling_matches_fixture(&glagol_project_std_import_num); @@ -2123,6 +2148,9 @@ fn promotion_gate_artifacts_are_aligned() { &glagol_project_std_layout_local_env, ); assert_project_std_layout_local_fs_tooling_matches_fixture(&glagol_project_std_layout_local_fs); + assert_project_std_layout_local_net_tooling_matches_fixture( + &glagol_project_std_layout_local_net, + ); assert_project_std_layout_local_io_tooling_matches_fixture(&glagol_project_std_layout_local_io); assert_project_std_layout_local_cli_tooling_matches_fixture( &glagol_project_std_layout_local_cli, @@ -3310,6 +3338,7 @@ fn assert_slovo_std_source_layout_alpha(repo: &Path, std_dir: &Path) { std_dir.join("fs.slo"), std_dir.join("io.slo"), std_dir.join("math.slo"), + std_dir.join("net.slo"), std_dir.join("num.slo"), std_dir.join("option.slo"), std_dir.join("process.slo"), @@ -3338,6 +3367,7 @@ fn assert_slovo_std_source_layout_alpha(repo: &Path, std_dir: &Path) { "env.slo", "fs.slo", "math.slo", + "net.slo", "num.slo", "option.slo", "process.slo", @@ -4194,6 +4224,56 @@ fn assert_slovo_std_source_layout_alpha(repo: &Path, std_dir: &Path) { helper ); } + + let local_net = repo.join("examples/projects/std-layout-local-net/src/net.slo"); + let slovo_net = read(&std_dir.join("net.slo")); + let glagol_net = read(&local_net); + assert!( + slovo_net.contains("(module net") && glagol_net.contains("(module net"), + "both Slovo std/net.slo and the Glagol local fixture should use explicit `net` module source shape" + ); + for runtime_name in STANDARD_NET_RUNTIME_NAMES { + assert!( + slovo_net.contains(runtime_name) && glagol_net.contains(runtime_name), + "standard net facade must stay backed by `{}`", + runtime_name + ); + } + assert_std_only_contains( + &slovo_net, + STANDARD_NET_RUNTIME_NAMES, + "Slovo std/net.slo must not introduce other compiler-known std names", + ); + assert_std_only_contains( + &glagol_net, + STANDARD_NET_RUNTIME_NAMES, + "Glagol local net fixture must not introduce other compiler-known std names", + ); + for source in [&slovo_net, &glagol_net] { + assert!( + !source.contains("dns") + && !source.contains("tls") + && !source.contains("udp") + && !source.contains("async") + && !source.contains("http") + && !source.contains("timeout") + && !source.contains("non_loopback") + && !source.contains("host_error"), + "standard net facade must not claim deferred networking policies" + ); + } + for helper in STANDARD_NET_SOURCE_FACADE_ALPHA { + assert!( + slovo_net.contains(&format!("(fn {} ", helper)), + "Slovo std/net.slo is missing beta6 facade `{}`", + helper + ); + assert!( + glagol_net.contains(&format!("(fn {} ", helper)), + "Glagol local net fixture is missing beta6 facade `{}`", + helper + ); + } } fn assert_source_shaped_file(path: &Path) { @@ -7007,6 +7087,26 @@ fn assert_project_std_import_fs_tooling_matches_fixture(project: &Path) { ); } +fn assert_project_std_import_net_tooling_matches_fixture(project: &Path) { + assert_project_std_import_host_facade_tooling_matches_fixture( + project, + "net", + STANDARD_NET_SOURCE_FACADE_ALPHA, + concat!( + "test \"explicit std net invalid connect facade\" ... ok\n", + "test \"explicit std net invalid listen facade\" ... ok\n", + "test \"explicit std net invalid bound port facade\" ... ok\n", + "test \"explicit std net invalid accept facade\" ... ok\n", + "test \"explicit std net invalid read facade\" ... ok\n", + "test \"explicit std net invalid write facade\" ... ok\n", + "test \"explicit std net invalid close facade\" ... ok\n", + "test \"explicit std net helper invalids facade\" ... ok\n", + "test \"explicit std net facade all\" ... ok\n", + "9 test(s) passed\n", + ), + ); +} + fn assert_project_std_import_process_tooling_matches_fixture(project: &Path) { assert_project_std_import_host_facade_shape( project, @@ -9089,6 +9189,88 @@ fn assert_standard_fs_source_facade_alpha(project: &Path) { } } +fn assert_project_std_layout_local_net_tooling_matches_fixture(project: &Path) { + assert!(project.join("slovo.toml").is_file()); + assert!(project.join("src/net.slo").is_file()); + assert!(project.join("src/main.slo").is_file()); + assert_standard_net_source_facade_alpha(project); + + let check = run_glagol([OsStr::new("check"), project.as_os_str()]); + assert_success_stdout(check, "", "std layout local net project check"); + + let test = run_glagol([OsStr::new("test"), project.as_os_str()]); + assert_success_stdout( + test, + concat!( + "test \"explicit local net invalid connect facade\" ... ok\n", + "test \"explicit local net invalid listen facade\" ... ok\n", + "test \"explicit local net invalid bound port facade\" ... ok\n", + "test \"explicit local net invalid accept facade\" ... ok\n", + "test \"explicit local net invalid read facade\" ... ok\n", + "test \"explicit local net invalid write facade\" ... ok\n", + "test \"explicit local net invalid close facade\" ... ok\n", + "test \"explicit local net helper invalids facade\" ... ok\n", + "test \"explicit local net facade all\" ... ok\n", + "9 test(s) passed\n", + ), + "std layout local net project test", + ); +} + +fn assert_standard_net_source_facade_alpha(project: &Path) { + let net_source = read(&project.join("src/net.slo")); + let main = read(&project.join("src/main.slo")); + + assert!( + net_source.starts_with("(module net (export "), + "local net fixture must stay an explicitly exported local module" + ); + assert!( + main.starts_with("(module main)\n\n(import net ("), + "local net fixture must stay an explicit local import" + ); + assert!( + !main.contains("(import std") && !main.contains("(import slovo.std"), + "standard net source facade fixture must not use automatic std imports" + ); + for runtime_name in STANDARD_NET_RUNTIME_NAMES { + assert!( + net_source.contains(runtime_name), + "standard net source facade fixture must wrap `{}`", + runtime_name + ); + } + assert_std_only_contains( + &net_source, + STANDARD_NET_RUNTIME_NAMES, + "standard net source facade fixture must use only existing std.net runtime names directly", + ); + assert!( + !main.contains("std.") + && !net_source.contains("dns") + && !net_source.contains("tls") + && !net_source.contains("udp") + && !net_source.contains("async") + && !net_source.contains("http") + && !net_source.contains("timeout") + && !net_source.contains("non_loopback") + && !net_source.contains("host_error"), + "standard net source facade fixture must remain local and must not claim deferred networking policies" + ); + for helper in STANDARD_NET_SOURCE_FACADE_ALPHA { + assert!( + net_source.contains(&format!("(fn {} ", helper)), + "local net fixture is missing facade `{}`", + helper + ); + assert!( + main.contains(helper), + "main fixture import/use is missing facade `{}`", + helper + ); + } +} + fn assert_project_std_layout_local_io_tooling_matches_fixture(project: &Path) { assert!(project.join("slovo.toml").is_file()); assert!(project.join("src/io.slo").is_file()); diff --git a/compiler/tests/standard_net.rs b/compiler/tests/standard_net.rs new file mode 100644 index 0000000..ae9e4fd --- /dev/null +++ b/compiler/tests/standard_net.rs @@ -0,0 +1,496 @@ +use std::{ + env, + ffi::OsStr, + fs, + io::{ErrorKind, Read, Write}, + net::{Shutdown, TcpListener, TcpStream}, + path::{Path, PathBuf}, + process::{Child, Command, Output, Stdio}, + sync::atomic::{AtomicUsize, Ordering}, + thread, + time::{Duration, Instant}, +}; + +static NEXT_FIXTURE_ID: AtomicUsize = AtomicUsize::new(0); + +#[test] +fn standard_net_lowers_to_private_runtime_helpers() { + let fixture = write_fixture( + "lowering", + r#" +(module main) + +(fn main () -> i32 + (match (std.net.tcp_connect_loopback_result 1) + ((ok client) + (std.net.tcp_write_text_result client "ping") + (std.net.tcp_read_all_result client) + (std.net.tcp_close_result client) + 0) + ((err code) + (match (std.net.tcp_listen_loopback_result 0) + ((ok listener) + (std.net.tcp_bound_port_result listener) + (std.net.tcp_accept_result listener) + (std.net.tcp_close_result listener) + 0) + ((err listen_code) + listen_code))))) +"#, + ); + let output = run_glagol([fixture.as_os_str()]); + assert_success("compile standard net lowering", &output); + let stdout = String::from_utf8_lossy(&output.stdout); + + assert!( + stdout.contains("declare i64 @__glagol_net_tcp_connect_loopback_result(i32)") + && stdout.contains("declare i64 @__glagol_net_tcp_listen_loopback_result(i32)") + && stdout.contains("declare i64 @__glagol_net_tcp_bound_port_result(i32)") + && stdout.contains("declare i64 @__glagol_net_tcp_accept_result(i32)") + && stdout.contains("declare ptr @__glagol_net_tcp_read_all_result(i32)") + && stdout.contains("declare i32 @__glagol_net_tcp_write_text_result(i32, ptr)") + && stdout.contains("declare i32 @__glagol_net_tcp_close_result(i32)") + && stdout.contains("call i64 @__glagol_net_tcp_connect_loopback_result(i32 1)") + && stdout.contains("call i32 @__glagol_net_tcp_write_text_result(") + && stdout.contains("call ptr @__glagol_net_tcp_read_all_result(") + && stdout.contains("call i64 @__glagol_net_tcp_listen_loopback_result(i32 0)") + && stdout.contains("call i64 @__glagol_net_tcp_bound_port_result(") + && stdout.contains("call i64 @__glagol_net_tcp_accept_result(") + && stdout.contains("call i32 @__glagol_net_tcp_close_result(") + && !stdout.contains("@std.net."), + "standard net LLVM shape drifted\nstdout:\n{}", + stdout + ); +} + +#[test] +fn test_runner_reports_deterministic_net_result_errors() { + let fixture = write_fixture( + "test-runner", + r#" +(module main) + +(test "connect invalid port returns err" + (= (unwrap_err (std.net.tcp_connect_loopback_result 0)) 1)) + +(test "listen invalid port returns err" + (= (unwrap_err (std.net.tcp_listen_loopback_result -1)) 1)) + +(test "bound port invalid handle returns err" + (= (unwrap_err (std.net.tcp_bound_port_result -1)) 1)) + +(test "accept invalid listener returns err" + (= (unwrap_err (std.net.tcp_accept_result -1)) 1)) + +(test "read invalid handle returns err" + (= (unwrap_err (std.net.tcp_read_all_result -1)) 1)) + +(test "write invalid handle returns err" + (= (unwrap_err (std.net.tcp_write_text_result -1 "ping")) 1)) + +(test "close invalid handle returns err" + (= (unwrap_err (std.net.tcp_close_result -1)) 1)) +"#, + ); + let output = run_glagol([OsStr::new("test"), fixture.as_os_str()]); + assert_success("run standard net tests", &output); + assert_eq!( + String::from_utf8_lossy(&output.stdout), + concat!( + "test \"connect invalid port returns err\" ... ok\n", + "test \"listen invalid port returns err\" ... ok\n", + "test \"bound port invalid handle returns err\" ... ok\n", + "test \"accept invalid listener returns err\" ... ok\n", + "test \"read invalid handle returns err\" ... ok\n", + "test \"write invalid handle returns err\" ... ok\n", + "test \"close invalid handle returns err\" ... ok\n", + "7 test(s) passed\n", + ), + "standard net test runner stdout drifted" + ); +} + +#[test] +fn standard_net_diagnostics_cover_promoted_and_unpromoted_names() { + let cases = [ + ( + "connect-arity", + r#" +(module main) + +(fn main () -> i32 + (std.net.tcp_connect_loopback_result)) +"#, + "ArityMismatch", + ), + ( + "write-type", + r#" +(module main) + +(fn main () -> i32 + (std.net.tcp_write_text_result "handle" "ping") + 0) +"#, + "TypeMismatch", + ), + ( + "unknown-net", + r#" +(module main) + +(fn main () -> i32 + (std.net.udp_bind_loopback_result 0)) +"#, + "UnsupportedStandardLibraryCall", + ), + ( + "promoted-shadow", + r#" +(module main) + +(fn std.net.tcp_close_result ((handle i32)) -> (result i32 i32) + (ok i32 i32 0)) + +(fn main () -> i32 + (unwrap_ok (std.net.tcp_close_result 1))) +"#, + "DuplicateFunction", + ), + ]; + + for (name, source, diagnostic) in cases { + let fixture = write_fixture(name, source); + let output = run_glagol([fixture.as_os_str()]); + let stdout = String::from_utf8_lossy(&output.stdout); + let stderr = String::from_utf8_lossy(&output.stderr); + + assert!( + !output.status.success(), + "compiler unexpectedly accepted `{}`\nstdout:\n{}\nstderr:\n{}", + name, + stdout, + stderr + ); + assert!( + stdout.is_empty(), + "rejected compile wrote stdout:\n{}", + stdout + ); + assert!( + stderr.contains(diagnostic), + "diagnostic `{}` was not reported for `{}`\nstderr:\n{}", + diagnostic, + name, + stderr + ); + } +} + +#[test] +fn hosted_loopback_client_and_server_smoke_when_clang_is_available() { + let Some(clang) = find_clang() else { + eprintln!("skipping standard net runtime smoke: set GLAGOL_CLANG or install clang"); + return; + }; + + let Some(port) = try_reserve_loopback_port() else { + eprintln!("skipping standard net server smoke: loopback bind is not permitted"); + return; + }; + let server_source = format!( + r#" +(module main) + +(fn main () -> i32 + (match (std.net.tcp_listen_loopback_result {port}) + ((ok listener) + (match (std.net.tcp_accept_result listener) + ((ok stream) + (match (std.net.tcp_read_all_result stream) + ((ok text) + (std.net.tcp_write_text_result stream "pong") + (std.net.tcp_close_result stream) + (std.net.tcp_close_result listener) + (if (= text "ping") 0 2)) + ((err read_code) + (std.net.tcp_close_result stream) + (std.net.tcp_close_result listener) + 3))) + ((err accept_code) + (std.net.tcp_close_result listener) + 4))) + ((err listen_code) + 5))) +"# + ); + let server_fixture = write_fixture("runtime-server", &server_source); + let compile = run_glagol([server_fixture.as_os_str()]); + assert_success("compile standard net server smoke", &compile); + let server_exe = compile_with_runtime(&clang, "standard-net-server", &compile.stdout); + + let mut child = Command::new(&server_exe) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .unwrap_or_else(|err| panic!("spawn `{}`: {}", server_exe.display(), err)); + + let mut stream = connect_with_retry(port, &mut child); + stream + .write_all(b"ping") + .unwrap_or_else(|err| panic!("write ping to loopback server: {}", err)); + stream + .shutdown(Shutdown::Write) + .expect("shutdown loopback client write half"); + + let mut response = String::new(); + stream + .read_to_string(&mut response) + .unwrap_or_else(|err| panic!("read loopback server response: {}", err)); + assert_eq!(response, "pong", "loopback response drifted"); + + let output = wait_child_with_timeout(child, Duration::from_secs(5)); + assert_success("run standard net server smoke", &output); + assert!( + output.stdout.is_empty(), + "standard net server smoke wrote stdout:\n{}", + String::from_utf8_lossy(&output.stdout) + ); +} + +#[test] +fn hosted_loopback_client_smoke_when_clang_is_available() { + let Some(clang) = find_clang() else { + eprintln!("skipping standard net client smoke: set GLAGOL_CLANG or install clang"); + return; + }; + + let listener = match TcpListener::bind(("127.0.0.1", 0)) { + Ok(listener) => listener, + Err(err) if err.kind() == ErrorKind::PermissionDenied => { + eprintln!("skipping standard net client smoke: loopback bind is not permitted"); + return; + } + Err(err) => panic!("bind loopback smoke listener: {}", err), + }; + let port = listener + .local_addr() + .expect("loopback smoke listener addr") + .port(); + let server = thread::spawn(move || { + let (mut stream, _) = listener.accept().expect("accept loopback client"); + let mut request = [0u8; 4]; + stream + .read_exact(&mut request) + .expect("read loopback client request"); + assert_eq!(&request, b"ping"); + stream + .write_all(b"pong") + .expect("write loopback client response"); + }); + + let client_source = format!( + r#" +(module main) + +(fn main () -> i32 + (match (std.net.tcp_connect_loopback_result {port}) + ((ok client) + (std.net.tcp_write_text_result client "ping") + (match (std.net.tcp_read_all_result client) + ((ok text) + (std.net.tcp_close_result client) + (if (= text "pong") 0 2)) + ((err read_code) + (std.net.tcp_close_result client) + 3))) + ((err connect_code) + 4))) +"# + ); + let client_fixture = write_fixture("runtime-client", &client_source); + let compile = run_glagol([client_fixture.as_os_str()]); + assert_success("compile standard net client smoke", &compile); + let run = compile_and_run_with_runtime(&clang, "standard-net-client", &compile.stdout); + assert_success("run standard net client smoke", &run); + assert!( + run.stdout.is_empty(), + "standard net client smoke wrote stdout:\n{}", + String::from_utf8_lossy(&run.stdout) + ); + server.join().expect("loopback smoke server thread"); +} + +fn run_glagol(args: I) -> Output +where + I: IntoIterator, + S: AsRef, +{ + Command::new(env!("CARGO_BIN_EXE_glagol")) + .args(args) + .current_dir(Path::new(env!("CARGO_MANIFEST_DIR"))) + .output() + .expect("run glagol") +} + +fn write_fixture(name: &str, source: &str) -> PathBuf { + let mut path = env::temp_dir(); + path.push(format!( + "glagol-standard-net-{}-{}-{}.slo", + name, + std::process::id(), + NEXT_FIXTURE_ID.fetch_add(1, Ordering::Relaxed) + )); + fs::write(&path, source).unwrap_or_else(|err| panic!("write `{}`: {}", path.display(), err)); + path +} + +fn try_reserve_loopback_port() -> Option { + let listener = match TcpListener::bind(("127.0.0.1", 0)) { + Ok(listener) => listener, + Err(err) if err.kind() == ErrorKind::PermissionDenied => return None, + Err(err) => panic!("reserve loopback port: {}", err), + }; + Some( + listener + .local_addr() + .expect("reserved loopback addr") + .port(), + ) +} + +fn connect_with_retry(port: u16, child: &mut Child) -> TcpStream { + let deadline = Instant::now() + Duration::from_secs(5); + loop { + match TcpStream::connect(("127.0.0.1", port)) { + Ok(stream) => return stream, + Err(connect_err) => { + if let Some(status) = child.try_wait().expect("poll loopback server child") { + let mut stdout = String::new(); + let mut stderr = String::new(); + if let Some(mut out) = child.stdout.take() { + let _ = out.read_to_string(&mut stdout); + } + if let Some(mut err) = child.stderr.take() { + let _ = err.read_to_string(&mut stderr); + } + panic!( + "loopback server exited before connect: {}\nstdout:\n{}\nstderr:\n{}", + status, stdout, stderr + ); + } + if Instant::now() >= deadline { + panic!("timed out connecting to loopback server: {}", connect_err); + } + thread::sleep(Duration::from_millis(20)); + } + } + } +} + +fn wait_child_with_timeout(mut child: Child, timeout: Duration) -> Output { + let deadline = Instant::now() + timeout; + while Instant::now() < deadline { + if child.try_wait().expect("poll child").is_some() { + return child.wait_with_output().expect("collect child output"); + } + thread::sleep(Duration::from_millis(20)); + } + + let _ = child.kill(); + let output = child + .wait_with_output() + .expect("collect killed child output"); + panic!( + "child timed out\nstdout:\n{}\nstderr:\n{}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); +} + +fn assert_success(context: &str, output: &Output) { + let stdout = String::from_utf8_lossy(&output.stdout); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!( + output.status.success(), + "{} failed\nstdout:\n{}\nstderr:\n{}", + context, + stdout, + stderr + ); +} + +fn compile_and_run_with_runtime(clang: &Path, name: &str, ir: &[u8]) -> Output { + let exe_path = compile_with_runtime(clang, name, ir); + Command::new(&exe_path) + .output() + .unwrap_or_else(|err| panic!("run `{}`: {}", exe_path.display(), err)) +} + +fn compile_with_runtime(clang: &Path, name: &str, ir: &[u8]) -> PathBuf { + let manifest = Path::new(env!("CARGO_MANIFEST_DIR")); + let temp_dir = env::temp_dir().join(format!( + "glagol-standard-net-{}-{}", + std::process::id(), + NEXT_FIXTURE_ID.fetch_add(1, Ordering::Relaxed) + )); + fs::create_dir_all(&temp_dir) + .unwrap_or_else(|err| panic!("create `{}`: {}", temp_dir.display(), err)); + + let ir_path = temp_dir.join(format!("{}.ll", name)); + let exe_path = temp_dir.join(name); + fs::write(&ir_path, ir).unwrap_or_else(|err| panic!("write `{}`: {}", ir_path.display(), err)); + + let runtime = manifest.join("../runtime/runtime.c"); + let mut clang_command = Command::new(clang); + clang_command + .arg(&runtime) + .arg(&ir_path) + .arg("-o") + .arg(&exe_path) + .current_dir(manifest); + configure_clang_runtime_env(&mut clang_command, clang); + let clang_output = clang_command + .output() + .unwrap_or_else(|err| panic!("run `{}`: {}", clang.display(), err)); + assert_success("clang standard net runtime smoke", &clang_output); + exe_path +} + +fn find_clang() -> Option { + if let Some(path) = env::var_os("GLAGOL_CLANG").filter(|value| !value.is_empty()) { + return Some(PathBuf::from(path)); + } + + let hermetic_clang = PathBuf::from("/tmp/glagol-clang-root/usr/bin/clang"); + if hermetic_clang.is_file() { + return Some(hermetic_clang); + } + + find_on_path("clang") +} + +fn find_on_path(program: &str) -> Option { + let path = env::var_os("PATH")?; + env::split_paths(&path) + .map(|dir| dir.join(program)) + .find(|candidate| candidate.is_file()) +} + +fn configure_clang_runtime_env(command: &mut Command, clang: &Path) { + if !clang.starts_with("/tmp/glagol-clang-root") { + return; + } + + let root = Path::new("/tmp/glagol-clang-root"); + let lib64 = root.join("usr/lib64"); + let lib = root.join("usr/lib"); + let mut paths = vec![lib64, lib]; + + if let Some(existing) = env::var_os("LD_LIBRARY_PATH") { + paths.extend(env::split_paths(&existing)); + } + + let joined = env::join_paths(paths).expect("join LD_LIBRARY_PATH"); + command.env("LD_LIBRARY_PATH", joined); +} diff --git a/compiler/tests/standard_net_source_facade_alpha.rs b/compiler/tests/standard_net_source_facade_alpha.rs new file mode 100644 index 0000000..103e4f4 --- /dev/null +++ b/compiler/tests/standard_net_source_facade_alpha.rs @@ -0,0 +1,245 @@ +use std::{ + ffi::OsStr, + fs, + path::Path, + process::{Command, Output}, +}; + +const EXPECTED_LOCAL_TEST_OUTPUT: &str = concat!( + "test \"explicit local net invalid connect facade\" ... ok\n", + "test \"explicit local net invalid listen facade\" ... ok\n", + "test \"explicit local net invalid bound port facade\" ... ok\n", + "test \"explicit local net invalid accept facade\" ... ok\n", + "test \"explicit local net invalid read facade\" ... ok\n", + "test \"explicit local net invalid write facade\" ... ok\n", + "test \"explicit local net invalid close facade\" ... ok\n", + "test \"explicit local net helper invalids facade\" ... ok\n", + "test \"explicit local net facade all\" ... ok\n", + "9 test(s) passed\n", +); + +const EXPECTED_STD_IMPORT_TEST_OUTPUT: &str = concat!( + "test \"explicit std net invalid connect facade\" ... ok\n", + "test \"explicit std net invalid listen facade\" ... ok\n", + "test \"explicit std net invalid bound port facade\" ... ok\n", + "test \"explicit std net invalid accept facade\" ... ok\n", + "test \"explicit std net invalid read facade\" ... ok\n", + "test \"explicit std net invalid write facade\" ... ok\n", + "test \"explicit std net invalid close facade\" ... ok\n", + "test \"explicit std net helper invalids facade\" ... ok\n", + "test \"explicit std net facade all\" ... ok\n", + "9 test(s) passed\n", +); + +const STANDARD_NET_SOURCE_FACADE_ALPHA: &[&str] = &[ + "tcp_connect_loopback_result", + "tcp_listen_loopback_result", + "tcp_bound_port_result", + "tcp_accept_result", + "tcp_read_all_result", + "tcp_write_text_result", + "tcp_close_result", + "tcp_write_text_ok", + "tcp_close_ok", +]; + +const STANDARD_NET_RUNTIME_NAMES: &[&str] = &[ + "std.net.tcp_connect_loopback_result", + "std.net.tcp_listen_loopback_result", + "std.net.tcp_bound_port_result", + "std.net.tcp_accept_result", + "std.net.tcp_read_all_result", + "std.net.tcp_write_text_result", + "std.net.tcp_close_result", +]; + +#[test] +fn standard_net_source_facade_project_checks_formats_and_tests() { + let project = + Path::new(env!("CARGO_MANIFEST_DIR")).join("../examples/projects/std-layout-local-net"); + + assert_local_net_fixture_is_source_authored(&project); + + let fmt = run_glagol([ + OsStr::new("fmt"), + OsStr::new("--check"), + project.as_os_str(), + ]); + assert_success("std layout local net fmt --check", &fmt); + + let check = run_glagol([OsStr::new("check"), project.as_os_str()]); + assert_success_stdout(check, "", "std layout local net check"); + + let test = run_glagol([OsStr::new("test"), project.as_os_str()]); + assert_success_stdout( + test, + EXPECTED_LOCAL_TEST_OUTPUT, + "std layout local net test output", + ); +} + +#[test] +fn standard_net_std_import_project_checks_formats_and_tests() { + let project = Path::new(env!("CARGO_MANIFEST_DIR")).join("../examples/projects/std-import-net"); + + assert_std_import_net_fixture_uses_repo_std(&project); + + let fmt = run_glagol([ + OsStr::new("fmt"), + OsStr::new("--check"), + project.as_os_str(), + ]); + assert_success("std import net fmt --check", &fmt); + + let check = run_glagol([OsStr::new("check"), project.as_os_str()]); + assert_success_stdout(check, "", "std import net check"); + + let test = run_glagol([OsStr::new("test"), project.as_os_str()]); + assert_success_stdout( + test, + EXPECTED_STD_IMPORT_TEST_OUTPUT, + "std import net test output", + ); +} + +fn assert_local_net_fixture_is_source_authored(project: &Path) { + let net = read(&project.join("src/net.slo")); + let main = read(&project.join("src/main.slo")); + + assert!( + net.starts_with("(module net (export "), + "net.slo must stay an explicit local module export" + ); + assert!( + main.starts_with("(module main)\n\n(import net ("), + "main.slo must stay an explicit local net import" + ); + assert!( + !main.contains("(import std") && !main.contains("(import slovo.std"), + "net fixture must not depend on automatic or package std imports" + ); + assert_net_source_shape(&net, &main, "local net fixture"); + assert!( + !main.contains("std."), + "local net main fixture must use only local imports" + ); +} + +fn assert_std_import_net_fixture_uses_repo_std(project: &Path) { + let std_net = read(&Path::new(env!("CARGO_MANIFEST_DIR")).join("../lib/std/net.slo")); + let main = read(&project.join("src/main.slo")); + + assert!( + !project.join("src/net.slo").exists(), + "std import net fixture must use repo-root std/net.slo, not a local copy" + ); + assert!( + main.starts_with("(module main)\n\n(import std.net ("), + "std import net fixture must use explicit `std.net` import syntax" + ); + assert_net_source_shape(&std_net, &main, "repo std.net fixture"); +} + +fn assert_net_source_shape(net: &str, main: &str, context: &str) { + for runtime_name in STANDARD_NET_RUNTIME_NAMES { + assert!( + net.contains(runtime_name), + "{} must wrap `{}`", + context, + runtime_name + ); + } + assert_std_only_contains(net, STANDARD_NET_RUNTIME_NAMES, context); + assert!( + !net.contains("dns") + && !net.contains("tls") + && !net.contains("udp") + && !net.contains("async") + && !net.contains("http") + && !net.contains("timeout") + && !net.contains("non_loopback") + && !net.contains("host_error") + && !main.contains("dns") + && !main.contains("tls") + && !main.contains("udp") + && !main.contains("async") + && !main.contains("http") + && !main.contains("timeout") + && !main.contains("non_loopback") + && !main.contains("host_error"), + "{} must not claim deferred networking policies", + context + ); + + for helper in STANDARD_NET_SOURCE_FACADE_ALPHA { + assert!( + net.contains(&format!("(fn {} ", helper)), + "{} is missing source facade `{}`", + context, + helper + ); + assert!( + main.contains(helper), + "{} main fixture import/use is missing `{}`", + context, + helper + ); + } +} + +fn assert_std_only_contains(source: &str, allowed: &[&str], context: &str) { + let mut remaining = source.to_string(); + for name in allowed { + remaining = remaining.replace(name, ""); + } + assert!( + !remaining.contains("std."), + "{} introduced unexpected compiler-known std names", + context + ); +} + +fn run_glagol(args: I) -> Output +where + I: IntoIterator, + S: AsRef, +{ + Command::new(env!("CARGO_BIN_EXE_glagol")) + .args(args) + .current_dir(Path::new(env!("CARGO_MANIFEST_DIR"))) + .output() + .expect("run glagol") +} + +fn read(path: &Path) -> String { + fs::read_to_string(path).unwrap_or_else(|err| panic!("read `{}`: {}", path.display(), err)) +} + +fn assert_success(context: &str, output: &Output) { + let stdout = String::from_utf8_lossy(&output.stdout); + let stderr = String::from_utf8_lossy(&output.stderr); + + assert!( + output.status.success(), + "{} failed\nstdout:\n{}\nstderr:\n{}", + context, + stdout, + stderr + ); + assert!(stderr.is_empty(), "{} wrote stderr:\n{}", context, stderr); +} + +fn assert_success_stdout(output: Output, expected: &str, context: &str) { + let stdout = String::from_utf8_lossy(&output.stdout); + let stderr = String::from_utf8_lossy(&output.stderr); + + assert!( + output.status.success(), + "{} failed\nstdout:\n{}\nstderr:\n{}", + context, + stdout, + stderr + ); + assert_eq!(stdout, expected, "{} stdout drifted", context); + assert!(stderr.is_empty(), "{} wrote stderr:\n{}", context, stderr); +} diff --git a/docs/POST_BETA_ROADMAP.md b/docs/POST_BETA_ROADMAP.md index 8f63913..dbcd7a3 100644 --- a/docs/POST_BETA_ROADMAP.md +++ b/docs/POST_BETA_ROADMAP.md @@ -157,6 +157,14 @@ Work: - keep TLS, async, DNS policy, HTTP server frameworks, and event loops deferred - add small examples such as echo client/server or local request/response +Released in `1.0.0-beta.6`: `lib/std/net.slo` now provides explicit wrappers +for loopback TCP connect, listen, bound-port lookup, accept, read-all, +write-text, and close result calls. The source fixtures use invalid port/handle +checks for deterministic result-shape coverage, and the compiler/runtime tests +cover lowering plus hosted loopback client/server smoke when the local sandbox +allows loopback sockets. DNS, TLS, UDP, async IO, non-loopback binding, HTTP +frameworks, rich host-error ADTs, and stable socket ABI/layout remain deferred. + Why sixth: networking is useful, but doing it before resources and host errors would create unstable API debt. diff --git a/docs/compiler/RELEASE_NOTES.md b/docs/compiler/RELEASE_NOTES.md index 2480b03..5764f12 100644 --- a/docs/compiler/RELEASE_NOTES.md +++ b/docs/compiler/RELEASE_NOTES.md @@ -12,6 +12,37 @@ integration/readiness release, not the first real beta. No unreleased changes yet. +## 1.0.0-beta.6 + +Release label: `1.0.0-beta.6` + +Release date: 2026-05-22 + +Release state: networking foundation beta update + +### Summary + +Glagol `1.0.0-beta.6` keeps the `1.0.0-beta` compiler support baseline and +adds the first narrow networking foundation: + +- compiler-known `std.net.tcp_connect_loopback_result`, + `std.net.tcp_listen_loopback_result`, `std.net.tcp_bound_port_result`, + `std.net.tcp_accept_result`, `std.net.tcp_read_all_result`, + `std.net.tcp_write_text_result`, and `std.net.tcp_close_result` +- POSIX loopback TCP runtime helpers using opaque process-local `i32` handles + and concrete `result` values +- deterministic interpreter/test-runner error behavior for `std.net` invalid + handles and ports +- focused lowering, diagnostics, source-facade, promotion, and hosted loopback + smoke tests, with hosted socket smokes skipped only when the local sandbox + denies loopback sockets + +### Explicit Deferrals + +This release does not add DNS, TLS, UDP, non-loopback binding, async IO, event +loops, HTTP frameworks, rich host-error ADTs, stable socket ABI/layout, +automatic resource ownership, or a stable standard-library API freeze. + ## 1.0.0-beta.5 Release label: `1.0.0-beta.5` diff --git a/docs/compiler/ROADMAP.md b/docs/compiler/ROADMAP.md index c6dd642..d63bc1e 100644 --- a/docs/compiler/ROADMAP.md +++ b/docs/compiler/ROADMAP.md @@ -22,15 +22,15 @@ general-purpose beta release. A Glagol feature is done only when it has parser/lowerer support, checker behavior, diagnostics for invalid forms, backend behavior or explicit unsupported diagnostics, and tests. -Current stage: `1.0.0-beta.5`, released on 2026-05-22 as the first post-beta -package/workspace discipline update. It keeps the `1.0.0-beta` -language/compiler support baseline and includes the `1.0.0-beta.1` tooling -hardening release, the `1.0.0-beta.2` runtime/resource foundation release, the -`1.0.0-beta.3` standard-library stabilization release, the `1.0.0-beta.4` -language-usability diagnostics release, `[workspace] default_package = "name"` -for deterministic build/run entry selection, tighter workspace-member/default -package diagnostics, workspace package/dependency docs, and -`docs/language/PACKAGES.md` for beta local-package rules. +Current stage: `1.0.0-beta.6`, released on 2026-05-22 as the first post-beta +networking foundation update. It keeps the `1.0.0-beta` language/compiler +support baseline and includes the `1.0.0-beta.1` tooling hardening release, the +`1.0.0-beta.2` runtime/resource foundation release, the `1.0.0-beta.3` +standard-library stabilization release, the `1.0.0-beta.4` +language-usability diagnostics release, the `1.0.0-beta.5` package/workspace +discipline release, and the `1.0.0-beta.6` compiler-known `std.net` loopback +TCP runtime family with focused lowering, interpreter, diagnostics, +source-facade, promotion, and hosted smoke coverage. The final experimental precursor scope is `exp-125`. Its unsigned direct-value flow, parse/format runtime lanes, and matching staged stdlib helper breadth diff --git a/docs/language/RELEASE_NOTES.md b/docs/language/RELEASE_NOTES.md index 775f52a..b7adb27 100644 --- a/docs/language/RELEASE_NOTES.md +++ b/docs/language/RELEASE_NOTES.md @@ -8,18 +8,47 @@ Historical `exp-*` releases listed here are experimental maturity milestones. The pushed tag `v2.0.0-beta.1` is historical. It is now documented as an experimental integration/readiness release, not as a beta maturity claim. -The current release is `1.0.0-beta.5`, published on 2026-05-22. It keeps the +The current release is `1.0.0-beta.6`, published on 2026-05-22. It keeps the `1.0.0-beta` language surface, includes the first post-beta tooling/install hardening bundle from `1.0.0-beta.1`, and adds the first runtime/resource foundation bundle from `1.0.0-beta.2` plus the first standard-library stabilization bundle from `1.0.0-beta.3`, the first language-usability diagnostics bundle from `1.0.0-beta.4`, and the first local -package/workspace discipline bundle. +package/workspace discipline bundle from `1.0.0-beta.5`, plus the first +loopback networking foundation bundle from `1.0.0-beta.6`. ## Unreleased No unreleased changes yet. +## 1.0.0-beta.6 + +Release label: `1.0.0-beta.6` + +Release name: Networking Foundation Bundle + +Release date: 2026-05-22 + +Status: released beta networking foundation update on the `1.0.0-beta` +language baseline. + +- `lib/std/net.slo` stages an explicit `std.net` source facade for blocking + loopback TCP only. +- The facade wraps the targeted compiler-known calls + `std.net.tcp_connect_loopback_result`, + `std.net.tcp_listen_loopback_result`, `std.net.tcp_bound_port_result`, + `std.net.tcp_accept_result`, `std.net.tcp_read_all_result`, + `std.net.tcp_write_text_result`, and `std.net.tcp_close_result`. +- `examples/projects/std-import-net/` and + `examples/projects/std-layout-local-net/` document the explicit import and + local source facade shapes using deterministic invalid port/handle result + checks. + +This release does not add DNS, TLS, async IO, UDP, non-loopback binding, HTTP +frameworks, stable socket ABI/layout, platform-specific error codes, rich +host-error ADTs, automatic ownership/finalization, or a stable standard-library +API freeze. + ## 1.0.0-beta.5 Release label: `1.0.0-beta.5` diff --git a/docs/language/ROADMAP.md b/docs/language/ROADMAP.md index c057c8f..fa4127e 100644 --- a/docs/language/ROADMAP.md +++ b/docs/language/ROADMAP.md @@ -10,15 +10,16 @@ Long-horizon planning lives in release train from the historical `v2.0.0-beta.1` tag toward and beyond the first real general-purpose beta Slovo contract. -Current stage: `1.0.0-beta.5`, released on 2026-05-22 as the first post-beta -package/workspace discipline update. It keeps the `1.0.0-beta` language -contract and includes the `1.0.0-beta.1` tooling hardening release, the -`1.0.0-beta.2` runtime/resource foundation release, the `1.0.0-beta.3` -standard-library stabilization release, the `1.0.0-beta.4` -language-usability diagnostics release, `[workspace] default_package = "name"` -for deterministic build/run entry selection, tighter duplicate-member/default -package diagnostics, workspace package/dependency docs, and -`docs/language/PACKAGES.md` for beta local-package rules. +Current stage: `1.0.0-beta.6`, released on 2026-05-22 as the first post-beta +networking foundation update. It keeps the `1.0.0-beta` language contract and +includes the `1.0.0-beta.1` tooling hardening release, the `1.0.0-beta.2` +runtime/resource foundation release, the `1.0.0-beta.3` standard-library +stabilization release, the `1.0.0-beta.4` language-usability diagnostics +release, the `1.0.0-beta.5` package/workspace discipline release, and a narrow +`std.net` source facade for blocking loopback TCP client/server primitives over +opaque `i32` handles and concrete `result` families. DNS, TLS, UDP, async IO, +non-loopback binding, HTTP frameworks, rich host-error ADTs, stable socket +ABI/layout, and a stable standard-library API freeze remain deferred. The final experimental precursor scope is `exp-125`, defined in `.llm/EXP_125_UNSIGNED_U32_U64_NUMERIC_AND_STDLIB_BREADTH_ALPHA.md`. Its diff --git a/docs/language/SPEC-v1.md b/docs/language/SPEC-v1.md index a5d4836..e295925 100644 --- a/docs/language/SPEC-v1.md +++ b/docs/language/SPEC-v1.md @@ -1042,6 +1042,40 @@ solving, package publishing, archive formats, optional/dev/target dependencies, feature flags, package build scripts, or stable package ABI/layout promises. +### 4.4.4 Post-Beta Networking Foundation + +Status: released in `1.0.0-beta.6`. + +The `1.0.0-beta.6` networking foundation stages blocking loopback TCP only. It +does not change source syntax. + +The source facade is `lib/std/net.slo`, imported explicitly as `std.net`: + +```text +std.net.tcp_connect_loopback_result: (i32) -> (result i32 i32) +std.net.tcp_listen_loopback_result: (i32) -> (result i32 i32) +std.net.tcp_bound_port_result: (i32) -> (result i32 i32) +std.net.tcp_accept_result: (i32) -> (result i32 i32) +std.net.tcp_read_all_result: (i32) -> (result string i32) +std.net.tcp_write_text_result: (i32 string) -> (result i32 i32) +std.net.tcp_close_result: (i32) -> (result i32 i32) +``` + +Successful connect/listen/accept operations return `ok handle`, where +`handle` is a positive opaque process-local `i32`. Successful +`tcp_bound_port_result` returns the bound loopback TCP port. Successful +`tcp_write_text_result` and `tcp_close_result` return `ok 0`. Ordinary host +failures return `err 1`. + +The source facade also provides `tcp_write_text_ok` and `tcp_close_ok`, both +ordinary source helpers over the result-returning operations. + +This release is not a general networking contract. DNS, TLS, UDP, Unix-domain +sockets, non-loopback binding, async IO, event loops, readiness polling, +timeouts, buffering policy, HTTP frameworks, platform-specific error codes, +rich host-error ADTs, stable runtime helper symbols, stable socket ABI/layout, +automatic cleanup, and affine ownership guarantees remain deferred. + ## 4.5 v2.0.0-beta.1 Experimental Integration Readiness Status: current experimental Slovo-side release contract, released 2026-05-17. diff --git a/docs/language/STANDARD_RUNTIME.md b/docs/language/STANDARD_RUNTIME.md index 2ea8996..46a5aad 100644 --- a/docs/language/STANDARD_RUNTIME.md +++ b/docs/language/STANDARD_RUNTIME.md @@ -21,6 +21,12 @@ use the existing concrete `result` families where host failures need to be observable. Handles are positive opaque `i32` process-local tokens, not stable file descriptors or ABI values. +The `1.0.0-beta.6` networking foundation release adds a narrow loopback TCP +runtime family behind the `std.net` source facade. These names are blocking, +result-returning, and loopback-only; they do not define DNS, TLS, UDP, async +IO, non-loopback binding, HTTP frameworks, stable socket ABI/layout, or rich +platform error values. + The exp-era catalog is closed to names promoted through exp-101. exp-29, exp-30, exp-32, exp-33, and exp-35 through exp-93 add no new standard-runtime operation names. exp-32/exp-39/exp-56/exp-57 `std/math.slo` helpers, @@ -97,6 +103,13 @@ source-level result helper names are the `std.result.*` names cataloged below. | `std.fs.open_text_read_result` | `(string) -> (result i32 i32)` | `1.0.0-beta.2` | `examples/projects/std-layout-local-fs` | Opens a text file for read-only resource-handle flow; success returns `ok handle` where `handle` is a positive opaque process-local `i32`, and ordinary open or handle-table failure returns `err 1`. | Uses existing standard-runtime usage recording if present; no schema change. | Writable handles, binary IO, directory handles, process handles, sockets, async IO, platform-specific error codes, stable handle ABI/layout. | | `std.fs.read_open_text_result` | `(i32) -> (result string i32)` | `1.0.0-beta.2` | `examples/projects/std-layout-local-fs` | Reads remaining text from an open read handle; success returns `ok text`, while invalid, closed, or failed reads return `err 1`. | Uses existing standard-runtime usage recording if present; no schema change. | Chunked reads, seek, buffering controls, binary IO, async IO, platform-specific error codes, stable handle ABI/layout. | | `std.fs.close_result` | `(i32) -> (result i32 i32)` | `1.0.0-beta.2` | `examples/projects/std-layout-local-fs` | Closes an open resource handle; success returns `ok 0`, while invalid, closed, or failed close operations return `err 1`. | Uses existing standard-runtime usage recording if present; no schema change. | Finalizers, destructors, affine ownership, automatic cleanup, stable handle ABI/layout. | +| `std.net.tcp_connect_loopback_result` | `(i32) -> (result i32 i32)` | `1.0.0-beta.6` | `examples/projects/std-layout-local-net` | Connects to a loopback TCP port; success returns `ok handle` where `handle` is a positive opaque process-local `i32`, and ordinary host failure returns `err 1`. | Uses existing standard-runtime usage recording if present; no schema change. | DNS, TLS, UDP, non-loopback connect policy, async IO, timeouts, rich host errors, stable socket ABI/layout. | +| `std.net.tcp_listen_loopback_result` | `(i32) -> (result i32 i32)` | `1.0.0-beta.6` | `examples/projects/std-layout-local-net` | Opens a blocking loopback TCP listener; success returns `ok handle`, and ordinary bind/listen or handle-table failure returns `err 1`. | Uses existing standard-runtime usage recording if present; no schema change. | Non-loopback binding, socket options, backlog policy beyond the implementation minimum, async IO, rich host errors, stable socket ABI/layout. | +| `std.net.tcp_bound_port_result` | `(i32) -> (result i32 i32)` | `1.0.0-beta.6` | `examples/projects/std-layout-local-net` | Returns the bound loopback TCP port for a listener handle as `ok port`; invalid handles or host lookup failure return `err 1`. | Uses existing standard-runtime usage recording if present; no schema change. | Stable handle ABI/layout, address inspection, IPv6 policy, rich host errors. | +| `std.net.tcp_accept_result` | `(i32) -> (result i32 i32)` | `1.0.0-beta.6` | `examples/projects/std-layout-local-net` | Accepts one blocking connection from a listener handle; success returns `ok handle`, and invalid handles or host accept failure return `err 1`. | Uses existing standard-runtime usage recording if present; no schema change. | Async accept, cancellation, readiness polling, peer address APIs, rich host errors, stable socket ABI/layout. | +| `std.net.tcp_read_all_result` | `(i32) -> (result string i32)` | `1.0.0-beta.6` | `examples/projects/std-layout-local-net` | Reads remaining text from a stream handle until EOF; success returns `ok text`, and invalid handles or host read failure return `err 1`. | Uses existing standard-runtime usage recording if present; no schema change. | Chunked reads, binary IO, buffering policy, encodings, async IO, rich host errors, stable socket ABI/layout. | +| `std.net.tcp_write_text_result` | `(i32, string) -> (result i32 i32)` | `1.0.0-beta.6` | `examples/projects/std-layout-local-net` | Writes the complete text to a stream handle; success returns `ok 0`, and invalid handles or host write failure return `err 1`. | Uses existing standard-runtime usage recording if present; no schema change. | Partial-write APIs, binary IO, buffering policy, async IO, rich host errors, stable socket ABI/layout. | +| `std.net.tcp_close_result` | `(i32) -> (result i32 i32)` | `1.0.0-beta.6` | `examples/projects/std-layout-local-net` | Closes a TCP stream or listener handle; success returns `ok 0`, while invalid, already closed, or failed close operations return `err 1`. | Uses existing standard-runtime usage recording if present; no schema change. | Finalizers, destructors, affine ownership, automatic cleanup, stable socket ABI/layout. | | `std.random.i32` | `() -> i32` | exp-11 | `examples/supported/random.slo` | Returns a non-negative implementation-owned random `i32`; unavailable randomness traps as `slovo runtime error: random i32 unavailable`. | Uses existing standard-runtime usage recording if present; no randomness-specific schema field. | Seed APIs, crypto/security promises, ranges, bytes, floats, UUIDs, stable RNG ABI/layout. | | `std.io.read_stdin_result` | `() -> (result string i32)` | exp-12 | `examples/supported/stdin-result.slo` | Reads remaining stdin as text; success returns `ok` text, ordinary EOF with no bytes returns `ok ""`, ordinary host/input failure returns `err 1`. | Uses existing standard-runtime usage recording if present; no stdin-specific schema field. | Trap stdin, line APIs, prompts, terminal mode, binary/streaming/async stdin. | | `std.string.parse_i32_result` | `(string) -> (result i32 i32)` | exp-13 | `examples/supported/string-parse-i32-result.slo` | Parses an entire ASCII decimal signed `i32`; success returns `ok` value, ordinary parse failure returns `err 1`. | Uses existing standard-runtime usage recording if present; no parse-specific schema field. | Trap parse, floats/bools/bytes, trimming, locale/radix/underscore/plus parsing, rich parse errors, Unicode digits, slicing/indexing. | diff --git a/docs/language/STDLIB_API.md b/docs/language/STDLIB_API.md index 07228d5..ca34d98 100644 --- a/docs/language/STDLIB_API.md +++ b/docs/language/STDLIB_API.md @@ -6,15 +6,15 @@ Do not edit this file by hand. ## Stability Tiers - `beta-supported`: exported from `lib/std` and covered by source-search, promotion, or facade gates in the current beta line. -- `experimental`: not used for exported `lib/std` helpers in `1.0.0-beta.5`; future releases may mark new helpers this way before they graduate. +- `experimental`: not used for exported `lib/std` helpers in `1.0.0-beta.6`; future releases may mark new helpers this way before they graduate. - `internal`: helper names that are not exported from their module; they are intentionally omitted from this catalog. The catalog is a beta compatibility aid, not a stable `1.0.0` API freeze. ## Summary -- Modules: 17 -- Exported helpers: 539 +- Modules: 18 +- Exported helpers: 548 - Default tier: `beta-supported` ## Modules @@ -238,6 +238,22 @@ The catalog is a beta compatibility aid, not a stable `1.0.0` API freeze. - `is_negative_f64` - `in_range_f64` +### std.net + +- Path: `lib/std/net.slo` +- Tier: `beta-supported` +- Exported helpers: 9 + +- `tcp_connect_loopback_result` +- `tcp_listen_loopback_result` +- `tcp_bound_port_result` +- `tcp_accept_result` +- `tcp_read_all_result` +- `tcp_write_text_result` +- `tcp_close_result` +- `tcp_write_text_ok` +- `tcp_close_ok` + ### std.num - Path: `lib/std/num.slo` diff --git a/docs/papers/GLAGOL_COMPILER_MANIFEST.pdf b/docs/papers/GLAGOL_COMPILER_MANIFEST.pdf index 191bb52..2ac4220 100644 Binary files a/docs/papers/GLAGOL_COMPILER_MANIFEST.pdf and b/docs/papers/GLAGOL_COMPILER_MANIFEST.pdf differ diff --git a/docs/papers/GLAGOL_WHITEPAPER.md b/docs/papers/GLAGOL_WHITEPAPER.md index dcc16a7..ecc0459 100644 --- a/docs/papers/GLAGOL_WHITEPAPER.md +++ b/docs/papers/GLAGOL_WHITEPAPER.md @@ -5,18 +5,19 @@ Sanjin Gumbarevic
hermeticum_lab@protonmail.com -Publication release: `1.0.0-beta.5` +Publication release: `1.0.0-beta.6` Technical behavior baseline: compiler and language support through `1.0.0-beta`; tooling and install workflow through `1.0.0-beta.1`; runtime/resource foundation through `1.0.0-beta.2`; standard-library stabilization through `1.0.0-beta.3`; language-usability diagnostics through -`1.0.0-beta.4`; package/workspace discipline through `1.0.0-beta.5` +`1.0.0-beta.4`; package/workspace discipline through `1.0.0-beta.5`; +loopback networking foundation through `1.0.0-beta.6` Date: 2026-05-22 Evidence source: paired local Slovo/Glagol monorepo verification and benchmark -reruns from a local checkout; beta.5 release-gate verification from the public +reruns from a local checkout; beta.6 release-gate verification from the public monorepo Maturity: beta @@ -28,19 +29,21 @@ Slovo. It exists to make the language support boundary inspectable: tokens, S-expression tree, AST, typed AST, LLVM IR, hosted native executable, tests, diagnostics, and release documents should agree. -The current publication release, `1.0.0-beta.5`, keeps the first real +The current publication release, `1.0.0-beta.6`, keeps the first real general-purpose beta toolchain baseline from `1.0.0-beta` and records the first post-beta tooling/install hardening update plus the first runtime/resource foundation update plus the first standard-library stabilization update plus the first language-usability diagnostics update and -the first local package/workspace discipline update. The beta baseline includes -the completed `u32` / `u64` unsigned compiler and stdlib breadth scope -alongside the current nine-kernel benchmark suite. This paper records the -current beta implementation surface, the benchmark method and results, the -distinction between Glagol and Lisp-family implementations, the beta.1 tooling -update, the beta.2 runtime/resource foundation, the beta.3 standard-library -stabilization slice, the beta.4 diagnostics usability slice, the beta.5 package -discipline slice, and the compiler path from beta to stable. +the first local package/workspace discipline update plus the first loopback +networking foundation update. The beta baseline includes the completed `u32` / +`u64` unsigned compiler and stdlib breadth scope, the narrow `std.net` +loopback TCP runtime family, and the current nine-kernel benchmark suite. This +paper records the current beta implementation surface, the benchmark method and +results, the distinction between Glagol and Lisp-family implementations, the +beta.1 tooling update, the beta.2 runtime/resource foundation, the beta.3 +standard-library stabilization slice, the beta.4 diagnostics usability slice, +the beta.5 package discipline slice, the beta.6 networking foundation slice, +and the compiler path from beta to stable. ## 1. Compiler Thesis @@ -124,9 +127,9 @@ At the current technical behavior beta baseline, Glagol supports: - benchmark scaffolds for Slovo, C, Rust, Python, Clojure, and Common Lisp/SBCL, with `cold-process` and `hot-loop` timing modes -The current release, `1.0.0-beta.5`, is a beta package/workspace discipline -update on the first release line that may honestly use beta maturity language -for this toolchain. +The current release, `1.0.0-beta.6`, is a beta networking foundation update on +the first release line that may honestly use beta maturity language for this +toolchain. ## 4. Diagnostics And Support Discipline @@ -285,8 +288,9 @@ The benchmark rows below remain the full-suite `1.0.0-beta` publication baseline. `1.0.0-beta.1` changes tooling and install workflow, and `1.0.0-beta.2` adds runtime/resource APIs, `1.0.0-beta.3` adds standard-library catalog and composition coverage, `1.0.0-beta.4` improves -diagnostics, and `1.0.0-beta.5` tightens package/workspace discipline. None of -these post-beta slices claims changed benchmark performance. +diagnostics, `1.0.0-beta.5` tightens package/workspace discipline, and +`1.0.0-beta.6` adds a narrow loopback networking foundation. None of these +post-beta slices claims changed benchmark performance. The exp-123 publication baseline widened the paired same-machine result set from seven rows to nine by adding two owned-vector kernels: @@ -374,13 +378,14 @@ coverage and compatibility: - package behavior becoming stable before dependency, manifest, and versioning rules are precise -## 9. Path Beyond `1.0.0-beta.5` +## 9. Path Beyond `1.0.0-beta.6` Glagol now implements the first real beta Slovo contract, the first post-beta tooling/install hardening release, the first runtime/resource foundation release, the first standard-library stabilization release, and the first diagnostics usability release, and the first package/workspace -discipline release. The remaining path is from beta to stable. +discipline release, and the first loopback networking foundation release. The +remaining path is from beta to stable. Recommended compiler sequence: diff --git a/docs/papers/GLAGOL_WHITEPAPER.pdf b/docs/papers/GLAGOL_WHITEPAPER.pdf index 520d41e..f814dd3 100644 Binary files a/docs/papers/GLAGOL_WHITEPAPER.pdf and b/docs/papers/GLAGOL_WHITEPAPER.pdf differ diff --git a/docs/papers/SLOVO_MANIFEST.pdf b/docs/papers/SLOVO_MANIFEST.pdf index 0b3da9e..d8e3c9e 100644 Binary files a/docs/papers/SLOVO_MANIFEST.pdf and b/docs/papers/SLOVO_MANIFEST.pdf differ diff --git a/docs/papers/SLOVO_WHITEPAPER.md b/docs/papers/SLOVO_WHITEPAPER.md index bd1cd49..40b583e 100644 --- a/docs/papers/SLOVO_WHITEPAPER.md +++ b/docs/papers/SLOVO_WHITEPAPER.md @@ -5,18 +5,19 @@ Sanjin Gumbarevic
hermeticum_lab@protonmail.com -Publication release: `1.0.0-beta.5` +Publication release: `1.0.0-beta.6` Technical behavior baseline: language surface through `1.0.0-beta`; tooling and install workflow through `1.0.0-beta.1`; runtime/resource foundation through `1.0.0-beta.2`; standard-library stabilization through `1.0.0-beta.3`; language-usability diagnostics through `1.0.0-beta.4`; package/workspace -discipline through `1.0.0-beta.5` +discipline through `1.0.0-beta.5`; loopback networking foundation through +`1.0.0-beta.6` Date: 2026-05-22 Evidence source: paired local Slovo/Glagol monorepo verification and benchmark -reruns from a local checkout; beta.5 release-gate verification from the public +reruns from a local checkout; beta.6 release-gate verification from the public monorepo Maturity: beta @@ -31,26 +32,28 @@ explicit types, explicit failure through `option` and `result`, lexical `unsafe`, and native compilation through the Glagol compiler to LLVM IR and hosted executables. -The current publication release, `1.0.0-beta.5`, keeps the first real +The current publication release, `1.0.0-beta.6`, keeps the first real general-purpose beta language baseline from `1.0.0-beta` and records the first post-beta tooling/install hardening update plus the first runtime/resource foundation update, the first standard-library stabilization update, and the first language-usability diagnostics update, plus the first local -package/workspace discipline update. The beta baseline includes the completed -`u32` / `u64` unsigned scope, the staged stdlib breadth that makes ordinary -command-line programs practical, and the current nine-kernel benchmark suite. -This paper records the current beta technical state, the difference between -Slovo and Lisp-family languages, the benchmark methodology, the beta.1 tooling -update, the beta.2 runtime/resource foundation, the beta.3 standard-library -stabilization slice, the beta.4 diagnostics usability slice, the beta.5 package -discipline slice, and the remaining path from beta to stable. +package/workspace discipline update, and the first loopback networking +foundation update. The beta baseline includes the completed `u32` / `u64` +unsigned scope, the staged stdlib breadth that makes ordinary command-line +programs practical, the current narrow `std.net` loopback TCP surface, and the +current nine-kernel benchmark suite. This paper records the current beta +technical state, the difference between Slovo and Lisp-family languages, the +benchmark methodology, the beta.1 tooling update, the beta.2 runtime/resource +foundation, the beta.3 standard-library stabilization slice, the beta.4 +diagnostics usability slice, the beta.5 package discipline slice, the beta.6 +networking foundation slice, and the remaining path from beta to stable. ## 1. Scope This document is a technical state paper for the current beta baseline. It summarizes the behavior represented by the paired local Slovo and Glagol workspaces, with `1.0.0-beta` as the current language-surface baseline and -`1.0.0-beta.5` as the current publication baseline. +`1.0.0-beta.6` as the current publication baseline. The support rule remains strict: @@ -62,7 +65,7 @@ The support rule remains strict: - partial parser recognition or speculative examples do not count as support Historical `exp-*` releases remain experimental alpha maturity. The current -publication accompanies `1.0.0-beta.5`. +publication accompanies `1.0.0-beta.6`. ## 2. Design Thesis @@ -362,8 +365,9 @@ The benchmark rows below remain the full-suite `1.0.0-beta` publication baseline. `1.0.0-beta.1` changes tooling and install workflow, and `1.0.0-beta.2` adds runtime/resource APIs, `1.0.0-beta.3` adds standard-library catalog and composition coverage, `1.0.0-beta.4` improves -diagnostics, and `1.0.0-beta.5` tightens package/workspace discipline. None of -these post-beta slices claims changed benchmark performance. +diagnostics, `1.0.0-beta.5` tightens package/workspace discipline, and +`1.0.0-beta.6` adds a narrow loopback networking foundation. None of these +post-beta slices claims changed benchmark performance. The exp-123 publication baseline widened the paired same-machine result set from seven rows to nine by adding two owned-vector kernels: @@ -484,7 +488,7 @@ Major remaining gaps before `1.0.0`: - semantic versioning and deprecation policy - a clear separation between stable and experimental features -## 10. Path Beyond `1.0.0-beta.5` +## 10. Path Beyond `1.0.0-beta.6` The beta threshold is now real. The next work should treat `1.0.0-beta` as the language compatibility-governed baseline, `1.0.0-beta.1` as the first @@ -492,6 +496,7 @@ tooling/install hardening point, `1.0.0-beta.2` as the first runtime/resource foundation point, and `1.0.0-beta.3` as the first standard-library stabilization point, and `1.0.0-beta.4` as the first diagnostics usability point, and `1.0.0-beta.5` as the first package/workspace discipline point, +and `1.0.0-beta.6` as the first loopback networking foundation point, then move deliberately toward stable general-purpose status. Recommended sequence: diff --git a/docs/papers/SLOVO_WHITEPAPER.pdf b/docs/papers/SLOVO_WHITEPAPER.pdf index 5df59f2..124adae 100644 Binary files a/docs/papers/SLOVO_WHITEPAPER.pdf and b/docs/papers/SLOVO_WHITEPAPER.pdf differ diff --git a/examples/projects/std-import-net/slovo.toml b/examples/projects/std-import-net/slovo.toml new file mode 100644 index 0000000..41c55ef --- /dev/null +++ b/examples/projects/std-import-net/slovo.toml @@ -0,0 +1,4 @@ +[project] +name = "std-import-net" +source_root = "src" +entry = "main" diff --git a/examples/projects/std-import-net/src/main.slo b/examples/projects/std-import-net/src/main.slo new file mode 100644 index 0000000..490aeb3 --- /dev/null +++ b/examples/projects/std-import-net/src/main.slo @@ -0,0 +1,94 @@ +(module main) + +(import std.net (tcp_connect_loopback_result tcp_listen_loopback_result tcp_bound_port_result tcp_accept_result tcp_read_all_result tcp_write_text_result tcp_close_result tcp_write_text_ok tcp_close_ok)) + +(fn i32_err_one ((value (result i32 i32))) -> bool + (match value + ((ok payload) + false) + ((err code) + (= code 1)))) + +(fn string_err_one ((value (result string i32))) -> bool + (match value + ((ok payload) + false) + ((err code) + (= code 1)))) + +(fn imported_connect_invalid_port_err () -> bool + (i32_err_one (tcp_connect_loopback_result 0))) + +(fn imported_listen_invalid_port_err () -> bool + (i32_err_one (tcp_listen_loopback_result -1))) + +(fn imported_bound_port_invalid_handle_err () -> bool + (i32_err_one (tcp_bound_port_result -1))) + +(fn imported_accept_invalid_listener_err () -> bool + (i32_err_one (tcp_accept_result -1))) + +(fn imported_read_invalid_handle_err () -> bool + (string_err_one (tcp_read_all_result -1))) + +(fn imported_write_invalid_handle_err () -> bool + (i32_err_one (tcp_write_text_result -1 "ping"))) + +(fn imported_close_invalid_handle_err () -> bool + (i32_err_one (tcp_close_result -1))) + +(fn imported_helper_invalids_false () -> bool + (if (tcp_write_text_ok -1 "ping") + false + (if (tcp_close_ok -1) + false + true))) + +(fn imported_invalid_paths_ok () -> bool + (if (imported_connect_invalid_port_err) + (if (imported_listen_invalid_port_err) + (if (imported_bound_port_invalid_handle_err) + (if (imported_accept_invalid_listener_err) + (if (imported_read_invalid_handle_err) + (if (imported_write_invalid_handle_err) + (if (imported_close_invalid_handle_err) + (imported_helper_invalids_false) + false) + false) + false) + false) + false) + false) + false)) + +(fn main () -> i32 + (if (imported_invalid_paths_ok) + 42 + 1)) + +(test "explicit std net invalid connect facade" + (imported_connect_invalid_port_err)) + +(test "explicit std net invalid listen facade" + (imported_listen_invalid_port_err)) + +(test "explicit std net invalid bound port facade" + (imported_bound_port_invalid_handle_err)) + +(test "explicit std net invalid accept facade" + (imported_accept_invalid_listener_err)) + +(test "explicit std net invalid read facade" + (imported_read_invalid_handle_err)) + +(test "explicit std net invalid write facade" + (imported_write_invalid_handle_err)) + +(test "explicit std net invalid close facade" + (imported_close_invalid_handle_err)) + +(test "explicit std net helper invalids facade" + (imported_helper_invalids_false)) + +(test "explicit std net facade all" + (= (main) 42)) diff --git a/examples/projects/std-layout-local-net/slovo.toml b/examples/projects/std-layout-local-net/slovo.toml new file mode 100644 index 0000000..25c58ea --- /dev/null +++ b/examples/projects/std-layout-local-net/slovo.toml @@ -0,0 +1,4 @@ +[project] +name = "std-layout-local-net" +source_root = "src" +entry = "main" diff --git a/examples/projects/std-layout-local-net/src/main.slo b/examples/projects/std-layout-local-net/src/main.slo new file mode 100644 index 0000000..53ddd5d --- /dev/null +++ b/examples/projects/std-layout-local-net/src/main.slo @@ -0,0 +1,94 @@ +(module main) + +(import net (tcp_connect_loopback_result tcp_listen_loopback_result tcp_bound_port_result tcp_accept_result tcp_read_all_result tcp_write_text_result tcp_close_result tcp_write_text_ok tcp_close_ok)) + +(fn i32_err_one ((value (result i32 i32))) -> bool + (match value + ((ok payload) + false) + ((err code) + (= code 1)))) + +(fn string_err_one ((value (result string i32))) -> bool + (match value + ((ok payload) + false) + ((err code) + (= code 1)))) + +(fn imported_connect_invalid_port_err () -> bool + (i32_err_one (tcp_connect_loopback_result 0))) + +(fn imported_listen_invalid_port_err () -> bool + (i32_err_one (tcp_listen_loopback_result -1))) + +(fn imported_bound_port_invalid_handle_err () -> bool + (i32_err_one (tcp_bound_port_result -1))) + +(fn imported_accept_invalid_listener_err () -> bool + (i32_err_one (tcp_accept_result -1))) + +(fn imported_read_invalid_handle_err () -> bool + (string_err_one (tcp_read_all_result -1))) + +(fn imported_write_invalid_handle_err () -> bool + (i32_err_one (tcp_write_text_result -1 "ping"))) + +(fn imported_close_invalid_handle_err () -> bool + (i32_err_one (tcp_close_result -1))) + +(fn imported_helper_invalids_false () -> bool + (if (tcp_write_text_ok -1 "ping") + false + (if (tcp_close_ok -1) + false + true))) + +(fn imported_invalid_paths_ok () -> bool + (if (imported_connect_invalid_port_err) + (if (imported_listen_invalid_port_err) + (if (imported_bound_port_invalid_handle_err) + (if (imported_accept_invalid_listener_err) + (if (imported_read_invalid_handle_err) + (if (imported_write_invalid_handle_err) + (if (imported_close_invalid_handle_err) + (imported_helper_invalids_false) + false) + false) + false) + false) + false) + false) + false)) + +(fn main () -> i32 + (if (imported_invalid_paths_ok) + 42 + 1)) + +(test "explicit local net invalid connect facade" + (imported_connect_invalid_port_err)) + +(test "explicit local net invalid listen facade" + (imported_listen_invalid_port_err)) + +(test "explicit local net invalid bound port facade" + (imported_bound_port_invalid_handle_err)) + +(test "explicit local net invalid accept facade" + (imported_accept_invalid_listener_err)) + +(test "explicit local net invalid read facade" + (imported_read_invalid_handle_err)) + +(test "explicit local net invalid write facade" + (imported_write_invalid_handle_err)) + +(test "explicit local net invalid close facade" + (imported_close_invalid_handle_err)) + +(test "explicit local net helper invalids facade" + (imported_helper_invalids_false)) + +(test "explicit local net facade all" + (= (main) 42)) diff --git a/examples/projects/std-layout-local-net/src/net.slo b/examples/projects/std-layout-local-net/src/net.slo new file mode 100644 index 0000000..5053062 --- /dev/null +++ b/examples/projects/std-layout-local-net/src/net.slo @@ -0,0 +1,36 @@ +(module net (export tcp_connect_loopback_result tcp_listen_loopback_result tcp_bound_port_result tcp_accept_result tcp_read_all_result tcp_write_text_result tcp_close_result tcp_write_text_ok tcp_close_ok)) + +(fn tcp_connect_loopback_result ((port i32)) -> (result i32 i32) + (std.net.tcp_connect_loopback_result port)) + +(fn tcp_listen_loopback_result ((port i32)) -> (result i32 i32) + (std.net.tcp_listen_loopback_result port)) + +(fn tcp_bound_port_result ((handle i32)) -> (result i32 i32) + (std.net.tcp_bound_port_result handle)) + +(fn tcp_accept_result ((listener i32)) -> (result i32 i32) + (std.net.tcp_accept_result listener)) + +(fn tcp_read_all_result ((handle i32)) -> (result string i32) + (std.net.tcp_read_all_result handle)) + +(fn tcp_write_text_result ((handle i32) (text string)) -> (result i32 i32) + (std.net.tcp_write_text_result handle text)) + +(fn tcp_close_result ((handle i32)) -> (result i32 i32) + (std.net.tcp_close_result handle)) + +(fn tcp_write_text_ok ((handle i32) (text string)) -> bool + (match (tcp_write_text_result handle text) + ((ok status) + true) + ((err code) + false))) + +(fn tcp_close_ok ((handle i32)) -> bool + (match (tcp_close_result handle) + ((ok status) + true) + ((err code) + false))) diff --git a/lib/std/README.md b/lib/std/README.md index b4a90ee..d081c85 100644 --- a/lib/std/README.md +++ b/lib/std/README.md @@ -31,7 +31,9 @@ updated through `exp-66`; fs option helpers updated through `exp-110`; num fallback helpers updated through `exp-64`; concrete vec helpers updated through released `exp-108`, including the current concrete `std/vec_string.slo`, `std/vec_f64.slo`, and `std/vec_bool.slo` -prefix/suffix helper scopes. +prefix/suffix helper scopes; `1.0.0-beta.6` networking foundation work releases +`std/net.slo` as an experimental loopback TCP facade over matching +compiler-known runtime calls. This directory is the source home for staged standard library modules and examples. exp-44 lets project-mode source explicitly import `std/math.slo` as @@ -83,6 +85,13 @@ filesystem status/mutation helpers over `std.fs.exists`, `std.fs.is_file`, `std.fs.is_dir`, `std.fs.remove_file_result`, and `std.fs.create_dir_result`, while keeping handles beta-scoped opaque `i32` values and leaving directory enumeration deferred; +`1.0.0-beta.6` releases `std/net.slo` as an experimental blocking +loopback TCP facade over `std.net.tcp_connect_loopback_result`, +`std.net.tcp_listen_loopback_result`, `std.net.tcp_bound_port_result`, +`std.net.tcp_accept_result`, `std.net.tcp_read_all_result`, +`std.net.tcp_write_text_result`, and `std.net.tcp_close_result`, while keeping +socket handles beta-scoped opaque `i32` values and leaving DNS, TLS, async, +UDP, non-loopback binding, and stable ABI/layout deferred; exp-76 extends project-mode source search to `std/vec_i32.slo`, a concrete source-authored collection facade over the current promoted `std.vec.i32` runtime family; exp-77 extends that facade with concrete option-returning @@ -127,12 +136,13 @@ For exp-44, exp-45, exp-47, exp-48, exp-49, exp-52, exp-53, exp-76, exp-94, exp-96, exp-97, exp-98, exp-99, exp-103, exp-104, exp-105, exp-107, and exp-108, `std/math.slo`, `std/result.slo`, `std/option.slo`, `std/time.slo`, `std/random.slo`, `std/env.slo`, -`std/fs.slo`, `std/string.slo`, `std/num.slo`, `std/io.slo`, -`std/process.slo`, `std/cli.slo`, `std/vec_i32.slo`, `std/vec_f64.slo`, -`std/vec_i64.slo`, and `std/vec_string.slo` carry explicit export lists. +`std/fs.slo`, `std/net.slo`, `std/string.slo`, `std/num.slo`, +`std/io.slo`, `std/process.slo`, `std/cli.slo`, `std/vec_i32.slo`, +`std/vec_f64.slo`, `std/vec_i64.slo`, and `std/vec_string.slo` carry explicit +export lists. Glagol may address them externally as `std.math`, `std.result`, `std.option`, `std.time`, `std.random`, `std.env`, `std.fs`, `std.string`, -`std.num`, `std.io`, `std.process`, `std.cli`, `std.vec_i32`, +`std.net`, `std.num`, `std.io`, `std.process`, `std.cli`, `std.vec_i32`, `std.vec_f64`, `std.vec_bool`, `std.vec_i64`, and `std.vec_string`. The file layout is the contract: @@ -153,6 +163,7 @@ The file layout is the contract: - `std/random.slo` - `std/env.slo` - `std/fs.slo` +- `std/net.slo` This follows a Zig-like standard-library facade discipline in a Slovo-sized form: flat `std/*.slo` facade files are the staged source surface now, and a @@ -261,6 +272,12 @@ over `read_text_result` plus those same concrete parse result families from `read_i64_option`, `read_f64_option`, and `read_bool_option` as ordinary source option helpers over those same read/parse result families through the exp-109 `std.result.ok_or_none_*` bridge helpers. +`std/net.slo` is the beta.6 networking foundation source facade. It wraps +blocking loopback TCP connect/listen/bound-port/accept/read-all/write-text/ +close result calls and adds only `tcp_write_text_ok` and `tcp_close_ok` source +helpers. It is not a general networking module: DNS, TLS, async IO, UDP, +non-loopback binding, socket options, rich host errors, and stable handle ABI +remain deferred. `std/process.slo` includes the exp-52 narrow source wrappers over already released process argument runtime calls and a source-authored `has_arg` predicate. exp-61 adds `arg_or` and `arg_or_empty` as ordinary source diff --git a/lib/std/net.slo b/lib/std/net.slo new file mode 100644 index 0000000..5053062 --- /dev/null +++ b/lib/std/net.slo @@ -0,0 +1,36 @@ +(module net (export tcp_connect_loopback_result tcp_listen_loopback_result tcp_bound_port_result tcp_accept_result tcp_read_all_result tcp_write_text_result tcp_close_result tcp_write_text_ok tcp_close_ok)) + +(fn tcp_connect_loopback_result ((port i32)) -> (result i32 i32) + (std.net.tcp_connect_loopback_result port)) + +(fn tcp_listen_loopback_result ((port i32)) -> (result i32 i32) + (std.net.tcp_listen_loopback_result port)) + +(fn tcp_bound_port_result ((handle i32)) -> (result i32 i32) + (std.net.tcp_bound_port_result handle)) + +(fn tcp_accept_result ((listener i32)) -> (result i32 i32) + (std.net.tcp_accept_result listener)) + +(fn tcp_read_all_result ((handle i32)) -> (result string i32) + (std.net.tcp_read_all_result handle)) + +(fn tcp_write_text_result ((handle i32) (text string)) -> (result i32 i32) + (std.net.tcp_write_text_result handle text)) + +(fn tcp_close_result ((handle i32)) -> (result i32 i32) + (std.net.tcp_close_result handle)) + +(fn tcp_write_text_ok ((handle i32) (text string)) -> bool + (match (tcp_write_text_result handle text) + ((ok status) + true) + ((err code) + false))) + +(fn tcp_close_ok ((handle i32)) -> bool + (match (tcp_close_result handle) + ((ok status) + true) + ((err code) + false))) diff --git a/runtime/runtime.c b/runtime/runtime.c index de18b71..29c8caf 100644 --- a/runtime/runtime.c +++ b/runtime/runtime.c @@ -1,15 +1,18 @@ #define _POSIX_C_SOURCE 200809L +#include #include #include #include #include #include +#include #include #include #include #include #include +#include #include #include #include @@ -48,6 +51,9 @@ static struct timespec __glagol_time_base; #define __GLAGOL_FILE_HANDLE_LIMIT 1024 static FILE *__glagol_file_handles[__GLAGOL_FILE_HANDLE_LIMIT]; +#define __GLAGOL_NET_HANDLE_LIMIT 1024 +static int __glagol_net_handles[__GLAGOL_NET_HANDLE_LIMIT]; + static int64_t __glagol_result_i32_encode(uint32_t status, int32_t payload) { uint64_t encoded = ((uint64_t)status << 32) | (uint32_t)payload; return (int64_t)encoded; @@ -57,6 +63,54 @@ static int64_t __glagol_result_i32_err(void) { return __glagol_result_i32_encode(1, 1); } +static int32_t __glagol_net_handle_insert(int fd) { + if (fd < 0 || fd == INT_MAX) { + return 0; + } + + for (int32_t handle = 1; handle < __GLAGOL_NET_HANDLE_LIMIT; handle++) { + if (__glagol_net_handles[handle] == 0) { + __glagol_net_handles[handle] = fd + 1; + return handle; + } + } + return 0; +} + +static int __glagol_net_handle_get(int32_t handle) { + if (handle <= 0 || handle >= __GLAGOL_NET_HANDLE_LIMIT) { + return -1; + } + + int stored = __glagol_net_handles[handle]; + return stored == 0 ? -1 : stored - 1; +} + +static int __glagol_net_handle_take(int32_t handle) { + int fd = __glagol_net_handle_get(handle); + if (fd >= 0) { + __glagol_net_handles[handle] = 0; + } + return fd; +} + +static bool __glagol_net_connect_port_is_valid(int32_t port) { + return port > 0 && port <= 65535; +} + +static bool __glagol_net_bind_port_is_valid(int32_t port) { + return port >= 0 && port <= 65535; +} + +static struct sockaddr_in __glagol_net_loopback_addr(int32_t port) { + struct sockaddr_in addr; + memset(&addr, 0, sizeof(addr)); + addr.sin_family = AF_INET; + addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); + addr.sin_port = htons((uint16_t)port); + return addr; +} + void print_i32(int32_t value) { printf("%d\n", value); } @@ -433,6 +487,222 @@ int32_t __glagol_fs_close_result(int32_t handle) { return fclose(file) == 0 ? 0 : 1; } +int64_t __glagol_net_tcp_connect_loopback_result(int32_t port) { + if (!__glagol_net_connect_port_is_valid(port)) { + return __glagol_result_i32_err(); + } + + int fd = socket(AF_INET, SOCK_STREAM, 0); + if (fd < 0) { + return __glagol_result_i32_err(); + } + + struct sockaddr_in addr = __glagol_net_loopback_addr(port); + if (connect(fd, (struct sockaddr *)&addr, sizeof(addr)) != 0) { + close(fd); + return __glagol_result_i32_err(); + } + + int32_t handle = __glagol_net_handle_insert(fd); + if (handle == 0) { + close(fd); + return __glagol_result_i32_err(); + } + + return __glagol_result_i32_encode(0, handle); +} + +int64_t __glagol_net_tcp_listen_loopback_result(int32_t port) { + if (!__glagol_net_bind_port_is_valid(port)) { + return __glagol_result_i32_err(); + } + + int fd = socket(AF_INET, SOCK_STREAM, 0); + if (fd < 0) { + return __glagol_result_i32_err(); + } + + int enabled = 1; + if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &enabled, sizeof(enabled)) != 0) { + close(fd); + return __glagol_result_i32_err(); + } + + struct sockaddr_in addr = __glagol_net_loopback_addr(port); + if (bind(fd, (struct sockaddr *)&addr, sizeof(addr)) != 0) { + close(fd); + return __glagol_result_i32_err(); + } + + if (listen(fd, 1) != 0) { + close(fd); + return __glagol_result_i32_err(); + } + + int32_t handle = __glagol_net_handle_insert(fd); + if (handle == 0) { + close(fd); + return __glagol_result_i32_err(); + } + + return __glagol_result_i32_encode(0, handle); +} + +int64_t __glagol_net_tcp_bound_port_result(int32_t handle) { + int fd = __glagol_net_handle_get(handle); + if (fd < 0) { + return __glagol_result_i32_err(); + } + + struct sockaddr_in addr; + socklen_t addr_len = sizeof(addr); + if (getsockname(fd, (struct sockaddr *)&addr, &addr_len) != 0) { + return __glagol_result_i32_err(); + } + + return __glagol_result_i32_encode(0, (int32_t)ntohs(addr.sin_port)); +} + +int64_t __glagol_net_tcp_accept_result(int32_t listener) { + int fd = __glagol_net_handle_get(listener); + if (fd < 0) { + return __glagol_result_i32_err(); + } + + int accepted; + do { + accepted = accept(fd, NULL, NULL); + } while (accepted < 0 && errno == EINTR); + + if (accepted < 0) { + return __glagol_result_i32_err(); + } + + int32_t handle = __glagol_net_handle_insert(accepted); + if (handle == 0) { + close(accepted); + return __glagol_result_i32_err(); + } + + return __glagol_result_i32_encode(0, handle); +} + +char *__glagol_net_tcp_read_all_result(int32_t handle) { + int fd = __glagol_net_handle_get(handle); + if (fd < 0) { + return NULL; + } + + size_t capacity = 1024; + size_t length = 0; + char *value = malloc(capacity); + if (value == NULL) { + return NULL; + } + + for (;;) { + if (length == capacity) { + if (capacity > SIZE_MAX / 2) { + free(value); + return NULL; + } + + size_t next_capacity = capacity * 2; + char *next = realloc(value, next_capacity); + if (next == NULL) { + free(value); + return NULL; + } + + value = next; + capacity = next_capacity; + } + + size_t available = capacity - length; + if (available > (size_t)SSIZE_MAX) { + available = (size_t)SSIZE_MAX; + } + + ssize_t read_count = recv(fd, value + length, available, 0); + if (read_count < 0) { + if (errno == EINTR) { + continue; + } + + free(value); + return NULL; + } + + if (read_count == 0) { + break; + } + + length += (size_t)read_count; + } + + if (length == capacity) { + if (capacity == SIZE_MAX) { + free(value); + return NULL; + } + + char *next = realloc(value, capacity + 1); + if (next == NULL) { + free(value); + return NULL; + } + value = next; + } + + value[length] = '\0'; + return value; +} + +int32_t __glagol_net_tcp_write_text_result(int32_t handle, const char *text) { + int fd = __glagol_net_handle_get(handle); + if (fd < 0) { + return 1; + } + + size_t length = strlen(text); + size_t written = 0; + while (written < length) { + size_t remaining = length - written; + if (remaining > (size_t)SSIZE_MAX) { + remaining = (size_t)SSIZE_MAX; + } + +#ifdef MSG_NOSIGNAL + ssize_t sent = send(fd, text + written, remaining, MSG_NOSIGNAL); +#else + ssize_t sent = send(fd, text + written, remaining, 0); +#endif + if (sent < 0) { + if (errno == EINTR) { + continue; + } + return 1; + } + + if (sent == 0) { + return 1; + } + + written += (size_t)sent; + } + + return 0; +} + +int32_t __glagol_net_tcp_close_result(int32_t handle) { + int fd = __glagol_net_handle_take(handle); + if (fd < 0) { + return 1; + } + + return close(fd) == 0 ? 0 : 1; +} + static void __glagol_allocation_trap(void) { fputs("slovo runtime error: string allocation failed\n", stderr); exit(1);