"""tools/kenchat.py — generate-and-run pipeline for Kenga Prophet. Second metric: program validity rate (compile + run + correct value). Third metric (M3.2): identifier recovery (declared == expected params). Supports: --model v01 (linear K=8) --model k16 (linear K=16) --model m3 (transformer K=32 D=32 L=1, 28-token) --model m31 (transformer K=64 D=48 L=2, 28-token) --model m32 (transformer K=64 D=48 L=2, 64-token learned codec) For transformer generation we now left-align the real prompt tokens and right-pad with a mask (1=real, 0=pad). Padding positions are zeroed in the embeddings and masked out of attention, so short prompts stay in-distribution (no more ID-token contamination). Positional embeddings for real tokens start at 0, matching training windows. """ import argparse, os, sys, subprocess, tempfile, re, pickle import numpy as np VOCAB_TOKENS = [ 'fn', 'return', 'let', 'if', 'else', 'while', 'for', 'i64', ':', ',', ';', '{', '}', '(', ')', '->', '+', '-', '*', '/', '=', '==', '<', '<=', '>', 'println', 'ID', 'NUM', ] V = len(VOCAB_TOKENS) K_LITE = 'bootstrap/bin/kenga-lite.exe' def read_header(path): out = {} with open(path) as f: first = f.readline() for tok in first.split(): if '=' in tok: k, v = tok.split('=', 1) try: out[k] = int(v) except ValueError: out[k] = v return out def load_codec_vocab(path='minds/kenga_bpe.pkl'): """Load a codec token list from a pkl (bpe or digits variant).""" with open(path, 'rb') as f: data = pickle.load(f) tokens = data['tokens'] merges = data['merges'] token_to_id = {t: i for i, t in enumerate(tokens)} merge_set = set(a + b for a, b in merges) KEYWORDS = {'fn','return','let','if','else','while','for','i64','println'} def encode_word(w): if w in KEYWORDS: return [token_to_id[w]] spellable = all(ch in token_to_id for ch in w) if not spellable: return [token_to_id['ID']] toks = list(w) changed = True while changed: changed = False i = 0 while i < len(toks) - 1: merged = toks[i] + toks[i+1] if merged in merge_set: toks[i:i+2] = [merged] changed = True i += 1 return [token_to_id[t] for t in toks] return { 'tokens': tokens, 'token_to_id': token_to_id, 'encode_word': encode_word, 'merges': merges, } def tokenize(src, codec=None): KEYWORDS = {'fn','return','let','if','else','while','for','i64','println'} TWO_CHAR = {'->','==','<=','>=','!=','&&','||','<<','>>','&','|','^','~'} if codec: VOCAB_MAP = codec['token_to_id'] else: VOCAB_MAP = {t: i for i, t in enumerate(VOCAB_TOKENS)} out = [] i = 0 n = len(src) while i < n: c = src[i] if c in ' \t\n\r': i += 1; continue if c == '/' and i+1 < n and src[i+1] == '/': while i < n and src[i] != '\n': i += 1 continue two = src[i:i+2] if two in TWO_CHAR: out.append(VOCAB_MAP.get(two, VOCAB_MAP['ID'])); i += 2; continue if c in (':', ',', ';', '{', '}', '(', ')', '+', '-', '*', '/', '=', '<', '>'): if c == '-' and i+1 < n and src[i+1] == '>': out.append(VOCAB_MAP['->']); i += 2; continue out.append(VOCAB_MAP[c]); i += 1; continue if c.isdigit(): j = i while j < n and src[j].isdigit(): j += 1 if 'NUM' in VOCAB_MAP: out.append(VOCAB_MAP['NUM']); i = j; continue for d in src[i:j]: out.append(VOCAB_MAP.get(d, VOCAB_MAP['ID'])) i = j; continue if c.isalpha() or c == '_': j = i while j < n and (src[j].isalnum() or src[j] == '_'): j += 1 word = src[i:j] if codec: out.extend(codec['encode_word'](word)) else: out.append(VOCAB_MAP[word] if word in KEYWORDS else VOCAB_MAP['ID']) i = j; continue i += 1 return out def detokenize(toks, codec=None): """Rebuild source text from token ids. Codec mode: letter/merge tokens that are consecutive form a single identifier word (syntax tokens break words). This reconstructs the exact identifier the model emitted, e.g. [a,d,d] -> 'add'. """ out = [] if codec: tokens = codec['tokens'] token_to_id = codec['token_to_id'] SYNTAX_SET = set(token_to_id[t] for t in token_to_id if t in VOCAB_TOKENS) buf = [] for t in toks: if t >= len(tokens): out.append('mkid'); continue tok = tokens[t] if tok == 'ID': if buf: out.append(''.join(buf)); buf = [] out.append('mkid') elif tok == 'NUM': if buf: out.append(''.join(buf)); buf = [] out.append('0') elif t in SYNTAX_SET: if buf: out.append(''.join(buf)); buf = [] out.append(tok) else: buf.append(tok) if buf: out.append(''.join(buf)) return ' '.join(out) for t in toks: if t < len(VOCAB_TOKENS): tok = VOCAB_TOKENS[t] if tok == 'ID': out.append('mkid') elif tok == 'NUM': out.append('0') else: out.append(tok) else: out.append('mkid') return ' '.join(out) def load_weights(path): weights = {} with open(path) as f: for line in f: line = line.strip() if not line.startswith('[v='): continue rb = line.find(']') if rb < 0: continue name = line[1:rb].strip() body = line[rb+1:].strip() nums = [] for x in body.split(','): x = x.strip() if not x: continue try: nums.append(int(round(float(x) / 1000.0))) except ValueError: pass weights[name] = np.array(nums, dtype=np.int32) return weights def load_tensors(path): info = read_header(path) scale = info.get('scale', 1000) tensors = {} with open(path) as f: for line in f: line = line.strip() if not line.startswith('['): continue rb = line.find(']') if rb < 0: continue name = line[1:rb].strip() body = line[rb+1:].strip() shape = [] si = body.find('shape=[') if si >= 0: si += len('shape=[') ei = body.find(']', si) shape = [int(x) for x in body[si:ei].split(',') if x.strip()] body = body[ei+1:] nums = [float(x) for x in body.split(',') if x.strip()] arr = np.array(nums, dtype=np.float32) / scale if shape: arr = arr.reshape(shape) tensors[name] = arr return info, tensors def m3_forward(tensors, info, x, pad_mask=None, read_pos=None): """Forward a batch (B, K) token ids -> logits (B, V). pad_mask: (B, K) float, 1=real token, 0=padding. Padding positions are zeroed in the embedding and masked out of attention. read_pos: int or None. If None, reads logits at position K-1. Otherwise reads logits at the given absolute position. """ K, D = info['k'], info['d'] H = info['h'] HEAD = info.get('head', D // H) V = info['vocab'] L = info.get('layers', 1) B = x.shape[0] E_tok = tensors['E_tok'] E_pos = tensors['E_pos'] X = E_tok[x] + E_pos[np.arange(K)] if pad_mask is not None: X = X * pad_mask[:, :, None] cur = X mask = np.triu(np.ones((K, K), dtype=bool), k=1) key_pad_mask = None if pad_mask is not None: key_pad_mask = (pad_mask == 0)[:, None, None, :] # (B,1,1,K) for li in range(L): if L == 1: Wq, Wk, Wv, Wo = tensors['Wq'], tensors['Wk'], tensors['Wv'], tensors['Wo'] W1, b1, W2, b2 = tensors['W1'], tensors['b1'], tensors['W2'], tensors['b2'] else: Wq = tensors[f'{li}:Wq']; Wk = tensors[f'{li}:Wk'] Wv = tensors[f'{li}:Wv']; Wo = tensors[f'{li}:Wo'] W1 = tensors[f'{li}:W1']; b1 = tensors[f'{li}:b1'] W2 = tensors[f'{li}:W2']; b2 = tensors[f'{li}:b2'] Q = cur @ Wq K_ = cur @ Wk V_ = cur @ Wv q = Q.reshape(B, K, H, HEAD).transpose(0, 2, 1, 3) k = K_.reshape(B, K, H, HEAD).transpose(0, 2, 1, 3) v = V_.reshape(B, K, H, HEAD).transpose(0, 2, 1, 3) scores = q @ k.transpose(0, 1, 3, 2) / np.sqrt(HEAD) scores = scores + np.where(mask, -1e9, 0.0) if key_pad_mask is not None: scores = scores + np.where(key_pad_mask, -1e9, 0.0) attn = np.exp(scores - scores.max(axis=-1, keepdims=True)) attn = attn / attn.sum(axis=-1, keepdims=True) ctx = attn @ v ctx = ctx.transpose(0, 2, 1, 3).reshape(B, K, D) attn_out = cur + ctx @ Wo h1 = np.tanh(attn_out @ W1 + b1) h2 = h1 @ W2 + b2 cur = attn_out + h2 pos = K - 1 if read_pos is None else read_pos last_y = cur[:, pos, :] logits = last_y @ tensors['Wout'] + tensors['bout'] return logits def predict_m3(tensors, info, window, temperature=None, rng=None, pad_mask=None, read_pos=None): K = info['k'] x = np.array(window[-K:], dtype=np.int64).reshape(1, K) logits = m3_forward(tensors, info, x, pad_mask, read_pos)[0] if temperature: logits = logits / temperature logits = logits - logits.max() exp = np.exp(logits) probs = exp / exp.sum() if rng is None: rng = np.random.default_rng() return int(rng.choice(len(probs), p=probs)) return int(np.argmax(logits)) def gen_tokens(prompt, weights_path, max_tokens=80, temperature=None, codec=None, seed=None): """Generate token ids + source for a prompt. Transformer path uses a left-aligned real-token window with right-padding and a mask, so short prompts stay in-distribution (padding is zeroed and masked out of attention). The mask/read_pos are tracked across steps. seed: RNG seed for temperature sampling (None -> deterministic greedy). """ info = read_header(weights_path) K = info.get('k', 8) is_transformer = info.get('arch') == 'transformer' if is_transformer: _, tensors = load_tensors(weights_path) rng = np.random.default_rng(seed if seed is not None else 1) else: W_dict = load_weights(weights_path) arr_dict = {int(k_.split('=', 1)[1]): v_ for k_, v_ in W_dict.items() if k_.startswith('v=')} if not arr_dict: return [], '' V_ = max(arr_dict.keys()) + 1 row0 = arr_dict[0] W_arr = np.zeros((V_, row0.shape[0]), dtype=np.int32) for v, arr in arr_dict.items(): W_arr[v, :arr.shape[0]] = arr[:row0.shape[0]] toks = tokenize(prompt, codec) ID_IDX = codec['token_to_id']['ID'] if codec else VOCAB_TOKENS.index('ID') T_OPEN, T_CLOSE, T_LPAR, T_RPAR = ( VOCAB_TOKENS.index('{'), VOCAB_TOKENS.index('}'), VOCAB_TOKENS.index('('), VOCAB_TOKENS.index(')'), ) n_tokens = info['vocab'] if is_transformer else V_ def scores_fn(buf, temp): """Return logits over vocab for the next token after buf.""" if is_transformer: if len(buf) >= K: window = buf[-K:] pad_mask = np.ones((1, K), dtype=np.float32) read_pos = K - 1 else: window = buf + [ID_IDX] * (K - len(buf)) pad_mask = np.zeros((1, K), dtype=np.float32) pad_mask[0, :len(buf)] = 1.0 read_pos = len(buf) - 1 x = np.array(window, dtype=np.int64).reshape(1, K) return m3_forward(tensors, info, x, pad_mask, read_pos)[0] # linear path: right-align last K real tokens (legacy behaviour) window = buf[-K:] KV = V_ * K feat = np.zeros(KV, dtype=np.float32) for j, t in enumerate(window): if 0 <= t < V_: feat[j * V_ + t] = 1.0 logits = (feat @ W_arr[:, :KV].T + W_arr[:, KV]).astype(np.float64) return logits def pick(logits, temp, allowed): if temp: lp = logits / temp lp = lp - lp.max() exp = np.exp(lp) probs = exp / exp.sum() mask = np.full(probs.shape, -np.inf) mask[allowed] = np.log(probs[allowed] + 1e-9) mask = mask - mask.max() e = np.exp(mask) p = e / e.sum() return int(rng.choice(len(p), p=p)) best = -1 for a in allowed: if best < 0 or logits[a] > logits[best]: best = a return best generated = [] brace = 0 paren = 0 saw_open = False for t in toks: if t == T_OPEN: brace += 1; saw_open = True elif t == T_CLOSE: brace -= 1 elif t == T_LPAR: paren += 1 elif t == T_RPAR: paren -= 1 n_stmts = 0 buf = list(toks) for step in range(max_tokens): logits = scores_fn(buf, temperature) allowed = list(range(n_tokens)) if brace <= 0: allowed.remove(T_CLOSE) if paren <= 0: allowed.remove(T_RPAR) nxt = pick(logits, temperature, allowed) generated.append(nxt) buf.append(nxt) if nxt == T_OPEN: brace += 1; saw_open = True elif nxt == T_CLOSE: brace -= 1 elif nxt == T_LPAR: paren += 1 elif nxt == T_RPAR: paren -= 1 if len(generated) > 4 and brace == 0 and paren == 0: db = detokenize(buf, codec) if 'fn main' in db: idx = db.index('fn main') if db.find('}', idx) != -1: break return generated, detokenize(generated, codec) def run_via_kenga_lite(source, workdir='minds/_kenchat', timeout=60): os.makedirs(workdir, exist_ok=True) f = tempfile.NamedTemporaryFile(mode='w', suffix='.kenga', delete=False, dir=workdir, encoding='utf-8', newline='') f.write(source) f.close() try: result = subprocess.run( [K_LITE, 'run', f.name], capture_output=True, text=True, timeout=timeout, ) return result.returncode, result.stdout, result.stderr except subprocess.TimeoutExpired: return -1, '', 'timeout' except Exception as e: return -2, '', str(e) finally: os.unlink(f.name) def gen_verified(prompt, weights_path, codec=None, n_samples=16, max_tokens=200, temperature=1.0, want=None): """Self-consistency generation: sample n candidates, return the first that compiles, runs (rc==0) and, if want is given, prints the expected value. Falls back to the first runnable candidate (or last candidate) otherwise. Returns (tokens, src, full_program, rc, out, err). """ first_runnable = None last = None for i in range(n_samples): toks, src = gen_tokens(prompt, weights_path, max_tokens=max_tokens, temperature=temperature, codec=codec, seed=i) full = make_valid_program(prompt, src) rc, out, err = run_via_kenga_lite(full) last = (toks, src, full, rc, out, err) if rc == 0: if first_runnable is None: first_runnable = last if want is None: return last first = out.strip().split('\n')[0] if out else '' if first == want: return last return first_runnable if first_runnable is not None else last PROBES = [ ("fn add", "5", {'a', 'b'}), ("fn sub", "7", {'a', 'b'}), ("fn mul", "42", {'a', 'b'}), ("fn fact", "120", {'n'}), ("fn fib", "21", {'n'}), ("fn max", "7", {'a', 'b'}), ("fn sqr", "81", {'x'}), ("fn pow", "1024", {'a', 'b'}), ("fn sumto", "55", {'n'}), ] def make_valid_program(prompt, generated_source): if (prompt and prompt[-1] not in ' \t\n' and generated_source and generated_source[0] not in ' \t\n('): src = prompt + ' ' + generated_source else: src = prompt + generated_source if 'fn main' not in src: src = src + '\nfn main() -> i64 { println(0); return 0; }\n' else: src = src + '\n' return src def declared_params(src): """Extract declared param identifiers of the FIRST fn in src (set).""" m = re.search(r'fn\s+\w+\s*\(([^)]*)\)', src) if not m: return None params = set() for chunk in m.group(1).split(','): chunk = chunk.strip() mm = re.match(r'([a-zA-Z_]\w*)\s*:', chunk) if mm: params.add(mm.group(1)) return params def main(): ap = argparse.ArgumentParser() ap.add_argument('prompt', nargs='?', help='generate from prompt') ap.add_argument('--probe', action='store_true', help='run all 9 probes') ap.add_argument('--model', default='v01', help='v01 (K=8), k16 (K=16), m3 (K=32), m31 (K=64 L=2), ' 'm32 (K=64 L=2, 64-token codec), ' 'm33 (K=64 L=2, 64-token codec, seeds-in-train), ' 'm34 (K=64 L=2, 64-token codec, seeds-in-train x20), ' 'm35 (K=64 L=2, 64-token codec, seeds-dominate x300)') ap.add_argument('--max-tokens', type=int, default=80) ap.add_argument('--temperature', type=float, default=None, help='sample with temperature (default: greedy argmax)') ap.add_argument('--verify', type=int, default=0, help='self-consistency: sample N candidates, pick first that runs (rc==0)') args = ap.parse_args() codec = None if args.model == 'v01': weights_path = 'minds/mid_prophet_m2_big_w.txt' label = 'kenga-prophet m2 v0.1 K=8' elif args.model == 'k16': weights_path = 'minds/mid_prophet_m2_k16_w.txt' label = 'kenga-prophet m2.1 K=16' elif args.model == 'm3': weights_path = 'minds/mid_prophet_m3_w.txt' label = 'kenga-prophet m3 transformer K=32' elif args.model == 'm31': weights_path = 'minds/mid_prophet_m31_w.txt' label = 'kenga-prophet m3.1 transformer K=64 D=48 L=2' elif args.model == 'm32': weights_path = 'minds/mid_prophet_m32_w.txt' label = 'kenga-prophet m3.2 transformer K=64 D=48 L=2 (64-token codec)' codec = load_codec_vocab() elif args.model == 'm33': weights_path = 'minds/mid_prophet_m33_w.txt' label = 'kenga-prophet m3.3 transformer K=64 D=48 L=2 (64-token codec, seeds-in-train)' codec = load_codec_vocab() elif args.model == 'm34': weights_path = 'minds/mid_prophet_m34_w.txt' label = 'kenga-prophet m3.4 transformer K=64 D=48 L=2 (64-token codec, seeds-in-train x20)' codec = load_codec_vocab() elif args.model == 'm35': weights_path = 'minds/mid_prophet_m35_w.txt' label = 'kenga-prophet m3.5 transformer K=64 D=48 L=2 (64-token codec, seeds-dominate x300)' codec = load_codec_vocab() elif args.model == 'm36': weights_path = 'minds/mid_prophet_m36_w.txt' label = 'kenga-prophet m3.6 transformer K=64 D=48 L=2 (64-token codec, per-position causal LM, seeds x300)' codec = load_codec_vocab() elif args.model == 'm37': weights_path = 'minds/mid_prophet_m37_w.txt' label = 'kenga-prophet m3.7 transformer K=64 D=48 L=2 (73-token digit codec, per-position causal LM, seeds x300)' codec = load_codec_vocab('minds/kenga_digits.pkl') elif args.model == 'm40': weights_path = 'minds/mid_prophet_m40_w.txt' label = 'kenga-prophet m4.0 transformer K=64 D=128 H=8 L=6 (128-token full codec, 830K params, full corpus incl big)' codec = load_codec_vocab('minds/kenga_full.pkl') elif args.model == 'm41': weights_path = 'minds/mid_prophet_m41_w.txt' label = 'kenga-prophet m4.1 transformer K=64 D=128 H=8 L=6 (128-token full codec, seeds x30, real code dominant)' codec = load_codec_vocab('minds/kenga_full.pkl') elif args.model == 'm42': weights_path = 'minds/mid_prophet_m42_w.txt' label = 'kenga-prophet m4.2 transformer K=128 D=128 H=8 L=6 (128-token full codec, 838K params, seeds x30)' codec = load_codec_vocab('minds/kenga_full.pkl') else: print('unknown model', file=sys.stderr); sys.exit(1) print(f'using model: {label}') if args.prompt: toks, src = gen_tokens(args.prompt, weights_path, args.max_tokens, args.temperature, codec) full = make_valid_program(args.prompt, src) print('=== generated program ===') print(full) rc, out, err = run_via_kenga_lite(full) print(f'=== run rc={rc} ===') if out: print('stdout:', out.strip()[:200]) if err: print('stderr:', err.strip()[:200]) return 0 if not args.probe and not args.prompt: ap.print_help(); return 1 print('=== program-validity probe (kenga-lite driven) ===') n_compile = 0; n_run = 0; n_correct = 0; n_recover = 0 for prompt, want, exp_ids in PROBES: if args.verify: _, src, full, rc, out, err = gen_verified( prompt, weights_path, codec, n_samples=args.verify, max_tokens=args.max_tokens, temperature=1.0, want=want) else: _, src = gen_tokens(prompt, weights_path, args.max_tokens, args.temperature, codec) full = make_valid_program(prompt, src) rc, out, err = run_via_kenga_lite(full) first = out.strip().split('\n')[0] if out else '' ok_compile = rc in (0, 2) ok_run = rc == 0 ok_value = first == want declared = declared_params(src) ok_recover = declared is not None and declared == exp_ids print(f'prompt={prompt!r:12s} want={want:>5} rc={rc:<3} out={first[:20]:<20} compile={ok_compile} run={ok_run} match={ok_value} ids={sorted(declared) if declared else None} recover={ok_recover}') if ok_compile: n_compile += 1 if ok_run: n_run += 1 if ok_value: n_correct += 1 if ok_recover: n_recover += 1 total = len(PROBES) print() sys.stdout.write('compile-ok: ' + str(n_compile) + '/' + str(total) + ' = %.1f%%\n' % (n_compile*100/total)) sys.stdout.write('run-ok: ' + str(n_run) + '/' + str(total) + ' = %.1f%%\n' % (n_run*100/total)) sys.stdout.write('match value: ' + str(n_correct) + '/' + str(total) + ' = %.1f%%\n' % (n_correct*100/total)) sys.stdout.write('id-recovery: ' + str(n_recover) + '/' + str(total) + ' = %.1f%%\n' % (n_recover*100/total)) sys.stdout.flush() return 0 if __name__ == '__main__': sys.exit(main())