#!/usr/bin/env python3
"""Format probe — tool-calling / structured AST. Instead of writing surface
syntax (where bracket-tracking is the wall), Qwen submits the program as the
canonical AILang AST JSON via a `submit_program` tool. Hypothesis: the model
produces structured JSON through the tool channel (which it is trained hard on,
and which the API validates) more reliably than free-text syntax. No converter
risk: the tool argument IS the .ail.json, written and `ail check`ed directly.

Same ablation ladder. Reads $IONOS_API_TOKEN (never printed). Live IONOS.
"""
from __future__ import annotations
import json, os, re, subprocess, urllib.request
from pathlib import Path

ENDPOINT="https://openai.inference.de-txl.ionos.com/v1/chat/completions"
MODEL="Qwen/Qwen3-Coder-Next"
REPO=Path(__file__).resolve().parents[2]
AIL=os.environ.get("AIL_BIN", str(REPO/"target/debug/ail"))
DOC=REPO/"experiments/2026-05-12-cross-model-authoring/qwen-tool-8k.md"

def ail_ast(src):
    p=Path("/tmp/abl/_fs.ail"); p.write_text(src+"\n")
    return subprocess.run([AIL,"parse",str(p)],capture_output=True,text=True).stdout.strip()

A="""(module demo_a
  (fn double
    (type (fn-type (params (own (con Int))) (ret (own (con Int)))))
    (params x) (body (app + x x)))
  (fn main
    (type (fn-type (params) (ret (own (con Unit))) (effects IO)))
    (params) (body (seq (app print (app double 21)) (do io/print_str "\\n")))))"""
B="""(module demo_b
  (data IntList (ctor Nil) (ctor Cons (con Int) (con IntList)))
  (fn sum
    (type (fn-type (params (own (con IntList))) (ret (own (con Int)))))
    (params xs) (body (match xs (case (pat-ctor Nil) 0) (case (pat-ctor Cons h t) (app + h (app sum t))))))
  (fn main
    (type (fn-type (params) (ret (own (con Unit))) (effects IO)))
    (params) (body (let xs (term-ctor IntList Cons 1 (term-ctor IntList Cons 2 (term-ctor IntList Nil)))
      (seq (app print (app sum xs)) (do io/print_str "\\n"))))))"""
S="""(module demo_s
  (fn main
    (type (fn-type (params) (ret (own (con Unit))) (effects IO)))
    (params) (body (let s (new Series (con Float) 3)
      (let s2 (app Series.push s 1.0)
        (let s3 (app Series.push s2 2.0)
          (seq (app print (app Series.len s3)) (do io/print_str "\\n"))))))))"""

TOOL={"type":"function","function":{
  "name":"submit_program",
  "description":"Submit a complete AILang program as its canonical AST JSON.",
  "parameters":{"type":"object","properties":{
    "module":{"type":"object","description":"The full module AST: an object with keys schema, name, imports, defs."}},
    "required":["module"]}}}

def sys_base(extra=""):
    return ("You author AILang programs as a canonical Abstract Syntax Tree in JSON, "
      "submitted with the submit_program tool (the `module` argument is the full AST: "
      "an object with keys schema, name, imports, defs). The AST node forms are shown "
      "by these complete, valid example programs.\n\n"
      "Example 1 AST:\n"+ail_ast(A)+"\n\nExample 2 AST:\n"+ail_ast(B)+"\n"+extra+
      "\nAlways call submit_program with the full module AST.")
SYS=sys_base()
SYS_SERIES=sys_base("\nExample 3 AST (uses the Series library — `new` term with type-arg "
      "Float and capacity; Series.push/Series.at/Series.len/Series.total_count called via "
      "app on a `var` named e.g. \"Series.push\"):\n"+ail_ast(S)+"\n")

SMA_OUT="3.0\n5.33333\n5.66667\n5.33333\n"
SMA_TASK=("Write a program named m computing a simple moving average, window 3, over the "
          "float stream 1.0, 5.0, 3.0, 8.0, 6.0, 2.0 using the Series library: create a Float "
          "Series lookback 3, push each value in turn, and after each push once at least 3 "
          "values have been pushed, print the average of the most recent 3 (their sum divided "
          "by 3.0) on its own line. Expected stdout: "+repr(SMA_OUT))

STAGES=[
 ("L0_const",SYS,"Write a program named m with a function answer taking no parameters that returns the Int 42.",None),
 ("L1_arith",SYS,"Write a program named m with a function triple taking one Int parameter, returning it multiplied by 3.",None),
 ("L2_print",SYS,"Write a program named m whose main prints the Int 7 on its own line.","7\n"),
 ("L3_adt",SYS,"Write a program named m with an ADT Light (ctors Red, Green) and a function code returning Int 0 for Red and 1 for Green via match.",None),
 ("L4_rec",SYS,"Write a program named m with an IntList ADT (Nil/Cons), a function length counting elements by recursion, and a main that prints the length of the list [1,2,3] on its own line.","3\n"),
 ("L5_series_len",SYS_SERIES,"Write a program named m using the Series library: create a Float Series lookback 3, push 1.0 then 2.0 then 3.0, then print its Series.len on its own line.","3\n"),
 ("L6_series_at",SYS_SERIES,"Write a program named m using the Series library: create a Float Series lookback 3, push 1.0 then 2.0 then 3.0, then print (Series.at on index 0, the newest) on its own line.","3.0\n"),
 ("L7_sma",SYS_SERIES,SMA_TASK,SMA_OUT),
]

def call(system,task):
    body=json.dumps({"model":MODEL,"temperature":0.2,"max_tokens":8000,
        "messages":[{"role":"system","content":system},{"role":"user","content":task}],
        "tools":[TOOL],"tool_choice":{"type":"function","function":{"name":"submit_program"}}}).encode()
    req=urllib.request.Request(ENDPOINT,data=body,headers={
        "Authorization":f"Bearer {os.environ['IONOS_API_TOKEN']}","Content-Type":"application/json"})
    with urllib.request.urlopen(req,timeout=120) as r: d=json.loads(r.read())
    return d["choices"][0]["message"], d.get("usage",{})

def evaluate(msg,expected):
    tc=msg.get("tool_calls")
    if not tc: return "no-tool-call",f"model returned content instead: {str(msg.get('content'))[:200]}",""
    try: args=json.loads(tc[0]["function"]["arguments"])
    except Exception as e: return "bad-json",f"tool arguments not valid JSON: {e}",tc[0]["function"]["arguments"][:400]
    module=args.get("module",args)
    if not isinstance(module,dict): return "bad-shape",f"module not an object: {type(module).__name__}",json.dumps(args)[:400]
    module.setdefault("schema","ailang/v0"); module.setdefault("imports",[])
    name=module.get("name","m")
    f=Path(f"/tmp/abl/{name}.ail.json"); f.parent.mkdir(exist_ok=True)
    f.write_text(json.dumps(module,indent=2)+"\n")
    pretty=json.dumps(module,indent=1)
    chk=subprocess.run([AIL,"check",str(f)],capture_output=True,text=True)
    if chk.returncode!=0: return "check",(chk.stderr or chk.stdout).strip(),pretty
    if expected is None: return "green","(check-only)",pretty
    run=subprocess.run([AIL,"run",str(f)],capture_output=True,text=True)
    if run.returncode!=0: return "run",(run.stderr or run.stdout).strip(),pretty
    if run.stdout!=expected: return "output",f"got {run.stdout!r}, expected {expected!r}",pretty
    return "green",run.stdout,pretty

def main():
    log=["# Qwen format probe — tool-calling / structured AST\n",
         f"**Model:** {MODEL}; submit_program tool, forced; AST written directly to .ail.json.\n","---\n"]
    summary=[]
    for sid,sysmsg,task,exp in STAGES:
        msg,usage=call(sysmsg,task)
        stage,fb,pretty=evaluate(msg,exp)
        ok=stage=="green"; mark="✅ green" if ok else f"❌ {stage}"
        summary.append((sid,mark)); print(f"{sid}: {mark}")
        log+=[f"## {sid} — {mark}",
              f"\ntokens: prompt={usage.get('prompt_tokens','?')} completion={usage.get('completion_tokens','?')}\n",
              "submitted AST:\n```json\n"+pretty[:1400]+"\n```\n",
              ("" if ok else f"**{stage}:**\n```\n{fb[:600]}\n```\n")]
    log+=["\n---\n## Ladder\n"]+[f"- {s}: {m}" for s,m in summary]
    DOC.write_text("\n".join(log)); print("doc →",DOC)

if __name__=="__main__":
    main()
