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 @@

A — Simple Main

 # 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 @@ 

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 @@ - + @@ -461,7 +461,7 @@ @loop(x, xs, { … }); # Vec elements @loop({ … @break() }); # forever @return(v); -@match(s, $A v => v, $B => 0)
+@match(s, $A v, { v }, $B, { 0 })
Functions
\\ 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,
+        { (('
  • ✓ ' ++ label) ++ '
  • ') }, + { (('
  • ○ ' ++ label) ++ '
  • ') }) +}; @fn main := { - web::console_log_str('Sigil Milestones'); - web::console_log_str(done_item('@fn keyword')); - web::console_log_str(done_item('web:: module system')); - web::console_log_str(done_item('Syntax highlighting')); - web::console_log_str(todo_item('HTML render panel')); - web::console_log_str(todo_item('Snake in Silicon')) + @mut html := '

    Silicon Milestones

    '; + html = (html ++ ''); + web::set_html(html); + 0 }; @export main;`, snake: -`# Snake counter — mutable globals, tick/handle_key export convention. -# Enable "run main" and compile. Call tick() repeatedly to advance. +`# Snake — a real, playable snake. Enable "run main", compile, then steer with +# the arrow keys. Eat the red food to grow; you die on a wall or yourself. +# Press Space to restart. The body lives in raw linear memory (WASM:: intrinsics). + +GRID := 16; # 16x16 board +CELL := 20; # pixels per cell (board = 320px) +BODY := 16384; # snake body: BODY[i] = the i-th segment's cell (y*GRID + x) + +@mut dir := 0; # 0=right 1=down 2=left 3=up +@mut pend := 0; # queued turn — applied once per tick (input can't 180°) +@mut len := 3; +@mut alive := 1; +@mut score := 0; +@mut food := 0; # food cell = y*GRID + x + +# Draw one grid cell as a rect (1px gap → grid look). Fill colour set by caller. +\\\\ cell_rect (Int, Int) +@fn cell_rect cx, cy := { + px := @toFloat(((cx * CELL) + 1)); + py := @toFloat(((cy * CELL) + 1)); + web::canvas_fill_rect(px, py, @toFloat((CELL - 2)), @toFloat((CELL - 2))); + 0 +}; -GRID := 20; +# Pick a new food cell that isn't on the snake (bounded retries). +@fn place_food := { + @mut placed := 0; + @mut tries := 0; + @loop({ + @if(placed == 1, { @break() }); + @if(tries >= 30, { @break() }); + fx := @toInt((web::math_random() * @toFloat(GRID))); + fy := @toInt((web::math_random() * @toFloat(GRID))); + cand := ((fy * GRID) + fx); + @mut on := 0; + @mut m := 0; + @loop(m < len, { + @if(WASM::i32_load((BODY + (m * 4))) == cand, { on = 1; }); + m = m + 1; + }); + @if(on == 0, { food = cand; placed = 1; }); + tries = tries + 1; + }); + 0 +}; -@mut head_x := 10; -@mut head_y := 10; -@mut dir := 0; # 0=right 1=down 2=left 3=up -@mut alive := 1; -@mut length := 3; -@mut score := 0; -@mut food_x := 5; -@mut food_y := 15; +# Render the board: background, food, body (head brighter), death bar. +@fn draw := { + web::canvas_set_fill(24, 28, 36); + web::canvas_fill_rect(0.0, 0.0, @toFloat((GRID * CELL)), @toFloat((GRID * CELL))); + + fx := food % GRID; + fy := food / GRID; + web::canvas_set_fill(220, 70, 60); + cell_rect(fx, fy); + + @mut i := (len - 1); + @loop(i >= 0, { + @if(i == 0, { web::canvas_set_fill(120, 230, 120); }, { web::canvas_set_fill(40, 170, 80); }); + c := WASM::i32_load((BODY + (i * 4))); + cell_rect((c % GRID), (c / GRID)); + i = i - 1; + }); -@fn main := { - head_x = 10; head_y = 10; - dir = 0; alive = 1; length = 3; score = 0; - food_x = 5; food_y = 15; - web::console_log_str('Snake started') + @if(alive == 0, { + web::canvas_set_fill(210, 50, 50); + web::canvas_fill_rect(0.0, 0.0, @toFloat((GRID * CELL)), 10.0); + }); + 0 +}; + +# Reset state: length-3 snake laid horizontally near the middle, fresh food. +@fn reset := { + dir = 0; pend = 0; len = 3; alive = 1; score = 0; + @mut i := 0; + @loop(i < len, { + WASM::i32_store((BODY + (i * 4)), (((8 * GRID) + 8) - i)); + i = i + 1; + }); + food = ((5 * GRID) + 11); + draw(); + 0 }; @fn tick := { @if(alive == 0, { @return(0) }); - @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((head_x < 0) || (head_x >= GRID), { alive = 0; }); - @if((head_y < 0) || (head_y >= GRID), { alive = 0; }); - @if(alive == 0, { @return(0) }); - @if(head_x == food_x, { - @if(head_y == food_y, { - length = length + 1; - score = score + 10; - food_x = (food_x + 7) % GRID; - food_y = (food_y + 13) % GRID; - }) + + # apply the queued turn once per tick — reject only a direct 180° reversal, + # so two fast key presses can never fold the head back into the neck. + @if(pend == 0, { @if(dir != 2, { dir = 0; }); }); + @if(pend == 1, { @if(dir != 3, { dir = 1; }); }); + @if(pend == 2, { @if(dir != 0, { dir = 2; }); }); + @if(pend == 3, { @if(dir != 1, { dir = 3; }); }); + + head := WASM::i32_load(BODY); + hx := head % GRID; + hy := head / GRID; + + @mut nx := hx; + @mut ny := hy; + @if(dir == 0, { nx = hx + 1; }); + @if(dir == 1, { ny = hy + 1; }); + @if(dir == 2, { nx = hx - 1; }); + @if(dir == 3, { ny = hy - 1; }); + + @if(nx < 0, { alive = 0; }); + @if(nx >= GRID, { alive = 0; }); + @if(ny < 0, { alive = 0; }); + @if(ny >= GRID, { alive = 0; }); + @if(alive == 0, { draw(); @return(0) }); + + nhead := ((ny * GRID) + nx); + + @mut hit := 0; + @mut j := 0; + @loop(j < (len - 1), { + @if(WASM::i32_load((BODY + (j * 4))) == nhead, { hit = 1; }); + j = j + 1; + }); + @if(hit == 1, { alive = 0; draw(); @return(0) }); + + @mut ate := 0; + @if(nhead == food, { + score = score + 10; + len = len + 1; + ate = 1; + }); + + # advance the body (len already grown if we ate), then write the new head + @mut k := (len - 1); + @loop(k > 0, { + WASM::i32_store((BODY + (k * 4)), WASM::i32_load((BODY + ((k - 1) * 4)))); + k = k - 1; }); + WASM::i32_store(BODY, nhead); + + # place food only AFTER the body is fully updated, so it never lands on the + # snake (including the new head) and never reads an unwritten segment slot. + @if(ate == 1, { place_food(); }); + + draw(); score }; +@fn main := { + reset(); + web::console_log_str('Snake — arrow keys to steer, Space to restart'); + 0 +}; + \\\\ 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; }); + @if(key == 37, { pend = 2; }); # left + @if(key == 38, { pend = 3; }); # up + @if(key == 39, { pend = 0; }); # right + @if(key == 40, { pend = 1; }); # down + @if(key == 32, { reset(); }); 0 }; @@ -1111,7 +1227,7 @@ @type Opt := $Some v Int | $None; \\\\ unwrap_or (Opt, Int) -@fn unwrap_or o, dflt := @match(o, $Some v => v, $None => dflt); +@fn unwrap_or o, dflt := @match(o, $Some v, { v }, $None, { dflt }); \\\\ demo () -> Int @fn demo := unwrap_or(Some(42), 0); @@ -1127,7 +1243,7 @@ @fn sum_squares := { v := vec_new(4); - # Push 1², 2², … 5² into the GC-managed vector. + # Push 1², 2², … 5² into the growable vector. @mut i := 1; @loop(i <= 5, { vec_push_i32(v, (i * i)); @@ -1157,7 +1273,6 @@ // Examples that compile with a non-default target (--target=wasm-gc, etc.). const EXAMPLE_TARGETS = { wasm_gc: 'wasm-gc', - gc_vec: 'wasm-gc', } // Cheatsheet side panel — collapse / expand. diff --git a/playground/playground/modules-plan.html b/playground/playground/modules-plan.html index 97d93d2b..722f3ba2 100644 --- a/playground/playground/modules-plan.html +++ b/playground/playground/modules-plan.html @@ -340,13 +340,13 @@

    Goal

    Before (Round 45) - @extern web_console_log_str ptr:String;
    - &web_console_log_str result; + \\ @extern web_console_log_str (String) -> Void;
    + web_console_log_str(result);
    After (Round 46) - &web::console_log result; + web::console_log(result);
    @@ -389,7 +389,7 @@

    Design Decisions

    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 @@

    Module file format

    the module name. Inside, functions are declared without the module prefix:

    -
    ; 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;

    Calling a module function

    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 namespace rule already handles web::console_log_str via the ("::" | ".") identifier - repetition. And FunctionCall = "&" FunctionCallBody already accepts a - namespace as the callee. No grammar changes are needed. + repetition. And the always-parenthesised call form ExprEnd = Primary { CallSuffix } + already accepts a namespace as the callee. No grammar changes are needed.
    @@ -554,12 +554,12 @@

    Implementation Steps

    collect @extern nodes and extract their name and type signature via the same siliconTypeNameToWasm helper already used in lower.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;
    depends on step 1 (ModuleRegistry type)
    @@ -629,7 +629,7 @@

    Implementation Steps

    - 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 @@

    Implementation Steps

    (double underscore avoids collision with user-defined functions). The call site emits (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 reads web.si automatically.

    -
    - @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 hypothetical Draw module. 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 to web::* 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

    The current strata loader recognises WASM::opcode and IR::kind - tokens in stratum bodies. To support calls like &str_concat a, b, the + tokens in stratum bodies. To support calls like str_concat(a, b), the loader must also handle plain Silicon function-call syntax and emit the appropriate IR Call node (or WAT call $str_concat) during expansion.

    @@ -686,7 +688,7 @@

    Implementation Steps

    This change is additive — existing WASM intrinsic strata continue to work unchanged. The parser for .si stratum files just needs to recognise - &fnName args… as a FunctionCall node alongside the + fnName(args…) as a FunctionCall node alongside the existing WASM::opcode and IR::kind forms.

    @@ -705,18 +707,18 @@

    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, ' ++ msg into - &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 with webEnv.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`)