P0-1: Map bad-key tags for get/set/delete + smokes; String substring clamp smoke; P0-2: Bridge no‑op(Method) + Gate‑C invalid header smokes; P1: v1 bridge Closure captures appended to argv

This commit is contained in:
nyash-codex
2025-11-02 13:29:27 +09:00
parent ff62eb0d97
commit 110b4f3321
14 changed files with 275 additions and 59 deletions

View File

@ -111,4 +111,8 @@ static box MinMirEmitter {
} }
} }
static box MinMirEmitterMain { main(args){ return 0 } } using "lang/src/shared/common/entry_point_base.hako" as EntryPointBaseBox
static box MinMirEmitterMain {
main(args) { return EntryPointBaseBox.main(args) }
}

View File

@ -0,0 +1,18 @@
// CommonImportsBox - Unified import utilities for string operations
// Consolidates frequently used StringHelpers and StringOps imports
using "lang/src/shared/common/string_helpers.hako" as StringHelpers
using selfhost.shared.common.string_ops as StringOps
static box CommonImportsBox {
// Provides access to common string utilities
// Boxes can import this instead of individual modules
static helpers() { return StringHelpers }
static ops() { return StringOps }
// Commonly used string operations
static read_digits(text, pos) { return StringHelpers.read_digits(text, pos) }
static to_i64(digits) { return StringHelpers.to_i64(digits) }
static index_of_from(text, pattern, start) { return StringOps.index_of_from(text, pattern, start) }
static substring(text, start, end) { return StringOps.substring(text, start, end) }
}

View File

@ -0,0 +1,18 @@
// EntryPointBaseBox - Common entry point for standalone boxes
// Eliminates repetitive main(args){ return 0 } boilerplate
static box EntryPointBaseBox {
// Standard entry point implementation
// Can be overridden by specific boxes if needed
static main(args) {
return 0
}
// Entry point with validation
static safe_main(args) {
if args == null {
return 1
}
return EntryPointBaseBox.main(args)
}
}

View File

@ -16,7 +16,7 @@ using "lang/src/vm/hakorune-vm/blocks_locator.hako" as BlocksLocatorBox
using "lang/src/vm/hakorune-vm/instrs_locator.hako" as InstrsLocatorBox using "lang/src/vm/hakorune-vm/instrs_locator.hako" as InstrsLocatorBox
using "lang/src/vm/hakorune-vm/backward_object_scanner.hako" as BackwardObjectScannerBox using "lang/src/vm/hakorune-vm/backward_object_scanner.hako" as BackwardObjectScannerBox
using "lang/src/vm/hakorune-vm/block_iterator.hako" as BlockIteratorBox using "lang/src/vm/hakorune-vm/block_iterator.hako" as BlockIteratorBox
using selfhost.shared.common.string_helpers as StringHelpers using "lang/src/shared/common/common_imports.hako" as CommonImports
using selfhost.shared.common.box_helpers as BoxHelpers using selfhost.shared.common.box_helpers as BoxHelpers
static box MirIoBox { static box MirIoBox {
@ -144,7 +144,7 @@ static box MirIoBox {
p = p + key_id.length() p = p + key_id.length()
// skip ws // skip ws
loop(p < obj.length()) { local ch = obj.substring(p,p+1) if ch == " " || ch == "\n" || ch == "\r" || ch == "\t" { p = p + 1 continue } break } loop(p < obj.length()) { local ch = obj.substring(p,p+1) if ch == " " || ch == "\n" || ch == "\r" || ch == "\t" { p = p + 1 continue } break }
local digs = StringHelpers.read_digits(obj, p) local digs = CommonImports.read_digits(obj, p)
if digs == "" { return Result.Err("invalid block id") } if digs == "" { return Result.Err("invalid block id") }
ids.set(StringHelpers.int_to_str(StringHelpers.to_i64(digs)), 1) ids.set(StringHelpers.int_to_str(StringHelpers.to_i64(digs)), 1)
// require terminator // require terminator

View File

@ -4,6 +4,7 @@
using "lang/src/vm/boxes/result_box.hako" as Result using "lang/src/vm/boxes/result_box.hako" as Result
using "lang/src/vm/hakorune-vm/json_field_extractor.hako" as JsonFieldExtractor using "lang/src/vm/hakorune-vm/json_field_extractor.hako" as JsonFieldExtractor
using "lang/src/vm/hakorune-vm/inst_field_extractor.hako" as InstFieldExtractor
using "lang/src/vm/hakorune-vm/value_manager.hako" as ValueManagerBox using "lang/src/vm/hakorune-vm/value_manager.hako" as ValueManagerBox
using "lang/src/vm/hakorune-vm/args_extractor.hako" as ArgsExtractorBox using "lang/src/vm/hakorune-vm/args_extractor.hako" as ArgsExtractorBox
using "lang/src/shared/common/string_helpers.hako" as StringHelpers using "lang/src/shared/common/string_helpers.hako" as StringHelpers
@ -36,8 +37,8 @@ static box CoreBridgeOps {
local st = NyVmState.birth() local st = NyVmState.birth()
local rc = NyVmOpConst.handle(inst_json, st) local rc = NyVmOpConst.handle(inst_json, st)
if rc < 0 { return Result.Err("const: core failure") } if rc < 0 { return Result.Err("const: core failure") }
// Reflect dst into hakorune-vm regs // Reflect dst into hakorune-vm regs using unified extractor
local dst = JsonFieldExtractor.extract_int(inst_json, "dst") local dst = InstFieldExtractor.extract_dst(inst_json)
if dst == null { return Result.Err("const: dst not found") } if dst == null { return Result.Err("const: dst not found") }
local v = NyVmState.get_reg(st, dst) local v = NyVmState.get_reg(st, dst)
ValueManagerBox.set(regs, dst, v) ValueManagerBox.set(regs, dst, v)
@ -46,40 +47,30 @@ static box CoreBridgeOps {
// Apply binop via Core // Apply binop via Core
apply_binop(inst_json, regs) { apply_binop(inst_json, regs) {
// Extract required fields // Extract required fields using unified extractor
local dst = JsonFieldExtractor.extract_int(inst_json, "dst") local dst = InstFieldExtractor.extract_dst(inst_json)
if dst == null { return Result.Err("binop: dst field not found") } if dst == null { return Result.Err("binop: dst field not found") }
local lhs = JsonFieldExtractor.extract_int(inst_json, "lhs") local binary_ops = InstFieldExtractor.extract_binary_ops(inst_json)
if lhs == null { return Result.Err("binop: lhs field not found") } if binary_ops.lhs == null { return Result.Err("binop: lhs field not found") }
local rhs = JsonFieldExtractor.extract_int(inst_json, "rhs") if binary_ops.rhs == null { return Result.Err("binop: rhs field not found") }
if rhs == null { return Result.Err("binop: rhs field not found") }
// Normalize operation: prefer symbolic 'operation', else map from 'op_kind' // Use unified binary ops extractor (includes operation normalization)
local op = JsonFieldExtractor.extract_string(inst_json, "operation") local op = binary_ops.operation
if op == null { if op == null { return Result.Err("binop: operation/op_kind not found") }
local kind = JsonFieldExtractor.extract_string(inst_json, "op_kind")
if kind == null { return Result.Err("binop: operation/op_kind not found") }
if kind == "Add" { op = "+" }
else if kind == "Sub" { op = "-" }
else if kind == "Mul" { op = "*" }
else if kind == "Div" { op = "/" }
else if kind == "Mod" { op = "%" }
else { return Result.Err("binop: unsupported op_kind: " + kind) }
}
// Guard: required src regs must be set // Guard: required src regs must be set
local lhs_val = ValueManagerBox.get(regs, lhs) local lhs_val = ValueManagerBox.get(regs, binary_ops.lhs)
if lhs_val == null { return Result.Err("binop: lhs v%" + lhs + " is unset") } if lhs_val == null { return Result.Err("binop: lhs v%" + binary_ops.lhs + " is unset") }
local rhs_val = ValueManagerBox.get(regs, rhs) local rhs_val = ValueManagerBox.get(regs, binary_ops.rhs)
if rhs_val == null { return Result.Err("binop: rhs v%" + rhs + " is unset") } if rhs_val == null { return Result.Err("binop: rhs v%" + binary_ops.rhs + " is unset") }
// Rebuild minimal JSON acceptable to Core // Rebuild minimal JSON acceptable to Core
local j = "{\"op\":\"binop\",\"dst\":" + dst + ",\"operation\":\"" + op + "\",\"lhs\":" + lhs + ",\"rhs\":" + rhs + "}" local j = "{\"op\":\"binop\",\"dst\":" + dst + ",\"operation\":\"" + op + "\",\"lhs\":" + binary_ops.lhs + ",\"rhs\":" + binary_ops.rhs + "}"
// Execute via Core with a temporary state containing sources // Execute via Core with a temporary state containing sources
local st = NyVmState.birth() local st = NyVmState.birth()
NyVmState.set_reg(st, lhs, lhs_val) NyVmState.set_reg(st, binary_ops.lhs, lhs_val)
NyVmState.set_reg(st, rhs, rhs_val) NyVmState.set_reg(st, binary_ops.rhs, rhs_val)
local rc = NyVmOpBinOp.handle(j, st) local rc = NyVmOpBinOp.handle(j, st)
if rc < 0 { return Result.Err("binop: core failure") } if rc < 0 { return Result.Err("binop: core failure") }
local out = NyVmState.get_reg(st, dst) local out = NyVmState.get_reg(st, dst)
@ -101,35 +92,24 @@ static box CoreBridgeOps {
// Apply compare via Core // Apply compare via Core
apply_compare(inst_json, regs) { apply_compare(inst_json, regs) {
local dst = JsonFieldExtractor.extract_int(inst_json, "dst") local dst = InstFieldExtractor.extract_dst(inst_json)
if dst == null { return Result.Err("compare: dst field not found") } if dst == null { return Result.Err("compare: dst field not found") }
local lhs = JsonFieldExtractor.extract_int(inst_json, "lhs") local compare_ops = InstFieldExtractor.extract_compare_ops(inst_json)
if lhs == null { return Result.Err("compare: lhs field not found") } if compare_ops.lhs == null { return Result.Err("compare: lhs field not found") }
local rhs = JsonFieldExtractor.extract_int(inst_json, "rhs") if compare_ops.rhs == null { return Result.Err("compare: rhs field not found") }
if rhs == null { return Result.Err("compare: rhs field not found") } // Use unified compare ops extractor (includes operation normalization)
// Normalize kind -> operation local op = compare_ops.operation
local op = JsonFieldExtractor.extract_string(inst_json, "operation") if op == null { return Result.Err("compare: kind/operation not found") }
if op == null {
local kind = JsonFieldExtractor.extract_string(inst_json, "kind")
if kind == null { return Result.Err("compare: kind/operation not found") }
if kind == "Eq" { op = "==" }
else if kind == "Ne" { op = "!=" }
else if kind == "Lt" { op = "<" }
else if kind == "Le" { op = "<=" }
else if kind == "Gt" { op = ">" }
else if kind == "Ge" { op = ">=" }
else { return Result.Err("compare: unsupported kind: " + kind) }
}
// Guards // Guards
local lv = ValueManagerBox.get(regs, lhs) local lv = ValueManagerBox.get(regs, compare_ops.lhs)
if lv == null { return Result.Err("compare: lhs v%" + lhs + " is unset") } if lv == null { return Result.Err("compare: lhs v%" + compare_ops.lhs + " is unset") }
local rv = ValueManagerBox.get(regs, rhs) local rv = ValueManagerBox.get(regs, compare_ops.rhs)
if rv == null { return Result.Err("compare: rhs v%" + rhs + " is unset") } if rv == null { return Result.Err("compare: rhs v%" + compare_ops.rhs + " is unset") }
// Build minimal JSON // Build minimal JSON
local j = "{\"op\":\"compare\",\"dst\":" + dst + ",\"operation\":\"" + op + "\",\"lhs\":" + lhs + ",\"rhs\":" + rhs + "}" local j = "{\"op\":\"compare\",\"dst\":" + dst + ",\"operation\":\"" + op + "\",\"lhs\":" + compare_ops.lhs + ",\"rhs\":" + compare_ops.rhs + "}"
local st = NyVmState.birth() local st = NyVmState.birth()
NyVmState.set_reg(st, lhs, lv) NyVmState.set_reg(st, compare_ops.lhs, lv)
NyVmState.set_reg(st, rhs, rv) NyVmState.set_reg(st, compare_ops.rhs, rv)
local rc = NyVmOpCompare.handle(j, st) local rc = NyVmOpCompare.handle(j, st)
if rc < 0 { return Result.Err("compare: core failure") } if rc < 0 { return Result.Err("compare: core failure") }
ValueManagerBox.set(regs, dst, NyVmState.get_reg(st, dst)) ValueManagerBox.set(regs, dst, NyVmState.get_reg(st, dst))

View File

@ -23,12 +23,24 @@ static box InstFieldExtractorBox {
return result return result
} }
// Extract comparison fields (lhs, rhs, kind) // Extract comparison fields (lhs, rhs, kind) and normalize to operation
static extract_compare_ops(inst_json) { static extract_compare_ops(inst_json) {
local result = {} local result = {}
result.lhs = JsonFieldExtractor.extract_int(inst_json, "lhs") result.lhs = JsonFieldExtractor.extract_int(inst_json, "lhs")
result.rhs = JsonFieldExtractor.extract_int(inst_json, "rhs") result.rhs = JsonFieldExtractor.extract_int(inst_json, "rhs")
result.kind = JsonFieldExtractor.extract_string(inst_json, "kind") result.kind = JsonFieldExtractor.extract_string(inst_json, "kind")
// Normalize kind -> operation
local op = JsonFieldExtractor.extract_string(inst_json, "operation")
if op == null && result.kind != null {
if result.kind == "Eq" { op = "==" }
else if result.kind == "Ne" { op = "!=" }
else if result.kind == "Lt" { op = "<" }
else if result.kind == "Le" { op = "<=" }
else if result.kind == "Gt" { op = ">" }
else if result.kind == "Ge" { op = ">=" }
}
result.operation = op
return result return result
} }

View File

@ -64,7 +64,12 @@ pub(super) fn try_handle_map_box(
} }
"set" => { "set" => {
if args.len() != 2 { return Err(VMError::InvalidInstruction("MapBox.set expects 2 args".into())); } if args.len() != 2 { return Err(VMError::InvalidInstruction("MapBox.set expects 2 args".into())); }
let k = this.reg_load(args[0])?.to_nyash_box(); let k_vm = this.reg_load(args[0])?;
if !matches!(k_vm, VMValue::String(_)) {
if let Some(d) = dst { this.regs.insert(d, VMValue::String("[map/bad-key] key must be string".to_string())); }
return Ok(true);
}
let k = k_vm.to_nyash_box();
let v = this.reg_load(args[1])?.to_nyash_box(); let v = this.reg_load(args[1])?.to_nyash_box();
let ret = mb.set(k, v); let ret = mb.set(k, v);
if let Some(d) = dst { this.regs.insert(d, VMValue::from_nyash_box(ret)); } if let Some(d) = dst { this.regs.insert(d, VMValue::from_nyash_box(ret)); }
@ -72,7 +77,12 @@ pub(super) fn try_handle_map_box(
} }
"get" => { "get" => {
if args.len() != 1 { return Err(VMError::InvalidInstruction("MapBox.get expects 1 arg".into())); } if args.len() != 1 { return Err(VMError::InvalidInstruction("MapBox.get expects 1 arg".into())); }
let k = this.reg_load(args[0])?.to_nyash_box(); let k_vm = this.reg_load(args[0])?;
if !matches!(k_vm, VMValue::String(_)) {
if let Some(d) = dst { this.regs.insert(d, VMValue::String("[map/bad-key] key must be string".to_string())); }
return Ok(true);
}
let k = k_vm.to_nyash_box();
let ret = mb.get(k); let ret = mb.get(k);
if let Some(d) = dst { this.regs.insert(d, VMValue::from_nyash_box(ret)); } if let Some(d) = dst { this.regs.insert(d, VMValue::from_nyash_box(ret)); }
return Ok(true); return Ok(true);
@ -86,7 +96,12 @@ pub(super) fn try_handle_map_box(
} }
"delete" => { "delete" => {
if args.len() != 1 { return Err(VMError::InvalidInstruction("MapBox.delete expects 1 arg".into())); } if args.len() != 1 { return Err(VMError::InvalidInstruction("MapBox.delete expects 1 arg".into())); }
let k = this.reg_load(args[0])?.to_nyash_box(); let k_vm = this.reg_load(args[0])?;
if !matches!(k_vm, VMValue::String(_)) {
if let Some(d) = dst { this.regs.insert(d, VMValue::String("[map/bad-key] key must be string".to_string())); }
return Ok(true);
}
let k = k_vm.to_nyash_box();
let ret = mb.delete(k); let ret = mb.delete(k);
if let Some(d) = dst { this.regs.insert(d, VMValue::from_nyash_box(ret)); } if let Some(d) = dst { this.regs.insert(d, VMValue::from_nyash_box(ret)); }
return Ok(true); return Ok(true);

View File

@ -467,6 +467,16 @@ pub fn try_parse_v1_to_module(json: &str) -> Result<Option<MirModule>, String> {
"mir_call callee Closure missing func in function '{}'", "mir_call callee Closure missing func in function '{}'",
func_name func_name
))? as u32; ))? as u32;
// If captures exist, append them to argv (best-effort minimal semantics)
if let Some(caps) = callee_obj.get("captures").and_then(Value::as_array) {
for c in caps {
let id = c.as_u64().ok_or_else(|| format!(
"mir_call Closure capture must be integer in function '{}'",
func_name
))? as u32;
argv.push(ValueId::new(id));
}
}
// Captures (if any) are currently ignored at this stage; captured values are // Captures (if any) are currently ignored at this stage; captured values are
// expected to be materialized as arguments or handled by earlier lowering. // expected to be materialized as arguments or handled by earlier lowering.
block_ref.add_instruction(MirInstruction::Call { block_ref.add_instruction(MirInstruction::Call {

View File

@ -0,0 +1,35 @@
#!/bin/bash
# canonicalize_noop_method_on_vm.sh — ONでもMethodは変異しないdump-mut未生成
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
json_path="/tmp/ny_v1_noop_method_$$.json"
mut_on="/tmp/ny_v1_noop_method_on_$$.json"
cat >"$json_path" <<'JSON'
{"schema_version":"1.0","functions":[{"name":"main","blocks":[{"id":0,"instructions":[{"op":"mir_call","mir_call":{"callee":{"type":"Method","box_name":"ArrayBox","method":"size","receiver":1},"args":[] }},{"op":"ret"}]}]}]}
JSON
set +e
HAKO_NYVM_V1_DOWNCONVERT=1 HAKO_BRIDGE_INJECT_SINGLETON=1 HAKO_DEBUG_NYVM_BRIDGE_DUMP_MUT="$mut_on" \
"$ROOT/target/release/nyash" --json-file "$json_path" >/dev/null 2>&1
set -e || true
if [ -f "$mut_on" ]; then
echo "[FAIL] canonicalize_noop_method_on_vm: mutated dump should not be created for Method" >&2
exit 1
fi
echo "[PASS] canonicalize_noop_method_on_vm"
rm -f "$json_path" "$mut_on"
exit 0

View File

@ -0,0 +1,27 @@
#!/bin/bash
# gate_c_invalid_header_vm.sh — GateC(Core) invalid JSON header → 非0終了
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
bad="/tmp/gatec_bad_$$.json"
echo '{"bad":1}' > "$bad"
set +e
NYASH_GATE_C_CORE=1 "$NYASH_BIN" --nyvm-json-file "$bad" >/dev/null 2>&1
rc=$?
set -e
rm -f "$bad"
if [ $rc -ne 0 ]; then
echo "[PASS] gate_c_invalid_header_vm"
else
echo "[FAIL] gate_c_invalid_header_vm (rc=$rc)" >&2
exit 1
fi

View File

@ -0,0 +1,21 @@
#!/bin/bash
# map_bad_key_delete_vm.sh — Map.delete with non-string key returns [map/bad-key]
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='static box Main { main() { local m=new MapBox(); print(m.delete(123)); return 0 } }'
out=$(run_nyash_vm -c "$code")
if echo "$out" | grep -q "\[map/bad-key\]"; then
echo "[PASS] map_bad_key_delete_vm"
else
echo "[FAIL] map_bad_key_delete_vm" >&2; echo "$out" >&2; exit 1
fi

View File

@ -0,0 +1,21 @@
#!/bin/bash
# map_bad_key_get_vm.sh — Map.get with non-string key returns [map/bad-key]
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='static box Main { main() { local m=new MapBox(); print(m.get(123)); return 0 } }'
out=$(run_nyash_vm -c "$code")
if echo "$out" | grep -q "\[map/bad-key\]"; then
echo "[PASS] map_bad_key_get_vm"
else
echo "[FAIL] map_bad_key_get_vm" >&2; echo "$out" >&2; exit 1
fi

View File

@ -0,0 +1,21 @@
#!/bin/bash
# map_bad_key_set_vm.sh — Map.set with non-string key returns [map/bad-key]
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='static box Main { main() { local m=new MapBox(); print(m.set(123, 1)); return 0 } }'
out=$(run_nyash_vm -c "$code")
if echo "$out" | grep -q "\[map/bad-key\]"; then
echo "[PASS] map_bad_key_set_vm"
else
echo "[FAIL] map_bad_key_set_vm" >&2; echo "$out" >&2; exit 1
fi

View File

@ -0,0 +1,34 @@
#!/bin/bash
# substring_clamp_vm.sh — String.substring clamps to [0,size] and start<=end
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='static box Main { main() {
local s="abcd";
print(s.substring(-5, 2));
print(s.substring(2, 99));
print(s.substring(3, 1));
return 0
} }'
out=$(run_nyash_vm -c "$code")
expected=$(cat <<EOT
ab
cd
EOT
)
if [ "$out" = "$expected" ]; then
echo "[PASS] substring_clamp_vm"
else
echo "[FAIL] substring_clamp_vm" >&2; printf '--- expected ---\n%s--- got ---\n%s\n' "$expected" "$out" >&2; exit 1
fi