HS: do→let/it chaining, single-IO fetch, fetch URL parser, IO mock

Compiler: do-blocks now compile to (let ((it cmd1)) (let ((it cmd2)) ...))
instead of (do cmd1 cmd2 ...). This chains the `it` variable through
command sequences, enabling `fetch X then put it into me` pattern.
Each command's result is bound to `it` for the next command.

Runtime: hs-fetch simplified to single perform (io-fetch url format)
instead of two-stage io-fetch + io-parse-text/json.

Parser: fetch URL /path handled by reading /+ident tokens.
Default fetch format changed to "text" (was "json").

Test runner: mock fetch routes with format-specific responses.
io-fetch handler returns content directly based on format param.

Fetch tests still need IO suspension to chain through let continuations.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-17 22:54:31 +00:00
parent 25db89a96c
commit ac193e8839
7 changed files with 53 additions and 22 deletions

View File

@@ -192,6 +192,8 @@ const _fetchRoutes = {
'/test': { status: 200, body: 'yay', json: '{"foo":1}', html: '<div>yay</div>' },
'/test-json': { status: 200, body: '{"foo":1}', json: '{"foo":1}' },
'/404': { status: 404, body: 'the body' },
'/number': { status: 200, body: '1.2' },
'/users/Joe': { status: 200, body: 'Joe', json: '{"name":"Joe"}' },
};
function _mockFetch(url) {
const route = _fetchRoutes[url] || _fetchRoutes['/test'];
@@ -201,10 +203,19 @@ function _mockFetch(url) {
globalThis._driveAsync=function driveAsync(r,d){d=d||0;if(d>500||!r||!r.suspended)return;if(_testDeadline && Date.now()>_testDeadline)throw new Error('TIMEOUT: wall clock exceeded');const req=r.request;const items=req&&(req.items||req);const op=items&&items[0];const opName=typeof op==='string'?op:(op&&op.name)||String(op);
function doResume(v){try{const x=r.resume(v);driveAsync(x,d+1);}catch(e){}}
if(opName==='io-sleep'||opName==='wait')doResume(null);
else if(opName==='io-fetch'){const url=items&&items[1];doResume(_mockFetch(typeof url==='string'?url:'/test'));}
else if(opName==='io-parse-text'){const resp=items&&items[1];doResume(resp&&resp._body?resp._body:'');}
else if(opName==='io-parse-json'){const resp=items&&items[1];try{doResume(JSON.parse(resp&&resp._json?resp._json:'{}'));}catch(e){doResume(null);}}
else if(opName==='io-parse-html'){const resp=items&&items[1];const frag=new El('fragment');frag.nodeType=11;frag.innerHTML=resp&&resp._html?resp._html:'';frag.textContent=frag.innerHTML.replace(/<[^>]*>/g,'');doResume(frag);}
else if(opName==='io-fetch'){
const url=typeof items[1]==='string'?items[1]:'/test';
const fmt=typeof items[2]==='string'?items[2]:'text';
const route=_fetchRoutes[url]||_fetchRoutes['/test'];
if(fmt==='json'){try{doResume(JSON.parse(route.json||route.body||'{}'));}catch(e){doResume(null);}}
else if(fmt==='html'){const frag=new El('fragment');frag.nodeType=11;frag.innerHTML=route.html||route.body||'';frag.textContent=frag.innerHTML.replace(/<[^>]*>/g,'');doResume(frag);}
else if(fmt==='response')doResume({ok:(route.status||200)<400,status:route.status||200,url});
else if(fmt==='Number'||fmt==='number')doResume(parseFloat(route.body||'0'));
else doResume(route.body||'');
}
else if(opName==='io-parse-text'){const resp=items&&items[1];doResume(resp&&resp._body?resp._body:typeof resp==='string'?resp:'');}
else if(opName==='io-parse-json'){const resp=items&&items[1];try{doResume(JSON.parse(typeof resp==='string'?resp:resp&&resp._json?resp._json:'{}'));}catch(e){doResume(null);}}
else if(opName==='io-parse-html'){const frag=new El('fragment');frag.nodeType=11;doResume(frag);}
else if(opName==='io-settle')doResume(null);
else if(opName==='io-wait-event')doResume(null);
else if(opName==='io-transition')doResume(null);