1+ from __future__ import annotations
2+
13import re
24from pathlib import Path
3- from typing import Annotated , Any , TypedDict
5+ from typing import Annotated , Any , TypedDict , Unpack , cast
46
57import yaml
68from jinja2 import Environment , FileSystemLoader , StrictUndefined
9+ from pymsch import Block , Content , ProcessorConfig , ProcessorLink , Schematic
710from typer import Option , Typer
811
12+ from mlogv32 .utils .msch import BEContent
13+
914from .extensions import CommentStatement
1015from .filters import FILTERS
11- from .parser .mlog import Label , Statement , parse_mlog
16+ from .models import BuildConfig
17+ from .parser import iter_labels , parse_mlog
1218
1319app = Typer (
1420 pretty_exceptions_show_locals = False ,
@@ -20,17 +26,9 @@ def file_command(
2026 path : Path ,
2127 output : Annotated [Path | None , Option ("-o" , "--output" )] = None ,
2228):
23- path = path .resolve ()
24-
25- env = create_jinja_env (path .parent )
26- template = env .get_template (path .name )
27- result = template .render ()
29+ """Preprocess a single .mlog.jinja file."""
2830
29- if output :
30- output .parent .mkdir (parents = True , exist_ok = True )
31- output .write_text (result , "utf-8" )
32- else :
33- print (result )
31+ render_template (path .resolve (), output )
3432
3533
3634@app .command ()
@@ -39,6 +37,8 @@ def labels(
3937 output : Annotated [Path | None , Option ("-o" , "--output" )] = None ,
4038 filter_str : Annotated [str | None , Option ("--filter" )] = None ,
4139):
40+ """Extract label addresses from a mlog file into set statements."""
41+
4242 if filter_str :
4343 filter_re = re .compile (filter_str )
4444 else :
@@ -47,16 +47,11 @@ def labels(
4747 with path .open ("r" , encoding = "utf-8" ) as f :
4848 lines = parse_mlog (f .read ())
4949
50- result_lines = list [str ]()
51- line_num = 0
52- for line in lines :
53- match line :
54- case Label (name = name ):
55- if not filter_re or filter_re .fullmatch (name ):
56- result_lines .append (f"set { name } { line_num } " )
57- case Statement ():
58- line_num += 1
59- result = "\n " .join (result_lines )
50+ result = "\n " .join (
51+ f"set { name } { line_num } "
52+ for name , line_num in iter_labels (lines )
53+ if not filter_re or filter_re .fullmatch (name )
54+ )
6055
6156 if output :
6257 output .write_text (result , encoding = "utf-8" )
@@ -66,6 +61,8 @@ def labels(
6661
6762@app .command ()
6863def configs (yaml_path : Path ):
64+ """Generate CPU configs."""
65+
6966 with yaml_path .open ("rb" ) as f :
7067 data : ConfigsYaml = yaml .load (f , yaml .Loader )
7168
@@ -78,6 +75,236 @@ def configs(yaml_path: Path):
7875 (output_dir / name ).with_suffix (".mlog" ).write_text (result , "utf-8" )
7976
8077
78+ @app .command ()
79+ def build (
80+ yaml_path : Path ,
81+ width : Annotated [int , Option ("-w" , "--width" )] = 16 ,
82+ height : Annotated [int , Option ("-h" , "--height" )] = 16 ,
83+ size : Annotated [int | None , Option ("-s" , "--size" )] = None ,
84+ output : Annotated [Path | None , Option ("-o" , "--output" )] = None ,
85+ cpu_only : bool = False ,
86+ ):
87+ """Generate a CPU schematic."""
88+
89+ if size :
90+ width = size
91+ height = size
92+
93+ config = BuildConfig .load (yaml_path )
94+
95+ worker_output = get_template_output_path (config .templates .worker )
96+ worker_code = render_template (config .templates .worker , worker_output )
97+
98+ labels = dict (iter_labels (parse_mlog (worker_code )))
99+
100+ controller_output = get_template_output_path (config .templates .controller )
101+ controller_code = render_template (
102+ config .templates .controller ,
103+ controller_output ,
104+ instructions = config .instructions ,
105+ labels = labels ,
106+ )
107+
108+ lookups_schem = cast (
109+ Schematic ,
110+ Schematic .read_file (str (config .schematics .lookups )), # type: ignore
111+ )
112+ assert lookups_schem .get_dimensions () == (4 , 4 )
113+
114+ ram_schem = cast (
115+ Schematic ,
116+ Schematic .read_file (str (config .schematics .ram )), # type: ignore
117+ )
118+ assert ram_schem .get_dimensions () == (1 , 1 )
119+
120+ schem = Schematic ()
121+
122+ for x in lenrange (0 , 16 ):
123+ for y in lenrange (0 , 16 ):
124+ schem .add_block (simple_block (BEContent .TILE_LOGIC_DISPLAY , x , y ))
125+
126+ display_link = ProcessorLink (0 , 0 , "display1" )
127+
128+ schem .add_schem (lookups_schem , 0 , 16 )
129+ lookup_links = [
130+ ProcessorLink (x = i % 4 , y = 16 + i // 4 , name = f"processor{ i + 1 } " )
131+ for i in range (16 )
132+ ]
133+
134+ add_label (schem , 4 , 19 , right = "UART0, UART2" , down = "LABELS" )
135+ schem .add_block (simple_block (Content .WORLD_CELL , 4 , 18 ))
136+ schem .add_block (simple_block (Content .WORLD_CELL , 4 , 17 ))
137+ add_label (schem , 4 , 16 , up = "COSTS" , right = "UART1, UART3" )
138+
139+ labels_link = ProcessorLink (4 , 18 , "cell1" )
140+ costs_link = ProcessorLink (4 , 17 , "cell2" )
141+
142+ uart_links = list [ProcessorLink ]()
143+ for x in lenrange (5 , 4 , 2 ):
144+ for y in lenrange (18 , - 4 , - 2 ):
145+ schem .add_block (simple_block (Content .MEMORY_BANK , x , y ))
146+ uart_links .append (ProcessorLink (x , y , f"bank{ len (uart_links ) + 1 } " ))
147+
148+ schem .add_block (simple_block (Content .MEMORY_CELL , 9 , 19 ))
149+ schem .add_schem (ram_schem , 9 , 18 )
150+ schem .add_schem (ram_schem , 9 , 17 )
151+ schem .add_block (Block (Content .MICRO_PROCESSOR , 9 , 16 , ProcessorConfig ("" , []), 0 ))
152+
153+ registers_link = ProcessorLink (9 , 19 , "cell3" )
154+ csrs_link = ProcessorLink (9 , 18 , "processor17" )
155+ incr_link = ProcessorLink (9 , 17 , "processor18" )
156+ config_link = ProcessorLink (9 , 16 , "processor19" )
157+
158+ add_with_label (
159+ schem ,
160+ simple_block (Content .MESSAGE , 11 , 19 ),
161+ left = "REGISTERS" ,
162+ right = "ERROR_OUTPUT" ,
163+ )
164+ add_with_label (
165+ schem ,
166+ Block (Content .SWITCH , 11 , 18 , False , 0 ),
167+ left = "CSRS" ,
168+ right = "POWER_SWITCH" ,
169+ )
170+ add_with_label (
171+ schem ,
172+ Block (Content .SWITCH , 11 , 17 , False , 0 ),
173+ left = "INCR" ,
174+ right = "PAUSE_SWITCH" ,
175+ )
176+ add_with_label (
177+ schem ,
178+ Block (Content .SWITCH , 11 , 16 , False , 0 ),
179+ left = "CONFIG" ,
180+ right = "SINGLE_STEP_SWITCH" ,
181+ )
182+
183+ error_output_link = ProcessorLink (11 , 19 , "message1" )
184+ power_switch_link = ProcessorLink (11 , 18 , "switch2" )
185+ pause_switch_link = ProcessorLink (11 , 17 , "switch3" )
186+ single_step_switch_link = ProcessorLink (11 , 16 , "switch1" )
187+
188+ # hack
189+ if cpu_only :
190+ schem = Schematic ()
191+
192+ schem .add_block (
193+ Block (
194+ block = Content .WORLD_PROCESSOR ,
195+ x = 16 ,
196+ y = 0 ,
197+ config = ProcessorConfig (
198+ code = controller_code ,
199+ links = relative_links (
200+ * lookup_links ,
201+ * uart_links ,
202+ labels_link ,
203+ costs_link ,
204+ csrs_link ,
205+ incr_link ,
206+ config_link ,
207+ error_output_link ,
208+ single_step_switch_link ,
209+ power_switch_link ,
210+ pause_switch_link ,
211+ display_link ,
212+ x = 16 ,
213+ y = 0 ,
214+ ),
215+ ),
216+ rotation = 0 ,
217+ )
218+ )
219+
220+ controller_link = ProcessorLink (16 , 0 , "processor20" )
221+
222+ for x in lenrange (16 , width ):
223+ for y in lenrange (0 , height ):
224+ # controller
225+ if x == 16 and y == 0 :
226+ continue
227+
228+ schem .add_block (
229+ Block (
230+ block = Content .WORLD_PROCESSOR ,
231+ x = x ,
232+ y = y ,
233+ config = ProcessorConfig (
234+ code = worker_code ,
235+ links = relative_links (
236+ * lookup_links ,
237+ * uart_links ,
238+ labels_link ,
239+ costs_link ,
240+ registers_link ,
241+ csrs_link ,
242+ incr_link ,
243+ config_link ,
244+ controller_link ,
245+ error_output_link ,
246+ single_step_switch_link ,
247+ display_link ,
248+ x = x ,
249+ y = y ,
250+ ),
251+ ),
252+ rotation = 0 ,
253+ )
254+ )
255+
256+ if output :
257+ schem .write_file (str (output ))
258+ else :
259+ schem .write_clipboard ()
260+
261+
262+ def lenrange (start : int , length : int , step : int = 1 ):
263+ return range (start , start + length , step )
264+
265+
266+ def simple_block (block : Content | BEContent , x : int , y : int ):
267+ return Block (block , x , y , None , 0 )
268+
269+
270+ def relative_links (* links : ProcessorLink , x : int , y : int ):
271+ return [ProcessorLink (link .x - x , link .y - y , link .name ) for link in links ]
272+
273+
274+ class Labels (TypedDict , total = False ):
275+ up : str
276+ left : str
277+ right : str
278+ down : str
279+
280+
281+ def add_label (schem : Schematic , x : int , y : int , ** labels : Unpack [Labels ]):
282+ label = "\n " .join (
283+ template .format (label )
284+ for key , template in [
285+ ("up" , "{} ⇧" ),
286+ ("left" , "⇦ {}" ),
287+ ("right" , "{} ⇨" ),
288+ ("down" , "{} ⇩" ),
289+ ]
290+ if (label := labels .get (key ))
291+ )
292+ schem .add_block (
293+ Block (
294+ block = Content .MESSAGE ,
295+ x = x ,
296+ y = y ,
297+ config = label ,
298+ rotation = 0 ,
299+ )
300+ )
301+
302+
303+ def add_with_label (schem : Schematic , block : Block , ** labels : Unpack [Labels ]):
304+ add_label (schem , block .x - 1 , block .y , ** labels )
305+ schem .add_block (block )
306+
307+
81308def create_jinja_env (template_dir : Path ):
82309 env = Environment (
83310 loader = FileSystemLoader (template_dir ),
@@ -95,6 +322,26 @@ def create_jinja_env(template_dir: Path):
95322 return env
96323
97324
325+ def render_template (path : Path , output : Path | None , ** kwargs : Any ):
326+ env = create_jinja_env (path .parent )
327+ template = env .get_template (path .name )
328+ result = template .render (** kwargs )
329+
330+ if output :
331+ output .parent .mkdir (parents = True , exist_ok = True )
332+ output .write_text (result , "utf-8" )
333+ else :
334+ print (result )
335+
336+ return result
337+
338+
339+ def get_template_output_path (path : Path ):
340+ if path .suffix != ".jinja" :
341+ raise ValueError (f"Expected .jinja suffix, but got { path .suffix } : { path } " )
342+ return path .with_suffix ("" )
343+
344+
98345class ConfigsYaml (TypedDict ):
99346 template : str
100347 defaults : dict [str , Any ]
0 commit comments