1pub mod ast;
11pub mod diagnostic;
12pub mod eval;
13pub mod geom;
14pub mod ir;
15pub mod lexer;
16mod math;
17pub mod parser;
18pub mod svg;
19pub mod token;
20
21pub use ast::{IncludeCtx, IncludePolicy};
22pub use diagnostic::{CompileError, Diagnostic, Span};
23pub use eval::{EvalError, eval};
24pub use ir::Drawing;
25pub use lexer::{LexError, lex};
26pub use math::{MathSpan, set_math_renderer};
27pub use parser::{ParseError, parse, parse_in_dir, parse_with_prelude};
28pub use svg::to_svg;
29pub use token::Token;
30
31pub const CIRCUITS: &str = include_str!("std/circuits.pic");
34
35pub fn compile(src: &str) -> Result<Drawing, String> {
37 compile_in_dir(src, None)
38}
39
40pub fn compile_in_dir(src: &str, base: Option<&std::path::Path>) -> Result<Drawing, String> {
42 let picture = parser::parse_in_dir(src, base).map_err(|e| e.to_string())?;
43 eval(&picture).map_err(|e| e.to_string())
44}
45
46pub fn render_svg(src: &str) -> Result<String, String> {
48 Ok(to_svg(&compile(src)?))
49}
50
51pub fn render_svg_in_dir(src: &str, base: Option<&std::path::Path>) -> Result<String, String> {
53 Ok(to_svg(&compile_in_dir(src, base)?))
54}
55
56pub fn animations_json(d: &Drawing) -> String {
59 let mut s = String::from("[");
60 for (i, a) in d.anims.iter().enumerate() {
61 if i > 0 {
62 s.push(',');
63 }
64 s.push_str(&format!(
65 "{{\"id\":\"s{}\",\"effect\":\"{}\",\"start\":{},\"duration\":{}}}",
66 a.shape,
67 json_str(&a.effect),
68 a.start,
69 a.duration
70 ));
71 }
72 s.push(']');
73 s
74}
75
76pub fn objects_json(d: &Drawing) -> String {
83 let geoms = svg::object_geometries(d);
84 let mut s = String::from("[");
85 for (i, g) in geoms.iter().enumerate() {
86 if i > 0 {
87 s.push(',');
88 }
89 s.push_str(&format!("{{\"id\":\"s{}\",\"kind\":\"{}\"", i, g.kind));
90 match g.bbox {
91 Some((x, y, w, h)) => s.push_str(&format!(
92 ",\"bbox\":{{\"x\":{},\"y\":{},\"w\":{},\"h\":{}}}",
93 json_num(x),
94 json_num(y),
95 json_num(w),
96 json_num(h)
97 )),
98 None => s.push_str(",\"bbox\":null"),
99 }
100 if let Some(span) = d.shape_spans.get(i).and_then(|s| s.as_ref()) {
101 s.push_str(&format!(
102 ",\"line\":{},\"col\":{},\"end_col\":{}",
103 span.line, span.col, span.end_col
104 ));
105 if let Some(f) = &span.file {
106 s.push_str(&format!(",\"file\":\"{}\"", json_str(f)));
107 }
108 }
109 s.push('}');
110 }
111 s.push(']');
112 s
113}
114
115fn json_num(x: f64) -> String {
117 let v = if x.is_finite() { x } else { 0.0 };
118 let s = format!("{v:.4}");
119 let s = s.trim_end_matches('0').trim_end_matches('.');
120 if s.is_empty() || s == "-" {
121 "0".into()
122 } else {
123 s.into()
124 }
125}
126
127pub fn diagnostics_json(d: &Drawing) -> String {
129 let mut s = String::from("[");
130 for (i, line) in d.diagnostics.iter().enumerate() {
131 if i > 0 {
132 s.push(',');
133 }
134 s.push('"');
135 s.push_str(&json_str(line));
136 s.push('"');
137 }
138 s.push(']');
139 s
140}
141
142#[derive(Debug, Clone, Default)]
148pub struct CompileOptions {
149 pub circuits: bool,
151 pub texlabels: bool,
154 pub base: Option<std::path::PathBuf>,
156 pub includes: IncludePolicy,
160}
161
162pub fn compile_with_options(src: &str, opts: &CompileOptions) -> Result<Drawing, String> {
164 compile_with_diagnostics(src, opts).map_err(|e| e.message)
165}
166
167pub fn compile_with_diagnostics(src: &str, opts: &CompileOptions) -> Result<Drawing, CompileError> {
171 let picture = parse_options(src, opts).map_err(|e| CompileError {
172 message: e.to_string(),
173 info: Box::new(e.diagnostic()),
174 })?;
175 eval(&picture).map_err(|e| {
176 let info = e
180 .info
181 .clone()
182 .unwrap_or_else(|| Box::new(Diagnostic::new("eval", e.msg.clone())));
183 CompileError {
184 message: e.msg,
185 info,
186 }
187 })
188}
189
190pub fn render_svg_with_options(src: &str, opts: &CompileOptions) -> Result<String, String> {
192 Ok(to_svg(&compile_with_options(src, opts)?))
193}
194
195fn parse_options(src: &str, opts: &CompileOptions) -> Result<ast::Picture, ParseError> {
196 parser::parse_with_prelude(
197 src,
198 ast::IncludeCtx::with_policy(opts.base.clone(), opts.includes),
199 opts.circuits,
200 opts.texlabels,
201 )
202}
203
204pub fn compile_json(src: &str) -> String {
209 compile_json_with_options(src, &CompileOptions::default())
210}
211
212pub fn compile_json_in_dir(src: &str, base: Option<&std::path::Path>) -> String {
215 compile_json_with_options(
216 src,
217 &CompileOptions {
218 base: base.map(|p| p.to_path_buf()),
219 ..Default::default()
220 },
221 )
222}
223
224pub fn compile_json_with_options(src: &str, opts: &CompileOptions) -> String {
228 match compile_with_diagnostics(src, opts) {
229 Ok(d) => drawing_json(&d),
230 Err(e) => error_json(&e.message, &e.info),
231 }
232}
233
234fn drawing_json(d: &Drawing) -> String {
235 format!(
236 "{{\"svg\":\"{}\",\"animations\":{},\"diagnostics\":{},\"warnings\":{},\"objects\":{}}}",
237 json_str(&to_svg(d)),
238 animations_json(d),
239 diagnostics_json(d),
240 diagnostics_json_structured(&d.warnings),
241 objects_json(d)
242 )
243}
244
245fn error_json(message: &str, diagnostic: &Diagnostic) -> String {
246 format!(
247 "{{\"error\":\"{}\",\"error_info\":{},\"warnings\":[]}}",
248 json_str(message),
249 diagnostic_json(diagnostic)
250 )
251}
252
253fn diagnostics_json_structured(items: &[Diagnostic]) -> String {
254 let mut s = String::from("[");
255 for (i, d) in items.iter().enumerate() {
256 if i > 0 {
257 s.push(',');
258 }
259 s.push_str(&diagnostic_json(d));
260 }
261 s.push(']');
262 s
263}
264
265fn diagnostic_json(d: &Diagnostic) -> String {
266 let mut s = String::from("{");
267 s.push_str(&format!("\"message\":\"{}\"", json_str(&d.message)));
268 s.push_str(&format!(",\"line\":{}", json_opt_u32(d.line)));
269 s.push_str(&format!(",\"col\":{}", json_opt_u32(d.col)));
270 s.push_str(&format!(",\"end_col\":{}", json_opt_u32(d.end_col)));
271 s.push_str(&format!(",\"file\":{}", json_opt_str(d.file.as_deref())));
272 s.push_str(&format!(",\"kind\":\"{}\"", json_str(&d.kind)));
273 s.push_str(&format!(",\"found\":{}", json_opt_str(d.found.as_deref())));
274 s.push_str(&format!(
275 ",\"expected\":{}",
276 json_opt_str(d.expected.as_deref())
277 ));
278 s.push_str(&format!(",\"hint\":{}", json_opt_str(d.hint.as_deref())));
279 s.push('}');
280 s
281}
282
283fn json_opt_u32(v: Option<u32>) -> String {
284 v.map(|n| n.to_string()).unwrap_or_else(|| "null".into())
285}
286
287fn json_opt_str(v: Option<&str>) -> String {
288 v.map(|s| format!("\"{}\"", json_str(s)))
289 .unwrap_or_else(|| "null".into())
290}
291
292fn json_str(s: &str) -> String {
294 let mut o = String::with_capacity(s.len() + 8);
295 for c in s.chars() {
296 match c {
297 '"' => o.push_str("\\\""),
298 '\\' => o.push_str("\\\\"),
299 '\n' => o.push_str("\\n"),
300 '\r' => o.push_str("\\r"),
301 '\t' => o.push_str("\\t"),
302 c if (c as u32) < 0x20 => o.push_str(&format!("\\u{:04x}", c as u32)),
303 c => o.push(c),
304 }
305 }
306 o
307}
308
309#[cfg(test)]
310mod tests {
311 use super::*;
312
313 #[test]
314 fn json_bundles_svg_and_animations() {
315 let j = compile_json("box\nanimate last box with \"fade\"");
316 assert!(j.starts_with("{\"svg\":\"<svg"));
317 assert!(j.contains("<g id=\\\"s0\\\">")); assert!(j.contains("\"animations\":[{\"id\":\"s0\",\"effect\":\"fade\""));
319 assert!(j.contains("\"diagnostics\":[]"));
320 }
321
322 #[test]
323 fn json_exports_object_geometry() {
324 let j = compile_json("box wid 1 ht 0.5\narrow right 0.5");
327 assert!(
328 j.contains("\"objects\":[{\"id\":\"s0\",\"kind\":\"box\",\"bbox\":{"),
329 "{j}"
330 );
331 assert!(j.contains("\"w\":96,\"h\":48"), "{j}");
333 assert!(j.contains("\"kind\":\"path\""), "{j}");
334 assert!(j.contains("\"line\":2,\"col\":1"), "{j}");
335 }
336
337 #[test]
338 fn json_object_geometry_marks_invisible_shapes() {
339 let j = compile_json("box invis \"x\"");
340 assert!(j.contains("\"kind\":\"box\",\"bbox\":null"), "{j}");
342 }
343
344 #[test]
345 fn json_object_geometry_names_library_sources() {
346 let opts = CompileOptions {
349 circuits: true,
350 ..Default::default()
351 };
352 let j = compile_json_with_options("A:(0,0); B:(2,0)\nresistor(A,B)", &opts);
353 assert!(j.contains("\"file\":\"circuits\""), "{j}");
354 }
355
356 #[test]
357 fn json_object_geometry_matches_svg_rect() {
358 let d = compile("box wid 1 ht 0.5").unwrap();
360 let svg = to_svg(&d);
361 let g = svg::object_geometries(&d);
362 let (x, y, w, h) = g[0].bbox.unwrap();
363 let rect = svg.lines().find(|l| l.contains("<rect")).unwrap();
364 for (attr, v) in [("x", x), ("y", y), ("width", w), ("height", h)] {
365 let needle = format!("{attr}=\"");
366 let i = rect.find(&needle).unwrap() + needle.len();
367 let s = &rect[i..rect[i..].find('"').unwrap() + i];
368 let got: f64 = s.parse().unwrap();
369 assert!((got - v).abs() < 1e-3, "{attr}: rect {got} vs bbox {v}");
370 }
371 }
372
373 #[test]
374 fn json_reports_errors() {
375 let j = compile_json("copy \"oops\"");
376 assert!(j.contains("\"error\""));
377 assert!(j.contains("\"error_info\""));
378 }
379
380 #[test]
381 fn json_reports_print_diagnostics() {
382 let j = compile_json("print \"hi\"\nprint 2+3");
383 assert!(j.contains("\"diagnostics\":[\"hi\",\"5\"]"));
384 }
385
386 #[test]
387 fn copy_circuits_loads_the_embedded_library() {
388 let body = "A:(0,0); B:(2,0)\nresistor(A,B)";
392 let via_copy = compile(&format!("copy \"circuits\"\n{body}")).unwrap();
393 let via_flag = compile(&format!("{CIRCUITS}\n{body}")).unwrap();
394 assert_eq!(to_svg(&via_copy), to_svg(&via_flag));
395 }
396
397 #[test]
398 fn copy_circuits_is_idempotent_with_the_flag() {
399 let body = "A:(0,0); B:(2,0)\nresistor(A,B)";
402 let both = compile(&format!("{CIRCUITS}\ncopy \"circuits\"\n{body}")).unwrap();
403 let flag = compile(&format!("{CIRCUITS}\n{body}")).unwrap();
404 assert_eq!(to_svg(&both), to_svg(&flag));
405 }
406
407 #[test]
408 fn options_preludes_do_not_shift_user_positions() {
409 let opts = CompileOptions {
412 circuits: true,
413 texlabels: true,
414 ..Default::default()
415 };
416 let j = compile_json_with_options("bxo\n", &opts);
417 assert!(j.contains("\"line\":1"), "{j}");
418 assert!(j.contains("\"col\":1"), "{j}");
419 assert!(j.contains("\"file\":null"), "{j}");
420 assert!(j.contains("\"error\":\"1:1: expected an object"), "{j}");
421 }
422
423 #[test]
424 fn options_output_matches_the_prepending_it_replaces() {
425 let body = "A:(0,0); B:(2,0)\nresistor(A,B)";
428 let opts = CompileOptions {
429 circuits: true,
430 ..Default::default()
431 };
432 let via_opts = compile_with_options(body, &opts).unwrap();
433 let via_prepend = compile(&format!("{CIRCUITS}\n{body}")).unwrap();
434 assert_eq!(to_svg(&via_opts), to_svg(&via_prepend));
435 }
436
437 #[test]
438 fn options_texlabels_is_an_initializer_only() {
439 let opts = CompileOptions {
441 texlabels: true,
442 ..Default::default()
443 };
444 let j = compile_json_with_options("texlabels = 0\nbox \"$x$\"\n", &opts);
445 assert!(!j.contains("no math renderer"), "{j}");
446 }
447
448 #[test]
449 fn diagnostics_inside_an_include_name_the_file() {
450 let dir = std::env::temp_dir().join(format!("rpic_incl_diag_{}", std::process::id()));
453 std::fs::create_dir_all(&dir).unwrap();
454 std::fs::write(dir.join("warn.pic"), "# comment\nbox \"a\" dashd\n").unwrap();
455 let j = compile_json_in_dir("circle\ncopy \"warn.pic\"", Some(dir.as_path()));
456 assert!(j.contains("\"kind\":\"ignored_attribute\""), "{j}");
457 assert!(j.contains("\"file\":\"warn.pic\""), "{j}");
458 assert!(j.contains("\"line\":2"), "{j}"); std::fs::write(dir.join("bad.pic"), "bxo\n").unwrap();
461 let e = compile_json_in_dir("circle\ncopy \"bad.pic\"", Some(dir.as_path()));
462 let _ = std::fs::remove_dir_all(&dir);
463 assert!(
464 e.contains("\"error\":\"bad.pic:1:1: expected an object"),
465 "{e}"
466 );
467 assert!(e.contains("\"file\":\"bad.pic\""), "{e}");
468 assert!(e.contains("\"line\":1"), "{e}");
469 }
470
471 #[test]
472 fn include_policy_sandboxes_and_denies() {
473 let root = std::env::temp_dir().join(format!("rpic_inc_policy_{}", std::process::id()));
475 let base = root.join("base");
476 std::fs::create_dir_all(&base).unwrap();
477 std::fs::write(root.join("outside.pic"), "circle\n").unwrap();
478 std::fs::write(base.join("inc.pic"), "box wid 0.5 ht 0.5\n").unwrap();
479
480 let sandboxed = CompileOptions {
481 base: Some(base.clone()),
482 includes: IncludePolicy::SandboxedToBase,
483 ..Default::default()
484 };
485 assert!(compile_with_options("copy \"inc.pic\"\nbox", &sandboxed).is_ok());
487 let esc = compile_json_with_options("copy \"../outside.pic\"\nbox", &sandboxed);
489 assert!(esc.contains("\"kind\":\"include_denied\""), "{esc}");
490 assert!(esc.contains("outside the include base directory"), "{esc}");
491 assert!(esc.contains("\"line\":1"), "{esc}");
492 let abs_src = format!("copy \"{}\"\nbox", root.join("outside.pic").display());
494 let abs = compile_json_with_options(&abs_src, &sandboxed);
495 assert!(abs.contains("\"kind\":\"include_denied\""), "{abs}");
496 assert!(abs.contains("absolute paths are not allowed"), "{abs}");
497 #[cfg(unix)]
499 {
500 let link = base.join("link.pic");
501 let _ = std::fs::remove_file(&link);
502 std::os::unix::fs::symlink(root.join("outside.pic"), &link).unwrap();
503 let sym = compile_json_with_options("copy \"link.pic\"\nbox", &sandboxed);
504 assert!(sym.contains("\"kind\":\"include_denied\""), "{sym}");
505 }
506 assert!(
508 compile_with_options(
509 "copy \"circuits\"\nA:(0,0); B:(1,0)\nresistor(A,B)",
510 &sandboxed
511 )
512 .is_ok()
513 );
514
515 let deny = CompileOptions {
516 base: Some(base.clone()),
517 includes: IncludePolicy::Deny,
518 ..Default::default()
519 };
520 let d = compile_json_with_options("copy \"inc.pic\"\nbox", &deny);
521 assert!(d.contains("\"kind\":\"include_denied\""), "{d}");
522 assert!(d.contains("disabled by the include policy"), "{d}");
523 assert!(compile_with_options("copy \"circuits\"\nbox", &deny).is_ok());
524
525 let open = CompileOptions {
527 base: Some(base.clone()),
528 ..Default::default()
529 };
530 assert!(compile_with_options("copy \"../outside.pic\"\nbox", &open).is_ok());
531
532 let _ = std::fs::remove_dir_all(&root);
533 }
534
535 #[test]
536 fn json_in_dir_resolves_copy_includes() {
537 let dir = std::env::temp_dir().join(format!("rpic_json_copy_{}", std::process::id()));
538 std::fs::create_dir_all(&dir).unwrap();
539 std::fs::write(dir.join("inc.pic"), "box wid 0.5 ht 0.5\n").unwrap();
540
541 let j = compile_json_in_dir("copy \"inc.pic\"\ncircle", Some(dir.as_path()));
542 let _ = std::fs::remove_dir_all(&dir);
543
544 assert!(j.starts_with("{\"svg\":\"<svg"), "{j}");
545 assert!(j.contains("<rect"), "{j}");
546 assert!(j.contains("<circle"), "{j}");
547 assert!(j.contains("\"diagnostics\":[]"), "{j}");
548 assert!(!j.contains("\"error\""), "{j}");
549 }
550
551 #[test]
552 fn json_error_info_uses_user_facing_tokens_and_spans() {
553 let j = compile_json("bxo\n");
554
555 assert!(
556 j.contains("\"error\":\"1:1: expected an object, found `bxo`\""),
557 "{j}"
558 );
559 assert!(j.contains("\"kind\":\"expected_token\""), "{j}");
560 assert!(j.contains("\"line\":1"), "{j}");
561 assert!(j.contains("\"col\":1"), "{j}");
562 assert!(j.contains("\"end_col\":4"), "{j}");
563 assert!(j.contains("\"found\":\"`bxo`\""), "{j}");
564 assert!(j.contains("\"expected\":\"an object\""), "{j}");
565 assert!(j.contains("\"hint\":\"did you mean `box`?\""), "{j}");
566 }
567
568 #[test]
569 fn json_unterminated_string_points_at_opening_quote() {
570 let j = compile_json("box wid 1\n \"oops\n");
571
572 assert!(j.contains("\"kind\":\"unterminated_string\""), "{j}");
573 assert!(j.contains("\"line\":2"), "{j}");
574 assert!(j.contains("\"col\":3"), "{j}");
575 }
576
577 #[test]
578 fn json_eval_errors_get_structured_locations_when_possible() {
579 let label = compile_json("box at A\n");
580 assert!(label.contains("\"kind\":\"unknown_label\""), "{label}");
581 assert!(label.contains("\"line\":1"), "{label}");
582 assert!(label.contains("\"col\":8"), "{label}");
583 assert!(label.contains("\"found\":\"A\""), "{label}");
584
585 let ordinal = compile_json("box\nbox at 3rd box\n");
586 assert!(
587 ordinal.contains("\"kind\":\"ordinal_out_of_range\""),
588 "{ordinal}"
589 );
590 assert!(ordinal.contains("\"line\":2"), "{ordinal}");
591 assert!(ordinal.contains("\"col\":8"), "{ordinal}");
592 assert!(ordinal.contains("\"found\":\"3\""), "{ordinal}");
593 assert!(ordinal.contains("\"expected\":\"1..1\""), "{ordinal}");
594 }
595
596 #[test]
597 fn json_eval_error_inside_include_names_the_file() {
598 let dir = std::env::temp_dir().join(format!("rpic_eval_incl_{}", std::process::id()));
601 std::fs::create_dir_all(&dir).unwrap();
602 std::fs::write(dir.join("inc-eval.pic"), "# comment\nbox at Missing\n").unwrap();
603 let j = compile_json_in_dir("circle\ncopy \"inc-eval.pic\"", Some(dir.as_path()));
604 let _ = std::fs::remove_dir_all(&dir);
605 assert!(j.contains("\"error\":\"unknown label `Missing`\""), "{j}");
606 assert!(j.contains("\"kind\":\"unknown_label\""), "{j}");
607 assert!(j.contains("\"file\":\"inc-eval.pic\""), "{j}");
608 assert!(j.contains("\"line\":2"), "{j}"); assert!(j.contains("\"col\":8"), "{j}");
610 }
611
612 #[test]
613 fn json_deferred_parse_errors_keep_their_structure() {
614 let j = compile_json("x = 1\nif x then { bxo }\n");
617 assert!(j.contains("\"kind\":\"expected_token\""), "{j}");
618 assert!(j.contains("\"line\":2"), "{j}");
619 assert!(j.contains("\"col\":13"), "{j}");
620 assert!(j.contains("\"found\":\"`bxo`\""), "{j}");
621 assert!(j.contains("\"hint\":\"did you mean `box`?\""), "{j}");
622
623 let f = compile_json("for i = 1 to 2 do { bxo }\n");
624 assert!(f.contains("\"kind\":\"expected_token\""), "{f}");
625 assert!(f.contains("\"hint\":\"did you mean `box`?\""), "{f}");
626 }
627
628 #[test]
629 fn json_success_bundle_reports_warnings() {
630 let attr = compile_json("box \"a\" dashd\n");
631 assert!(attr.contains("\"warnings\":[{"), "{attr}");
632 assert!(attr.contains("\"kind\":\"ignored_attribute\""), "{attr}");
633 assert!(attr.contains("\"found\":\"dashd\""), "{attr}");
634 assert!(
635 attr.contains("\"hint\":\"did you mean `dashed`?\""),
636 "{attr}"
637 );
638
639 let anim = compile_json("box\nanimate 1st box with \"zoom\"\n");
640 assert!(
641 anim.contains("\"kind\":\"unknown_animation_effect\""),
642 "{anim}"
643 );
644 assert!(anim.contains("\"found\":\"zoom\""), "{anim}");
645 assert!(anim.contains("\"line\":2"), "{anim}");
646 }
647
648 #[test]
649 fn circuits_library_compiles_and_draws() {
650 let src = format!(
651 "{}\nA:(0,0); B:(1.6,0); C:(3,0)\nresistor(A,B)\ncapacitor(A,B)\ninductor(A,B)\n\
652 diode(A,B)\nbattery(A,B)\nground(A)\ndot(A)\nwire(A,B)\n\
653 and_gate(C)\nor_gate(C)\nnand_gate(C)\nnor_gate(C)\nxor_gate(C)\n\
654 xnor_gate(C)\nbuffer(C)\nnot_gate(C)\n\
655 opamp(C)\nnpn(C)\npnp(C)\nac_source(A,B)\nswitch(A,B)\n\
656 potentiometer(A,B)\ntransformer(C)\n\
657 nmos(C)\npmos(C)\ncurrent_source(A,B)\nvsource_ctrl(A,B)\n\
658 isource_ctrl(A,B)\nled(A,B)\nphotodiode(A,B)\nzener(A,B)\n\
659 clabel(A,\"x\")\nterminal(A)\nvdd(A)\nantenna(A)\ncurrent(A,B)\n\
660 voltage(A,B)\nlamp(A,B)\nfuse(A,B)\n\
661 voltmeter(A,B)\nammeter(A,B)\nohmmeter(A,B)\nmeter(A,B,\"X\")\n\
662 crystal(A,B)\nspeaker(A,B)\nhop(A,B)\nspdt(A,B,C)\n\
663 iec_resistor(A,B)\nvoltage_source(A,B)\npushbutton(A,B)\nrelay(A,B)\n\
664 thermistor(A,B)\nmotor(A,B)\ngenerator(A,B)\nbell(A,B)\n\
665 ieee_and(C)\nieee_or(C)\nieee_xor(C)\nieee_buf(C)\niecgate(C,\"x\")\n\
666 varistor(A,B)\ntline(A,B)\nbus(A,B)\n\
667 polcap(A,B)\nvarcap(A,B)\nvarind(A,B)\nschottky(A,B)\nldr(A,B)\n\
668 spark_gap(A,B)\nchassis_ground(A)\nsignal_ground(A)\n\
669 njfet(C)\npjfet(C)\nphototransistor(C)\nic_block(C,\"x\")\nmux(C)\n\
670 solar_cell(A,B)\nmicrophone(A,B)\nthermocouple(A,B)",
671 CIRCUITS
672 );
673 let d = compile(&src).expect("circuit library should compile");
674 assert!(!d.shapes.is_empty());
675 }
676}