core: add Core Direct string canaries (substring/charAt/replace); Stage‑B: alias table (ENV) support with BundleResolver; docs: Exit Code Policy tag→rc rules and checklist updates.

This commit is contained in:
nyash-codex
2025-11-02 16:24:50 +09:00
parent 8b006575c1
commit 0cd2342b05
14 changed files with 371 additions and 26 deletions

View File

@ -7,6 +7,62 @@ static box BundleResolver {
// - --require-mod Name must appear in named bundles ([bundle/missing] Name)
// - Merge order: bundle-src* → bundle-mod* (in given order)
resolve(bundle_srcs, bundle_names, bundle_mod_srcs, require_mods) {
// Alias table via env: HAKO_BUNDLE_ALIAS_TABLE / NYASH_BUNDLE_ALIAS_TABLE
// Format: entries separated by '|||', each entry as 'Name:code'
local table = env.get("HAKO_BUNDLE_ALIAS_TABLE")
if table == null || table == "" { table = env.get("NYASH_BUNDLE_ALIAS_TABLE") }
if table != null && table != "" {
local i = 0
loop(i < table.length()) {
// find next delimiter or end
local j = table.indexOf("|||", i)
local seg = ""
if j >= 0 { seg = table.substring(i, j) } else { seg = table.substring(i, table.length()) }
if seg != "" {
local pos = -1
local k = 0
loop(k < seg.length()) { if seg.substring(k,k+1) == ":" { pos = k break } k = k + 1 }
if pos >= 0 {
local name = seg.substring(0, pos)
local code = seg.substring(pos+1, seg.length())
if name != "" && code != "" {
if bundle_names == null { bundle_names = new ArrayBox() }
if bundle_mod_srcs == null { bundle_mod_srcs = new ArrayBox() }
bundle_names.push(name)
bundle_mod_srcs.push(code)
}
}
}
if j < 0 { break }
i = j + 3
}
}
// Env alias injection (TTL, dev-only): HAKO_BUNDLE_ALIAS_<Name> / NYASH_BUNDLE_ALIAS_<Name>
// If a required module is not provided via --bundle-mod, but an env alias
// supplies its code, synthesize a named bundle entry before checks/merge.
if require_mods != null {
local i0 = 0; local rn0 = require_mods.length()
loop(i0 < rn0) {
local need = "" + require_mods.get(i0)
local present = 0
if bundle_names != null {
local j0 = 0; local bn0 = bundle_names.length()
loop(j0 < bn0) { if ("" + bundle_names.get(j0)) == need { present = 1 break } j0 = j0 + 1 }
}
if present == 0 {
local alias_key = "HAKO_BUNDLE_ALIAS_" + need
local code = env.get(alias_key)
if code == null || code == "" { code = env.get("NYASH_BUNDLE_ALIAS_" + need) }
if code != null && code != "" {
if bundle_names == null { bundle_names = new ArrayBox() }
if bundle_mod_srcs == null { bundle_mod_srcs = new ArrayBox() }
bundle_names.push(need)
bundle_mod_srcs.push("" + code)
}
}
i0 = i0 + 1
}
}
// Fail on duplicate names
if bundle_names != null && bundle_names.length() > 1 {
local i = 0; local n = bundle_names.length()
@ -50,4 +106,3 @@ static box BundleResolver {
return merged
}
}

View File

@ -1,6 +1,7 @@
// Stage-B compiler entry — ParserBox → FlowEntry emit-only
using sh_core as StringHelpers // Required: ParserStringUtilsBox depends on this (using chain unresolved)
include "lang/src/compiler/entry/bundle_resolver.hako"
using lang.compiler.parser.box as ParserBox
// Note: Runner resolves entry as Main.main by default.
@ -148,25 +149,7 @@ static box Main {
i = i + 1
}
}
// FailFast: required modules must be provided via --bundle-mod
if require_mods.length() > 0 {
local idx = 0
local rn = require_mods.length()
loop(idx < rn) {
local need = "" + require_mods.get(idx)
local found = 0
{
local j = 0
local bn = bundle_names.length()
loop(j < bn) { if ("" + bundle_names.get(j)) == need { found = 1 break } j = j + 1 }
}
if found == 0 {
print("[bundle/missing] " + need)
return 1
}
idx = idx + 1
}
}
// Required modules are validated in BundleResolver.resolve (includes env alias injection)
// 4.6) FailFast on duplicate named bundles to avoid ambiguity
// Policy: duplicate module names are not allowed. Emit a stable diagnostic tag and exit.
@ -188,7 +171,6 @@ static box Main {
}
if bundles.length() > 0 || bundle_srcs.length() > 0 {
include "lang/src/compiler/entry/bundle_resolver.hako"
local merged_prefix = BundleResolver.resolve(bundles, bundle_names, bundle_srcs, require_mods)
if merged_prefix == null { return 1 }
body_src = merged_prefix + body_src

View File

@ -45,6 +45,19 @@ Exit Code Policy
- GateC(Core): numeric return is mapped to process exit code。タグ付きの失敗時は安定メッセージを出し、可能な限り非0で終了。
- VM backendRust Interpreter: 戻り値は標準出力に出す。プロセスの終了コードは戻り値と一致しない場合があるため、スモークは安定タグや標準出力の数値で検証するrcは参考
- 推奨: CIやスクリプトでは GateC(Core) を優先し rc を厳密化。開発時の対話検証は VM ルートで標準出力を検証。
Tag→RCCore Direct
- Core Direct`HAKO_CORE_DIRECT=1`)では、数値行が見つからない場合は rc≠0 を返すFailFast
- 代表タグ(例)
- `[core/string/bounds]` → rc=1
- `[core/array/oob_set]` → rc=1
- `[core/mir_call/method_unsupported]` → rc=1
Core Direct Toggle
- `HAKO_CORE_DIRECT=1`(互換: `NYASH_CORE_DIRECT`で、GateC(Core) の JSON 実行を "Core Dispatcher 直行" 子経路に切り替える。
- 形: 一時Hakoスクリプトに `include "lang/src/vm/core/dispatcher.hako"` を埋め込み、`NyVmDispatcher.run(json)` を実行。
- rc: 最後の数値行を rc にマップ。数値がない場合(タグ等)は rc≠0 とするFailFast
- 用途: Core の診断タグや rc を CI で直接検証したい時に使用。
- Runner Core toggle: `HAKO_NYVM_CORE=1` (or `NYASH_NYVM_CORE=1`) selects the
Core bridge for the nyvm wrapper path.
- GateC Core route: set `NYASH_GATE_C_CORE=1` (or `HAKO_GATE_C_CORE=1`) to

View File

@ -401,7 +401,7 @@ static box NyVmOpMirCall {
if method == "keys" {
local dst = me._read_dst(inst_json, "map keys")
if dst == null { return -1 }
// Build keys by scanning mem for receiver-specific entry slots
// Build keys by scanning mem for receiver-specific entry slots (non-null values only)
local keys_arr = new ArrayBox()
local prefix = me._map_entry_slot(recv_id, "")
// Iterate memory keys (MapBox.keys())
@ -416,8 +416,11 @@ static box NyVmOpMirCall {
if k.substring(0, prefix.length()) == prefix { ok = 1 }
}
if ok == 1 {
local tail = k.substring(prefix.length(), k.length())
keys_arr.push(tail)
local v = mem.get(k)
if v != null {
local tail = k.substring(prefix.length(), k.length())
keys_arr.push(tail)
}
}
i = i + 1
}
@ -473,7 +476,20 @@ static box NyVmOpMirCall {
return 0
}
if method == "clear" {
// clear(): set length to 0; entries are logically clearedTTL: metadataonly
// clear(): set length to 0 and remove all entries for this map from mem
local prefix = me._map_entry_slot(recv_id, "")
local all_keys = mem.keys()
local i = 0
local n = all_keys.length()
loop(i < n) {
local k = "" + all_keys.get(i)
local ok = 0
if k.length() >= prefix.length() {
if k.substring(0, prefix.length()) == prefix { ok = 1 }
}
if ok == 1 { mem.set(k, null) }
i = i + 1
}
me._map_len_set(state, recv_id, 0)
local dst_opt2 = me._read_optional_vid_field(inst_json, "dst")
if dst_opt2 != null { NyVmState.set_reg(state, dst_opt2, void) }

View File

@ -119,5 +119,5 @@ fn try_run_core_direct(json: &str) -> Option<i32> {
break;
}
}
rc
if let Some(code) = rc { Some(code) } else { Some(1) }
}

View File

@ -93,6 +93,42 @@ stageb_compile_to_json_with_bundles() {
return 1
}
stageb_compile_to_json_with_require() {
# Args: MAIN_CODE REQUIRES_CSV (e.g., "U1,U2")
local code="$1"; shift || true
local requires_csv="$1"; shift || true
local hako_tmp="/tmp/hako_stageb_$$.hako"
local json_out="/tmp/hako_stageb_$$.mir.json"
printf "%s\n" "$code" > "$hako_tmp"
local raw="/tmp/hako_stageb_raw_$$.txt"
local extra_args=()
IFS=',' read -r -a REQS <<< "$requires_csv"
for r in "${REQS[@]}"; do
[ -n "$r" ] && extra_args+=("--require-mod" "$r")
done
(
export NYASH_PARSER_ALLOW_SEMICOLON=1
export NYASH_ALLOW_USING_FILE=0
export HAKO_ALLOW_USING_FILE=0
export NYASH_USING_AST=1
export HAKO_PARSER_STAGE3=1
export NYASH_PARSER_STAGE3=1
export NYASH_VARMAP_GUARD_STRICT=0
export NYASH_BLOCK_SCHEDULE_VERIFY=0
NYASH_QUIET=0 HAKO_QUIET=0 NYASH_CLI_VERBOSE=0 \
"$NYASH_BIN" --backend vm \
"$NYASH_ROOT/lang/src/compiler/entry/compiler_stageb.hako" -- \
"${extra_args[@]}" --source "$(cat "$hako_tmp")"
) > "$raw" 2>&1 || true
if awk '(/"version":0/ && /"kind":"Program"/) {print; found=1; exit} END{exit(found?0:1)}' "$raw" > "$json_out"; then
rm -f "$raw" "$hako_tmp"
echo "$json_out"
return 0
fi
rm -f "$raw" "$hako_tmp" "$json_out"
return 1
}
stageb_json_nonempty() {
local path="$1"
[ -s "$path" ]

View File

@ -0,0 +1,31 @@
#!/bin/bash
# core_direct_string_bounds_rc_vm.sh — Core direct path: substring bounds → nonzero rc
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
if ROOT_GIT=$(git -C "$SCRIPT_DIR" rev-parse --show-toplevel 2>/dev/null); then
ROOT="$ROOT_GIT"
else
ROOT="$(cd "$SCRIPT_DIR/../../../../../../../../.." && pwd)"
fi
source "$ROOT/tools/smokes/v2/lib/test_runner.sh"
source "$ROOT/tools/smokes/v2/lib/stageb_helpers.sh"
require_env || exit 2
code='static box Main { method main(args) { local s="abc"; local t=s.substring(-1,1); return 0 } }'
json=$(stageb_compile_to_json "$code") || { echo "[FAIL] core_direct_string_bounds_rc_vm (emit failed)" >&2; exit 1; }
set +e
NYASH_GATE_C_CORE=1 HAKO_GATE_C_CORE=1 HAKO_CORE_DIRECT=1 \
NYASH_QUIET=1 HAKO_QUIET=1 NYASH_CLI_VERBOSE=0 NYASH_NYRT_SILENT_RESULT=1 \
"$NYASH_BIN" --json-file "$json" >/dev/null 2>&1
rc=$?
set -e
rm -f "$json"
if [ "$rc" -ne 0 ]; then
echo "[PASS] core_direct_string_bounds_rc_vm"
else
echo "[FAIL] core_direct_string_bounds_rc_vm (rc=$rc)" >&2; exit 1
fi

View File

@ -0,0 +1,31 @@
#!/bin/bash
# core_direct_string_charat_bounds_rc_vm.sh — Core Direct: charAt bounds → nonzero rc
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
if ROOT_GIT=$(git -C "$SCRIPT_DIR" rev-parse --show-toplevel 2>/dev/null); then
ROOT="$ROOT_GIT"
else
ROOT="$(cd "$SCRIPT_DIR/../../../../../../../../.." && pwd)"
fi
source "$ROOT/tools/smokes/v2/lib/test_runner.sh"
source "$ROOT/tools/smokes/v2/lib/stageb_helpers.sh"
require_env || exit 2
code='static box Main { method main(args) { local s="abc"; local c=s.charAt(5); return 0 } }'
json=$(stageb_compile_to_json "$code") || { echo "[FAIL] core_direct_string_charat_bounds_rc_vm (emit failed)" >&2; exit 1; }
set +e
NYASH_GATE_C_CORE=1 HAKO_GATE_C_CORE=1 HAKO_CORE_DIRECT=1 \
NYASH_QUIET=1 HAKO_QUIET=1 NYASH_CLI_VERBOSE=0 NYASH_NYRT_SILENT_RESULT=1 \
"$NYASH_BIN" --json-file "$json" >/dev/null 2>&1
rc=$?
set -e
rm -f "$json"
if [ "$rc" -ne 0 ]; then
echo "[PASS] core_direct_string_charat_bounds_rc_vm"
else
echo "[FAIL] core_direct_string_charat_bounds_rc_vm (rc=$rc)" >&2; exit 1
fi

View File

@ -0,0 +1,27 @@
#!/bin/bash
# core_direct_string_replace_ok_vm.sh — Core Direct: replace first occurrence
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
if ROOT_GIT=$(git -C "$SCRIPT_DIR" rev-parse --show-toplevel 2>/dev/null); then
ROOT="$ROOT_GIT"
else
ROOT="$(cd "$SCRIPT_DIR/../../../../../../../../.." && pwd)"
fi
source "$ROOT/tools/smokes/v2/lib/test_runner.sh"
source "$ROOT/tools/smokes/v2/lib/stageb_helpers.sh"
require_env || exit 2
code='static box Main { method main(args) { local s="a-b-c"; local t=s.replace("-","+"); print(t); return 0 } }'
json=$(stageb_compile_to_json "$code") || { echo "[FAIL] core_direct_string_replace_ok_vm (emit failed)" >&2; exit 1; }
out=$(NYASH_GATE_C_CORE=1 HAKO_GATE_C_CORE=1 HAKO_CORE_DIRECT=1 \
NYASH_QUIET=0 HAKO_QUIET=0 NYASH_CLI_VERBOSE=0 \
"$NYASH_BIN" --json-file "$json" 2>&1)
rm -f "$json"
if echo "$out" | tail -n1 | grep -qx "a+b-c"; then
echo "[PASS] core_direct_string_replace_ok_vm"
else
echo "[FAIL] core_direct_string_replace_ok_vm" >&2; echo "$out" >&2; exit 1
fi

View File

@ -0,0 +1,27 @@
#!/bin/bash
# core_direct_string_substring_ok_vm.sh — Core Direct: substring positive
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
if ROOT_GIT=$(git -C "$SCRIPT_DIR" rev-parse --show-toplevel 2>/dev/null); then
ROOT="$ROOT_GIT"
else
ROOT="$(cd "$SCRIPT_DIR/../../../../../../../../.." && pwd)"
fi
source "$ROOT/tools/smokes/v2/lib/test_runner.sh"
source "$ROOT/tools/smokes/v2/lib/stageb_helpers.sh"
require_env || exit 2
code='static box Main { method main(args) { local s="abcdef"; local t=s.substring(2,5); print(t); return 0 } }'
json=$(stageb_compile_to_json "$code") || { echo "[FAIL] core_direct_string_substring_ok_vm (emit failed)" >&2; exit 1; }
out=$(NYASH_GATE_C_CORE=1 HAKO_GATE_C_CORE=1 HAKO_CORE_DIRECT=1 \
NYASH_QUIET=0 HAKO_QUIET=0 NYASH_CLI_VERBOSE=0 \
"$NYASH_BIN" --json-file "$json" 2>&1)
rm -f "$json"
if echo "$out" | tail -n1 | grep -qx "cde"; then
echo "[PASS] core_direct_string_substring_ok_vm"
else
echo "[FAIL] core_direct_string_substring_ok_vm" >&2; echo "$out" >&2; exit 1
fi

View File

@ -0,0 +1,38 @@
#!/bin/bash
# map_clear_reset_vm.sh — Map.clear resets size/has/keys
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
if ROOT_GIT=$(git -C "$SCRIPT_DIR" rev-parse --show-toplevel 2>/dev/null); then
ROOT="$ROOT_GIT"
else
ROOT="$(cd "$SCRIPT_DIR/../../../../../../../../.." && pwd)"
fi
source "$ROOT/tools/smokes/v2/lib/test_runner.sh"
require_env || exit 2
code=$(cat <<'NY'
static box Main {
main() {
local m = new MapBox();
m.set("a", 1); m.set("b", 2);
m.clear();
print(m.size());
print(m.has("a"));
local ks = m.keys();
print(ks.size());
return 0
}
}
NY
)
out=$(run_nyash_vm -c "$code")
sz=$(echo "$out" | sed -n '1p')
has=$(echo "$out" | sed -n '2p')
ksz=$(echo "$out" | sed -n '3p')
if [ "$sz" = "0" ] && [ "$has" = "false" ] && [ "$ksz" = "0" ]; then
echo "[PASS] map_clear_reset_vm"
else
echo "[FAIL] map_clear_reset_vm" >&2; echo "$out" >&2; exit 1
fi

View File

@ -0,0 +1,35 @@
#!/bin/bash
# map_delete_has_size_vm.sh — Map.delete then has/size
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
if ROOT_GIT=$(git -C "$SCRIPT_DIR" rev-parse --show-toplevel 2>/dev/null); then
ROOT="$ROOT_GIT"
else
ROOT="$(cd "$SCRIPT_DIR/../../../../../../../../.." && pwd)"
fi
source "$ROOT/tools/smokes/v2/lib/test_runner.sh"
require_env || exit 2
code=$(cat <<'NY'
static box Main {
main() {
local m = new MapBox();
m.set("a", 1); m.set("b", 2);
m.delete("a");
print(m.has("a"));
print(m.size());
return 0
}
}
NY
)
out=$(run_nyash_vm -c "$code")
first=$(echo "$out" | sed -n '1p')
second=$(echo "$out" | sed -n '2p')
if [ "$first" = "false" ] && [ "$second" = "1" ]; then
echo "[PASS] map_delete_has_size_vm"
else
echo "[FAIL] map_delete_has_size_vm" >&2; echo "$out" >&2; exit 1
fi

View File

@ -0,0 +1,27 @@
#!/bin/bash
# stageb_bundle_alias_env_ok_vm.sh — StageB: alias via env HAKO_BUNDLE_ALIAS_<Name>
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
if ROOT_GIT=$(git -C "$SCRIPT_DIR" rev-parse --show-toplevel 2>/dev/null); then
ROOT="$ROOT_GIT"
else
ROOT="$(cd "$SCRIPT_DIR/../../../../../../../../.." && pwd)"
fi
source "$ROOT/tools/smokes/v2/lib/test_runner.sh"
source "$ROOT/tools/smokes/v2/lib/stageb_helpers.sh"
require_env || exit 2
main='static box Main { method main(args) { return 0 } }'
alias_code='static box UZ { method id(a){ return a } }'
export HAKO_BUNDLE_ALIAS_UZ="$alias_code"
json=$(stageb_compile_to_json_with_require "$main" "UZ") || { echo "[FAIL] stageb_bundle_alias_env_ok_vm (emit failed)" >&2; exit 1; }
if [ -s "$json" ] && head -n1 "$json" | grep -q '"version":0' && head -n1 "$json" | grep -q '"kind":"Program"'; then
rm -f "$json"; echo "[PASS] stageb_bundle_alias_env_ok_vm"; exit 0
else
echo "[FAIL] stageb_bundle_alias_env_ok_vm (missing header)" >&2
test -f "$json" && head -n1 "$json" >&2 || true
rm -f "$json"; exit 1
fi

View File

@ -0,0 +1,27 @@
#!/bin/bash
# stageb_bundle_alias_table_ok_vm.sh — StageB: alias table via env (multiple entries)
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
if ROOT_GIT=$(git -C "$SCRIPT_DIR" rev-parse --show-toplevel 2>/dev/null); then
ROOT="$ROOT_GIT"
else
ROOT="$(cd "$SCRIPT_DIR/../../../../../../../../.." && pwd)"
fi
source "$ROOT/tools/smokes/v2/lib/test_runner.sh"
source "$ROOT/tools/smokes/v2/lib/stageb_helpers.sh"
require_env || exit 2
main='static box Main { method main(args) { return 0 } }'
table='U1:static box U1 { method id(a){ return a } }|||U2:static box U2 { method id(a){ return a } }'
export HAKO_BUNDLE_ALIAS_TABLE="$table"
json=$(stageb_compile_to_json_with_require "$main" "U1,U2") || { echo "[FAIL] stageb_bundle_alias_table_ok_vm (emit failed)" >&2; exit 1; }
if [ -s "$json" ] && head -n1 "$json" | grep -q '"version":0' && head -n1 "$json" | grep -q '"kind":"Program"'; then
rm -f "$json"; echo "[PASS] stageb_bundle_alias_table_ok_vm"; exit 0
else
echo "[FAIL] stageb_bundle_alias_table_ok_vm (missing header)" >&2
test -f "$json" && head -n1 "$json" >&2 || true
rm -f "$json"; exit 1
fi