Untitled

Java Guest 31 Views Size: 30.01 KB Posted on: Jun 18, 26 @ 2:09 AM
  1. /*
  2.  * HexDroidIRC - An IRC Client for Android
  3.  * Copyright (C) 2026 boxlabs
  4.  *
  5.  * This program is free software: you can redistribute it and/or modify
  6.  * it under the terms of the GNU General Public License as published by
  7.  * the Free Software Foundation, either version 3 of the License, or
  8.  * (at your option) any later version.
  9.  *
  10.  * This program is distributed in the hope that it will be useful,
  11.  * but WITHOUT ANY WARRANTY; without even the implied warranty of
  12.  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  13.  * GNU General Public License for more details.
  14.  *
  15.  * You should have received a copy of the GNU General Public License
  16.  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
  17.  */
  18.  
  19. package com.boxlabs.hexdroid.script
  20.  
  21. /**
  22.  * HexDroid's scripting interpreter, a custom mIRC-flavoured DSL, implementing [ScriptBackend] so
  23.  * it implements the same ScriptBackend interface (the engine is backend-neutral):
  24.  * ScriptEngine(host, HexScriptBackend())
  25.  *
  26.  * Runs on Main, synchronously, See the `.hex` language spec.
  27.  *
  28.  * this is v1 of the interpreter.
  29.  */
  30. class HexScriptBackend : ScriptBackend {
  31.  
  32.     override val scriptExtension: String = "hex"
  33.  
  34.     private lateinit var cb: EngineCallbacks
  35.     private var budget: Budget = Budget(200_000, 200)
  36.  
  37.     /** Global %vars persist across events within a session; cleared on reset(). */
  38.     private val globals = HashMap<String, HexVal>()
  39.     /** User-defined aliases, kept locally so they can be invoked as value functions $name(...). */
  40.     private val aliases = HashMap<String, HexBlock>()
  41.  
  42.     override fun start(callbacks: EngineCallbacks, budget: Budget) {
  43.         cb = callbacks; this.budget = budget
  44.     }
  45.  
  46.     override fun reset() { globals.clear(); aliases.clear() }
  47.     override fun shutdown() { globals.clear() }
  48.  
  49.     override fun loadScript(name: String, source: String): String? {
  50.         val blocks = try {
  51.             HexParser(source).parse()
  52.         } catch (e: HexError) {
  53.             val msg = "parse error: ${e.message}"; cb.log("[$name] $msg"); return msg
  54.         }
  55.         for (b in blocks) when (b.kind) {
  56.             HexBlock.Kind.EVENT -> cb.registerEvent(b.name, b)              // engine uppercases the key
  57.             HexBlock.Kind.ALIAS -> { aliases[b.name.lowercase()] = b; cb.registerCommand(b.name.lowercase(), b) }
  58.         }
  59.         return null
  60.     }
  61.  
  62.     // ScriptBackend dispatch
  63.  
  64.     override fun dispatchTransform(handlers: List<Any>, event: EventData): TextResult {
  65.         var text = event.text
  66.         var halted = false
  67.         for (h in handlers) {
  68.             val block = h as? HexBlock ?: continue
  69.             if (!filterMatches(block, text)) continue
  70.             val env = envForEvent(event.copy(text = text), cb)
  71.             val flow = runBody(block.body, env)
  72.             text = env.fields["text"] ?: text         // `rewrite` updates env text
  73.             if (flow == Flow.HALT) { halted = true; break }
  74.         }
  75.         return TextResult(text, halted)
  76.     }
  77.  
  78.     override fun dispatchNotify(handlers: List<Any>, event: EventData) {
  79.         for (h in handlers) {
  80.             val block = h as? HexBlock ?: continue
  81.             if (!filterMatches(block, event.text)) continue
  82.             runBody(block.body, envForEvent(event, cb))
  83.         }
  84.     }
  85.  
  86.     override fun runCommand(handler: Any, args: String, network: String?, buffer: String?) {
  87.         val block = handler as? HexBlock ?: return
  88.         val event = EventData(
  89.             network = network ?: "", buffer = buffer ?: "", text = args,
  90.             args = if (args.isBlank()) emptyList() else args.trim().split(Regex("\\s+")),
  91.         )
  92.         runBody(block.body, envForEvent(event, cb))
  93.     }
  94.  
  95.     private fun filterMatches(block: HexBlock, text: String): Boolean {
  96.         val f = block.filter ?: return true
  97.         return glob(f, text)
  98.     }
  99.  
  100.     // evaluator
  101.  
  102.     private enum class Flow { NORMAL, BREAK, CONTINUE, HALT, RETURN }
  103.  
  104.     /** Shared per-top-level-dispatch budget/recursion state (one Frame across nested calls). */
  105.     private inner class Frame {
  106.         var steps = 0
  107.         val deadline = System.currentTimeMillis() + budget.maxMillis
  108.         var depth = 0
  109.     }
  110.  
  111.     /** Per-call scope. Nested function calls share the [frame] (so one budget bounds the whole tree). */
  112.     private inner class Env(
  113.         val frame: Frame,
  114.         val fields: HashMap<String, String>,
  115.         val args: List<String>,
  116.         val locals: HashMap<String, HexVal>,
  117.     ) {
  118.         var returnValue: HexVal? = null
  119.  
  120.         fun tick() {
  121.             if (++frame.steps > budget.maxInstructions) throw HexAbort("step budget exceeded")
  122.             if (System.currentTimeMillis() > frame.deadline) throw HexAbort("time budget exceeded")
  123.         }
  124.  
  125.         /** A child scope for a function call: fresh locals + bound args, shared frame/fields. */
  126.         fun childForCall(callArgs: List<String>): Env = Env(frame, fields, callArgs, HashMap())
  127.     }
  128.  
  129.     private fun runBody(body: List<HexStmt>, env: Env): Flow {
  130.         for (s in body) {
  131.             env.tick()
  132.             when (s) {
  133.                 is HexStmt.Command -> if (execCommand(s, env) == Flow.HALT) return Flow.HALT
  134.                 is HexStmt.If -> {
  135.                     var ran = false
  136.                     for ((cond, b) in s.branches) {
  137.                         if (evalCond(cond, env)) { val fl = runBody(b, env); if (fl != Flow.NORMAL) return fl; ran = true; break }
  138.                     }
  139.                     if (!ran && s.elseBody != null) { val fl = runBody(s.elseBody, env); if (fl != Flow.NORMAL) return fl }
  140.                 }
  141.                 is HexStmt.While -> {
  142.                     while (evalCond(s.cond, env)) {
  143.                         env.tick()
  144.                         when (runBody(s.body, env)) {
  145.                             Flow.BREAK -> break
  146.                             Flow.CONTINUE, Flow.NORMAL -> {}
  147.                             Flow.HALT -> return Flow.HALT
  148.                             Flow.RETURN -> return Flow.RETURN
  149.                         }
  150.                     }
  151.                 }
  152.                 is HexStmt.ViewStmt -> {
  153.                     val tree = try { HexViewParser(renderViewBody(s.rawBody, env)).parse() } catch (_: Throwable) { null }
  154.                     if (tree != null) cb.mountView(tree)
  155.                 }
  156.                 is HexStmt.ForEach -> {
  157.                     val coll = evalVal(s.collExpr, env)
  158.                     val items: List<HexVal> = when (coll) {
  159.                         is HexVal.Lst -> coll.items.toList()
  160.                         is HexVal.Mp -> coll.map.keys.map { HexVal.Str(it) }
  161.                         is HexVal.Str -> if (coll.s.isEmpty()) emptyList() else coll.s.split(" ").map { HexVal.Str(it) }
  162.                     }
  163.                     val nm = varName(s.itemVar)
  164.                     loop@ for (it in items) {
  165.                         env.tick(); env.locals[nm] = it
  166.                         when (runBody(s.body, env)) {
  167.                             Flow.BREAK -> break@loop
  168.                             Flow.CONTINUE, Flow.NORMAL -> {}
  169.                             Flow.HALT -> return Flow.HALT
  170.                             Flow.RETURN -> return Flow.RETURN
  171.                         }
  172.                     }
  173.                 }
  174.                 HexStmt.Break -> return Flow.BREAK
  175.                 HexStmt.Continue -> return Flow.CONTINUE
  176.                 HexStmt.Halt -> return Flow.HALT
  177.                 is HexStmt.Return -> { if (s.expr != null) env.returnValue = evalVal(s.expr, env); return Flow.RETURN }
  178.             }
  179.         }
  180.         return Flow.NORMAL
  181.     }
  182.  
  183.     private fun execCommand(c: HexStmt.Command, env: Env): Flow {
  184.         val verb = c.verb.lowercase()
  185.         val raw = c.rawArgs
  186.         when (verb) {
  187.             "set" -> doSet(raw, env)
  188.             "unset" -> { val n = varName(expand(raw, env).trim()); env.locals.remove(n); globals.remove(n) }
  189.             "inc" -> bumpVar(raw, env, +1)
  190.             "dec" -> bumpVar(raw, env, -1)
  191.             "push" -> { val (nm, rest) = head(raw); varRef(varName(nm), env)?.let { HexValues.push(it, evalVal(rest, env)) } }
  192.             "setat" -> doSetAt(raw, env)
  193.             "rewrite" -> env.fields["text"] = expand(raw, env)
  194.             "echo" -> { val (t, txt) = splitTarget(expand(raw, env)); cb.echo(t, null, txt) }
  195.             "msg" -> { val (t, txt) = splitTarget(expand(raw, env)); if (t != null) cb.sendMessage(t, txt) }
  196.             "raw" -> cb.sendRaw(expand(raw, env))
  197.             "signal" -> { val a = expand(raw, env).trim().split(' '); if (a.isNotEmpty()) cb.raiseEvent("SIGNAL:${a[0].uppercase()}", emptyMap(), a.drop(1)) }
  198.             "timer" -> doTimer(raw, env)
  199.             "toast", "decorate", "action", "sidebar" -> cb.uiIntent(verb, expand(raw, env).trim().split(' '))
  200.             "http.get" -> doHttp(raw, env, post = false)
  201.             "http.post" -> doHttp(raw, env, post = true)
  202.             else -> when {
  203.                 verb.startsWith("age.") || verb.startsWith("media.") ->
  204.                     cb.capability(verb, expand(raw, env).trim().split(' ').filter { it.isNotEmpty() })
  205.                 aliases.containsKey(verb) -> return runAliasCommand(verb, raw, env)
  206.                 else -> cb.appCommand("${c.verb} ${expand(raw, env)}".trim())   // unknown -> slash pipeline
  207.             }
  208.         }
  209.         return Flow.NORMAL
  210.     }
  211.  
  212.     /** Invoke a user alias as a statement: bind space-separated args to $1.., run body. */
  213.     private fun runAliasCommand(verb: String, raw: String, env: Env): Flow {
  214.         val block = aliases[verb] ?: return Flow.NORMAL
  215.         if (env.frame.depth >= MAX_CALL_DEPTH) throw HexAbort("call depth exceeded")
  216.         val argExprs = splitTop(raw.trim(), " ").map { it.trim() }.filter { it.isNotEmpty() }
  217.         val callArgs = argExprs.map { evalVal(it, env).asStr() }
  218.         val child = env.childForCall(callArgs)
  219.         env.frame.depth++
  220.         val fl = try { runBody(block.body, child) } finally { env.frame.depth-- }
  221.         return if (fl == Flow.HALT) Flow.HALT else Flow.NORMAL
  222.     }
  223.  
  224.     private fun doSet(raw: String, env: Env) {
  225.         var s = raw.trim(); var local = false
  226.         if (s.startsWith("-l ")) { local = true; s = s.removePrefix("-l ").trim() }
  227.         val sp = s.indexOf(' ')
  228.         val name = varName(if (sp < 0) s else s.substring(0, sp))
  229.         val value = if (sp < 0) HexVal.EMPTY else evalVal(s.substring(sp + 1), env)
  230.         if (local) env.locals[name] = value else globals[name] = value
  231.     }
  232.  
  233.     private fun bumpVar(raw: String, env: Env, by: Int) {
  234.         val name = varName(expand(raw, env).trim())
  235.         val cur = (varRef(name, env)?.asNum() ?: 0.0)
  236.         val nv = HexVal.num(cur + by)
  237.         if (env.locals.containsKey(name)) env.locals[name] = nv else globals[name] = nv
  238.     }
  239.  
  240.     private fun doTimer(raw: String, env: Env) {
  241.         val a = expand(raw, env).trim().split(' ')
  242.         if (a.size < 2) return
  243.         val ms = a[0].toLongOrNull() ?: return
  244.         cb.scheduleSignal(ms, a[1].uppercase(), a.drop(2))
  245.     }
  246.  
  247.     /** http.get <url> <signal> [ctx...]   /   http.post <url> <body> <signal> [ctx...] */
  248.     private fun doHttp(raw: String, env: Env, post: Boolean) {
  249.         val parts = expand(raw, env).trim().split(' ')
  250.         if (post && parts.size < 3) return
  251.         if (!post && parts.size < 2) return
  252.         val url = parts[0]
  253.         val onDone: (HttpResult) -> Unit = { res ->
  254.             // raised on Main by the engine; surface body/status + passthrough ctx as $1-
  255.             val ctxStart = if (post) 3 else 2
  256.             val sigName = (if (post) parts[2] else parts[1]).uppercase()
  257.             val fields = mapOf(
  258.                 "httpok" to res.ok.toString(), "httpstatus" to res.status.toString(), "httpbody" to res.body,
  259.             )
  260.             cb.raiseEvent("SIGNAL:$sigName", fields, parts.drop(ctxStart))
  261.         }
  262.         if (post) cb.httpPost(url, parts[1], onDone) else cb.httpGet(url, onDone)
  263.     }
  264.  
  265.     // conditions
  266.  
  267.     private fun evalCond(cond: String, env: Env): Boolean {
  268.         // v1: left-to-right || then &&; no nested parens inside the condition.
  269.         for (orPart in splitTop(cond, "||")) {
  270.             if (splitTop(orPart, "&&").all { evalComparison(it.trim(), env) }) return true
  271.         }
  272.         return false
  273.     }
  274.  
  275.     private fun evalComparison(expr: String, env: Env): Boolean {
  276.         var e = expr.trim(); var negate = false
  277.         if (e.startsWith("!")) { negate = true; e = e.substring(1).trim() }
  278.         val ops = listOf("==", "!=", "<=", ">=", "<", ">", "isin", "iswm")
  279.         for (op in ops) {
  280.             val pat = if (op[0].isLetter()) " $op " else op
  281.             val i = e.indexOf(pat)
  282.             if (i >= 0) {
  283.                 val l = expand(e.substring(0, i).trim(), env)
  284.                 val r = expand(e.substring(i + pat.length).trim(), env)
  285.                 val res = compare(l, r, op)
  286.                 return res != negate
  287.             }
  288.         }
  289.         // bare value: truthy if non-empty and not "false"/"0"
  290.         val v = expand(e, env).trim()
  291.         val truthy = v.isNotEmpty() && v != "false" && v != "0"
  292.         return truthy != negate
  293.     }
  294.  
  295.     private fun compare(l: String, r: String, op: String): Boolean {
  296.         val ln = l.toDoubleOrNull(); val rn = r.toDoubleOrNull()
  297.         return when (op) {
  298.             "==" -> l == r
  299.             "!=" -> l != r
  300.             "isin" -> r.contains(l)
  301.             "iswm" -> glob(r, l)
  302.             "<" -> if (ln != null && rn != null) ln < rn else l < r
  303.             ">" -> if (ln != null && rn != null) ln > rn else l > r
  304.             "<=" -> if (ln != null && rn != null) ln <= rn else l <= r
  305.             ">=" -> if (ln != null && rn != null) ln >= rn else l >= r
  306.             else -> false
  307.         }
  308.     }
  309.  
  310.     // substitution
  311.  
  312.     /** Expand $... and %... in [s]. */
  313.     private fun expand(s: String, env: Env): String {
  314.         val out = StringBuilder(s.length + 16)
  315.         var i = 0
  316.         while (i < s.length) {
  317.             val ch = s[i]
  318.             when {
  319.                 ch == '$' && i + 1 < s.length && s[i + 1] == '$' -> { out.append('$'); i += 2 }
  320.                 ch == '%' && i + 1 < s.length && s[i + 1] == '%' -> { out.append('%'); i += 2 }
  321.                 ch == '%' -> { val (name, n) = readIdent(s, i + 1); out.append(varRef(name, env)?.asStr() ?: ""); i = n }
  322.                 ch == '$' -> { val (val0, n) = readDollar(s, i + 1, env); out.append(val0); i = n }
  323.                 else -> { out.append(ch); i++ }
  324.             }
  325.         }
  326.         return out.toString()
  327.     }
  328.  
  329.     private fun readIdent(s: String, start: Int): Pair<String, Int> {
  330.         var i = start
  331.         while (i < s.length && (s[i].isLetterOrDigit() || s[i] == '_' || s[i] == '.')) i++
  332.         return s.substring(start, i) to i
  333.     }
  334.  
  335.     private fun readDollar(s: String, start: Int, env: Env): Pair<String, Int> {
  336.         // positional: $0, $N, $N-, $N-M
  337.         if (start < s.length && s[start].isDigit()) {
  338.             var i = start
  339.             while (i < s.length && s[i].isDigit()) i++
  340.             val first = s.substring(start, i).toInt()
  341.             if (i < s.length && s[i] == '-') {
  342.                 i++
  343.                 if (i < s.length && s[i].isDigit()) {
  344.                     val js = i; while (i < s.length && s[i].isDigit()) i++
  345.                     val last = s.substring(js, i).toInt()
  346.                     return positional(env, first, last) to i
  347.                 }
  348.                 return positional(env, first, env.args.size) to i   // $N-
  349.             }
  350.             return positional(env, first, first) to i               // $N
  351.         }
  352.         // identifier, optionally with (args)
  353.         val (name, after) = readIdent(s, start)
  354.         if (after < s.length && s[after] == '(') {
  355.             val close = matchParen(s, after)
  356.             val inner = s.substring(after + 1, close)
  357.             val argExprs = if (inner.isBlank()) emptyList() else splitTop(inner, ",").map { it.trim() }
  358.             return callIdVal(name, argExprs, env).asStr() to close + 1
  359.         }
  360.         if (name.startsWith("age.") || name.startsWith("media.")) return cb.capability(name, emptyList()) to after
  361.         if (aliases.containsKey(name.lowercase())) return (callUser(name, emptyList(), env)?.asStr() ?: "") to after
  362.         if (name == "0") return env.args.size.toString() to after
  363.         return (env.fields[name] ?: "") to after
  364.     }
  365.  
  366.     private fun positional(env: Env, from: Int, to: Int): String {
  367.         if (from == 0) return env.args.size.toString()
  368.         if (from < 1 || from > env.args.size) return ""
  369.         val end = minOf(to, env.args.size)
  370.         return env.args.subList(from - 1, end).joinToString(" ")
  371.     }
  372.  
  373.     /** Closed built-in identifier set (spec §2). */
  374.     private fun builtin(name: String, a: List<String>, env: Env): String = when (name) {
  375.         "len" -> (a.getOrNull(0)?.length ?: 0).toString()
  376.         "lower" -> a.getOrNull(0)?.lowercase() ?: ""
  377.         "upper" -> a.getOrNull(0)?.uppercase() ?: ""
  378.         "left" -> a.getOrNull(0)?.take(a.getOrNull(1)?.toIntOrNull() ?: 0) ?: ""
  379.         "right" -> a.getOrNull(0)?.takeLast(a.getOrNull(1)?.toIntOrNull() ?: 0) ?: ""
  380.         "replace" -> (a.getOrNull(0) ?: "").replace(a.getOrNull(1) ?: "", a.getOrNull(2) ?: "")
  381.         "trim" -> (a.getOrNull(0) ?: "").trim()
  382.         "contains" -> (a.getOrNull(0) ?: "").contains(a.getOrNull(1) ?: "").toString()
  383.         "indexof" -> (a.getOrNull(0) ?: "").indexOf(a.getOrNull(1) ?: "").toString()
  384.         "substr" -> { val t = a.getOrNull(0) ?: ""; val st = (a.getOrNull(1)?.toIntOrNull() ?: 0).coerceIn(0, t.length); val ln = a.getOrNull(2)?.toIntOrNull() ?: (t.length - st); t.substring(st, (st + ln).coerceIn(st, t.length)) }
  385.         "repeat" -> (a.getOrNull(0) ?: "").repeat((a.getOrNull(1)?.toIntOrNull() ?: 0).coerceIn(0, 1000))
  386.         "abs" -> HexVal.num(kotlin.math.abs(a.getOrNull(0)?.toDoubleOrNull() ?: 0.0)).asStr()
  387.         "min" -> HexVal.num(a.mapNotNull { it.toDoubleOrNull() }.minOrNull() ?: 0.0).asStr()
  388.         "max" -> HexVal.num(a.mapNotNull { it.toDoubleOrNull() }.maxOrNull() ?: 0.0).asStr()
  389.         "setting" -> cb.setting(a.getOrNull(0) ?: "") ?: ""
  390.         "json" -> jsonFlat(a.getOrNull(0) ?: "", a.getOrNull(1) ?: "")
  391.         else -> ""   // unknown identifier => empty (closed set)
  392.     }
  393.  
  394.     /** Flat top-level JSON string extractor: $json(body, key). v1 = flat keys only. */
  395.     private fun jsonFlat(body: String, key: String): String {
  396.         val m = Regex("\"" + Regex.escape(key) + "\"\\s*:\\s*\"((?:[^\"\\\\]|\\\\.)*)\"").find(body)
  397.         return m?.groupValues?.get(1)?.replace("\\\"", "\"")?.replace("\\\\", "\\") ?: ""
  398.     }
  399.  
  400.     private val MAX_CALL_DEPTH = 64
  401.  
  402.     /** Invoke a user alias as a value function: bind args to $1.., run body, yield `return`. */
  403.     private fun callUser(name: String, argExprs: List<String>, env: Env): HexVal? {
  404.         val block = aliases[name.lowercase()] ?: return null
  405.         if (env.frame.depth >= MAX_CALL_DEPTH) throw HexAbort("call depth exceeded")
  406.         val callArgs = argExprs.map { evalVal(it, env).asStr() }
  407.         val child = env.childForCall(callArgs)
  408.         env.frame.depth++
  409.         try { runBody(block.body, child) } finally { env.frame.depth-- }
  410.         return child.returnValue ?: HexVal.EMPTY
  411.     }
  412.  
  413.     /** Build the root scope for an event dispatch (fresh Frame). */
  414.     private fun envForEvent(e: EventData, cb: EngineCallbacks): Env {
  415.         val f = HashMap<String, String>()
  416.         f["network"] = e.network; f["buffer"] = e.buffer; f["chan"] = e.buffer
  417.         f["target"] = e.buffer; f["text"] = e.text
  418.         e.from?.let { f["nick"] = it }
  419.         f["me"] = cb.nick().orEmpty()
  420.         f["isme"] = e.isMine.toString()
  421.         f.putAll(e.fields)
  422.         val args = if (e.args.isNotEmpty()) e.args
  423.         else if (e.text.isBlank()) emptyList() else e.text.split(' ')
  424.         return Env(Frame(), f, args, HashMap())
  425.     }
  426.  
  427.     /**
  428.      * Render a `view{}` body to its final DSL text. Expands `foreach %x %coll { .. }`
  429.      * by repetition (so a script can lay out a data-driven list) and applies
  430.      * $/% substitution per iteration.
  431.      */
  432.     private fun renderViewBody(s: String, env: Env): String {
  433.         val sb = StringBuilder()
  434.         var i = 0
  435.         while (i < s.length) {
  436.             val ctl = nextControl(s, i)
  437.             if (ctl == null) { sb.append(expand(s.substring(i), env)); break }
  438.             val (at, kw) = ctl
  439.             sb.append(expand(s.substring(i, at), env))
  440.             i = if (kw == "foreach") renderForeach(s, at, env, sb) else renderIf(s, at, env, sb)
  441.         }
  442.         return sb.toString()
  443.     }
  444.  
  445.     /** Earliest word-boundary `foreach %x ..` or `if (..)` at/after [from]. */
  446.     private fun nextControl(s: String, from: Int): Pair<Int, String>? {
  447.         var best = -1; var kw = ""
  448.         for (k in listOf("foreach", "if")) {
  449.             var p = from
  450.             while (true) {
  451.                 val idx = s.indexOf(k, p); if (idx < 0) break
  452.                 val before = if (idx == 0) ' ' else s[idx - 1]
  453.                 var a = idx + k.length
  454.                 while (a < s.length && s[a].isWhitespace()) a++
  455.                 val nextCh = if (a < s.length) s[a] else ' '
  456.                 val ok = before.isWhitespace() && (if (k == "if") nextCh == '(' else nextCh == '%')
  457.                 if (ok) { if (best < 0 || idx < best) { best = idx; kw = k }; break }
  458.                 p = idx + k.length
  459.             }
  460.         }
  461.         return if (best < 0) null else best to kw
  462.     }
  463.  
  464.     /** Inner of `{ .. }` from the next `{` at/after [from]; returns (inner, indexAfterClose). */
  465.     private fun readBraceBlock(s: String, from: Int): Pair<String, Int> {
  466.         var j = from
  467.         while (j < s.length && s[j] != '{') j++
  468.         if (j >= s.length) return "" to s.length
  469.         var depth = 0; val open = j
  470.         while (j < s.length) { if (s[j] == '{') depth++ else if (s[j] == '}') { depth--; if (depth == 0) { j++; break } }; j++ }
  471.         return s.substring(open + 1, j - 1) to j
  472.     }
  473.  
  474.     /** Inner of `(..)` from the next `(` at/after [from]; returns (inner, indexAfterClose). */
  475.     private fun readParenGroup(s: String, from: Int): Pair<String, Int> {
  476.         var j = from
  477.         while (j < s.length && s[j] != '(') j++
  478.         if (j >= s.length) return "" to s.length
  479.         var depth = 0; val open = j
  480.         while (j < s.length) { if (s[j] == '(') depth++ else if (s[j] == ')') { depth--; if (depth == 0) { j++; break } }; j++ }
  481.         return s.substring(open + 1, j - 1) to j
  482.     }
  483.  
  484.     private fun renderForeach(s: String, at: Int, env: Env, sb: StringBuilder): Int {
  485.         var j = at + "foreach".length
  486.         fun skip() { while (j < s.length && s[j].isWhitespace()) j++ }
  487.         fun word(): String { skip(); val st = j; while (j < s.length && !s[j].isWhitespace() && s[j] != '{') j++; return s.substring(st, j) }
  488.         val itemVar = varName(word())
  489.         val collTok = word()
  490.         val (inner, after) = readBraceBlock(s, j)
  491.         val coll = evalVal(collTok, env)
  492.         val items: List<HexVal> = when (coll) {
  493.             is HexVal.Lst -> coll.items.toList()
  494.             is HexVal.Mp -> coll.map.keys.map { HexVal.Str(it) }
  495.             is HexVal.Str -> if (coll.s.isBlank()) emptyList() else coll.s.split(" ").map { HexVal.Str(it) }
  496.         }
  497.         val saved = env.locals[itemVar]
  498.         for (it in items) { env.tick(); env.locals[itemVar] = it; sb.append(renderViewBody(inner, env)); sb.append('\n') }
  499.         if (saved != null) env.locals[itemVar] = saved else env.locals.remove(itemVar)
  500.         return after
  501.     }
  502.  
  503.     /** `if (cond) { ..} [elseif (cond) { .. }]* [else { .. }]` emit the first matching branch. */
  504.     private fun renderIf(s: String, at: Int, env: Env, sb: StringBuilder): Int {
  505.         val (cond, afterCond) = readParenGroup(s, at + "if".length)
  506.         val (body, afterBody) = readBraceBlock(s, afterCond)
  507.         var chosen: String? = if (evalCond(cond, env)) body else null
  508.         var j = afterBody
  509.         while (true) {
  510.             var k = j
  511.             while (k < s.length && s[k].isWhitespace()) k++
  512.             if (s.startsWith("elseif", k)) {
  513.                 val (c2, ac2) = readParenGroup(s, k + "elseif".length)
  514.                 val (b2, ab2) = readBraceBlock(s, ac2)
  515.                 if (chosen == null && evalCond(c2, env)) chosen = b2
  516.                 j = ab2
  517.             } else if (s.startsWith("else", k)) {
  518.                 val (b3, ab3) = readBraceBlock(s, k + "else".length)
  519.                 if (chosen == null) chosen = b3
  520.                 j = ab3
  521.             } else break
  522.         }
  523.         chosen?.let { sb.append(renderViewBody(it, env)); sb.append('\n') }
  524.         return j
  525.     }
  526.  
  527.     // v2: lists/maps/arithmetic)
  528.  
  529.     private fun varRef(name: String, env: Env): HexVal? = env.locals[name] ?: globals[name]
  530.  
  531.     /** Evaluate a value expression to a [HexVal]: a bare %var (container-preserving), a
  532.      *  whole $id(...) call, or otherwise a stringified scalar. */
  533.     private fun evalVal(expr: String, env: Env): HexVal {
  534.         val t = expr.trim()
  535.         if (t.length > 1 && t[0] == '%' && t.drop(1).all { it.isLetterOrDigit() || it == '_' })
  536.             return varRef(t.substring(1), env) ?: HexVal.EMPTY
  537.         if (t.startsWith("$")) {
  538.             val (name, after) = readIdent(t, 1)
  539.             if (after < t.length && t[after] == '(') {
  540.                 val close = runCatching { matchParen(t, after) }.getOrNull() ?: -1
  541.                 if (close == t.length - 1) {
  542.                     val inner2 = t.substring(after + 1, close)
  543.                     val argExprs = if (inner2.isBlank()) emptyList() else splitTop(inner2, ",").map { it.trim() }
  544.                     return callIdVal(name, argExprs, env)
  545.                 }
  546.             }
  547.         }
  548.         return HexVal.Str(expand(t, env))
  549.     }
  550.  
  551.     private fun doSetAt(raw: String, env: Env) {
  552.         val nm = raw.trim().substringBefore(' ')
  553.         val afterColl = raw.trim().substringAfter(' ', "").trim()
  554.         val keyTok = afterColl.substringBefore(' ')
  555.         val valExpr = afterColl.substringAfter(' ', "")
  556.         val coll = varRef(varName(nm), env) ?: return
  557.         HexValues.setAt(coll, evalVal(keyTok, env), evalVal(valExpr, env))
  558.     }
  559.  
  560.     private fun head(raw: String): Pair<String, String> {
  561.         val t = raw.trim(); val sp = t.indexOf(' ')
  562.         return if (sp < 0) t to "" else t.substring(0, sp) to t.substring(sp + 1)
  563.     }
  564.  
  565.     /** Value-returning built-ins. Container ops evaluate args as HexVal; scalar string
  566.      *  built-ins fall through to [builtin]. */
  567.     private fun callIdVal(name: String, argExprs: List<String>, env: Env): HexVal {
  568.         fun v(i: Int) = if (i < argExprs.size) evalVal(argExprs[i], env) else HexVal.EMPTY
  569.         fun str(i: Int, def: String = "") = if (i < argExprs.size) expand(argExprs[i], env) else def
  570.         if (name.startsWith("age.") || name.startsWith("media."))
  571.             return HexVal.Str(cb.capability(name, argExprs.map { expand(it, env) }))
  572.         if (aliases.containsKey(name.lowercase()))
  573.             return callUser(name, argExprs, env) ?: HexVal.EMPTY
  574.         return when (name) {
  575.             "list" -> HexValues.list(argExprs.map { evalVal(it, env) })
  576.             "map" -> {
  577.                 val a = argExprs.map { evalVal(it, env) }
  578.                 val pairs = ArrayList<Pair<String, HexVal>>()
  579.                 var i = 0; while (i + 1 < a.size + 1 && i + 1 <= a.size - 1) { pairs.add(a[i].asStr() to a[i + 1]); i += 2 }
  580.                 HexValues.map(pairs)
  581.             }
  582.             "get" -> HexValues.get(v(0), v(1))
  583.             "len" -> HexVal.num(HexValues.len(v(0)).toDouble())
  584.             "keys" -> HexValues.keys(v(0))
  585.             "has" -> HexVal.Str(HexValues.has(v(0), v(1)).toString())
  586.             "sort" -> HexValues.sort(v(0))
  587.             "join" -> HexVal.Str(HexValues.join(v(0), str(1, " ")))
  588.             "split" -> HexValues.split(str(0), str(1, " "))
  589.             "slice" -> HexValues.slice(v(0), str(1, "0").toIntOrNull() ?: 0, str(2, "0").toIntOrNull() ?: 0)
  590.             "calc" -> HexVal.num(HexValues.calc(str(0, "0")))
  591.             "range" -> { val lo = str(0, "0").toIntOrNull() ?: 0; val hiRaw = str(1, "0").toIntOrNull() ?: 0; val hi = if (hiRaw - lo > 10000) lo + 10000 else hiRaw; HexVal.Lst((lo..hi).map { HexVal.Str(it.toString()) }.toMutableList()) }
  592.             "values" -> (v(0) as? HexVal.Mp)?.let { HexVal.Lst(it.map.values.toMutableList()) } ?: HexVal.Lst()
  593.             else -> HexVal.Str(builtin(name, argExprs.map { expand(it, env) }, env))
  594.         }
  595.     }
  596.  
  597.     private fun varName(token: String): String = token.removePrefix("%").trim()
  598.  
  599.     private fun splitTarget(s: String): Pair<String?, String> {
  600.         val t = s.trim()
  601.         val sp = t.indexOf(' ')
  602.         if (sp < 0) return null to t
  603.         return t.substring(0, sp) to t.substring(sp + 1)
  604.     }
  605.  
  606.     private fun matchParen(s: String, open: Int): Int {
  607.         var depth = 0
  608.         var i = open
  609.         while (i < s.length) { if (s[i] == '(') depth++ else if (s[i] == ')') { depth--; if (depth == 0) return i }; i++ }
  610.         throw HexError("unbalanced ( )")
  611.     }
  612.  
  613.     /** Split [s] on top-level [sep] (not inside ()/{}). */
  614.     private fun splitTop(s: String, sep: String): List<String> {
  615.         val parts = ArrayList<String>(); var depth = 0; var i = 0; var last = 0
  616.         while (i < s.length) {
  617.             val c = s[i]
  618.             if (c == '(' || c == '{') depth++
  619.             else if (c == ')' || c == '}') depth--
  620.             else if (depth == 0 && s.startsWith(sep, i)) { parts.add(s.substring(last, i)); i += sep.length; last = i; continue }
  621.             i++
  622.         }
  623.         parts.add(s.substring(last))
  624.         return parts
  625.     }
  626.  
  627.     private fun glob(pattern: String, text: String): Boolean {
  628.         val rx = StringBuilder("(?i)^")
  629.         for (c in pattern) when (c) {
  630.             '*' -> rx.append(".*"); '?' -> rx.append('.')
  631.             else -> rx.append(Regex.escape(c.toString()))
  632.         }
  633.         rx.append('$')
  634.         return Regex(rx.toString()).matches(text)
  635.     }
  636. }
  637.  
  638. /** Abort for the step/time budget (uncatchable by scripts since they have no try/catch). */
  639. private class HexAbort(message: String) : RuntimeException(message)

Raw Paste

Comments 0
Login to post a comment.
  • No comments yet. Be the first.
Login to post a comment. Login or Register
We use cookies. To comply with GDPR in the EU and the UK we have to show you these.

We use cookies and similar technologies to keep this website functional (including spam protection via Google reCAPTCHA or Cloudflare Turnstile), and — with your consent — to measure usage and show ads. See Privacy.