Files
Brummel 525112e32e experiment(cma): bracket-free formats tested — they make Qwen WORSE, not better (refs #68)
Tested the user's bracket-free ideas (indent, YAML) against the annotated-paren
result. Hypothesis was that removing the closing token removes the repetition
tail. The opposite held.

Instrument note (the watch-out paid off twice): a real YAML library auto-quotes
and gives *,-,true special meaning, which collides with the AILang operators —
Qwen wrote `- *` (multiply), a YAML alias marker, and the library crashed. That
is a measurement bug, not a model failure. Rebuilt with a hand-written literal
indent parser (atoms verbatim, no quoting); `- *` then round-trips fine.

With the instrument fixed, the result is monotone across four formats:
  format 4 annotated parens 7/8 > plain Form-A parens 6/8 > yamlish indent 3/8
  > YAML library 2/8.
More explicit structure marking helps this model; less hurts. Qwen's weakness
is structure-tracking at depth, and indentation makes it track the nesting
itself — as error-prone as matching parens but without the redundant cue. The
bracket-free surfaces fail EARLIER (L3/L4), before the L7 repetition ceiling is
even reached.

Lever for an LLM-authored language: redundant explicit structure (depth-
annotated brackets), not a lighter surface. Synthesis in format-findings.md;
raw ladders qwen-yaml.md (PyYAML, discarded) and qwen-yamlish.md (own parser).
2026-06-02 18:49:35 +02:00

179 lines
8.7 KiB
Python

#!/usr/bin/env python3
"""Format probe — YAML surface. Does encoding the tree as YAML (no parens,
native nesting, massively in LLM training) help Qwen, esp. at depth where
format 4 hit a closing-token repetition tail?
Node `(head a b ...)` <-> YAML mapping `head: [a, b, ...]`; each arg is a
scalar atom or another mapping. Converter uses the STANDARD PyYAML parser
(round-trip-verified incl. the -,*,/,true,false special-char atoms) before
judging any model output. Same ablation ladder as the other probes.
Reads $IONOS_API_TOKEN (never printed). Live IONOS — run with consent.
"""
from __future__ import annotations
import json, os, re, subprocess, urllib.request, yaml
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-yaml.md"
# ---- S-expr <-> tree <-> YAML (verified converter) ----
def tokenize(s):
toks=[]; i=0
while i<len(s):
c=s[i]
if c.isspace(): i+=1; continue
if c in '()': toks.append(c); i+=1; continue
if c=='"':
j=i+1
while j<len(s):
if s[j]=='\\': j+=2; continue
if s[j]=='"': break
j+=1
toks.append(s[i:j+1]); i=j+1; continue
j=i
while j<len(s) and not s[j].isspace() and s[j] not in '()"': j+=1
toks.append(s[i:j]); i=j
return toks
def parse(toks):
t=toks.pop(0)
if t=='(':
node=[]
while toks and toks[0]!=')': node.append(parse(toks))
if not toks: raise ValueError("unbalanced (")
toks.pop(0); return node
return t
def to_tree(src):
toks=tokenize(src); t=parse(toks)
return t
def emit(n): return '('+' '.join(emit(x) for x in n)+')' if isinstance(n,list) else n
def to_yamlobj(n):
if isinstance(n,list):
if not n or not isinstance(n[0],str): raise ValueError("node head must be an atom")
return {n[0]:[to_yamlobj(a) for a in n[1:]]}
return n
def from_yamlobj(o):
if isinstance(o,dict):
if len(o)!=1: raise ValueError(f"each node must be a single-key mapping, got keys {list(o)}")
(k,v),=o.items()
if v is None: v=[]
if not isinstance(v,list): raise ValueError(f"value of `{k}` must be a list, got {type(v).__name__}")
return [str(k)]+[from_yamlobj(x) for x in v]
return str(o)
def to_yaml(src): return yaml.dump(to_yamlobj(to_tree(src)),default_flow_style=False,sort_keys=False)
def from_yaml(y):
o=yaml.safe_load(y)
return emit(from_yamlobj(o))
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"))))))))"""
INTRO=("This programming language is written as YAML. Every node is a single-key "
"mapping `head:` whose value is a LIST of its arguments; each argument is "
"either a scalar atom (a number, name, or operator like `+`) or another "
"single-key mapping. An empty argument list is `[]`. Study these complete, "
"valid examples, then write the requested program in the SAME YAML "
"structure. Return ONLY the YAML, no prose, no fences.\n\n")
BASE=INTRO+"Example 1:\n"+to_yaml(A)+"\nExample 2:\n"+to_yaml(B)+"\n"
SERIES=(BASE+"\nExample 3 (Series ring-buffer library: `new` with head `new`, "
"args `Series`, a `con` Float type, and capacity N; `Series.push` takes "
"an owned series and an element, returns the new series; `Series.at` "
"takes a series and an Int index (0=newest); `Series.len` / "
"`Series.total_count` take a series, return Int):\n"+to_yaml(S)+"\n")
SMA_OUT="3.0\n5.33333\n5.66667\n5.33333\n"
SMA_TASK=("Write a program `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:\n"+SMA_OUT)
STAGES=[
("L0_const",BASE,"Write a program `m` with a function `answer` taking no parameters that returns the Int 42.",None),
("L1_arith",BASE,"Write a program `m` with a function `triple` taking one Int parameter, returning it multiplied by 3.",None),
("L2_print",BASE,"Write a program `m` whose `main` prints the Int 7 on its own line.","7\n"),
("L3_adt",BASE,"Write a program `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",BASE,"Write a program `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",SERIES,"Write a program `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",SERIES,"Write a program `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",SERIES,SMA_TASK,SMA_OUT),
]
def call(system,task):
body=json.dumps({"model":MODEL,"temperature":0.2,"max_tokens":2000,
"messages":[{"role":"system","content":system},{"role":"user","content":task}]}).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"]["content"], d.get("usage",{})
def extract_yaml(resp):
m=re.search(r"```(?:ya?ml)?\n(.*?)```",resp,re.S)
return (m.group(1) if m else resp).strip()
def evaluate(yaml_src,expected):
try:
plain=from_yaml(yaml_src)
except Exception as e:
return "yaml-convert",f"converter rejected model YAML: {e}",""
m=re.search(r"\(module\s+([A-Za-z_][A-Za-z0-9_]*)",plain)
name=m.group(1) if m else "m"
f=Path(f"/tmp/abl/{name}.ail"); f.parent.mkdir(exist_ok=True); f.write_text(plain+"\n")
chk=subprocess.run([AIL,"check",str(f)],capture_output=True,text=True)
if chk.returncode!=0: return "check",(chk.stderr or chk.stdout).strip(),plain
if expected is None: return "green","(check-only)",plain
run=subprocess.run([AIL,"run",str(f)],capture_output=True,text=True)
if run.returncode!=0: return "run",(run.stderr or run.stdout).strip(),plain
if run.stdout!=expected: return "output",f"got {run.stdout!r}, expected {expected!r}",plain
return "green",run.stdout,plain
def main():
log=["# Qwen format probe — YAML surface\n",
f"**Model:** {MODEL}; single-shot; YAML dialect; converted via verified PyYAML round-trip.\n","---\n"]
summary=[]
for sid,ctx,task,exp in STAGES:
resp,usage=call(ctx,task)
y=extract_yaml(resp)
stage,fb,plain=evaluate(y,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",
"model YAML:\n```yaml\n"+y[:1500]+"\n```\n",
("converted Form-A:\n```\n"+plain[:1200]+"\n```\n" if plain else ""),
("" 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()