diff --git a/playground/playground/features-plan.html b/playground/playground/features-plan.html index 40e8832d..bfe474da 100644 --- a/playground/playground/features-plan.html +++ b/playground/playground/features-plan.html @@ -430,18 +430,18 @@
# Entry point example — call helpers from main and print results. -@fn add a:Int, b:Int := { a + b }; +@fn add a, b := { a + b }; -@fn factorial n:Int := { - &@if n <= 1, { &@return 1 }; - n * (&factorial (n - 1)) +@fn factorial n := { + @if(n <= 1, { @return(1) }); + n * factorial((n - 1)) }; @fn main := { - @local sum := (&add 10, 32); - @local fac10 := (&factorial 10); - &web::console_log sum; - &web::console_log fac10 + sum := add(10, 32); + fac10 := factorial(10); + web::console_log(sum); + web::console_log(fac10) }; @export main; @@ -463,27 +463,27 @@+@match(s, $A v, { v }, $B, { 0 })B — HTML TODO List
# TODO list rendered into the HTML panel via web::set_html. # Check "run main" and click ▶ Compile. -@fn item label:String, done:Int := { - @local cls := (&@if done, { 'done' }, { 'todo' }); +@fn item label, done := { + cls := @if(done == 1, { 'done' }, { 'todo' }); '<li class="' ++ cls ++ '">' ++ label ++ '</li>' }; @fn main := { - @local html := ( + html := ( '<style>' ++ 'body{font:16px system-ui;padding:20px;}' ++ 'ul{list-style:none;padding:0}' ++ 'li{padding:8px 12px;margin:4px 0;border-radius:6px;background:#21262d;color:#e6edf3}' ++ 'li.done{opacity:0.5;text-decoration:line-through}' ++ '</style><ul>' - ++ (&item 'Add @fn keyword', 1) - ++ (&item 'Module system (web::)', 1) - ++ (&item 'Syntax highlighting', 1) - ++ (&item 'HTML render panel', 0) - ++ (&item 'Snake in Silicon', 0) + ++ item('Add @fn keyword', 1) + ++ item('Module system (web::)', 1) + ++ item('Syntax highlighting', 1) + ++ item('HTML render panel', 0) + ++ item('Snake in Silicon', 0) ++ '</ul>' ); - &web::set_html html + web::set_html(html) }; @export main; @@ -517,67 +517,68 @@C — Snake
# Snake — uses the tick + handle_key export convention. # Enable "run main" and compile. -@let GRID := 20; # cells per side -@let CELL := 16; # px per cell - -@var head_x := 10; -@var head_y := 10; -@var dir := 0; # 0=right 1=down 2=left 3=up -@var food_x := 5; -@var food_y := 15; -@var alive := 1; -@var length := 3; - -@fn cell x:Int, y:Int, r:Int, g:Int, b:Int := { - &web::canvas_set_fill r, g, b; - &web::canvas_fill_rect - (&@toFloat (x * CELL)), - (&@toFloat (y * CELL)), - 15.0, 15.0 +GRID := 20; # cells per side +CELL := 16; # px per cell + +@mut head_x := 10; +@mut head_y := 10; +@mut dir := 0; # 0=right 1=down 2=left 3=up +@mut food_x := 5; +@mut food_y := 15; +@mut alive := 1; +@mut length := 3; + +@fn cell x, y, r, g, b := { + web::canvas_set_fill(r, g, b); + web::canvas_fill_rect( + @toFloat((x * CELL)), + @toFloat((y * CELL)), + 15.0, 15.0) }; @fn draw := { # Background - &web::canvas_set_fill 13, 17, 23; - &web::canvas_fill_rect 0.0, 0.0, - (&@toFloat (GRID * CELL)), - (&@toFloat (GRID * CELL)); + web::canvas_set_fill(13, 17, 23); + web::canvas_fill_rect(0.0, 0.0, + @toFloat((GRID * CELL)), + @toFloat((GRID * CELL))); # Snake head - &cell head_x, head_y, 63, 185, 80; + cell(head_x, head_y, 63, 185, 80); # Food - &cell food_x, food_y, 248, 81, 73 + cell(food_x, food_y, 248, 81, 73) }; @fn main := { head_x = 10; head_y = 10; dir = 0; alive = 1; length = 3; - &draw + draw() }; @fn tick := { - &@if alive == 0, { &@return }; + @if(alive == 0, { @return() }); # Move head - &@if dir == 0, { head_x = head_x + 1 }; - &@if dir == 1, { head_y = head_y + 1 }; - &@if dir == 2, { head_x = head_x - 1 }; - &@if dir == 3, { head_y = head_y - 1 }; + @if(dir == 0, { head_x = head_x + 1 }); + @if(dir == 1, { head_y = head_y + 1 }); + @if(dir == 2, { head_x = head_x - 1 }); + @if(dir == 3, { head_y = head_y - 1 }); # Wall collision - &@if (head_x < 0) || (head_x >= GRID), { alive = 0; &@return }; - &@if (head_y < 0) || (head_y >= GRID), { alive = 0; &@return }; - # Food pickup - &@if (head_x == food_x) && (head_y == food_y), { + @if((head_x < 0) || (head_x >= GRID), { alive = 0; @return() }); + @if((head_y < 0) || (head_y >= GRID), { alive = 0; @return() }); + # Food pickup (nested @if — there is no && operator) + @if(head_x == food_x, { @if(head_y == food_y, { length = length + 1; food_x = (food_x + 7) % GRID; food_y = (food_y + 13) % GRID - }; - &draw + }) }); + draw() }; -@fn handle_key key:Int := { - &@if key == 37, { dir = 2 }; # ← - &@if key == 38, { dir = 3 }; # ↑ - &@if key == 39, { dir = 0 }; # → - &@if key == 40, { dir = 1 } # ↓ +\\ handle_key (Int) +@fn handle_key key := { + @if(key == 37, { dir = 2 }); # ← + @if(key == 38, { dir = 3 }); # ↑ + @if(key == 39, { dir = 0 }); # → + @if(key == 40, { dir = 1 }) # ↓ }; @export main; diff --git a/playground/playground/index.html b/playground/playground/index.html index 13dbd273..7d7b638c 100644 --- a/playground/playground/index.html +++ b/playground/playground/index.html @@ -390,7 +390,7 @@
\\ add (Int, Int) -> Int
@fn add a, b := { a + b };
@@ -733,8 +733,8 @@
\\\\ greet (String)
@fn greet msg := {
- @local result := ('Hello, ' ++ msg);
- &web::console_log_str result;
+ result := ('Hello, ' ++ msg);
+ web::console_log_str(result);
result
};
@@ -1011,74 +1011,190 @@
@export main;`,
html_todo:
-`# String-building TODO list — console output via web::console_log_str.
-# Enable "run main" and compile.
-
-\\\\ done_item (String) -> String
-@fn done_item label := { '[x] ' ++ label };
-
-\\\\ todo_item (String) -> String
-@fn todo_item label := { '[ ] ' ++ label };
+`# HTML list — build an HTML string and render it into the panel via set_html.
+# Enable "run main" + compile; the list appears in the Render panel (right).
+
+\\\\ item (String, Int) -> String
+@fn item label, done := {
+ @if(done == 1,
+ { (('
No @use declaration required. The namespace in the call site
- is the import — &web::console_log auto-resolves
+ is the import — web::console_log auto-resolves
the web module. Namespaces must always be written in full;
there is no aliasing (use Draw as D is a non-goal for this round).
Compiler error if the namespace does not match any registered module.
@@ -468,35 +468,35 @@
; src/strata/modules/web.si — the built-in web:: environment module -; Functions here become (import "web" "name" …) in WAT. +-# src/strata/modules/web.si — the built-in web:: environment module +# Functions here become (import "web" "name" …) in WAT. -@extern console_log v:Int; -@extern console_log_f v:Float; -@extern console_log_str ptr:String; -@extern console_error v:Int; -@extern console_warn v:Int;+\\ @extern console_log (Int) -> Void; +\\ @extern console_log_f (Float) -> Void; +\\ @extern console_log_str (String) -> Void; +\\ @extern console_error (Int) -> Void; +\\ @extern console_warn (Int) -> Void;
; modules/Draw.si — a hypothetical third-party drawing module -; Host must provide { Draw: { clear, fill_rect, … } } in the WASM import object. +# modules/Draw.si — a hypothetical third-party drawing module +# Host must provide { Draw: { clear, fill_rect, … } } in the WASM import object. -@extern clear () -> Void; -@extern fill_rect (x:Int, y:Int, w:Int, h:Int) -> Void; -@extern set_color (r:Int, g:Int, b:Int) -> Void;+\\ @extern clear () -> Void; +\\ @extern fill_rect (Int, Int, Int, Int) -> Void; +\\ @extern set_color (Int, Int, Int) -> Void;
In user Silicon code, no extern declaration is needed — just use the namespaced call:
-@let greet msg:String := { - @local result := ('Hello, ' ++ msg); - &web::console_log_str result; ; no @extern needed — compiler reads web.si +@fn greet msg := { + result := ('Hello, ' ++ msg); + web::console_log_str(result); # no @extern needed — compiler reads web.si result };Grammar note: The Silicon grammar's@@ -554,12 +554,12 @@namespacerule already handlesweb::console_log_strvia the("::" | ".") identifier- repetition. AndFunctionCall = "&" FunctionCallBodyalready accepts a - namespace as the callee. No grammar changes are needed. + repetition. And the always-parenthesised call formExprEnd = Primary { CallSuffix }+ already accepts a namespace as the callee. No grammar changes are needed.Implementation Steps
collect@externnodes and extract their name and type signature via the samesiliconTypeNameToWasmhelper already used inlower.ts. -; src/strata/modules/web.si -@extern console_log v:Int; -@extern console_log_f v:Float; -@extern console_log_str ptr:String; -@extern console_error v:Int; -@extern console_warn v:Int;+# src/strata/modules/web.si +\\ @extern console_log (Int) -> Void; +\\ @extern console_log_f (Float) -> Void; +\\ @extern console_log_str (String) -> Void; +\\ @extern console_error (Int) -> Void; +\\ @extern console_warn (Int) -> Void;
- When codegen sees &web::console_log_str msg, the callee namespace
+ When codegen sees web::console_log_str(msg), the callee namespace
path is ['web', 'console_log_str']. Look up web in the
module registry → found as an env module → look up console_log_str →
get its signature → emit (import "web" "console_log_str" (func $web__console_log_str (param i32))).
@@ -639,7 +639,7 @@
(call $web__console_log_str …).
- ; emitted WAT for &web::console_log_str result +; emitted WAT for web::console_log_str(result) (import "web" "console_log_str" (func $web__console_log_str (param i32))) … (call $web__console_log_str (local.get $result))@@ -661,15 +661,15 @@Implementation Steps
web externs inline. After this step those go away — the compiler readsweb.siautomatically. -- @extern web_console_log_str ptr:String; -- @extern web_console_log_f v:Float; +\\ @extern web_console_log_str (String) -> Void; +\\ @extern web_console_log_f (Float) -> Void; - @let greet msg:String := { - @local result := ('Hello, ' ++ msg); -- &web_console_log_str result; -+ &web::console_log_str result; - result - };+@fn greet msg := { + result := ('Hello, ' ++ msg); + web_console_log_str(result); + web::console_log_str(result); + result +};Any e2e or integration tests that use
web_*extern declarations need the same treatment. @@ -714,15 +714,15 @@Implementation Steps
Demonstrate the module system with a hypotheticalDrawmodule. This is documentation / test fixture only — no compiler changes. -; modules/Draw.si — dropped in by the user, no install step needed -@extern clear () -> Void; -@extern fill_rect (x:Int, y:Int, w:Int, h:Int) -> Void; -@extern set_color (r:Int, g:Int, b:Int) -> Void;-; main.si — user code using the Draw module -@let render _ := { - &Draw::clear; - &Draw::set_color 255, 128, 0; - &Draw::fill_rect 10, 10, 200, 100; +# modules/Draw.si — dropped in by the user, no install step needed +\\ @extern clear () -> Void; +\\ @extern fill_rect (Int, Int, Int, Int) -> Void; +\\ @extern set_color (Int, Int, Int) -> Void;+# main.si — user code using the Draw module +@fn render _ := { + Draw::clear(); + Draw::set_color(255, 128, 0); + Draw::fill_rect(10, 10, 200, 100); };The JS host provides
{ Draw: { clear: …, fill_rect: …, set_color: … } }@@ -790,7 +790,7 @@File Change Summary
playground/index.html rename -Remove inline +@extern web_*; change call sites to&web::*Remove inline @extern web_*; change call sites toweb::*e2e / test fixtures diff --git a/playground/playground/plan.html b/playground/playground/plan.html index 6b980741..e3ca2271 100644 --- a/playground/playground/plan.html +++ b/playground/playground/plan.html @@ -439,14 +439,14 @@Implementation Steps
is an echo — the function receives a String pointer and returns it unchanged. This proves the round-trip without requiring string manipulation in Silicon. --@let greet n:Int := { -- &web_console_log_str 'Hello, World!'; -- n --}; -+@let greet msg:String := { -+ &web_console_log_str msg; -+ msg -+};+@fn greet n := { + web_console_log_str('Hello, World!'); + n +}; +@fn greet msg := { + web_console_log_str(msg); + msg +};The WASM signature becomes
(param i32) (result i32)— the pointer goes in and back out. JS writes the string before the call and reads the string after. @@ -629,8 +629,10 @@Implementation Steps
# src/strata/strings.si # String operator strata for Silicon. -@stratum_operator Concat ('++', Node) := - &str_concat a, b; +@stratum Concat := { + Compiler::register::operator('++'); + str_concat(a, b) +}; # `a` and `b` are the left- and right-hand operands injected by the elaborator. # str_concat is resolved to $str_concat in std.wat at link time.@@ -658,7 +660,7 @@
Implementation Steps
@@ -705,18 +707,18 @@The current strata loader recognises
@@ -686,7 +688,7 @@WASM::opcodeandIR::kind- tokens in stratum bodies. To support calls like&str_concat a, b, the + tokens in stratum bodies. To support calls likestr_concat(a, b), the loader must also handle plain Silicon function-call syntax and emit the appropriate IRCallnode (or WATcall $str_concat) during expansion.Implementation Steps
This change is additive — existing WASM intrinsic strata continue to work unchanged. The parser for
.sistratum files just needs to recognise -&fnName args…as aFunctionCallnode alongside the +fnName(args…)as aFunctionCallnode alongside the existingWASM::opcodeandIR::kindforms.Implementation Steps
Once steps 7–9 are complete, the playground example can produce a real greeting by concatenating the literal prefix'Hello, 'with the caller-supplied name. --@let greet msg:String := { -- &web_console_log_str msg; -- msg --}; -+@let greet msg:String := { -+ let result := ('Hello, ' ++ msg); -+ &web_console_log_str result; -+ result -+};+@fn greet msg := { + web_console_log_str(msg); + msg +}; +@fn greet msg := { + result := ('Hello, ' ++ msg); + web_console_log_str(result); + result +};The elaborator rewrites
'Hello, ' ++ msginto -&str_concat 'Hello, ', msg, which lowers to +str_concat('Hello, ', msg), which lowers to(call $str_concat (i32.const <literal_ptr>) (local.get $msg))in WAT. The JS exports panel then reads the returned pointer withwebEnv.readString(result)and displays"Hello, Alice!". @@ -773,7 +775,7 @@Change Summary
src/strata/strings.si -Define ++ as @stratum_operator Concat — body calls &str_concat; no new WASM intrinsic needed (intrinsics map 1-to-1 to WASM ops only) +Define ++ as @stratum_operator Concat — body calls str_concat; no new WASM intrinsic needed (intrinsics map 1-to-1 to WASM ops only) 8 diff --git a/playground/playground/server.ts b/playground/playground/server.ts index ffdaff0f..89a9944c 100644 --- a/playground/playground/server.ts +++ b/playground/playground/server.ts @@ -17,7 +17,7 @@ import { compileToWasm, elaborate, buildStrataRegistry, typecheck, formatTypeError, formatType, wasmTypeOf, type FunctionSig, - loadModules, + loadModules, inlineStdlibUses, loadPlatform, getRequiredExports, type PlatformConfig, } from '@silicon/compiler/pipeline' @@ -109,6 +109,9 @@ async function compileSilicon(source: string, platformConfig?: PlatformConfig, t : fallbackModuleRegistry const options = target && target !== 'host' ? { target: target as any } : undefined + // Inline bare-name stdlib `@use` directives before parsing — the raw parser + // doesn't understand `@use` (mirrors the browser build in web/entry.ts). + source = inlineStdlibUses(source) const ast = parse(source) const registry = buildStrataRegistry(ast as Program) const { program: elaborated, errors: elabErrors } = elaborate(ast as Program, registry) diff --git a/playground/web/verify-examples.ts b/playground/web/verify-examples.ts index a8dc605f..2a9de7e0 100644 --- a/playground/web/verify-examples.ts +++ b/playground/web/verify-examples.ts @@ -29,28 +29,50 @@ const browserSwap: BunPlugin = { }, } -// ── extract the EXAMPLES object literal from index.html ───────────────────── +// ── extract object literals from index.html ───────────────────────────────── const html = readFileSync(join(WEB, '..', 'playground', 'index.html'), 'utf8') -const start = html.indexOf('const EXAMPLES = {') -if (start < 0) throw new Error('EXAMPLES object not found in index.html') -// find the matching closing brace of the object literal -let i = html.indexOf('{', start), depth = 0, end = -1, inStr = '', esc = false -for (; i < html.length; i++) { - const c = html[i] - if (inStr) { - if (esc) { esc = false; continue } - if (c === '\\') { esc = true; continue } - if (c === inStr) inStr = '' - continue + +/** Balance-match the `{ … }` object literal that follows `marker` and eval it. */ +function extractObject (marker: string, required = true): T { + const start = html.indexOf(marker) + if (start < 0) { if (required) throw new Error(`${marker} not found in index.html`); return {} as T } + let i = html.indexOf('{', start), depth = 0, end = -1, inStr = '', esc = false + for (; i < html.length; i++) { + const c = html[i] + if (inStr) { + if (esc) { esc = false; continue } + if (c === '\\') { esc = true; continue } + if (c === inStr) inStr = '' + continue + } + if (c === '"' || c === "'" || c === '`') { inStr = c; continue } + if (c === '{') depth++ + else if (c === '}') { depth--; if (depth === 0) { end = i; break } } + } + if (end < 0) throw new Error(`could not balance ${marker} braces`) + // eslint-disable-next-line no-eval + return eval('(' + html.slice(html.indexOf('{', start), end + 1) + ')') +} + +const EXAMPLES = extractObject >('const EXAMPLES = {') +const EXAMPLE_FEATURES = extractObject >('const EXAMPLE_FEATURES = {') +const EXAMPLE_TARGETS = extractObject >('const EXAMPLE_TARGETS = {') + +// Mirror index.html's example-picker: resolve the exact features + target each +// example is compiled with in the UI, so this check matches real playground use. +const VALID_FEATURES = new Set(['canvas', 'game', 'dom']) +function featuresFor(key: string, src: string): string[] { + let feats: string[] + if (key in EXAMPLE_FEATURES) feats = EXAMPLE_FEATURES[key] + else if (key === 'empty') feats = [] + else { + feats = [] + if (src.includes('canvas_')) feats.push('canvas') + if (src.includes('@export tick')) feats.push('game') + if (src.includes('set_html')) feats.push('dom') } - if (c === '"' || c === "'" || c === '`') { inStr = c; continue } - if (c === '{') depth++ - else if (c === '}') { depth--; if (depth === 0) { end = i; break } } + return feats.filter(f => VALID_FEATURES.has(f)) } -if (end < 0) throw new Error('could not balance EXAMPLES object braces') -const objText = html.slice(html.indexOf('{', start), end + 1) -// eslint-disable-next-line no-eval -const EXAMPLES: Record = eval('(' + objText + ')') // ── bundle entry.ts (browser target) exactly like verify.ts ───────────────── const outdir = join(tmpdir(), 'silicon-playground-verify-examples') @@ -61,21 +83,24 @@ const result = await Bun.build({ if (!result.success) { for (const log of result.logs) console.error(log); process.exit(1) } await import(result.outputs[0].path) const SiliconCompiler = (globalThis as any).SiliconCompiler as { - compile(req: { source: string; platform?: string; features?: string[] }): Promise + compile(req: { source: string; platform?: string; features?: string[]; target?: string }): Promise } if (!SiliconCompiler) throw new Error('globalThis.SiliconCompiler was not set by the bundle') -// ── compile every non-empty example ───────────────────────────────────────── +// ── compile every non-empty example with its real features + target ────────── let failed = 0, ok = 0 for (const [name, source] of Object.entries(EXAMPLES)) { if (!source.trim()) continue - const data = await SiliconCompiler.compile({ source, platform: 'web', features: [] }) + const features = featuresFor(name, source) + const target = EXAMPLE_TARGETS[name] ?? 'host' + const data = await SiliconCompiler.compile({ source, platform: 'web', features, target }) + const tag = `[${features.join(',') || 'core'}${target !== 'host' ? ` · ${target}` : ''}]` if (data.success) { ok++ - console.log(`✓ ${name} — ${data.exports.length} export(s)`) + console.log(`✓ ${name} ${tag} — ${data.exports.length} export(s)`) } else { failed++ - console.error(`✗ ${name} FAILED:\n${(data.error || '').split('\n').map((l: string) => ' ' + l).join('\n')}`) + console.error(`✗ ${name} ${tag} FAILED:\n${(data.error || '').split('\n').map((l: string) => ' ' + l).join('\n')}`) } } console.log(`\nEXAMPLES: ${ok} compiled, ${failed} failed`)