diff --git a/.github/scripts/contrib_check.sh b/.github/scripts/contrib_check.sh index 1a9c54642..641c97d3c 100755 --- a/.github/scripts/contrib_check.sh +++ b/.github/scripts/contrib_check.sh @@ -126,10 +126,10 @@ TESTS=( "templating/liquid/filters|$LQ/test_filters.ae||run|" "templating/liquid/inheritance|$LQ/test_inheritance.ae||run|" "tinyweb/spec|$TW/test_spec.ae||run|" - "tinyweb/inventory|$TW/test_inventory.ae|$TW/ws_handshake.c|run|" - "tinyweb/integration|$TW/test_integration.ae|$TW/ws_handshake.c|run|" - "tinyweb/schema_api|$TW/test_schema_api.ae|$TW/ws_handshake.c|run|" - "tinyweb/websocket|$TW/test_websocket.ae|$TW/ws_handshake.c|run|" + "tinyweb/inventory|$TW/test_inventory.ae||run|" + "tinyweb/integration|$TW/test_integration.ae||run|" + "tinyweb/schema_api|$TW/test_schema_api.ae||run|" + "tinyweb/streams|$TW/test_streams.ae||run|" "i18n/collate|$I18N/collate/test_collate.ae|$I18N/aether_i18n.c std/unicode/utf8proc/utf8proc.c $I18N/ducet/ducet_data.c|leak|" # png: pure Aether over std.zlib; its test decodes what it encoded. "png/encode|contrib/png/test_png.ae||leak|" diff --git a/Makefile b/Makefile index 748ac4d14..88177b06d 100644 --- a/Makefile +++ b/Makefile @@ -1413,6 +1413,7 @@ test-ae: compiler ae stdlib @# A bad AE_SWEEP_SHARD (#2757) must fail here: inside the pipelines below @# it would leave the sweep with nothing to run, and nothing reads as a pass. @sh tests/scripts/sweep_shard.sh < /dev/null + @rm -f $(BUILD_DIR)/test-ae.verdict @tmpdir=$$(mktemp -d); \ root=$$(pwd); \ script="$$root/tests/scripts/run_ae_test.sh"; \ @@ -1478,8 +1479,12 @@ test-ae: compiler ae stdlib fi; \ sh "$$root/tests/scripts/sweep_timings.sh" "$$tmpdir"; \ echo "Aether Tests: $$passed passed, $$failed failed, $$skipped skipped, $$total total"; \ + printf '%s %s %s %s\n' "$$passed" "$$failed" "$$skipped" "$$total" > "$$root/$(BUILD_DIR)/test-ae.verdict"; \ rm -rf "$$tmpdir"; \ if [ "$$failed" -gt 0 ]; then exit 1; fi + @# A shell that stopped before its tally can still leave make with status + @# 0: only a written tally with no failure is a pass. + @sh tests/scripts/sweep_verdict.sh $(BUILD_DIR)/test-ae.verdict endif # Install smoke test: installs to a temp dir, runs ae init + ae run, cleans up @@ -2547,8 +2552,6 @@ install: $(VERSION_HEADER) release-build ae stdlib # runtime needs the factor-language fork) # - host/aether (fork+exec sandboxed child via LD_PRELOAD; # libc + in-tree sandbox runtime only) -# - tinyweb (libc only — SHA-1 + base64 for WebSocket -# handshake; no third-party dep) # Out of scope for v1: host/{java,go} (separate-process or JNI-style # bridges, different build shape — javac/jar and cgo c-archive don't fit # the cc→ar pipeline). The widget toolkit @@ -2799,7 +2802,9 @@ help: @echo " make test-all - Run both C and .ae tests" @echo " make check-standalone - Compile every standalone C main (benches, demos)" @echo " make check-docs - Compile the documentation's complete examples" - @echo " make check-tests - The test suite can fail: verdicts reach the exit code, prune list is consistent, every std module has tests, listeners bind loopback, shell tests end background programs through server_jobs.sh, test programs write temp files under names of their own" + @echo " make check-tests - The test suite can fail: verdicts reach the exit code, prune list is consistent, every std module has tests, listeners bind loopback, shell tests end background programs through server_jobs.sh and compare files with same_bytes, test programs write temp files under names of their own and ask the build for the platform" + @echo " make check-extern-widths - Every extern bound to C in this tree agrees with the C definition on each width; no plain C long in a bound signature" + @echo " make check-raw-calls - No std or contrib file calls a raw C binding another module declares" @echo " make check-contrib-modules - Type-check every non-host contrib module" @echo " make check-changelog - Validate changelog fragments, catch a release-fold" @echo " make add-changelog - Write a new_changelogs/ fragment (SECTION=, SLUG=)" @@ -2990,6 +2995,8 @@ ci: clean @$(CI_STAGE) 900 check-standalone $(MAKE) check-standalone @$(CI_STAGE) 900 check-docs $(MAKE) check-docs @$(CI_STAGE) 900 check-tests $(MAKE) check-tests + @$(CI_STAGE) 900 check-extern-widths $(MAKE) check-extern-widths + @$(CI_STAGE) 600 check-raw-calls $(MAKE) check-raw-calls @echo "" @echo "[5/11] Running .ae integration tests..." @$(MAKE) test-ae @@ -3051,6 +3058,45 @@ ownership-grid ownership-grid-check: compiler ae stdlib NPROC=$(NPROC) ./$(OWNERSHIP_GRID_RUNNER) \ $(if $(filter ownership-grid-check,$@),--check) $(OWNERSHIP_GRID_ARGS) +# ----------------------------------------------------------------- +# Extern widths (#3097): every extern bound to a C function of this tree +# agrees with the C definition on the width of each argument and of the +# result, and no bound C signature uses a plain `long` (32 bits on Windows, +# 64 elsewhere). No C compiler sees an extern's prototype beside the C +# definition, so nothing else catches a disagreement: the call is made with +# the wrong width. The checker is Aether (tests/extern_widths/check.ae). Its +# self-test first: the fixture holds exactly 11 mismatches, one of each +# kind, among definitions that agree. +# ----------------------------------------------------------------- +EXTERN_WIDTHS_CHECKER := $(BUILD_DIR)/extern_widths/check$(EXE_EXT) +EXTERN_WIDTHS_DIRS := std contrib runtime tests examples tools lsp benchmarks +.PHONY: check-extern-widths +check-extern-widths: compiler ae stdlib + @mkdir -p $(BUILD_DIR)/extern_widths + @./$(BUILD_DIR)/ae$(EXE_EXT) build tests/extern_widths/check.ae -o $(EXTERN_WIDTHS_CHECKER) + @./$(EXTERN_WIDTHS_CHECKER) --expect 11 tests/extern_widths/fixture \ + > $(BUILD_DIR)/extern_widths/self_test.log 2>&1 || \ + { cat $(BUILD_DIR)/extern_widths/self_test.log; exit 1; } + @./$(EXTERN_WIDTHS_CHECKER) --exclude tests/extern_widths/fixture $(EXTERN_WIDTHS_DIRS) + +# ----------------------------------------------------------------- +# Raw calls (#3107): a std module binds C through `*_raw` externs and +# builds its public functions on them (error results, ownership of what +# is stored, bounds). No std or contrib file calls such a binding that +# another module declares; it calls that module's public function. The +# checker is Aether (tests/raw_calls/check.ae). Its self-test first: the +# fixture holds exactly 4 such calls among uses that are not. +# ----------------------------------------------------------------- +RAW_CALLS_CHECKER := $(BUILD_DIR)/raw_calls/check$(EXE_EXT) +.PHONY: check-raw-calls +check-raw-calls: compiler ae stdlib + @mkdir -p $(BUILD_DIR)/raw_calls + @./$(BUILD_DIR)/ae$(EXE_EXT) build tests/raw_calls/check.ae -o $(RAW_CALLS_CHECKER) + @./$(RAW_CALLS_CHECKER) --expect 4 tests/raw_calls/fixture \ + > $(BUILD_DIR)/raw_calls/self_test.log 2>&1 || \ + { cat $(BUILD_DIR)/raw_calls/self_test.log; exit 1; } + @./$(RAW_CALLS_CHECKER) std contrib + # The C a program gets when the C compiler is not GCC-compatible # (AETHER_GCC_COMPAT 0, which is what MSVC compiles): strict C11 under gcc, # and under cl.exe too when it is on PATH (#2725). CI runs the gcc half on the @@ -3274,7 +3320,7 @@ test-optional: compiler ae stdlib check-tests: @echo "===================================" - @echo " test suite (verdicts, prune list, loopback listeners, shell kills, temp names)" + @echo " test suite (verdicts, prune list, loopback listeners, shell kills, shell tools, temp names, platform)" @echo "===================================" @py=""; \ for cand in python3 python "py -3"; do \ @@ -3290,10 +3336,14 @@ check-tests: $$py tests/scripts/check_loopback_listeners.py && \ $$py tests/scripts/test_check_shell_kills.py && \ $$py tests/scripts/check_shell_kills.py && \ + $$py tests/scripts/test_check_shell_tools.py && \ + $$py tests/scripts/check_shell_tools.py && \ $$py tests/scripts/test_check_shell_tmp.py && \ $$py tests/scripts/check_shell_tmp.py && \ $$py tests/scripts/test_check_ae_tmp.py && \ - $$py tests/scripts/check_ae_tmp.py; \ + $$py tests/scripts/check_ae_tmp.py && \ + $$py tests/scripts/test_check_platform_env.py && \ + $$py tests/scripts/check_platform_env.py; \ else \ echo " [SKIP] test suite checks — no working Python found"; \ echo " (checked on the Linux/macOS legs, which run the same target)"; \ diff --git a/compiler/aether_aea.c b/compiler/aether_aea.c index 6d68146fa..398bf4bbf 100644 --- a/compiler/aether_aea.c +++ b/compiler/aether_aea.c @@ -124,7 +124,7 @@ static int encode_type(Buf* b, const Type* t, const char** err) { static int encode_node(Buf* b, const ASTNode* n, const char* source_file, int depth, const char** err) { if (depth > AEA_MAX_DEPTH) { *err = "the AST is deeper than an artifact may hold"; return 0; } - if (n->is_imported || n->origin_module || n->warned || n->arg_order) { + if (n->is_imported || n->origin_module || n->warned || n->arg_order || n->callee_extern) { *err = "the AST has been merged or analysed (not a fresh parse)"; return 0; } diff --git a/compiler/aether_error.c b/compiler/aether_error.c index 64055ffe5..6d61b6976 100644 --- a/compiler/aether_error.c +++ b/compiler/aether_error.c @@ -44,6 +44,19 @@ static const char* current_filename = NULL; static const char* current_source = NULL; static int error_count_global = 0; static int warning_count_global = 0; +/* While set, a diagnostic is neither printed nor counted. Codegen sets it + * around the second rendering of one function (#3029's view body): the + * same source emitted twice would otherwise report everything twice, and + * the other rendering reports it once. */ +static int diagnostics_muted = 0; + +void aether_diagnostics_set_muted(int muted) { + diagnostics_muted = muted ? 1 : 0; +} + +int aether_diagnostics_muted(void) { + return diagnostics_muted; +} // Get suggestion based on error code static const char* get_common_suggestion(AetherErrorCode code) { @@ -172,8 +185,8 @@ static const char* get_line(const char* source, int line_num, int* line_length) } void aether_error_report(AetherError* error) { - if (!error) return; - + if (!error || diagnostics_muted) return; + error_count_global++; /* A diagnostic built by hand often leaves filename and source_code NULL @@ -288,7 +301,7 @@ void aether_error_report(AetherError* error) { } void aether_warning_report(AetherError* warning) { - if (!warning) return; + if (!warning || diagnostics_muted) return; warning_count_global++; diff --git a/compiler/aether_error.h b/compiler/aether_error.h index 4c72059a8..6275772b4 100644 --- a/compiler/aether_error.h +++ b/compiler/aether_error.h @@ -60,6 +60,10 @@ void aether_error_get_source(const char** out_filename, const char** out_source) void aether_error_set_source(const char* filename, const char* source); void aether_error_report(AetherError* error); void aether_warning_report(AetherError* warning); +// While muted, errors, warnings and codegen notes are neither printed nor +// counted. Codegen mutes the second rendering of one function (#3029). +void aether_diagnostics_set_muted(int muted); +int aether_diagnostics_muted(void); // Helper functions void aether_error_simple(const char* message, int line, int column); diff --git a/compiler/aether_module.c b/compiler/aether_module.c index a9936b6bf..941a22d44 100644 --- a/compiler/aether_module.c +++ b/compiler/aether_module.c @@ -1119,6 +1119,29 @@ static char* pkg_probe_root(const char* root, const char* sub_path) { } // Resolve a local module path (e.g., "mypackage.utils") to a file path. +/* The module `converted` (a path with slashes) under `base`, the importing + * file's directory with its trailing slash, in the shapes and the lib path + * order the CWD-relative probes use (Issue #413). */ +static char* resolve_under_source_dir(const char* base, const char* converted) { + char path[4096]; + for (int li = 0; li < global_module_registry->lib_dir_count; li++) { + const char* lib = global_module_registry->lib_dirs[li]; + snprintf(path, sizeof(path), "%s%s/%s/module.ae", base, lib, converted); + if (module_probe(path)) return strdup(path); + snprintf(path, sizeof(path), "%s%s/%s.ae", base, lib, converted); + if (module_probe(path)) return strdup(path); + } + snprintf(path, sizeof(path), "%ssrc/%s/module.ae", base, converted); + if (module_probe(path)) return strdup(path); + snprintf(path, sizeof(path), "%ssrc/%s.ae", base, converted); + if (module_probe(path)) return strdup(path); + snprintf(path, sizeof(path), "%s%s/module.ae", base, converted); + if (module_probe(path)) return strdup(path); + snprintf(path, sizeof(path), "%s%s.ae", base, converted); + if (module_probe(path)) return strdup(path); + return NULL; +} + char* module_resolve_local_path(const char* module_path) { char converted[512]; char path[4096]; @@ -1130,6 +1153,17 @@ char* module_resolve_local_path(const char* module_path) { if (*p == '.') *p = '/'; } + /* #3103: a module's own import names a file beside it first. Tried after + * the CWD, std.jsonpath's `import parser` found contrib/jq/parser.ae + * whenever the compiler ran in contrib/jq, and a program importing + * both failed there ("'free_query' is not exported from module + * 'parser'"). The entry program keeps the CWD (the project root) first. */ + const char* src_dir = global_module_registry->source_dir; + if (global_module_registry->source_is_module && src_dir && src_dir[0]) { + char* found = resolve_under_source_dir(src_dir, converted); + if (found) return found; + } + /* Try 1/2: search every CWD-relative lib_dirs[] entry in order, * first hit wins. Each entry probes both shapes (`/ * /module.ae` then `/.ae`) so the @@ -1161,25 +1195,9 @@ char* module_resolve_local_path(const char* module_path) { if (module_probe(path)) return strdup(path); // Try 6b: Search relative to source file directory - if (global_module_registry->source_dir && global_module_registry->source_dir[0]) { - /* Mirror the CWD-relative loop above, but anchored at the - * source file's directory. Same left-to-right semantics - * across the multi-entry lib path. Issue #413. */ - for (int li = 0; li < global_module_registry->lib_dir_count; li++) { - const char* lib = global_module_registry->lib_dirs[li]; - snprintf(path, sizeof(path), "%s%s/%s/module.ae", global_module_registry->source_dir, lib, converted); - if (module_probe(path)) return strdup(path); - snprintf(path, sizeof(path), "%s%s/%s.ae", global_module_registry->source_dir, lib, converted); - if (module_probe(path)) return strdup(path); - } - snprintf(path, sizeof(path), "%ssrc/%s/module.ae", global_module_registry->source_dir, converted); - if (module_probe(path)) return strdup(path); - snprintf(path, sizeof(path), "%ssrc/%s.ae", global_module_registry->source_dir, converted); - if (module_probe(path)) return strdup(path); - snprintf(path, sizeof(path), "%s%s/module.ae", global_module_registry->source_dir, converted); - if (module_probe(path)) return strdup(path); - snprintf(path, sizeof(path), "%s%s.ae", global_module_registry->source_dir, converted); - if (module_probe(path)) return strdup(path); + if (src_dir && src_dir[0] && !global_module_registry->source_is_module) { + char* found = resolve_under_source_dir(src_dir, converted); + if (found) return found; } // Try 7-9: Search installed packages at ~/.aether/packages/ @@ -2052,9 +2070,10 @@ static void module_bind_import_identity(ASTNode* imp, const char* resolved, } /* Put back the source directory saved around a module's imports. */ -static void module_restore_source_dir(char* saved) { +static void module_restore_source_dir(char* saved, int saved_is_module) { free(global_module_registry->source_dir); global_module_registry->source_dir = saved; + global_module_registry->source_is_module = saved_is_module; } // Recursive helper: load a single module and its transitive imports @@ -2180,8 +2199,10 @@ static int orchestrate_module(const char* module_name, const char* file_path, * the parent's source_dir. The parent's is kept as it is, whole, and * put back (#2543). */ char* saved_source_dir = global_module_registry->source_dir; + int saved_is_module = global_module_registry->source_is_module; global_module_registry->source_dir = NULL; module_set_source_dir(file_path); + global_module_registry->source_is_module = 1; // Recursively process this module's imports for (int i = 0; i < ast->child_count; i++) { @@ -2192,7 +2213,7 @@ static int orchestrate_module(const char* module_name, const char* file_path, if (import_lane_target(child)) { if (!module_bind_target_import(child, file_path)) { /* #2607 */ free(sub_file); - module_restore_source_dir(saved_source_dir); + module_restore_source_dir(saved_source_dir, saved_is_module); return 0; } } else { @@ -2207,13 +2228,13 @@ static int orchestrate_module(const char* module_name, const char* file_path, if (sub_file) { if (!orchestrate_module(sub_path, sub_file, graph)) { free(sub_file); - module_restore_source_dir(saved_source_dir); + module_restore_source_dir(saved_source_dir, saved_is_module); return 0; } free(sub_file); module_note_lane_target(child); /* #2607 */ if (!check_selective_import_exports(child, file_path)) { - module_restore_source_dir(saved_source_dir); + module_restore_source_dir(saved_source_dir, saved_is_module); return 0; } } else { @@ -2221,7 +2242,7 @@ static int orchestrate_module(const char* module_name, const char* file_path, } } - module_restore_source_dir(saved_source_dir); + module_restore_source_dir(saved_source_dir, saved_is_module); return 1; } @@ -2790,10 +2811,16 @@ static void rename_intra_module_refs(ASTNode* node, const char* prefix, int renamed = name_in_list(node->value, local_names, local_count); for (int i = 0; !renamed && i < func_count; i++) { if (strcmp(node->value, func_names[i]) == 0) { + /* #3116: the module's own `free` taking one argument. The + * checker gives a slice argument to the builtin instead, + * once its type is known (typecheck_function_call_inner). */ + int own_free = strcmp(node->value, "free") == 0 && + node->child_count == 1 && !node->annotation; char prefixed[256]; merged_function_name_ns(prefix, node->value, prefixed, sizeof(prefixed)); free(node->value); node->value = strdup(prefixed); + if (own_free) node->annotation = strdup("own_free"); renamed = 1; break; } diff --git a/compiler/aether_module.h b/compiler/aether_module.h index 9fee15479..80acd3b50 100644 --- a/compiler/aether_module.h +++ b/compiler/aether_module.h @@ -41,6 +41,9 @@ typedef struct { // Source file directory for relative resolution, with its trailing // slash: a heap copy of any length (#2543), NULL or "" when none. char* source_dir; + // #3103: 1 while a module's own imports are resolved (source_dir is + // that module's directory), 0 for the entry program's. + int source_is_module; // Lib-search path: an ordered list of directories (PATH-style), // searched left-to-right; first hit wins. Issue #413. Default is // a single entry, `"lib"`, populated at registry init. Each entry diff --git a/compiler/analysis/hoist.c b/compiler/analysis/hoist.c index 9cc4f7324..baf0cf3a9 100644 --- a/compiler/analysis/hoist.c +++ b/compiler/analysis/hoist.c @@ -6,51 +6,11 @@ #include "hoist.h" #include "../aether_strmap.h" +#include +#include #include #include -int hoist_direct_decl_names(ASTNode* block, const char** out, int count, int cap) { - if (!block || !out) return count; - for (int i = 0; i < block->child_count; i++) { - ASTNode* child = block->children[i]; - if (!child) continue; - if (child->type == AST_VARIABLE_DECLARATION && child->value && count < cap) { - int already = 0; - for (int j = 0; j < count; j++) { - if (strcmp(out[j], child->value) == 0) { already = 1; break; } - } - if (!already) out[count++] = child->value; - } - } - return count; -} - -ASTNode* hoist_find_decl(ASTNode* block, const char* name) { - if (!block || !name) return NULL; - for (int i = 0; i < block->child_count; i++) { - ASTNode* child = block->children[i]; - if (!child) continue; - if (child->type == AST_VARIABLE_DECLARATION && child->value - && strcmp(child->value, name) == 0) { - return child; - } - } - return NULL; -} - -int hoist_has_identifier_ref(ASTNode* node, const char* name) { - if (!node || !name) return 0; - if (node->type == AST_IDENTIFIER && node->value && - strcmp(node->value, name) == 0) return 1; - /* Any identifier counts as a use; declaration order within a subtree is - * not tracked. Over-eager, and safe: an extra hoisted declaration is a - * zero-initialised local. */ - for (int i = 0; i < node->child_count; i++) { - if (hoist_has_identifier_ref(node->children[i], name)) return 1; - } - return 0; -} - Type* hoist_decl_type(ASTNode* decl) { if (!decl) return NULL; Type* t = decl->node_type; @@ -148,56 +108,9 @@ static void fold_bind(JoinFold* f, const ASTNode* n) { fold_add(f, t, v, written); } -/* The bindings of `name` under `n` (closures excluded, each its own - * function), joined in the order they are written. */ -static void join_walk(ASTNode* n, const char* name, JoinFold* f) { - if (!n) return; - if (n->type == AST_CLOSURE || n->type == AST_QUOTE) return; /* its own function */ - if (n->type == AST_VARIABLE_DECLARATION && n->value && strcmp(n->value, name) == 0) - fold_bind(f, n); - for (int i = 0; i < n->child_count; i++) join_walk(n->children[i], name, f); -} - -static JoinFold fold_of(ASTNode* root, const char* name) { - JoinFold f; - memset(&f, 0, sizeof(f)); - for (int i = 0; root && name && i < root->child_count; i++) - join_walk(root->children[i], name, &f); - return f; -} - -Type* hoist_numeric_join(ASTNode* root, const char* name) { - JoinFold f = fold_of(root, name); - Type* r = fold_result(&f); - fold_free(&f); - return r; -} - -/* The first binding of `name` under `n` (closures excluded) that writes - * its type, of any kind. */ -static const ASTNode* first_written_decl(const ASTNode* n, const char* name) { - if (!n || n->type == AST_CLOSURE || n->type == AST_QUOTE) return NULL; - if (n->type == AST_VARIABLE_DECLARATION && n->value && !n->type_inferred && n->node_type && - n->node_type->kind != TYPE_UNKNOWN && n->node_type->kind != TYPE_VOID && - strcmp(n->value, name) == 0) - return n; - for (int i = 0; i < n->child_count; i++) { - const ASTNode* d = first_written_decl(n->children[i], name); - if (d) return d; - } - return NULL; -} - -Type* hoist_written_type(ASTNode* root, const char* name) { - for (int i = 0; root && name && i < root->child_count; i++) { - const ASTNode* d = first_written_decl(root->children[i], name); - if (d) return clone_type(d->node_type); - } - return NULL; -} - /* #3064: the bindings of one name under a HoistJoins' root, in the order - * join_walk meets them, and what the walk typing them has made of them. + * a walk of the root meets them, and what the walk typing them has made of + * them. * The walk types binds[0..typed) in that order; their parts are final for * it, folded in `head`. A binding not typed yet changes only when the walk * types it, so the parts of binds[typed..count) that `join` read (`rest`, @@ -347,14 +260,159 @@ void hoist_joins_free(HoistJoins* j) { free(j); } -/* Over the inferred bindings of `name` in `n` (closures excluded): sets - * `*strings` when one is a string, `*others` when one is neither a string - * nor `null`. */ -static void null_string_walk(ASTNode* n, const char* name, int* strings, int* others) { - if (!n) return; - if (n->type == AST_CLOSURE || n->type == AST_QUOTE) return; - if (n->type == AST_VARIABLE_DECLARATION && n->value && n->type_inferred && - strcmp(n->value, name) == 0) { +/* ---- #3083: one walk per body or statement ------------------------------ + * The questions below are asked once per name, and a generated body binds + * thousands of names: walking the body for each was quadratic in them. Each + * answer is read from what one walk of the body (or the statement) gathered, + * kept until the next checker or codegen run (hoist_cache_reset). A walk + * gathers the tree's shape, which neither pass changes after codegen's + * renames; a binding's type, which the checker fills in as it goes, is read + * when it is asked for. The answers are kept by node, and a node's go with + * it (hoist_node_freed): one allocated later at its address is a new node. */ + +struct HoistNodeCache { + HoistJoins* binds; /* as a body: its bindings by name */ + HoistNames* branch; /* as a body: hoist_if_branch_hoists */ + HoistNames* common; /* as a statement: hoist_common_hoists */ +}; + +/* Open addressing over the node's address; a removed slot is a tombstone + * until the table next grows. */ +typedef struct { + const ASTNode* node; + struct HoistNodeCache* cache; +} CacheSlot; + +#define CACHE_TOMBSTONE ((const ASTNode*)(uintptr_t)1) + +static CacheSlot* g_cache; +static size_t g_cache_cap; /* a power of two, or 0 */ +static size_t g_cache_used; /* live and tombstone slots */ + +static size_t cache_hash(const ASTNode* n) { + uint64_t h = (uint64_t)(uintptr_t)n; + h ^= h >> 33; + h *= 0xff51afd7ed558ccdULL; + h ^= h >> 33; + return (size_t)h; +} + +static CacheSlot* cache_slot(const ASTNode* n) { + if (!g_cache_cap) return NULL; + for (size_t i = cache_hash(n) & (g_cache_cap - 1);; i = (i + 1) & (g_cache_cap - 1)) { + if (g_cache[i].node == n) return &g_cache[i]; + if (!g_cache[i].node) return NULL; + } +} + +static void names_free(HoistNames* h) { + if (!h) return; + free((void*)h->names); + free(h->decls); + strmap_free(&h->at); + free(h); +} + +static void cache_free(struct HoistNodeCache* c) { + hoist_joins_free(c->binds); + names_free(c->branch); + names_free(c->common); + free(c); +} + +static struct HoistNodeCache* node_cache(ASTNode* n) { + CacheSlot* s = cache_slot(n); + if (s) return s->cache; + if ((g_cache_used + 1) * 2 > g_cache_cap) { + size_t cap = g_cache_cap ? g_cache_cap * 2 : 256; + CacheSlot* grown = (CacheSlot*)calloc(cap, sizeof(CacheSlot)); + if (!grown) { + fprintf(stderr, "Fatal: out of memory in the hoisting cache\n"); + exit(1); + } + size_t used = 0; + for (size_t i = 0; i < g_cache_cap; i++) { + const ASTNode* k = g_cache[i].node; + if (!k || k == CACHE_TOMBSTONE) continue; + size_t j = cache_hash(k) & (cap - 1); + while (grown[j].node) j = (j + 1) & (cap - 1); + grown[j] = g_cache[i]; + used++; + } + free(g_cache); + g_cache = grown; + g_cache_cap = cap; + g_cache_used = used; + } + struct HoistNodeCache* c = (struct HoistNodeCache*)calloc(1, sizeof(*c)); + if (!c) { + fprintf(stderr, "Fatal: out of memory in the hoisting cache\n"); + exit(1); + } + size_t j = cache_hash(n) & (g_cache_cap - 1); + while (g_cache[j].node && g_cache[j].node != CACHE_TOMBSTONE) j = (j + 1) & (g_cache_cap - 1); + if (!g_cache[j].node) g_cache_used++; + g_cache[j].node = n; + g_cache[j].cache = c; + return c; +} + +void hoist_node_freed(const ASTNode* n) { + CacheSlot* s = cache_slot(n); + if (!s) return; + cache_free(s->cache); + s->node = CACHE_TOMBSTONE; + s->cache = NULL; +} + +void hoist_cache_reset(void) { + for (size_t i = 0; i < g_cache_cap; i++) { + if (g_cache[i].node && g_cache[i].node != CACHE_TOMBSTONE) cache_free(g_cache[i].cache); + } + free(g_cache); + g_cache = NULL; + g_cache_cap = 0; + g_cache_used = 0; +} + +/* The bindings of `name` under `root` (closures excluded), in the order a + * walk of the body meets them; NULL for none. */ +static const HoistNameJoin* body_binds(ASTNode* root, const char* name) { + if (!root || !name) return NULL; + struct HoistNodeCache* c = node_cache(root); + if (!c->binds) c->binds = hoist_joins_new(root); + if (!c->binds) return NULL; + return (const HoistNameJoin*)strmap_get(&c->binds->names, name); +} + +Type* hoist_numeric_join(ASTNode* root, const char* name) { + const HoistNameJoin* nj = body_binds(root, name); + JoinFold f; + memset(&f, 0, sizeof(f)); + for (int i = 0; nj && i < nj->count; i++) fold_bind(&f, nj->binds[i]); + Type* r = fold_result(&f); + fold_free(&f); + return r; +} + +Type* hoist_written_type(ASTNode* root, const char* name) { + const HoistNameJoin* nj = body_binds(root, name); + for (int i = 0; nj && i < nj->count; i++) { + const ASTNode* n = nj->binds[i]; + if (!n->type_inferred && n->node_type && n->node_type->kind != TYPE_UNKNOWN && + n->node_type->kind != TYPE_VOID) + return clone_type(n->node_type); + } + return NULL; +} + +/* Over the inferred bindings of `name` under `root`: sets `*strings` when + * one is a string, `*others` when one is neither a string nor `null`. */ +static void null_string_binds(ASTNode* root, const char* name, int* strings, int* others) { + const HoistNameJoin* nj = body_binds(root, name); + for (int i = 0; nj && i < nj->count; i++) { + ASTNode* n = (ASTNode*)nj->binds[i]; + if (!n->type_inferred) continue; ASTNode* init = n->child_count > 0 ? n->children[0] : NULL; Type* t = hoist_decl_type(n); if (init && init->type == AST_NULL_LITERAL) { @@ -365,20 +423,18 @@ static void null_string_walk(ASTNode* n, const char* name, int* strings, int* ot *others = 1; } } - for (int i = 0; i < n->child_count; i++) null_string_walk(n->children[i], name, strings, others); } -/* #2980 review: over the inferred bindings of `name` in `n` (closures - * excluded), function values joined with `acc` by fn_type_mode_join. */ -static Type* fn_join_walk(ASTNode* n, const char* name, Type* acc) { - if (!n) return acc; - if (n->type == AST_CLOSURE || n->type == AST_QUOTE) return acc; - if (n->type == AST_VARIABLE_DECLARATION && n->value && n->type_inferred && - strcmp(n->value, name) == 0) { +/* #2980 review: the inferred bindings of `name` under `root` that are + * function values, joined with `acc` by fn_type_mode_join. */ +static Type* fn_join_binds(ASTNode* root, const char* name, Type* acc) { + const HoistNameJoin* nj = body_binds(root, name); + for (int i = 0; nj && i < nj->count; i++) { + ASTNode* n = (ASTNode*)nj->binds[i]; + if (!n->type_inferred) continue; Type* j = fn_type_mode_join(acc, hoist_decl_type(n)); if (j) { free_type(acc); acc = j; } } - for (int i = 0; i < n->child_count; i++) acc = fn_join_walk(n->children[i], name, acc); return acc; } @@ -399,7 +455,7 @@ Type* hoist_join_type(ASTNode* scope_body, const char* name, Type* seed) { * through the stronger mode at each parameter, where either may stand; * typed from the first binding alone, a `@keep` in the other was lost. */ if (seed->kind == TYPE_FUNCTION) { - Type* acc = fn_join_walk(scope_body, name, clone_type(seed)); + Type* acc = fn_join_binds(scope_body, name, clone_type(seed)); if (acc && types_equal(acc, seed)) { free_type(acc); return NULL; } return acc; } @@ -410,7 +466,7 @@ Type* hoist_join_type(ASTNode* scope_body, const char* name, Type* seed) { * which a `string` local could not take (E0202). */ if (seed->kind == TYPE_PTR && !seed->element_type) { int strings = 0, others = 0; - null_string_walk(scope_body, name, &strings, &others); + null_string_binds(scope_body, name, &strings, &others); if (strings && !others) return create_type(TYPE_STRING); } if (!is_number_type(seed)) return NULL; /* not a numeric kind: nothing to join */ @@ -426,8 +482,8 @@ Type* hoist_join_type(ASTNode* scope_body, const char* name, Type* seed) { /* The arms of a branching statement: an if's then and else blocks, a * switch's case statements, whose children (after a case's selector) are * its body, or a match's arm bodies. A case's selector is never a - * declaration, so the case node itself is the body hoist_direct_decl_names - * and hoist_find_decl read (#2732). A match arm that binds a name has a + * declaration, so the case node itself is the body whose direct + * declarations are read (#2732). A match arm that binds a name has a * block body (the parser makes `1 -> v = 10` the block `{ v = 10 }`); any * other arm body is a value or a statement that binds nothing, and counts * as an arm all the same (#2768). Returns the count written to `arms` (at @@ -459,15 +515,18 @@ static int is_branching(ASTNode* stmt) { stmt->type == AST_MATCH_STATEMENT); } -/* Does the arm pattern `p` bind `name`: `some(name)`, an element of a - * list pattern (`[name, y]`, `[name | t]`), or a field of a struct pattern - * (`Pt { x: name }`)? A bare name in a pattern is a - * value compared with, or a sum's variant, and binds nothing. */ -static int pattern_binds_name(const ASTNode* p, const char* name) { - if (!p || !name) return 0; +/* Each name the arm pattern `p` binds, to `visit` until it returns nonzero + * (which this returns): `some(name)`, an element of a list pattern (`[name, + * y]`, `[name | t]`), or a field of a struct pattern (`Pt { x: name }`). A + * bare name in a pattern is a value compared with, or a sum's variant, and + * binds nothing. */ +typedef int (*BoundNameVisit)(const char* name, void* user); + +static int pattern_each_bound(const ASTNode* p, BoundNameVisit visit, void* user) { + if (!p) return 0; if (p->type == AST_PATTERN_VARIABLE) - return p->annotation && strcmp(p->annotation, "some_pattern") == 0 && p->value && - strcmp(p->value, name) == 0; + return p->value && p->annotation && strcmp(p->annotation, "some_pattern") == 0 && + visit(p->value, user); /* #2992: a struct pattern binds the names its fields take, and those of * a struct pattern over a field. */ if (p->type == AST_PATTERN_STRUCT) { @@ -476,21 +535,26 @@ static int pattern_binds_name(const ASTNode* p, const char* name) { const ASTNode* sub = (fp && fp->type == AST_ASSIGNMENT && fp->child_count > 0) ? fp->children[0] : NULL; if (!sub) continue; - if (sub->type == AST_PATTERN_VARIABLE && sub->value && strcmp(sub->value, name) == 0) + if (sub->type == AST_PATTERN_VARIABLE && sub->value && visit(sub->value, user)) return 1; - if (sub->type == AST_PATTERN_STRUCT && pattern_binds_name(sub, name)) return 1; + if (sub->type == AST_PATTERN_STRUCT && pattern_each_bound(sub, visit, user)) return 1; } return 0; } if (p->type != AST_PATTERN_LIST && p->type != AST_PATTERN_CONS) return 0; for (int i = 0; i < p->child_count; i++) { const ASTNode* e = p->children[i]; - if (e && e->type == AST_PATTERN_VARIABLE && e->value && strcmp(e->value, name) == 0) - return 1; + if (e && e->type == AST_PATTERN_VARIABLE && e->value && visit(e->value, user)) return 1; } return 0; } +static int is_name(const char* name, void* want) { return strcmp(name, (const char*)want) == 0; } + +static int pattern_binds_name(const ASTNode* p, const char* name) { + return name && pattern_each_bound(p, is_name, (void*)name); +} + /* #2768: does an arm pattern of `stmt`, a match, bind `name`? Such a name * is the arm's own, and is not hoisted out of the match by any rule: a * binding of it in that arm rebinds the pattern's value, and one in another @@ -509,120 +573,194 @@ static int match_pattern_binds(const ASTNode* stmt, const char* name) { #define HOIST_MAX_ARMS 256 -int hoist_if_branch_candidates(ASTNode* body, const char** out, int cap) { - if (!body || !out) return 0; +static HoistNames* names_new(int per) { + HoistNames* h = (HoistNames*)calloc(1, sizeof(HoistNames)); + if (!h) return NULL; + h->per = per; + strmap_init(&h->at); + return h; +} + +/* Add `name` (one not in `h` yet), its bindings unset; returns its index. */ +static int names_push(HoistNames* h, const char* name) { + if (h->count == h->cap) { + int cap = h->cap ? h->cap * 2 : 8; + h->names = (const char**)aether_xrealloc((void*)h->names, (size_t)cap * sizeof(char*)); + h->decls = (ASTNode**)aether_xrealloc(h->decls, (size_t)cap * (size_t)h->per * + sizeof(ASTNode*)); + memset(h->decls + (size_t)h->cap * (size_t)h->per, 0, + (size_t)(cap - h->cap) * (size_t)h->per * sizeof(ASTNode*)); + h->cap = cap; + } + h->names[h->count] = name; + strmap_put(&h->at, name, (void*)(intptr_t)(h->count + 1)); + return h->count++; +} + +int hoist_names_find(const HoistNames* h, const char* name) { + void* at = h && name ? strmap_get(&h->at, name) : NULL; + return at ? (int)(intptr_t)at - 1 : -1; +} + +ASTNode* hoist_names_decl(const HoistNames* h, int i) { + if (!h || i < 0 || i >= h->count) return NULL; + ASTNode* any = NULL; + for (int k = 0; k < h->per; k++) { + ASTNode* d = h->decls[(size_t)i * (size_t)h->per + (size_t)k]; + if (!d) continue; + if (!any) any = d; + Type* t = hoist_decl_type(d); + if (t && t->kind != TYPE_VOID && t->kind != TYPE_UNKNOWN) return d; + } + return any; +} + +static int put_name(const char* name, void* out) { + strmap_put((StrMap*)out, name, (void*)1); + return 0; +} + +static void pattern_bound_names(const ASTNode* p, StrMap* out) { + pattern_each_bound(p, put_name, out); +} + +/* The names the arm patterns of `stmt`, a match, bind (match_pattern_binds + * for every name at once). */ +static void pattern_names(const ASTNode* stmt, StrMap* out) { + if (!stmt || stmt->type != AST_MATCH_STATEMENT) return; + for (int i = 1; i < stmt->child_count; i++) { + const ASTNode* a = stmt->children[i]; + if (a && a->type == AST_MATCH_ARM && a->child_count > 0) + pattern_bound_names(a->children[0], out); + } +} + +/* Every identifier name under `n` (closures included: any use counts). */ +static void identifier_names(const ASTNode* n, StrMap* out) { + if (!n) return; + if (n->type == AST_IDENTIFIER && n->value) strmap_put(out, n->value, (void*)1); + for (int i = 0; i < n->child_count; i++) identifier_names(n->children[i], out); +} + +static HoistNames* branch_hoists_build(ASTNode* body) { + HoistNames* all = names_new(1); + HoistNames* kept = names_new(1); + if (!all || !kept) { names_free(all); names_free(kept); return NULL; } /* Names a top-level statement declares get their own function-scope * declaration (and heap tracker) where they are; hoisting them from an - * arm as well would declare them twice. */ - const char* top_level_decls[HOIST_MAX_NAMES]; - int top_count = 0; - for (int i = 0; i < body->child_count && top_count < HOIST_MAX_NAMES; i++) { + * arm as well would declare them twice. Only a name read outside the + * arms -- by a top-level statement that does not branch, or by an if's + * condition or a switch's or a match's subject -- is hoisted. */ + StrMap top, outside; + strmap_init(&top); + strmap_init(&outside); + for (int i = 0; i < body->child_count; i++) { ASTNode* child = body->children[i]; if (!child) continue; - if (child->type == AST_VARIABLE_DECLARATION && child->value) { - top_level_decls[top_count++] = child->value; + if (child->type == AST_VARIABLE_DECLARATION && child->value) + strmap_put(&top, child->value, (void*)1); + if (is_branching(child)) { + if (child->child_count > 0) identifier_names(child->children[0], &outside); + } else { + identifier_names(child, &outside); } } - - const char* names[HOIST_MAX_NAMES]; - int count = 0; + /* Each name an arm binds, in the order the arms bind them, with its + * first binding in an arm of a statement whose patterns do not bind it + * (a name a match's pattern binds comes out of no arm of that match). */ for (int i = 0; i < body->child_count; i++) { + ASTNode* stmt = body->children[i]; ASTNode* arms[HOIST_MAX_ARMS]; - int na = branch_arms(body->children[i], arms, HOIST_MAX_ARMS); + int na = branch_arms(stmt, arms, HOIST_MAX_ARMS); + if (na == 0) continue; + StrMap bound; + strmap_init(&bound); + pattern_names(stmt, &bound); for (int j = 0; j < na; j++) { - count = hoist_direct_decl_names(arms[j], names, count, HOIST_MAX_NAMES); - } - } - /* A name a match's pattern binds comes out of no arm of that match; - * hoist_if_branch_first_decl, below, finds a binding of it elsewhere or - * none. */ - - int kept = 0; - for (int n = 0; n < count && kept < cap; n++) { - const char* name = names[n]; - int dup = 0; - for (int k = 0; k < top_count; k++) { - if (strcmp(name, top_level_decls[k]) == 0) { dup = 1; break; } - } - if (dup) continue; - /* Only a name read outside the arms -- by a top-level statement that - * does not branch, or by an if's condition or a switch's or a - * match's subject -- is hoisted. */ - int referenced_outside = 0; - for (int i = 0; i < body->child_count && !referenced_outside; i++) { - ASTNode* child = body->children[i]; - if (!child) continue; - if (is_branching(child)) { - if (child->child_count > 0 && - hoist_has_identifier_ref(child->children[0], name)) { - referenced_outside = 1; - } - continue; + for (int k = 0; arms[j] && k < arms[j]->child_count; k++) { + ASTNode* d = arms[j]->children[k]; + if (!d || d->type != AST_VARIABLE_DECLARATION || !d->value) continue; + int at = hoist_names_find(all, d->value); + if (at < 0) at = names_push(all, d->value); + if (!all->decls[at] && !strmap_has(&bound, d->value)) all->decls[at] = d; } - if (hoist_has_identifier_ref(child, name)) referenced_outside = 1; } - if (!referenced_outside) continue; - if (!hoist_if_branch_first_decl(body, name)) continue; - out[kept++] = name; + strmap_free(&bound); } + for (int n = 0; n < all->count; n++) { + const char* name = all->names[n]; + if (strmap_has(&top, name) || !strmap_has(&outside, name) || !all->decls[n]) continue; + int at = names_push(kept, name); + kept->decls[at] = all->decls[n]; + } + strmap_free(&top); + strmap_free(&outside); + names_free(all); return kept; } -ASTNode* hoist_if_branch_first_decl(ASTNode* body, const char* name) { +const HoistNames* hoist_if_branch_hoists(ASTNode* body) { if (!body) return NULL; - for (int i = 0; i < body->child_count; i++) { - if (match_pattern_binds(body->children[i], name)) continue; - ASTNode* arms[HOIST_MAX_ARMS]; - int na = branch_arms(body->children[i], arms, HOIST_MAX_ARMS); - for (int j = 0; j < na; j++) { - ASTNode* d = hoist_find_decl(arms[j], name); - if (d) return d; - } - } - return NULL; + struct HoistNodeCache* c = node_cache(body); + if (!c->branch) c->branch = branch_hoists_build(body); + return c->branch; } -int hoist_common_decl_names(ASTNode* stmt, const char** out, int cap) { - if (!stmt || !out) return 0; +static HoistNames* common_hoists_build(ASTNode* stmt) { ASTNode* arms[HOIST_MAX_ARMS]; int na = branch_arms(stmt, arms, HOIST_MAX_ARMS); + HoistNames* h = names_new(na > 0 ? na : 1); + if (!h) return NULL; /* Every path must pass through an arm: an if needs its else, a switch * its default, a match a `_` arm or arms that cover its type (#2768). */ - if (stmt->type == AST_IF_STATEMENT && na < 2) return 0; + if (na == 0) return h; + if (stmt->type == AST_IF_STATEMENT && na < 2) return h; if (stmt->type == AST_SWITCH_STATEMENT) { int has_default = 0; for (int j = 0; j < na; j++) { if (arms[j]->value && strcmp(arms[j]->value, "default") == 0) has_default = 1; } - if (!has_default || na < 2) return 0; - } - if (stmt->type == AST_MATCH_STATEMENT && !ast_match_covers(stmt)) return 0; - if (na == 0) return 0; - const char* first[HOIST_MAX_NAMES]; - int nfirst = hoist_direct_decl_names(arms[0], first, 0, HOIST_MAX_NAMES); - int kept = 0; - for (int i = 0; i < nfirst && kept < cap; i++) { - int everywhere = !match_pattern_binds(stmt, first[i]); - for (int j = 1; j < na && everywhere; j++) { - if (!hoist_find_decl(arms[j], first[i])) everywhere = 0; - } - if (everywhere) out[kept++] = first[i]; + if (!has_default || na < 2) return h; } - return kept; -} - -ASTNode* hoist_common_first_decl(ASTNode* stmt, const char* name) { - ASTNode* arms[HOIST_MAX_ARMS]; - int na = branch_arms(stmt, arms, HOIST_MAX_ARMS); - ASTNode* any = NULL; + if (stmt->type == AST_MATCH_STATEMENT && !ast_match_covers(stmt)) return h; + /* Each arm's first direct binding of each name it binds. */ + StrMap* firsts = (StrMap*)calloc((size_t)na, sizeof(StrMap)); + if (!firsts) return h; for (int j = 0; j < na; j++) { - ASTNode* d = hoist_find_decl(arms[j], name); - if (!d) continue; - if (!any) any = d; - Type* t = hoist_decl_type(d); - if (t && t->kind != TYPE_VOID && t->kind != TYPE_UNKNOWN) return d; + strmap_init(&firsts[j]); + for (int k = 0; arms[j] && k < arms[j]->child_count; k++) { + ASTNode* d = arms[j]->children[k]; + if (d && d->type == AST_VARIABLE_DECLARATION && d->value && + !strmap_has(&firsts[j], d->value)) + strmap_put(&firsts[j], d->value, d); + } } - return any; + StrMap bound; + strmap_init(&bound); + pattern_names(stmt, &bound); + /* The names the first arm binds that every other arm binds too. */ + for (int i = 0; i < strmap_count(&firsts[0]); i++) { + const char* name = strmap_key_at(&firsts[0], i); + int everywhere = !strmap_has(&bound, name); + for (int j = 1; j < na && everywhere; j++) + if (!strmap_has(&firsts[j], name)) everywhere = 0; + if (!everywhere) continue; + ASTNode* d0 = (ASTNode*)strmap_value_at(&firsts[0], i); + int at = names_push(h, d0->value); + for (int j = 0; j < na; j++) + h->decls[(size_t)at * (size_t)na + (size_t)j] = (ASTNode*)strmap_get(&firsts[j], name); + } + strmap_free(&bound); + for (int j = 0; j < na; j++) strmap_free(&firsts[j]); + free(firsts); + return h; +} + +const HoistNames* hoist_common_hoists(ASTNode* stmt) { + if (!stmt) return NULL; + struct HoistNodeCache* c = node_cache(stmt); + if (!c->common) c->common = common_hoists_build(stmt); + return c->common; } /* The matches whose arms a hoist_loop_decls walk is inside, innermost last: diff --git a/compiler/analysis/hoist.h b/compiler/analysis/hoist.h index aae526a49..682e05567 100644 --- a/compiler/analysis/hoist.h +++ b/compiler/analysis/hoist.h @@ -10,37 +10,26 @@ * then or else block, a case of a switch (#2732), or the body of a match * arm (#2768): * - * - hoist_if_branch_candidates: a name first bound in an arm of one of a + * - hoist_if_branch_hoists: a name first bound in an arm of one of a * function body's top-level ifs, switches or matches is declared at * function scope, when a top-level statement, an if condition or a * switch's or match's subject reads it and no top-level statement * declares it. * - the names every arm of an if/else, of a switch with a default, or of * a match with a `_` arm or arms that cover its type, declares - * (hoist_common_decl_names) are declared before that statement, at any + * (hoist_common_hoists) are declared before that statement, at any * depth. * - a while body's locals, and those of the ifs, switches, matches and * loops nested in it (hoist_loop_decls), are declared before the while. * * Whether a name is already declared, or is a module-level `var`, depends on * the caller's own state and is checked by the caller, and so is, for - * hoist_common_decl_names, whether a match is a statement: one whose value + * hoist_common_hoists, whether a match is a statement: one whose value * is used (bound, returned, or yielded by an arm of another) keeps its arms' * names in the arms. */ #include "../ast.h" - -#define HOIST_MAX_NAMES 64 - -/* Append the names `block`'s direct AST_VARIABLE_DECLARATION children bind to - * `out` (deduplicated, at most `cap`); returns the new count. */ -int hoist_direct_decl_names(ASTNode* block, const char** out, int count, int cap); - -/* The first direct declaration of `name` in `block`, or NULL. */ -ASTNode* hoist_find_decl(ASTNode* block, const char* name); - -/* Does `node`'s subtree contain an identifier `name`? */ -int hoist_has_identifier_ref(ASTNode* node, const char* name); +#include "../aether_strmap.h" /* The type a hoisted declaration of `decl` is given: its own node_type, else * its initializer's. Borrowed; NULL when neither is known. */ @@ -82,25 +71,44 @@ void hoist_joins_typed(HoistJoins* j, const ASTNode* decl); Type* hoist_joins_get(HoistJoins* j, const char* name); void hoist_joins_free(HoistJoins* j); -/* The names hoist_if_branch_vars declares at the top of function body - * `body` (before the caller's already-declared / module-global checks). - * Returns the count written to `out` (at most `cap`). */ -int hoist_if_branch_candidates(ASTNode* body, const char** out, int cap); +/* The names one of the rules hoists, in the order the arms bind them, each + * with its bindings (#3083: any number of names, each found by name). Kept + * by this file until hoist_cache_reset; the caller does not free it. */ +typedef struct { + int count; + int cap; + const char** names; + ASTNode** decls; /* names[i]'s bindings: decls[i * per] .. decls[i * per + per - 1] */ + int per; + StrMap at; /* name -> index + 1 */ +} HoistNames; + +/* The index of `name` in `h`, or -1. */ +int hoist_names_find(const HoistNames* h, const char* name); + +/* The binding of names[i] whose type the hoisted variable takes: the first + * whose hoist_decl_type is known, else the first. */ +ASTNode* hoist_names_decl(const HoistNames* h, int i); -/* The declaration of `name` in the first arm of a top-level if, switch or - * match of `body` that declares it (the one whose type the hoisted variable - * takes). */ -ASTNode* hoist_if_branch_first_decl(ASTNode* body, const char* name); +/* The names hoist_if_branch_vars declares at the top of function body + * `body` (before the caller's already-declared / module-global checks), + * each with its first binding: the one in the first arm of a top-level if, + * switch or match of `body` that declares it. */ +const HoistNames* hoist_if_branch_hoists(ASTNode* body); /* The names every arm of `stmt` declares directly, when `stmt` is an if with * an else, a switch with a default, or a match with a `_` arm or arms that - * cover its type (so every path binds them); 0 for any other statement. - * Returns the count written to `out` (at most `cap`). */ -int hoist_common_decl_names(ASTNode* stmt, const char** out, int cap); + * cover its type (so every path binds them), each with its binding in every + * arm; none for any other statement. */ +const HoistNames* hoist_common_hoists(ASTNode* stmt); + +/* Forget every answer kept for a body or a statement: at the start of each + * checker and codegen run, and after codegen renames names. */ +void hoist_cache_reset(void); -/* The declaration of `name` in an arm of `stmt` whose type the hoisted - * variable takes: the first whose hoist_decl_type is known, else the first. */ -ASTNode* hoist_common_first_decl(ASTNode* stmt, const char* name); +/* `n` is being freed: forget what was kept for it, so a node allocated + * later at its address is asked afresh. */ +void hoist_node_freed(const ASTNode* n); /* Visit, in codegen's order, the declarations hoist_loop_vars declares * before a while whose body is `body`: the body's direct declarations and, diff --git a/compiler/analysis/typechecker.c b/compiler/analysis/typechecker.c index 2b19350d3..45bbfd20c 100644 --- a/compiler/analysis/typechecker.c +++ b/compiler/analysis/typechecker.c @@ -1728,6 +1728,34 @@ static int reject_string_char_write(ASTNode* target, SymbolTable* table, ASTNode return 1; } +/* #3104: a write to the length of a slice or an array: `xs.len = n`, + * `xs.len += n`, `xs.len++`. `.len` is a value computed from the slice + * (#1286): a slice's length is fixed when the slice is made, and an + * array's is a constant. The write reached the C compiler as a store to a + * non-lvalue ("lvalue required"). Returns 1 when it reported. */ +static int reject_length_write(ASTNode* target, SymbolTable* table, ASTNode* at) { + if (!target || target->type != AST_MEMBER_ACCESS || !target->value || + strcmp(target->value, "len") != 0 || target->child_count < 1 || !target->children[0]) + return 0; + Type* ot = infer_type(target->children[0], table); + int is_array = ot && ot->kind == TYPE_ARRAY; + if (ot) free_type(ot); + if (!is_array) return 0; + ASTNode* base = target->children[0]; + int named = base->type == AST_IDENTIFIER && base->value; + char msg[300]; + char hint[300]; + snprintf(msg, sizeof(msg), + "cannot write to the length of %s%s%s: a slice's length is fixed when the slice " + "is made, and an array's is a constant", + named ? "'" : "", named ? base->value : "a slice or an array", named ? "'" : ""); + snprintf(hint, sizeof(hint), + "for fewer elements, take a slice of the same ones: `%s[lo..hi]`", + named ? base->value : "xs"); + type_error_hint(msg, hint, at->line, at->column); + return 1; +} + static int is_integer_scalar(TypeKind kind) { return kind == TYPE_INT || kind == TYPE_INT64 || kind == TYPE_UINT64 || kind == TYPE_UINT32 || kind == TYPE_UINT16 || kind == TYPE_UINT8; @@ -2247,6 +2275,35 @@ static int is_const_array_element(ASTNode* elem, SymbolTable* table) { return is_const_expression(elem, table); } +/* #3113: does `e` name only literals and the program's `const`s, as this + * scope sees them? A local, a parameter or a module `var` of the name + * (one that hides a `const` included) is a value known only at run time. */ +static int index_names_only_consts(ASTNode* e, SymbolTable* table) { + if (!e) return 0; + switch (e->type) { + case AST_LITERAL: + return 1; + case AST_IDENTIFIER: { + Symbol* s = e->value ? lookup_symbol(table, e->value) : NULL; + return s && s->is_const; /* #2516: set for a `const`, never a `var` */ + } + case AST_UNARY_EXPRESSION: + case AST_BINARY_EXPRESSION: + for (int i = 0; i < e->child_count; i++) + if (!index_names_only_consts(e->children[i], table)) return 0; + return e->child_count > 0; + default: + return 0; + } +} + +/* #3113: the value of an index known when the program is compiled: a + * literal, a `const`, or arithmetic over them. */ +static int tc_index_constant(ASTNode* idx, SymbolTable* table, int64_t* out) { + return index_names_only_consts(idx, table) && + contract_eval_int64(idx, g_typecheck_program, out); +} + /* #2590: a struct literal whose every field is a compile-time constant or * such a literal itself. A module-level `var` takes one: it lowers to an * initializer list (`static T g = { .a = 0 };`), which C accepts for a @@ -3045,8 +3102,11 @@ static void reject_non_closure_callee(ASTNode* call, SymbolTable* table) { else snprintf(name, sizeof(name), "the first argument"); char msg[640]; - snprintf(msg, sizeof(msg), - "call() needs a closure, but %s has type %s", name, what); + if (annotation_has_marker(call->annotation, "value_callee")) /* #3099: `xs[0]()` */ + snprintf(msg, sizeof(msg), "the value called here is not a function: it has type %s", what); + else + snprintf(msg, sizeof(msg), + "call() needs a closure, but %s has type %s", name, what); const char* hint = t->kind == TYPE_TUPLE ? "a multi-value return bound to one name stays a tuple; destructure " @@ -3530,17 +3590,33 @@ static ASTNode* global_var_decl(ASTNode* child); /* The module-level `var` a bare `name = value` binding assigns: the * program's global of that name, when no local of it is in scope (#701). * NULL for any other binding. */ +static ASTNode* module_var_named(const char* name) { + ASTNode* program = aether_typecheck_program_node(); + for (int i = 0; program && i < program->child_count; i++) { + ASTNode* g = global_var_decl(program->children[i]); + if (g && strcmp(g->value, name) == 0) return g; + } + return NULL; +} + static ASTNode* rebound_global_var(ASTNode* stmt, SymbolTable* table) { if (stmt->type != AST_VARIABLE_DECLARATION || !stmt->type_inferred || !stmt->value) return NULL; for (SymbolTable* s = table; s && s->parent; s = s->parent) if (lookup_symbol_local(s, stmt->value)) return NULL; - ASTNode* program = aether_typecheck_program_node(); - for (int i = 0; program && i < program->child_count; i++) { - ASTNode* g = global_var_decl(program->children[i]); - if (g && strcmp(g->value, stmt->value) == 0) return g; + return module_var_named(stmt->value); +} + +/* #2734: does a C-style for loop's bare init (`i = 0`) name a variable bound + * where the loop is: a local bound before it (an actor's state field is + * one), or a module `var`? Then the init assigns it, as `i = 0` does + * anywhere. */ +static int for_init_names_bound(const char* name, SymbolTable* table) { + for (SymbolTable* t = table; t && t->parent; t = t->parent) { + Symbol* s = lookup_symbol_local(t, name); + if (s) return !s->hoist_pending; } - return NULL; + return module_var_named(name) != NULL; } static Type* fnptr_binding_slot(ASTNode* stmt, SymbolTable* table) { @@ -5003,41 +5079,44 @@ typedef struct { int used; } TrackedVar; +/* A function's tracked locals, found by name (#3083): `first` maps a name + * to its first entry + 1, and `next` chains the entries of one name, which a + * reference marks used together. Scanning them all for each reference was + * quadratic in a function's locals. */ +typedef struct { + TrackedVar* vars; + int count; + StrMap first; + int* next; +} TrackedVars; + +/* The entries of a name are marked together, so a marked first entry means + * every one is: a local rebound n times and read n times costs n, not n^2. */ +static void mark_used(TrackedVars* t, const char* name) { + intptr_t at = (intptr_t)strmap_get(&t->first, name); + if (!at || t->vars[at - 1].used) return; + for (; at; at = t->next[at - 1]) t->vars[at - 1].used = 1; +} + // Collect all AST_IDENTIFIER references in a subtree (excluding declarations) -static void collect_references(ASTNode* node, TrackedVar* vars, int var_count) { +static void collect_references(ASTNode* node, TrackedVars* t) { if (!node) return; // An identifier in expression position is a reference - if (node->type == AST_IDENTIFIER && node->value) { - for (int i = 0; i < var_count; i++) { - if (strcmp(vars[i].name, node->value) == 0) { - vars[i].used = 1; - } - } - } + if (node->type == AST_IDENTIFIER && node->value) mark_used(t, node->value); /* A lowered quote's body is the tree's now: each variable it captures * is read through the tree (docs/quote.md). */ if (node->type == AST_QUOTE) { QuoteInfo* q = quote_of(node); - for (int k = 0; q && k < q->ncaps; k++) { - for (int i = 0; i < var_count; i++) { - if (strcmp(vars[i].name, q->caps[k].name) == 0) vars[i].used = 1; - } - } + for (int k = 0; q && k < q->ncaps; k++) mark_used(t, q->caps[k].name); } // A call through a local is a use of that local: `g(p)` where `g` was // assigned via `f as fn(ptr) -> int` parses as AST_FUNCTION_CALL with // the callee name in node->value (no AST_IDENTIFIER child), so match // the call name against tracked variables too. - if (node->type == AST_FUNCTION_CALL && node->value) { - for (int i = 0; i < var_count; i++) { - if (strcmp(vars[i].name, node->value) == 0) { - vars[i].used = 1; - } - } - } + if (node->type == AST_FUNCTION_CALL && node->value) mark_used(t, node->value); // A call through a local's fn-pointer field, `f.get_text(p)`, uses `f` // too. The parser collapsed the callee to the dotted name `f.get_text` @@ -5047,12 +5126,10 @@ static void collect_references(ASTNode* node, TrackedVar* vars, int var_count) { if (node->type == AST_FUNCTION_CALL && node->value && node->annotation && strncmp(node->annotation, "fnfield_", 8) == 0) { const char* dot = strchr(node->value, '.'); - size_t rlen = dot ? (size_t)(dot - node->value) : 0; - for (int i = 0; i < var_count && rlen > 0; i++) { - if (strlen(vars[i].name) == rlen && - strncmp(vars[i].name, node->value, rlen) == 0) { - vars[i].used = 1; - } + char recv[256]; + if (dot && dot > node->value && dot - node->value < (ptrdiff_t)sizeof(recv)) { + snprintf(recv, sizeof(recv), "%.*s", (int)(dot - node->value), node->value); + mark_used(t, recv); } } @@ -5068,11 +5145,7 @@ static void collect_references(ASTNode* node, TrackedVar* vars, int var_count) { !(mt && mt->kind == TYPE_ARRAY) && !is_string_seq_ptr_type(mt)) { char len_name[256]; snprintf(len_name, sizeof(len_name), "%s_len", match_expr->value); - for (int i = 0; i < var_count; i++) { - if (strcmp(vars[i].name, len_name) == 0) { - vars[i].used = 1; - } - } + mark_used(t, len_name); } } @@ -5080,13 +5153,13 @@ static void collect_references(ASTNode* node, TrackedVar* vars, int var_count) { if (node->type == AST_VARIABLE_DECLARATION) { // Only walk children (RHS expression), not the declaration name for (int i = 0; i < node->child_count; i++) { - collect_references(node->children[i], vars, var_count); + collect_references(node->children[i], t); } return; } for (int i = 0; i < node->child_count; i++) { - collect_references(node->children[i], vars, var_count); + collect_references(node->children[i], t); } } @@ -5172,9 +5245,18 @@ static void check_unused_variables(ASTNode* body, const char** global_names, } // Collect references + TrackedVars t = { vars, var_count, {0}, (int*)calloc((size_t)var_count, sizeof(int)) }; + strmap_init(&t.first); + if (!t.next) { free(vars); return; } + for (int i = var_count - 1; i >= 0; i--) { + t.next[i] = (int)(intptr_t)strmap_get(&t.first, vars[i].name); + strmap_put(&t.first, vars[i].name, (void*)(intptr_t)(i + 1)); + } for (int i = 0; i < body->child_count; i++) { - collect_references(body->children[i], vars, var_count); + collect_references(body->children[i], &t); } + strmap_free(&t.first); + free(t.next); // Warn about unused for (int i = 0; i < var_count; i++) { @@ -5513,11 +5595,15 @@ static int is_borrowed_view_source(const ASTNode* e) { * is checked: each element is then checked as a value of the sum (one of its * variants), and codegen wraps each variant as the sum. Left to its own * elements, the literal was an array of its first one's type, `Circle`, and - * a `Square` beside it was refused. */ -static void sum_place_to_array_literal(ASTNode* value, Type* place) { + * a `Square` beside it was refused. So does a place whose elements are + * typed function pointers (`let fs: Op[2] = [twice, half]`), where a named + * function element is its address (#3098), and one of `ptr`s, where it is + * its C symbol. */ +static void place_to_array_literal(ASTNode* value, Type* place) { if (!value || value->type != AST_ARRAY_LITERAL || value->child_count == 0 || !place || place->kind != TYPE_ARRAY || !place->element_type || - place->element_type->kind != TYPE_SUM) + !(place->element_type->kind == TYPE_SUM || place->element_type->kind == TYPE_PTR || + (place->element_type->kind == TYPE_FUNCTION && place->element_type->is_fnptr))) return; set_node_type(value, create_array_type(clone_type(place->element_type), value->child_count)); } @@ -6577,6 +6663,7 @@ int typecheck_program(ASTNode* program) { g_typecheck_program = program; g_tc_ptr_to_closure = 0; tc_clauses_reset(); /* #2647: a program's clause sets, indexed afresh */ + hoist_cache_reset(); /* #3083: the hoisting answers, gathered afresh */ /* #2631: a module file checked on its own (it has an `exports(...)` * list, the test `program_is_module` below makes too) knows the @@ -7407,7 +7494,14 @@ int typecheck_program(ASTNode* program) { Symbol* full_sym = lookup_symbol(global_table, full_name); Symbol* short_sym = lookup_symbol_local(global_table, short_name); - if (full_sym && short_sym && short_sym->is_function && full_sym->type) { + /* #3086: only a symbol the import bound. One it left alone (the + * builtin `free`, which std.string's glob names too) was given the + * import's type and declaration here, so a module's body, where a + * bare `free` is the builtin, was checked against `string.free`. A + * program's call of the name is resolved through the alias + * (typecheck_function_call_inner). */ + if (full_sym && short_sym && short_sym->import_bound && short_sym->is_function && + full_sym->type) { if (short_sym->type) free_type(short_sym->type); short_sym->type = clone_type(full_sym->type); short_sym->node = full_sym->node; @@ -7811,6 +7905,14 @@ int typecheck_actor_definition(ASTNode* actor, SymbolTable* table) { ASTNode* child = actor->children[i]; if (child->type == AST_STATE_DECLARATION) { + /* The generated actor keeps the runtime's actor fields in a + * member of this name, beside the states (#3094): a state may be + * named like any of those fields, but not like the member. */ + if (child->value && strcmp(child->value, "_aether_base") == 0) { + type_error_hint("a state cannot be named '_aether_base'", + "the generated actor keeps the runtime's actor fields under that name; rename the state", + child->line, child->column); + } int checked = 0; if ((!child->node_type || child->node_type->kind == TYPE_UNKNOWN) && child->child_count > 0 && child->children[0]) { @@ -7958,25 +8060,7 @@ static int scoped_mentions(ASTNode* node, const char* name) { return 0; } -/* Curated container-insert builtins: handing a @scoped value to one of these - * stores it beyond the block. Same curation posture as codegen's - * is_nonstoring_builtin allowlist. Names are the as-written qualified - * spellings (the AST stores callees in dotted form). */ -static int scoped_store_call(const char* fn) { - if (!fn) return 0; - /* The std calls that keep their argument in a container. #3001: the - * list named `list.push`, `map.set` and `seq.cons`, which std does not - * have, and missed the sequence cons and the raw and owning forms. */ - /* #3034: and std.collections' unqualified `list_add` and `map_put`, - * which tinyweb stores its route handlers through. */ - static const char* sinks[] = { - "list.add", "list.add_raw", "list.add_string_owned", "list.set", - "map.put", "map.put_raw", "map.put_string_owned", - "set.add", "set.try_add", "string.seq_cons", "list_add", "map_put", NULL }; - for (int i = 0; sinks[i]; i++) - if (strcmp(fn, sinks[i]) == 0) return 1; - return 0; -} +static int call_arg_mode(ASTNode* program, ASTNode* call, int i); /* defined below */ /* Reason string if `node` escapes `name` AT THIS NODE, else NULL. */ static const char* scoped_escape_reason(ASTNode* node, const char* name) { @@ -8006,14 +8090,18 @@ static const char* scoped_escape_reason(ASTNode* node, const char* name) { return "it is captured by a closure that can outlive the block"; if (node->type == AST_QUOTE && scoped_mentions(node, name)) return "it is captured by a quote, whose tree can outlive the block"; - if (node->type == AST_FUNCTION_CALL && node->value && - scoped_store_call(node->value)) { - int first = strcmp(node->value, "call") == 0 ? 1 : 0; - for (int i = first; i < node->child_count; i++) { + /* #3086: given to a parameter that keeps it, as its callee declares: + * an Aether function's `@keep` one, an extern's `@retain` one (std's + * list and map stores), or one of a closure type that says so. #3001 + * named the stores instead, and #3079 knew them by the function a call + * reaches; a parameter that copies its argument (a set's item, a map's + * key) keeps nothing. */ + if (node->type == AST_FUNCTION_CALL && node->value) { + for (int i = 0; i < node->child_count; i++) { ASTNode* a = node->children[i]; - if (a && a->type == AST_IDENTIFIER && a->value && - strcmp(a->value, name) == 0) - return "it is inserted into a container"; + if (a && a->type == AST_IDENTIFIER && a->value && strcmp(a->value, name) == 0 && + call_arg_mode(g_typecheck_program, node, i) == FN_MODE_KEEP) + return "it is passed to a parameter that keeps it"; } } return NULL; @@ -8777,6 +8865,8 @@ static int keep_set_other_param(const KeepSet* s, const char* param) { static int is_bare_ptr_type(Type* t); +static int type_holds_borrowable(ASTNode* program, Type* t, int depth); + /* #3034: does the struct `name` hold, by value, a bare `ptr` or a closure * (in a field, or in a struct field held by value)? */ static int struct_holds_borrowable(ASTNode* program, const char* name, int depth) { @@ -8788,17 +8878,30 @@ static int struct_holds_borrowable(ASTNode* program, const char* name, int depth for (int f = 0; f < sd->child_count; f++) { ASTNode* fd = sd->children[f]; Type* ft = fd && fd->type == AST_STRUCT_FIELD ? fd->node_type : NULL; - if (!ft) continue; - if (is_bare_ptr_type(ft) || (ft->kind == TYPE_FUNCTION && !ft->is_fnptr)) return 1; - if (ft->kind == TYPE_STRUCT && ft->struct_name && - struct_holds_borrowable(program, ft->struct_name, depth + 1)) - return 1; + if (ft && type_holds_borrowable(program, ft, depth + 1)) return 1; } return 0; } return 0; } +/* #3034: does a value of type `t` hold, by value, a bare `ptr` or a + * closure: it is one, or a struct with one in a field (or in a struct + * field held by value). #3079: or an optional of one (a `fn?` field), or + * a sum one of whose variants holds one. */ +static int type_holds_borrowable(ASTNode* program, Type* t, int depth) { + if (!t || depth > 8) return 0; + if (is_bare_ptr_type(t) || (t->kind == TYPE_FUNCTION && !t->is_fnptr)) return 1; + if (t->kind == TYPE_OPTIONAL) return type_holds_borrowable(program, t->element_type, depth + 1); + if (t->kind == TYPE_STRUCT && t->struct_name) + return struct_holds_borrowable(program, t->struct_name, depth + 1); + if (t->kind == TYPE_SUM) { + for (int i = 0; t->tuple_types && i < t->tuple_count; i++) + if (type_holds_borrowable(program, t->tuple_types[i], depth + 1)) return 1; + } + return 0; +} + /* #3034: what a parameter of type `t` is borrowed as, besides a bare * `ptr`: a `fn` (a closure, `*is_fn`) or a struct taken by value that holds * a pointer or a closure, spelled as the type is; NULL for anything else (a @@ -8806,12 +8909,16 @@ static int struct_holds_borrowable(ASTNode* program, const char* name, int depth static const char* borrowed_param_kind(ASTNode* program, Type* t, int* is_fn) { *is_fn = 0; if (!t) return NULL; - if (t->kind == TYPE_FUNCTION && !t->is_fnptr) { + /* #3079: a `fn?` is borrowed as the closure it may hold. */ + Type* inner = t->kind == TYPE_OPTIONAL ? t->element_type : t; + if (inner && inner->kind == TYPE_FUNCTION && !inner->is_fnptr) { *is_fn = 1; return aether_intern(type_spelled(t)); } - if (t->kind == TYPE_STRUCT && t->struct_name && !t->distinct_name && - struct_holds_borrowable(program, t->struct_name, 0)) + /* A struct taken by value that holds a pointer or a closure, and #3079 + * a sum one of whose variants does (or an optional of either). */ + if (inner && (inner->kind == TYPE_STRUCT || inner->kind == TYPE_SUM) && + !inner->distinct_name && type_holds_borrowable(program, inner, 0)) return aether_intern(type_spelled(t)); return NULL; } @@ -8915,11 +9022,15 @@ static ASTNode* call_extern_decl(ASTNode* program, const char* name) { } /* The declaration a call is checked against: an Aether function, or an - * extern. NULL for a call through a value or a builtin. */ + * extern. NULL for a call through a value or a builtin. #3086: an extern a + * module keeps to itself (std.list's `list_add_raw`, a bare `extern`) is + * not merged into the program, so it is the declaration the call was typed + * against (callee_extern), the module's own. */ static ASTNode* call_callee_decl(ASTNode* program, ASTNode* call) { if (!program || !call || !call->value) return NULL; ASTNode* def = effect_resolve_callee(program, call->value); - return def ? def : call_extern_decl(program, call->value); + if (def) return def; + return call->callee_extern ? call->callee_extern : call_extern_decl(program, call->value); } /* The parameter of `decl` that argument `i` of `call` fills, or NULL. */ @@ -9300,6 +9411,7 @@ static void bind_pattern_holders(KeepSet* s, ASTNode* pat, Carried c, Type* t, } } +static int call_keeps_in_local_holder(ASTNode* program, ASTNode* call, int i, const KeepSet* s); static void check_ptr_keeps_in(ASTNode* program, ASTNode* n, KeepSet* s, const char** globals, int nglobals, int in_closure); @@ -9905,21 +10017,16 @@ static void check_ptr_keeps_in(ASTNode* program, ASTNode* n, KeepSet* s, report_borrowed_keep(s, f, c, "sent in a message"); } } + /* #3086: a parameter that keeps what it is given, as its callee + * declares it (call_arg_mode): std's list and map stores declare theirs + * `@retain` at the C boundary, so a borrowed `ptr` put in a container is + * E0902 as one stored in a module `var` is, not only a closure or a + * struct that holds one (#3001's holders-only rule). */ if (n->type == AST_FUNCTION_CALL && n->value) { - int container = scoped_store_call(n->value); for (int i = 0; i < n->child_count; i++) { if (!(c = carries_borrowed_ptr(n->children[i], s)).n) continue; - if (call_keeps_arg(program, n, i)) { + if (call_keeps_arg(program, n, i) && !call_keeps_in_local_holder(program, n, i, s)) report_borrowed_keep(s, n->children[i], c, "passed to a parameter that keeps it"); - } else if (container) { - /* What holds a parameter, and #3034 a `fn` or struct - * parameter itself (a closure is boxed into the slot). */ - Carried kept = { 0 }; - for (int k = 0; k < c.n; k++) - if (c.vias[k] || keep_set_other_param(s, c.params[k]) >= 0) - carried_add_at(&kept, c.params[k], c.vias[k], c.parts[k]); - if (kept.n) report_borrowed_keep(s, n->children[i], kept, "stored in a container"); - } } } for (int i = 0; i < n->child_count; i++) @@ -10082,6 +10189,70 @@ static int c_name_outlives(const KeepCtx* c, const char* name, int depth) { return s.escaped; } +/* #3086: is `e` a holder (`@holds`) whose memory ends with the function: a + * local bound only from calls to `@fresh` externs (a container the function + * makes, std's list and map constructors) or `null`, and that goes nowhere + * a borrowed `ptr` parameter would escape to (the E0902 facts, asked as + * c_name_outlives asks them). Decided by those declarations alone: a + * parameter, a module `var`, a field or anything else is taken to outlive + * it. */ +typedef struct { int found; int other; } HolderBindings; + +static void scan_holder_bindings(ASTNode* program, ASTNode* n, const char* name, + HolderBindings* b) { + if (!n || b->other) return; + if (n->type == AST_FUNCTION_DEFINITION || n->type == AST_BUILDER_FUNCTION) return; + ASTNode* v = NULL; + if (n->type == AST_VARIABLE_DECLARATION && n->value && !strcmp(n->value, name) && + n->child_count > 0) + v = n->children[n->child_count - 1]; + else if (n->type == AST_BINARY_EXPRESSION && n->value && !strcmp(n->value, "=") && + n->child_count == 2 && n->children[0] && n->children[0]->type == AST_IDENTIFIER && + n->children[0]->value && !strcmp(n->children[0]->value, name)) + v = n->children[1]; + if (v) { + b->found = 1; + v = strip_ptr_casts(v); + ASTNode* decl = v && v->type == AST_FUNCTION_CALL ? call_callee_decl(program, v) : NULL; + int fresh = v && (v->type == AST_NULL_LITERAL || + (decl && decl->type == AST_EXTERN_FUNCTION && + annotation_has_marker(decl->annotation, "fresh_return"))); + if (!fresh) b->other = 1; + } + for (int i = 0; i < n->child_count; i++) scan_holder_bindings(program, n->children[i], name, b); +} + +static int holder_outlives(const KeepCtx* c, ASTNode* e, int depth) { + e = strip_ptr_casts(e); + if (!c || depth > 8 || !e || e->type != AST_IDENTIFIER || !e->value) return 1; + const char* name = e->value; + for (int g = 0; g < c->nglobals; g++) + if (!strcmp(c->globals[g], name)) return 1; + for (int i = 0; c->fn && i < c->fn->child_count && c->fn->children[i] != c->body; i++) { + ASTNode* p = c->fn->children[i]; + if (p && p->value && !strcmp(p->value, name)) return 1; + } + HolderBindings b = { 0, 0 }; + scan_holder_bindings(c->program, c->body, name, &b); + if (!b.found || b.other) return 1; + return c_name_outlives(c, name, depth); +} + +/* #3086: does `call` keep argument `i` only in a holder that ends with the + * function? An extern names the argument that holds what its `@retain` + * parameters are given with `@holds` (std's list and map stores); without + * one, a keep is taken to outlive the call. */ +static int call_keeps_in_local_holder(ASTNode* program, ASTNode* call, int i, const KeepSet* s) { + if (!s || !s->ctx || call_value_type(call)) return 0; + ASTNode* decl = call_callee_decl(program, call); + if (!decl || decl->type != AST_EXTERN_FUNCTION) return 0; + for (int h = 0; h < decl->child_count && h < call->child_count; h++) { + if (h == i || !param_has_mode(decl->children[h], "holder_param")) continue; + return !holder_outlives(s->ctx, call->children[h], s->depth + 1); + } + return 0; +} + /* Does the memory the store target `lhs` writes (`p.f`, `p[i]`, `(p as * T[])[i]`) outlive the function? That of the pointer it is reached * through: a local allocation that does not escape ends with it. */ @@ -11591,24 +11762,18 @@ static int report_unheld_binding(ASTNode* stmt, Type* w, Type* t, ASTNode* init) /* Function (or main) entry: hoist_if_branch_vars's names. */ static void declare_if_branch_hoists(ASTNode* body, SymbolTable* table) { - const char* names[HOIST_MAX_NAMES]; - int n = hoist_if_branch_candidates(body, names, HOIST_MAX_NAMES); - for (int i = 0; i < n; i++) { - declare_hoisted_local(table, names[i], - hoist_decl_type(hoist_if_branch_first_decl(body, names[i]))); - } + const HoistNames* h = hoist_if_branch_hoists(body); + for (int i = 0; h && i < h->count; i++) + declare_hoisted_local(table, h->names[i], hoist_decl_type(hoist_names_decl(h, i))); } /* Before an if/else, or a switch with a default (#2732): hoist_common_vars's * names (every arm declares them), typed from the first arm that types * them. */ static void declare_common_hoists(ASTNode* stmt, SymbolTable* table) { - const char* names[HOIST_MAX_NAMES]; - int n = hoist_common_decl_names(stmt, names, HOIST_MAX_NAMES); - for (int i = 0; i < n; i++) { - declare_hoisted_local(table, names[i], - hoist_decl_type(hoist_common_first_decl(stmt, names[i]))); - } + const HoistNames* h = hoist_common_hoists(stmt); + for (int i = 0; h && i < h->count; i++) + declare_hoisted_local(table, h->names[i], hoist_decl_type(hoist_names_decl(h, i))); } /* Before a while: hoist_loop_vars's names. */ @@ -12879,7 +13044,7 @@ int typecheck_statement(ASTNode* stmt, SymbolTable* table) { ex->type->kind != TYPE_UNKNOWN) slot = ex->type; } - sum_place_to_array_literal(init, slot); + place_to_array_literal(init, slot); } // Match-as-expression: typecheck as statement, then use its type if (init->type == AST_MATCH_STATEMENT) { @@ -13678,7 +13843,8 @@ int typecheck_statement(ASTNode* stmt, SymbolTable* table) { ASTNode* right = stmt->children[1]; if (reject_const_element_write(left, table, stmt) || /* #2516 */ reject_string_char_write(left, table, stmt) || /* #3022 */ - reject_readonly_view_write(left, stmt)) /* #3025 */ + reject_readonly_view_write(left, stmt) || /* #3025 */ + reject_length_write(left, table, stmt)) /* #3104 */ return 0; Symbol* symbol = lookup_symbol(table, left->value); @@ -13698,7 +13864,7 @@ int typecheck_statement(ASTNode* stmt, SymbolTable* table) { // #1044 implicit enum selector: `d = Blue` where `d` has an enum // type resolves the bare member against that enum. coerce_bare_enum_member(right, symbol->type, table); - sum_place_to_array_literal(right, symbol->type); /* #3071 */ + place_to_array_literal(right, symbol->type); /* #3071 */ Type* right_type = infer_type(right, table); /* #1286: a `T[N]` array or `null` into a `T[]` binding. */ @@ -13935,7 +14101,15 @@ int typecheck_statement(ASTNode* stmt, SymbolTable* table) { // Type check init (child 0) if (stmt->child_count > 0 && stmt->children[0]) { - typecheck_statement(stmt->children[0], loop_table); + /* #2734: a bare init of a name not bound here declares the + * loop's own variable, as a typed init or a range-for's + * does; one of a bound name stays the binding that assigns + * it. Codegen reads which from `type_inferred`. */ + ASTNode* init = stmt->children[0]; + if (init->type == AST_VARIABLE_DECLARATION && init->type_inferred && + init->value && !for_init_names_bound(init->value, table)) + init->type_inferred = 0; + typecheck_statement(init, loop_table); } // Type check condition (child 1). #3042 review: as for `if` and @@ -14896,13 +15070,27 @@ int typecheck_statement(ASTNode* stmt, SymbolTable* table) { /* #3071: an array literal into a written array-of-sum result. */ if (stmt->child_count == 1 && g_tc_return_owner && g_tc_return_owner->type != AST_CLOSURE && !g_tc_return_owner->type_inferred) - sum_place_to_array_literal(stmt->children[i], g_tc_return_type); + place_to_array_literal(stmt->children[i], g_tc_return_type); if (stmt->children[i] && stmt->children[i]->type == AST_MATCH_STATEMENT) { tc_push_value_match(stmt->children[i]); /* #2768 */ typecheck_node(stmt->children[i], table); tc_pop_value_match(); - } else + } else { typecheck_expression(stmt->children[i], table); + /* `return held` from a `-> fn` function: the named function + * is the value returned, so codegen emits its adapter, as + * for an argument. Unmarked, the return was wrapped in a + * call to an adapter nothing had registered: undeclared + * in C. A typed function pointer result, or part of a + * tuple result, takes the function's address instead + * (#2636), and a `ptr` result its C symbol. */ + Type* slot = g_tc_return_type; + if (slot && slot->kind == TYPE_TUPLE && stmt->child_count > 1) + slot = i < slot->tuple_count ? slot->tuple_types[i] : NULL; + if (!(slot && (slot->kind == TYPE_PTR || + (slot->kind == TYPE_FUNCTION && slot->is_fnptr)))) + function_value_annotate(stmt->children[i], table); + } } /* #2697, #2733: a return of something with no value, a call of * a function that returns nothing or a `match` whose arms give @@ -15729,8 +15917,30 @@ int typecheck_expression(ASTNode* expr, SymbolTable* table) { } } // Type check all array elements + Type* place = expr->node_type; + Type* place_elem = place && place->kind == TYPE_ARRAY ? place->element_type : NULL; + Type* fnptr_elem = place_elem && place_elem->kind == TYPE_FUNCTION && + place_elem->is_fnptr ? place_elem : NULL; for (int i = 0; i < expr->child_count; i++) { - typecheck_expression(expr->children[i], table); + ASTNode* el = expr->children[i]; + typecheck_expression(el, table); + /* A named function among the elements is a function value, + * as it is in an argument or a local's value: `[held, null]` + * is an array of closures. Left a bare name, the element + * had no type, the literal defaulted to an int array, and C + * got `int fs[2] = {held, NULL}` (#3092, #3099). In an + * array the place types as typed function pointers (`let + * fs: Op[2] = [twice, half]`), it is the function's address, + * as in a local of that type (#2623); in an array of `ptr`s, + * its C symbol. */ + if (fnptr_elem && fn_name_matches_signature(table, el, fnptr_elem)) { + refuse_moded_fn_in_fnptr_slot(table, el, fnptr_elem); + set_node_type(el, clone_type(fnptr_elem)); + if (el->annotation) free(el->annotation); + el->annotation = strdup("fn_addr"); + } else if (!(place_elem && place_elem->kind == TYPE_PTR)) { + function_value_annotate(el, table); + } if (refuse_valueless(expr->children[i], table, "store")) return 0; } /* The element type was chosen by the early inference pass from @@ -16150,6 +16360,27 @@ int typecheck_expression(ASTNode* expr, SymbolTable* table) { } if (idx_type) free_type(idx_type); + /* #3113: an index into a `T[N]` is in `0 ..< N`, checked + * when the program runs; one known when it is compiled and + * outside that is refused here. */ + int64_t civ = 0; + if (type_is_sized_array(arr_type) && !arr_type->index_enum_name && + tc_index_constant(expr->children[1], table, &civ) && + (civ < 0 || civ >= arr_type->array_size)) { + char error_msg[320]; + char tn[160]; + snprintf(error_msg, sizeof(error_msg), + "array index %lld is out of range for '%s', whose indexes are " + "0 through %d", + (long long)civ, ret_type_spell(arr_type, tn, sizeof(tn)), + arr_type->array_size - 1); + type_error(error_msg, expr->children[1]->line ? expr->children[1]->line + : expr->line, + expr->children[1]->line ? expr->children[1]->column + : expr->column); + return 0; + } + // Propagate element type from the array expression. The // array's element type is authoritative: the early inference // pass may have stamped this access from the array's FIRST @@ -16207,7 +16438,7 @@ int typecheck_expression(ASTNode* expr, SymbolTable* table) { ASTNode* fd = sdef->children[f]; if (fd && fd->type == AST_STRUCT_FIELD && fd->value && strcmp(fd->value, field_init->value) == 0) - sum_place_to_array_literal(field_init->children[0], fd->node_type); + place_to_array_literal(field_init->children[0], fd->node_type); } typecheck_expression(field_init->children[0], table); if (refuse_valueless(field_init->children[0], table, "store")) { @@ -16285,6 +16516,19 @@ int typecheck_expression(ASTNode* expr, SymbolTable* table) { set_node_type(expr, create_type(TYPE_INT)); return 1; } + if (lt && lt->kind == TYPE_ARRAY) { + /* #3092: `.len` is an array's one member (a slice's and a + * fixed-size array's alike), so any other name is a + * mistake to name here; left alone it reached the C as a + * member of a pointer or an array (`arr.length`). */ + char msg[256]; + snprintf(msg, sizeof(msg), + "'%s' is not a member of an array: an array's length is `.len`, its only member", + expr->value); + free_type(lt); + type_error(msg, expr->line, expr->column); + return 0; + } if (lt && is_lane_type(lt->kind)) { int idx = lane_accessor_index(lt->kind, expr->value); TypeKind elem = lane_scalar_kind(lt->kind); @@ -16750,7 +16994,8 @@ static int reject_step_operand(ASTNode* expr, SymbolTable* table, const char* wr if (!t) return 0; if (reject_const_element_write(t, table, expr) || /* #2516 */ reject_string_char_write(t, table, expr) || - reject_readonly_view_write(t, expr)) /* #3025 */ + reject_readonly_view_write(t, expr) || /* #3025 */ + reject_length_write(t, table, expr)) /* #3104 */ return 1; char msg[512]; char hint[640]; @@ -16935,7 +17180,7 @@ int typecheck_binary_expression(ASTNode* expr, SymbolTable* table) { * [c, q]`) takes the place's type before it is checked. */ if (expr->value && strcmp(expr->value, "=") == 0 && right->type == AST_ARRAY_LITERAL) { Type* lt = infer_type(left, table); - sum_place_to_array_literal(right, lt); + place_to_array_literal(right, lt); if (lt) free_type(lt); } typecheck_expression(right, table); @@ -17132,7 +17377,8 @@ int typecheck_binary_expression(ASTNode* expr, SymbolTable* table) { if (operator == TOKEN_ASSIGN) { if (reject_const_element_write(left, table, expr) || /* #2516 */ reject_string_char_write(left, table, expr) || /* #3022 */ - reject_readonly_view_write(left, expr)) { /* #3025 */ + reject_readonly_view_write(left, expr) || /* #3025 */ + reject_length_write(left, table, expr)) { /* #3104 */ free_type(left_type); free_type(right_type); return 0; @@ -19039,6 +19285,28 @@ static int typecheck_function_call_inner(ASTNode* call, SymbolTable* table) { if (!call || call->type != AST_FUNCTION_CALL) return 0; note_short_name(call, table); /* #2976: as written, before any rewrite */ + /* #3116: `free(x)` in a module that defines its own `free`, which the + * merger renamed to that function. A slice argument is freed by the + * builtin, the one release of a `make` buffer: the module's function can + * only read the slice's pointer as its own object, and named alike it + * left the builtin out of reach in the module. Unless the function itself + * takes a slice, which is then its own business. */ + if (call->annotation && strcmp(call->annotation, "own_free") == 0) { + free(call->annotation); + call->annotation = NULL; + Type* at = call->child_count == 1 ? infer_type(call->children[0], table) : NULL; + int slice_arg = type_is_slice(at); + if (at) free_type(at); + if (slice_arg) { + Symbol* fs = lookup_symbol(table, call->value); + ASTNode* p0 = (fs && is_definition(fs->node)) ? def_param_at(fs->node, 0) : NULL; + if (!(p0 && type_is_slice(p0->node_type))) { + free(call->value); + call->value = strdup("free"); + } + } + } + /* #928 UFCS, shape (a): the parser tagged this as a method-call on a * non-identifier receiver (e.g. `expect(5).to_eq(...)`) and stashed the * receiver subtree at children[0]. Rewrite to `method(recv, args)` up @@ -19051,6 +19319,26 @@ static int typecheck_function_call_inner(ASTNode* call, SymbolTable* table) { * error (the bare method name is already in call->value). */ free(call->annotation); call->annotation = NULL; + /* #3099: a function held in a field of a receiver that is not a + * name (`hs[0].cb()`, `make_h().cb(2)`) is called as one held in + * a named receiver's field is (#2653): `call(recv.cb, ...)`. */ + ASTNode* recv = call->child_count > 0 ? call->children[0] : NULL; + if (recv && call->value && !strchr(call->value, '.')) { + ASTNode* fn_ref = create_ast_node(AST_MEMBER_ACCESS, call->value, call->line, call->column); + add_child(fn_ref, recv); + Type* ft = infer_type(fn_ref, table); + int is_fn = ft && ft->kind == TYPE_FUNCTION; + if (ft) free_type(ft); + if (is_fn) { + call->children[0] = fn_ref; + free(call->value); + call->value = strdup("call"); + return typecheck_function_call(call, table); + } + fn_ref->children[0] = NULL; /* the receiver stays the call's */ + fn_ref->child_count = 0; + free_ast_node(fn_ref); + } } } @@ -19101,6 +19389,16 @@ static int typecheck_function_call_inner(ASTNode* call, SymbolTable* table) { // Use qualified lookup to handle namespaced calls like string.new -> string_new Symbol* symbol = lookup_qualified_symbol(table, call->value); + /* #3086: a bare name the program's import binds is the import's in the + * program (the alias below rewrites the call to it), also where the + * import left a builtin of the name in place (std.string's glob and + * `free`): it is checked against the import's declaration. Not in a + * module's merged body, whose bare names are its own. */ + if (symbol && call->value && !strchr(call->value, '.') && !table->merged_from && + !symbol->import_bound && find_import_alias(call->value)) { + Symbol* aliased = lookup_qualified_symbol(table, find_import_alias(call->value)); + if (aliased && aliased->is_function) symbol = aliased; + } /* #924 re-export: a `hub.fn(...)` call where hub re-exports fn from * another module resolves (via lookup_qualified_symbol) to the defining @@ -19410,6 +19708,10 @@ static int typecheck_function_call_inner(ASTNode* call, SymbolTable* table) { return 0; } + /* #3086: the extern the call resolves to, for call_callee_decl. */ + if (symbol->node && symbol->node->type == AST_EXTERN_FUNCTION) + call->callee_extern = symbol->node; + if (!check_quote_callee(call, symbol)) return 0; if (!bind_named_arguments(call, symbol)) return 0; @@ -19846,7 +20148,7 @@ static int typecheck_function_call_inner(ASTNode* call, SymbolTable* table) { if (p->node_type && p->node_type->kind == TYPE_ENUM) coerce_bare_enum_member(call->children[aslot], p->node_type, table); adopt_closure_signature(call->children[aslot], p->node_type); - sum_place_to_array_literal(call->children[aslot], p->node_type); /* #3071 */ + place_to_array_literal(call->children[aslot], p->node_type); /* #3071 */ } } diff --git a/compiler/ast.c b/compiler/ast.c index 0a6d97f5d..9727a7480 100644 --- a/compiler/ast.c +++ b/compiler/ast.c @@ -3,6 +3,7 @@ #include #include #include "ast.h" +#include "analysis/hoist.h" #include #include "../std/mem/aether_grow.h" @@ -1060,6 +1061,7 @@ ASTNode* create_ast_node(ASTNodeType type, const char* value, int line, int colu node->origin_module = NULL; node->arg_order = NULL; node->arg_order_count = 0; + node->callee_extern = NULL; return node; } @@ -1134,6 +1136,7 @@ ASTNode* clone_ast_node(ASTNode* node) { memcpy(clone->arg_order, node->arg_order, sizeof(int) * (size_t)node->arg_order_count); clone->arg_order_count = node->arg_order_count; } + clone->callee_extern = node->callee_extern; if (node->value_len > 0) ast_set_literal_bytes(clone, node->value, node->value_len); /* #2520 */ /* Slot for slot: an empty one stays where it is, as the parser left it. */ @@ -1178,6 +1181,7 @@ void free_ast_node(ASTNode* node) { free(node->children); } + hoist_node_freed(node); free(node); } @@ -1432,7 +1436,14 @@ void clause_set_join(ASTNode* const* nodes, int count, ClauseJoin* out) { join_arm = join_arm_after(join_arm, arm); /* Beside a written type, an inferred clause fits it (their join is * it, and one with no join does not); a second written type is the - * same one or none. */ + * same one or none. #3082: a value the written type holds fits it + * (ast_number_holds), as a local's written type holds a binding + * (#3070): `g(0) -> 5` beside `-> uint8`, which the join of an int + * and a uint8 (an int) refused. */ + if (written && cl->type_inferred && ast_number_holds(join, t, arm)) { + if (k) free_type(k); + k = clone_type(join); + } int unfit = written && (!k || !types_equal(k, join) || (!cl->type_inferred && !types_equal(t, join))); if (!k || unfit) { diff --git a/compiler/ast.h b/compiler/ast.h index 26c35735d..2cb0e9202 100644 --- a/compiler/ast.h +++ b/compiler/ast.h @@ -651,6 +651,12 @@ typedef struct ASTNode { * on every other node. */ int* arg_order; int arg_order_count; + + /* #3086: on a call the checker typed against an extern, that extern's + * declaration: a module's own extern is not merged into the program, + * so this is how what the call keeps is read from its declaration. Not + * owned; NULL on every other node. */ + struct ASTNode* callee_extern; } ASTNode; // Type functions diff --git a/compiler/codegen/codegen.c b/compiler/codegen/codegen.c index b130fe363..7965f09da 100644 --- a/compiler/codegen/codegen.c +++ b/compiler/codegen/codegen.c @@ -12,6 +12,7 @@ #include "../aether_error.h" #include "../analysis/actor_reply.h" #include "../analysis/sandbox_trust.h" +#include "../analysis/hoist.h" /* The AetherString header the emitted C inspects, from the definition the * runtime uses (std/string/aether_string_abi.h): its magic's bytes in @@ -444,6 +445,37 @@ int contains_send_expression(ASTNode* node) { return 0; } +/* #3117: can `node`, a loop, keep its sends in a batch (delivered at the + * loop's end, or 256 at a time)? Only when nothing in it can wait between two + * sends or look at what a send did: it calls nothing, prints nothing, asks + * nothing and reads no actor's state. A send, arithmetic, an assignment, a + * branch and an inner loop are fine: the counting and fork-join shape the + * batch exists for. A `return`, or a labeled `break` or `continue` (which can + * leave for an enclosing loop), would leave the batch unflushed, and a + * closure made in the loop runs outside it, so each keeps the loop out too. */ +int loop_sends_without_waiting(ASTNode* node) { + if (!node) return 1; + switch (node->type) { + case AST_FUNCTION_CALL: case AST_PRINT_STATEMENT: case AST_SEND_ASK: + case AST_CLOSURE: case AST_RETURN_STATEMENT: + return 0; + case AST_BREAK_STATEMENT: case AST_CONTINUE_STATEMENT: + if (node->value) return 0; + break; + case AST_MEMBER_ACCESS: + if (node->child_count > 0 && node->children[0] && node->children[0]->node_type && + node->children[0]->node_type->kind == TYPE_ACTOR_REF) + return 0; + break; + default: + break; + } + for (int i = 0; i < node->child_count; i++) { + if (!loop_sends_without_waiting(node->children[i])) return 0; + } + return 1; +} + // Tree-shake of merged-but-unused stdlib functions runs around typecheck // (module_mark_unreachable before it, module_sweep_unreachable after it, // #2613); see aether_module.c. Codegen no longer needs a separate pass; @@ -903,6 +935,18 @@ void code_generator_release(CodeGenerator* gen) { gen->closures = NULL; gen->closure_count = 0; gen->closure_capacity = 0; + /* Which closure literal a variable holds (closure_var_bind): a + * strdup'd scope and name per entry. Nothing freed it, so every + * compile of a program that binds a closure to a variable leaked + * the table. */ + for (int i = 0; i < gen->closure_var_count; i++) { + free(gen->closure_var_map[i].scope); + free(gen->closure_var_map[i].var_name); + } + free(gen->closure_var_map); + gen->closure_var_map = NULL; + gen->closure_var_count = 0; + gen->closure_var_capacity = 0; for (int i = 0; i < gen->promoted_func_count; i++) { for (int j = 0; j < gen->promoted_funcs[i].count; j++) { free(gen->promoted_funcs[i].names[j]); @@ -948,6 +992,8 @@ void code_generator_release(CodeGenerator* gen) { } free(gen->declared_vars); } + strmap_free(&gen->declared_index); + strmap_free(&gen->reads_index); if (gen->module_global_vars) { for (int i = 0; i < gen->module_global_var_count; i++) { free(gen->module_global_vars[i]); @@ -997,12 +1043,7 @@ void free_code_generator(CodeGenerator* gen) { // Helper: check if variable was already declared in current function int is_var_declared(CodeGenerator* gen, const char* var_name) { - for (int i = 0; i < gen->declared_var_count; i++) { - if (strcmp(gen->declared_vars[i], var_name) == 0) { - return 1; - } - } - return 0; + return strmap_get(&gen->declared_index, var_name) != NULL; } // #701: is `name` a module-level `var` global (a file-scope static)? @@ -1061,6 +1102,10 @@ void register_module_global_var(CodeGenerator* gen, const char* name, int is_pro * without bound. */ void truncate_declared_vars(CodeGenerator* gen, int saved_count) { for (int i = saved_count; i < gen->declared_var_count; i++) { + /* A later slot of the name goes with this one: the slots go as a + * suffix. */ + if ((intptr_t)strmap_get(&gen->declared_index, gen->declared_vars[i]) == i + 1) + strmap_put(&gen->declared_index, gen->declared_vars[i], NULL); free(gen->declared_vars[i]); if (gen->declared_var_types && gen->declared_var_types[i]) { free_type(gen->declared_var_types[i]); @@ -1086,16 +1131,14 @@ void mark_var_declared_typed(CodeGenerator* gen, const char* var_name, Type* hoi gen->declared_var_types = new_types; gen->declared_vars[gen->declared_var_count] = strdup(var_name); gen->declared_var_types[gen->declared_var_count] = hoisted_type ? clone_type(hoisted_type) : NULL; + if (!strmap_get(&gen->declared_index, var_name)) + strmap_put(&gen->declared_index, var_name, (void*)(intptr_t)(gen->declared_var_count + 1)); gen->declared_var_count++; } Type* declared_var_type(CodeGenerator* gen, const char* var_name) { - for (int i = 0; i < gen->declared_var_count; i++) { - if (strcmp(gen->declared_vars[i], var_name) == 0) { - return gen->declared_var_types ? gen->declared_var_types[i] : NULL; - } - } - return NULL; + intptr_t at = (intptr_t)strmap_get(&gen->declared_index, var_name); + return at && gen->declared_var_types ? gen->declared_var_types[at - 1] : NULL; } // Helper: clear declared vars (call at function start) @@ -1117,6 +1160,8 @@ void clear_declared_vars(CodeGenerator* gen) { gen->declared_var_types = NULL; gen->hoist_scope_body = NULL; gen->declared_var_count = 0; + strmap_free(&gen->declared_index); + strmap_init(&gen->declared_index); } // Helper: check if variable currently holds a heap-allocated string @@ -6126,6 +6171,9 @@ static void rename_locals_shadowing_functions(ASTNode* program) { * binding's (a nested binding of the name included, renamed alike), and * field, argument and function names are not value names. * + * A for loop's init that assigns a variable already bound there (#2734) + * declares nothing and keeps its name. + * * #2994: so is every name a `match` arm's pattern binds (`some(v)` over * any optional, the names of a list or a struct pattern), which is that * arm's own: codegen writes the value of a bound `match` in the arm's @@ -6198,8 +6246,8 @@ static void rename_scoped_bindings_in(ASTNode* scope, ASTNode* node, const NameS for (int i = 0; i < node->child_count; i++) rename_scoped_bindings_in(scope, node->children[i], fixed); if (node->type == AST_FOR_LOOP && node->child_count > 0 && node->children[0] && - node->children[0]->type == AST_VARIABLE_DECLARATION && node->children[0]->node_type && - node->children[0]->node_type->kind == TYPE_STRING) { + node->children[0]->type == AST_VARIABLE_DECLARATION && !node->children[0]->type_inferred && + node->children[0]->node_type && node->children[0]->node_type->kind == TYPE_STRING) { rename_scoped_binding(scope, node, node->children[0]->value, fixed); } else if (node->type == AST_CATCH_CLAUSE && node->value) { rename_scoped_binding(scope, node, node->value, fixed); @@ -8067,6 +8115,8 @@ void generate_program(CodeGenerator* gen, ASTNode* program) { /* The rename changed definition names, which are the index's keys, without changing the child count it is keyed on. */ program_index_reset(); + /* So are the hoisting answers the checker kept (#3083). */ + hoist_cache_reset(); erase_string_retype_casts(program); /* #2586: the functions used as fn-pointer values, by name, before any * pass classifies a function or emits a return of one. */ @@ -8639,7 +8689,7 @@ void generate_program(CodeGenerator* gen, ASTNode* program) { print_line(gen, "#include "); print_line(gen, "#include "); print_line(gen, "extern void* aether_caps_malloc(size_t bytes);"); - print_line(gen, "extern const char* string_new_with_length(void* data, int length);"); + print_line(gen, "extern const char* string_new_with_length(void* data, int64_t length);"); print_line(gen, "extern void string_retain(const char*);"); print_line(gen, "extern void aether_string_publish(const void*);"); /* The header of a counted string, as the runtime lays it out. A string @@ -8664,7 +8714,7 @@ void generate_program(CodeGenerator* gen, ASTNode* program) { (unsigned)AETHER_STRING_SHARED_BIT); print_line(gen, " return s;"); print_line(gen, " }"); - print_line(gen, " return string_new_with_length((void*)s, (int)strlen(s));"); + print_line(gen, " return string_new_with_length((void*)s, (int64_t)strlen(s));"); print_line(gen, "}"); /* A string handed out as a `ptr` result: a pinned string (a literal, a * fault's name) is its C characters, as a literal used as a `ptr` is @@ -9003,8 +9053,9 @@ void generate_program(CodeGenerator* gen, ASTNode* program) { * the duplicates are tolerated by C: no conflicting-types error. * string_new_with_length is declared as the std modules' extern is * (a `ptr` buffer, `-> string @heap`: `const char*` from `void*`, #3018, - * #3030) for the same reason. */ - print_line(gen, "extern const char* string_new_with_length(void* data, int length);"); + * #3030) for the same reason, with a `long` length: the runtime takes a + * size_t, which an `int` filled only half of (#3097). */ + print_line(gen, "extern const char* string_new_with_length(void* data, int64_t length);"); /* Issue #467: list / map heap-string-value owning-add helpers. * Prototypes emitted here so codegen can route `list.add(l, * heap_str)` / `map.put(m, k, heap_str)` calls to these symbols @@ -9131,6 +9182,16 @@ void generate_program(CodeGenerator* gen, ASTNode* program) { print_line(gen, "#endif"); // Closure support: generic closure struct (function pointer + captured environment) print_line(gen, "typedef struct { void (*fn)(void); void* env; } _AeClosure;"); + /* #3091: the function a call through a closure jumps to. The empty + * closure (`null` in a closure slot, also when a `fn?` holds it) has + * none, and the call panics with `where`, its category and source + * position, instead of jumping to address 0. A call through a typed + * function pointer an expression yields is checked here too (#3098). */ + print_line(gen, "typedef void (*_AeFnPtr)(void);"); + print_line(gen, "static inline _AeFnPtr _aether_callee_fn(_AeFnPtr fn, const char* where) {"); + print_line(gen, " if (!fn) aether_panic(where);"); + print_line(gen, " return fn;"); + print_line(gen, "}"); /* The head every closure env starts with: its destructor (#1398) and its * reference count (#2494). An env is held by the closure value's owner * and by every env that captured the closure; each holder releases diff --git a/compiler/codegen/codegen.h b/compiler/codegen/codegen.h index 2e42c05fd..de83f8482 100644 --- a/compiler/codegen/codegen.h +++ b/compiler/codegen/codegen.h @@ -64,6 +64,13 @@ typedef struct { // cannot flow into that type is reported in the language's terms // instead of by the C compiler. Type** declared_var_types; + // #3083: name -> its first slot in declared_vars + 1 (NULL: not + // declared), so a lookup is one probe, not a scan of every local. + StrMap declared_index; + // #3083: every name `reads_root` (the function being emitted) reads, by + // tree_reads_name's rule, gathered by one walk of it. + const ASTNode* reads_root; + StrMap reads_index; // #2124: the body of the function (or main) being generated, walked // by the hoisters to find every binding of a name across sibling // branches and loop bodies, so a hoisted local is declared with the @@ -84,6 +91,17 @@ typedef struct { const char* current_origin_module; int module_global_var_count; int generating_lvalue; // Track if we're generating an assignment target (lvalue) + /* #3029: versioning on the unbounded-view sentinel. While a function's + * view body is emitted, `slice_view_names` are its slice parameters, + * every one an unbounded view there, indexed with no test of the + * sentinel; `fn_cname_override` is that body's C name. While the + * function itself is emitted, `slice_view_dispatch` names the same + * parameters for the test at its entry that calls the view body. */ + const char** slice_view_names; + int slice_view_count; + const char* fn_cname_override; + const char** slice_view_dispatch; + int slice_view_dispatch_count; int in_const_init; // #2799: depth inside a file-scope constant's initializer, where a // constant named is written as its own initializer (ISO C) int in_condition; // Track if we're in a condition (if/while) to avoid double parens diff --git a/compiler/codegen/codegen_actor.c b/compiler/codegen/codegen_actor.c index fc9852c41..56e723984 100644 --- a/compiler/codegen/codegen_actor.c +++ b/compiler/codegen/codegen_actor.c @@ -432,12 +432,16 @@ void generate_actor_definition(CodeGenerator* gen, ASTNode* actor) { print_line(gen, "%s {", actor->value); indent(gen); - /* The prefix of a generated actor IS an ActorBase: the scheduler is handed - * a pointer to it and casts. The fields come from the runtime's one - * definition (AETHER_ACTOR_BASE_FIELDS), so a field added there is here - * too; a hand-written copy that missed one put the first state field at - * its offset, and the scheduler wrote through it. */ - print_line(gen, "AETHER_ACTOR_BASE_FIELDS"); + /* The first member of a generated actor IS an ActorBase: the scheduler is + * handed a pointer to it and casts. It is the runtime's one definition, + * so a field added there is here too; a hand-written copy that missed one + * put the first state field at its offset, and the scheduler wrote + * through it. A member of its own, not the base's fields spliced in: a + * state named like one of them (`state id`, `state active`) was a second + * member of that name and did not compile (#3094). The generated code + * reaches the base's fields through it, and the state fields keep the + * names the program gave them. */ + print_line(gen, "ActorBase _aether_base;"); print_line(gen, ""); // State fields (user-defined) @@ -478,13 +482,11 @@ void generate_actor_definition(CodeGenerator* gen, ASTNode* actor) { unindent(gen); print_line(gen, "} %s;", actor->value); emit_actor_destroy_state(gen, actor); - /* The scheduler casts this to ActorBase*, so every field it touches has to - * sit at the ActorBase offset. Checking the last one pins the whole - * prefix. */ + /* The scheduler casts this to ActorBase*, so the base has to be the first + * member, at offset 0. */ print_line(gen, "#if defined(__STDC_VERSION__) && __STDC_VERSION__ >= 201112L"); - print_line(gen, "_Static_assert(offsetof(%s, scheduler_owned) == offsetof(ActorBase, scheduler_owned),", - actor->value); - print_line(gen, " \"%s prefix must match ActorBase\");", actor->value); + print_line(gen, "_Static_assert(offsetof(%s, _aether_base) == 0,", actor->value); + print_line(gen, " \"%s's ActorBase must be its first member\");", actor->value); print_line(gen, "#endif"); print_line(gen, ""); @@ -820,13 +822,13 @@ void generate_actor_definition(CodeGenerator* gen, ASTNode* actor) { // Timeout check — fires if idle longer than timeout_ns if (timeout_arm) { print_line(gen, "// Timeout check"); - print_line(gen, "if (self->timeout_ns > 0 && self->last_activity_ns > 0) {"); + print_line(gen, "if (self->_aether_base.timeout_ns > 0 && self->_aether_base.last_activity_ns > 0) {"); indent(gen); print_line(gen, "uint64_t _now = (uint64_t)_aether_clock_ns();"); - print_line(gen, "if ((_now - self->last_activity_ns) >= self->timeout_ns) {"); + print_line(gen, "if ((_now - self->_aether_base.last_activity_ns) >= self->_aether_base.timeout_ns) {"); indent(gen); - print_line(gen, "self->timeout_ns = 0; // one-shot"); - print_line(gen, "self->last_activity_ns = 0;"); + print_line(gen, "self->_aether_base.timeout_ns = 0; // one-shot"); + print_line(gen, "self->_aether_base.last_activity_ns = 0;"); // Generate the timeout body if (timeout_arm->child_count >= 2 && timeout_arm->children[1]) { ASTNode* tbody = timeout_arm->children[1]; @@ -867,24 +869,24 @@ void generate_actor_definition(CodeGenerator* gen, ASTNode* actor) { print_line(gen, "Message msg;"); print_line(gen, ""); - print_line(gen, "if (unlikely(!mailbox_receive(&self->mailbox, &msg))) {"); + print_line(gen, "if (unlikely(!mailbox_receive(&self->_aether_base.mailbox, &msg))) {"); indent(gen); if (timeout_arm) { // Start timeout countdown when mailbox is empty - print_line(gen, "if (self->timeout_ns > 0 && self->last_activity_ns == 0) {"); + print_line(gen, "if (self->_aether_base.timeout_ns > 0 && self->_aether_base.last_activity_ns == 0) {"); indent(gen); - print_line(gen, "self->last_activity_ns = (uint64_t)_aether_clock_ns();"); + print_line(gen, "self->_aether_base.last_activity_ns = (uint64_t)_aether_clock_ns();"); unindent(gen); print_line(gen, "}"); } - print_line(gen, "atomic_store_explicit(&self->active, 0, memory_order_relaxed);"); + print_line(gen, "atomic_store_explicit(&self->_aether_base.active, 0, memory_order_relaxed);"); print_line(gen, "return;"); unindent(gen); print_line(gen, "}"); if (timeout_arm) { // Message received — cancel timeout (one-shot: fire only if no messages ever arrive) - print_line(gen, "self->timeout_ns = 0;"); - print_line(gen, "self->last_activity_ns = 0;"); + print_line(gen, "self->_aether_base.timeout_ns = 0;"); + print_line(gen, "self->_aether_base.last_activity_ns = 0;"); } print_line(gen, "aether_reply_slot_set(msg._reply_slot);"); print_line(gen, ""); @@ -1099,10 +1101,10 @@ void generate_actor_definition(CodeGenerator* gen, ASTNode* actor) { * its block came from an allocator that scheduler_release_actor's free * does not match (#2485). */ print_line(gen, "if (!actor) return NULL;"); - print_line(gen, "atomic_init(&actor->active, 0); // inactive until first message send"); - print_line(gen, "atomic_init(&actor->migrate_to, -1);"); - print_line(gen, "atomic_init(&actor->dead, 0);"); - print_line(gen, "actor->auto_process = 0;"); + print_line(gen, "atomic_init(&actor->_aether_base.active, 0); // inactive until first message send"); + print_line(gen, "atomic_init(&actor->_aether_base.migrate_to, -1);"); + print_line(gen, "atomic_init(&actor->_aether_base.dead, 0);"); + print_line(gen, "actor->_aether_base.auto_process = 0;"); print_line(gen, ""); for (int i = 0; i < actor->child_count; i++) { @@ -1143,7 +1145,7 @@ void generate_actor_definition(CodeGenerator* gen, ASTNode* actor) { } } } - print_line(gen, "actor->destroy_state = _aether_state_drop_%s; /* #2528 */", actor->value); + print_line(gen, "actor->_aether_base.destroy_state = _aether_state_drop_%s; /* #2528 */", actor->value); // Auto-initialize "my_ref" to the actor's own pointer so it is valid // immediately after spawn — no Setup message needed. This eliminates @@ -1162,16 +1164,16 @@ void generate_actor_definition(CodeGenerator* gen, ASTNode* actor) { print_line(gen, ""); print_line(gen, "#if AETHER_HAS_THREADS"); - print_line(gen, "if (actor->auto_process) {"); + print_line(gen, "if (actor->_aether_base.auto_process) {"); indent(gen); // Without its thread the actor is an ordinary one, stepped by its core // and ended by its release (#2517). The runtime starts the thread: the // program's C has the thread types but not the platform calls behind // them, which on Windows would mean (#2673). - print_line(gen, "if (aether_actor_thread_start(&actor->thread, actor) != 0) {"); + print_line(gen, "if (aether_actor_thread_start(&actor->_aether_base.thread, actor) != 0) {"); indent(gen); - print_line(gen, "actor->thread = 0;"); - print_line(gen, "actor->auto_process = 0;"); + print_line(gen, "actor->_aether_base.thread = 0;"); + print_line(gen, "actor->_aether_base.auto_process = 0;"); unindent(gen); print_line(gen, "}"); unindent(gen); @@ -1194,14 +1196,14 @@ void generate_actor_definition(CodeGenerator* gen, ASTNode* actor) { if (timeout_arm && timeout_arm->child_count >= 1) { print_line(gen, "// Receive timeout (milliseconds -> nanoseconds)"); print_indent(gen); - fprintf(gen->output, "actor->timeout_ns = (uint64_t)("); + fprintf(gen->output, "actor->_aether_base.timeout_ns = (uint64_t)("); const char* prev_alias = gen->state_self_alias; gen->state_self_alias = "actor"; generate_expression(gen, timeout_arm->children[0]); gen->state_self_alias = prev_alias; fprintf(gen->output, ") * 1000000ULL;\n"); - print_line(gen, "actor->last_activity_ns = (uint64_t)_aether_clock_ns(); // Start timeout countdown at spawn"); - print_line(gen, "atomic_store_explicit(&actor->active, 1, memory_order_release); // Activate for timeout polling"); + print_line(gen, "actor->_aether_base.last_activity_ns = (uint64_t)_aether_clock_ns(); // Start timeout countdown at spawn"); + print_line(gen, "atomic_store_explicit(&actor->_aether_base.active, 1, memory_order_release); // Activate for timeout polling"); } print_line(gen, ""); @@ -1213,7 +1215,7 @@ void generate_actor_definition(CodeGenerator* gen, ASTNode* actor) { print_line(gen, "void _aether_send_%s(%s* actor, int type, int payload) {", actor->value, actor->value); indent(gen); print_line(gen, "Message msg = {type, 0, payload, NULL};"); - print_line(gen, "if (atomic_load_explicit(&actor->assigned_core, memory_order_relaxed) == aether_core_id_get()) {"); + print_line(gen, "if (atomic_load_explicit(&actor->_aether_base.assigned_core, memory_order_relaxed) == aether_core_id_get()) {"); indent(gen); print_line(gen, "scheduler_send_local((ActorBase*)actor, msg);"); unindent(gen); diff --git a/compiler/codegen/codegen_expr.c b/compiler/codegen/codegen_expr.c index cee660f5b..2fbe63466 100644 --- a/compiler/codegen/codegen_expr.c +++ b/compiler/codegen/codegen_expr.c @@ -1,6 +1,7 @@ #include "../analysis/quote.h" #include "../analysis/hoist.h" #include "../analysis/missing_return.h" +#include "../analysis/contract_eval.h" #include "codegen_internal.h" #include "../analysis/sandbox_trust.h" #include "../aether_defines.h" @@ -608,6 +609,15 @@ static void arg_drain_select(CodeGenerator* gen, ASTNode* expr, int first_arg, /* Open the wrap: declare and fill each temp, bind the arguments to them * (generate_expression then emits the temp's name for each), and start the * `_ad_r` result. The callee's own emission follows. */ +/* A callee that is a name or a field path read through names (`f`, `h.cb`, + * `a.b.cb`): reading it again has no effect and gives the same closure, so + * a closure call may name it twice (`.fn`, `.env`). Anything else, an + * index or a call among them, is evaluated once into a temp (#3099). */ +static int closure_callee_is_path(ASTNode* e) { + while (e && e->type == AST_MEMBER_ACCESS && e->child_count > 0) e = e->children[0]; + return e && e->type == AST_IDENTIFIER; +} + static void arg_drain_open(CodeGenerator* gen, ASTNode* expr, ArgDrainWrap* w) { if (w->count == 0) return; char* names[16] = {0}; @@ -1948,12 +1958,13 @@ static int is_stdlib_string_aware_extern(const char* c_func_name) { * where C wants an ae_opt_int, and `h(none)` an initializer list. */ static void emit_closure_argument(CodeGenerator* gen, ASTNode* arg, Type* pt); -static void generate_fnptr_call_args(CodeGenerator* gen, Type* sig, ASTNode* call) { - for (int i = 0; i < call->child_count; i++) { +static void generate_fnptr_call_args(CodeGenerator* gen, Type* sig, ASTNode* call, int first) { + for (int i = first; i < call->child_count; i++) { ASTNode* arg = call->children[i]; - if (i > 0) fprintf(gen->output, ", "); - Type* param = (sig && sig->param_types && i < sig->param_count) - ? sig->param_types[i] : NULL; + if (i > first) fprintf(gen->output, ", "); + int pi = i - first; + Type* param = (sig && sig->param_types && pi < sig->param_count) + ? sig->param_types[pi] : NULL; if (param && param->kind == TYPE_STRING && arg && arg->node_type && (arg->node_type->kind == TYPE_STRING || arg->node_type->kind == TYPE_PTR)) { fprintf(gen->output, "aether_string_data("); @@ -2003,6 +2014,19 @@ static void emit_fnptr_take_close(CodeGenerator* gen, int took) { if (took) fprintf(gen->output, "))"); } +static void emit_slice_diag_file(CodeGenerator* gen, const ASTNode* n); + +/* #3098: the check before a call through a typed function pointer, which + * panics at the call when `callee` is null instead of jumping to address 0. + * `callee` is a name or a field path, so reading it again is free. Opens a + * comma expression the caller ends by naming the callee. */ +static void emit_fnptr_null_check(CodeGenerator* gen, const char* callee, ASTNode* call) { + fprintf(gen->output, "(%s ? (void)0 : aether_panic(\"null_function_call: call through a null function pointer at \" ", + callee); + emit_slice_diag_file(gen, call); + fprintf(gen->output, " \":%d:%d\"), ", call->line, call->column); +} + static void generate_fnptr_local_call(CodeGenerator* gen, Type* sig, const char* local_name, ASTNode* call, int discarded) { @@ -2019,9 +2043,38 @@ static void generate_fnptr_local_call(CodeGenerator* gen, Type* sig, int narrow = !discarded && fnptr_returns_bool(sig); if (narrow) fprintf(gen->output, "((_Bool)(unsigned char)("); /* #2651: spelled as the declarators spell it (fnptr_c_spelling). */ - fprintf(gen->output, "((%s)(%s))(", fnptr_c_spelling(sig, NULL), - safe_value_name(local_name)); - generate_fnptr_call_args(gen, sig, call); + const char* callee = safe_value_name(local_name); + emit_fnptr_null_check(gen, callee, call); + fprintf(gen->output, "(%s)(%s))(", fnptr_c_spelling(sig, NULL), callee); + generate_fnptr_call_args(gen, sig, call, 0); + fprintf(gen->output, ")"); + if (narrow) fprintf(gen->output, "))"); + emit_fnptr_take_close(gen, took); + arg_drain_close(gen, call, &ad); +} + +/* #3098: `call(f, args)` where `f` is a typed function pointer an + * expression other than a name yields (`fs[1]`, `w.h.cb`, `pick()`): + * evaluated once, through the null check, and cast to its signature. */ +static void generate_fnptr_value_call(CodeGenerator* gen, ASTNode* call, int discarded) { + Type* sig = call->children[0]->node_type; + const char* ret_c = sig->return_type ? get_c_type(sig->return_type) : "void"; + ArgDrainWrap ad; + ad.ret_ct = ret_c; + ad.ret_type = NULL; + ad.have_value = strcmp(ret_c, "void") != 0; + ad.discarded = discarded; + arg_drain_select(gen, call, 1, NULL, NULL, !ad.have_value || discarded, &ad); + arg_drain_open(gen, call, &ad); + int took = emit_fnptr_take_open(gen, call); + int narrow = !discarded && fnptr_returns_bool(sig); + if (narrow) fprintf(gen->output, "((_Bool)(unsigned char)("); + fprintf(gen->output, "((%s)_aether_callee_fn((_AeFnPtr)(", fnptr_c_spelling(sig, NULL)); + generate_expression(gen, call->children[0]); + fprintf(gen->output, "), \"null_function_call: call through a null function pointer at \" "); + emit_slice_diag_file(gen, call); + fprintf(gen->output, " \":%d:%d\"))(", call->line, call->column); + generate_fnptr_call_args(gen, sig, call, 1); fprintf(gen->output, ")"); if (narrow) fprintf(gen->output, "))"); emit_fnptr_take_close(gen, took); @@ -2078,6 +2131,8 @@ int typed_fnptr_call(CodeGenerator* gen, ASTNode* call) { if (!call || call->type != AST_FUNCTION_CALL || !call->value) return 0; if (call->annotation && (strcmp(call->annotation, "fnptr_local_call") == 0 || strncmp(call->annotation, "fnfield_", 8) == 0)) return 1; + if (strcmp(call->value, "call") == 0 && call->child_count > 0 && call->children[0] && + is_fnptr_type(call->children[0]->node_type)) return 1; Type* sig = lookup_fnptr_global(gen, call->value); return sig && sig->kind == TYPE_FUNCTION && sig->is_fnptr; } @@ -2835,12 +2890,8 @@ static ASTNode* stmt_hoisted_decl(ASTNode* s, const char* var_name) { if (s->child_count > 1) hoist_loop_decls(s->children[1], hoist_name_hunt, &h); return h.decl; } - const char* names[HOIST_MAX_NAMES]; - int n = hoist_common_decl_names(s, names, HOIST_MAX_NAMES); - for (int i = 0; i < n; i++) - if (names[i] && strcmp(names[i], var_name) == 0) - return hoist_common_first_decl(s, var_name); - return NULL; + const HoistNames* h = hoist_common_hoists(s); + return hoist_names_decl(h, hoist_names_find(h, var_name)); } // The line of the first declaration of `var_name` that is VISIBLE from @@ -2960,13 +3011,9 @@ static int enclosing_decl_line(ASTNode* program, const char* func_name, * switch or match and read after it is declared at the top * of the function (hoist_if_branch_vars): visible from its * first binding on. */ - const char* names[HOIST_MAX_NAMES]; - int n = hoist_if_branch_candidates(body, names, HOIST_MAX_NAMES); - for (int c = 0; c < n; c++) { - if (!names[c] || strcmp(names[c], var_name) != 0) continue; - ASTNode* first = hoist_if_branch_first_decl(body, var_name); - if (first && !subtree_contains(first, viewer)) return first->line; - } + const HoistNames* h = hoist_if_branch_hoists(body); + ASTNode* first = hoist_names_decl(h, hoist_names_find(h, var_name)); + if (first && !subtree_contains(first, viewer)) return first->line; return INT_MAX; } } @@ -4953,9 +5000,11 @@ void emit_closure_definitions(CodeGenerator* gen) { char** prev_declared = gen->declared_vars; Type** prev_declared_types = gen->declared_var_types; int prev_declared_count = gen->declared_var_count; + StrMap prev_declared_index = gen->declared_index; gen->declared_vars = NULL; gen->declared_var_types = NULL; gen->declared_var_count = 0; + strmap_init(&gen->declared_index); /* #2124: the closure body is a C function of its own; its * hoisted locals join over ITS bindings, not the enclosing * function's. */ @@ -5172,6 +5221,7 @@ void emit_closure_definitions(CodeGenerator* gen) { gen->declared_vars = prev_declared; gen->declared_var_types = prev_declared_types; gen->declared_var_count = prev_declared_count; + gen->declared_index = prev_declared_index; gen->hoist_scope_body = prev_hoist_scope; // Free this closure body's heap-string set and restore the // enclosing scope's (see the matching reset above). @@ -5355,7 +5405,7 @@ void emit_message_string_copy(CodeGenerator* gen, const char* lv, ASTNode* init) is_heap_string_expr(gen, init); fprintf(gen->output, "if (%s) { const char* _mt = %s; size_t _ml = aether_string_length(_mt); " - "%s = (const char*)string_new_with_length((void*)aether_string_data(_mt), (int)_ml); ", + "%s = (const char*)string_new_with_length((void*)aether_string_data(_mt), (int64_t)_ml); ", lv, lv, lv); if (owned_temp) fprintf(gen->output, "aether_heap_str_free(_mt); "); fprintf(gen->output, "} "); @@ -6010,10 +6060,21 @@ static const char* slice_elem_c_type(const Type* t) { } /* The source path a bounds-check failure names, as a C string literal - * (#2305). A path is written into generated C, so it is escaped like any - * string: a Windows path's backslashes were emitted raw, and each `\G`, - * `\s`, `\d` became an unknown-escape warning per access (a `\a` a BEL in - * the message). Control characters are written as octal escapes. */ + * (#2305), and the one a call through the empty closure names (#3091). + * A path is written into generated C, so it is escaped like any string: a + * Windows path's backslashes were emitted raw, and each `\G`, `\s`, `\d` + * became an unknown-escape warning per access (a `\a` a BEL in the + * message). Control characters are written as octal escapes. */ +/* #3113: is `idx` an integer literal in `0 ..< n`? Such an index into a + * `T[N]` needs no run-time test (the checker refuses one outside it). */ +static int index_literal_within(const ASTNode* idx, int n) { + if (!idx || idx->type != AST_LITERAL || !idx->value || !idx->node_type) return 0; + TypeKind k = idx->node_type->kind; + if (k != TYPE_INT && k != TYPE_INT64) return 0; + int64_t v = 0; + return contract_eval_int64((ASTNode*)idx, NULL, &v) && v >= 0 && v < n; +} + static void emit_slice_diag_file(CodeGenerator* gen, const ASTNode* n) { const char* path = (n && n->source_file) ? n->source_file : "?"; fputc('"', gen->output); @@ -9376,9 +9437,10 @@ void generate_expression(CodeGenerator* gen, ASTNode* expr) { int took = emit_fnptr_take_open(gen, expr); int narrow = gen->discard_call_node != expr && fnptr_returns_bool(field_sig); if (narrow) fprintf(gen->output, "((_Bool)(unsigned char)("); - fprintf(gen->output, "(%s%s%s)(", - recv_c, is_ptr ? "->" : ".", field_c); - generate_fnptr_call_args(gen, field_sig, expr); + const char* callee = cg_internf("%s%s%s", recv_c, is_ptr ? "->" : ".", field_c); + emit_fnptr_null_check(gen, callee, expr); + fprintf(gen->output, "%s)(", callee); + generate_fnptr_call_args(gen, field_sig, expr, 0); fprintf(gen->output, ")"); if (narrow) fprintf(gen->output, "))"); emit_fnptr_take_close(gen, took); @@ -10196,6 +10258,10 @@ void generate_expression(CodeGenerator* gen, ASTNode* expr) { else if (strcmp(func_name, "builder_depth") == 0) { fprintf(gen->output, "_aether_ctx_depth"); } + else if (strcmp(func_name, "call") == 0 && expr->child_count >= 1 && + is_fnptr_type(expr->children[0]->node_type)) { + generate_fnptr_value_call(gen, expr, ad_call_discarded); + } // call(closure_var, args...) — invoke a closure stored in a variable // Looks up the closure's hoisted function signature and calls through it else if (strcmp(func_name, "call") == 0 && expr->child_count >= 1) { @@ -10350,10 +10416,15 @@ void generate_expression(CodeGenerator* gen, ASTNode* expr) { ad.closure[ad.count] = 1; ad.idx[ad.count++] = 0; } else if (closure_arg && !arg_drain_lookup(closure_arg) && ad.count < 16 && - is_closure_cell_read(gen, closure_arg)) { + (is_closure_cell_read(gen, closure_arg) || + !closure_callee_is_path(closure_arg))) { /* #2755: a closure a cell holds is held for its * own call, which may rebind the cell and give - * back the cell's reference to it. */ + * back the cell's reference to it. #3099: so is + * any callee other than a name or a field path + * (`fs[pick()]`), which is also read twice below + * (`.fn`, `.env`): held once, it is evaluated + * once. */ ad.closure[ad.count] = 2; ad.idx[ad.count++] = 0; } @@ -10386,9 +10457,13 @@ void generate_expression(CodeGenerator* gen, ASTNode* expr) { fprintf(gen->output, ", %s", atype); sig_pi++; } - fprintf(gen->output, "))"); + /* #3091: through `_aether_callee_fn`, which + * panics at this call when the closure is empty. */ + fprintf(gen->output, "))_aether_callee_fn("); generate_expression(gen, closure_arg); - fprintf(gen->output, ".fn)("); + fprintf(gen->output, ".fn, \"empty_closure_call: call through an empty closure at \" "); + emit_slice_diag_file(gen, expr); + fprintf(gen->output, " \":%d:%d\"))(", expr->line, expr->column); generate_expression(gen, closure_arg); fprintf(gen->output, ".env"); int arg_pi = 0; @@ -10862,6 +10937,15 @@ void generate_expression(CodeGenerator* gen, ASTNode* expr) { if (fresh_t) { emit_composite_arg_temp(gen, arg, fresh_t); } else if (array_literal_arg_temp(gen, arg)) { + } else if (ext_name && arg->node_type && + is_string_seq_ptr_type(arg->node_type) && + is_retain_extern_param(gen, ext_name, arg_printed)) { + /* #3086: an `@retain` extern keeps the sequence + * (std's `string.seq_cons` its tail), so the call + * hands it a reference of its own, as it does a + * string or a closure (R4): a fresh one as it + * is, a place's retained. */ + emit_seq_take(gen, arg); } else if (extern_param_t && extern_param_t->c_alias && strcmp(extern_param_t->c_alias, "va_list") == 0) { @@ -11570,6 +11654,33 @@ void generate_expression(CodeGenerator* gen, ASTNode* expr) { * through aether_slice_at_w, which refuses a read-only * view; a read is checked as it was. */ const char* elem = slice_elem_c_type(base->node_type); + /* #3029: in a function's view body its slice parameters + * are unbounded views, tested once at entry: the element's + * address with no test of the sentinel or a bound (a + * store still refuses a read-only view). */ + int view = 0; + if (gen->slice_view_count > 0 && base->type == AST_IDENTIFIER && base->value) { + for (int k = 0; k < gen->slice_view_count; k++) + if (strcmp(gen->slice_view_names[k], base->value) == 0) view = 1; + } + if (view) { + fprintf(gen->output, "(*(%s*)aether_slice_view_at%s(", elem, + gen->generating_lvalue ? "_w" : ""); + int saved = gen->generating_lvalue; + gen->generating_lvalue = 0; + generate_expression(gen, base); + fprintf(gen->output, ", (int64_t)("); + generate_read(gen, expr->children[1]); + gen->generating_lvalue = saved; + fprintf(gen->output, "), sizeof(%s)", elem); + if (gen->generating_lvalue) { + fprintf(gen->output, ", "); + emit_slice_diag_file(gen, expr); + fprintf(gen->output, ", %d", expr->line); + } + fprintf(gen->output, "))"); + break; + } fprintf(gen->output, "(*(%s*)aether_slice_at%s(", elem, gen->generating_lvalue ? "_w" : ""); /* The base is READ even when `s[i]` is a store target, @@ -11587,11 +11698,26 @@ void generate_expression(CodeGenerator* gen, ASTNode* expr) { } int str_base = base && base->node_type && base->node_type->kind == TYPE_STRING; + /* #3113: an index into a fixed-size `T[N]` is checked + * against N, as a slice's is against its length: a local, + * a field, a parameter, an actor's state or a `const` + * table, read or stored, `+=` and `++` alike, the index + * evaluated once. A literal index the checker has seen in + * range needs no test. */ + int sized = base && type_is_sized_array(base->node_type) && + !index_literal_within(expr->children[1], + base->node_type->array_size); if (str_base) fprintf(gen->output, "_aether_safe_str("); generate_expression(gen, base); if (str_base) fprintf(gen->output, ")"); fprintf(gen->output, "["); + if (sized) fprintf(gen->output, "aether_array_index((int64_t)("); generate_read(gen, expr->children[1]); + if (sized) { + fprintf(gen->output, "), %d, ", base->node_type->array_size); + emit_slice_diag_file(gen, expr); + fprintf(gen->output, ", %d)", expr->line); + } fprintf(gen->output, "]"); } break; @@ -11605,9 +11731,14 @@ void generate_expression(CodeGenerator* gen, ASTNode* expr) { MessageDef* msg_def = lookup_message(gen->message_registry, message->value); if (msg_def) { const char* single_int = get_single_int_field(msg_def); - if (single_int) { + /* A message with no fields carries nothing: it goes + * as a one-int one does, with no payload to allocate + * (its handler reads none). */ + int no_fields = msg_def->fields == NULL; + if (single_int || no_fields) { // Single-field inline: value stored in payload_int (no malloc) fprintf(gen->output, "{ Message _imsg = {%d, 0, ", msg_def->message_id); + if (no_fields) fprintf(gen->output, "0"); for (int i = 0; i < message->child_count; i++) { ASTNode* field_init = message->children[i]; if (field_init && field_init->type == AST_FIELD_INIT && field_init->child_count > 0) { @@ -11685,7 +11816,11 @@ void generate_expression(CodeGenerator* gen, ASTNode* expr) { } } } - fprintf(gen->output, "aether_send_message("); + /* #3117: in a batched loop every send joins the + * batch, so a payload message keeps its place + * among the loop's one-int ones. */ + fprintf(gen->output, gen->in_main_loop ? "aether_send_message_batch(" + : "aether_send_message("); emit_send_target(gen, target, "void*"); fprintf(gen->output, ", &_msg, sizeof(%s)); }", message->value); } diff --git a/compiler/codegen/codegen_func.c b/compiler/codegen/codegen_func.c index e0da925ad..c7e176744 100644 --- a/compiler/codegen/codegen_func.c +++ b/compiler/codegen/codegen_func.c @@ -1,4 +1,5 @@ #include "codegen_internal.h" +#include "../aether_error.h" // True when this function definition is annotated `@c_callback`. // The annotation (#235) marks a function as having a stable, externally- @@ -1827,7 +1828,10 @@ static void emit_function(CodeGenerator* gen, ASTNode* func, const char* clause_ // generated C colliding at link time. Closes #279. // A clause function is static whatever its set's linkage: only the // set's dispatcher calls it. - if (as_clause) { + if (gen->fn_cname_override) { + // A function's view body (#3029): only its own entry calls it. + fprintf(gen->output, "static AETHER_MAYBE_UNUSED "); + } else if (as_clause) { fprintf(gen->output, fn_body_inlinable(func) ? "static inline AETHER_MAYBE_UNUSED " : "static AETHER_MAYBE_UNUSED "); } else if (fn_has_internal_linkage(func)) { @@ -1867,8 +1871,9 @@ static void emit_function(CodeGenerator* gen, ASTNode* func, const char* clause_ // mangling); the symbol is what other translation units reach for // when they take the address of this function. const char* cb_sym = c_callback_symbol(func); - fprintf(gen->output, " %s(", as_clause ? clause_cname - : (cb_sym ? cb_sym : safe_c_name(func->value))); + fprintf(gen->output, " %s(", gen->fn_cname_override ? gen->fn_cname_override + : as_clause ? clause_cname + : (cb_sym ? cb_sym : safe_c_name(func->value))); // Generate parameters - handle pattern matching. A parameter's C name // carries its position: `_pattern_` for a literal or struct @@ -2003,6 +2008,33 @@ static void emit_function(CodeGenerator* gen, ASTNode* func, const char* clause_ fprintf(gen->output, ") {\n"); + /* #3029: one test of the sentinel at entry. Every slice parameter the + * body indexes an unbounded view: the view body runs instead, with the + * arguments as they arrived. Otherwise this checked body does. */ + if (gen->slice_view_dispatch_count > 0) { + fprintf(gen->output, " if ("); + for (int k = 0; k < gen->slice_view_dispatch_count; k++) { + fprintf(gen->output, "%s%s.len == AETHER_SLICE_UNBOUNDED", k ? " && " : "", + gen->slice_view_dispatch[k]); + } + fprintf(gen->output, ") %s%s__aether_view(", result ? "return " : "{ ", + safe_c_name(func->value)); + int na = 0; + for (int i = 0; i < func->child_count; i++) { + ASTNode* child = func->children[i]; + if (!child || !child->value || (child->type != AST_PATTERN_VARIABLE && + child->type != AST_VARIABLE_DECLARATION)) continue; + int is_promoted = 0; + for (int pp = 0; pp < promoted_count; pp++) + if (promoted[pp] && strcmp(promoted[pp], child->value) == 0) is_promoted = 1; + fprintf(gen->output, "%s%s%s", na ? ", " : "", + (is_promoted || is_sized_array_param(child->node_type)) ? "_param_" : "", + child->value); + na++; + } + fprintf(gen->output, result ? ");\n" : "); return; }\n"); + } + /* std.mem scalar accessors: emit the body inline instead of the extern * call (#1733). * @@ -2290,7 +2322,124 @@ static void emit_function(CodeGenerator* gen, ASTNode* func, const char* clause_ print_line(gen, ""); } +/* ---- #3029: versioning on the unbounded-view sentinel --------------------- + * + * A function whose caller the C compiler cannot see (another translation + * unit, a function pointer, a library boundary) cannot tell a bounded slice + * from an unbounded view (`p as T[]`, its length the sentinel), so every + * index of a slice parameter tests the sentinel and then the bound, and the + * loop around it does not vectorize: 4.2x on an int sum and 6.4x on an f32 + * map at -O3 (the issue's table). Such a function is emitted twice: a view + * body, where every slice parameter it indexes is a view and an index is + * the element's address with no test (a view has no length to test, which + * is what the checked access does with it too), and the function itself, + * whose entry tests the sentinel once and calls the view body when every + * one of them is a view, and runs its own checked body otherwise. Per + * function, not per loop: a body's declarations, defers and labels are the + * function's. A slice the function binds again, takes the address of, or + * that a closure inside it captures, is not versioned. */ + +/* Whether `operand`, under any member accesses, is the identifier `name`: + * `&xs` or `&xs.field`, the address of the slice itself rather than of an + * element. */ +static int view_param_address_of(ASTNode* operand, const char* name) { + while (operand && operand->type == AST_MEMBER_ACCESS && operand->child_count > 0) + operand = operand->children[0]; + return operand && operand->type == AST_IDENTIFIER && operand->value && + strcmp(operand->value, name) == 0; +} + +/* Whether `node` binds, assigns or names `name` other than by reading it, + * or takes its address: through the address the slice can be made + * bounded after the entry's test, and the view body would index it with + * no check. */ +static int view_param_rebound(ASTNode* node, const char* name) { + if (!node) return 0; + if (node->value && node->type != AST_IDENTIFIER && strcmp(node->value, name) == 0) return 1; + if (node->type == AST_UNARY_EXPRESSION && node->value && strcmp(node->value, "&") == 0 && + node->child_count > 0 && view_param_address_of(node->children[0], name)) return 1; + for (int i = 0; i < node->child_count; i++) + if (view_param_rebound(node->children[i], name)) return 1; + return 0; +} + +/* Whether `node` indexes the identifier `name`. */ +static int view_param_indexed(ASTNode* node, const char* name) { + if (!node) return 0; + if (node->type == AST_ARRAY_ACCESS && node->child_count >= 2 && node->children[0] && + node->children[0]->type == AST_IDENTIFIER && node->children[0]->value && + strcmp(node->children[0]->value, name) == 0) return 1; + for (int i = 0; i < node->child_count; i++) + if (view_param_indexed(node->children[i], name)) return 1; + return 0; +} + +/* Whether `node` holds a closure or a quote: a body of its own, emitted + * once, which a second copy of the function must not emit again. */ +static int view_body_has_closure(ASTNode* node) { + if (!node) return 0; + if (node->type == AST_CLOSURE) return 1; + for (int i = 0; i < node->child_count; i++) + if (view_body_has_closure(node->children[i])) return 1; + return 0; +} + +/* The slice parameters of `func` that its body indexes and never binds + * again, at most `max`; 0 when the function is not one to version. */ +static int slice_view_params(CodeGenerator* gen, ASTNode* func, const char** out, int max) { + if (!func || func->type != AST_FUNCTION_DEFINITION || !func->value) return 0; + if (annotation_has_marker(func->annotation, "varargs") || is_c_callback(func)) return 0; + ASTNode* body = NULL; + for (int i = 0; i < func->child_count; i++) { + ASTNode* child = func->children[i]; + if (!child) return 0; + if (child->type == AST_BLOCK) { + body = child; + } else if (child->type != AST_PATTERN_VARIABLE && + child->type != AST_VARIABLE_DECLARATION) { + return 0; // a pattern or a guard: a clause shape, dispatched elsewhere + } + } + if (!body || view_body_has_closure(body)) return 0; + char** promoted = NULL; + int promoted_count = 0; + get_promoted_names_for_func(gen, fn_scope_name(gen->program, func), &promoted, &promoted_count); + int n = 0; + for (int i = 0; i < func->child_count && n < max; i++) { + ASTNode* child = func->children[i]; + if (child->type == AST_BLOCK || !child->value || !type_is_slice(child->node_type)) continue; + int is_promoted = 0; + for (int p = 0; p < promoted_count; p++) + if (promoted[p] && strcmp(promoted[p], child->value) == 0) is_promoted = 1; + if (is_promoted || view_param_rebound(body, child->value)) continue; + if (view_param_indexed(body, child->value)) out[n++] = child->value; + } + return n; +} + void generate_function_definition(CodeGenerator* gen, ASTNode* func) { + const char* views[8]; + int nviews = slice_view_params(gen, func, views, 8); + if (nviews > 0) { + gen->slice_view_names = views; + gen->slice_view_count = nviews; + gen->fn_cname_override = cg_internf("%s__aether_view", safe_c_name(func->value)); + /* The same source is rendered twice: the view body reports + * nothing, and the function's own body, emitted next, reports + * every diagnostic once. */ + aether_diagnostics_set_muted(1); + emit_function(gen, func, NULL); + aether_diagnostics_set_muted(0); + gen->slice_view_names = NULL; + gen->slice_view_count = 0; + gen->fn_cname_override = NULL; + gen->slice_view_dispatch = views; + gen->slice_view_dispatch_count = nviews; + emit_function(gen, func, NULL); + gen->slice_view_dispatch = NULL; + gen->slice_view_dispatch_count = 0; + return; + } emit_function(gen, func, NULL); } diff --git a/compiler/codegen/codegen_internal.h b/compiler/codegen/codegen_internal.h index 5d8637b1b..8326d055c 100644 --- a/compiler/codegen/codegen_internal.h +++ b/compiler/codegen/codegen_internal.h @@ -749,6 +749,7 @@ void push_struct_destroy_defer(CodeGenerator* gen, const char* var_name, /* Internal helpers shared across files */ int contains_send_expression(ASTNode* node); +int loop_sends_without_waiting(ASTNode* node); const char* get_single_int_field(MessageDef* msg_def); /* The fields of a message in the order its C struct declares them, and the * C type each has there; the .c and the --emit-header file share them (#2517). */ diff --git a/compiler/codegen/codegen_stmt.c b/compiler/codegen/codegen_stmt.c index f21edeaa2..3f1e1a144 100644 --- a/compiler/codegen/codegen_stmt.c +++ b/compiler/codegen/codegen_stmt.c @@ -5539,18 +5539,20 @@ void push_heap_string_exit_free_defers(CodeGenerator* gen, ASTNode* body) { * emits `elem name[N];`. (Hit repeatedly by the Redis port's per-loop * scratch buffers; the first-statement-in-block decl path already does * this correctly, this fixes the not-first / hoisted path.) */ -/* The zero initializer for a hoisted local. A hoisted variable is - * declared at function scope and assigned inside the branch or loop - * body that binds it; on a path that skips the body its value is - * indeterminate, and reading an indeterminate value is undefined — not +/* The zero initializer for a local that starts with no value of its own. + * A hoisted variable is declared at function scope and assigned inside the + * branch or loop body that binds it; on a path that skips the body its + * value is indeterminate, and reading an indeterminate value is undefined — not * a garbage value, undefined: gcc's interprocedural constant * propagation may treat it as whatever the other call sites pass (a * literal 0), so a parameter that "genuinely varies" arrives as a * constant, with no sanitizer able to say so (#2128 is the shape of that - * report). Initialized, the variable is 0/NULL/{0} on such a path and - * every read is defined. Spelled per kind rather than as a universal - * `{0}`, which clang flags on scalars (-Wbraced-scalar-init). */ -static const char* hoisted_zero_init(Type* t, const char* c_type) { + * report). A declaration without a value (`long x`, `int[3] k`, `P p`) + * starts at its type's zero value the same way (#3114). Initialized, the + * variable is 0/NULL/{0} and every read is defined. Spelled per kind + * rather than as a universal `{0}`, which clang flags on scalars + * (-Wbraced-scalar-init). */ +static const char* local_zero_init(Type* t, const char* c_type) { if (t) { switch (t->kind) { case TYPE_INT: case TYPE_INT64: case TYPE_UINT64: case TYPE_UINT32: @@ -5560,7 +5562,7 @@ static const char* hoisted_zero_init(Type* t, const char* c_type) { case TYPE_BITSET: case TYPE_BITSTRUCT: /* backing integers */ return " = 0"; case TYPE_ISOLATED: - return hoisted_zero_init(t->element_type, c_type); + return local_zero_init(t->element_type, c_type); case TYPE_F32X4: case TYPE_F64X2: case TYPE_I32X4: case TYPE_I64X2: case TYPE_I16X8: case TYPE_F32X8: case TYPE_I32X8: return " = {0}"; /* #2146: a vector, not a scalar 0 */ @@ -5568,6 +5570,8 @@ static const char* hoisted_zero_init(Type* t, const char* c_type) { return " = NULL"; case TYPE_FUNCTION: return t->is_fnptr ? " = NULL" : " = {0}"; + case TYPE_ARRAY: + return " = {0}"; /* a sized array, or a slice's AetherSlice */ default: break; } @@ -5627,7 +5631,7 @@ static void emit_hoisted_local_decl(CodeGenerator* gen, Type* var_type, } const char* c_type = get_c_type(var_type); fprintf(gen->output, "%s %s%s;", volatile_decl_type(vq, c_type), name, - hoisted_zero_init(var_type, c_type)); + local_zero_init(var_type, c_type)); /* A hoisted local nothing reads (`_ = f()` in a loop) is unused. */ if (codegen_local_unread(gen, name)) fprintf(gen->output, " (void)%s;", name); fprintf(gen->output, "\n"); @@ -5639,14 +5643,13 @@ static void emit_hoisted_local_decl(CodeGenerator* gen, Type* var_type, } /* Declare, before an if/else or a switch with a default, the names every arm - * binds (hoist_common_decl_names, shared with the typechecker), so the + * binds (hoist_common_hoists, shared with the typechecker), so the * binding survives the arm (#2732 added the switch). */ static void hoist_common_vars(CodeGenerator* gen, ASTNode* stmt) { - const char* names[HOIST_MAX_NAMES]; - int count = hoist_common_decl_names(stmt, names, HOIST_MAX_NAMES); + const HoistNames* h = hoist_common_hoists(stmt); - for (int i = 0; i < count; i++) { - const char* n = names[i]; + for (int i = 0; h && i < h->count; i++) { + const char* n = h->names[i]; // Skip if already declared at outer scope. if (is_var_declared(gen, n)) continue; @@ -5658,7 +5661,7 @@ static void hoist_common_vars(CodeGenerator* gen, ASTNode* stmt) { if (is_actor_state_var(gen, n)) continue; // Recover a usable type from the first arm that has one. - ASTNode* decl = hoist_common_first_decl(stmt, n); + ASTNode* decl = hoist_names_decl(h, i); Type* var_type = hoist_decl_type(decl); Type* joined = sibling_join_type(gen, n, var_type); if (joined) var_type = joined; @@ -5911,7 +5914,8 @@ static void td_explain(ASTNode* loop, const char* why) { const char* v = getenv("AETHER_EXPLAIN_THREADED"); on = (v && *v && strcmp(v, "0") != 0) ? 1 : 0; } - if (!on || !loop || loop->child_count < 2 || !loop->children[1]) return; + if (!on || aether_diagnostics_muted() || !loop || loop->child_count < 2 || + !loop->children[1]) return; /* Neither the while nor the switch node carries a position; the * switch's selector expression does. */ ASTNode* body = loop->children[1]; @@ -6230,12 +6234,11 @@ static int function_has_ensures(ASTNode* func) { void hoist_if_branch_vars(CodeGenerator* gen, ASTNode* body) { if (!body) return; - /* Which names: hoist_if_branch_candidates (shared with the typechecker, + /* Which names: hoist_if_branch_hoists (shared with the typechecker, * which must accept exactly the reads this makes compile). */ - const char* names[HOIST_MAX_NAMES]; - int count = hoist_if_branch_candidates(body, names, HOIST_MAX_NAMES); - for (int n = 0; n < count; n++) { - const char* name = names[n]; + const HoistNames* h = hoist_if_branch_hoists(body); + for (int n = 0; h && n < h->count; n++) { + const char* name = h->names[n]; if (is_var_declared(gen, name)) continue; /* #744: a module-level `var` global first assigned inside an * if-branch must NOT be hoisted as a fresh function local — @@ -6247,7 +6250,7 @@ void hoist_if_branch_vars(CodeGenerator* gen, ASTNode* body) { * skip it here exactly as that path does. */ if (is_module_global_var(gen, name)) continue; if (is_actor_state_var(gen, name)) continue; /* #2505: self->name */ - ASTNode* first_decl = hoist_if_branch_first_decl(body, name); + ASTNode* first_decl = hoist_names_decl(h, n); if (!first_decl) continue; Type* var_type = first_decl->node_type; if ((!var_type || var_type->kind == TYPE_VOID || var_type->kind == TYPE_UNKNOWN) @@ -8832,6 +8835,30 @@ static int tree_reads_name(const ASTNode* node, const char* name, size_t len) { return 0; } +/* The names `node` reads by tree_reads_name's rule, into `out`: each value, + * and each part of a dotted one up to a `.`. */ +static void reads_collect(const ASTNode* node, StrMap* out) { + if (!node) return; + if ((node->type == AST_IDENTIFIER || node->type == AST_FUNCTION_CALL) && node->value) { + strmap_put(out, node->value, (void*)1); + for (const char* dot = strchr(node->value, '.'); dot; dot = strchr(dot + 1, '.')) + strmap_put(out, cg_intern_n(node->value, (size_t)(dot - node->value)), (void*)1); + } + for (int i = 0; i < node->child_count; i++) reads_collect(node->children[i], out); +} + +/* #3083: does the function being emitted read `name`? One walk of it for + * every local asked about, not one per local. */ +static int current_function_reads(CodeGenerator* gen, const char* name) { + if (gen->reads_root != gen->current_function) { + strmap_free(&gen->reads_index); + strmap_init(&gen->reads_index); + reads_collect(gen->current_function, &gen->reads_index); + gen->reads_root = gen->current_function; + } + return strmap_has(&gen->reads_index, name); +} + /* Is a local a destructure declares one the program never reads? A name * spelled with a leading `_` says so (the type checker does not warn about * it), and any other is looked for in the function being emitted. Such a @@ -8843,8 +8870,7 @@ static int tree_reads_name(const ASTNode* node, const char* name, size_t len) { static int destructure_local_unread(CodeGenerator* gen, const char* name) { if (!name) return 0; if (name[0] == '_') return 1; - return !gen->current_function || - !tree_reads_name(gen->current_function, name, strlen(name)); + return !gen->current_function || !current_function_reads(gen, name); } int codegen_local_unread(CodeGenerator* gen, const char* name) { @@ -9942,6 +9968,25 @@ static void generate_statement_body(CodeGenerator* gen, ASTNode* stmt) { strcmp(stmt->children[0]->value, stmt->value) == 0) { break; } + /* #3114: a declaration without a value of a variable + * declared already (hoisted out of the loop or branch + * it is written in) starts it at its type's zero value + * again, as one with that value would: each pass of a + * loop gets a fresh `long acc`. What it holds is + * released first, as any rebind releases it. */ + if (stmt->child_count == 0) { + Type* vt = hoisted ? hoisted : stmt->node_type; + const char* lv = is_promoted_capture(gen, stmt->value) + ? cg_internf("(*%s)", stmt->value) : stmt->value; + own_emit_part(gen, vt, lv, OWN_DROP); + print_indent(gen); + const char* zero = local_zero_init(vt, get_c_type(vt)); + if (strcmp(zero, " = {0}") == 0) + fprintf(gen->output, "memset((void*)&%s, 0, sizeof(%s));\n", lv, lv); + else + fprintf(gen->output, "%s%s;\n", lv, zero); + break; + } /* #2289: an array literal bound to a fixed-size array * that is already declared -- hoisted out of the loop * or branch it is written in, or bound earlier -- is @@ -10470,9 +10515,12 @@ static void generate_statement_body(CodeGenerator* gen, ASTNode* stmt) { } } - /* #2618: an owning array starts empty, so its exit - * frees nothing it did not take. */ - if (decl_str_array && stmt->child_count == 0) fprintf(gen->output, " = {0}"); + /* #3114: a declaration without a value starts at its + * type's zero value (an owning array starts empty, so + * its exit frees nothing it did not take, #2618). */ + if (stmt->child_count == 0) + fputs(local_zero_init(stmt->node_type, get_c_type(stmt->node_type)), + gen->output); fprintf(gen->output, ";\n"); unwind_track_release(gen); if (decl_array_src) { @@ -10927,7 +10975,17 @@ static void generate_statement_body(CodeGenerator* gen, ASTNode* stmt) { ASTNode* for_init = stmt->child_count > 0 ? stmt->children[0] : NULL; const char* loop_var = for_init && for_init->type == AST_VARIABLE_DECLARATION ? for_init->value : NULL; - int cell_init = loop_var && is_promoted_capture(gen, loop_var); + int state_var = loop_var && is_actor_state_var(gen, loop_var); + /* #2734: a bare init (`i = 0`) of a name already bound where the + * loop is, a local, a module `var` or an actor's state field, + * assigns it, as `x = ...` does anywhere (the checker leaves + * `type_inferred` on exactly those): emitted as that statement + * ahead of the loop, it takes the ownership any binding does, + * and the loop leaves the variable with its last value. The + * header declared a new one, so `i = 5; for (i = 0; i < 3; i++) + * {}` left `i` at 5. */ + int assign_init = loop_var && for_init->type_inferred; + int cell_init = !assign_init && loop_var && is_promoted_capture(gen, loop_var); /* #2742: the loop's variable is owned as the binding a while-loop * rewrite makes, in the same block of its own: * - a string: the loop declares its own `s` and `_heap_s`, @@ -10944,16 +11002,16 @@ static void generate_statement_body(CodeGenerator* gen, ASTNode* stmt) { * of an outer loop, frees it. Declared in the header, the * variable owned nothing: the value it ended with was never * freed. Any other loop variable keeps the header declaration: - * one that shadows an enclosing local or a module `var` (the - * statement would assign those), and an actor's state field, - * which a handler reads and writes as `self->name`. */ - int state_var = loop_var && is_actor_state_var(gen, loop_var); - int str_init = !cell_init && loop_var && !state_var && for_init->node_type && - for_init->node_type->kind == TYPE_STRING; - int stmt_init = !cell_init && !str_init && loop_var && !state_var && + * one a typed init or a range-for declares over an enclosing + * local, a module `var` or an actor's state field of its name, + * which it shadows. */ + int str_init = !assign_init && !cell_init && loop_var && !state_var && + for_init->node_type && for_init->node_type->kind == TYPE_STRING; + int stmt_init = !assign_init && !cell_init && !str_init && loop_var && !state_var && !is_var_declared(gen, loop_var) && !is_module_global_var(gen, loop_var); ScopedStringLocal str_local = {0}; + if (assign_init) generate_statement(gen, for_init); int own_block = cell_init || str_init || stmt_init; if (own_block) { print_line(gen, "{"); @@ -10989,7 +11047,7 @@ static void generate_statement_body(CodeGenerator* gen, ASTNode* stmt) { int step_ctl = step && !step_stmt && hoist_ready(gen) && ctl_contains(gen, step); /* #2798: so are the constructs in an init the header keeps, ahead * of the loop: nothing in it is evaluated before the init. */ - if (!own_block && for_init && hoist_ready(gen)) { + if (!own_block && !assign_init && for_init && hoist_ready(gen)) { ASTNode* iv = for_init; if (iv->type == AST_VARIABLE_DECLARATION) { iv = iv->child_count > 0 ? iv->children[0] : NULL; @@ -11005,7 +11063,7 @@ static void generate_statement_body(CodeGenerator* gen, ASTNode* stmt) { } if (own_block) print_indent(gen); fprintf(gen->output, "for ("); - if (!own_block && for_init) { + if (!own_block && !assign_init && for_init) { ASTNode* init = for_init; if (init->type == AST_VARIABLE_DECLARATION) { generate_type(gen, init->node_type); @@ -11118,9 +11176,13 @@ static void generate_statement_body(CodeGenerator* gen, ASTNode* stmt) { break; // collapsed — done } - // Batch optimization: only in main() (not inside actors) + // Batch optimization: only outside actors, and only around a + // loop that cannot wait between its sends (#3117). A loop inside + // a batched one joins its batch rather than opening another. // Uses queue_enqueue_batch to reduce atomics from N to num_cores - if (has_sends && gen->current_actor == NULL) { + int batch = has_sends && gen->current_actor == NULL && !gen->in_main_loop && + loop_sends_without_waiting(stmt); + if (batch) { print_line(gen, "scheduler_send_batch_start();"); gen->in_main_loop = 1; } @@ -11230,7 +11292,7 @@ static void generate_statement_body(CodeGenerator* gen, ASTNode* stmt) { print_line(gen, "#endif"); } - if (has_sends && gen->current_actor == NULL) { + if (batch) { print_line(gen, "scheduler_send_batch_flush();"); gen->in_main_loop = 0; } diff --git a/compiler/codegen/optimizer.c b/compiler/codegen/optimizer.c index b9970748d..3b0f619ae 100644 --- a/compiler/codegen/optimizer.c +++ b/compiler/codegen/optimizer.c @@ -1,5 +1,6 @@ #include "optimizer.h" #include "../aether_error.h" +#include "../analysis/hoist.h" #include #include #include @@ -845,6 +846,7 @@ ASTNode* optimize_dead_code(ASTNode* node) { free(node->value); free_type(node->node_type); free(node->children); + hoist_node_freed(node); free(node); return optimize_dead_code(then_branch); @@ -857,6 +859,7 @@ ASTNode* optimize_dead_code(ASTNode* node) { free(node->value); free_type(node->node_type); free(node->children); + hoist_node_freed(node); free(node); return optimize_dead_code(else_branch); diff --git a/compiler/parser/parser.c b/compiler/parser/parser.c index 5e69ad915..56329371c 100644 --- a/compiler/parser/parser.c +++ b/compiler/parser/parser.c @@ -2,6 +2,7 @@ #include #include #include +#include #include "parser.h" #include "../aether_defines.h" @@ -26,11 +27,20 @@ Parser* create_parser(Token** tokens, int token_count) { parser->when_top_level = 0; parser->depth = 0; parser->depth_exceeded = 0; + parser->int_consts = NULL; + parser->int_const_count = 0; + parser->int_consts_scanned = 0; return parser; } +typedef struct ParserIntConst { + const char* name; /* the token's text, alive for the whole parse */ + long long value; +} ParserIntConst; + void free_parser(Parser* parser) { if (parser) { + free(parser->int_consts); free(parser); } } @@ -50,6 +60,22 @@ Token* peek_ahead(Parser* parser, int offset) { return parser->tokens[pos]; } +/* #3115: the offset of the `]` that closes the `[` at offset `open` from the + * current token, on that `[`'s line; -1 when there is none. A declaration's + * array type may hold an expression (`Pair[N + 1] ps`), so the lookaheads + * that tell `T[n] name` from an index expression skip the whole group. */ +static int closing_bracket_offset(Parser* parser, int open) { + Token* o = peek_ahead(parser, open); + if (!o || o->type != TOKEN_LEFT_BRACKET) return -1; + int depth = 0; + for (int k = open;; k++) { + Token* t = peek_ahead(parser, k); + if (!t || t->line != o->line) return -1; + if (t->type == TOKEN_LEFT_BRACKET) depth++; + else if (t->type == TOKEN_RIGHT_BRACKET && --depth == 0) return k; + } +} + // Disambiguate a `|` (or `||`) right after a function call: is it the start // of a trailing closure `func(args) |params| { ... }` / `func(args) || { ... }`, // or a bitwise/logical operator `func(args) | EXPR` / `func(args) || EXPR`? @@ -386,6 +412,232 @@ static Type* parse_struct_type_name(Parser* parser, Token* first) { return type; } +/* #3115: an integer literal's text as the language reads it (`128`, `0x40`, + * `0b1000`, `0o20`, `1_024`). 0 for anything else, a float among them. */ +static int int_literal_text(const char* text, long long* out) { + char digits[72]; + size_t k = 0; + for (const char* c = text; c && *c && k + 1 < sizeof digits; c++) + if (*c != '_') digits[k++] = *c; + digits[k] = '\0'; + int base = 0; + const char* s = digits; + if (s[0] == '0' && (s[1] == 'b' || s[1] == 'B')) { base = 2; s += 2; } + else if (s[0] == '0' && (s[1] == 'o' || s[1] == 'O')) { base = 8; s += 2; } + char* end = NULL; + unsigned long long v = strtoull(s, &end, base); + if (!end || *end || end == s || v > (unsigned long long)LLONG_MAX) return 0; + *out = (long long)v; + return 1; +} + +/* #3115: an integer constant expression over the tokens from `pos`: + * literals, names among the file's integer constants, unary minus, + * parentheses, and `* / %`, `+ -`, `<< >>` with C's precedence. It stops at + * the first token that cannot continue it. `fail` names why it is not one: + * the first unknown name, or a division by zero. */ +typedef struct { + Parser* p; + int pos; + const char* fail; + Token* fail_at; +} ConstEval; + +static Token* ce_peek(ConstEval* ce) { + return ce->pos < ce->p->token_count ? ce->p->tokens[ce->pos] : NULL; +} + +static int ce_lookup(Parser* p, const char* name, long long* out) { + for (int i = 0; i < p->int_const_count; i++) + if (strcmp(p->int_consts[i].name, name) == 0) { *out = p->int_consts[i].value; return 1; } + return 0; +} + +/* Every value an array-length expression reaches stays within +-2^31: a + * length is at most INT_MAX, and inside that bound no `*`, `+`, `-` or + * `<<` of two such values overflows a long long. */ +#define CE_LIMIT (1LL << 31) + +static int ce_in_range(ConstEval* ce, long long v, Token* at) { + if (v >= -CE_LIMIT && v <= CE_LIMIT) return 1; + if (!ce->fail) { ce->fail = "range"; ce->fail_at = at; } + return 0; +} + +static int ce_shift(ConstEval* ce, long long* out); + +static int ce_unary(ConstEval* ce, long long* out) { + Token* t = ce_peek(ce); + if (!t) return 0; + if (t->type == TOKEN_MINUS) { + ce->pos++; + if (!ce_unary(ce, out)) return 0; + *out = -*out; + return 1; + } + if (t->type == TOKEN_LEFT_PAREN) { + ce->pos++; + if (!ce_shift(ce, out)) return 0; + Token* r = ce_peek(ce); + if (!r || r->type != TOKEN_RIGHT_PAREN) return 0; + ce->pos++; + return 1; + } + if (t->type == TOKEN_NUMBER) { + if (!int_literal_text(t->value, out) || !ce_in_range(ce, *out, t)) return 0; + ce->pos++; + return 1; + } + if (t->type == TOKEN_IDENTIFIER) { + if (!ce_lookup(ce->p, t->value, out)) { + if (!ce->fail) { ce->fail = "unknown"; ce->fail_at = t; } + return 0; + } + ce->pos++; + return 1; + } + return 0; +} + +static int ce_mul(ConstEval* ce, long long* out) { + if (!ce_unary(ce, out)) return 0; + for (;;) { + Token* t = ce_peek(ce); + if (!t || (t->type != TOKEN_MULTIPLY && t->type != TOKEN_DIVIDE && + t->type != TOKEN_MODULO)) return 1; + ce->pos++; + long long r; + if (!ce_unary(ce, &r)) return 0; + if (t->type == TOKEN_MULTIPLY) *out *= r; + else if (r == 0) { + if (!ce->fail) { ce->fail = "zero"; ce->fail_at = t; } + return 0; + } else if (t->type == TOKEN_DIVIDE) *out /= r; + else *out %= r; + if (!ce_in_range(ce, *out, t)) return 0; + } +} + +static int ce_add(ConstEval* ce, long long* out) { + if (!ce_mul(ce, out)) return 0; + for (;;) { + Token* t = ce_peek(ce); + if (!t || (t->type != TOKEN_PLUS && t->type != TOKEN_MINUS)) return 1; + ce->pos++; + long long r; + if (!ce_mul(ce, &r)) return 0; + *out = t->type == TOKEN_PLUS ? *out + r : *out - r; + if (!ce_in_range(ce, *out, t)) return 0; + } +} + +static int ce_shift(ConstEval* ce, long long* out) { + if (!ce_add(ce, out)) return 0; + for (;;) { + Token* t = ce_peek(ce); + if (!t || (t->type != TOKEN_LSHIFT && t->type != TOKEN_RSHIFT)) return 1; + ce->pos++; + long long r; + if (!ce_add(ce, &r)) return 0; + /* a shift of a negative value, or by a negative or too-wide count, + * is no length */ + if (*out < 0 || r < 0 || r > 31) { + if (!ce->fail) { ce->fail = "range"; ce->fail_at = t; } + return 0; + } + *out = t->type == TOKEN_LSHIFT ? *out << r : *out >> r; + if (!ce_in_range(ce, *out, t)) return 0; + } +} + +/* #3115: collect the file's integer constants: every top-level + * `const NAME = e` or `const NAME: T = e` whose whole value is an integer + * constant expression. A constant built from a later one is found by + * scanning again, until a pass finds nothing new. */ +static void scan_int_consts(Parser* p) { + p->int_consts_scanned = 1; + int cap = 0; + for (int pass = 0; pass < 16; pass++) { + int found = 0, depth = 0; + for (int i = 0; i < p->token_count; i++) { + Token* t = p->tokens[i]; + if (t->type == TOKEN_LEFT_BRACE) { depth++; continue; } + if (t->type == TOKEN_RIGHT_BRACE) { if (depth > 0) depth--; continue; } + if (depth != 0 || t->type != TOKEN_CONST || i + 2 >= p->token_count) continue; + Token* name = p->tokens[i + 1]; + if (name->type != TOKEN_IDENTIFIER) continue; + long long known; + if (ce_lookup(p, name->value, &known)) continue; + /* skip `: T` to the `=` on the same line */ + int j = i + 2; + while (j < p->token_count && p->tokens[j]->type != TOKEN_ASSIGN && + p->tokens[j]->line == name->line) + j++; + if (j >= p->token_count || p->tokens[j]->type != TOKEN_ASSIGN) continue; + ConstEval ce = { p, j + 1, NULL, NULL }; + long long v; + if (!ce_shift(&ce, &v) || ce.pos == j + 1) continue; + /* the value is all of the expression: what follows starts a new + * statement (a later line, a `;`, the end) */ + Token* last = p->tokens[ce.pos - 1]; + Token* next = ce.pos < p->token_count ? p->tokens[ce.pos] : NULL; + if (next && next->type != TOKEN_EOF && next->type != TOKEN_SEMICOLON && + next->line == last->line) + continue; + if (p->int_const_count == cap) { + cap = cap ? cap * 2 : 16; + ParserIntConst* grown = realloc(p->int_consts, (size_t)cap * sizeof *grown); + if (!grown) return; + p->int_consts = grown; + } + p->int_consts[p->int_const_count].name = name->value; + p->int_consts[p->int_const_count].value = v; + p->int_const_count++; + found = 1; + } + if (!found) return; + } +} + +/* #3115: a fixed-size array's length, after its `[`: an integer constant + * expression of at least 1. Reports and returns 0 for anything else. */ +static int parse_array_length(Parser* parser) { + Token* start = peek_token(parser); + if (!parser->int_consts_scanned) scan_int_consts(parser); + ConstEval ce = { parser, parser->current_token, NULL, NULL }; + long long v = 0; + int ok = ce_shift(&ce, &v); + char msg[256]; + if (!ok || ce.pos == parser->current_token) { + if (ce.fail && strcmp(ce.fail, "zero") == 0) { + parser_error_at(parser, ce.fail_at, "division by zero in an array length"); + } else if (ce.fail && strcmp(ce.fail, "range") == 0) { + snprintf(msg, sizeof msg, "an array length must be from 1 to %d; this one " + "reaches a value outside that", INT_MAX); + parser_error_at(parser, ce.fail_at, msg); + } else if (ce.fail) { + snprintf(msg, sizeof msg, + "array length '%s' is not an integer constant of this file: a length is " + "a number, a `const` of the file holding one, or an expression over those", + ce.fail_at->value); + parser_error_at(parser, ce.fail_at, msg); + } else { + parser_error_at(parser, start, + "an array length is a number, a `const` of the file holding one, or an " + "expression over those (`int[4]`, `long[QWORDS]`, `long[NW + 1]`)"); + } + return 0; + } + if (v < 1 || v > INT_MAX) { + snprintf(msg, sizeof msg, "an array length must be from 1 to %d, but this one is %lld", + INT_MAX, v); + parser_error_at(parser, start, msg); + return 0; + } + parser->current_token = ce.pos; + return (int)v; +} + // #340: a type may carry a postfix `?` making it optional (`int?`, `string?`, // `Vec2?`, `*Node?`). Parse the base type, then wrap once per `?`. In type // position `?` is unambiguous (the actor-ask `?` only appears in expressions). @@ -990,10 +1242,10 @@ static Type* parse_type_unsuffixed(Parser* parser) { // Dynamic array type = create_array_type(type, -1); } else { - // Fixed-size array - Token* size_token = expect_token(parser, TOKEN_NUMBER); - if (size_token) { - int size = atoi(size_token->value); + // Fixed-size array: the length is an integer constant + // expression (#3115), reported here when it is not one. + int size = parse_array_length(parser); + if (size > 0) { if (!expect_token(parser, TOKEN_RIGHT_BRACKET)) return NULL; type = create_array_type(type, size); } @@ -1085,7 +1337,7 @@ static ASTNode* parse_interp_string_expr(const char* raw, int line, int column) int n = sub_tokens[sub_count - 1]->type == TOKEN_EOF ? sub_count - 1 : sub_count; Parser* sub = create_parser(sub_tokens, n); ASTNode* expr_node = parse_expression(sub); - free(sub); + free_parser(sub); /* AST nodes copy a token's text (create_ast_node strdups it) and * keep no reference to the token itself, so these are ours to * release: every `${...}` used to leak its whole token run @@ -2563,6 +2815,11 @@ static ASTNode* parse_postfix_expression(Parser* parser) { // here and handed to the typechecker as the implicit first // argument; ufcs_recv holds it until the call node exists. ASTNode* ufcs_recv = NULL; + /* #3099: any other callee is a value, called as a closure local + * or field is (`call(callee, args)`, the form the checker gives + * those). The callee was dropped and the call left nameless: + * `fs[1]()` was "Undefined function '?'". */ + ASTNode* value_callee = NULL; if (expr && expr->type == AST_IDENTIFIER && expr->value) { // Simple call: foo() func_name = strdup(expr->value); @@ -2585,6 +2842,9 @@ static ASTNode* parse_postfix_expression(Parser* parser) { func_name = strdup(expr->value); ufcs_recv = expr->children[0]; expr->children[0] = NULL; // detach so free_ast_node(expr) won't reclaim it + } else if (expr) { + func_name = strdup("call"); + value_callee = expr; } // heap.new(TypeName) — like sizeof, the argument is a *type @@ -2632,6 +2892,10 @@ static ASTNode* parse_postfix_expression(Parser* parser) { func_call->annotation = strdup("ufcs"); add_child(func_call, ufcs_recv); } + if (value_callee) { + add_child(func_call, value_callee); + func_call->annotation = annotation_add_marker(func_call->annotation, "value_callee"); + } // Parse arguments if (!match_token(parser, TOKEN_RIGHT_PAREN)) { @@ -2797,8 +3061,9 @@ static ASTNode* parse_postfix_expression(Parser* parser) { } } - // Free the original identifier node since we've copied its name - if (expr) free_ast_node(expr); + // Free the original identifier node since we've copied its name; + // a value callee is not freed, it is the call's first argument. + if (expr && expr != value_callee) free_ast_node(expr); expr = func_call; continue; @@ -3363,7 +3628,7 @@ static ASTNode* parse_statement_inner(Parser* parser) { } // The array form of the same declaration, with an element // type spelled by an identifier: `uint32[4] xs = [...]`, - // `uint16[2] hs`, `Pair[8] ps`. Shape IDENT `[` NUMBER? `]` + // `uint16[2] hs`, `Pair[N + 1] ps`. Shape IDENT `[` length? `]` // IDENT on one line — an index expression statement (`xs[i]`) // is never followed by an identifier on its own line, exactly // as `IDENT IDENT` above; the line check keeps `xs[1]` followed @@ -3372,12 +3637,10 @@ static ASTNode* parse_statement_inner(Parser* parser) { // without this branch `uint32[4] xs` parsed as an index into an // undefined variable `uint32`. if (next && next->type == TOKEN_LEFT_BRACKET) { - Token* t2 = peek_ahead(parser, 2); - int off = (t2 && t2->type == TOKEN_NUMBER) ? 3 : 2; - Token* rb = peek_ahead(parser, off); - Token* nm = peek_ahead(parser, off + 1); - if (rb && rb->type == TOKEN_RIGHT_BRACKET && - nm && nm->type == TOKEN_IDENTIFIER && nm->line == rb->line) { + int rbo = closing_bracket_offset(parser, 1); + Token* rb = rbo > 0 ? peek_ahead(parser, rbo) : NULL; + Token* nm = rbo > 0 ? peek_ahead(parser, rbo + 1) : NULL; + if (rb && nm && nm->type == TOKEN_IDENTIFIER && nm->line == rb->line) { return parse_variable_declaration(parser); } } @@ -3906,6 +4169,11 @@ ASTNode* parse_for_loop(Parser* parser) { if (!name) return NULL; init = create_ast_node(AST_VARIABLE_DECLARATION, name->value, name->line, name->column); init->node_type = create_type(TYPE_UNKNOWN); + /* #2734: a bare `x = expr`, as anywhere else: it assigns a + * variable already bound there. The checker clears the mark + * for a new name, which the loop declares as its own, as it + * does a typed init's (`int i = 0`) or a range-for's. */ + init->type_inferred = 1; Token* eq = peek_token(parser); if (match_token(parser, TOKEN_ASSIGN)) { int errors_before = aether_error_count(); @@ -5254,12 +5522,11 @@ ASTNode* parse_actor_definition(Parser* parser) { if (t1 && t1->type == TOKEN_IDENTIFIER && t1->line == next_tok->line) { ident_typed = 1; } else if (t1 && t1->type == TOKEN_LEFT_BRACKET) { - Token* t2 = peek_ahead(parser, 2); - int off = (t2 && t2->type == TOKEN_NUMBER) ? 3 : 2; - Token* rb = peek_ahead(parser, off); - Token* nm = peek_ahead(parser, off + 1); - ident_typed = rb && rb->type == TOKEN_RIGHT_BRACKET && - nm && nm->type == TOKEN_IDENTIFIER && nm->line == rb->line; + int rbo = closing_bracket_offset(parser, 1); + Token* rb = rbo > 0 ? peek_ahead(parser, rbo) : NULL; + Token* nm = rbo > 0 ? peek_ahead(parser, rbo + 1) : NULL; + ident_typed = rb && nm && nm->type == TOKEN_IDENTIFIER && + nm->line == rb->line; } } @@ -5569,6 +5836,14 @@ ASTNode* parse_extern_struct_field(Parser* parser) { * hands `@retain` a string's characters with a reference of its * own, right for C and not what the Aether function expects. * + * @holds the `ptr` argument is where the function keeps what its + * `@retain` parameters are given (#3086): std's list and map + * stores declare their container so. A keep through such a + * call outlives the caller only when that argument's memory + * does: a container the caller makes with an `@fresh` extern + * and lets go nowhere keeps nothing past it. Only meaningful + * on `ptr`. + * * Multiple annotations stack: `name: @aether @retain string` is legal. * Order is irrelevant; storage is a comma-separated set on * `param->annotation`, tested by substring. */ @@ -5588,12 +5863,14 @@ static void parse_extern_param_attrs_and_type(Parser* parser, ASTNode* param) { tag = "into_param"; } else if (strcmp(attr->value, "keep") == 0) { tag = "keep_param"; + } else if (strcmp(attr->value, "holds") == 0) { + tag = "holder_param"; } if (tag) advance_token(parser); } if (!tag) { parser_error(parser, "unknown extern-param attribute (expected @aether, @retain, " - "@noescape, @into or @keep)"); + "@noescape, @into, @keep or @holds)"); break; } /* Append to the comma-separated set, deduping. */ @@ -5625,6 +5902,10 @@ static void parse_extern_param_attrs_and_type(Parser* parser, ASTNode* param) { param->node_type->kind != TYPE_PTR && param->node_type->kind != TYPE_FUNCTION) { parser_error(parser, "@noescape on an extern parameter is only valid on `ptr` or `fn`"); } + if (param->annotation && strstr(param->annotation, "holder_param") && + param->node_type->kind != TYPE_PTR) { + parser_error(parser, "@holds on an extern parameter is only valid on `ptr`"); + } } /* Trailing `@`-attributes on an extern signature, shared by the bare @@ -5635,6 +5916,10 @@ static void parse_extern_param_attrs_and_type(Parser* parser, ASTNode* param) { * codegen_stmt.c. Only meaningful on `-> string`; anything else * is a parse error since integers and pointers aren't heap- * tracked and void has nothing to own. + * @fresh the `-> ptr` result is memory the call allocates, which nothing + * else holds (#3086): std's list and map constructors. A + * container bound from one is a holder (`@holds`) that keeps + * nothing past its function unless it escapes. * @borrow the unannotated default. A no-op, accepted so callers can be * explicit and symmetric with the tuple form. * @c_import a C header owns the prototype, so codegen emits no declaration @@ -5674,6 +5959,14 @@ static void parse_extern_trailing_attrs(Parser* parser, ASTNode* ext) { annotation_add_marker(ext->annotation, "heap_return"); } advance_token(parser); /* consume 'heap' */ + } else if (strcmp(tag->value, "fresh") == 0) { + advance_token(parser); /* consume '@' */ + if (!ext->node_type || ext->node_type->kind != TYPE_PTR) { + parser_error(parser, "@fresh on extern return is only valid on `-> ptr`"); + } else { + ext->annotation = annotation_add_marker(ext->annotation, "fresh_return"); + } + advance_token(parser); /* consume 'fresh' */ } else if (strcmp(tag->value, "borrow") == 0) { advance_token(parser); /* consume '@' */ advance_token(parser); /* consume 'borrow' */ diff --git a/compiler/parser/parser.h b/compiler/parser/parser.h index fb979f1b0..69930bbce 100644 --- a/compiler/parser/parser.h +++ b/compiler/parser/parser.h @@ -34,6 +34,13 @@ typedef struct { // matters too: the guard returns without consuming a // token, so the loop's force-advance would otherwise // emit one error per remaining token. + /* #3115: the file's integer constants, for an array length written as a + * name or an expression (`long[QWORDS]`, `long[NW + 1]`). Scanned once, + * on the first length that needs them, from every top-level + * `const NAME[: T] = `. */ + struct ParserIntConst* int_consts; + int int_const_count; + int int_consts_scanned; } Parser; // The deepest real nesting in this repository is 22 braces and 9 parens, so diff --git a/contrib/avcodec/module.ae b/contrib/avcodec/module.ae index dfb1acc39..5ea9ed233 100644 --- a/contrib/avcodec/module.ae +++ b/contrib/avcodec/module.ae @@ -74,7 +74,7 @@ extern avc_audio_get_pcm_length() -> int extern avc_audio_sample_rate() -> int extern avc_audio_channels() -> int extern avc_audio_release() -extern string_new_with_length(data: ptr, length: int) -> string @heap +extern string_new_with_length(data: ptr, length: long) -> string @heap // Open a media source. `want_w`/`want_h` <= 0 means "source size"; anything // else scales during the RGBA conversion, so resizing costs no extra pass. diff --git a/contrib/host/aether/module.ae b/contrib/host/aether/module.ae index 599bea07a..7ec086b77 100644 --- a/contrib/host/aether/module.ae +++ b/contrib/host/aether/module.ae @@ -51,9 +51,9 @@ extern aether_host_run_script_sandboxed(perms: sandbox.Grants, script_path: stri // via aether_map_get/aether_map_put. The map is serialized to // shared memory before launch and read back after the child exits. // Returns: child exit code (0 = success). -extern aether_host_run_sandboxed_with_map(perms: sandbox.Grants, binary_path: string, map_token: int) -> int +extern aether_host_run_sandboxed_with_map(perms: sandbox.Grants, binary_path: string, map_token: long) -> int extern aether_host_run_script_sandboxed_with_map( - perms: sandbox.Grants, script_path: string, map_token: int) -> int + perms: sandbox.Grants, script_path: string, map_token: long) -> int // Run without sandbox (full access). Still a subprocess — useful for // testing or trusted scripts that need unrestricted access. diff --git a/contrib/host/duktape/module.ae b/contrib/host/duktape/module.ae index 103c19e1d..71497fa21 100644 --- a/contrib/host/duktape/module.ae +++ b/contrib/host/duktape/module.ae @@ -25,7 +25,7 @@ exports ( ) extern duktape_run_sandboxed(perms: sandbox.Grants, code: string) -> int -extern duktape_run_sandboxed_with_map(perms: sandbox.Grants, code: string, map_token: int) -> int +extern duktape_run_sandboxed_with_map(perms: sandbox.Grants, code: string, map_token: long) -> int extern duktape_run(code: string) -> int extern duktape_init() -> int extern duktape_finalize() diff --git a/contrib/host/factor/module.ae b/contrib/host/factor/module.ae index dee1e5ec8..430b2fe6f 100644 --- a/contrib/host/factor/module.ae +++ b/contrib/host/factor/module.ae @@ -32,7 +32,7 @@ extern factor_run_sandboxed(perms: sandbox.Grants, code: string) -> int // First-class shared map: the Factor script reads/writes the Aether-owned // shared map live via FFI words aether-map-get / aether-map-put, so keys the // script discovers at runtime come back too. Enumerate with map.keys(map). -extern factor_run_sandboxed_with_map(perms: sandbox.Grants, code: string, map_token: int) -> int +extern factor_run_sandboxed_with_map(perms: sandbox.Grants, code: string, map_token: long) -> int extern factor_run(code: string) -> int // Evaluate `code` in the persistent VM; returns its captured output as a // string ("" on failure). State persists across calls. The string is a diff --git a/contrib/host/go/module.ae b/contrib/host/go/module.ae index cd75ef0b4..d623dc500 100644 --- a/contrib/host/go/module.ae +++ b/contrib/host/go/module.ae @@ -54,5 +54,5 @@ extern go_run(binary_path: string) -> int extern go_run_script(script_path: string) -> int // Shared-map variants for typed input/output exchange. -extern go_run_sandboxed_with_map(perms: sandbox.Grants, binary_path: string, map_token: int) -> int -extern go_run_script_sandboxed_with_map(perms: sandbox.Grants, script_path: string, map_token: int) -> int +extern go_run_sandboxed_with_map(perms: sandbox.Grants, binary_path: string, map_token: long) -> int +extern go_run_script_sandboxed_with_map(perms: sandbox.Grants, script_path: string, map_token: long) -> int diff --git a/contrib/host/js/module.ae b/contrib/host/js/module.ae index bd1c2217c..aff080b0f 100644 --- a/contrib/host/js/module.ae +++ b/contrib/host/js/module.ae @@ -25,7 +25,7 @@ exports ( ) @extern("duktape_run_sandboxed") js_run_sandboxed(perms: sandbox.Grants, code: string) -> int -@extern("duktape_run_sandboxed_with_map") js_run_sandboxed_with_map(perms: sandbox.Grants, code: string, map_token: int) -> int +@extern("duktape_run_sandboxed_with_map") js_run_sandboxed_with_map(perms: sandbox.Grants, code: string, map_token: long) -> int @extern("duktape_run") js_run(code: string) -> int @extern("duktape_init") js_init() -> int @extern("duktape_finalize") js_finalize() diff --git a/contrib/host/lua/aether_host_lua.c b/contrib/host/lua/aether_host_lua.c index 8cc637541..be824c748 100644 --- a/contrib/host/lua/aether_host_lua.c +++ b/contrib/host/lua/aether_host_lua.c @@ -481,7 +481,7 @@ void lua_vm_set_global_str(void* vm, const char* name, const char* value) { g_lua.lua_setglobal(s, name); } -void lua_vm_set_global_int(void* vm, const char* name, long value) { +void lua_vm_set_global_int(void* vm, const char* name, int64_t value) { if (!vm || !name) return; lua_State* s = (lua_State*)vm; g_lua.lua_pushinteger(s, (lua_Integer)value); @@ -532,10 +532,10 @@ int lua_vm_result_type(void* vm) { if (!vm) return AE_LUA_TNIL; return ae_lua_tag_at((lua_State*)vm, -1); } -long lua_vm_result_int(void* vm) { +int64_t lua_vm_result_int(void* vm) { if (!vm) return 0; int isnum = 0; - return (long)g_lua.lua_tointegerx((lua_State*)vm, -1, &isnum); + return (int64_t)g_lua.lua_tointegerx((lua_State*)vm, -1, &isnum); } const char* lua_vm_result_str(void* vm) { if (!vm) return ""; @@ -567,16 +567,16 @@ const char* lua_arg_str(void* vm, int i) { const char* r = g_lua.lua_tolstring((lua_State*)vm, i, NULL); return r ? r : ""; } -long lua_arg_int(void* vm, int i) { +int64_t lua_arg_int(void* vm, int i) { if (!vm) return 0; int isnum = 0; - return (long)g_lua.lua_tointegerx((lua_State*)vm, i, &isnum); + return (int64_t)g_lua.lua_tointegerx((lua_State*)vm, i, &isnum); } int lua_arg_bool(void* vm, int i) { if (!vm) return 0; return g_lua.lua_toboolean((lua_State*)vm, i); } -void lua_push_int(void* vm, long v) { +void lua_push_int(void* vm, int64_t v) { if (vm) g_lua.lua_pushinteger((lua_State*)vm, (lua_Integer)v); } void lua_push_str(void* vm, const char* v) { @@ -652,7 +652,7 @@ void lua_table_seti_str(void* vm, int i, const char* v) { g_lua.lua_pushstring(s, v ? v : ""); g_lua.lua_seti(s, -2, (lua_Integer)i); } -void lua_table_seti_int(void* vm, int i, long v) { +void lua_table_seti_int(void* vm, int i, int64_t v) { if (!vm || g_tbl_depth <= 0) return; lua_State* s = (lua_State*)vm; g_lua.lua_pushinteger(s, (lua_Integer)v); @@ -672,7 +672,7 @@ void lua_table_set_str(void* vm, const char* key, const char* v) { g_lua.lua_pushstring(s, v ? v : ""); g_lua.lua_setfield(s, -2, key); } -void lua_table_set_int(void* vm, const char* key, long v) { +void lua_table_set_int(void* vm, const char* key, int64_t v) { if (!vm || g_tbl_depth <= 0 || !key) return; lua_State* s = (lua_State*)vm; g_lua.lua_pushinteger(s, (lua_Integer)v); @@ -722,8 +722,11 @@ void lua_push_double(void* vm, double v) { * float push isn't resolved. Most RESP Doubles arrive integral. */ lua_State* s = (lua_State*)vm; double r = v < 0 ? -v : v; - if (r == (double)(long)v) { - g_lua.lua_pushinteger(s, (lua_Integer)(long)v); + /* In range before the conversion: a double past 2^63 does not fit. Then + * `v` itself against its integer, not `r`: |v| never equalled a negative + * integer, and every negative whole Double went out as a string. */ + if (r < 9223372036854775808.0 && v == (double)(int64_t)v) { + g_lua.lua_pushinteger(s, (lua_Integer)(int64_t)v); } else { char buf[40]; snprintf(buf, sizeof(buf), "%.17g", v); @@ -735,8 +738,8 @@ void lua_push_double(void* vm, double v) { // The host arms a count hook; when the budget is hit the hook raises a Lua // error (the script-timeout shape). One global budget per process is enough // for the single-threaded EVAL model; a richer per-VM budget can follow. -static long g_lua_instr_budget = 0; -static long g_lua_instr_used = 0; +static int64_t g_lua_instr_budget = 0; +static int64_t g_lua_instr_used = 0; static void ae_lua_count_hook(lua_State* s, lua_Debug* ar) { (void)ar; @@ -749,7 +752,7 @@ static void ae_lua_count_hook(lua_State* s, lua_Debug* ar) { /* Arm the count hook: fire every `every` VM instructions, abort after * `budget` total. `budget`<=0 disarms. LUA_MASKCOUNT == 1<<3 == 8. */ -void lua_vm_set_instruction_limit(void* vm, long budget, int every) { +void lua_vm_set_instruction_limit(void* vm, int64_t budget, int every) { if (!vm) return; lua_State* s = (lua_State*)vm; g_lua_instr_budget = budget; @@ -781,33 +784,33 @@ void* lua_vm_new(void) { (void)lua_init(); return 0; } void lua_vm_free(void* vm) { (void)vm; } int lua_vm_register(void* vm, const char* n, void* f) { (void)vm; (void)n; (void)f; return -1; } void lua_vm_set_global_str(void* vm, const char* n, const char* v) { (void)vm; (void)n; (void)v; } -void lua_vm_set_global_int(void* vm, const char* n, long v) { (void)vm; (void)n; (void)v; } +void lua_vm_set_global_int(void* vm, const char* n, int64_t v) { (void)vm; (void)n; (void)v; } void lua_vm_set_global_strlist(void* vm, const char* n, void* it) { (void)vm; (void)n; (void)it; } int lua_vm_eval(void* vm, const char* c) { (void)vm; (void)c; return -1; } int lua_vm_result_type(void* vm) { (void)vm; return 0; } -long lua_vm_result_int(void* vm) { (void)vm; return 0; } +int64_t lua_vm_result_int(void* vm) { (void)vm; return 0; } const char* lua_vm_result_str(void* vm) { (void)vm; return ""; } int lua_vm_result_bool(void* vm) { (void)vm; return 0; } void lua_vm_pop_result(void* vm) { (void)vm; } int lua_arg_count(void* vm) { (void)vm; return 0; } int lua_arg_type(void* vm, int i) { (void)vm; (void)i; return 0; } const char* lua_arg_str(void* vm, int i) { (void)vm; (void)i; return ""; } -long lua_arg_int(void* vm, int i) { (void)vm; (void)i; return 0; } +int64_t lua_arg_int(void* vm, int i) { (void)vm; (void)i; return 0; } int lua_arg_bool(void* vm, int i) { (void)vm; (void)i; return 0; } -void lua_push_int(void* vm, long v) { (void)vm; (void)v; } +void lua_push_int(void* vm, int64_t v) { (void)vm; (void)v; } void lua_push_str(void* vm, const char* v) { (void)vm; (void)v; } void lua_push_bool(void* vm, int v) { (void)vm; (void)v; } void lua_push_nil(void* vm) { (void)vm; } void lua_push_tagged_table(void* vm, const char* f, const char* m) { (void)vm; (void)f; (void)m; } int lua_raise_error(void* vm, const char* m) { (void)vm; (void)m; return 0; } -void lua_vm_set_instruction_limit(void* vm, long b, int e) { (void)vm; (void)b; (void)e; } +void lua_vm_set_instruction_limit(void* vm, int64_t b, int e) { (void)vm; (void)b; (void)e; } // #910 table builder — unavailable stubs. void lua_table_begin(void* vm, int a, int r) { (void)vm; (void)a; (void)r; } void lua_table_seti_str(void* vm, int i, const char* v) { (void)vm; (void)i; (void)v; } -void lua_table_seti_int(void* vm, int i, long v) { (void)vm; (void)i; (void)v; } +void lua_table_seti_int(void* vm, int i, int64_t v) { (void)vm; (void)i; (void)v; } void lua_table_seti_bool(void* vm, int i, int v) { (void)vm; (void)i; (void)v; } void lua_table_set_str(void* vm, const char* k, const char* v) { (void)vm; (void)k; (void)v; } -void lua_table_set_int(void* vm, const char* k, long v) { (void)vm; (void)k; (void)v; } +void lua_table_set_int(void* vm, const char* k, int64_t v) { (void)vm; (void)k; (void)v; } void lua_table_set_bool(void* vm, const char* k, int v) { (void)vm; (void)k; (void)v; } void lua_table_end_seti(void* vm, int i) { (void)vm; (void)i; } void lua_table_end_setfield(void* vm, const char* k) { (void)vm; (void)k; } diff --git a/contrib/host/lua/aether_host_lua.h b/contrib/host/lua/aether_host_lua.h index 0ca55ad1f..18f80e280 100644 --- a/contrib/host/lua/aether_host_lua.h +++ b/contrib/host/lua/aether_host_lua.h @@ -14,34 +14,34 @@ void* lua_vm_new(void); void lua_vm_free(void* vm); int lua_vm_register(void* vm, const char* name, void* host_fn); void lua_vm_set_global_str(void* vm, const char* name, const char* value); -void lua_vm_set_global_int(void* vm, const char* name, long value); +void lua_vm_set_global_int(void* vm, const char* name, int64_t value); void lua_vm_set_global_strlist(void* vm, const char* name, void* items); int lua_vm_eval(void* vm, const char* code); int lua_vm_result_type(void* vm); -long lua_vm_result_int(void* vm); +int64_t lua_vm_result_int(void* vm); const char* lua_vm_result_str(void* vm); int lua_vm_result_bool(void* vm); void lua_vm_pop_result(void* vm); int lua_arg_count(void* vm); int lua_arg_type(void* vm, int i); const char* lua_arg_str(void* vm, int i); -long lua_arg_int(void* vm, int i); +int64_t lua_arg_int(void* vm, int i); int lua_arg_bool(void* vm, int i); -void lua_push_int(void* vm, long v); +void lua_push_int(void* vm, int64_t v); void lua_push_str(void* vm, const char* v); void lua_push_bool(void* vm, int v); void lua_push_nil(void* vm); void lua_push_tagged_table(void* vm, const char* field, const char* msg); int lua_raise_error(void* vm, const char* msg); -void lua_vm_set_instruction_limit(void* vm, long budget, int every); +void lua_vm_set_instruction_limit(void* vm, int64_t budget, int every); /* #910 host-callback table builder (RESP multibulk → Lua table). */ void lua_table_begin(void* vm, int narr, int nrec); void lua_table_seti_str(void* vm, int i, const char* v); -void lua_table_seti_int(void* vm, int i, long v); +void lua_table_seti_int(void* vm, int i, int64_t v); void lua_table_seti_bool(void* vm, int i, int v); void lua_table_set_str(void* vm, const char* key, const char* v); -void lua_table_set_int(void* vm, const char* key, long v); +void lua_table_set_int(void* vm, const char* key, int64_t v); void lua_table_set_bool(void* vm, const char* key, int v); void lua_table_end_seti(void* vm, int i); void lua_table_end_setfield(void* vm, const char* key); diff --git a/contrib/host/lua/module.ae b/contrib/host/lua/module.ae index 77ccafae3..d6f9f2414 100644 --- a/contrib/host/lua/module.ae +++ b/contrib/host/lua/module.ae @@ -69,7 +69,7 @@ const LUA_TOTHER = 6 // --- Tier 1: fire-and-forget ----------------------------------------------- extern lua_run_sandboxed(perms: sandbox.Grants, code: string) -> int -extern lua_run_sandboxed_with_map(perms: sandbox.Grants, code: string, map_token: int) -> int +extern lua_run_sandboxed_with_map(perms: sandbox.Grants, code: string, map_token: long) -> int extern lua_run(code: string) -> int extern lua_init() -> int extern lua_finalize() diff --git a/contrib/host/perl/module.ae b/contrib/host/perl/module.ae index a038ef5b1..f1e938f75 100644 --- a/contrib/host/perl/module.ae +++ b/contrib/host/perl/module.ae @@ -31,7 +31,7 @@ exports ( // Canonical `perl.*` shape (matches python.run / lua.run / etc.). @extern("aether_perl_run_sandboxed") perl_run_sandboxed(perms: sandbox.Grants, code: string) -> int -@extern("aether_perl_run_sandboxed_with_map") perl_run_sandboxed_with_map(perms: sandbox.Grants, code: string, map_token: int) -> int +@extern("aether_perl_run_sandboxed_with_map") perl_run_sandboxed_with_map(perms: sandbox.Grants, code: string, map_token: long) -> int @extern("aether_perl_run") perl_run(code: string) -> int @extern("aether_perl_init") perl_init() -> int @extern("aether_perl_finalize") perl_finalize() diff --git a/contrib/host/python/module.ae b/contrib/host/python/module.ae index 6384c8f2d..e9dd4b6af 100644 --- a/contrib/host/python/module.ae +++ b/contrib/host/python/module.ae @@ -24,7 +24,7 @@ exports ( // Run Python code string within a sandbox // Returns: 0 on success, -1 on error extern python_run_sandboxed(perms: sandbox.Grants, code: string) -> int -extern python_run_sandboxed_with_map(perms: sandbox.Grants, code: string, map_token: int) -> int +extern python_run_sandboxed_with_map(perms: sandbox.Grants, code: string, map_token: long) -> int // Run Python code string without sandbox (full access) // Returns: 0 on success, -1 on error diff --git a/contrib/host/racket/module.ae b/contrib/host/racket/module.ae index d007efefb..e261af68a 100644 --- a/contrib/host/racket/module.ae +++ b/contrib/host/racket/module.ae @@ -40,7 +40,7 @@ extern racket_run_sandboxed(perms: sandbox.Grants, code: string) -> int // First-class shared map: the Racket script reads/writes the Aether-owned // shared map live via aether-map-get / aether-map-put, so keys it discovers // at runtime come back too. Enumerate with map.keys(map). -extern racket_run_sandboxed_with_map(perms: sandbox.Grants, code: string, map_token: int) -> int +extern racket_run_sandboxed_with_map(perms: sandbox.Grants, code: string, map_token: long) -> int extern racket_run(code: string) -> int // Evaluate `code` in the persistent VM; returns its captured stdout as a // string ("" on failure). State persists across calls. The string is a diff --git a/contrib/host/rhombus/module.ae b/contrib/host/rhombus/module.ae index 3b0af5d1d..73cecd02f 100644 --- a/contrib/host/rhombus/module.ae +++ b/contrib/host/rhombus/module.ae @@ -48,7 +48,7 @@ extern rhombus_run_sandboxed(perms: sandbox.Grants, code: string) -> int // First-class shared map: the Rhombus script reads/writes the Aether-owned // shared map live via aether-map-get / aether-map-put. Enumerate with // map.keys(map). -extern rhombus_run_sandboxed_with_map(perms: sandbox.Grants, code: string, map_token: int) -> int +extern rhombus_run_sandboxed_with_map(perms: sandbox.Grants, code: string, map_token: long) -> int extern rhombus_run(code: string) -> int // Evaluate `code` in the persistent VM; returns its captured stdout as a // string ("" on failure). State persists across calls. The string is a diff --git a/contrib/host/ruby/module.ae b/contrib/host/ruby/module.ae index a8be4d549..ed3492073 100644 --- a/contrib/host/ruby/module.ae +++ b/contrib/host/ruby/module.ae @@ -21,7 +21,7 @@ exports ( ) extern ruby_run_sandboxed(perms: sandbox.Grants, code: string) -> int -extern ruby_run_sandboxed_with_map(perms: sandbox.Grants, code: string, map_token: int) -> int +extern ruby_run_sandboxed_with_map(perms: sandbox.Grants, code: string, map_token: long) -> int extern ruby_run(code: string) -> int extern ruby_init_host() -> int extern ruby_finalize_host() diff --git a/contrib/host/tinygo/aether_host_tinygo.c b/contrib/host/tinygo/aether_host_tinygo.c index 42691ad6c..5024f7c8d 100644 --- a/contrib/host/tinygo/aether_host_tinygo.c +++ b/contrib/host/tinygo/aether_host_tinygo.c @@ -215,7 +215,7 @@ static ffi_type* kind_to_ffi(char k) { } int tinygo_call_dynamic(void* handle, const char* sym, - char return_kind, const char* arg_kinds, + int return_kind, const char* arg_kinds, void** args, void* result_out) { void* fn = tg_resolve(handle, sym); if (!fn) return 0; @@ -264,7 +264,7 @@ int tinygo_call_dynamic(void* handle, const char* sym, #else /* !AETHER_HAS_LIBFFI */ int tinygo_call_dynamic(void* handle, const char* sym, - char return_kind, const char* arg_kinds, + int return_kind, const char* arg_kinds, void** args, void* result_out) { (void)handle; (void)sym; (void)return_kind; (void)arg_kinds; (void)args; (void)result_out; diff --git a/contrib/host/tinygo/aether_host_tinygo.h b/contrib/host/tinygo/aether_host_tinygo.h index 7900d44ab..51629106c 100644 --- a/contrib/host/tinygo/aether_host_tinygo.h +++ b/contrib/host/tinygo/aether_host_tinygo.h @@ -110,14 +110,15 @@ struct AetherString* tinygo_call_str_str_owned (void* h, const char* s, const c // // kinds: a NUL-terminated array of single-character codes // 'v' void 'i' int32 'l' int64 'd' double 's' const char* 'p' void* -// return_kind is one of the same codes. +// return_kind is one of the same codes, passed as an int (the width the +// Aether extern gives it, #3097). // args is an array of argument-value pointers; each pointer points // to a value of the matching kind. The result (int64-sized buffer) // is written into *result_out. // // Returns 1 on success, 0 on failure (consult last_error). int tinygo_call_dynamic(void* handle, const char* sym, - char return_kind, const char* arg_kinds, + int return_kind, const char* arg_kinds, void** args, void* result_out); #endif diff --git a/contrib/jq/ast.ae b/contrib/jq/ast.ae index 2558b168a..1baf3ad6f 100644 --- a/contrib/jq/ast.ae +++ b/contrib/jq/ast.ae @@ -182,7 +182,7 @@ free_node(n: ptr) { list.free(work) } -_push_child(work: ptr, child: ptr) { +_push_child(work: ptr, child: @keep ptr) { if child != null { list.add_raw(work, child) } } diff --git a/contrib/jq/eval.ae b/contrib/jq/eval.ae index 9f137f690..2a4dc7a7e 100644 --- a/contrib/jq/eval.ae +++ b/contrib/jq/eval.ae @@ -1714,7 +1714,7 @@ _bind_alternatives(ctx: *Ctx, patterns: ptr, v: @keep ptr, env: @keep ptr, s: *S } // The P_VAR / P_ENTRY-with-$name nodes of a pattern, appended to `names`. -_collect_pattern_vars(pat: ptr, names: ptr) { +_collect_pattern_vars(pat: @keep ptr, names: ptr) { pn = pat as *Node if pn.kind == ast.P_VAR { list.add_raw(names, pat) @@ -3637,9 +3637,9 @@ _contains(a: ptr, b: ptr) -> int { // Codepoint offsets of every occurrence of `sub` in `s` (overlapping). _str_indices(s: string, sub: string) -> ptr { - out = json.arr() m = string.length(sub) if m == 0 { return json.null_value() } + out = json.arr() start = 0 while 1 == 1 { idx = string.index_of_from(s, sub, start) @@ -3647,7 +3647,6 @@ _str_indices(s: string, sub: string) -> ptr { json.array_add_raw(out, json.from_int(value.codepoint_of_byte(s, idx) as long)) start = idx + 1 } - return out } _flatten_into(out: ptr, arr: ptr, depth: int) { diff --git a/contrib/jq/lexer.ae b/contrib/jq/lexer.ae index 69698b6b9..36bf8188a 100644 --- a/contrib/jq/lexer.ae +++ b/contrib/jq/lexer.ae @@ -141,7 +141,6 @@ is_keyword(s: string) -> int { "__loc__" -> { return 1 } _ -> { return 0 } } - return 0 } // --- character classes --------------------------------------------- diff --git a/contrib/jq/module.ae b/contrib/jq/module.ae index 89ca24ca3..cdf8377a5 100644 --- a/contrib/jq/module.ae +++ b/contrib/jq/module.ae @@ -23,7 +23,7 @@ // input, _ = json.parse("[{\"name\":\"ann\",\"age\":41},{\"name\":\"bob\",\"age\":7}]") // out, err = jq.run_json(prog, input) // one line per output // print(out) // "ann" -// json.json_free(input) +// json.free(input) // jq.free(prog) // } // diff --git a/contrib/jq/test_after_jsonpath.ae b/contrib/jq/test_after_jsonpath.ae index 0969e0055..f2978244a 100644 --- a/contrib/jq/test_after_jsonpath.ae +++ b/contrib/jq/test_after_jsonpath.ae @@ -34,7 +34,7 @@ main() { spec.assert_str_eq(err2, "", "no jsonpath error on descent") spec.assert_eq(jsonpath.result_size(r2), 1, "one match") jsonpath.free_result(r2) - json.json_free(doc) + json.free(doc) } } diff --git a/contrib/jq/test_with_jsonpath.ae b/contrib/jq/test_with_jsonpath.ae index 44161bab4..657eeab36 100644 --- a/contrib/jq/test_with_jsonpath.ae +++ b/contrib/jq/test_with_jsonpath.ae @@ -34,7 +34,7 @@ main() { spec.assert_str_eq(err2, "", "no jsonpath error on descent") spec.assert_eq(jsonpath.result_size(r2), 1, "one match") jsonpath.free_result(r2) - json.json_free(doc) + json.free(doc) } } diff --git a/contrib/jq/value.ae b/contrib/jq/value.ae index 8a0eb6e6f..34c3c0c69 100644 --- a/contrib/jq/value.ae +++ b/contrib/jq/value.ae @@ -113,7 +113,7 @@ string_value(v: ptr) -> string { return json.get_string_raw(v) } -// Deep copy. Owned by the caller; free with json.json_free. +// Deep copy. Owned by the caller; free with json.free. copy(v: ptr) -> ptr { t = type_of(v) if t == T_NULL { return json.null_value() } diff --git a/contrib/parsers/xml_expat/module.ae b/contrib/parsers/xml_expat/module.ae index 89d6134b7..0ac1d196e 100644 --- a/contrib/parsers/xml_expat/module.ae +++ b/contrib/parsers/xml_expat/module.ae @@ -259,19 +259,19 @@ extern xml_parser_install_handlerset(p: ptr, hs: ptr) // The first `env: ptr` parameter is required by the dispatcher's // ABI but is unused in the user's body (the closure already has // captured-variable access via its own env). -bind_start(_ctx: ptr, cb: fn) { +bind_start(_ctx: ptr, cb: @keep fn) { boxed = box_closure(cb) - map_put_raw(_ctx, "start", boxed) + map.put(_ctx, "start", boxed) } -bind_end(_ctx: ptr, cb: fn) { +bind_end(_ctx: ptr, cb: @keep fn) { boxed = box_closure(cb) - map_put_raw(_ctx, "end", boxed) + map.put(_ctx, "end", boxed) } -bind_text(_ctx: ptr, cb: fn) { +bind_text(_ctx: ptr, cb: @keep fn) { boxed = box_closure(cb) - map_put_raw(_ctx, "text", boxed) + map.put(_ctx, "text", boxed) } // One-shot: parse an in-memory document with closure-based @@ -296,15 +296,15 @@ builder parse_with(input: string): int { if _builder != null { if map_has(_builder, "start") == 1 { - boxed = map_get_raw(_builder, "start") + boxed = map.lookup(_builder, "start") xml_handlerset_set_start(hs, boxed) } if map_has(_builder, "end") == 1 { - boxed = map_get_raw(_builder, "end") + boxed = map.lookup(_builder, "end") xml_handlerset_set_end(hs, boxed) } if map_has(_builder, "text") == 1 { - boxed = map_get_raw(_builder, "text") + boxed = map.lookup(_builder, "text") xml_handlerset_set_text(hs, boxed) } } diff --git a/contrib/sqlite/module.ae b/contrib/sqlite/module.ae index 14e5e6280..ba88c5c7f 100644 --- a/contrib/sqlite/module.ae +++ b/contrib/sqlite/module.ae @@ -252,7 +252,7 @@ extern sqlite_errmsg_raw(db: ptr) -> string // Length-aware AetherString constructor — re-used from std.string. // Lets `column_blob` return a binary-safe AetherString rather than // a strlen-truncated copy. -extern string_new_with_length(data: ptr, length: int) -> string @heap +extern string_new_with_length(data: ptr, length: long) -> string @heap // ---- Go-style wrappers ---- diff --git a/contrib/templating/liquid/module.ae b/contrib/templating/liquid/module.ae index 9b13881e8..22b106f75 100644 --- a/contrib/templating/liquid/module.ae +++ b/contrib/templating/liquid/module.ae @@ -25,6 +25,7 @@ import std.string import std.strbuilder import std.map import std.collections +import std.list import std.cryptography import std.encoding import std.fs @@ -377,15 +378,15 @@ lex(src: string) -> (ptr, string) { string_list_add(kinds, TOK_TEXT) string_list_add(contents, tail) toks = list_new() - list_add_raw(toks, kinds) - list_add_raw(toks, contents) + list.add(toks, kinds) + list.add(toks, contents) return toks, string.concat("unterminated `{{`", loc_suffix(src, n, open_at)) } raw_body = string.substring_n(src, n, open_at + 2, close_at) if has_control_char(raw_body) == 1 { toks = list_new() - list_add_raw(toks, kinds) - list_add_raw(toks, contents) + list.add(toks, kinds) + list.add(toks, contents) return toks, string.concat("control character inside a tag", loc_suffix(src, n, open_at)) } body, ls = peel_left_dash(raw_body) @@ -404,15 +405,15 @@ lex(src: string) -> (ptr, string) { string_list_add(kinds, TOK_TEXT) string_list_add(contents, tail) toks = list_new() - list_add_raw(toks, kinds) - list_add_raw(toks, contents) + list.add(toks, kinds) + list.add(toks, contents) return toks, string.concat("unterminated `{%`", loc_suffix(src, n, open_at)) } raw_body = string.substring_n(src, n, open_at + 2, close_at) if has_control_char(raw_body) == 1 { toks = list_new() - list_add_raw(toks, kinds) - list_add_raw(toks, contents) + list.add(toks, kinds) + list.add(toks, contents) return toks, string.concat("control character inside a tag", loc_suffix(src, n, open_at)) } body, ls = peel_left_dash(raw_body) @@ -440,8 +441,8 @@ lex(src: string) -> (ptr, string) { end_at, end_close = find_block_end(src, n, pos_after, "endcomment") if end_at < 0 { toks = list_new() - list_add_raw(toks, kinds) - list_add_raw(toks, contents) + list.add(toks, kinds) + list.add(toks, contents) return toks, string.concat("unterminated `{% comment %}`", loc_suffix(src, n, open_at)) } // The opener's right-strip would consume the very @@ -458,8 +459,8 @@ lex(src: string) -> (ptr, string) { end_at, end_close = find_block_end(src, n, pos_after, "endraw") if end_at < 0 { toks = list_new() - list_add_raw(toks, kinds) - list_add_raw(toks, contents) + list.add(toks, kinds) + list.add(toks, contents) return toks, string.concat("unterminated `{% raw %}`", loc_suffix(src, n, open_at)) } raw_text = string.substring_n(src, n, pos_after, end_at) @@ -486,8 +487,8 @@ lex(src: string) -> (ptr, string) { lerr2 = emit_liquid_lines(kinds, contents, inner_body) if lerr2 != "" { toks = list_new() - list_add_raw(toks, kinds) - list_add_raw(toks, contents) + list.add(toks, kinds) + list.add(toks, contents) return toks, lerr2 } if rs == 1 { strip_next = 1 } @@ -504,8 +505,8 @@ lex(src: string) -> (ptr, string) { } } toks = list_new() - list_add_raw(toks, kinds) - list_add_raw(toks, contents) + list.add(toks, kinds) + list.add(toks, contents) return toks, "" } @@ -2465,7 +2466,7 @@ filter_date(ctx: ptr, val: string, format: string) -> (string, string) { if map_has(ctx, KEY_NOW) == 0 { return value_nil(), "date: \"now\" needs the time: call context_set_now first" } - e, _nerr = string.to_long(map_get_raw(ctx, KEY_NOW)) + e, _nerr = string.to_long(map.lookup(ctx, KEY_NOW)) epoch = e } else if all_digits(s) == 1 { e, eerr = string.to_long(s) @@ -2617,8 +2618,8 @@ parse_file(path: string) -> (ptr, string) { // handed to a partial cache belongs to the cache. template_free(t: ptr) { if t == null { return } - string_list_free(list_get_raw(t, 0)) - string_list_free(list_get_raw(t, 1)) + string_list_free(list.at(t, 0)) + string_list_free(list.at(t, 1)) list_free(t) } @@ -2661,7 +2662,7 @@ partial_cache_free(cache: ptr) { n = list_size(c.owned) i = 0 while i < n { - template_free(list_get_raw(c.owned, i)) + template_free(list.at(c.owned, i)) i = i + 1 } list_free(c.owned) @@ -2679,7 +2680,7 @@ partial_cache_key() -> string { // Render with `cache`: every include, render and layout parent the context // reaches is parsed once and reused, including through `{% render %}`'s // fresh contexts. -context_set_partial_cache(ctx: ptr, cache: ptr) { +context_set_partial_cache(ctx: ptr, cache: @keep ptr) { map_put(ctx, partial_cache_key(), cache) } @@ -2690,11 +2691,11 @@ context_set_partial_cache(ctx: ptr, cache: ptr) { load_partial(ctx: ptr, resolved: string) -> (ptr, string, string, int) { key = partial_cache_key() cache = null - if map_has(ctx, key) == 1 { cache = map_get_raw(ctx, key) } + if map_has(ctx, key) == 1 { cache = map.lookup(ctx, key) } if cache != null { c = cache as *PartialCache if map_has(c.by_path, resolved) == 1 { - return map_get_raw(c.by_path, resolved), "", "", 0 + return map.lookup(c.by_path, resolved), "", "", 0 } } src, ferr = fs.read(resolved) @@ -2705,7 +2706,7 @@ load_partial(ctx: ptr, resolved: string) -> (ptr, string, string, int) { if cache != null { c2 = cache as *PartialCache map_put(c2.by_path, resolved, t) - list_add_raw(c2.owned, t) + list.add(c2.owned, t) return t, "", "", 0 } return t, "", "", 1 @@ -2747,20 +2748,20 @@ context_set_now(ctx: ptr, unix_seconds: long) { // Free the context: its bindings, and every array and object it holds. context_free(ctx: ptr) { - if map_has(ctx, KEY_VALUES) == 1 { store_free(map_get_raw(ctx, KEY_VALUES)) } + if map_has(ctx, KEY_VALUES) == 1 { store_free(map.lookup(ctx, KEY_VALUES)) } bindings_free(ctx) } // Free a context's own maps -- its bare and its typed bindings -- but not a // value store it may share (a `{% render %}` context's is its caller's). bindings_free(ctx: ptr) { - if map_has(ctx, KEY_TYPED) == 1 { map_free(map_get_raw(ctx, KEY_TYPED)) } + if map_has(ctx, KEY_TYPED) == 1 { map_free(map.lookup(ctx, KEY_TYPED)) } map_free(ctx) } // The map of `ctx`'s typed bindings (name -> packed value), made on first use. typed_of(ctx: ptr) -> ptr { - have = map_get_raw(ctx, KEY_TYPED) + have = map.lookup(ctx, KEY_TYPED) if have != null { return have } typed = map_new() map_put(ctx, KEY_TYPED, typed) @@ -2787,12 +2788,12 @@ lookup_value(ctx: ptr, name: string) -> string { if nn == 0 || string.char_at_n(name, nn, 0) < 32 { return value_nil() } // A stored value is never null, so a null from map_get_raw is "not // bound" -- one hash lookup a step rather than map_has and then a get. - typed = map_get_raw(ctx, KEY_TYPED) + typed = map.lookup(ctx, KEY_TYPED) if typed != null { - v = map_get_raw(typed, name) + v = map.lookup(typed, name) if v != null { return v } } - bare = map_get_raw(ctx, name) + bare = map.lookup(ctx, name) if bare != null { return bare } return value_nil() } @@ -2816,7 +2817,7 @@ bump_counter(ctx: ptr, name: string, direction: int) -> (string, string) { map_put(ctx, key, "-1") return "-1", "" } - cur_s = map_get_raw(ctx, key) + cur_s = map.lookup(ctx, key) cur, err = string.to_int(cur_s) if err != "" { return "", string.concat("counter not an integer: ", cur_s) @@ -2864,7 +2865,7 @@ cycle_emit(ctx: ptr, rest: string, full_body: string) -> (string, string) { key = string.concat("\x01cyc:", full_body) pos = 0 if map_has(ctx, key) == 1 { - cur, err = string.to_int(map_get_raw(ctx, key)) + cur, err = string.to_int(map.lookup(ctx, key)) if err == "" { pos = cur } } if pos >= ic { pos = pos % ic } @@ -2983,7 +2984,7 @@ eval_value(ctx: ptr, text: string) -> (string, string) { // `block.super` inside a layout child block is the parent's content for // that block; outside an override it is nil, not an error. if string.equals(a, "block.super") == 1 { - if map_has(ctx, KEY_BLOCK_SUPER) == 1 { return value_str(map_get_raw(ctx, KEY_BLOCK_SUPER)), "" } + if map_has(ctx, KEY_BLOCK_SUPER) == 1 { return value_str(map.lookup(ctx, KEY_BLOCK_SUPER)), "" } return value_nil(), "" } // A number literal keeps the text it was written with. @@ -3706,7 +3707,6 @@ render_if_block(kinds: ptr, contents: ptr, start: int, end: int, } branch_start = nxt + 1 } - return start, "unreachable" } // Render a `{% case x %}{% when v %}...{% when w %}...{% else %}...{% endcase %}` @@ -3777,7 +3777,6 @@ render_case_block(kinds: ptr, contents: ptr, start: int, end: int, } branch_start = nxt + 1 } - return start, "unreachable" } // Parse an integer modifier `limit:N` / `offset:N` / `cols:N` from a tag's @@ -3853,7 +3852,7 @@ has_reversed(rest: string) -> int { save_value(ctx: ptr, name: string) -> (string, int) { typed = typed_of(ctx) if map_has(typed, name) == 0 { return "", 0 } - return string.copy(map_get_raw(typed, name)), 1 + return string.copy(map.lookup(typed, name)), 1 } // Inverse of save_value. @@ -4309,13 +4308,13 @@ const KEY_NOW = "\x01now" // Read the include-root from ctx. Returns ("", 0) when unset. get_include_root(ctx: ptr) -> (string, int) { if map_has(ctx, KEY_INCLUDE_ROOT) == 0 { return "", 0 } - return map_get_raw(ctx, KEY_INCLUDE_ROOT), 1 + return map.lookup(ctx, KEY_INCLUDE_ROOT), 1 } // Read the current include-depth. 0 if unset. get_include_depth(ctx: ptr) -> int { if map_has(ctx, KEY_INCLUDE_DEPTH) == 0 { return 0 } - v = map_get_raw(ctx, KEY_INCLUDE_DEPTH) + v = map.lookup(ctx, KEY_INCLUDE_DEPTH) n, _ = string.to_int(v) return n } @@ -4461,8 +4460,8 @@ render_include_tag(outer_ctx: @keep ptr, rest: string, strict: int) -> (string, // host gave, so `date` reads the same "now". map_put(child, KEY_INCLUDE_ROOT, root) pkey = partial_cache_key() - if map_has(outer_ctx, pkey) == 1 { map_put(child, pkey, map_get_raw(outer_ctx, pkey)) } - if map_has(outer_ctx, KEY_NOW) == 1 { map_put(child, KEY_NOW, string.copy(map_get_raw(outer_ctx, KEY_NOW))) } + if map_has(outer_ctx, pkey) == 1 { map_put(child, pkey, map.lookup(outer_ctx, pkey)) } + if map_has(outer_ctx, KEY_NOW) == 1 { map_put(child, KEY_NOW, string.copy(map.lookup(outer_ctx, KEY_NOW))) } } // Bump depth on the child ctx for both forms. map_put(child, KEY_INCLUDE_DEPTH, string.from_int(depth + 1)) @@ -4614,7 +4613,7 @@ render_block_from(chain_kinds: ptr, chain_contents: ptr, chain_blocks: ptr, leve ctx: @keep ptr) -> (string, string) { lv = from while lv < levels { - if map_has(list_get_raw(chain_blocks, lv), name) == 1 { break } + if map_has(list.at(chain_blocks, lv), name) == 1 { break } lv = lv + 1 } if lv >= levels { @@ -4624,7 +4623,7 @@ render_block_from(chain_kinds: ptr, chain_contents: ptr, chain_blocks: ptr, leve lv + 1, name, base_kinds, base_contents, base_lo, base_hi, ctx) if serr != "" { return "", serr } - packed = map_get_raw(list_get_raw(chain_blocks, lv), name) + packed = map.lookup(list.at(chain_blocks, lv), name) pn = string_length(packed) comma = string.index_of_from_n(packed, pn, ",", 0) lo, _ = string.to_int(string.substring_n(packed, pn, 0, comma)) @@ -4633,7 +4632,7 @@ render_block_from(chain_kinds: ptr, chain_contents: ptr, chain_blocks: ptr, leve prior = "" if had_prior == 1 { prior, _ = map.get_string(ctx, KEY_BLOCK_SUPER) } map_put(ctx, KEY_BLOCK_SUPER, super_text) - inner, rerr = render_range(list_get_raw(chain_kinds, lv), list_get_raw(chain_contents, lv), + inner, rerr = render_range(list.at(chain_kinds, lv), list.at(chain_contents, lv), lo, hi, ctx) if had_prior == 1 { map_put(ctx, KEY_BLOCK_SUPER, prior) @@ -4704,7 +4703,7 @@ free_layout_chain(chain_kinds: ptr, chain_contents: ptr, chain_blocks: ptr, nb = list_size(chain_blocks) i = 0 while i < nb { - map_free(list_get_raw(chain_blocks, i)) + map_free(list.at(chain_blocks, i)) i = i + 1 } list_free(chain_blocks) @@ -4714,7 +4713,7 @@ free_layout_chain(chain_kinds: ptr, chain_contents: ptr, chain_blocks: ptr, n = list_size(owned) i = 0 while i < n { - template_free(list_get_raw(owned, i)) + template_free(list.at(owned, i)) i = i + 1 } list_free(owned) @@ -4761,9 +4760,9 @@ render_layout_template(child_kinds: ptr, child_contents: ptr, child_n: int, free_layout_chain(chain_kinds, chain_contents, chain_blocks, seen, owned_parents) return "", cerr } - list_add_raw(chain_kinds, cur_kinds) - list_add_raw(chain_contents, cur_contents) - list_add_raw(chain_blocks, blocks) + list.add(chain_kinds, cur_kinds) + list.add(chain_contents, cur_contents) + list.add(chain_blocks, blocks) levels = levels + 1 resolved, perr = resolve_partial_path(root, cur_parent) @@ -4786,7 +4785,7 @@ render_layout_template(child_kinds: ptr, child_contents: ptr, child_n: int, free_layout_chain(chain_kinds, chain_contents, chain_blocks, seen, owned_parents) return "", string.concat("layout: cannot read parent: ", cur_parent) } - if owned == 1 { list_add_raw(owned_parents, parent_t) } + if owned == 1 { list.add(owned_parents, parent_t) } if terr != "" { msg = string.concat("layout parse error in ", cur_parent) msg = string.concat(msg, ": ") @@ -4794,8 +4793,8 @@ render_layout_template(child_kinds: ptr, child_contents: ptr, child_n: int, free_layout_chain(chain_kinds, chain_contents, chain_blocks, seen, owned_parents) return "", msg } - cur_kinds = list_get_raw(parent_t, 0) - cur_contents = list_get_raw(parent_t, 1) + cur_kinds = list.at(parent_t, 0) + cur_contents = list.at(parent_t, 1) cur_n = string_list_size(cur_kinds) next_lay, next_name, lerr = layout_target(cur_kinds, cur_contents, cur_n) if lerr != "" { @@ -4847,8 +4846,8 @@ render(t: ptr, ctx: @keep ptr) -> (string, string) { } render_template(t: ptr, ctx: @keep ptr) -> (string, string) { - kinds = list_get_raw(t, 0) - contents = list_get_raw(t, 1) + kinds = list.at(t, 0) + contents = list.at(t, 1) n = string_list_size(kinds) // A template whose first tag is `{% layout 'parent' %}` renders through @@ -5067,9 +5066,8 @@ struct LiquidNode { struct LiquidStore { nodes: ptr // list: id -> *LiquidNode, null for an id that is free - free_ids: ptr // intarr used as a stack of the free ids + free_ids: int[] // a stack of the free ids; its .len is the capacity free_len: int - free_cap: int live: int // nodes held made: int // nodes made since the last collection depth: int // renders in progress over the contexts sharing this store @@ -5078,14 +5076,13 @@ struct LiquidStore { store_new() -> ptr { st = heap.new(LiquidStore) st.nodes = list_new() - st.free_cap = 16 - st.free_ids = intarr_new_raw(16) + st.free_ids = make([]int, 16) return st as ptr } // The store `ctx` keeps its arrays and objects in, made on first use. store_of(ctx: ptr) -> ptr { - have = map_get_raw(ctx, KEY_VALUES) + have = map.lookup(ctx, KEY_VALUES) if have != null { return have } st = store_new() map_put(ctx, KEY_VALUES, st) @@ -5104,12 +5101,12 @@ store_free(p: ptr) { n = list_size(st.nodes) i = 0 while i < n { - nd = list_get_raw(st.nodes, i) + nd = list.at(st.nodes, i) if nd != null { node_free(nd) } i = i + 1 } list_free(st.nodes) - intarr_free(st.free_ids) + free(st.free_ids) heap.free(st) } @@ -5124,11 +5121,11 @@ node_new(ctx: ptr, kind: int) -> (string, ptr) { id = 0 if st.free_len > 0 { st.free_len = st.free_len - 1 - id = intarr_get_raw(st.free_ids, st.free_len) + id = st.free_ids[st.free_len] list_set(st.nodes, id, nd as ptr) } else { id = list_size(st.nodes) - list_add_raw(st.nodes, nd as ptr) + list.add(st.nodes, nd as ptr) } st.live = st.live + 1 st.made = st.made + 1 @@ -5145,7 +5142,7 @@ store_node(sp: ptr, v: string) -> ptr { st = sp as *LiquidStore id, err = string.to_int(value_payload(v)) if err != "" || id < 0 || id >= list_size(st.nodes) { return null } - p = list_get_raw(st.nodes, id) + p = list.at(st.nodes, id) if p == null { return null } nd = p as *LiquidNode if nd.kind != k { return null } @@ -5154,7 +5151,7 @@ store_node(sp: ptr, v: string) -> ptr { // The node `v` names in `ctx`'s store (store_node). node_of(ctx: ptr, v: string) -> ptr { - return store_node(map_get_raw(ctx, KEY_VALUES), v) + return store_node(map.lookup(ctx, KEY_VALUES), v) } arr_len(nd: ptr) -> int { return string_list_size((nd as *LiquidNode).items) } @@ -5180,25 +5177,24 @@ obj_replace(nd: ptr, key: string, v: string) { // (value, 1) for a key the object has, (nil, 0) otherwise. obj_get(nd: ptr, key: string) -> (string, int) { o = nd as *LiquidNode - v = map_get_raw(o.fields, key) + v = map.lookup(o.fields, key) if v == null { return value_nil(), 0 } return v, 1 } // Keep `id` for the next node. store_push_free(st: *LiquidStore, id: int) { - if st.free_len == st.free_cap { - grown = intarr_new_raw(st.free_cap * 2) + if st.free_len == st.free_ids.len { + grown = make([]int, st.free_ids.len * 2) i = 0 while i < st.free_len { - intarr_set_raw(grown, i, intarr_get_raw(st.free_ids, i)) + grown[i] = st.free_ids[i] i = i + 1 } - intarr_free(st.free_ids) + free(st.free_ids) st.free_ids = grown - st.free_cap = st.free_cap * 2 } - intarr_set_raw(st.free_ids, st.free_len, id) + st.free_ids[st.free_len] = id st.free_len = st.free_len + 1 } @@ -5214,11 +5210,11 @@ store_collect(ctx: ptr, sp: ptr) { st = sp as *LiquidStore pending = string_list_new() typed = typed_of(ctx) - keys = map_keys_raw(typed) - nk = map_keys_size_raw(keys) + keys, _kerr = map.keys(typed) + nk = map.keys_size(keys) i = 0 while i < nk { - v = map_get_raw(typed, map_keys_get_raw(keys, i)) + v = map.lookup(typed, map.keys_get(keys, i)) if is_node_value(v) == 1 { string_list_add(pending, v) } i = i + 1 } @@ -5251,7 +5247,7 @@ store_collect(ctx: ptr, sp: ptr) { total = list_size(st.nodes) id = 0 while id < total { - q = list_get_raw(st.nodes, id) + q = list.at(st.nodes, id) if q != null { held = q as *LiquidNode if held.marked == 1 { @@ -5274,7 +5270,7 @@ store_collect(ctx: ptr, sp: ptr) { // last collection. For measuring; a template never needs it. context_value_nodes(ctx: ptr) -> int { if map_has(ctx, KEY_VALUES) == 0 { return 0 } - return (map_get_raw(ctx, KEY_VALUES) as *LiquidStore).live + return (map.lookup(ctx, KEY_VALUES) as *LiquidStore).live } // ---- Reading values: text, output, truthiness, equality, order ------------- diff --git a/contrib/tinyweb/README.md b/contrib/tinyweb/README.md index 1318ced79..f4d3062f7 100644 --- a/contrib/tinyweb/README.md +++ b/contrib/tinyweb/README.md @@ -15,7 +15,7 @@ server = web_server_host("localhost", 8080) { response_write(res, "Hello, World!") } web_socket("/eee") |msg, sender, ctx| { - ws_send_frame(sender, "Echo: ${msg}") + http.ws_send_text(sender, "Echo: ${msg}") } } @@ -25,7 +25,7 @@ server = web_server_host("localhost", 8080) { response_write(res, "User: ${request_get_path_param(ctx, \"1\")}") } } -server_start(server) +err = tw_start(server) ``` ## Features @@ -34,15 +34,49 @@ server_start(server) - **Path nesting**: `path("/api") { path("/v1") { end_point(...) } }` - **Filters**: `filter(GET, "/.*") |req, res, ctx| { CONTINUE / STOP }` - **Composition**: `compose(server) { ... }` to add routes after construction -- **WebSocket**: server + client with RFC 6455 handshake +- **WebSocket**: `web_socket("/ws") |msg, sender, ctx| { ... }`, on the + server's own port - **SSE**: `sse_endpoint("/events") |stream| { sse_send(stream, data) }` - **Static files**: MIME detection, traversal protection, index.html fallback - **Chunked transfer**: `chunked_start`, `write_chunk`, `chunked_end` - **Cookies**: `request_get_cookie(req, "name")` - **Attributes**: `ctx_set_attribute` / `ctx_get_attribute` for filter-to-endpoint data -- **Error callbacks**: `on_error`, `on_server_error`, `on_ws_timeout`, `on_ws_error` - **Statistics**: `on_stats(server) |stats| { ... }` -- **Config**: `with_timeout`, `with_backlog`, `with_ws_backlog`, `with_keep_alive`, `with_tls` +- **Config**: `with_timeout`, `with_keep_alive`, `with_tls` + +### WebSocket, SSE and chunked responses + +All three are std.http's, served on the same port as the HTTP routes, TLS +included: + +- A `web_socket` handler runs once per message. `msg` is the whole message, + of any length, a binary one zero bytes and all; `sender` is the + connection's std.http WebSocket (reply with `http.ws_send_text` or + `http.ws_send_binary`); `ctx` is a map the connection's messages share. + Pings are answered, and a quiet connection stays open. +- An `sse_endpoint` handler gets the stream and runs until it returns. + `sse_send`, `sse_send_event` and `sse_comment` return 0, or -1 once the + client has gone, so a handler that streams in a loop knows when to stop. +- An endpoint streams a body of unknown length with + `stream, err = chunked_start(res, status)`, `write_chunk(stream, data)` + and `chunked_end(stream)`. The headers set on `res` before it go out with + it; a response that already has a body is refused. + +### Statistics + +`on_stats(server) |stats| { ... }` runs after each request to an +`end_point` with a record of it: `stats_get_path`, `stats_get_endpoint` (the +route's pattern), `stats_get_status`, `stats_get_duration` and +`stats_get_endpoint_duration` (milliseconds), and `stats_get_filters`, a +list of the filters that ran (`filter_stat_path`, `filter_stat_result`, +"CONTINUE" or "STOP", and `filter_stat_duration`). + +### Timeouts and keep-alive + +`with_timeout(server, ms)` is how long a connection may wait for its next +request (30000 by default); an upgraded WebSocket or an SSE stream is not +held to it. `with_keep_alive(server, 0)` closes each connection after its +response. ### TLS @@ -128,12 +162,9 @@ leaf: its block, if any, is a request-time handler, not more DSL. ## Files - `module.ae`: The library -- `ws_client.ae`: WebSocket client -- `ws_handshake.c` (SHA-1 + Base64 for the WebSocket accept key, plus - `ws_unmask` (RFC 6455 §5.3 frame unmasking)) C externs - `example_app.ae`: HTTP endpoints with filters and nested paths - `example_composition.ae`: Modular composition, deep nesting, auth filters -- `example_websocket.ae`: HTTP + WebSocket server with echo handler +- `example_websocket.ae`: HTTP + WebSocket routes with echo handlers - `example_static.ae`: Static file serving - `example_sse.ae`: Server-Sent Events + chunked transfer - `example_auth.ae`: Cookie parsing + filter-to-endpoint attributes @@ -143,7 +174,5 @@ leaf: its block, if any, is a request-time handler, not more DSL. - `test_schema_api.ae`: schema_api integration tests (validation + schema/OpenAPI endpoints) - `test_spec.ae`: DSL registration unit tests - `test_integration.ae`: HTTP round-trip integration tests -- `test_websocket.ae`: WebSocket codec round-trip (handshake, client-frame - unmasking, and extended-length server framing). Its C extern, - `ws_handshake.c`, is compiled in by the module's `@source`, so - `ae run test_websocket.ae` needs no `--extra` +- `test_streams.ae`: WebSocket, SSE and chunked endpoints and the server + options, over loopback diff --git a/contrib/tinyweb/example_schema_api.ae b/contrib/tinyweb/example_schema_api.ae index 60ad0afba..4738c3e72 100644 --- a/contrib/tinyweb/example_schema_api.ae +++ b/contrib/tinyweb/example_schema_api.ae @@ -22,9 +22,8 @@ import contrib.tinyweb import contrib.tinyweb.schema_api import std.schema import std.list -import std.http(http_post_raw, http_get_raw, http_response_status_code, - http_response_body_str, http_response_free) -import std.string(string_starts_with) +import std.http.client +import std.string extern exit(code: int) extern sleep(ms: int) @@ -53,25 +52,33 @@ actor SrvActor { } } } -actor Sched { - state x = 0 - receive { Ping() -> { x = 1 } } -} -message Ping {} +// The whole response through std.http.client: the showcase prints the +// status (201, 422) next to the body. Null when no response came back at +// all, which reads as status 0. +fn fetch(method: string, path: string, body: string) -> ptr { + req = client.request(method, "http://127.0.0.1:18096${path}") + if string.length(body) > 0 { + client.set_body(req, string.bytes(body), "application/json") + } + resp, err = client.send_request(req) + client.request_free(req) + if err != "" { println(" ${method} ${path}: ${err}") } + return resp +} fn get(path: string) -> ptr { - return http_get_raw("http://127.0.0.1:18096${path}") + return fetch("GET", path, "") } fn post(path: string, body: string) -> ptr { - return http_post_raw("http://127.0.0.1:18096${path}", body, "application/json") + return fetch("POST", path, body) } fn show(label: string, resp: ptr) { - st = http_response_status_code(resp) - body = http_response_body_str(resp) + st = client.response_status(resp) + body = client.response_body(resp) print(" ${label}: ${st}\n") print(" ${body}\n") - http_response_free(resp) + client.response_free(resp) } main() { @@ -102,7 +109,7 @@ main() { } schema.field("coupon", schema.STR) { schema.refine(| v: string | { - if string_starts_with(v, "SAVE") == 1 { + if string.starts_with(v, "SAVE") == 1 { return 1 } return 0 @@ -135,7 +142,6 @@ main() { schema_api.openapi_route("/openapi.json", registry) } - spawn(Sched()) a = spawn(SrvActor()) a ! StartSrv { srv: server } sleep(500) diff --git a/contrib/tinyweb/example_sse.ae b/contrib/tinyweb/example_sse.ae index 36c0a73a5..291ed2fe3 100644 --- a/contrib/tinyweb/example_sse.ae +++ b/contrib/tinyweb/example_sse.ae @@ -1,19 +1,12 @@ -// Server-Sent Events (SSE) Example — streaming events to browser clients +// Server-Sent Events (SSE) and chunked Example — streaming to browser clients import contrib.tinyweb -import std.http -import std.string -import std.list -import std.map -import std.tcp - main() { println("=== TinyWeb SSE + Chunked Example ===\n") - server = tinyweb.web_server_full("127.0.0.1", 8080, 0, 8082) { - - tinyweb.end_point(tinyweb.GET, "/") |req: ptr, res: ptr, ctx: ptr| { + server = tinyweb.web_server_host("127.0.0.1", 8080) { + tinyweb.end_point(tinyweb.GET, "/") | req: ptr, res: ptr, ctx: ptr | { tinyweb.response_set_header(res, "Content-Type", "text/html") tinyweb.response_write(res, " @@ -22,7 +15,7 @@ main() {

Server-Sent Events

diff --git a/docs/api/map.html b/docs/api/map.html index 46181816b..ca32a4095 100644 --- a/docs/api/map.html +++ b/docs/api/map.html @@ -16,6 +16,7 @@

Aether

Exports