restore(lang/compiler): bring back lang/src/compiler from e917d400; add Hako index canaries and docs; implement Rust-side index operator (Array/Map get/set) with Fail‑Fast diagnostics

- restore: lang/src/compiler/** (parser/emit/builder/pipeline_v2) from e917d400
- docs: docs/development/selfhosting/index-operator-hako.md
- smokes(hako): tools/smokes/v2/profiles/quick/core/index_operator_hako.sh (opt-in)
- smokes(vm): adjust index_operator_vm.sh for semicolon gate + stable error text
- rust/parser: allow IndexExpr and assignment LHS=Index; postfix parse LBRACK chain
- rust/builder: lower arr/map index to BoxCall get/set; annotate array/map literals; Fail‑Fast for unsupported types
- CURRENT_TASK: mark Rust side done; add Hako tasks checklist

Note: files disappeared likely due to branch FF to a lineage without lang/src/compiler; no explicit delete commit found. Added anchor checks and suggested CI guard in follow-up.
This commit is contained in:
nyash-codex
2025-10-31 20:18:39 +09:00
parent 86fd03afe8
commit 5e3d9e7ae4
86 changed files with 6214 additions and 20 deletions

View File

@ -0,0 +1,355 @@
// Moved from apps/selfhost-compiler/boxes/parser/expr/parser_expr_box.hako
// ParserExprBox — expression parser coordinator
// Responsibility: Parse expressions and delegate to specialized boxes
// API: parse(src, i, ctx) -> JSON (delegates to parse_expr2)
using lang.compiler.parser.scan.parser_number_scan_box
using lang.compiler.parser.expr.parser_peek_box
using lang.compiler.parser.expr.parser_literal_box
static box ParserExprBox {
parse_number2(src, i, ctx) {
local pair = ParserNumberScanBox.scan_int(src, i)
local at = pair.lastIndexOf("@")
local json = pair.substring(0, at)
local pos = i
if at >= 0 { pos = ctx.to_int(pair.substring(at+1, pair.size())) }
ctx.gpos_set(pos)
return json
}
parse_string2(src, i, ctx) {
local n = src.size()
local j = i + 1
local out = ""
local guard = 0
local max = 200000
loop(j < n) {
if guard > max { break }
guard = guard + 1
local ch = src.substring(j, j+1)
if ch == "\"" {
j = j + 1
ctx.gpos_set(j)
return "{\"type\":\"Str\",\"value\":\"" + ctx.esc_json(out) + "\"}"
}
if ch == "\\" && j + 1 < n {
local nx = src.substring(j+1, j+2)
if nx == "\"" { out = out + "\"" j = j + 2 }
else { if nx == "\\" { out = out + "\\" j = j + 2 }
else { if nx == "n" { out = out + "\n" j = j + 2 }
else { if nx == "r" { out = out + "\r" j = j + 2 }
else { if nx == "t" { out = out + "\t" j = j + 2 }
else { if nx == "u" && j + 5 < n { out = out + src.substring(j, j+6) j = j + 6 }
else { out = out + nx j = j + 2 } } } } } }
} else {
out = out + ch
j = j + 1
}
}
ctx.gpos_set(j)
return "{\"type\":\"Str\",\"value\":\"" + ctx.esc_json(out) + "\"}"
}
parse_factor2(src, i, ctx) {
local j = ctx.skip_ws(src, i)
if j >= src.size() {
ctx.gpos_set(j)
return "{\"type\":\"Int\",\"value\":0}"
}
if ctx.starts_with_kw(src, j, "true") == 1 {
ctx.gpos_set(j + 4)
return "{\"type\":\"Bool\",\"value\":true}"
}
if ctx.starts_with_kw(src, j, "false") == 1 {
ctx.gpos_set(j + 5)
return "{\"type\":\"Bool\",\"value\":false}"
}
if ctx.starts_with_kw(src, j, "null") == 1 {
ctx.gpos_set(j + 4)
return "{\"type\":\"Null\"}"
}
// Peek expression: delegate to ParserPeekBox
if ctx.starts_with_kw(src, j, "peek") == 1 {
j = j + 4
return ParserPeekBox.parse(src, j, ctx)
}
local ch = src.substring(j, j+1)
// Parenthesized
if ch == "(" {
local inner = me.parse_expr2(src, j + 1, ctx)
local k = ctx.gpos_get()
k = ctx.skip_ws(src, k)
if src.substring(k, k+1) == ")" { k = k + 1 }
ctx.gpos_set(k)
return inner
}
// String literal
if ch == "\"" {
return me.parse_string2(src, j, ctx)
}
// Map literal: delegate to ParserLiteralBox
if ch == "{" {
return ParserLiteralBox.parse_map(src, j, ctx)
}
// Array literal: delegate to ParserLiteralBox
if ch == "[" {
return ParserLiteralBox.parse_array(src, j, ctx)
}
// new Class(args)
if ctx.starts_with_kw(src, j, "new") == 1 {
local p = ctx.skip_ws(src, j + 3)
local idp = ctx.read_ident2(src, p)
local at = idp.lastIndexOf("@")
local cls = idp.substring(0, at)
local k = ctx.to_int(idp.substring(at+1, idp.size()))
k = ctx.skip_ws(src, k)
if src.substring(k, k+1) == "(" { k = k + 1 }
local args_and_pos = me.parse_args2(src, k, ctx)
local at2 = args_and_pos.lastIndexOf("@")
local args_json = args_and_pos.substring(0, at2)
k = ctx.to_int(args_and_pos.substring(at2+1, args_and_pos.size()))
k = ctx.skip_ws(src, k)
if src.substring(k, k+1) == ")" { k = k + 1 }
ctx.gpos_set(k)
return "{\"type\":\"New\",\"class\":\"" + cls + "\",\"args\":" + args_json + "}"
}
// Identifier / Call / Method chain
if ctx.is_alpha(ch) {
local idp = ctx.read_ident2(src, j)
local at = idp.lastIndexOf("@")
local name = idp.substring(0, at)
local k = ctx.to_int(idp.substring(at+1, idp.size()))
local node = "{\"type\":\"Var\",\"name\":\"" + name + "\"}"
local cont2 = 1
loop(cont2 == 1) {
k = ctx.skip_ws(src, k)
local tch = src.substring(k, k+1)
if tch == "(" {
k = k + 1
local args_and_pos = me.parse_args2(src, k, ctx)
local at2 = args_and_pos.lastIndexOf("@")
local args_json = args_and_pos.substring(0, at2)
k = ctx.to_int(args_and_pos.substring(at2+1, args_and_pos.size()))
k = ctx.skip_ws(src, k)
if src.substring(k, k+1) == ")" { k = k + 1 }
node = "{\"type\":\"Call\",\"name\":\"" + name + "\",\"args\":" + args_json + "}"
} else {
if tch == "." {
k = k + 1
k = ctx.skip_ws(src, k)
local midp = ctx.read_ident2(src, k)
local at3 = midp.lastIndexOf("@")
local mname = midp.substring(0, at3)
k = ctx.to_int(midp.substring(at3+1, midp.size()))
k = ctx.skip_ws(src, k)
if src.substring(k, k+1) == "(" { k = k + 1 }
local args2 = me.parse_args2(src, k, ctx)
local at4 = args2.lastIndexOf("@")
local args_json2 = args2.substring(0, at4)
k = ctx.to_int(args2.substring(at4+1, args2.size()))
k = ctx.skip_ws(src, k)
if src.substring(k, k+1) == ")" { k = k + 1 }
node = "{\"type\":\"Method\",\"recv\":" + node + ",\"method\":\"" + mname + "\",\"args\":" + args_json2 + "}"
} else {
cont2 = 0
}
}
}
ctx.gpos_set(k)
return node
}
// Fallback: number
return me.parse_number2(src, j, ctx)
}
parse_unary2(src, i, ctx) {
local j = ctx.skip_ws(src, i)
if src.substring(j, j+1) == "-" {
local rhs = me.parse_factor2(src, j + 1, ctx)
j = ctx.gpos_get()
local zero = "{\"type\":\"Int\",\"value\":0}"
ctx.gpos_set(j)
return "{\"type\":\"Binary\",\"op\":\"-\",\"lhs\":" + zero + ",\"rhs\":" + rhs + "}"
}
return me.parse_factor2(src, j, ctx)
}
parse_term2(src, i, ctx) {
local lhs = me.parse_unary2(src, i, ctx)
local j = ctx.gpos_get()
local cont = 1
loop(cont == 1) {
j = ctx.skip_ws(src, j)
if j >= src.size() {
cont = 0
} else {
local op = src.substring(j, j+1)
if op != "*" && op != "/" {
cont = 0
} else {
local rhs = me.parse_unary2(src, j+1, ctx)
j = ctx.gpos_get()
lhs = "{\"type\":\"Binary\",\"op\":\"" + op + "\",\"lhs\":" + lhs + ",\"rhs\":" + rhs + "}"
}
}
}
ctx.gpos_set(j)
return lhs
}
parse_sum2(src, i, ctx) {
local lhs = me.parse_term2(src, i, ctx)
local j = ctx.gpos_get()
local cont = 1
loop(cont == 1) {
j = ctx.skip_ws(src, j)
if j >= src.size() {
cont = 0
} else {
local op = src.substring(j, j+1)
if op != "+" && op != "-" {
cont = 0
} else {
local rhs = me.parse_term2(src, j+1, ctx)
j = ctx.gpos_get()
lhs = "{\"type\":\"Binary\",\"op\":\"" + op + "\",\"lhs\":" + lhs + ",\"rhs\":" + rhs + "}"
}
}
}
ctx.gpos_set(j)
return lhs
}
parse_compare2(src, i, ctx) {
local lhs = me.parse_sum2(src, i, ctx)
local j = ctx.gpos_get()
j = ctx.skip_ws(src, j)
local two = src.substring(j, j+2)
local one = src.substring(j, j+1)
local op = ""
if two == "==" || two == "!=" || two == "<=" || two == ">=" {
op = two
j = j + 2
} else {
if one == "<" || one == ">" {
op = one
j = j + 1
}
}
if op == "" {
ctx.gpos_set(j)
return lhs
}
local rhs = me.parse_sum2(src, j, ctx)
j = ctx.gpos_get()
ctx.gpos_set(j)
return "{\"type\":\"Compare\",\"op\":\"" + op + "\",\"lhs\":" + lhs + ",\"rhs\":" + rhs + "}"
}
parse_expr2(src, i, ctx) {
local lhs = me.parse_compare2(src, i, ctx)
local j = ctx.gpos_get()
local cont = 1
loop(cont == 1) {
j = ctx.skip_ws(src, j)
local two = src.substring(j, j+2)
if two != "&&" && two != "||" {
cont = 0
} else {
local rhs = me.parse_compare2(src, j+2, ctx)
j = ctx.gpos_get()
lhs = "{\"type\":\"Logical\",\"op\":\"" + two + "\",\"lhs\":" + lhs + ",\"rhs\":" + rhs + "}"
}
}
j = ctx.skip_ws(src, j)
if src.substring(j, j+1) == "?" {
j = j + 1
j = ctx.skip_ws(src, j)
local then_expr = me.parse_expr2(src, j, ctx)
j = ctx.gpos_get()
j = ctx.skip_ws(src, j)
if src.substring(j, j+1) == ":" { j = j + 1 }
j = ctx.skip_ws(src, j)
local else_expr = me.parse_expr2(src, j, ctx)
j = ctx.gpos_get()
if else_expr.size() == 0 { else_expr = "{\"type\":\"Int\",\"value\":0}" }
ctx.gpos_set(j)
return "{\"type\":\"Ternary\",\"cond\":" + lhs + ",\"then\":" + then_expr + ",\"else\":" + else_expr + "}"
}
ctx.gpos_set(j)
return lhs
}
parse_args2(src, i, ctx) {
local j = ctx.skip_ws(src, i)
local n = src.size()
local out = "["
j = ctx.skip_ws(src, j)
if j < n && src.substring(j, j+1) == ")" {
return "[]@" + ctx.i2s(j)
}
// first argument
local e = me.parse_expr2(src, j, ctx)
j = ctx.gpos_get()
out = out + e
// subsequent arguments with guard
local cont_args = 1
local guard = 0
local max = 100000
loop(cont_args == 1) {
if guard > max { cont_args = 0 } else { guard = guard + 1 }
local before = j
j = ctx.skip_ws(src, j)
if j < n && src.substring(j, j+1) == "," {
j = j + 1
j = ctx.skip_ws(src, j)
e = me.parse_expr2(src, j, ctx)
j = ctx.gpos_get()
out = out + "," + e
} else {
cont_args = 0
}
if j == before { cont_args = 0 }
}
out = out + "]"
return out + "@" + ctx.i2s(j)
}
}

View File

@ -0,0 +1,119 @@
// Moved from apps/selfhost-compiler/boxes/parser/expr/parser_literal_box.hako
// ParserLiteralBox — Map/Array literal parser
// Responsibility: Parse Map {"k": v, ...} and Array [e1, e2, ...] literals
// API: parse_map(src, i, ctx) -> JSON, parse_array(src, i, ctx) -> JSON
static box ParserLiteralBox {
// Map literal: {"k": v, ...} (string keys only) → Call{name:"map.of", args:[Str(k1), v1, Str(k2), v2, ...]}
parse_map(src, i, ctx) {
local n = src.size()
local j = i + 1 // skip opening '{'
local out = "["
local first = 1
local cont = 1
local guard = 0
local max = 400000
loop(cont == 1) {
if guard > max { cont = 0 } else { guard = guard + 1 }
j = ctx.skip_ws(src, j)
if j >= n {
cont = 0
} else {
if src.substring(j, j+1) == "}" {
j = j + 1
cont = 0
} else {
// key (string only for Stage-2)
if src.substring(j, j+1) != "\"" {
// degrade by skipping one char to avoid infinite loop
j = j + 1
continue
}
local key_raw = ctx.read_string_lit(src, j)
j = ctx.gpos_get()
j = ctx.skip_ws(src, j)
if src.substring(j, j+1) == ":" { j = j + 1 }
j = ctx.skip_ws(src, j)
local val_json = ctx.parse_expr2(src, j)
j = ctx.gpos_get()
local key_json = "{\"type\":\"Str\",\"value\":\"" + ctx.esc_json(key_raw) + "\"}"
if first == 1 {
out = out + key_json + "," + val_json
first = 0
} else {
out = out + "," + key_json + "," + val_json
}
// optional comma
local before2 = j
j = ctx.skip_ws(src, j)
if j < n && src.substring(j, j+1) == "," { j = j + 1 }
// progress guard (in case of malformed input)
if j <= before2 {
if j < n { j = j + 1 } else { j = n }
}
}
}
}
out = out + "]"
ctx.gpos_set(j)
return "{\"type\":\"Call\",\"name\":\"map.of\",\"args\":" + out + "}"
}
// Array literal: [e1, e2, ...] → Call{name:"array.of", args:[...]}
parse_array(src, i, ctx) {
local n = src.size()
local j = i + 1 // skip opening '['
local out = "["
local first = 1
local cont = 1
local guard = 0
local max = 400000
loop(cont == 1) {
if guard > max { cont = 0 } else { guard = guard + 1 }
j = ctx.skip_ws(src, j)
if j >= n {
cont = 0
} else {
if src.substring(j, j+1) == "]" {
j = j + 1
cont = 0
} else {
local before = j
local ej = ctx.parse_expr2(src, j)
j = ctx.gpos_get()
if first == 1 {
out = out + ej
first = 0
} else {
out = out + "," + ej
}
// optional comma+whitespace
local before2 = j
j = ctx.skip_ws(src, j)
if j < n && src.substring(j, j+1) == "," { j = j + 1 }
// progress guard
if j <= before {
if j < n { j = j + 1 } else { j = n }
}
}
}
}
out = out + "]"
ctx.gpos_set(j)
return "{\"type\":\"Call\",\"name\":\"array.of\",\"args\":" + out + "}"
}
}

View File

@ -0,0 +1,104 @@
// Moved from apps/selfhost-compiler/boxes/parser/expr/parser_peek_box.hako
// ParserPeekBox — peek expression parser (peek <expr> { "label" => <expr>, ..., else => <expr> })
// Responsibility: Parse peek expressions (pattern-matching syntax)
// API: parse(src, i, ctx) -> JSON string
static box ParserPeekBox {
parse(src, i, ctx) {
// ctx is ParserBox for delegation
local j = i
local n = src.size()
// Parse scrutinee expression
local scr = ctx.parse_expr2(src, j)
j = ctx.gpos_get()
j = ctx.skip_ws(src, j)
// Enter arms block
if src.substring(j, j+1) == "{" { j = j + 1 }
j = ctx.skip_ws(src, j)
local arms_json = "["
local first_arm = 1
local else_json = null
local contp = 1
local guardp = 0
local maxp = 400000
loop(contp == 1) {
if guardp > maxp { contp = 0 } else { guardp = guardp + 1 }
j = ctx.skip_ws(src, j)
if j >= n {
contp = 0
} else {
if src.substring(j, j+1) == "}" {
j = j + 1
contp = 0
} else {
// else arm or labeled arm
if ctx.starts_with_kw(src, j, "else") == 1 {
j = j + 4
j = ctx.skip_ws(src, j)
if src.substring(j, j+2) == "=>" { j = j + 2 }
j = ctx.skip_ws(src, j)
// else body may be a block or bare expr
if src.substring(j, j+1) == "{" {
j = j + 1
j = ctx.skip_ws(src, j)
else_json = ctx.parse_expr2(src, j)
j = ctx.gpos_get()
j = ctx.skip_ws(src, j)
if src.substring(j, j+1) == "}" { j = j + 1 }
} else {
else_json = ctx.parse_expr2(src, j)
j = ctx.gpos_get()
}
} else {
// labeled arm: string literal label
if src.substring(j, j+1) != "\"" {
// degrade safely to avoid infinite loop
j = j + 1
continue
}
local label_raw = ctx.read_string_lit(src, j)
j = ctx.gpos_get()
j = ctx.skip_ws(src, j)
if src.substring(j, j+2) == "=>" { j = j + 2 }
j = ctx.skip_ws(src, j)
// arm expr: block or bare expr
local expr_json = "{\"type\":\"Int\",\"value\":0}"
if src.substring(j, j+1) == "{" {
j = j + 1
j = ctx.skip_ws(src, j)
expr_json = ctx.parse_expr2(src, j)
j = ctx.gpos_get()
j = ctx.skip_ws(src, j)
if src.substring(j, j+1) == "}" { j = j + 1 }
} else {
expr_json = ctx.parse_expr2(src, j)
j = ctx.gpos_get()
}
local arm_json = "{\"label\":\"" + ctx.esc_json(label_raw) + "\",\"expr\":" + expr_json + "}"
if first_arm == 1 {
arms_json = arms_json + arm_json
first_arm = 0
} else {
arms_json = arms_json + "," + arm_json
}
}
}
}
}
arms_json = arms_json + "]"
if else_json == null { else_json = "{\"type\":\"Null\"}" }
ctx.gpos_set(j)
return "{\"type\":\"Peek\",\"scrutinee\":" + scr + ",\"arms\":" + arms_json + ",\"else\":" + else_json + "}"
}
}

View File

@ -0,0 +1,239 @@
// Moved from apps/selfhost-compiler/boxes/parser/parser_box.hako
// ParserBox — Stage1 JSON v0 generator (coordinator, delegates to specialized boxes)
// Responsibility: Coordinate parsing, manage state, delegate to specialized boxes
// API: parse_program2(src) -> JSON
using lang.compiler.parser.scan.parser_string_utils_box
using lang.compiler.parser.scan.parser_ident_scan_box
using lang.compiler.parser.scan.parser_string_scan_box
using lang.compiler.parser.using.using_collector_box
using lang.compiler.parser.expr.parser_expr_box
using lang.compiler.parser.stmt.parser_stmt_box
using lang.compiler.parser.stmt.parser_control_box
box ParserBox {
gpos
usings_json
stage3
birth() {
me.gpos = 0
me.usings_json = "[]"
me.stage3 = 0
return 0
}
stage3_enable(flag) {
if flag == null { flag = 0 }
if flag == 0 { me.stage3 = 0 } else { me.stage3 = 1 }
return 0
}
stage3_enabled() {
if me.stage3 == 1 { return 1 }
return 0
}
// === State management ===
gpos_set(i) { me.gpos = i return 0 }
gpos_get() { return me.gpos }
// === JSON utilities ===
esc_json(s) {
local out = ""
local i = 0
local n = s.size()
loop(i < n) {
local ch = s.substring(i, i+1)
if ch == "\\" { out = out + "\\\\" }
else { if ch == "\"" { out = out + "\\\"" }
else { out = out + ch } }
i = i + 1
}
return out
}
// === Delegation to ParserStringUtilsBox ===
is_digit(ch) { return ParserStringUtilsBox.is_digit(ch) }
is_space(ch) { return ParserStringUtilsBox.is_space(ch) }
is_alpha(ch) { return ParserStringUtilsBox.is_alpha(ch) }
starts_with(src, i, pat) { return ParserStringUtilsBox.starts_with(src, i, pat) }
index_of(src, i, pat) { return ParserStringUtilsBox.index_of(src, i, pat) }
trim(s) { return ParserStringUtilsBox.trim(s) }
starts_with_kw(src, i, kw) { return ParserStringUtilsBox.starts_with_kw(src, i, kw) }
i2s(v) { return ParserStringUtilsBox.i2s(v) }
to_int(s) { return ParserStringUtilsBox.to_int(s) }
skip_ws(src, i) { return ParserStringUtilsBox.skip_ws(src, i) }
// === Delegation to scanner boxes ===
read_ident2(src, i) { return ParserIdentScanBox.scan_ident(src, i) }
read_string_lit(src, i) {
local pair = ParserStringScanBox.scan(src, i)
local at = pair.lastIndexOf("@")
local content = pair.substring(0, at)
local pos = 0
if at >= 0 { pos = me.to_int(pair.substring(at+1, pair.size())) }
else { pos = i }
me.gpos_set(pos)
return content
}
// === using system ===
add_using(kind, target, alias) {
local cur = me.usings_json
if cur == null || cur.size() == 0 { cur = "[]" }
local name = ""
local path = null
if kind == "path" {
path = target
if alias != null {
name = alias
} else {
local p = target
local idx = -1
local t = 0
loop(t < p.size()) {
if p.substring(t,t+1) == "/" { idx = t }
t = t + 1
}
if idx >= 0 { p = p.substring(idx+1, p.size()) }
if p.size() > 5 && me.starts_with(p, p.size()-5, ".hako") == 1 {
p = p.substring(0, p.size()-5)
} else {
if p.size() > 6 && me.starts_with(p, p.size()-6, ".nyash") == 1 {
p = p.substring(0, p.size()-6)
}
}
name = p
}
} else {
name = target
if alias != null { name = alias }
}
local entry = "{\"name\":\"" + me.esc_json(name) + "\""
if path != null { entry = entry + ",\"path\":\"" + me.esc_json(path) + "\"" }
entry = entry + "}"
if cur == "[]" {
me.usings_json = "[" + entry + "]"
return 0
}
local pos = cur.lastIndexOf("]")
if pos < 0 {
me.usings_json = "[" + entry + "]"
return 0
}
me.usings_json = cur.substring(0, pos) + "," + entry + "]"
return 0
}
extract_usings(src) {
me.usings_json = UsingCollectorBox.collect(src)
return 0
}
get_usings_json() {
return me.usings_json
}
// === Delegation to ParserExprBox ===
parse_expr2(src, i) {
local expr = new ParserExprBox()
return expr.parse_expr2(src, i, me)
}
// === Delegation to ParserStmtBox ===
parse_stmt2(src, i) {
local stmt = new ParserStmtBox()
return stmt.parse(src, i, me)
}
// === Delegation to ParserControlBox ===
parse_block2(src, i) {
local ctrl = new ParserControlBox()
return ctrl.parse_block(src, i, me)
}
// === Top-level program parser ===
parse_program2(src) {
local i = me.skip_ws(src, 0)
local body = "["
local first = 1
local cont_prog = 1
loop(cont_prog == 1) {
i = me.skip_ws(src, i)
if i >= src.size() {
cont_prog = 0
} else {
local start_i = i
local s = me.parse_stmt2(src, i)
i = me.gpos_get()
// Progress guard
if i <= start_i {
if i < src.size() { i = i + 1 }
else { i = src.size() }
me.gpos_set(i)
}
// consume optional semicolons
local done2 = 0
local guard2 = 0
local max2 = 100000
loop(done2 == 0) {
if guard2 > max2 { done2 = 1 }
else { guard2 = guard2 + 1 }
local before2 = i
i = me.skip_ws(src, i)
if i < src.size() && src.substring(i, i+1) == ";" {
i = i + 1
} else {
done2 = 1
}
if i == before2 { done2 = 1 }
}
if s.size() > 0 {
if first == 1 {
body = body + s
first = 0
} else {
body = body + "," + s
}
}
}
}
body = body + "]"
return "{\"version\":0,\"kind\":\"Program\",\"body\":" + body + "}"
}
}
static box ParserStub {
main(args) {
return 0
}
}

View File

@ -0,0 +1,72 @@
// Moved from apps/selfhost-compiler/boxes/parser/scan/parser_common_utils_box.hako
// ParserCommonUtilsBox — shared utility functions for parser boxes
// Responsibility: Provide common string/character operations used across parser components
// Notes: Pure utility functions; no state, no dependencies
static box ParserCommonUtilsBox {
// ===== 数値・文字列変換 =====
i2s(v) { return "" + v }
// ===== 文字判定 =====
is_alpha(ch) {
return (ch >= "A" && ch <= "Z") || (ch >= "a" && ch <= "z") || ch == "_"
}
is_digit(ch) {
return ch >= "0" && ch <= "9"
}
// ===== 文字列検索・操作 =====
dq() { return "\"" }
starts_with(src, i, pat) {
local n = src.size()
local m = pat.size()
if i + m > n { return 0 }
local k = 0
loop(k < m) {
if src.substring(i + k, i + k + 1) != pat.substring(k, k + 1) { return 0 }
k = k + 1
}
return 1
}
index_of(src, i, pat) {
local n = src.size()
local m = pat.size()
if m == 0 { return i }
local j = i
loop(j + m <= n) {
if me.starts_with(src, j, pat) { return j }
j = j + 1
}
return -1
}
trim(s) {
local i = 0
local n = s.size()
loop(i < n && (s.substring(i,i+1) == " " || s.substring(i,i+1) == "\t")) { i = i + 1 }
local j = n
loop(j > i && (s.substring(j-1,j) == " " || s.substring(j-1,j) == "\t" || s.substring(j-1,j) == ";")) { j = j - 1 }
return s.substring(i, j)
}
esc_json(s) {
local out = ""
local i = 0
local n = s.size()
loop(i < n) {
local ch = s.substring(i, i+1)
if ch == "\\" { out = out + "\\\\" }
else { if ch == "\"" { out = out + "\\\"" }
else { out = out + ch } }
i = i + 1
}
return out
}
}

View File

@ -0,0 +1,22 @@
// Moved from apps/selfhost-compiler/boxes/parser/scan/parser_ident_scan_box.hako
using lang.compiler.parser.scan.parser_string_utils_box as ParserStringUtilsBox
static box ParserIdentScanBox {
scan_ident(src, i) {
local j = i
local n = src.size()
if j >= n { return "@" + ParserStringUtilsBox.i2s(i) }
// first char: alpha or '_'
local ch = src.substring(j, j+1)
if !(ParserStringUtilsBox.is_alpha(ch) || ch == "_") { return "@" + ParserStringUtilsBox.i2s(i) }
j = j + 1
loop(j < n) {
local ch2 = src.substring(j, j+1)
if ParserStringUtilsBox.is_alpha(ch2) || ParserStringUtilsBox.is_digit(ch2) || ch2 == "_" { j = j + 1 }
else { break }
}
local s = src.substring(i, j)
return s + "@" + ParserStringUtilsBox.i2s(j)
}
}

View File

@ -0,0 +1,26 @@
// Moved from apps/selfhost-compiler/boxes/parser/scan/parser_number_scan_box.hako
// ParserNumberScanBox — scan integer literal starting at index i
// Returns: "{\"type\":\"Int\",\"value\":<digits>}@<pos>"
using lang.compiler.parser.scan.parser_common_utils_box as Utils
static box ParserNumberScanBox {
scan_int(src, i) {
if src == null { return "{\"type\":\"Int\",\"value\":0}@" + Utils.i2s(i) }
local n = src.size()
local j = i
local cont = 1
local guard = 0
local max = 100000
loop(cont == 1) {
if guard > max { cont = 0 } else { guard = guard + 1 }
if j < n {
if Utils.is_digit(src.substring(j, j+1)) { j = j + 1 } else { cont = 0 }
} else { cont = 0 }
}
local s = src.substring(i, j)
if s.size() == 0 { s = "0" }
return "{\"type\":\"Int\",\"value\":" + s + "}@" + Utils.i2s(j)
}
}

View File

@ -0,0 +1,50 @@
// Moved from apps/selfhost-compiler/boxes/parser/scan/parser_string_scan_box.hako
// ParserStringScanBox — string literal scanner (escape-aware)
// Responsibility: read a Ny string literal starting at index i (i points to '"').
// Returns: "<content>@<pos>" where <pos> is the index after the closing quote.
// Notes: pure string scanning; no external deps.
using lang.compiler.parser.scan.parser_common_utils_box as Utils
static box ParserStringScanBox {
scan(src, i) {
if src == null { return "@" + Utils.i2s(i) }
local n = src.size()
local j = i
if j >= n || src.substring(j, j+1) != "\"" { return "@" + Utils.i2s(i) }
j = j + 1
local out = ""
local guard = 0
local max = 200000
loop(j < n) {
if guard > max { break } else { guard = guard + 1 }
local ch = src.substring(j, j+1)
if ch == "\"" {
j = j + 1
return out + "@" + Utils.i2s(j)
}
if ch == "\\" && j + 1 < n {
local nx = src.substring(j+1, j+2)
if nx == "\"" { out = out + "\"" j = j + 2 }
else {
if nx == "\\" { out = out + "\\" j = j + 2 } else {
if nx == "n" { out = out + "\n" j = j + 2 } else {
if nx == "r" { out = out + "\n" j = j + 2 } else {
if nx == "t" { out = out + "\t" j = j + 2 } else {
if nx == "u" && j + 5 < n { out = out + src.substring(j, j+6) j = j + 6 }
else { out = out + nx j = j + 2 }
}
}
}
}
}
} else {
out = out + ch
j = j + 1
}
}
// if unterminated, return what we have and the last pos to avoid infinite loops
return out + "@" + Utils.i2s(j)
}
}

View File

@ -0,0 +1,20 @@
// Moved from apps/selfhost-compiler/boxes/parser/scan/parser_string_utils_box.hako
// ParserStringUtilsBox — Delegation to StringHelpers (unified string utilities)
// Responsibility: Backward compatibility wrapper for parser code
// Notes: All functionality now provided by apps/selfhost/common/string_helpers.hako
using "lang/src/shared/common/string_helpers.hako" as StringHelpers
static box ParserStringUtilsBox {
// Delegate all methods to StringHelpers (centralized implementation)
i2s(v) { return StringHelpers.int_to_str(v) }
is_digit(ch) { return StringHelpers.is_digit(ch) }
is_space(ch) { return StringHelpers.is_space(ch) }
is_alpha(ch) { return StringHelpers.is_alpha(ch) }
starts_with(src, i, pat) { return StringHelpers.starts_with(src, i, pat) }
starts_with_kw(src, i, kw) { return StringHelpers.starts_with_kw(src, i, kw) }
index_of(src, i, pat) { return StringHelpers.index_of(src, i, pat) }
trim(s) { return StringHelpers.trim(s) }
to_int(s) { return StringHelpers.to_i64(s) }
skip_ws(src, i) { return StringHelpers.skip_ws(src, i) }
}

View File

@ -0,0 +1,173 @@
// Moved from apps/selfhost-compiler/boxes/parser/stmt/parser_control_box.hako
// ParserControlBox — if/loop/break/continue parser
// Responsibility: Parse control flow statements
// API: parse_if, parse_loop, parse_break, parse_continue, parse_block
static box ParserControlBox {
// Parse: if (cond) { ... } else { ... }
parse_if(src, i, stmt_start, ctx) {
local j = i + 2 // skip "if"
j = ctx.skip_ws(src, j)
local paren = 0
if src.substring(j, j+1) == "(" { paren = 1 j = j + 1 }
local cond = ctx.parse_expr2(src, j)
j = ctx.gpos_get()
if paren == 1 {
j = ctx.skip_ws(src, j)
if src.substring(j, j+1) == ")" { j = j + 1 }
}
j = ctx.skip_ws(src, j)
local then_res = me.parse_block(src, j, ctx)
local at1 = then_res.lastIndexOf("@")
local then_json = then_res.substring(0, at1)
j = ctx.to_int(then_res.substring(at1+1, then_res.size()))
j = ctx.skip_ws(src, j)
local else_json = null
if ctx.starts_with_kw(src, j, "else") == 1 {
j = j + 4
j = ctx.skip_ws(src, j)
local else_res = me.parse_block(src, j, ctx)
local at2 = else_res.lastIndexOf("@")
else_json = else_res.substring(0, at2)
j = ctx.to_int(else_res.substring(at2+1, else_res.size()))
}
if j <= stmt_start {
if j < src.size() { j = j + 1 } else { j = src.size() }
}
ctx.gpos_set(j)
if else_json == null {
return "{\"type\":\"If\",\"cond\":" + cond + ",\"then\":" + then_json + "}"
} else {
return "{\"type\":\"If\",\"cond\":" + cond + ",\"then\":" + then_json + ",\"else\":" + else_json + "}"
}
}
// Parse: loop(cond) { ... }
parse_loop(src, i, stmt_start, ctx) {
local j = i + 4 // skip "loop"
j = ctx.skip_ws(src, j)
if src.substring(j, j+1) == "(" { j = j + 1 }
local cond = ctx.parse_expr2(src, j)
j = ctx.gpos_get()
j = ctx.skip_ws(src, j)
if src.substring(j, j+1) == ")" { j = j + 1 }
j = ctx.skip_ws(src, j)
local body_res = me.parse_block(src, j, ctx)
local at3 = body_res.lastIndexOf("@")
local body_json = body_res.substring(0, at3)
j = ctx.to_int(body_res.substring(at3+1, body_res.size()))
if j <= stmt_start {
if j < src.size() { j = j + 1 } else { j = src.size() }
}
ctx.gpos_set(j)
return "{\"type\":\"Loop\",\"cond\":" + cond + ",\"body\":" + body_json + "}"
}
// Parse: break → {type:"Break"} (Stage-3) or no-op
parse_break(src, i, stmt_start, ctx) {
local j = i + 5 // skip "break"
if ctx.stage3_enabled() == 1 {
j = ctx.skip_ws(src, j)
if j <= stmt_start {
if j < src.size() { j = j + 1 } else { j = src.size() }
}
ctx.gpos_set(j)
return "{\"type\":\"Break\"}"
}
if j <= stmt_start {
if j < src.size() { j = j + 1 } else { j = src.size() }
}
ctx.gpos_set(j)
return "{\"type\":\"Expr\",\"expr\":{\"type\":\"Int\",\"value\":0}}"
}
// Parse: continue → {type:"Continue"} (Stage-3) or no-op
parse_continue(src, i, stmt_start, ctx) {
local j = i + 8 // skip "continue"
if ctx.stage3_enabled() == 1 {
j = ctx.skip_ws(src, j)
if j <= stmt_start {
if j < src.size() { j = j + 1 } else { j = src.size() }
}
ctx.gpos_set(j)
return "{\"type\":\"Continue\"}"
}
if j <= stmt_start {
if j < src.size() { j = j + 1 } else { j = src.size() }
}
ctx.gpos_set(j)
return "{\"type\":\"Expr\",\"expr\":{\"type\":\"Int\",\"value\":0}}"
}
// Parse: { stmt1; stmt2; ... }
parse_block(src, i, ctx) {
local j = ctx.skip_ws(src, i)
if src.substring(j, j+1) != "{" { return "[]@" + ctx.i2s(j) }
j = j + 1
local body = "["
local first = 1
local cont_block = 1
loop(cont_block == 1) {
j = ctx.skip_ws(src, j)
if j >= src.size() {
cont_block = 0
} else {
if src.substring(j, j+1) == "}" {
j = j + 1
cont_block = 0
} else {
local start_j = j
local s = ctx.parse_stmt2(src, j)
j = ctx.gpos_get()
// Progress guard: ensure forward movement to avoid infinite loop on malformed input
if j <= start_j {
if j < src.size() { j = j + 1 } else { j = src.size() }
ctx.gpos_set(j)
}
// consume optional semicolons (ASI minimal)
local done = 0
local guard = 0
local max = 100000
loop(done == 0) {
if guard > max { done = 1 } else { guard = guard + 1 }
local before = j
j = ctx.skip_ws(src, j)
if j < src.size() && src.substring(j, j+1) == ";" { j = j + 1 } else { done = 1 }
if j == before { done = 1 }
}
if s.size() > 0 {
if first == 1 {
body = body + s
first = 0
} else {
body = body + "," + s
}
}
}
}
}
body = body + "]"
return body + "@" + ctx.i2s(j)
}
}

View File

@ -0,0 +1,152 @@
// Moved from apps/selfhost-compiler/boxes/parser/stmt/parser_exception_box.hako
// ParserExceptionBox — try/catch/throw parser
// Responsibility: Parse exception handling constructs
// API: parse_try(src, i, ctx) -> JSON, parse_throw(src, i, ctx) -> JSON
static box ParserExceptionBox {
// Parse: throw expr → {type:"Throw", expr:...} (Stage-3) or {type:"Expr", expr:...} (fallback)
parse_throw(src, i, stmt_start, ctx) {
local j = i + 5 // skip "throw"
j = ctx.skip_ws(src, j)
local e_throw = ctx.parse_expr2(src, j)
j = ctx.gpos_get()
if ctx.stage3_enabled() == 1 {
if j <= stmt_start {
if j < src.size() { j = j + 1 } else { j = src.size() }
}
ctx.gpos_set(j)
return "{\"type\":\"Throw\",\"expr\":" + e_throw + "}"
}
if j <= stmt_start {
if j < src.size() { j = j + 1 } else { j = src.size() }
}
ctx.gpos_set(j)
return "{\"type\":\"Expr\",\"expr\":" + e_throw + "}"
}
// Parse: try { ... } (catch ...)* (cleanup { ... })? → {type:"Try", ...} (Stage-3) or no-op
parse_try(src, i, stmt_start, ctx) {
local j = i + 3 // skip "try"
j = ctx.skip_ws(src, j)
// parse try block
local try_res = ctx.parse_block2(src, j)
local at_t = try_res.lastIndexOf("@")
local try_json = try_res.substring(0, at_t)
j = ctx.to_int(try_res.substring(at_t+1, try_res.size()))
local catches_json = "["
local catch_first = 1
// zero or more catch
local guard_ct = 0
local max_ct = 100
local cont_ct = 1
loop(cont_ct == 1) {
if guard_ct > max_ct { cont_ct = 0 } else { guard_ct = guard_ct + 1 }
j = ctx.skip_ws(src, j)
if ctx.starts_with_kw(src, j, "catch") == 1 {
j = j + 5
j = ctx.skip_ws(src, j)
local catch_type = null
local catch_param = null
if src.substring(j, j+1) == "(" {
j = j + 1
j = ctx.skip_ws(src, j)
// optional type + name
if ctx.is_alpha(src.substring(j, j+1)) {
local id1 = ctx.read_ident2(src, j)
local at1 = id1.lastIndexOf("@")
catch_type = id1.substring(0, at1)
j = ctx.to_int(id1.substring(at1+1, id1.size()))
j = ctx.skip_ws(src, j)
}
if ctx.is_alpha(src.substring(j, j+1)) {
local id2 = ctx.read_ident2(src, j)
local at2 = id2.lastIndexOf("@")
catch_param = id2.substring(0, at2)
j = ctx.to_int(id2.substring(at2+1, id2.size()))
j = ctx.skip_ws(src, j)
}
if src.substring(j, j+1) == ")" { j = j + 1 }
}
j = ctx.skip_ws(src, j)
// catch body
local c_res = ctx.parse_block2(src, j)
local atc = c_res.lastIndexOf("@")
j = ctx.to_int(c_res.substring(atc+1, c_res.size()))
if ctx.stage3_enabled() == 1 {
local entry = "{"
local wrote = 0
if catch_param != null && catch_param.size() > 0 {
entry = entry + "\"param\":\"" + ctx.esc_json(catch_param) + "\""
wrote = 1
}
if catch_type != null && catch_type.size() > 0 {
if wrote == 1 { entry = entry + "," }
entry = entry + "\"typeHint\":\"" + ctx.esc_json(catch_type) + "\""
wrote = 1
}
local body_json = c_res.substring(0, atc)
if wrote == 1 { entry = entry + "," }
entry = entry + "\"body\":" + body_json + "}"
if catch_first == 0 {
catches_json = catches_json + "," + entry
} else {
catches_json = catches_json + entry
catch_first = 0
}
}
} else {
cont_ct = 0
}
}
catches_json = catches_json + "]"
// optional cleanup
j = ctx.skip_ws(src, j)
local finally_json = null
if ctx.starts_with_kw(src, j, "cleanup") == 1 {
j = j + 7
j = ctx.skip_ws(src, j)
local f_res = ctx.parse_block2(src, j)
local atf = f_res.lastIndexOf("@")
j = ctx.to_int(f_res.substring(atf+1, f_res.size()))
finally_json = f_res.substring(0, atf)
}
if ctx.stage3_enabled() == 1 {
if j <= stmt_start {
if j < src.size() { j = j + 1 } else { j = src.size() }
}
ctx.gpos_set(j)
local node = "{\"type\":\"Try\",\"try\":" + try_json + ",\"catches\":" + catches_json
if finally_json != null { node = node + ",\"finally\":" + finally_json }
node = node + "}"
return node
}
if j <= stmt_start {
if j < src.size() { j = j + 1 } else { j = src.size() }
}
ctx.gpos_set(j)
return "{\"type\":\"Expr\",\"expr\":{\"type\":\"Int\",\"value\":0}}"
}
}

View File

@ -0,0 +1,202 @@
// Moved from apps/selfhost-compiler/boxes/parser/stmt/parser_stmt_box.hako
// ParserStmtBox — statement parser coordinator
// Responsibility: Parse statements and delegate to specialized boxes
// API: parse(src, i, ctx) -> JSON
using lang.compiler.parser.stmt.parser_control_box
using lang.compiler.parser.stmt.parser_exception_box
static box ParserStmtBox {
parse(src, i, ctx) {
local j = ctx.skip_ws(src, i)
local stmt_start = j
// using statement
if ctx.starts_with_kw(src, j, "using") == 1 {
return me.parse_using(src, j, stmt_start, ctx)
}
// assignment: IDENT '=' expr
if j < src.size() && ctx.is_alpha(src.substring(j, j+1)) {
local idp0 = ctx.read_ident2(src, j)
local at0 = idp0.lastIndexOf("@")
if at0 > 0 {
local name0 = idp0.substring(0, at0)
local k0 = ctx.to_int(idp0.substring(at0+1, idp0.size()))
k0 = ctx.skip_ws(src, k0)
if k0 < src.size() && src.substring(k0, k0+1) == "=" {
local eq_two = "="
if k0 + 1 < src.size() { eq_two = src.substring(k0, k0+2) }
if eq_two != "==" {
k0 = k0 + 1
k0 = ctx.skip_ws(src, k0)
local default_local = "{\"type\":\"Int\",\"value\":0}"
local expr_json0 = default_local
local end_pos0 = k0
if k0 < src.size() {
local ahead = src.substring(k0, k0+1)
if ahead != "}" && ahead != ";" {
expr_json0 = ctx.parse_expr2(src, k0)
end_pos0 = ctx.gpos_get()
}
}
k0 = end_pos0
if k0 <= stmt_start {
if k0 < src.size() { k0 = k0 + 1 } else { k0 = src.size() }
}
ctx.gpos_set(k0)
return "{\"type\":\"Local\",\"name\":\"" + name0 + "\",\"expr\":" + expr_json0 + "}"
}
}
}
}
// return statement
if ctx.starts_with_kw(src, j, "return") == 1 {
j = j + 6
j = ctx.skip_ws(src, j)
local default_ret = "{\"type\":\"Int\",\"value\":0}"
local expr_json_ret = default_ret
local end_pos_ret = j
if j < src.size() {
local ahead_ret = src.substring(j, j+1)
if ahead_ret != "}" && ahead_ret != ";" {
expr_json_ret = ctx.parse_expr2(src, j)
end_pos_ret = ctx.gpos_get()
}
}
j = end_pos_ret
if j <= stmt_start {
if j < src.size() { j = j + 1 } else { j = src.size() }
}
ctx.gpos_set(j)
return "{\"type\":\"Return\",\"expr\":" + expr_json_ret + "}"
}
// local declaration
if ctx.starts_with_kw(src, j, "local") == 1 {
j = j + 5
j = ctx.skip_ws(src, j)
local idp = ctx.read_ident2(src, j)
local at = idp.lastIndexOf("@")
local name = idp.substring(0, at)
j = ctx.to_int(idp.substring(at+1, idp.size()))
j = ctx.skip_ws(src, j)
if j < src.size() && src.substring(j, j+1) == "=" { j = j + 1 }
j = ctx.skip_ws(src, j)
local default_local = "{\"type\":\"Int\",\"value\":0}"
local expr_json_local = default_local
local end_pos_local = j
if j < src.size() {
local ahead_local = src.substring(j, j+1)
if ahead_local != "}" && ahead_local != ";" {
expr_json_local = ctx.parse_expr2(src, j)
end_pos_local = ctx.gpos_get()
}
}
j = end_pos_local
if j <= stmt_start {
if j < src.size() { j = j + 1 } else { j = src.size() }
}
ctx.gpos_set(j)
return "{\"type\":\"Local\",\"name\":\"" + name + "\",\"expr\":" + expr_json_local + "}"
}
// Delegate to specialized boxes
if ctx.starts_with_kw(src, j, "if") == 1 {
return ParserControlBox.parse_if(src, j, stmt_start, ctx)
}
if ctx.starts_with_kw(src, j, "loop") == 1 {
return ParserControlBox.parse_loop(src, j, stmt_start, ctx)
}
if ctx.starts_with_kw(src, j, "break") == 1 {
return ParserControlBox.parse_break(src, j, stmt_start, ctx)
}
if ctx.starts_with_kw(src, j, "continue") == 1 {
return ParserControlBox.parse_continue(src, j, stmt_start, ctx)
}
if ctx.starts_with_kw(src, j, "throw") == 1 {
return ParserExceptionBox.parse_throw(src, j, stmt_start, ctx)
}
if ctx.starts_with_kw(src, j, "try") == 1 {
return ParserExceptionBox.parse_try(src, j, stmt_start, ctx)
}
// Fallback: expression or unknown token
local expr_start = j
local e = ctx.parse_expr2(src, j)
j = ctx.gpos_get()
if j <= expr_start {
if j < src.size() { j = j + 1 } else { j = src.size() }
}
ctx.gpos_set(j)
return "{\"type\":\"Expr\",\"expr\":" + e + "}"
}
// Parse using statement
parse_using(src, i, stmt_start, ctx) {
local j = i + 5 // skip "using"
j = ctx.skip_ws(src, j)
if src.substring(j, j+1) == "\"" {
local p = ctx.read_string_lit(src, j)
j = ctx.gpos_get()
j = ctx.skip_ws(src, j)
local alias = null
if ctx.starts_with_kw(src, j, "as") == 1 {
j = j + 2
j = ctx.skip_ws(src, j)
local idp = ctx.read_ident2(src, j)
local at = idp.lastIndexOf("@")
alias = idp.substring(0, at)
j = ctx.to_int(idp.substring(at+1, idp.size()))
}
ctx.add_using("path", p, alias)
} else {
if ctx.is_alpha(src.substring(j, j+1)) {
local idp = ctx.read_ident2(src, j)
local at = idp.lastIndexOf("@")
local name = idp.substring(0, at)
j = ctx.to_int(idp.substring(at+1, idp.size()))
local cont = 1
loop(cont == 1) {
j = ctx.skip_ws(src, j)
if src.substring(j, j+1) == "." {
j = j + 1
j = ctx.skip_ws(src, j)
idp = ctx.read_ident2(src, j)
at = idp.lastIndexOf("@")
name = name + "." + idp.substring(0, at)
j = ctx.to_int(idp.substring(at+1, idp.size()))
} else {
cont = 0
}
}
j = ctx.skip_ws(src, j)
local alias2 = null
if ctx.starts_with_kw(src, j, "as") == 1 {
j = j + 2
j = ctx.skip_ws(src, j)
idp = ctx.read_ident2(src, j)
at = idp.lastIndexOf("@")
alias2 = idp.substring(0, at)
j = ctx.to_int(idp.substring(at+1, idp.size()))
}
ctx.add_using("ns", name, alias2)
}
}
// ensure progress
if j <= stmt_start {
if j < src.size() { j = j + 1 } else { j = src.size() }
}
ctx.gpos_set(j)
return ""
}
}

View File

@ -0,0 +1,80 @@
// Moved from apps/selfhost-compiler/boxes/parser/using/using_collector_box.hako
// UsingCollectorBox — line-based `using` extractor (Path/Namespace → JSON array)
// Responsibility: Parse source text line-by-line and produce a JSON array
// [{"name":"AliasOrBase","path":"path/if/any"}, ...]
// Notes:
// - ParserBox.extract_usings delegates to this box (Phase 2 split)
// - Pure string scan依存ゼロ。FailFastはせず、安全にスキップでループを進める
using lang.compiler.parser.scan.parser_common_utils_box as Utils
static box UsingCollectorBox {
// Public API: collect line-based using declarations to JSON array string
collect(src) {
if src == null { return "[]" }
local n = src.size()
local i = 0
local first = 1
local out = "["
loop(i < n) {
// line slice [i, j)
local j = i
loop(j < n && src.substring(j, j+1) != "\n") { j = j + 1 }
local line = src.substring(i, j)
// trim left spaces/tabs
local k = 0
loop(k < line.size() && (line.substring(k,k+1) == " " || line.substring(k,k+1) == "\t")) { k = k + 1 }
if Utils.starts_with(line, k, "using ") == 1 {
local rest = Utils.trim(line.substring(k + 6, line.size()))
// split on ' as '
local as_pos = Utils.index_of(rest, 0, " as ")
local target = rest
local alias = null
if as_pos >= 0 { target = Utils.trim(rest.substring(0, as_pos)) alias = Utils.trim(rest.substring(as_pos + 4, rest.size())) }
// path or namespace
local is_path = 0
if target.size() > 0 {
if Utils.starts_with(target, 0, Utils.dq()) == 1 { is_path = 1 }
if Utils.starts_with(target, 0, "./") == 1 { is_path = 1 }
if Utils.starts_with(target, 0, "/") == 1 { is_path = 1 }
if target.size() >= 5 && Utils.starts_with(target, target.size()-5, ".hako") == 1 { is_path = 1 }
if target.size() >= 6 && Utils.starts_with(target, target.size()-6, ".nyash") == 1 { is_path = 1 }
}
local name = ""
local path = null
if is_path == 1 {
// strip quotes
if Utils.starts_with(target, 0, Utils.dq()) == 1 {
target = target.substring(1, target.size())
if target.size() > 0 && target.substring(target.size()-1, target.size()) == Utils.dq() { target = target.substring(0, target.size()-1) }
}
path = target
if alias != null { name = alias } else {
// basename
local p = target
local idx = -1
local t = 0
loop(t < p.size()) { if p.substring(t,t+1) == "/" { idx = t } t = t + 1 }
if idx >= 0 { p = p.substring(idx+1, p.size()) }
// strip extension
if p.size() > 5 && Utils.starts_with(p, p.size()-5, ".hako") == 1 { p = p.substring(0, p.size()-5) }
else { if p.size() > 6 && Utils.starts_with(p, p.size()-6, ".nyash") == 1 { p = p.substring(0, p.size()-6) } }
name = p
}
} else {
name = target
}
// append entry
if first == 0 { out = out + "," } else { first = 0 }
out = out + "{" + Utils.dq() + "name" + Utils.dq() + ":" + Utils.dq() + Utils.esc_json(name) + Utils.dq()
if path != null { out = out + "," + Utils.dq() + "path" + Utils.dq() + ":" + Utils.dq() + Utils.esc_json(path) + Utils.dq() }
out = out + "}"
}
i = j + 1
}
out = out + "]"
return out
}
}