본문으로 건너뛰기
code0.HDL

하드웨어에서 출력하기

printf와 cf 보간으로 회로 안에서 값을 출력하고, 하드웨어 출력과 elaboration 시점의 println을 구분하며, 컴파일러가 val에 붙이는 신호 이름을 읽습니다.

이 장에서 배울 내용

  • 모듈 안에 printf를 두고 어느 사이클에 출력되는지 말할 수 있습니다
  • cf로 Vec과 Bundle을 포함한 하드웨어 값을 형식화할 수 있습니다
  • elaboration의 println과 시뮬레이션의 printf를 구분하고 suggestName으로 신호 이름을 정할 수 있습니다
  • 형식 지정자마다 신호 폭에 따라 정해지는 필드 폭을 포함해 cf 형식이 출력할 문자열을 정확히 예측할 수 있습니다
이 장에서 배우는 개념
  • 하드웨어에서 출력하기
먼저 알아야 할 개념

테스트벤치는 모듈을 포트로만 관찰합니다. 모듈 안쪽 레지스터의 값이 틀렸을 때 디버그 포트를 새로 만들지 않고 그 값을 확인하려면 회로가 직접 값을 출력해야 합니다. Chisel의 printf는 시뮬레이션 회로에 들어가는 출력문입니다. 이 장은 공식 Chisel 문서 Explanations의 "Printing"과, 생성된 회로의 신호 이름을 정하는 "Naming"의 일부를 다룹니다.

먼저 알아 두면 좋은 것. 이 장은 다형성과 매개변수화 다음 장입니다. 예제는 레지스터와 step 장의 레지스터와 RegNext, Bundle과 Vec 장의 Bundle과 VecInit, 그리고 when 블록을 사용합니다. 지금까지 모든 Tb.scala에서 써 온 println이 printf와 어떻게 다른지도 이 장에서 설명합니다.

1. 모듈 안의 printf

1.1 출력문도 하드웨어입니다

모듈 본문에서 printf를 호출해도 그 Scala 줄이 실행되는 순간에는 아무것도 출력되지 않습니다. printf 호출은 +나 RegNext처럼 회로에 노드 하나를 추가합니다. 이 노드는 모듈의 클록에 연결되며 형식 문자열, 출력할 신호 목록, enable 조건을 가집니다. 시뮬레이터는 상승 에지마다 enable 조건을 확인하고, 조건이 참이면 신호 값을 형식에 맞춰 메시지 한 줄을 출력합니다.

형식

printf(cf"문자열 $신호%지정자 ${식}%지정자\n")

다음 모듈은 매 사이클 증가하는 카운터 값을 출력합니다.

class Ticker extends Module {
  val io = IO(new Bundle { val in = Input(UInt(4.W)) })
  val count = RegInit(0.U(4.W))
  count := count + 1.U
  printf(cf"count=$count in=${io.in}\n")
}
코드의미
printf(...)모듈의 암묵적 클록에 연결된 출력 노드를 추가합니다
cf"..."문자열과 하드웨어 값으로 메시지를 구성합니다
$count레지스터 값을 넣습니다. 지정자가 없으므로 10진수입니다
${io.in}경로나 식을 넣을 때는 중괄호로 감쌉니다
\n줄을 끝냅니다. printf는 줄바꿈을 자동으로 붙이지 않습니다

에지에서 출력되는 값은 그 에지가 레지스터를 갱신하기 직전의 값입니다. Ticker의 첫 메시지는 count=0이고, 메시지가 출력된 뒤에 count가 1이 됩니다.

확인 문제

Tb.scala가 in = 3을 인가하고 리셋 뒤에 Ticker를 세 번 step합니다. 세 번째 메시지에 나오는 count 값은 무엇일까요?

답

참고. printf는 시뮬레이션용 디버깅 수단입니다. 칩의 논리로 만들어지지 않으며 회로가 계산하는 값에도 영향을 주지 않습니다.

1.2 출력 조건

printf가 어떤 에지에서 출력할지는 두 조건이 결정합니다.

  1. 감싸는 when 블록. when (c) 안의 printf는 c가 참인 에지에서만 출력합니다.
  2. 리셋. Chisel은 모든 printf에 "리셋 중이 아님" 조건을 암묵적으로 추가합니다. reset이 1인 동안에는 모듈이 아무것도 출력하지 않으므로 초기화 사이클의 출력이 로그에 섞이지 않습니다.

형식

when (조건) {
  printf(cf"...\n")
}
when (io.valid && io.ready) {
  printf(cf"accepted ${io.data}%x\n")
}

이 printf는 전송이 성립한 사이클마다 한 줄을 출력하고 나머지 사이클에는 출력하지 않습니다. 관심 있는 사건으로 출력을 제한하면 긴 실행에서도 로그를 읽기 쉽습니다.

에지의 상황출력 여부
reset이 1모듈의 모든 printf가 출력하지 않습니다
reset이 0, 감싸는 when 없음출력합니다
reset이 0, 감싸는 when 조건이 거짓출력하지 않습니다
reset이 0, 감싸는 when 조건이 참출력합니다

확인 문제

위의 accepted printf가 모듈 안에 있습니다. 테스트벤치가 리셋을 두 step 동안 1로 두고, valid를 1로 올린 뒤 ready를 1, 0, 1, 1로 바꾸며 네 번 step합니다. accepted 줄은 몇 번 나올까요?

답

1.3 출력할 수 있는 값

printf에는 범위 안의 모든 하드웨어 값을 넣을 수 있습니다. 포트, wire, 레지스터, 그리고 이들로 만든 식이 해당합니다. 반면 테스트 중에 바뀌는 Scala var는 따라가지 못합니다. 모듈 본문은 elaboration 중에 한 번만 실행되므로 형식 문자열에 들어간 Scala 값은 회로를 만들 때 고정됩니다.

2. 값의 출력 형식

2.1 cf interpolator

cf"..."는 Scala 문자열 interpolator와 모양이 같지만 문자열이 아니라 Printable을 만듭니다. Printable은 하드웨어 노드를 참조하는 메시지 기술입니다. $이름이나 ${식} 바로 뒤에 형식 지정자를 붙일 수 있고, 지정자가 없으면 숫자는 10진수로 출력됩니다. 퍼센트 기호 자체를 출력하려면 %%를 씁니다.

형식

cf"label=$signal%x"        // 값 하나, 16진수
cf"sum=${a + b}%d"         // 식, 10진수
cf"load=$pct%%"            // 값 뒤에 % 문자

2.2 형식 지정자

지정자출력 형태8비트 값 65의 출력
%d 또는 생략10진수공백으로 오른쪽 정렬한 65
%x16진수, 앞을 0으로 채움41
%b2진수, 공백으로 오른쪽 정렬공백 하나 뒤의 1000001
%c값을 ASCII 문자로 출력A

이 실습 환경의 시뮬레이터는 신호 폭에 따라 필드 폭을 고정합니다. 그래서 사이클이 바뀌어도 열이 정렬됩니다.

신호%d 필드%x 필드%b 필드
UInt(8.W)공백 포함 4칸0으로 채운 2자리공백 포함 8칸
UInt(4.W)공백 포함 3칸1자리공백 포함 4칸
Bool공백 포함 2칸1자리1칸

따라서 8비트 값 7은 %d로 7, %x로 07이 됩니다. 부호 있는 값은 10진수에서 부호와 함께 출력됩니다. -3을 담은 SInt(8.W)는 %d로 -3, %x로 2의 보수 비트인 fd가 출력됩니다.

주의. %c는 문자 코드를 그대로 출력합니다. 32보다 작은 값은 제어 문자이므로 화면에 보이지 않습니다. 예를 들어 7은 보이지 않는 벨 문자입니다. %c는 출력 가능한 ASCII 코드를 담은 신호에 사용하세요.

확인 문제

x는 10을 담은 UInt(4.W)입니다. 형식이 cf 뒤의 [$x%x|$x%b|$x]일 때 printf는 무엇을 출력할까요?

답

2.3 Vec와 Bundle 출력

$ 하나에 집합 타입 전체를 넣을 수 있습니다. Vec는 Vec(...) 안에 원소를 인덱스 순서로 나열합니다. Bundle은 클래스 이름 뒤에 필드 -> 값 쌍을 선언 순서대로 나열합니다.

class Pkt extends Bundle {
  val kind = UInt(2.W)
  val len  = UInt(4.W)
}
val p = Wire(new Pkt)
val v = VecInit(io.b, io.b + 1.U)
printf(cf"p=$p v=$v\n")

위 코드는 출력 형식을 보여 주는 조각이며, p.kind와 p.len을 구동하는 연결은 아직 없습니다. 두 필드를 각각 1.U와 io.b로 구동하고 io.b가 5이면, p= 뒤에는 1과 5를 담은 Pkt(kind -> ..., len -> ...)가, v= 뒤에는 5와 6을 담은 Vec(...)가 출력됩니다. 각 숫자는 %d 필드와 같은 폭으로 채워집니다.

Bundle 클래스는 toPrintable을 override해 출력 형식을 직접 정할 수 있습니다.

형식

override def toPrintable: Printable = cf"..."
class Pkt extends Bundle {
  val kind = UInt(2.W)
  val len  = UInt(4.W)
  override def toPrintable: Printable = cf"<$kind:$len%x>"
}

이렇게 정의하면 어느 모듈에서든 Pkt를 넣은 모든 cf가 이 형식을 사용합니다.

2.4 C 형식 printf

printf는 C처럼 일반 형식 문자열 뒤에 인자를 순서대로 나열하는 형태도 받습니다.

형식

printf("형식 문자열 %d %x\n", 첫째_인자, 둘째_인자)
printf("a=%d b=%x\n", io.a, io.b)

지정자와 필드 폭은 cf와 같습니다. C 형식은 형식 문자열을 다른 곳에서 만들어 받을 때 유용합니다. 직접 쓰는 메시지에는 cf가 낫습니다. cf는 값이 이름표 바로 옆에 있으므로 인자 순서가 어긋날 일이 없습니다.

2.5 예제: 모듈 안에서 값 추적하기

Tracer의 입력은 8비트 신호 하나입니다. 모듈 안에는 매 사이클 증가하는 카운터 cycle, 입력을 한 사이클 지연한 last, 현재 입력과 이전 입력을 담은 두 원소 Vec window, 입력으로 만든 Sample Bundle이 있습니다. 끝에 있는 printf 세 개를 먼저 확인하세요.

  • 조건 없이 출력하며, 형식 네 가지와 Vec, Bundle을 포함하는 cf 출력
  • when (io.in === 0.U) 안의 출력
  • io.in과 last를 출력하는 C 형식 출력
Tracer.scala
import chisel3._

class Sample extends Bundle {
  val addr = UInt(8.W)
  val ok   = Bool()
}

class Tracer extends Module {
  val io = IO(new Bundle {
    val in = Input(UInt(8.W))
  })
  val cycle = RegInit(0.U(8.W))
  cycle := cycle + 1.U
  val last = RegNext(io.in, 0.U)
  val window = VecInit(io.in, last)
  val s = Wire(new Sample)
  s.addr := io.in
  s.ok := io.in(0)

  // 상승 에지마다 한 줄을 출력하며, 에지 직전의 값을 보고한다.
  printf(cf"[$cycle%d] in=${io.in}%d hex=${io.in}%x bin=${io.in}%b win=$window s=$s\n")
  // when 안의 printf는 조건이 참인 사이클에만 출력한다.
  when (io.in === 0.U) {
    printf(cf"[$cycle] zero input\n")
  }
  // C 형식: 일반 형식 문자열 뒤에 인자를 나열한다.
  printf("C-style in=%d last=%x\n", io.in, last)
}

Tb.scala는 reset을 한 step 동안 1로 유지한 뒤 해제하고 표지 문자열을 출력합니다. 이어서 65, 0, 255를 차례로 poke하며 클록 step 직전마다 TB poke 표지를 출력합니다.

Tb.scala
import chisel3._
import chiseltest._
import chiseltest.RawTester.test

object Main extends App {
  test(new Tracer) { dut =>
    // reset이 1인 동안 printf는 출력하지 않는다.
    dut.reset.poke(true.B)
    dut.clock.step()
    dut.reset.poke(false.B)
    println("TB reset released")
    for (v <- Seq(65, 0, 255)) {
      dut.io.in.poke(v.U)
      println(s"TB poke $v")
      dut.clock.step()
    }
    println("TB done")
  }
}

이 개념 실습하기

다음 단계로 짧은 Chisel 문제에 이 개념을 적용해 보세요.

printf는 에지 직전의 값을 출력합니다. 리셋 뒤 세 step의 값은 다음과 같습니다.

stepcycleio.inlastwindow추가 출력
리셋000없음. 리셋이 모든 printf를 막습니다
10650Vec(65, 0)없음
21065Vec(0, 65)zero input
322550Vec(255, 0)없음

에지가 지나면 cycle은 1 증가하고 last는 방금 끝난 step의 입력을 저장합니다.

실행하기 전에 예측해 보세요.

  1. 각 TB poke 표지 뒤에는 다음 테스트벤치 표지와 회로 메시지 가운데 어느 것이 먼저 출력될까요?
  2. 입력이 255인 세 번째 step에서 C 형식 줄은 last를 무엇으로 출력할까요?
  3. zero input 줄은 모두 몇 개일까요?
  4. ok는 입력의 bit 0입니다. 첫 step에서 s=의 ok는 무엇일까요?

결과 확인.

출력확인할 내용
TB reset released 앞에 회로 출력이 없음리셋 step에서는 printf가 출력하지 않았습니다
각 TB poke 표지 뒤의 회로 출력메시지는 clock.step() 안에서 출력되므로 시간 순서상 표지 뒤에 나타납니다
첫 [0] 줄의 hex=41, bin= 1000001, ok -> 1한 값을 세 형식으로 출력했습니다. 65는 홀수이므로 bit 0이 1입니다
cycle 1 줄의 win=Vec(0, 65)last가 이전 사이클의 입력을 저장합니다
세 번째 C 형식 줄의 last=00직전 입력이 0이었고 %x는 두 자리로 채웁니다
cycle 1에만 있는 zero input 한 줄when 분기는 조건이 참일 때만 출력합니다. 지정자 없는 [cycle]도 10진수로 출력됩니다

직접 바꿔 보세요.

  1. Sample에 다음 override를 추가해 보세요.

    override def toPrintable: Printable = cf"@$addr%x/${Mux(ok, 'y'.U, 'n'.U)}%c"

    모든 s= 필드가 기본 형태 Sample(addr -> ..., ok -> ...)에서 짧은 형태로 바뀝니다. 입력 65는 @41/y, 0은 @00/n, 255는 @ff/y가 됩니다. override도 하드웨어입니다. Mux가 사이클마다 문자 코드 둘 가운데 하나를 고르고, %c가 고른 코드를 문자로 출력합니다.

  2. C 형식 printf를 새 when (io.in =/= 0.U) 블록 안으로 옮겨 보세요. 입력이 0인 cycle 1에서는 C 형식 줄이 사라지고, cycle 0과 cycle 2에는 그대로 남습니다.

3. println과 printf

3.1 실행 단계가 다른 두 출력

Chisel 코드는 두 단계에 걸쳐 실행됩니다. elaboration 단계에서는 Scala가 모듈 본문을 한 번 실행해 회로를 만듭니다. 시뮬레이션 단계에서는 만들어진 회로가 사이클 단위로 동작합니다. println은 일반 Scala 함수이므로 그 줄을 실행하는 단계에서 출력합니다. printf는 언제나 시뮬레이션 단계에서 출력합니다.

문장실행 단계실행 횟수출력할 수 있는 것
모듈 본문의 println(...)elaboration모듈 인스턴스마다 한 번Scala 값, 폭, 타입
테스트벤치의 println(...)테스트 실행그 줄이 실행될 때마다peek으로 읽은 값
모듈 본문의 printf(...)시뮬레이션enable이 참인 상승 에지마다하드웨어 값

형식

println(s"문자열 ${Scala_값}")   // elaboration 중 한 번
printf(cf"문자열 $하드웨어\n")     // enable이 참인 에지마다
class Widths extends Module {
  val io = IO(new Bundle { val a = Input(UInt(8.W)) })
  println(s"a is ${io.a.getWidth} bits")   // 한 번 출력: a is 8 bits
  println(s"a = ${io.a}")                  // 값이 아니라 객체 설명을 출력
}

두 번째 println은 Widths.io.a: IO[UInt<8>] 같은 객체 설명을 출력합니다. elaboration 중의 포트에는 타입과 폭은 있지만 아직 값이 없습니다.

확인 문제

Ticker 본문에 ELAB Ticker를 쓰는 println과 조건 없는 printf가 하나씩 있습니다. 테스트벤치가 모듈을 한 번 만들고 리셋을 두 step 동안 유지한 뒤 네 번 더 step합니다. ELAB 줄과 printf 줄은 각각 몇 개일까요?

답

3.2 예제: printf 안의 s"..."

Scala에 익숙하면 습관처럼 s"..."를 쓰기 쉽습니다. printf 안의 s interpolator는 회로를 만드는 동안 Chisel 객체의 toString을 호출해 그 문자열을 메시지에 고정합니다. 그러면 모든 사이클이 wire의 값 대신 같은 객체 설명을 출력합니다. Chisel은 이 형태를 찾아 Scala 컴파일 오류로 처리합니다.

다음 예제는 의도적으로 컴파일 오류를 발생시킵니다. Probe가 printf에 s"in = $in\n"을 전달합니다.

Probe.scala
import chisel3._

class Probe extends Module {
  val in = IO(Input(UInt(8.W)))
  // s-interpolator는 wire 값이 아니라 Scala 객체를 문자열로 만든다.
  printf(s"in = $in\n")
}

object Main extends App {
  println(circt.stage.ChiselStage.emitCHIRRTL(new Probe))
}

결과 확인.

확인 항목내용
잘못된 코드printf(s"in = $in\n")
실패 단계Scala 컴파일 단계입니다. 회로가 만들어지기 전이며 Main은 실행되지 않습니다
오류 메시지error: The s-interpolator prints the Scala .toString of Data objects와 두 가지 대안
고치는 방법사이클마다 하드웨어 값을 출력하려면 cf"를 씁니다. 회로를 만드는 동안 한 번만 출력하려면 s"..."와 함께 println을 씁니다

4. 신호 이름이 정해지는 방식

4.1 val 이름이 신호 이름이 됩니다

생성된 FIRRTL, Verilog, 파형은 신호를 이름으로 보여 줍니다. 모든 예제 명령의 -Xplugin 플래그가 불러오는 Chisel 컴파일러 플러그인은 하드웨어 값의 이름을 그 값을 담은 val의 이름으로 정합니다. val에 담지 않은 값은 밑줄로 시작하는 생성 이름을 받으며, 생성 이름에는 연결 대상의 이름이 들어가는 경우가 많습니다.

형식

val 이름 = 하드웨어_식
val sum = io.a + io.b
io.y := sum
io.z := io.a - io.b
소스생성된 FIRRTL의 이름
val sum = io.a + io.bsum. 내부의 전체 폭 덧셈은 _sum_T입니다
io.z := io.a - io.b연결 대상 io.z에서 만든 _io_z_T와 _io_z_T_1
이름 없는 printf 세 개printf, printf_1, printf_2

Tracer에서도 레지스터는 cycle과 last, wire는 window와 s라는 이름을 받습니다. 소스에 이름을 문자열로 적은 곳은 없습니다.

4.2 suggestName

val을 따로 만들 정도는 아닌 식에는 suggestName으로 원하는 이름을 붙입니다.

형식

식.suggestName("이름")
io.w := (io.a ^ io.b).suggestName("mix")

이 XOR 노드의 이름은 io_w_mix입니다. Chisel은 연결문 안에서 만든 값의 이름 앞에 연결 대상의 이름을 붙이므로, 제안한 이름은 최종 이름의 일부가 됩니다. 별도 줄에서 val mix = io.a ^ io.b로 선언하면 이름은 정확히 mix입니다.

참고. _io_z_T_1 같은 생성 이름은 주변 코드가 바뀌면 달라질 수 있습니다. 로그나 파형에서 찾을 신호에는 val 이름을 주세요. 파형 장에서는 이름 있는 값만 VCD 파일에 기록된다는 사실을 확인합니다.

5. 자주 하는 실수

5.1 cf"..." 대신 s"..." 사용

// 잘못된 코드: 컴파일 오류
printf(s"in = $in\n")

// 바른 코드: enable이 참인 에지마다 값을 출력
printf(cf"in = $in\n")

s interpolator는 elaboration 중에 Scala 객체를 문자열로 만듭니다. 하드웨어 값을 참조하는 것은 cf뿐이므로 s 형태에는 The s-interpolator prints the Scala .toString of Data objects 오류가 발생합니다.

5.2 \n 빠뜨리기

// 잘못된 코드: 두 메시지가 한 줄에 이어집니다
printf(cf"a=$a")
printf(cf"b=$b")

// 바른 코드
printf(cf"a=$a\n")
printf(cf"b=$b\n")

printf는 받은 문자만 정확히 출력합니다. \n이 없으면 다음 메시지나 테스트벤치의 다음 println이 같은 줄에 이어집니다.

5.3 에지 이후의 값을 기대하기

count := count + 1.U
printf(cf"count=$count\n")   // 이 에지에서 갱신되기 전의 값

printf는 에지 직전의 값을 출력합니다. 같은 에지에서 갱신되는 레지스터는 이전 값으로 출력되므로 출력이 한 사이클 늦은 것처럼 보입니다. 정상 동작입니다. 새 값은 다음 줄에서 확인하세요.

5.4 println으로 하드웨어 값 출력하기

// 잘못된 코드: elaboration 중에 타입 설명을 한 번 출력
println(s"sum = $sum")

// 바른 코드: 에지마다 값을 출력
printf(cf"sum = $sum\n")

모듈 본문은 한 번만 실행됩니다. 그 안의 println이 보는 것은 시뮬레이션 값이 아니라 Chisel 객체입니다.

5.5 리셋 중의 출력을 기대하기

// 테스트벤치가 reset을 두 step 동안 1로 유지
dut.reset.poke(true.B)
dut.clock.step(2)   // 모듈의 printf는 출력하지 않습니다

reset이 1인 동안에는 모든 printf가 꺼집니다. 로그가 늦게 시작하는 것처럼 보이면 버그를 찾기 전에 리셋 사이클 수부터 세어 보세요.

6. 정리

주제핵심
printfenable이 참인 상승 에지마다 메시지를 출력하는 하드웨어 노드입니다
출력 시점에지가 레지스터를 갱신하기 직전의 값을 출력합니다
enable감싸는 when 조건과 암묵적 "리셋 중이 아님" 조건으로 정해집니다
cf"..."$x나 ${식} 뒤에 %d, %x, %b, %c를 붙입니다. %%는 퍼센트 기호입니다
필드 폭신호 폭에 따라 고정되어 열이 정렬됩니다
집합 타입기본 형태는 Vec(...)와 클래스(필드 -> 값)이며, toPrintable로 바꿉니다
println일반 Scala입니다. 모듈에서는 elaboration 중 한 번, 테스트벤치에서는 호출마다 실행됩니다
이름val 이름이 신호 이름이 되고, 이름 없는 값은 _로 시작하는 이름을 받습니다. suggestName은 이름을 제안합니다

7. 확인 문제

  1. printf 예제에서 테스트벤치가 방금 65를 poke했는데 첫 번째 출력의 last가 0인 이유는 무엇일까요?

    정답 보기

    printf는 에지 직전의 값을 출력합니다. 첫 에지 직전에 last는 아직 리셋 값 0을 가지고 있습니다. last는 메시지가 출력된 뒤 그 에지에서 65를 저장합니다.

  2. 어떤 에지에서 printf가 출력하지 않게 만드는 조건 두 가지는 무엇일까요?

    정답 보기

    조건이 거짓인 when이 printf를 감싸는 경우와 reset이 1인 경우입니다.

  3. printf(s"...")가 쓸모없는 문자열을 출력하는 대신 컴파일 오류가 되는 이유는 무엇일까요?

    정답 보기

    s interpolator는 elaboration 중에 Chisel 객체를 한 번 문자열로 바꾸고, 모든 사이클이 그 문자열을 반복합니다. 이런 코드는 거의 언제나 실수이므로 Chisel은 Scala 코드를 컴파일할 때 오류로 알려 줍니다.

  4. UInt(4.W)의 %d 필드는 몇 칸이며, 필드 폭이 고정되면 무엇이 편리할까요?

    정답 보기

    3칸입니다. 같은 신호는 모든 줄에서 같은 폭을 쓰므로 사이클이 바뀌어도 값이 같은 열에 놓이고 비교하기 쉽습니다.

  5. val total = a + b로 쓰면 FIRRTL에서 결과 노드의 이름은 무엇일까요? out := a + b로 쓰면 어떤 이름이 예상될까요?

    정답 보기

    결과 노드는 total이고, 내부의 전체 폭 덧셈은 _total_T입니다. out := a + b에는 val이 없으므로 연결 대상에서 만든 생성 이름 _out_T와 _out_T_1이 붙습니다.

확인 지점

예제를 실행해 예측과 비교합니다

참고 자료

참고한 자료의 라이선스와 고지