Ada 프로그래밍


최종 수정일:

김호동 <hodong@nimfsoft.art>

머리말

이 책은 기존 프로그래밍 경험이 있는 개발자를 대상으로 하는 Ada 프로그래밍 언어 학습서입니다. 모든 기술적인 내용은 Ada 2022 레퍼런스 매뉴얼을 기반으로 합니다.

Ada는 신뢰성, 유지보수성, 효율성에 중점을 두고 설계되어, 소프트웨어의 오류가 치명적인 결과로 이어질 수 있는 고신뢰성 시스템 개발에 사용되어 왔습니다. 이 책은 Ada의 기본 문법과 주요 특징을 공부하고, 그 외 C/C++ 연동 등의 실용적인 주제를 학습하는 것을 목표로 합니다.

이 책의 모든 예시 소스코드는 부록에 수록된 Clair 코딩 스타일 가이드를 따릅니다. 이 가이드는 타 언어 사용자들이 Ada에 쉽게 적응할 수 있도록, 다른 주요 프로그래밍 언어의 일반적인 코딩 스타일을 Ada의 특성에 맞게 조정한 것입니다.

선수 지식과 시작점

이 책은 다른 범용 프로그래밍 언어에서 변수, 조건문, 반복문, 함수나 프로시저에 해당하는 기본 개념을 접해 본 독자를 기본 대상으로 합니다. Ada 문법이나 GNAT, GPRbuild, 태스킹, SPARK에 대한 사전 지식은 요구하지 않습니다. 개발 도구는 2장에서 준비하므로 Ada 개발 환경이 없는 상태에서 시작할 수 있습니다.

이 책을 읽는 방법

처음부터 학습하는 독자는 다음 다섯 구간을 순서대로 읽을 수 있습니다. 실제 장 heading은 기존 공개 문서 구조를 유지하며, 아래 구간은 학습 경로를 설명하기 위한 묶음입니다.

  1. 시작과 표기 — 1~3장: Ada의 배경, 개발 환경, 소스 텍스트의 기본 단위를 준비합니다.
  2. 핵심 언어 — 4~7장: 타입과 객체, 표현식, 제어 흐름, 서브프로그램을 익힙니다.
  3. 프로그램 구조와 추상화 — 8~11장: 패키지, 접근성, 제네릭, 객체 지향 프로그래밍을 다룹니다.
  4. 신뢰성·빌드·외부 경계 — 12~14장: 예외와 계약, 프로젝트 빌드, 다른 언어와의 인터페이스를 다룹니다.
  5. 동시성·실시간·형식 검증 — 15~17장: 동시성, 실시간/태스킹 프로파일과 SPARK 형식 검증을 다룹니다.

1~12장은 일반적인 Ada 언어 학습의 중심 경로입니다. 13~16장은 실제 프로젝트, 외부 시스템 연동, 동시성 또는 실시간 프로그래밍이 필요한 독자가 이어서 읽을 수 있습니다. 17장은 SPARK 언어와 GNATprove의 flow analysis·proof 모델을 소개합니다. 실제 proof 결과는 사용하는 GNATprove 버전과 분석 범위·계약·가정에 따라 판정해야 합니다. 이미 Ada를 사용하는 독자는 목차에서 필요한 장으로 바로 이동하고, 해당 장이 가리키는 앞선 정본 절을 선수 지식으로 참고할 수 있습니다.


크리에이티브 커먼즈 라이선스 이 저작물은 크리에이티브 커먼즈 저작자표시-비영리-변경금지 4.0 국제 라이선스에 따라 이용할 수 있습니다.


목차


1. Ada 언어 소개

Ada는 미국 국방부(U.S. Department of Defense)의 공통 고급 언어 사업에서 출발한 범용 프로그래밍 언어입니다. 언어의 원래 설계 목표는 프로그램의 신뢰성과 유지보수, 인간 활동으로서의 프로그래밍, 효율성이었고, 이후 표준 개정에서도 이 방향을 유지하면서 객체 지향, 실시간, 계약, 병렬 실행 등의 기능을 확장해 왔습니다. Ada는 특히 크고 장기간 유지되는 시스템과 임베디드 시스템에서 신뢰성과 효율성이 중요한 경우를 주요 설계 대상으로 삼아 왔습니다. 본 장에서는 Ada의 역사, 설계 목표, 주요 언어 특성을 개괄합니다.

1.1 Ada의 역사1

Ada의 개발 배경에는 1970년대 미국 국방부의 프로그래밍 언어 난립 문제가 있었습니다. HOLWG를 이끌었던 William A. Whitaker의 회고에 따르면 사업 시작 당시 DoD에서 사용 중인 언어는 450개가 넘었고, 많은 컴파일러가 특정 기계와 무기 체계에 묶여 있었습니다. Whitaker는 이 숫자가 엄밀한 감사 통계라기보다 당시 기록에서 찾을 수 있었던 최선의 수치라고도 설명합니다. 공통 언어 사업의 목적은 단순히 새 문법을 만드는 데 있지 않았고, 공통 도구·교육·재사용 가능한 소프트웨어를 축적할 수 있는 기반을 마련하는 데에도 있었습니다.

고급 언어 실무 그룹(High Order Language Working Group, HOLWG)은 요구사항을 단계적으로 정제하고 기존 언어들을 평가했습니다. 최종 요구사항은 1978년의 Steelman 문서에 정리되었습니다. 기존 언어 가운데 하나를 그대로 채택하기 어렵다고 판단한 뒤 네 개의 경쟁 설계가 만들어졌고, 1978년에는 CII-Honeywell Bull과 Intermetrics의 두 설계가 다음 단계로 진출했습니다. 1979년 5월 2일 HOLWG는 Jean Ichbiah가 이끈 CII-Honeywell Bull의 설계, 경쟁 중 코드명 Green을 최종 선택했습니다.

선택된 언어에는 Charles Babbage와 협력한 Ada Augusta, Lady Lovelace를 기려 Ada라는 이름이 붙었습니다. Preliminary Ada Reference Manual과 설계 Rationale은 1979년 6월 ACM SIGPLAN Notices 14권 6A·6B호에 공개되어 시험과 공개 검토를 받았습니다. 1980년 12월 10일에는 DoD 표준 MIL-STD-1815가 승인되었고, 1815라는 번호는 Ada Lovelace의 출생 연도에서 따왔습니다.

이후 표준은 다음과 같이 발전했습니다.2

  • Ada 83: 1983년의 ANSI/MIL-STD-1815A가 흔히 Ada 83으로 불리는 기준판입니다. 이 계열은 이후 ISO/IEC 8652:1987 국제 표준이 되었습니다. 패키지, 강한 타입 구분, 예외, 제네릭, 태스킹 등 Ada의 핵심 구조가 이때 이미 자리 잡았습니다.

  • Ada 95: ISO/IEC 8652:1995. 태그된 타입과 type extension, class-wide type과 dispatching을 통한 객체 지향 기능, child unit, protected object, Real-Time Systems Annex 등이 추가·정비되었습니다. Ada Resource Association은 Ada 95를 최초의 국제 표준화된 완전한 객체 지향 프로그래밍 언어로 소개합니다.

  • Ada 2005: Ada 95의 Amendment 1, ISO/IEC 8652:1995/Amd 1:2007. interface type, 제한된 상호 의존 package specification, 컨테이너 라이브러리, Ravenscar profile과 실시간·객체 지향 기능의 확장 등이 포함되었습니다.

  • Ada 2012: ISO/IEC 8652:2012. Pre/Post, type invariant, subtype predicate 같은 계약·assertion 기능이 크게 강화되었고, conditional/case expression, quantified expression, expression function, iterator 등 표현식 중심의 기능이 확장되었습니다.

  • Ada 2022: 흔히 Ada 2022라고 부르지만 국제 표준의 실제 발행 번호는 ISO/IEC 8652:2023입니다. parallel loop와 parallel block, 병렬 실행에서의 conflict 및 potentially blocking 동작 탐지 지원, delta aggregate, declare expression, iterator filter, reduction 등 여러 기능이 추가되었습니다.

1.2 Ada의 설계 목표3

Ada Reference Manual은 원래 Ada 설계의 세 가지 최우선 관심사를 프로그램의 신뢰성과 유지보수(program reliability and maintenance), 인간 활동으로서의 프로그래밍(programming as a human activity), 효율성(efficiency)으로 설명합니다. Ada 95 이후의 개정판은 기능을 확장하면서도 이 원래의 강조점을 유지해 왔습니다.

신뢰성 및 유지보수(reliability and maintenance)

Ada는 작성 편의보다 가독성을 중시하도록 설계되었습니다. 변수는 명시적으로 선언되고 타입이 정해지며, 컴파일러는 해당 타입에 허용된 연산과 프로그램 단위 사이의 일관성을 검사할 수 있습니다. 이는 “모든 런타임 오류를 컴파일 시점에 제거한다”는 뜻이 아니라, 타입 불일치·가시성·프로파일 불일치처럼 실행 전에 판정할 수 있는 오류를 언어 규칙으로 일찍 차단한다는 뜻입니다.

또한 Ada는 프로그램 단위를 분리 컴파일하면서도 단위 사이의 정합성을 검사하도록 설계되었습니다. 이는 큰 프로그램을 여러 구성요소로 나누어 개발하고 교체하면서도 인터페이스의 오류를 줄이는 데 중요합니다.

인간 활동으로서의 프로그래밍(programming as a human activity)

Ada는 비교적 적은 수의 기본 개념을 일관된 방식으로 결합하고, 언어 구문이 프로그래머의 통상적인 기대에 가깝도록 설계하려는 목표를 갖습니다. 독립적으로 작성한 구성요소를 조립하는 능력도 중심 설계 아이디어입니다. 패키지, private 타입, 제네릭은 이 목적과 직접 연결되며, type extension과 계층적 library 구조는 기존 구성요소에 대한 영향을 줄이면서 프로그램을 확장하는 데 사용됩니다.

효율성(efficiency)

Ada의 설계에서는 언어 기능이 과도하게 복잡한 컴파일러를 요구하거나 저장 공간·실행 시간을 본질적으로 낭비하지 않도록 구현 가능성을 고려했습니다. Reference Manual은 구현 방법이 불명확하거나 과도한 기계 자원을 요구하는 제안을 배제했다고 설명합니다. Ada 2022에서는 현대 멀티코어 시스템을 안전하고 효율적으로 활용하기 쉽게 하기 위해 parallel construct도 추가되었습니다.

이 설계 목표가 특정 Ada 코드의 성능을 자동으로 보장한다는 뜻은 아닙니다. 실제 실행 시간과 메모리 사용은 알고리즘, 컴파일러, 최적화 설정, 런타임, 대상 하드웨어에 따라 달라집니다.

1.3 Ada의 주요 특징

Ada의 주요 기능은 앞서 설명한 설계 목표를 구체적인 언어 규칙과 프로그램 구조로 구현합니다. 여기서는 이후 장에서 자세히 다룰 개념들의 경계만 소개합니다.

신뢰성과 안전성을 위한 언어 규칙 (reliability and safety)

Ada는 강한 타입 구분과 많은 정적 적법성 규칙을 사용합니다. 서로 다른 명명 타입은 단순히 기계 표현이 비슷하다는 이유만으로 자동 호환되지 않습니다. 예를 들어 별도의 정수 타입으로 정의한 Meters와 Kilograms는 같은 정수 표현을 사용할 수 있어도 서로 직접 더하거나 대입할 수 없으며, 언어가 허용하는 경우에도 의도적인 타입 변환이 필요합니다. 이런 구분은 단위나 의미가 다른 값을 우연히 섞는 오류를 줄이는 데 도움이 됩니다.

컴파일 단계에서는 타입 일치, 이름과 가시성, 서브프로그램 프로파일, private 영역 접근 같은 정적 규칙을 검사합니다. 그러나 초기화되지 않은 변수의 모든 사용을 Ada 언어가 컴파일 시점에 반드시 검출하는 것은 아닙니다. 특히 명시적 초기값이 없는 scalar object는 유효하지 않은 표현을 가질 수 있고, 그런 값을 읽는 것은 Ada 2022 RM 13.9.1에서 bounded error로 다룹니다. 구현이 이를 탐지하면 Constraint_Error 또는 Program_Error를 일으킬 수 있지만, 항상 정적으로 거부해야 하는 규칙은 아닙니다.

Ada에는 index, range, overflow, null access, elaboration, task 상태 등 다양한 language-defined check도 있습니다. 수행된 check가 실패하면 해당 규칙에 정해진 예외가 발생합니다. 다만 이것을 “Ada에서는 잘못된 실행이 언제나 예외로 안전하게 바뀐다”고 일반화해서는 안 됩니다. Suppress로 check를 생략할 수 있고, 표준에는 bounded error와 erroneous execution이라는 별도 범주도 존재합니다. check를 억제한 상태에서 해당 오류 상황이 발생하면 실행이 erroneous가 될 수 있습니다.

Ada 2012 이후의 Pre, Post, predicate, invariant 같은 계약도 신뢰성 도구의 하나입니다. 그러나 assertion의 실행 검사는 Assertion_Policy의 영향을 받으며, 어떤 계약 검사가 통과했다고 해서 프로그램의 전체 논리적 정확성이 자동으로 증명되는 것은 아닙니다. 더 강한 정적 보증이 필요하면 17장에서 다루는 SPARK/GNATprove를 이용해 명시한 계약과 분석 가정의 범위 안에서 런타임 오류 부재나 기능적 성질을 증명할 수 있습니다.

동시성 (concurrency)

Ada 프로그램의 실행은 하나 이상의 task 실행으로 구성됩니다. 각 task는 다른 task와 상호작용하는 지점 사이에서 독립적이고 concurrent하게 진행되는 활동을 나타냅니다. task entry와 accept를 사용하면 동기적인 rendezvous를 구성할 수 있습니다.

여러 task가 공유 상태에 접근할 때는 protected object를 사용할 수 있습니다. protected object는 visible protected operation을 통해 공유 데이터에 대한 조정된 접근을 제공합니다. protected procedure와 entry는 상태 변경이나 조건 대기를 캡슐화하고, protected function은 보호 상태에 대한 읽기 전용 view를 사용합니다. 정확한 상호배제·동시 실행 규칙과 Exclusive_Functions 같은 세부 사항은 15장에서 다룹니다.

Ada 2022는 tasking model과 별도로 parallel loop, parallel block 등의 parallel construct를 제공합니다. 이 기능은 하나의 task 안에서도 여러 logical thread of control이 병렬로 진행될 수 있게 하며, conflict check와 potentially blocking 관련 규칙을 통해 병렬 코드의 위험을 탐지할 수 있도록 지원합니다. 실제로 여러 CPU에서 동시에 실행되는지와 성능 향상 정도는 구현과 실행 환경에 달려 있습니다.

모듈성과 데이터 추상화

Ada의 package는 논리적으로 관련된 타입, 객체, 서브프로그램 등을 하나의 단위로 묶는 기본 추상화 수단입니다. 모든 package에는 specification이 있지만 모든 package가 명시적인 body를 갖는 것은 아닙니다. specification 안의 선언이 completion을 요구하거나 실제 구현을 둘 필요가 있을 때 package body가 필요합니다.

package specification은 외부에 보이는 visible part와 선택적인 private part를 가질 수 있습니다. 이를 통해 외부에는 필요한 타입과 연산만 보여 주고 표현이나 보조 구현을 숨길 수 있습니다. 별도로 컴파일되는 library unit과 parent/child unit 구조는 큰 시스템의 namespace와 의존 관계를 계층적으로 구성하는 데 사용됩니다.

객체 지향 프로그래밍 (Object-Oriented Programming)

Ada 95는 tagged type, type extension, class-wide type과 dispatching을 통해 객체 지향 프로그래밍을 직접 지원합니다.

캡슐화란 데이터와 그 데이터에 작용하는 연산을 함께 묶고 내부 표현에 대한 접근을 제한하는 설계 원칙입니다.4 Ada에서는 package와 private type 또는 private extension을 이용해 내부 표현을 외부 인터페이스와 분리할 수 있습니다.

상속(inheritance)은 기존 타입을 바탕으로 새 타입을 정의하면서 관련 특성을 이어받는 메커니즘입니다.5 Ada의 tagged record/private type은 type extension을 통해 새로운 component와 primitive operation을 추가할 수 있습니다.

다형성(polymorphism)은 서로 다른 구체 타입을 공통 인터페이스로 다루는 능력입니다.6 Ada에서는 class-wide type과 controlling operand를 이용한 dispatching call이 실제 객체의 tag에 따라 실행할 primitive operation을 선택합니다.

함수형 스타일과 표현식 중심 기능 (functional style)

Ada는 순수 함수형 언어가 아니지만, Ada 2012와 2022는 표현식 중심으로 코드를 구성하기 좋은 기능들을 확장했습니다. Ada 2012에는 expression function, conditional/case expression, quantified expression과 generalized iterator 등이 들어갔고, Ada 2022에는 declare expression, delta aggregate, reduction, iterator filter 등이 추가되었습니다.

이 기능들은 함수형 스타일을 표현하기 쉽게 하는 도구이지 프로그램을 자동으로 순수하거나 immutable하게 만드는 규칙은 아닙니다. 예를 들어 delta aggregate는 기존 값에서 일부 component가 바뀐 새 값을 표현하기 좋지만, 그 결과를 mutable object에 대입하는 것도 가능합니다. 부작용의 실제 범위는 서브프로그램과 객체의 사용 방식, Global 같은 계약, 필요하다면 SPARK 분석을 통해 별도로 판단해야 합니다.

효율성과 저수준 제어 (efficiency and low-level control)

Ada는 효율성을 원래 설계 목표로 포함하지만 특정 추상화가 언제나 저수준 손코딩과 같은 성능을 낸다고 표준이 보장하지는 않습니다. generic instance의 코드 생성 방식, inline 여부, 최적화 정도는 구현의 영향을 받을 수 있습니다.

대신 Ada는 representation aspect와 representation clause를 통해 타입의 크기, 정렬, record component의 배치 등 underlying machine과의 mapping을 명시할 수 있는 수단을 제공합니다. 실제로 지원 가능한 표현과 정렬의 범위는 구현과 target ABI에 따라 달라질 수 있으며, C ABI와의 관계는 14장에서 별도로 다룹니다.

가독성과 명확성 (readability and clarity)

Ada 구문은 프로그램 구조를 명시적인 예약어로 드러내는 경향이 있습니다. 예를 들어 if는 end if, case는 end case, loop는 end loop로 끝나며, subprogram body나 package는 해당 단위의 end로 범위를 닫습니다. package specification처럼 begin이 없는 구조도 있으므로 모든 프로그램 단위를 단순히 begin ... end 형태라고 설명할 수는 없습니다.

이처럼 시작과 종료 구조를 텍스트에 명시하는 문법은 중첩된 프로그램의 범위를 읽기 쉽게 하고, 유지보수 과정에서 구조를 파악하는 데 도움을 줍니다.

2. 개발 환경 구성

Ada 프로그램을 작성하고 실행하려면 Ada 구현과 빌드 도구가 필요합니다. 이 장에서는 GCC 계열의 GNAT을 사용하여 기본 개발 환경을 구성하고, 간단한 프로그램을 직접 컴파일하고 실행합니다. 설치 예시는 FreeBSD를 중심으로 설명하되 Debian 계열 Linux에서의 대응 방법도 함께 제시합니다.

Ada 언어 표준은 특정 컴파일러를 “표준 컴파일러”로 지정하지 않습니다. 이 책에서 GNAT을 사용하는 이유는 널리 사용되는 오픈 소스 Ada 구현이고 GCC, GPRbuild, Alire 등과 함께 실제 개발에 필요한 도구 생태계를 제공하기 때문입니다. 컴파일러와 빌드 도구의 동작은 Ada 언어 규칙과 구분하여 이해해야 합니다.

2.1 GNAT 툴체인

GNAT은 GCC에 포함된 Ada 컴파일러 계열입니다. 현재 GNU와 AdaCore 문서는 GNAT을 GNU Ada Development Environment라고 부르며, GCC의 gcc 드라이버는 Ada 소스를 처리할 때 Ada front end인 gnat1을 호출합니다.7 GNAT은 원래 뉴욕 대학교(NYU)의 GNAT 팀에서 개발되었고 이후 GCC의 Ada 구현으로 발전했습니다.

AdaCore는 상업적으로 지원되는 GNAT Pro와 관련 도구를 개발합니다. 과거의 무료 배포판인 GNAT Community Edition은 2021 릴리스가 마지막이며, 현재의 커뮤니티 사용자는 GNAT FSF, 운영체제 배포판 패키지, Alire를 사용하는 것이 기본 경로입니다.8

GNAT 기반 개발 환경에서 자주 만나는 도구의 역할은 다음과 같습니다.

  • Ada compiler (gcc): .adb나 .ads와 같은 Ada 소스를 컴파일합니다. 일반적인 body 컴파일에서는 목적 파일(.o)과 함께 Ada Library Information(ALI) 파일(.ali)이 생성됩니다. ALI 파일에는 이후 binder가 프로그램의 정합성과 의존 관계를 확인하는 데 필요한 정보가 들어 있습니다.

  • GNAT Binder (gnatbind): main에서 필요한 단위의 ALI 파일을 읽어 구성 단위 사이의 일관성을 확인하고 Ada 규칙에 맞는 elaboration 순서를 결정합니다. 문제가 없으면 elaboration과 main 호출을 준비하는 binder 생성물을 만듭니다. binder 자체가 최종 실행 파일을 링크하는 것은 아닙니다.

  • GNAT Linker (gnatlink): binder 단계의 생성물, 프로그램의 목적 파일, 필요한 Ada 런타임과 라이브러리를 시스템 linker에 전달하여 실행 파일을 만듭니다.

  • GNAT Make (gnatmake): 한 Ada main에 필요한 소스를 찾아 필요한 재컴파일, binding, linking을 순서대로 수행하는 builder입니다. 작은 native Ada 프로그램을 빠르게 빌드하기에 편리합니다.

  • GPRbuild (gprbuild): GPR 프로젝트 트리를 기반으로 Ada뿐 아니라 C, C++, Fortran 등의 소스, 라이브러리, 여러 소스 디렉터리와 빌드 옵션을 관리하는 multi-language build tool입니다. 큰 프로젝트나 여러 언어가 섞인 프로젝트에서는 gnatmake보다 프로젝트 구조를 명시적으로 관리하기 좋습니다. 자세한 사용법은 13장에서 다룹니다.

  • Alire (alr): Ada/SPARK 프로젝트의 패키지·의존성·workspace를 관리하는 도구입니다. Alire 자체는 Ada 컴파일러가 아니며, 프로젝트를 빌드할 때 선택된 GNAT과 GPRbuild toolchain을 사용합니다. 지원되는 호스트에서는 compiler/GPRbuild toolchain 선택과 설치도 도울 수 있습니다.9

GNAT의 compile-bind-link 모델과 각 도구의 정확한 역할은 GNAT User’s Guide에 문서화되어 있습니다.7

FreeBSD

2026년 9월 1일 기준 FreeBSD Ports Collection에는 여러 버전의 GNAT 포트(lang/gnat12~lang/gnat15)와 devel/gprbuild, devel/alire가 있습니다.10 다만 실제 binary package의 제공 여부와 버전은 FreeBSD 릴리스, 아키텍처, latest/quarterly 저장소에 따라 달라질 수 있으므로 먼저 현재 저장소를 확인하는 편이 안전합니다.

$ pkg search -x 'gnat[0-9]+'
$ pkg search -x 'gprbuild'
$ pkg search -x 'alire'

사용할 GNAT 버전이 현재 저장소에서 제공된다면 해당 버전과 필요한 도구를 설치합니다. 예를 들어 gnat15가 제공되는 저장소에서는 다음과 같이 설치할 수 있습니다.

# pkg install gnat15 gprbuild alire

FreeBSD의 versioned GNAT 패키지는 여러 compiler version을 함께 설치할 수 있으므로, 설치 뒤 실제로 선택된 실행 파일과 버전을 확인해야 합니다. gnatmake가 바로 검색되지 않는다면 설치된 패키지의 파일 목록과 안내를 확인하여 원하는 GNAT의 bin 디렉터리를 PATH에 추가합니다. Alire의 upstream 배포가 제공하는 compiler toolchain의 host 범위와 FreeBSD Ports에서 제공하는 alr의 범위도 같다고 가정해서는 안 됩니다. FreeBSD에서는 시스템에 설치한 GNAT/GPRbuild를 Alire와 함께 사용하는 구성이 가능합니다.

Debian 계열 Linux

Debian 계열 배포판에서는 배포판 패키지로 기본 도구를 설치할 수 있습니다.

$ sudo apt install gnat gprbuild alire

배포판 패키지는 upstream 최신 버전과 시차가 있을 수 있습니다. 중요한 것은 책의 예제와 프로젝트에서 어떤 compiler와 GPRbuild가 실제로 선택되었는지 확인하는 것입니다.

설치 뒤에는 최소한 다음 명령으로 현재 도구를 확인합니다.

$ gnatmake --version
$ gprbuild --version
$ alr version

Alire를 사용하지 않을 경우 alr 확인은 생략할 수 있습니다. 여러 GNAT이 설치된 환경에서는 command -v gnatmake, command -v gprbuild도 함께 확인하면 의도하지 않은 toolchain을 사용하는 실수를 줄일 수 있습니다.

2.2 통합 개발 환경(IDE)

Ada 소스는 일반 텍스트 편집기로도 작성할 수 있습니다. IDE나 language server는 필수 구성 요소가 아니라 편집, 탐색, 빌드, 디버깅 같은 작업을 편리하게 묶어 주는 선택 사항입니다.

GNAT Studio

GNAT Studio는 AdaCore의 통합 개발 환경입니다. Ada, C, C++에 대한 고급 지원을 기본 제공하고 GPR 프로젝트와 AdaCore 도구를 통합합니다. 현재 문서에는 소스 편집과 탐색, Build -> Project -> Build All 빌드 동작, GDB 기반 디버깅, task view 등의 기능이 문서화되어 있습니다.11

GNAT Studio가 compiler, debugger, GNATprove 같은 모든 도구의 기능을 자체적으로 대체하는 것은 아닙니다. 프로젝트에서 사용할 compiler와 분석 도구가 설치되어 있어야 해당 기능을 사용할 수 있습니다. Alire crate를 열면 GNAT Studio가 alire.toml을 인식하여 프로젝트 환경을 구성하고, 현재 버전에서는 Build All 같은 동작에 alr build를 사용할 수 있습니다.11

Visual Studio Code와 AdaCore 확장 프로그램

AdaCore의 Ada & SPARK 확장 프로그램은 VS Code에 Ada Language Server 기반의 구문 강조, 탐색, 자동 완성, refactoring 등을 제공합니다. 확장 식별자는 AdaCore.ada입니다.12

확장 프로그램 자체에는 GNAT compiler나 debugger가 포함되지 않습니다. 완전한 build/debug 환경이 필요하면 사용할 GNAT, GPRbuild와 debugger 등을 별도로 설치하고 PATH에서 찾을 수 있게 해야 합니다. 프로젝트에 필요한 분석 도구나 GNATprove도 마찬가지입니다.

VS Code에서 기본 GPR 프로젝트 build task를 설정하면 ada: Build current project가 gprbuild를 호출하도록 구성할 수 있고, 그 task를 기본 build task로 지정한 경우 Ctrl + Shift + B로 실행할 수 있습니다. 따라서 “VS Code가 Ada를 빌드한다”기보다 VS Code 확장이 설치된 build tool을 호출한다고 이해하는 편이 정확합니다.

2.3 기본적인 프로그램: “Hello, World!”

개발 환경이 준비되었는지 확인하기 위해 작은 프로그램을 직접 컴파일하고 실행해 보겠습니다. 다음 예제는 이 책의 모든 코드에 적용하는 부록의 Clair 코딩 스타일 가이드를 따릅니다.

hello.adb라는 파일을 만듭니다. GNAT의 기본 파일 명명 관례에서는 최상위 Ada 단위 이름과 소스 파일 이름을 대응시키며, 자세한 규칙과 GPR의 naming 설정은 13.3.1절에서 다룹니다.

with Ada.Text_IO;

procedure hello is
begin
  Ada.Text_IO.put_line ("Hello, World!");
end hello;

명령줄에서 이 프로그램을 빌드하고 실행하려면 gnatmake를 사용할 수 있습니다.

$ gnatmake hello.adb
$ ./hello
Hello, World!

GNAT의 기본 build model에는 compile, bind, link 단계가 있습니다. 위의 작은 예제에서 gnatmake가 자동화하는 과정을 개별 도구로 확인하면 다음과 같은 형태입니다.

$ gcc -c hello.adb
$ gnatbind hello.ali
$ gnatlink hello.ali
$ ./hello
Hello, World!

gcc -c 단계는 hello.o와 hello.ali를 만들고, gnatbind는 ALI 정보를 읽어 프로그램의 일관성과 elaboration 순서를 처리하며, gnatlink가 최종 실행 파일을 링크합니다. 실제 builder는 변경된 의존 관계와 옵션을 고려하여 필요한 명령을 선택하므로 이 세 줄을 모든 프로젝트의 고정된 build script로 해석해서는 안 됩니다. 큰 프로젝트에서는 13장에서 다루는 GPRbuild를 사용하는 편이 적합합니다.

이 간단한 프로그램의 Ada 요소를 살펴보겠습니다.

  • with Ada.Text_IO;: 이것은 with 절(with_clause)이며 library item Ada.Text_IO에 대한 semantic dependence를 문맥에 추가합니다. with 자체는 객체나 서브프로그램을 도입하는 declaration이 아닙니다. 이 절 덕분에 Ada.Text_IO의 이름을 문맥에서 사용할 수 있고, 예제처럼 공개 연산을 selected name으로 참조할 수 있습니다.

선언(Declaration) Ada에서 declaration은 객체, 타입, 서브프로그램 등의 entity를 명시적으로 또는 암시적으로 선언하는 언어 구성입니다. 이 예제의 procedure hello is ... end hello;는 서브프로그램 body이면서 hello라는 procedure를 도입합니다. 반면 with Ada.Text_IO;는 context clause의 일부이며 declaration과 구분합니다.

  • procedure hello is ... end hello;: 이 예제에서 실행할 main subprogram입니다. 이식성 있는 기본 형태로 Ada 구현은 public parameterless library procedure를 main으로 지원해야 하며, GNAT도 이 형태를 지원합니다. Ada 표준이 가능한 모든 main을 이 형태 하나로 제한하는 것은 아닙니다. 자세한 main subprogram 규칙은 7.1.2절에서 다룹니다.

    • procedure 뒤의 hello는 서브프로그램의 이름입니다.

    • is 뒤에서 begin 전까지는 지역 객체, 타입, 중첩 서브프로그램 같은 선언을 둘 수 있는 declarative part입니다. 이 예제에서는 비어 있습니다.

    • begin 뒤에는 실행할 문장들이 옵니다.

    • end hello;는 body를 끝냅니다. 끝의 hello는 Ada 문법상 생략할 수 있지만, 적었다면 시작 부분의 defining designator와 일치해야 합니다. 이 책의 Clair 스타일에서는 가독성을 위해 이름을 반복합니다.

  • Ada.Text_IO.put_line ("Hello, World!");: Ada.Text_IO 패키지의 put_line procedure를 selected name으로 호출합니다. use Ada.Text_IO;를 추가해 직접 가시성을 넓히는 대신 패키지 이름을 명시하므로 이 호출이 어느 패키지에 속하는지 코드에서 바로 드러납니다.

    • Clair 스타일 가이드는 호출 이름과 여는 괄호 사이에 공백을 둡니다. 이것은 Ada의 legality rule이 아니라 이 책에서 사용하는 formatting convention입니다.

    • "Hello, World!"는 put_line에 전달되는 문자열 literal입니다.

    • ;는 이 procedure call statement를 끝냅니다.

이 예제는 Ada 언어와 GNAT toolchain의 경계를 함께 보여 줍니다. with 절과 library unit의 의미는 Ada 언어가 정의하지만, 소스 파일을 어떻게 찾고 ALI 파일을 만들며 어떤 순서로 compiler, binder, linker를 실행하는지는 GNAT의 compilation model입니다. 규모가 커질수록 이 두 층위를 구분해야 빌드 설정이나 toolchain 고유 동작을 Ada 언어 규칙으로 오해하지 않을 수 있습니다.

3. 어휘 구조 (lexical structure)

Ada 프로그램의 텍스트는 문자로 이루어진 어휘 요소(lexical element)의 연속입니다. Ada 2022 RM 2.2에서 어휘 요소는 delimiter, identifier, reserved word, numeric literal, character literal, string literal, comment로 분류합니다.13 공백·탭·줄 끝 같은 separator는 필요한 곳에서 인접한 어휘 요소를 분리하지만 그 자체가 lexical element인 것은 아닙니다.

프로그램의 의미는 comment를 제외한 lexical element의 순서에 의해 결정됩니다. 따라서 공백과 줄바꿈은 필요한 분리를 유지하는 범위에서 대체로 자유롭게 배치할 수 있지만, 하나의 lexical element 내부를 임의로 나눌 수는 없습니다.

Ada 2022의 문자 분류는 ISO/IEC 10646:2020의 character category를 사용합니다. 이것이 곧 모든 소스 파일이 자동으로 UTF-8로 해석된다는 뜻은 아닙니다. 실제 source encoding 선택은 구현의 입력 규칙을 따라야 합니다. GNAT에서는 UTF-8 source를 사용할 때 -gnatW8 같은 wide-character encoding 옵션이 관련됩니다.14

3.1 식별자 (identifier)

식별자는 이름으로 사용됩니다. Ada 2022에서 identifier의 첫 문자는 Unicode category상 대문자·소문자·titlecase·modifier·other letter 또는 letter number (number_letter)여야 합니다. 그 뒤에는 이러한 시작 문자 외에 nonspacing/spacing combining mark, decimal digit, connector punctuation이 올 수 있습니다.15

따라서 단순히 “문자, 숫자, 밑줄만 허용된다”고 설명하면 범위가 너무 좁습니다. 반대로 모든 Unicode 문자가 식별자에 허용되는 것도 아닙니다. identifier는 Normalization Form KC에 나타날 수 있는 문자만 포함해야 하며, connector punctuation 두 개가 연속하거나 마지막에 올 수 없습니다.

Ada identifier는 locale-independent simple case folding 후 같은 문자열이면 같은 identifier로 취급됩니다. 이 과정을 거친 결과가 reserved word와 같아지는 identifier도 허용되지 않습니다.

대표적인 식별자는 다음과 같습니다.

Temperature, page_count, x1, get_symbol, Next_Item, Matrix2x2, Πλάτων, Чайковский

GNAT에서 비ASCII identifier를 사용할 때에는 source encoding 설정도 맞아야 합니다. 현재 Debian/GNAT 14.2.0에서 -gnatW8을 사용하면 한글·한자·히라가나·accented Latin identifier와 connector punctuation을 포함한 예제가 컴파일됩니다. 다만 RM 문법이 허용하는 number_letter의 한 예인 U+2163 Ⅳ은 이 GNAT 14.2.0 환경에서 invalid wide character in identifier로 거부되었습니다. 따라서 언어 표준의 허용 범위와 특정 compiler 버전의 Unicode table 지원을 동일시해서는 안 됩니다.

잘못된 예의 대표적인 경우는 다음과 같습니다.

  • 1st_reading — decimal digit으로 시작
  • _Temp — connector punctuation으로 시작
  • Page__count — connector punctuation 연속
  • End_Of_File_ — connector punctuation으로 끝남
  • end — reserved word

3.2 예약어 (reserved words)

Reserved word는 identifier로 사용할 수 없는 언어 키워드입니다. Ada의 reserved word 비교도 대소문자를 구분하지 않으므로 IF, If, if는 같은 reserved word입니다.

Ada 2022의 reserved word 목록은 다음과 같습니다.16

abort       delay       in          parallel    some
abs         delta       interface   pragma      subtype
abstract    digits      is          private     synchronized
accept      do          limited     procedure   tagged
access      else        loop        protected   task
aliased     elsif       mod         raise       terminate
all         end         new         range       then
and         entry       not         record      type
array       exception   null        rem         until
at          exit        of          renames     use
begin       for         or          requeue     when
body        function    others      return      while
case        generic     out         reverse     with
constant    goto        overriding  select      xor
declare     if          package     separate

3.3 리터럴 (literal)

이 장에서는 lexical element로 정의되는 numeric, character, string literal을 다룹니다. literal의 실제 타입과 값 해석에는 이후 장에서 설명할 expected type과 universal type 규칙도 관여합니다.

3.3.1 숫자 리터럴 (numeric literal)

Ada의 numeric literal은 decimal literal 또는 based literal입니다. decimal literal은 numeral [.numeral] [exponent] 형태이고, based literal은 base#based_numeral[.based_numeral]#[exponent] 형태입니다.17

숫자 사이의 underline은 아무 위치에나 자유롭게 넣을 수 있는 것이 아닙니다. 문법상 digit 사이에만 들어갈 수 있으므로 1_000_000은 가능하지만 1__0처럼 연속된 underline은 허용되지 않습니다.

123
1_000_000
5e2
0.0
12.34
3.14159_26
1.2e-3

소수점이 있는 decimal literal은 real literal이고, 없는 형태는 integer literal입니다. Integer literal의 exponent는 음수가 될 수 없습니다. 현재 GNAT 14.2.0에서도 1e-1을 integer literal 위치에 쓰면 거부합니다.

Based literal의 base는 2에서 16 사이여야 합니다. extended digit은 0~9, A~F이며 문자는 대소문자를 구분하지 않습니다.

2#1111_1111#
16#FF#
16#ff#
16#E#E1
2#1.1111_1111_111#E11

Based literal의 exponent는 10이 아니라 해당 base의 거듭제곱을 뜻합니다. 예를 들어 16#E#E1은 14 * 16입니다. 17#1#처럼 base 범위를 벗어난 표기는 illegal입니다.

숫자 앞의 + 또는 -는 numeric literal 자체의 일부가 아니라 expression 문법의 unary adding operator입니다. 예를 들어 -10은 lexical하게 - delimiter/operator와 10 numeric literal로 나뉩니다.

3.3.2 문자 리터럴 (character literal)

Character literal은 apostrophe 두 개 사이의 graphic character 하나로 표기합니다.18

'A'
'*'
' '

Unicode 문자를 source에 쓸 수 있다는 사실과 그 literal이 임의의 character type에 속한다는 것은 별개의 문제입니다. 예를 들어 GNAT 14.2.0에서 UTF-8 source mode(-gnatW8)를 사용하면 다음은 가능합니다.

C : Wide_Wide_Character := '가';

반면 같은 환경에서 다음은 Standard.Character에 해당 문자가 정의되어 있지 않아 거부됩니다.

C : Character := '가';

즉 literal 표기 가능성과 expected character type의 값 집합을 함께 고려해야 합니다.

3.3.3 문자열 리터럴 (string literal)

String literal은 quotation mark로 둘러싼 0개 이상의 graphic character sequence입니다.19

"This is a string."
"Hello, Ada!"
""
"A"
"He said, ""Hello!"""

문자열 안에 quotation mark 자체를 넣으려면 두 번 연속하여 적습니다. Character literal과 마찬가지로 비ASCII 문자를 쓸 때에는 source encoding과 expected string type을 함께 고려해야 합니다. GNAT 14.2.0의 UTF-8 source mode에서는 다음처럼 Wide_Wide_String으로 사용할 수 있습니다.

S : Wide_Wide_String := "가∞א";

3.4 구분자 (delimiter)

Delimiter는 lexical element의 한 종류입니다. Ada 2022의 single-character delimiter는 다음 특수 문자들입니다.20

&    '    (    )    *    +    ,    -    .    /
:    ;    <    =    >    @    [    ]    |

Compound delimiter는 다음과 같습니다.

=>    ..    **    :=    /=    >=    <=    <<    >>    <>

대표적인 이름은 => arrow, .. double dot, ** exponentiate, := assignment, /= inequality입니다. 같은 문자가 comment, string literal, character literal, numeric literal 또는 compound delimiter의 일부로 쓰이는 경우에는 독립 single delimiter로 세지지 않습니다.

Separator는 delimiter와 다른 개념입니다. separator space, format effector, end of line이 필요한 곳에서 lexical element를 분리합니다. 예를 들어 if x then에서 if와 x 사이에는 분리가 필요하지만, x:=1;처럼 delimiter가 경계를 명확히 하면 공백이 없어도 됩니다.

3.5 주석 (comment)

Ada comment는 두 개의 연속된 hyphen-minus --로 시작하여 그 줄의 끝까지 이어집니다.21 여러 줄 주석을 만드는 별도 block-comment lexical form은 없으므로 각 줄을 --로 시작합니다.

-- 이 줄 전체는 주석입니다.
x : Integer := 1;  -- 코드 뒤에도 둘 수 있습니다.

Comment는 lexical element이지만 프로그램의 의미를 결정하는 lexical-element sequence에서는 제외됩니다. 다만 documentation generator나 정적 분석 도구가 특정 comment convention을 별도의 입력으로 해석할 수 있으므로, “compiler가 의미상 무시한다”는 설명을 모든 외부 도구가 comment를 무시한다는 뜻으로 확대해서는 안 됩니다.

3.6 프라그마 (pragma)

Pragma는 lexical element 자체가 아니라 Ada syntax의 한 구성입니다. pragma라는 reserved word, identifier, delimiter, expression 등의 lexical element가 결합하여 pragma를 이룹니다. Ada RM 2.8은 pragma의 문법과 language-defined/implementation-defined pragma 처리 규칙을 정의합니다.22

일반적인 형태는 다음과 같습니다.

pragma Pragma_Name [(인수, ...)];

실제 pragma가 허용되는 위치와 효과는 pragma 종류에 따라 달라집니다. 모든 pragma가 “해당 위치부터 compilation unit 끝까지” 같은 동일한 범위를 갖는 것은 아닙니다.

Ada에는 language-defined pragma가 있고 구현은 추가적인 implementation-defined pragma를 제공할 수 있습니다. GNAT은 Warnings, Style_Checks, Ada_2012, Ada_2022 등을 implementation-defined pragma로 문서화합니다.23

구현이 pragma identifier를 인식하지 못하는 경우 Ada 2022 RM의 처리 규칙에 따라 그 pragma는 프로그램의 legality나 execution에 영향을 주지 않습니다. GNAT 14.2.0은 알 수 없는 pragma를 warning으로 보고하면서도 compile을 계속하는 동작을 확인했습니다.

3.6.1 GNAT 언어 버전 모드 (pragma Ada_2022, pragma Ada_2012)

pragma Ada_2022;와 pragma Ada_2012;는 GNAT의 implementation-defined configuration pragma입니다. Ada 표준이 모든 compiler에 이 pragma 이름을 요구하는 것은 아닙니다. GNAT에서는 각각 해당 language mode를 unit에 설정합니다.24

pragma Ada_2022;

재사용 가능한 프로젝트에서는 source 안의 pragma보다 GPR/command-line에서 language version을 일관되게 관리하는 편이 더 적합할 수 있습니다. 어느 방식을 택하든 “Ada 2022 언어 기능”과 “GNAT에서 그 mode를 선택하는 방법”을 구분해야 합니다.

3.6.2 경고 메시지 제어 (pragma warnings)

pragma Warnings는 GNAT implementation-defined pragma입니다. 전체 warning을 On/Off 하거나 local name, warning-option string, warning-text pattern 등을 이용해 더 좁게 제어할 수 있습니다.25

pragma Warnings (Off, Unused);
-- ...
pragma Warnings (On, Unused);

Warnings (Off)처럼 전체 warning 생성을 끄는 형식과 특정 entity를 지정하는 형식, text pattern을 지정하는 형식은 효력 범위가 서로 다릅니다. 특히 text pattern을 끄는 형식은 대응하는 On이 없으면 파일 끝까지 이어질 수 있습니다. 따라서 suppression 범위를 최소화하고 가능한 한 다시 켜는 위치를 명시하는 편이 유지보수에 안전합니다.

3.6.3 스타일 검사 제어 (pragma style_checks)

pragma Style_Checks도 GNAT implementation-defined pragma입니다. GNAT의 built-in style checking을 켜거나 끄고 -gnaty 계열과 같은 style option을 source에서 조정할 수 있습니다.

pragma Style_Checks (Off);
-- 자동 생성 코드 등 필요한 최소 영역
pragma Style_Checks (On);

이 pragma는 Ada 언어의 syntax/legality rule을 바꾸는 기능이 아니라 GNAT의 style diagnostics를 제어합니다. pragma Warnings가 일반 warning을 제어한다고 해서 style message까지 함께 제어되는 것도 아닙니다.

4. 타입, 서브타입, 객체, 스코프와 생명주기

이 장에서는 Ada 프로그램의 데이터 모델을 이해하는 데 필요한 타입(type), 서브타입(subtype), 값(value), 객체(object), 스코프(scope)와 객체 생명주기(lifetime)의 기초를 설명합니다.

Ada에서 타입은 값의 집합과 그 타입의 의미를 구성하는 기본 연산들을 정합니다. 서브타입은 어떤 타입에 constraint나 null exclusion 등을 적용하여 그 타입의 값 가운데 사용할 수 있는 범위를 정합니다. 객체는 실행 중 만들어져 어떤 타입의 값을 가지는 실체입니다.26

여기서 객체는 객체지향 프로그래밍에서 말하는 class instance보다 넓은 언어 개념입니다. 정수 변수나 상수, 배열·레코드 객체, formal parameter, loop parameter, 함수의 return object, aggregate의 결과와 객체의 component도 Ada의 객체가 될 수 있습니다. tagged object 역시 이 일반적인 Ada 객체의 한 종류입니다.

이 장에서는 다음 내용을 다룹니다.

  • 타입·서브타입·값·객체의 차이
  • constraint와 predicate의 차이
  • 객체 초기화와 range check의 기본 경계
  • limited type의 복사·초기화 규칙
  • constant view와 불변성의 범위
  • scope와 visibility의 차이
  • 객체의 생성, finalization과 생명주기

이러한 개념은 이후의 서브프로그램, 패키지, 접근 타입과 접근성, 객체 지향 프로그래밍, 동시성 프로그래밍을 이해하는 선수 지식이 됩니다.


4.1 타입, 객체와 제약

Ada에서는 새 타입을 만드는 것과 기존 타입의 서브타입을 만드는 것을 구분해야 합니다. 다음 두 타입은 같은 정수 표현을 사용할 수 있어도 서로 다른 타입입니다.

type Meters  is new Integer;
type Seconds is new Integer;

따라서 Meters 값을 Seconds 객체에 암시적으로 대입할 수 없습니다. 반면 subtype 선언은 새 타입을 만들지 않습니다.

subtype Short_Distance is Meters range 0 .. 100;

Short_Distance는 Meters 타입의 서브타입이며, 그 값도 Meters 값입니다. 이런 구분이 Ada의 strong typing과 범위 모델의 출발점입니다.

4.1.1 객체와 값

객체와 그 객체가 가진 값은 서로 다른 개념입니다. Ada RM은 객체를 실행 중 만들어져 주어진 타입의 값을 가지는 실체로 정의합니다.27

A : Integer := 10;
B : Integer := A;

위 초기화 뒤 A와 B는 둘 다 값 10을 가지지만 서로 다른 객체입니다. 이후 A를 바꾸어도 그것만으로 B가 함께 바뀌지는 않습니다.

객체가 항상 명시적인 변수 선언으로만 만들어지는 것도 아닙니다. formal parameter와 loop parameter, 함수의 return object, aggregate 평가 결과, access-to-object 값의 dereference, 다른 객체의 component나 slice 등도 언어가 정의하는 객체 또는 객체의 view가 될 수 있습니다.

4.1.2 객체의 종류

객체 또는 객체의 view는 variable이거나 constant입니다. 가장 흔한 stand-alone object declaration은 다음과 같습니다.

Counter : Integer := 0;
Limit   : constant Integer := 100;
Buffer  : String (1 .. 16) := (others => ' ');

Counter와 Buffer는 variable object이고 Limit은 constant object입니다. 배열이나 레코드는 그 자체가 “객체”라는 뜻이 아니라 배열 타입 또는 레코드 타입을 가진 실행 시점 인스턴스와 그 component가 객체라는 뜻으로 이해해야 합니다.

object declaration의 초기화 식은 선택 사항인 경우가 많지만, 초기화 식을 생략했다고 해서 모든 타입이 자동으로 0이나 빈 값으로 초기화되는 것은 아닙니다. 타입의 default initialization 규칙과 component default에 따라 결과가 달라집니다. 특히 명시적 초기값이 없는 scalar object는 1장에서 설명했듯 invalid representation과 bounded error 문제를 만들 수 있으므로, “선언만 하면 안전한 기본값이 들어간다”고 가정하면 안 됩니다.

반대로 unconstrained array처럼 indefinite subtype만으로 객체의 구체적인 bounds를 결정할 수 없는 경우에는 추가 constraint나 명시적인 initialization expression이 필요합니다. 객체 선언과 초기화의 상세 규칙은 Ada RM 3.3과 3.3.1에서 정의합니다.

4.1.3 서브타입(Subtype)

서브타입은 기존 타입과 별개의 새 타입이 아닙니다. Ada의 subtype은 어떤 타입과 그 타입의 값에 적용되는 constraint, null exclusion 및 subtype별 성질을 함께 나타냅니다.28

subtype Natural is Integer range 0 .. Integer'Last;
subtype Positive is Integer range 1 .. Integer'Last;

위 두 subtype은 Integer와 다른 정수 타입을 만드는 것이 아니라 Integer 값에 서로 다른 range constraint를 둡니다. 같은 타입의 다른 subtype 사이에서는 별도의 타입 변환 없이 값을 사용할 수 있지만, 해당 문맥에서 요구되는 subtype constraint는 검사될 수 있습니다.

Ada의 constraint에는 scalar range뿐 아니라 array의 index constraint, discriminant constraint, fixed/floating-point 관련 constraint 등이 있습니다. access subtype에는 null exclusion도 적용할 수 있습니다. subtype의 값 집합은 원래 타입과 같을 수도 있고 더 작을 수도 있으며, null range처럼 비어 있을 수도 있습니다.

중요하게도 Static_Predicate와 Dynamic_Predicate는 constraint와 별개의 assertion aspect입니다. predicate가 붙었다고 해서 그 subtype이 언어 용어상 “constrained subtype”이 되는 것은 아닙니다.

4.1.4 서브타입 제약의 런타임 검사

값을 더 좁은 scalar subtype에 넣을 때 필요한 range check가 수행되고 그 값이 허용 범위를 벗어나면 Constraint_Error가 발생합니다.

declare
  Source : Integer := -1;
  Target : Natural := 0;
begin
  Target := Source;  -- range check가 실패하면 Constraint_Error
end;

값이 컴파일 시점에 명백하더라도 “범위를 벗어나는 대입은 언제나 compile-time legality error”라고 일반화할 수는 없습니다. 예를 들어 GNAT은 일부 정적으로 명백한 경우 warning을 내면서 실행 파일을 만들고, 실행 시 해당 check가 실패하도록 할 수 있습니다.

또한 Ada의 language-defined check는 Suppress 같은 메커니즘으로 억제될 수 있습니다. 따라서 “어떤 잘못된 값도 항상 안전하게 Constraint_Error로 바뀐다”는 보장은 아닙니다. check suppression과 erroneous execution의 경계는 1장과 12장에서 다시 다룹니다.

4.1.5 Predicate와 의미 제약

Ada 2012 이후에는 Static_Predicate와 Dynamic_Predicate를 이용해 단순 range보다 풍부한 조건을 subtype에 연결할 수 있습니다.29

subtype Even_Number is Integer
  with Dynamic_Predicate => Even_Number mod 2 = 0;

predicate는 assertion aspect이므로 일반 constraint와 검사 제어 방식이 다릅니다. predicate check가 활성화되어 있고 ARM이 지정한 검사 지점에서 조건이 거짓이면, 별도의 Predicate_Failure가 예외를 전파하는 경우를 제외하고 일반적으로 Ada.Assertions.Assertion_Error가 발생합니다. GNAT에서는 -gnata가 assertion 계열 검사를 활성화하는 한 방법이지만 이는 GNAT 옵션이지 Ada 언어 문법이 아닙니다.

또한 Dynamic_Predicate가 붙은 composite object의 component를 바꿀 때마다 predicate가 자동으로 재검사되는 것은 아닙니다. predicate의 정확한 검사 지점과 Assertion_Policy는 12.2~12.3절의 정본 설명을 따릅니다.

4.1.6 제한된 타입 (Limited Types)

Ada의 limited type은 객체의 값을 일반적인 복사 연산으로 복제하지 못하게 하는 타입입니다.30 대표적으로 assignment statement에서 limited object의 값을 다른 limited object로 복사할 수 없고, limited type에는 미리 정의된 equality operator가 없습니다.

type File_Handle is limited record
  Id : Integer;
end record;

limited type은 limited, synchronized, task, protected가 타입 정의에 나타나는 경우뿐 아니라 limited component를 포함한 composite type이나 limited parent에서 파생된 타입 등에서도 생길 수 있습니다. 다만 tagged record가 limited component를 포함한다면 그 record type 정의에도 limited를 명시해야 합니다. untagged composite type은 limited component 때문에 암시적으로 limited가 될 수 있습니다.

limited type이 “값을 전혀 만들거나 전달할 수 없는 타입”이라는 뜻은 아닙니다. Ada는 aggregate나 function call을 이용한 limited object의 initialization을 허용하고, 필요한 경우 결과를 목표 객체에 build in place하도록 요구합니다. 따라서 다음처럼 limited 결과를 반환하는 함수와 in parameter를 사용할 수 있습니다.

type Item is limited record
  Value : Integer;
end record;

function Make (Value : Integer) return Item is
begin
  return (Value => Value);
end Make;

procedure Show (X : in Item);

A : Item := Make (42);

반면 이미 존재하는 두 limited object 사이의 일반 assignment statement는 허용되지 않습니다.

-- B := A;  -- illegal: limited value의 일반 복사

미리 정의된 =이 없다는 것도 “어떤 equality도 정의할 수 없다”는 뜻은 아닙니다. 프로그램이 해당 limited type을 인수로 받는 사용자 정의 = 함수를 선언할 수 있습니다.

자원 수명 관리에서는 Ada.Finalization.Controlled과 Ada.Finalization.Limited_Controlled을 구분해야 합니다. 전자는 복사가 가능한 controlled type의 Initialize/Adjust/Finalize 훅을 제공하고, 후자는 복사되지 않는 limited controlled object를 위한 Initialize/Finalize 모델을 제공합니다. limited object에 대해 Adjust로 복사를 제어한다고 설명하면 두 모델을 혼동하게 됩니다.

파라미터의 mode와 실제 parameter passing mechanism은 limited 여부만으로 결정되지 않습니다. 해당 정본 규칙은 7.3절에서 다룹니다.

4.1.7 상수와 불변 객체

constant object는 일반적인 Ada 코드에서 값 변경을 막는 핵심 수단입니다. constant view는 assignment의 target으로 사용할 수 없고 그 view를 통해 객체의 값을 수정할 수 없습니다.31

Limit : constant Integer := 100;

다만 이를 모든 composite object에 대한 언어 차원의 deep immutability 보장으로 확대해서는 안 됩니다. Ada RM은 controlled 또는 immutably limited part 등을 포함한 constant composite object에서 일부 part에 variable view가 존재할 수 있는 경우를 별도로 정의합니다. 따라서 “constant가 붙으면 도달 가능한 모든 내부 상태가 영원히 불변”이라고 일반화하지 않습니다.

서브프로그램의 in mode parameter도 본체에서 constant view로 다뤄집니다. parameter mode와 aliasing/전달 메커니즘의 상세 규칙은 7.3절에서 다룹니다.

Ada 2022의 delta aggregate는 기존 composite value를 바탕으로 일부 component만 다른 새 값을 표현하는 데 유용하지만, 이것 자체가 원래 객체나 결과를 저장할 객체를 immutable하게 만드는 것은 아닙니다. 함수의 부작용과 purity 역시 객체의 constant 여부와는 별개이며 7.2.2절에서 다룹니다.


4.2 스코프와 가시성

scope와 visibility는 서로 관련되지만 같은 개념이 아닙니다. declaration의 scope는 그 declaration이 영향을 미칠 수 있는 프로그램 텍스트의 범위이고, 그 scope 안에서 실제로 어떤 이름 표기법으로 해당 entity를 참조할 수 있는지는 visibility와 overloading 규칙이 결정합니다.32

블록의 declarative part에 선언한 일반적인 local object declaration은 그 declaration에서 시작하여 둘러싼 declarative region의 끝까지 immediate scope를 가집니다. 그래서 다음처럼 선언 뒤의 statement에서 이름을 사용할 수 있습니다.

declare
  Count : Integer := 0;
begin
  Count := Count + 1;
end;

안쪽 영역에서 같은 direct name을 다시 선언하면 바깥 declaration 자체가 즉시 “scope 밖”으로 사라지는 것이 아니라 direct visibility가 가려질 수 있습니다. 이 차이는 package의 selected name, use clause, overload resolution을 배울 때 중요합니다.

X : Integer := 1;

-- ...
declare
  X : Integer := 2;  -- 이 영역에서 바깥 X의 direct visibility를 가림
begin
  Ada.Text_IO.Put_Line (Integer'Image (X));  -- 2
end;

따라서 “scope = 이름을 아무 조건 없이 직접 쓸 수 있는 범위”라고 정의하면 visibility 규칙을 놓치게 됩니다. package와 use에 따른 visibility의 상세 규칙은 8장에서 다룹니다.


4.3 객체의 생명주기

scope는 소스 텍스트에 관한 정적 개념인 반면, 객체의 lifetime은 실행 중 그 객체가 존재하는 기간에 관한 동적 개념입니다.

block 안의 object declaration은 프로그램이 block에 “진입하는 순간 한꺼번에” 만들어지는 것이 아니라, 실행이 declarative part를 elaboration하면서 각 declaration에 도달할 때 객체를 생성하고 초기화합니다. 초기화 중 예외가 발생하면 그 객체가 성공적으로 initialized되었는지 여부도 이후 finalization에 영향을 줍니다.

block statement의 실행은 master를 이루며, block 실행이 완료된 뒤 그 master를 떠나기 전에 finalization이 수행됩니다.33 이 과정에서는 먼저 master에 의존하는 task의 종료를 기다리고, 그 다음 성공적으로 초기화되어 아직 존재하는 해당 level의 객체를 필요한 경우 finalize합니다. 정상적으로 마지막 statement에 도달한 경우뿐 아니라 exception이나 다른 control transfer로 master를 벗어나는 경우에도 포함된 master의 finalization 규칙이 적용됩니다.

모든 객체의 finalization이 사용자 코드의 destructor 호출을 뜻하는 것은 아닙니다. elementary object는 finalization에 별 효과가 없고, controlled object는 Finalize가 호출되며 task/protected object에는 각각의 언어 규칙이 적용됩니다. finalization 뒤 해당 master와 함께 끝나는 local object는 존재를 마칩니다.

Ada 언어는 이 의미를 “반드시 stack memory가 즉시 운영체제에 반환된다”고 정의하지 않습니다. 실제 저장 위치와 storage 재사용은 구현 문제입니다. 또한 allocator로 생성한 객체는 lexical block의 local object와 다른 수명 규칙을 가질 수 있으므로 접근 타입과 accessibility를 다루는 9장에서 별도로 설명합니다.


5. 연산자와 표현식

표현식(expression)은 평가될 때 값(value)을 산출하는 Ada 구문입니다. 연산자(operator)는 표현식을 구성하는 한 종류의 연산 표기이며, Ada의 많은 연산자는 언어가 미리 정의한 함수처럼 overload될 수 있습니다.34

이 장에서는 산술·관계·논리·membership 연산자, 우선순위와 평가 순서, short-circuit control form, qualified expression, conditional/quantified/reduction expression을 다룹니다. 타입 자체의 의미와 subtype constraint는 4장을, 문장 단위 제어 흐름은 6장을, 사용자 정의 operator와 overload resolution의 자세한 내용은 7장을 선수 지식으로 삼습니다.

5.1 표현식(Expression)의 기본 개념

Ada expression에는 literal, name, function call, type conversion, qualified expression, aggregate, operator expression, conditional expression 등 여러 형태가 있습니다. 따라서 “표현식은 연산자와 피연산자의 결합”이라고만 정의하면 범위가 너무 좁습니다.

42                              -- numeric literal
Count                           -- object의 name
Square (X)                      -- function call
Integer (Value)                 -- type conversion
Integer'(10)                    -- qualified expression
(if Ready then A else B)        -- conditional expression

표현식의 해석에는 expected type과 overload resolution이 중요합니다. 예를 들어 숫자 literal은 처음부터 Standard.Integer 하나에 고정되는 것이 아니라 universal numeric type의 규칙과 주변 문맥을 통해 타입이 결정될 수 있습니다. 반대로 서로 다른 named type 사이의 연산은 표현이 같아 보여도 암시적으로 섞이지 않습니다.

type Meters  is new Integer;
type Seconds is new Integer;

M : Meters  := 10;
S : Seconds := 20;

-- M + S;  -- 서로 다른 타입이므로 직접 더할 수 없음

함수 호출이나 operator의 operand가 여러 개 있을 때 각 operand의 평가 순서를 소스의 왼쪽에서 오른쪽이라고 가정해서는 안 됩니다. Ada가 순서를 명시한 construct를 제외하면 operand 평가 순서는 임의일 수 있습니다. 따라서 서로 다른 operand에 의존적인 side effect를 넣어 특정 평가 순서에 기대는 코드는 피해야 합니다.35

5.2 산술 연산자 (+, -, *, /, mod, rem, abs, **)

Ada는 numeric type에 대해 미리 정의된 산술 연산자를 제공합니다. 실제로 어떤 operator가 사용 가능한지는 operand type과 overload resolution에 따라 결정됩니다.

A + B
A - B
A * B
A / B
A mod B
A rem B
abs A
A ** 3

+와 -는 unary와 binary 형태가 모두 있습니다. 소스의 -10은 lexical level에서 하나의 “음수 literal”이 아니라 unary minus operator와 positive numeric literal의 결합입니다.

정수 나눗셈 /의 몫은 0 방향으로 잘립니다. rem과 mod는 음수 operand에서 결과가 다를 수 있습니다.

(-7) / 3     = -2
(-7) rem 3   = -1
(-7) mod 3   =  2
7 mod (-3)   = -2

rem 결과의 부호는 0이 아니라면 왼쪽 operand와 같고, mod 결과의 부호는 0이 아니라면 오른쪽 operand와 같습니다. 두 번째 operand가 0인 integer division, rem, mod는 필요한 check가 수행될 때 Constraint_Error를 일으킵니다.36

정수 타입의 exponentiation에서 exponent는 음수가 될 수 없습니다. floating-point/fixed-point operator 집합과 overflow/base-range 규칙은 타입 종류에 따라 다르므로, “모든 numeric type이 모든 operator에서 같은 규칙”이라고 일반화하면 안 됩니다.

5.3 관계 연산자 (=, /=, <, <=, >, >=)

관계 연산자는 equality와 ordering 관계를 검사합니다. 결과는 Boolean입니다.

A = B
A /= B
A < B
A <= B

하지만 모든 타입에 모든 관계 연산자가 미리 정의되는 것은 아닙니다. 예를 들어 4장에서 본 limited type에는 predefined equality가 없으며, ordering operator의 정의 가능 여부도 타입 category에 따라 달라집니다.

관계 연산자는 수학 표기처럼 연속해서 chain할 수 없습니다. 범위를 검사하려면 다음처럼 각 관계를 Boolean operator로 연결합니다.

A <= X and then X <= B

5.4 논리 연산자 (and, or, xor, not)

and, or, xor, not은 Boolean type에 미리 정의되며, 적절한 one-dimensional array of Boolean component에도 연산이 정의될 수 있습니다.37

Ready and Enabled
Failed or Cancelled
Left xor Right
not Done

중요한 점은 and와 or 자체가 short-circuit를 보장하지 않는다는 것입니다. 예를 들어 False and Probe에서는 Probe가 평가될 수 있습니다. 두 번째 조건을 반드시 생략해야 한다면 5.7절의 and then과 or else를 사용합니다.

5.5 멤버십 테스트 (in, not in)

membership test는 값이 하나 이상의 membership choice와 일치하는지를 검사합니다.38

X in 1 .. 10
X not in 1 .. 10
X in 1 | 3 | 5
X in Natural

choice에는 값, range, subtype mark 등이 올 수 있으며 Ada 2012 이후에는 |로 여러 alternative를 표현할 수 있습니다. 결과는 Boolean입니다.

membership test를 “subtype conversion”과 혼동하면 안 됩니다. X in Natural은 membership를 검사할 뿐 X를 Natural로 변환하거나 그 자체로 새로운 object를 만들지 않습니다.

5.6 연산자 우선순위 (Operator Precedence)

Ada는 operator class 사이에 우선순위를 정의합니다. 높은 쪽에서 낮은 쪽으로 핵심 순서를 정리하면 다음과 같습니다.39

  1. **, abs, not
  2. *, /, mod, rem
  3. unary +, unary -
  4. binary +, binary -, &
  5. =, /=, <, <=, >, >=, in, not in
  6. and, or, xor

우선순위는 operand의 실제 평가 순서와 다른 개념입니다. *가 +보다 먼저 결합된다고 해서 서로 독립적인 함수 호출 operand가 반드시 왼쪽부터 실행된다는 뜻은 아닙니다.

가독성이나 의미가 조금이라도 모호하다면 괄호를 사용합니다.

Result := (A + B) * C;

5.7 단락 평가 제어 구문 (and then, or else)

Ada 표준은 and then과 or else를 short-circuit control form으로 정의합니다. 일반 and/or와 달리 두 번째 operand의 평가 여부가 언어 의미로 정해져 있습니다.37

if P /= null and then P.all.Ready then
  ...
end if;

and then은 왼쪽 operand가 False이면 오른쪽을 평가하지 않습니다. or else는 왼쪽 operand가 True이면 오른쪽을 평가하지 않습니다. 따라서 null/access 검사나 비용이 큰 후속 조건처럼 두 번째 평가를 피해야 정확하거나 안전한 경우에 중요합니다.

로컬 GNAT 14.2.0 실험에서도 False and then Probe는 Probe를 호출하지 않았지만, False and Probe는 Probe를 호출했습니다.

5.8 한정 표현식 (Qualified Expressions)

qualified expression은 다음 형태입니다.40

subtype_mark'(expression)
subtype_mark'(aggregate)

예를 들어:

type Meters is new Integer;
X : Meters := Meters'(10);

qualification은 type conversion과 같은 연산이 아닙니다. 내부 expression을 주어진 expected type으로 해석하도록 하고, 필요한 subtype constraint check를 적용하는 문맥을 제공합니다. 이미 다른 타입으로 결정된 값을 Meters'(Value)라고 적는다고 임의로 Meters로 변환되는 것은 아닙니다. 타입 변환은 Meters (Value) 형태를 사용합니다.

qualified expression은 aggregate나 overloaded literal/call의 해석이 모호한 곳에서 특히 유용합니다.

5.9 조건부 표현식 (Conditional Expressions)

Ada 2012는 if expression과 case expression을 도입했습니다.41 문장인 if statement/case statement와 달리 이 construct는 선택한 dependent expression의 값을 결과로 만듭니다.

Max : Integer := (if A > B then A else B);

if expression은 condition을 순서대로 검사하고 처음 True인 branch의 dependent expression만 평가합니다. 일반적으로 else가 필요하지만 expected type이 Boolean type인 경우에는 마지막 else를 생략할 수 있습니다. 이때 생략된 else는 True로 취급됩니다. 예를 들어:

Ok : Boolean := (if Enabled then Resource_Ready);

Enabled = False이면 위 expression 결과는 True입니다. 이는 implication 형태의 조건을 간결하게 표현할 때 유용하지만, 모든 if expression에서 else를 생략할 수 있는 것은 아닙니다.

case expression은 selecting expression에 맞는 하나의 alternative를 선택하고 해당 dependent expression만 평가합니다.

type Grade is (A, B, C, F);
My_Grade : Grade := B;

Points : constant Natural :=
  (case My_Grade is
     when A => 4,
     when B => 3,
     when C => 2,
     when F => 0);

choice는 case statement와 마찬가지로 completeness와 중복 관련 legality 규칙을 따라야 합니다.

conditional expression은 많은 문맥에서 괄호로 둘러싸지만, Ada 문법은 특정 context에서 surrounding parentheses를 생략할 수 있도록 허용합니다. 따라서 “conditional expression은 언제나 자체 괄호가 필수”라고 일반화하지 않습니다.

5.10 리덕션 표현식 (Reduction Expressions) (Ada 2022)

Ada 2022는 value sequence에 Reduce attribute를 적용하는 reduction expression을 도입했습니다.42

기본 형태는 다음과 같습니다.

value_sequence'Reduce (Reducer, Initial_Value)

예를 들어 배열의 합은 다음처럼 표현할 수 있습니다.

Values : array (1 .. 5) of Integer := [1, 2, 3, 4, 5];

Total : Integer :=
  [for Value of Values => Value]'Reduce ("+", 0);

위 예제는 GNAT 14.2.0 -gnat2022에서 컴파일·실행되어 15가 됨을 확인했습니다.

Reducer는 두 값을 결합하는 callable entity를 지정하며, attribute argument는 일반 function call의 named parameter 목록이 아닙니다. 따라서 기존 원고에 있던 다음 형태는 Ada 2022 reduction 문법이 아닙니다.

-- 잘못된 형태
-- Values'Reduce (Reducer => Sum_Reducer'Access,
--                Initial_Value => 0)

Ada 2022는 Parallel_Reduce도 정의합니다. 다만 현재 검증 환경의 GNAT 14.2.0에서는 최소 Parallel_Reduce 예제를 받아들이지 않았으므로, 이 책에서는 표준에 존재한다는 사실과 현재 compiler 지원 여부를 분리합니다. 실제 병렬 reduction을 사용할 때는 대상 compiler/runtime의 Ada 2022 지원 상태와 reducer가 병렬 결합에 적합한지 별도로 확인해야 합니다.

reduction에서 연산 순서가 달라져도 같은 결과를 요구한다면 reducer의 associativity, overflow, floating-point rounding 같은 성질을 함께 고려해야 합니다. 특히 병렬 reduction 결과를 순차 left fold와 bit-for-bit 동일하다고 가정하면 안 됩니다.

5.11 수량자 표현식 (Quantified Expressions) (Ada 2012)

quantified expression은 Ada 2012에서 도입되었습니다. Ada 2022 신기능이 아닙니다.43

두 형태가 있습니다.

(for all  X of Values => X > 0)
(for some X of Values => X < 0)

(for all  I in 1 .. 10 => Ready (I))
(for some I in 1 .. 10 => Failed (I))

for all은 predicate가 False가 되는 iteration을 만나면 그 시점에서 결과가 False로 결정되며 더 평가하지 않습니다. for some은 predicate가 True가 되는 iteration을 만나면 결과가 True로 결정되며 더 평가하지 않습니다. 이는 단순히 “compiler가 생략할 수도 있다”는 최적화가 아니라 quantified expression의 동작 규칙입니다.

빈 iteration 범위에서는 논리적 항등에 따라 for all이 True, for some이 False가 됩니다. GNAT 14.2.0에서도 이 결과를 확인했습니다.

Values : array (1 .. 5) of Integer := [10, 20, -5, 30, 40];

All_Positive : Boolean := (for all  V of Values => V > 0);
Has_Negative : Boolean := (for some V of Values => V < 0);

quantified expression은 Boolean 결과를 만드는 expression이며, loop statement를 실행하는 문장과는 구분됩니다. iterator/filter 자체의 정본 설명은 6장에서 이어집니다.

6. 제어 구조와 반복

4장에서 설명한 객체와 스코프, 5장에서 설명한 표현식을 바탕으로 Ada 프로그램의 실행 흐름을 구성하는 문장(statement)을 다룹니다. Ada의 sequence_of_statements는 기본적으로 문장을 차례대로 실행하지만, if, case, block, loop, exit, goto 같은 복합 문장과 제어 전송이 그 순서를 바꿀 수 있습니다. Ada 2022의 parallel construct는 한 task 안에 여러 logical thread of control을 만들어 이 순차 모델을 확장합니다.44

이 장에서는 순차 실행, block과 조건 분기, loop와 iterator, parallel block/loop, 제한적인 goto를 다룹니다. 서브프로그램 호출과 return은 7장, 예외에 의한 제어 전송은 12장, tasking과 protected object는 15장에서 이어서 설명합니다.

6.1 문장(Statements)과 순차적 실행

6.1.1 프로그램 실행의 기본 흐름

Ada에서 sequence_of_statements를 실행하면 그 안의 문장들을 앞에서 뒤로 차례대로 실행합니다. 다만 어떤 문장이 return, exit, goto, 예외 전파와 같은 제어 전송을 수행하면 남은 문장을 건너뛸 수 있습니다. 또한 Ada 2022의 parallel block과 parallel loop에서는 하나의 task 안에서도 둘 이상의 logical thread가 진행될 수 있으므로, “Ada 프로그램 전체가 언제나 한 줄씩 순차 실행된다”고 일반화해서는 안 됩니다.

X := 1;
Y := X + 1;
Put_Line (Integer'Image (Y));

위와 같이 별도의 제어 전송이나 병렬 construct가 없는 sequence에서는 각 문장이 앞 문장의 실행이 끝난 뒤 차례로 실행됩니다.

6.1.2 널 문 (Null Statement)

null;은 아무 동작도 하지 않고 정상적으로 완료되는 null statement입니다.

null;

문법상 하나 이상의 statement가 필요한 위치에서 의도적으로 아무 작업도 하지 않을 때 유용합니다. 예를 들어 case alternative나 exception handler를 비워 두고 싶다면 null;을 둘 수 있습니다.

6.2 블록 문 (Block Statements)

6.2.1 declare를 이용한 지역 변수 선언

block statement는 자체 declarative region과 선택적인 exception handler를 갖는 compound statement입니다. 중요한 점은 모든 block이 declare로 시작하는 것은 아니라는 것입니다. declarative part가 필요할 때만 declare 부분을 둡니다.45

declare
  Temp : Integer := X;
begin
  X := Y;
  Y := Temp;
exception
  when Constraint_Error =>
    null;
end;

선언이 필요 없다면 다음도 완전한 block statement입니다.

begin
  Put_Line ("block");
end;

block에는 이름을 붙일 수도 있습니다.

Work :
begin
  null;
end Work;

block의 declarative part는 실행될 때 elaboration되며, 여기서 선언한 객체의 생성과 초기화도 declaration의 elaboration 규칙을 따릅니다. 단순히 declare 키워드를 만나는 순간 모든 local object가 한꺼번에 만들어지는 것은 아닙니다.

6.2.2 블록의 범위(scope)와 생명주기

block의 declaration에 대한 scope와 visibility, block master에 속한 객체의 lifetime/finalization은 각각 4.2절과 4.3절에서 설명한 규칙을 따릅니다.

block을 정상적으로 끝내거나 예외·제어 전송으로 block master를 벗어나면 필요한 finalization이 수행됩니다. 이것을 “지역 메모리가 항상 즉시 운영체제에 반환된다”는 구현 방식으로 해석해서는 안 됩니다.

6.2.3 병렬 블록 (Parallel Block Statements) (Ada 2022)

Ada 2022의 parallel block은 둘 이상의 sequence_of_statements를 독립적인 activity로 나타냅니다.46

parallel do
  Left_Result := Compute_Left;
and
  Right_Result := Compute_Right;
end do;

정확한 형태는 다음과 같습니다.

parallel [(chunk_specification)] [aspect_specification] do
   sequence_of_statements
and
   sequence_of_statements
{and
   sequence_of_statements}
end do;

각 sequence는 하나 이상의 chunk로 묶이고 각 chunk는 자신의 logical thread of control에서 실행됩니다. 이것은 실제 물리 CPU에서 반드시 동시에 실행된다는 보장이 아닙니다. 구현과 runtime이 여러 logical thread를 어떻게 scheduling할지는 별도 문제입니다.

chunk_specification을 지정했다면 최대 chunk 수가 0보다 큰지 검사하며 실패하면 Program_Error가 발생합니다. parallel construct 안에서 potentially blocking operation을 수행하는 상황은 ARM 5.1이 bounded error로 다룹니다. 한 logical thread가 construct 밖으로 제어를 전송하면 구현은 나머지 logical thread를 취소하려 시도한 뒤 해당 제어 전송을 수행합니다.

현재 이 책의 Debian/GNAT 14.2.0 검증 환경에서는 이 Ada 2022 parallel-block 구문이 파싱되지 않았습니다. 따라서 이 절은 표준 문서 검증 상태이며, 실제 실행 검증은 해당 기능을 구현한 compiler/runtime 환경에서 수행해야 합니다.

6.3 조건문 (Conditional Statements)

6.3.1 if ... then ... end if; 구조

if statement의 각 condition은 expected type이 Boolean인 expression입니다.47

if Temperature < 0.0 then
  Put_Line ("경고: 온도가 영하입니다.");
  Activate_Heater;
end if;

condition이 True이면 해당 sequence를 실행하고, False이면 그 sequence를 건너뜁니다.

6.3.2 else 절: 택일 구조

else가 있으면 앞의 모든 condition이 False인 경우 실행할 sequence를 지정합니다.

if A > B then
  Max := A;
else
  Max := B;
end if;

한 번 True인 alternative가 선택되면 다른 alternative의 statement는 실행하지 않습니다.

6.3.3 elsif 절: 다중 조건 분기

elsif condition들은 위에서 아래로 평가하며 처음 True인 alternative 하나만 실행합니다.

if Score >= 90.0 then
  Grade := 'A';
elsif Score >= 80.0 then
  Grade := 'B';
elsif Score >= 70.0 then
  Grade := 'C';
else
  Grade := 'F';
end if;

각 condition이 논리적으로 서로 배타적이어야 하는 것은 아닙니다. 예를 들어 Score >= 90.0과 Score >= 80.0은 동시에 참일 수 있지만, 앞의 condition이 먼저 참이므로 첫 alternative만 실행됩니다. 따라서 elsif 순서는 프로그램 의미의 일부입니다.

6.4 다중 선택문 (Case Statements)

6.4.1 case 문의 기본 구조

case statement는 하나의 selecting expression을 평가한 뒤 그 값과 일치하는 alternative 하나를 실행합니다. selecting expression의 expected type은 discrete type입니다.48

type Color is (Red, Green, Blue, Yellow);
My_Color : Color := Blue;

case My_Color is
  when Red =>
    Put_Line ("정지 신호");
  when Green =>
    Put_Line ("진행 신호");
  when Blue | Yellow =>
    Put_Line ("대기 신호");
end case;

case choice는 compile-time에 legality를 검사할 수 있도록 static이어야 하며, 같은 값이 둘 이상의 alternative에 포함되어서는 안 됩니다. 또한 필요한 값 범위를 빠짐없이 덮어야 합니다. GNAT 14.2.0에서도 열거형 값 하나를 빠뜨린 최소 예제는 missing case value compile error로 거부되었습니다.

6.4.2 선택지 (when)와 범위 (|, ..)

하나의 alternative에는 단일 discrete choice, |로 연결한 choice 목록, static discrete range 등을 사용할 수 있습니다.

case Day_Number is
  when 1 | 3 | 5 | 7 =>
    Put_Line ("홀수 후보");
  when 10 .. 19 =>
    Put_Line ("10대 범위");
  when others =>
    null;
end case;

choice들의 범위는 겹칠 수 없습니다. case statement의 completeness 검사는 selecting expression의 subtype과 staticness에 따른 ARM의 coverage 규칙을 따르므로, 단순히 “항상 base type의 모든 기계 표현을 직접 열거해야 한다”고 설명해서는 안 됩니다.

6.4.3 when others의 사용

others를 사용하면 앞에서 명시하지 않은 나머지 covered value를 한 alternative로 처리할 수 있습니다. others choice가 있다면 해당 discrete_choice_list는 다른 choice와 섞이지 않으며 마지막 alternative에 놓입니다.

case My_Color is
  when Red =>
    Put_Line ("정지 신호");
  when Green =>
    Put_Line ("진행 신호");
  when others =>
    Put_Line ("기타 신호");
end case;

정상적으로 생성된 값에 대한 compile-time completeness와, invalid representation 같은 비정상적인 값의 실행 의미는 구분해야 합니다. scalar object에 invalid representation이 존재할 수 있는 경계는 1장과 4장에서 다뤘습니다.

값을 반환하는 case expression은 5.9절에서 설명합니다.

6.5 반복문 (Loop Statements)

Ada의 loop statement는 선택적인 이름, iteration scheme, sequence of statements로 구성됩니다.49

6.5.1 기본 loop ... end loop; 구조

iteration scheme이 없는 loop는 본체를 반복합니다.

loop
  Do_Work;
end loop;

자체 종료 condition은 없지만 반드시 영원히 실행된다는 뜻은 아닙니다. exit, return, goto, 예외 전파 등 허용된 control transfer가 loop를 벗어날 수 있습니다.

6.5.2 exit 문: 루프 탈출

이름 없는 exit;는 이를 둘러싸는 가장 안쪽 loop를 종료합니다.

loop
  exit;
end loop;

제어는 종료한 loop 뒤의 다음 statement로 이동합니다.

6.5.3 exit when을 이용한 조건부 탈출

exit when Condition;은 Condition이 True일 때 loop를 빠져나갑니다.

Count : Natural := 0;

loop
  Count := Count + 1;
  exit when Count >= 10;
end loop;

condition은 exit statement에 도달할 때마다 평가됩니다. 이 패턴을 loop 앞이나 뒤쪽에 두어 pre-test/post-test 형태를 구성할 수 있지만 Ada에 별도의 until loop statement가 존재하는 것은 아닙니다.

6.5.4 while ... loop: 선(Pre-condition) 검사 반복

while condition은 각 iteration 전에 평가됩니다.

while Counter > 0 loop
  Put_Line (Integer'Image (Counter));
  Counter := Counter - 1;
end loop;

처음부터 condition이 False이면 본체는 0번 실행됩니다.

6.5.5 for ... loop: 범위 기반 반복 (이산 범위)

for I in discrete_subtype_definition loop 형태의 loop parameter는 loop가 암시적으로 선언하는 constant입니다. programmer가 본체에서 I := ...로 값을 바꿀 수 없습니다.

Total : Integer := 0;

for I in 1 .. 10 loop
  Total := Total + I;
end loop;

discrete_subtype_definition은 loop 진입 때 elaboration됩니다. 따라서 다음 예제에서 Upper_Bound를 본체에서 10으로 바꿔도 현재 loop의 범위는 이미 1 .. 3으로 결정되어 세 번만 실행됩니다.

Upper_Bound : Integer := 3;

for I in 1 .. Upper_Bound loop
  Upper_Bound := 10;
  Put_Line (Integer'Image (I));
end loop;

GNAT 14.2.0 최소 실행에서도 위 성질을 확인했습니다. loop parameter에 대입하려는 예제는 compile-fail했습니다.

loop parameter의 scope는 문법상 parameter specification에서 loop statement 끝까지 이어지지만, 선언 지점 이전에는 이름을 직접 사용할 수 없습니다. 이를 단순히 “scope는 본체 안뿐”이라고 설명하기보다 scope와 visibility를 구분해야 합니다.

null range라면 본체를 0번 실행합니다.

6.5.6 for ... in reverse ... loop: 역방향 반복

reverse는 이미 elaboration된 discrete range의 방향을 뒤집어 순회합니다.

for I in reverse 1 .. 10 loop
  Put_Line (Integer'Image (I));
end loop;

이는 10부터 1까지 순회합니다. 하지만 reverse 1 .. 0이 0 .. 1로 바뀌는 것은 아닙니다. 1 .. 0은 null range이므로 reverse loop도 0번 실행됩니다. GNAT 14.2.0에서도 이 경우 본체가 실행되지 않음을 확인했습니다.

6.5.7 for ... of ... loop: 배열 및 컨테이너 반복

for ... of ...는 iterator specification을 사용해 배열 component 또는 iterable container의 element view를 순회합니다.50

배열에서는 다음처럼 component 자체를 loop parameter view로 받을 수 있습니다.

A : array (1 .. 3) of Integer := (1, 2, 3);

for E of A loop
  E := E * 2;
end loop;

A가 variable이면 위의 E는 variable view가 되어 실제 배열 element를 수정할 수 있습니다. GNAT 14.2.0에서 이 예제는 배열 합을 6에서 12로 바꿨습니다. iterable name이 constant이거나 필요한 variable-indexing view를 제공하지 않는 container라면 loop parameter도 constant view입니다.

다차원 배열의 component iteration은 Ada가 정의한 canonical order를 따릅니다. 이를 target ABI의 row-major/column-major memory layout과 같은 개념으로 섞어서는 안 됩니다. representation convention은 물리 배치의 문제이고 iterator의 language-defined logical order는 별도입니다.

Ada.Containers.Vectors 같은 iterable container에서는 다음처럼 element를 직접 받을 수 있습니다.

for E of My_Vector loop
  Process (E);
end loop;

6.5.8 병렬 loop (Parallel loops) (Ada 2022)

Ada 2022는 parallel iteration scheme을 정의합니다.51

parallel [(chunk_specification)] [aspect_specification]
   for loop_parameter_specification

또는 parallel-capable iterator specification을 사용할 수 있습니다. reverse는 parallel loop parameter specification에서 허용되지 않습니다.

parallel loop는 iteration을 하나 이상의 chunk로 나누며 각 chunk를 별도의 logical thread of control이 실행합니다. for ... in의 경우 각 chunk는 서로 겹치지 않는 subrange를 담당하고 전체 iteration 값을 빠짐없이 덮습니다. parallel-capable iterator의 경우 iterator가 제공하는 chunk 분할 연산을 사용합니다.

parallel (Chunk in Chunk_Index)
  for I in Data_Grid'Range loop
    Partial_Sum (Chunk) := Partial_Sum (Chunk) + Data_Grid (I);
  end loop;

위 패턴에서 각 Chunk가 서로 다른 Partial_Sum component를 담당하도록 만드는 것은 shared accumulator 하나에 모든 logical thread가 동시에 쓰는 것과 다릅니다. 일반적으로 parallel construct에서 같은 variable view에 conflicting action이 생기지 않도록 설계해야 하며, 필요한 공유 상태는 atomic/protected synchronization 등 적합한 메커니즘을 사용해야 합니다. Ada 2022의 conflict-checking 규칙이 있다고 해서 모든 data race 가능성이 자동으로 사라진다고 주장해서는 안 됩니다.

실제 CPU 병렬 실행이나 성능 향상은 표준이 보장하지 않습니다. 또한 현재 Debian/GNAT 14.2.0은 이 parallel-loop 구문 자체를 파싱하지 못하므로, 이 환경에서는 표준 문서 검증만 완료했고 실행 검증은 deferred입니다.

6.5.9 루프 이름(Loop Naming)과 중첩 루프 탈출

loop에는 statement identifier를 붙일 수 있습니다.

Search_Loop :
for Row in Matrix'Range (1) loop
  for Col in Matrix'Range (2) loop
    exit Search_Loop when Matrix (Row, Col) = Target;
  end loop;
end loop Search_Loop;

이름이 붙은 loop에서는 end loop 뒤의 이름도 이에 맞게 적습니다. exit Search_Loop; 또는 exit Search_Loop when Condition;을 사용하면 가장 안쪽 loop가 아니라 명시한 enclosing loop를 종료할 수 있습니다.

6.6 고급 반복자와 데이터 순회

6.6.1 반복자 필터 (Iterator Filter)

Ada 2022는 iterator filter를 추가했습니다. when 뒤의 Boolean condition이 True인 iteration에서만 loop body를 실행합니다.52

Sum : Integer := 0;

for I in 1 .. 10 when I mod 2 = 0 loop
  Sum := Sum + I;
end loop;

GNAT 14.2.0에서 위 예제는 30을 출력했습니다. filter condition 자체는 iteration마다 평가되므로 side effect가 있는 condition을 작성할 때는 그 평가 횟수와 의미를 의식해야 합니다.

배열/컨테이너 element iterator에서도 같은 형태를 사용할 수 있습니다.

for E of Values when E > 0 loop
  Process (E);
end loop;

6.6.2 일반화된 반복자 (Generalized Iterators: for...in Iterator_Object)

Ada 2012의 generalized iterator에서 for C in Iterator_Object 형태의 loop parameter는 일반적으로 element가 아니라 cursor입니다. 이 점을 6.5.7절의 for E of Container element iterator와 구분해야 합니다.

Ada.Containers.Vectors를 예로 들면 다음과 같습니다.

for C in My_Vector.Iterate loop
  Process (My_Vector.Element (C));
end loop;

반면 element iterator는 바로 element view를 제공합니다.

for E of My_Vector loop
  Process (E);
end loop;

GNAT 14.2.0의 Ada.Containers.Vectors 최소 예제에서 두 방식 모두 동일한 두 element의 합 30을 만들었고, generalized iterator의 C에는 Element (C)로 접근해야 함을 확인했습니다.

6.6.3 절차적 반복자 (Procedural Iterators: for...of Iterator_Procedure)

Procedural iterator는 Ada 2022에 추가된 기능입니다.53 iterator procedure가 loop body에 대응하는 access-to-procedure callback을 받아 iteration마다 호출하는 형태를 language syntax로 연결합니다.

대표적인 형태는 다음과 같습니다.

for (Name, Value) of Ada.Environment_Variables.Iterate loop
  Put_Line (Name & "=" & Value);
end loop;

loop parameter specification은 iterator procedure의 callback profile과 일치해야 합니다. compiler가 단순히 임의의 procedure를 “자동 iterator”로 취급하는 것이 아니라 ARM 5.5.3의 profile/legality 규칙을 만족해야 합니다.

현재 Debian/GNAT 14.2.0은 위 procedural-iterator syntax를 identifier expected로 거부했습니다. 따라서 이 절도 Ada 2022 표준 문서로 의미를 검증했으며 로컬 실행 검증은 지원 compiler가 제공되는 환경으로 deferred합니다.

6.6.4 반복 가능한 타입 메커니즘 (Iterable Type Mechanism: Aspects)

Ada에는 Iterable => True라는 language-defined Boolean aspect가 없습니다. 기존 원고처럼 이를 별도 표준 aspect로 쓰면 잘못된 코드가 됩니다.

ARM 5.5.1에서 iterable container type은 indexing과 iterator 관련 aspect의 조합으로 정의됩니다. 핵심은 다음과 같습니다.

  • Default_Iterator: container의 기본 iterator를 만드는 function을 지정합니다.
  • Iterator_Element: iterator가 가리키는 element type을 지정합니다.
  • Constant_Indexing: cursor/index를 통해 constant element view를 얻는 indexing operation을 지정합니다.
  • Variable_Indexing: 가능한 경우 variable element view를 얻는 indexing operation을 지정합니다.

개념적 형태는 다음과 같습니다.

type My_List is tagged private
  with Constant_Indexing => Constant_Reference,
       Variable_Indexing => Reference,
       Default_Iterator  => Iterate,
       Iterator_Element  => Integer;

실제 aspect expression과 profile은 ARM 4.1.5~4.1.6 및 5.5.1의 legality 규칙을 만족해야 하므로 위 조각은 전체 container 구현을 대신하는 복사 가능한 완성 예제는 아닙니다. 표준 Ada.Containers 타입을 사용할 때는 패키지가 이미 필요한 iterator/indexing 계약을 제공합니다.

6.7 무조건 분기문 (goto statement)

6.7.1 레이블(Label) 선언

statement identifier는 <<Label_Name>> 형태의 label로 statement에 붙일 수 있습니다.54

<<Retry>>
null;

엄밀히 말하면 <<Retry>> 자체가 일반 object declaration처럼 explicit declaration인 것은 아닙니다. statement identifier에는 둘러싸는 body/block의 declarative part 끝에 implicit declaration이 있는 것으로 정의됩니다.

Ada 2022는 nonempty sequence_of_statements 끝에 label만 남는 형태도 허용하며, 이 경우 trailing label 뒤에 implicit null statement가 있는 것으로 봅니다.

begin
  Do_Work;
  <<Done>>
end;

GNAT 14.2.0에서도 앞에 실제 statement가 있는 위 trailing-label 형태가 컴파일됨을 확인했습니다.

6.7.2 goto 문의 사용과 제한

goto Error_Handler;

goto target에는 단순한 “같은 scope”보다 더 정확한 reachability 규칙이 적용됩니다. target statement를 가장 안쪽에서 감싸는 sequence_of_statements는 goto statement도 함께 감싸야 합니다. 따라서 바깥 sequence에서 if, loop, block, case alternative 같은 더 안쪽 sequence의 label로 뛰어들 수 없습니다.

-- illegal: 바깥에서 if alternative 내부로 진입
-- goto Inside;
-- if Ready then
--   <<Inside>>
--   null;
-- end if;

반대로 inner sequence에서 이를 둘러싸는 outer sequence의 뒤쪽 label로 빠져나오는 전송은 허용될 수 있습니다.

if Failed then
  goto Done;
end if;

Do_Normal_Work;

<<Done>>
null;

GNAT 14.2.0 최소 실험에서도 첫 형태는 target of goto statement is not reachable로 compile-fail했고, 두 번째 형태는 정상 실행됐습니다.

goto가 block/master를 벗어나면 해당 construct에 필요한 finalization과 task waiting 규칙을 우회하는 것이 아닙니다. 제어 전송은 language-defined finalization 규칙과 함께 해석해야 합니다.

6.7.3 goto 문과 프로그램 구조

goto는 Ada가 제공하는 합법적인 control transfer이며 저수준 상태 기계나 생성 코드 등에서 적합한 경우가 있습니다. 다만 일반적인 application logic에서는 if, case, loop, 이름 붙은 loop와 exit, return, 적절한 예외 처리처럼 정적 구조가 드러나는 construct가 실행 흐름을 추적하기 더 쉬운 경우가 많습니다.

따라서 “goto는 언제나 나쁘다”는 규칙보다는, 왜 구조화된 construct로 표현하기 어려운 전송이 필요한지 설명할 수 있을 때 제한적으로 사용하는 것이 유지보수에 유리합니다.

7. 서브프로그램 (Subprograms)

7장에서는 Ada에서 실행 가능한 알고리즘의 기본 단위인 서브프로그램(subprogram)을 다룹니다. Ada Reference Manual 6장의 정의에 따르면 서브프로그램은 서브프로그램 호출(subprogram call)에 의해 실행되는 프로그램 단위(program unit) 또는 고유 연산(intrinsic operation)입니다.

서브프로그램에는 두 형태가 있습니다.

  • 프로시저(procedure): 호출 자체가 문장(statement)입니다.
  • 함수(function): 호출이 표현식(expression)을 이루며 결과 값을 반환합니다.

서브프로그램의 인터페이스는 선언(declaration)으로 먼저 제시하고 나중에 본체(body)로 완성할 수 있습니다. 선언과 본체가 분리되어 있지 않은 경우에는 본체가 서브프로그램을 직접 정의합니다. 연산자와 열거형 리터럴도 함수입니다(ARM 6).

프로시저와 함수를 먼저 구분한 뒤 파라미터 모드와 실제 전달 메커니즘의 차이를 살펴봅니다. 이어 같은 이름의 여러 서브프로그램이 컴파일 시점의 문맥에서 어떻게 구별되는지 설명하고, 연산자 중복정의를 함수 중복정의의 특수한 경우로 다룹니다. 마지막에는 재귀 호출과 종료·자원 한계를 살펴봅니다.

7.1 프로시저 (procedure)

7.1.1 프로시저의 기본 구조 및 선언

프로시저는 특정 동작 또는 일련의 작업을 수행하는 서브프로그램입니다. 프로시저 호출(procedure call)은 하나의 문장(statement)이며, 프로시저 자체에는 함수처럼 결과 타입이 없습니다. 필요한 결과는 out/in out 파라미터나 다른 프로그램 상태를 통해 전달할 수 있습니다.

프로시저의 인터페이스를 별도 선언으로 제공할 때는 이름과 파라미터 프로파일을 명시합니다.

procedure 식별자 (파라미터_프로파일);

본체는 동일한 서브프로그램을 완성하는 specification 뒤에 선언 영역과 실행할 문장을 둡니다.

procedure 식별자 (파라미터_프로파일) is
   -- 로컬 선언
begin
   -- 실행될 문장
exception
   -- 예외 처리
end 식별자;

프로시저에는 식을 동반하지 않는 return; 문을 사용할 수 있습니다. 이 문은 값을 반환하는 것이 아니라 해당 프로시저의 실행을 끝내고 호출자에게 제어를 돌려줍니다(ARM 6.5).

7.1.2 프로그램의 주 실행점(Main Entry Point) 요구 사항

Ada 언어의 main subprogram 규칙과 GNAT/GPRbuild가 실행 파일의 main을 선택하는 방법은 구분해야 합니다.

Ada 2022 ARM 10.2에서 사용자는 구현이 정하는 방법으로 한 서브프로그램을 partition의 main subprogram으로 지정할 수 있습니다. main이 지정되었다면 그것은 서브프로그램이어야 하지만, 표준이 모든 구현에 대해 “반드시 패키지 밖의 parameterless procedure만 가능하다”고 제한하지는 않습니다. main에 파라미터가 있을 때 실제 인수를 어디서 얻는지, main이 함수일 때 결과를 어떻게 처리하는지는 구현 정의입니다.

구현은 지원하는 main의 종류를 제한할 수 있습니다. 다만 Ada 구현은 최소한 public parameterless library procedure를 main으로 지원해야 합니다. 따라서 다음 형태는 이식성 있는 기본 선택입니다.

with Core_Logic;

procedure Hello is
begin
   Core_Logic.Run;
end Hello;

위와 같이 main을 얇은 래퍼(thin wrapper)로 두고 실제 애플리케이션 로직을 패키지에 넣는 구성은 실용적인 설계 패턴이지 Ada 언어가 강제하는 구조는 아닙니다. GNAT의 소스 파일 명명, gprbuild의 Main 속성, 실행 파일 생성 방법은 13.3.1절에서 다룹니다.

7.2 함수 (function)

함수 호출(function call)은 표현식(expression)을 이루고 결과 값을 반환합니다. Ada의 연산자(operator)와 열거형 리터럴(enumeration literal)도 함수입니다(ARM 6).

함수의 인터페이스는 이름, 파라미터 프로파일, 결과 subtype을 정의합니다.

function 식별자 (파라미터_프로파일) return 타입_이름;

일반적인 함수 본체는 다음과 같습니다.

function 식별자 (파라미터_프로파일) return 타입_이름 is
   -- 로컬 선언
begin
   -- 실행될 문장
   return 결과값;
exception
   -- 예외 처리
end 식별자;

Ada 2022에서 함수 본체는 해당 함수에 적용되는 return 문을 적어도 하나 포함해야 합니다(기계 코드 code_statement를 포함하는 특수한 경우는 제외, ARM 6.5). 함수에 적용되는 단순 return 문에는 결과 식이 필요합니다.

함수라고 해서 모든 파라미터가 항상 in인 것은 아닙니다. 일반 함수는 out 또는 in out 파라미터를 가질 수 있으므로 호출 과정에서 다른 상태를 변경할 수도 있습니다. 파라미터 모드의 정확한 규칙은 7.3절에서, 연산자 함수에 적용되는 더 강한 제한은 7.5절에서 구분해 설명합니다.

7.2.1 표현식 함수 (Expression Functions)

표현식 함수(expression function)는 본체가 사실상 하나의 return으로 이루어진 함수를 간결하게 선언하는 구문입니다(ARM 6.8). Ada 2022 문법은 괄호로 둘러싼 expression뿐 아니라 aggregate 형태도 허용합니다.

function 함수_이름 (파라미터_목록) return 반환_타입 is
   (표현식);

괄호 안 expression의 expected type은 함수 결과 타입입니다. 호출 시에는 이 표현식 함수에 대응하는 암시적 함수 본체가 실행되고, return expression의 값이 함수 결과가 됩니다.

정수 값을 제곱하는 예는 다음과 같습니다.

-- 일반적인 함수 본체
function Square (X : Integer) return Integer is
begin
   return X * X;
end Square;

-- 표현식 함수
function Square (X : Integer) return Integer is
   (X * X);

두 정의는 같은 계산을 표현합니다. 두 선언을 같은 위치에 동시에 두는 예제가 아니라, 동일한 함수를 두 방식 중 하나로 구현할 수 있음을 비교한 것입니다.

7.2.2 순수 함수와 부작용

함수형 프로그래밍에서 순수 함수(pure function) 는 보통 외부 상태를 변경하는 부작용이 없고, 같은 입력에 대해 같은 결과를 내는 함수라는 일반 개념을 가리킵니다. 이 개념과 Ada의 Pure 애스펙트는 이름은 비슷하지만 같은 규칙이 아닙니다.

Ada의 Pure는 라이브러리 단위의 범주입니다

Ada 2022에서 Pure는 라이브러리 단위(library unit) 에 적용되는 언어 정의 애스펙트입니다(ARM 10.2.1). Pure => True인 라이브러리 단위는 선언과 본체의 elaboration, 다른 라이브러리 항목에 대한 의미적 의존성 등에 제약을 받습니다. 예를 들어 pure program unit의 elaboration에는 변수 선언이나 일반적인 access-to-variable allocator 같은 동작이 허용되지 않습니다.

따라서 Pure를 개별 함수에 붙여 “이 함수에는 부작용이 없다”고 선언하는 계약으로 이해하면 안 됩니다. Ada 2022는 declared-pure library unit의 library-level 서브프로그램 호출을 구현이 생략하거나 이전 결과로 재사용할 수 있도록 허용하며, 그 허용은 해당 서브프로그램이 호출될 때 다른 부작용을 일으키더라도 적용될 수 있다고 명시합니다. 즉 Ada의 Pure는 함수형 프로그래밍에서 말하는 순수 함수와 직접 대응하지 않습니다.

package Math_Tools
  with Pure
is
   function Square (X : Integer) return Integer;
end Math_Tools;

위 예제의 Pure는 Square 하나의 부작용 없음 계약이 아니라 Math_Tools라는 라이브러리 패키지의 categorization을 지정합니다.

Global은 Ada 2022의 언어 정의 애스펙트입니다

Ada 2022의 Global은 서브프로그램, 엔트리, access-to-subprogram 타입, 태스크·보호 단위, 라이브러리 패키지 등에 지정할 수 있는 언어 정의 애스펙트입니다(ARM 6.1.2). 실행 중 읽거나 갱신할 수 있는 전역 변수 집합을 기술합니다.

function Square (X : Integer) return Integer
  with Global => null;

Global => null은 해당 연산의 Global 애스펙트가 식별하는 전역 변수 집합이 비어 있음을 뜻합니다. 다만 Ada의 global-effect 규칙에는 access 값 역참조로 도달하는 객체처럼 별도로 다뤄지는 경우가 있으므로, 이를 “어떤 형태의 외부 효과도 절대로 없다”는 일반적인 순수성 선언과 동일시해서는 안 됩니다.

SPARK와 GNATprove는 이 Global 정보를 데이터 의존성 계약으로 활용합니다. GNATprove의 flow analysis는 구현이 지정된 Global/Depends 등의 데이터 흐름 계약을 지키는지 검사합니다. 따라서 일반적인 순수 함수 개념, Ada의 library-unit Pure, Ada 2022의 Global, SPARK/GNATprove의 정적 분석은 서로 연결될 수는 있지만 각각 다른 층위의 개념입니다.

7.3 파라미터 모드 (Parameter Modes)

Ada의 formal parameter는 in, in out, out 세 모드 중 하나를 가집니다(ARM 6.1). 모드를 생략하면 in이며, 모드는 호출자와 서브프로그램 사이에서 정보가 어느 방향으로 흐르는지를 설명합니다. 모드와 실제 전달 메커니즘(by copy/by reference)은 서로 다른 규칙입니다.

ARM 6.2는 타입에 따라 전달 메커니즘을 정합니다.

  • by-copy 타입: elementary type과, full type이 by-copy인 private type의 descendant 등은 원칙적으로 by copy로 전달됩니다. 다만 explicitly aliased formal은 by reference입니다.
  • by-reference 타입: tagged type, task/protected type, explicitly limited record type, by-reference subcomponent를 가진 composite type 등은 by reference로 전달됩니다.
  • 그 밖의 타입은 by copy인지 by reference인지 지정되지 않은 경우가 있습니다. 따라서 프로그램 의미가 두 메커니즘 중 특정 하나에만 의존하도록 작성해서는 안 됩니다.

7.3.1 in 모드 (Mode in)

in formal parameter는 constant view입니다. 본체에서 그 formal 자체를 갱신할 수 없으며, 호출의 actual parameter는 expression으로 해석됩니다(ARM 6.2, 6.4.1).

by-copy라면 actual의 값을 formal의 nominal subtype으로 변환하여 별도 formal object에 넣습니다. by-reference라면 formal은 actual object의 변환된 view를 나타냅니다. 어느 경우든 in이라는 모드 자체를 “항상 값 복사”라는 뜻으로 해석해서는 안 됩니다.

7.3.2 out 모드 (Mode out)

out formal parameter는 variable view이며, actual parameter는 변수를 나타내는 name이어야 합니다. 따라서 리터럴이나 일반 식을 out actual로 전달할 수 없습니다(ARM 6.4.1).

by-copy인 out parameter는 호출 시작 시 무조건 “완전히 초기화되지 않는다”고 단순화할 수 없습니다. access type, Default_Value가 있는 scalar type, discriminant나 implicit initial value가 있는 일부 composite type에는 기존 actual의 값 또는 초기화 규칙이 관여하고, 그 밖의 경우에는 formal이 초기화되지 않을 수 있습니다. 따라서 out은 입력값을 전달받기 위한 모드가 아니라 결과를 써서 돌려주기 위한 모드로 사용하는 것이 기본적인 해석입니다.

서브프로그램이 정상적으로 끝나면 by-copy인 out formal의 최종 값은 actual 변수의 subtype으로 변환되어 actual에 다시 할당됩니다. by-reference라면 formal을 통한 갱신은 실제 actual object에 직접 적용됩니다.

7.3.3 in out 모드 (Mode in out)

in out formal parameter는 variable view이며 읽기와 갱신을 모두 허용합니다. actual은 out과 마찬가지로 변수를 나타내는 name이어야 합니다.

by-copy라면 호출 전에 actual 값이 formal로 복사되고, 정상 종료 후 formal의 최종 값이 actual로 copy back됩니다. by-reference라면 formal이 actual object의 view를 나타내므로 읽기와 갱신이 실제 object를 대상으로 일어납니다.

파라미터 전달 메커니즘이 지정되지 않은 타입에서 aliasing에 의존하는 코드는 특히 주의해야 합니다. ARM 6.2와 6.4.1에는 서로 다른 access path를 통해 같은 객체를 갱신·조회할 때 적용되는 bounded error 및 legality rule이 별도로 정의되어 있습니다.

7.3.4 매개변수 기본값 (Default Parameter Expressions)

default_expression은 mode in인 formal parameter에만 지정할 수 있습니다(ARM 6.1). 호출에서 해당 actual association을 생략하면 그 시점에 대응하는 default expression이 actual parameter로 사용됩니다. 기본식은 서브프로그램 선언을 elaboration할 때 미리 평가되는 것이 아닙니다.

function Open_Display
  (Display_Name : String := "") return Display_Handle;

모든 파라미터가 기본값으로 채워질 수 있는 함수는 expression 문맥에서 actual parameter part 자체를 생략할 수 있습니다.

Display := Open_Display;              -- Display_Name => ""
Display := Open_Display ("server:0"); -- 명시적 actual

Open_Display ()처럼 빈 괄호를 쓰는 문법은 Ada에 없습니다. actual parameter part를 쓴다면 괄호 안에 적어도 하나의 parameter association이 있어야 합니다(ARM 6.4). 또한 함수 호출은 expression이므로 C 계열 언어처럼 반환값을 버리고 Open_Display;만을 독립 statement로 쓰는 형태와도 구분해야 합니다.

7.4 서브프로그램 중복정의 (Subprogram Overloading)

Ada에서는 같은 defining name을 가진 여러 callable declaration이 한 지점에서 모두 직접 보일 수 있으며, 이들을 overloaded declaration이라고 합니다(ARM 8.3). 어떤 선언이 실제로 의미되는지는 실행 중 동적 탐색으로 고르는 것이 아니라 컴파일 시점의 overload resolution으로 결정됩니다(ARM 8.6).

호출의 actual parameter, named association, expected type 등 전체 문맥이 가능한 해석을 줄이는 데 사용됩니다. 함수의 결과 타입도 문맥을 통해 해석을 구별하는 데 사용될 수 있습니다.

function Pick return Integer is (1);
function Pick return Float   is (2.0);

I : Integer := Pick; -- Integer 결과의 Pick
F : Float   := Pick; -- Float 결과의 Pick

다만 “선언 모양이 조금이라도 다르면 overload할 수 있다”는 뜻은 아닙니다. Ada의 homograph 판정에는 type conformance가 사용되며(ARM 6.3.1, 8.3), 파라미터 mode만 in과 out으로 바꾸고 타입을 그대로 둔 두 프로시저는 서로 다른 overload로 둘 수 없습니다.

타입이 다른 파라미터로 같은 연산 이름을 제공하는 전형적인 예는 다음과 같습니다.

procedure Put (Item : Integer);
procedure Put (Item : String);

Put (123);       -- Integer 버전
Put ("Hello");   -- String 버전

주어진 complete context에서 허용 가능한 해석이 하나로 결정되지 않으면 호출은 ambiguous하여 불법입니다. 필요하면 qualification이나 명시적 변환 등으로 문맥을 더 분명하게 만들 수 있습니다.

7.5 연산자 중복정의 (Operator Overloading)

Ada에서 연산자(operator)는 operator_symbol을 designator로 갖는 함수입니다(ARM 6.6). 단항 또는 이항 연산자 사용은 해당 연산자 함수를 각각 하나 또는 두 개의 positional actual parameter로 호출하는 것과 동등하게 해석됩니다.

연산자 함수에는 일반 함수보다 추가 제한이 있습니다.

  1. 단항 연산자는 파라미터가 하나, 이항 연산자는 두 개여야 합니다.
  2. 연산자의 모든 formal parameter는 in 모드여야 합니다.
  3. 연산자 파라미터에는 default_expression을 둘 수 없습니다.
  4. 결과 타입이 predefined Boolean인 =을 명시적으로 선언하면 complementary result를 주는 /= 연산자가 암시적으로 함께 선언됩니다.
  5. /=을 명시적으로 선언할 때 그 결과 타입을 predefined Boolean로 할 수 없습니다.

프로시저는 operator_symbol을 defining name으로 사용할 수 없습니다. Ada 2022의 procedure_specification은 defining_program_unit_name을 사용하지만 function_specification은 defining_designator를 사용하며, operator_symbol은 함수 쪽 designator에 속합니다(ARM 6.1). 따라서 procedure "+" (...)와 같은 “in-place operator procedure”는 Ada 연산자 중복정의가 아닙니다. 상태를 변경하는 연산이 필요하면 Add_To 같은 일반 프로시저 이름을 사용합니다.

또한 :=은 사용자 정의 operator function의 operator_symbol이 아니라 Ada의 assignment statement 문법이므로 중복정의할 수 없습니다.

예를 들어 Vector 타입의 +는 다음처럼 정의할 수 있습니다.

function "+" (Left, Right : Vector) return Vector;

A, B, C가 Vector 객체라면 다음 두 식은 같은 연산자 함수를 나타냅니다.

A := B + C;
A := "+" (B, C);

7.6 재귀 서브프로그램 (Recursive Subprograms)

Ada 서브프로그램은 재귀적으로 호출할 수 있습니다(ARM 6.1 Note 4). 재귀는 다음 두 형태로 나타날 수 있습니다.

  1. 직접 재귀(Direct Recursion): 서브프로그램이 자신의 본체에서 자기 자신을 호출합니다.
  2. 간접 재귀(Indirect Recursion): 여러 서브프로그램의 호출 연쇄를 거쳐 원래 서브프로그램이 다시 호출됩니다.

Ada 언어가 재귀 함수에 “base case가 있어야 한다”는 legality rule을 두는 것은 아닙니다. 그러나 정상 종료해야 하는 재귀 알고리즘이라면 호출 깊이를 줄여 결국 도달할 수 있는 종료 조건을 설계해야 합니다. 끝나지 않는 재귀나 지나치게 깊은 호출은 서브프로그램에 사용할 수 있는 저장 공간을 소진할 수 있으며, Ada의 Storage_Check는 task 또는 subprogram에 사용할 공간이 초과되는 상황을 검사하고 실패 시 Storage_Error에 대응합니다(ARM 11.5).

재귀가 깊어지기 전에 다른 런타임 검사가 먼저 실패할 수도 있습니다. 예를 들어 다음 팩토리얼 함수는 작은 입력에서는 정상적으로 동작하지만, 결과가 Integer 계열의 범위를 넘으면 재귀 깊이 한계보다 먼저 곱셈 overflow에 의해 Constraint_Error가 발생할 수 있습니다.

function Factorial (N : Natural) return Positive is
begin
   if N = 0 then
      return 1;
   else
      return N * Factorial (N - 1);
   end if;
end Factorial;

Factorial (5)는 120을 반환합니다. 이 예제의 핵심은 재귀 문법 자체가 아니라 종료 조건, 결과 타입의 범위, 실행 중 필요한 저장 공간을 함께 고려해야 한다는 점입니다.

8. 패키지와 컴파일 단위

패키지(package)는 논리적으로 관련된 타입, 객체, 서브프로그램과 다른 선언을 하나의 명명된 영역에 묶는 Ada의 program unit입니다. 패키지는 interface와 implementation을 분리하고, private view와 visibility 규칙을 이용해 정보 은닉 경계를 만들 수 있습니다.55

이 장에서는 언어의 package/program-unit 규칙과 GNAT의 source-file/build 관례를 구분합니다. 패키지는 중첩해서 선언할 수도 있으므로, 모든 package declaration이 곧 별도 파일이나 compilation unit인 것은 아닙니다. 별도 컴파일되는 library package일 때 package declaration과 필요한 package body가 각각 library item으로 다뤄집니다.

8.1 패키지의 개념: 명세와 본체

Ada package는 항상 package specification을 가지지만 모든 package가 명시적인 package body를 가지는 것은 아닙니다. package declaration이 별도의 completion을 요구하는 선언을 포함하면 body가 필요하며, library package에서는 Elaborate_Body로 body를 요구하도록 할 수도 있습니다.

8.1.1 패키지 명세부 (Package Specification)

package specification의 첫 번째 declaration list가 visible part이고, private 뒤의 선택적인 declaration list가 private part입니다. private가 없으면 빈 private part가 암시됩니다.

package Counters is
   type Counter is private;

   procedure Increment (C : in out Counter);
   function Value (C : Counter) return Integer;
private
   type Counter is new Integer range 0 .. 1_000;
end Counters;

클라이언트가 일반 nonlimited with clause로 library package를 문맥에 넣으면 package library item 자체를 이름으로 사용할 수 있게 됩니다. 외부에서 expanded name으로 접근할 수 있는 선언은 visible part에 있는 것들입니다. private part의 선언은 일반 클라이언트에게 직접 노출되지 않습니다.

명세에는 반드시 “signature만” 들어가는 것도 아닙니다. 예를 들어 null procedure declaration이나 expression function declaration처럼 명세 안에서 completion까지 제공하는 선언 형태도 있습니다.

package Constants is
   Answer : constant := 42;
   function Get_Answer return Integer is (Answer);
   procedure Reset is null;
end Constants;

이 package는 위 선언들만으로 별도 body를 요구하지 않습니다. 반대로 단순한 procedure P;처럼 completion을 요구하는 선언을 명세에 두고 명세 안에서 completion을 제공하지 않으면 package body가 필요합니다.

8.1.2 패키지 본체부 (Package Body)

package body는 명세가 요구한 body들을 완성하고, 클라이언트에 공개하지 않을 내부 선언을 둘 수 있습니다.

package body Counters is
   procedure Increment (C : in out Counter) is
   begin
      if C < Counter'Last then
         C := C + 1;
      end if;
   end Increment;

   function Value (C : Counter) return Integer is
     (Integer (C));
end Counters;

body의 declarative part 뒤에는 선택적으로 begin ... exception ... end 형태의 handled sequence를 둘 수 있습니다. 이 statement part는 package body가 elaboration될 때 실행됩니다. statement part가 없으면 언어 의미상 implicit null statement가 있습니다.

library package declaration은 필요하지 않은 body를 임의로 가질 수 있는 것이 아닙니다. body가 필요한 package declaration이거나 Elaborate_Body가 적용된 경우 등에 body가 허용됩니다. 예를 들어 상수 하나만 있는 library package에 이유 없이 .adb body를 추가하면 GNAT도 이를 거부합니다.

명세와 본체를 분리하면 클라이언트의 언어 차원의 semantic dependency를 구현 body가 아니라 공개 declaration에 둘 수 있습니다. 다만 “body만 바꾸면 어떤 build system에서도 client object를 절대로 다시 만들지 않는다”고 확대해서는 안 됩니다. 재컴파일·최적화·링크 전략은 구현과 빌드 도구에 따라 달라질 수 있습니다.

8.1.3 패키지 파일 명명 규칙 (GNAT 관례)

Ada 표준은 source file의 이름을 .ads/.adb로 정하지 않습니다. 다음은 GNAT의 기본 naming convention입니다.56

Ada unit GNAT 기본 파일 이름
My_Util 명세 my_util.ads
My_Util 본체 my_util.adb
Parent.Child 명세 parent-child.ads
Parent.Child 본체 parent-child.adb
Split의 Worker subunit split-worker.adb

기본 규칙은 full expanded unit name을 소문자로 만들고 unit-name의 점을 하이픈으로 바꾼 뒤, 명세에는 .ads, body에는 .adb를 붙입니다. 따라서 package My_Util_Body is ...가 my_util.adb를 뜻하는 것은 아닙니다. 그것은 My_Util_Body라는 별도의 package 이름입니다.

GNAT은 project/configuration 설정으로 기본 naming scheme을 바꿀 수도 있습니다. 그러므로 파일 이름은 Ada 언어 의미가 아니라 선택된 GNAT source naming configuration의 일부로 취급해야 합니다.

8.2 패키지 사용과 가시성 (Package Usage and Visibility)

with와 use는 서로 다른 문제를 다룹니다. with는 compilation unit 사이의 semantic dependency와 library-item visibility를 만들고, use는 이미 visible한 package/type 관련 선언을 직접 이름으로 쓸 수 있게 하는 visibility 도구입니다.57

8.2.1 with 절: semantic dependency와 library-item visibility

with Ada.Text_IO;

procedure Hello is
begin
   Ada.Text_IO.Put_Line ("Hello, World!");
end Hello;

with Ada.Text_IO;는 현재 compilation unit이 Ada.Text_IO library item에 semantically dependent하도록 하고 그 library unit 선언을 해당 범위에서 visible하게 합니다. 그러나 Put_Line이라는 내부 선언을 자동으로 directly visible하게 만들지는 않습니다. 그래서 with만 쓴 위 예에서는 Ada.Text_IO.Put_Line이라는 selected/expanded name을 사용합니다.

with Ada.Text_IO;

procedure Bad is
begin
   -- Put_Line ("x"); -- `use`도 selected name도 없으므로 보이지 않음
   null;
end Bad;

context clause에는 일반 with 외에도 limited with와 private with 같은 형태가 있습니다. 특히 limited with는 library package의 limited view만 의존하게 하여 일부 순환 의존성을 끊는 데 쓰이지만, normal with와 같은 full view를 제공하지 않습니다. 이 장에서는 기본 nonlimited with를 중심으로 설명합니다.

8.2.2 use 절: 직접 가시성

with Ada.Text_IO;
use Ada.Text_IO;

procedure Hello is
begin
   Put_Line ("Hello, World!");
end Hello;

use P;는 P의 visible declaration 가운데 해당 지점에서 visible한 선언을 potentially use-visible하게 만들고, 이름 충돌 규칙을 통과한 선언을 use-visible하게 합니다. use clause는 context clause에도, declarative region 안에도 둘 수 있습니다. 따라서 “항상 같은 source unit의 with 바로 다음에 와야 한다”는 문법 규칙은 아닙니다. 필요한 것은 use에 적은 package name이 그 지점에서 적법하게 visible한 것입니다.

use된 두 package에 같은 identifier가 있다고 해서 항상 compile error가 되는 것도 아닙니다. overloadable declaration들은 하나의 overload set을 이룰 수 있고 호출 문맥이 구별해 줄 수 있습니다.

-- A.Process (Integer), B.Process (Float)가 있다고 가정
use A;
use B;

Process (1);    -- Integer profile로 해석 가능
Process (1.0);  -- Float profile로 해석 가능

반대로 두 package가 구별할 수 없는 동일 profile의 Process (Integer)를 각각 제공하면 Process (1)은 ambiguous합니다. 이런 경우 A.Process (1)처럼 선택 이름으로 명확히 지정할 수 있습니다.

package와 내부 type의 이름이 같은 경우도 visibility 규칙으로 설명해야 합니다.

package Counter is
   type Counter is private;
private
   type Counter is new Integer;
end Counter;

client에서 root package 이름 Counter가 직접 visible한 상태에서 use Counter;를 하더라도, 내부 type Counter라는 homograph가 단순히 package 이름과 함께 모호한 후보로 남는다고 생각하면 정확하지 않습니다. use-visibility의 homograph 규칙 때문에 My_Value : Counter;에서는 root package가 보이고 subtype mark가 되지 못해 오류가 날 수 있습니다. 이런 구조에서는 다음처럼 full expanded name을 사용하는 것이 명확합니다.

My_Value : Counter.Counter;

8.2.3 use type 절

use type T;는 T의 primitive operator를 use-visible하게 합니다. “T를 인수나 결과로 사용하는 모든 함수·프로시저”를 보이게 하는 구문은 아닙니다.

with Ops;

procedure Example is
   use type Ops.T;
   A, B : Ops.T := 1;
   C    : Ops.T;
begin
   C := A + B;
   if C = 2 then
      null;
   end if;

   -- Touch (A); -- Touch가 identifier인 primitive procedure라면 use type만으로는 안 보임
end Example;

Ada의 use all type T; 형태는 primitive operator뿐 아니라 해당 타입의 primitive subprogram들을 use-visible하게 하는 더 넓은 형태입니다. 따라서 package 전체를 use하지 않고도 타입 중심 API의 primitive operation을 직접 이름으로 사용할 수 있습니다.

use all type Ops.T;
Touch (A);

use type/use all type은 visibility를 바꾸는 것이지 새로운 연산을 만들거나 runtime 동작을 추가하는 것이 아닙니다.

8.2.4 renames를 이용한 인터페이스 재구성

renames는 기존 entity를 복사해 새 entity를 만드는 기능이 아니라, 기존 entity에 대한 새 view/name을 선언합니다. Ada에는 object, exception, package, subprogram, generic renaming이 있습니다.58

package Low_Level is
   procedure Close (Handle : Integer);
end Low_Level;

with Low_Level;
package File_API is
   procedure Close (Handle : Integer) renames Low_Level.Close;
end File_API;

File_API.Close는 별도의 wrapper body를 복사해 만든 것이 아니라 renamed subprogram의 새 view입니다. 이런 renaming declaration을 package visible part에 두면 client-facing interface를 재구성하는 데 사용할 수 있습니다. 다만 re-export는 Ada RM의 별도 linkage 개념이 아니라 이런 visibility 배치의 결과를 설명하는 설계 용어로 이해하는 편이 정확합니다.

8.3 private 타입을 이용한 정보 은닉 (Information Hiding via private Types)

private type은 하나의 타입에 대해 외부 client가 보는 partial view와 package 내부에서 보이는 full view를 분리합니다.59

8.3.1 private 타입의 부분 뷰와 풀 뷰

package Counter is
   type Object is private;
   procedure Increment (C : in out Object);
   function Value (C : Object) return Integer;
private
   type Object is record
      Current_Value : Integer range 0 .. 1_000 := 0;
   end record;
end Counter;

visible part의 type Object is private;가 partial view이고 private part의 record declaration이 full view입니다. 두 declaration은 서로 다른 타입 두 개가 아니라 같은 타입의 두 view를 정의합니다.

언어 규칙상 private type declaration으로 보이는 partial view는 composite view로 취급됩니다. 그러나 full view 자체는 반드시 composite일 필요는 없습니다. 따라서 client가 partial view만 보고 full representation이 record인지 integer-derived type인지 추측해서는 안 됩니다.

외부에서 full view의 component를 직접 선택할 수는 없습니다.

My_Counter : Counter.Object;
-- My_Counter.Current_Value := 10; -- full view가 보이지 않으므로 illegal

nonlimited partial view에서는 assignment가 허용되고 equality/inequality 같은 공개적으로 존재하는 연산도 사용할 수 있습니다. 다만 operator 역시 Ada의 visibility 규칙을 따르므로 package prefix나 use type 같은 이름 해석 수단이 필요할 수 있습니다. full view에서만 생기는 다른 산술·component 연산이 client에게 자동으로 노출되는 것은 아닙니다.

private part의 full representation을 바꿔도 visible contract가 유지될 수 있다는 것이 data abstraction의 핵심입니다. 다만 representation/ABI, inline/code generation 또는 build-system dependency까지 아무 영향이 없다고 일반화해서는 안 됩니다.

8.3.2 limited private 타입

type Handle is limited private;

client가 보는 limited private view에서는 assignment statement를 사용할 수 없고 predefined equality도 제공되지 않습니다. 이 기본 의미는 4.1.6절의 limited type 설명과 같습니다.

그러나 limited private object가 “함수에서 반환될 수 없다”, “in parameter로 전달할 수 없다”, 또는 “패키지가 제공하는 프로시저 외에는 어떤 방식으로도 초기화할 수 없다”는 뜻은 아닙니다. limited function result의 build-in-place 초기화, in parameter, aggregate가 허용되는 경우 등은 4.1.6절의 규칙을 따릅니다. package가 사용자 정의 equality function을 visible part에 제공하는 것도 가능합니다.

또한 untagged limited private partial view의 full view는 경우에 따라 nonlimited일 수 있습니다. client는 partial view 때문에 여전히 limited하게 다루지만, full view가 보이는 package 내부에서는 더 많은 연산이 가능할 수 있습니다.

8.4 컴파일 단위

Ada의 compilation_unit은 단순히 “운영체제 파일 하나”를 뜻하지 않습니다. 문법상 compilation unit은 context_clause와 library_item의 조합이거나 context_clause와 subunit의 조합입니다.60

compilation_unit
  = context_clause + library_item
  | context_clause + subunit

library item에는 library-unit declaration, library-unit body, library-unit renaming declaration이 포함됩니다. library-unit declaration에는 library subprogram/package/generic declaration과 generic instantiation이 올 수 있습니다. subunit도 compilation unit이지만 library unit은 아닙니다.

with clause는 compilation unit 사이에 semantic dependency를 만드는 대표적인 방법입니다. 이 dependency는 cross-unit legality와 visibility에 사용됩니다. 그러나 Ada 표준이 “사용자가 반드시 dependency source file을 먼저 수동 compile해야 한다”는 command workflow를 정하지는 않습니다. GNAT의 gnatmake/GPRbuild 같은 builder는 필요한 unit을 찾아 적절히 compile할 수 있습니다.

마찬가지로 body를 교체했을 때 어떤 object file을 다시 생성할지는 언어의 compilation-unit 모델과 build tool의 rebuild 정책을 구분해 봐야 합니다. client unit은 일반적으로 withed package declaration에 semantically dependent하며 package body 자체에 자동으로 semantic dependency를 갖는 것은 아닙니다.

8.5 서브유닛 (Subunit)을 이용한 본체 분리 (separate 키워드)

subunit은 body의 일부를 별도의 compilation unit로 분리하는 언어 기능입니다. 분리 가능한 body stub에는 subprogram, package, task, protected body stub가 있습니다.61

8.5.1 서브유닛의 정의와 목적

subunit은 declaration을 별도 library child처럼 확장하는 기능이 아니라 body를 separate compilation하는 기능입니다. parent body의 declarative part에는 body stub가 있고, 별도 compilation unit에는 그 stub에 대응하는 proper body가 있습니다.

package body Split is
   procedure Worker is separate;

   procedure Run is
   begin
      Worker;
   end Run;
end Split;

8.5.2 separate 키워드 사용법

대응하는 subunit은 다음과 같습니다.

with Ada.Text_IO;

separate (Split)
procedure Worker is
begin
   Ada.Text_IO.Put_Line ("worker");
end Worker;

parent body에는 대응 body stub가 있어야 하며, subunit proper body의 kind/profile은 stub와 맞아야 합니다. post-compilation rule상 각 body stub에는 대응 subunit이 존재해야 합니다.

8.5.3 서브유닛의 접근 범위

subunit은 의미상 body stub가 있는 자리의 proper body처럼 취급되므로, 그 지점에서 visible한 parent-body declaration을 사용할 수 있습니다. 그래서 parent body의 내부 상태를 separate body에서 사용할 수 있다는 장점이 있습니다.

다만 “parent에 있는 모든 declaration을 순서와 관계없이 볼 수 있다”고 설명하면 너무 넓습니다. visibility는 body stub가 놓인 declarative context와 일반 scope 규칙을 따르며, stub 뒤에 선언된 이름을 시간 역순으로 자동 가시화하는 기능은 아닙니다.

GNAT 기본 naming convention에서는 위 subunit이 split-worker.adb가 됩니다. underscore를 붙인 split_worker.adb가 Ada 표준 또는 GNAT 기본 규칙인 것은 아닙니다. naming scheme은 설정으로 바꿀 수 있습니다.

subunit이 incremental build에 유리할 수는 있지만, “subunit만 수정하면 parent와 dependents를 절대로 재컴파일하지 않는다”는 보장은 build implementation에 맡겨야 합니다.

8.6 정교화

elaboration은 단순한 “main 전에 package initialization을 한 번 실행한다”보다 넓은 Ada 실행 개념입니다. declaration/body를 elaboration하면서 타입·객체·subprogram body 등의 runtime 준비가 이루어지고, partition을 실행할 때 environment task가 필요한 library items를 정해진 dependency 순서에 맞게 elaboration한 뒤 main subprogram이 있다면 호출합니다.62

package declaration과 body의 elaboration

package declaration과 package body는 각각 자신의 elaboration 의미를 가집니다. package body가 elaboration되면 먼저 declarative part를 elaboration하고, statement part가 있으면 그 handled sequence를 실행합니다.

package body Registry is
   Table : Table_Type := Build_Table;
begin
   Register_Defaults;
end Registry;

needed library package body는 partition의 environment elaboration 과정에서 library item으로 elaboration됩니다. 그러나 중첩 package 등 모든 package instance에 대해 “프로그램 전체에서 정확히 한 번”이라고 일반화해서는 안 됩니다.

with와 elaboration order는 같은 규칙이 아니다

with B;가 있는 compilation unit은 B의 해당 library item에 semantic dependence를 가집니다. 이것은 B의 declaration이 필요한 순서를 만들지만, 그 사실만으로 B의 body가 현재 unit보다 반드시 먼저 elaboration된다고 말할 수는 없습니다.

Ada는 library-item elaboration dependence를 만족하는 total order를 요구하지만, 그 제약을 만족하는 여러 순서 가운데 나머지는 implementation-defined입니다. body-before-use가 추가로 필요하면 language-defined Elaborate, Elaborate_All, Elaborate_Body 같은 elaboration-control pragma/aspect가 관련될 수 있습니다.

특히 package body가 아직 elaboration되지 않았는데 그 body가 필요한 subprogram을 호출하면 access-before-elaboration check가 Program_Error를 일으킬 수 있습니다. 즉 with clause만 보고 “body 초기화까지 안전하게 끝났다”고 가정해서는 안 됩니다.

initialization 중 예외

package body statement part에서 예외가 발생하고 처리되지 않으면 그 예외가 해당 elaboration에서 전파됩니다. 이것이 자동으로 Program_Error로 변환되는 것은 아닙니다. 예를 들어 GNAT 14.2.0에서 library package body initialization이 명시적으로 Constraint_Error를 raise하도록 한 최소 예제는 main 진입 전에 그대로 CONSTRAINT_ERROR로 종료되었습니다.

Program_Error는 대표적으로 elaboration되지 않은 body에 대한 호출 같은 별도의 language-defined elaboration check 실패에서 발생할 수 있습니다. 둘을 구분해야 합니다.

8.7 자식 패키지

library package는 child library unit을 가질 수 있습니다. child의 expanded name은 parent 이름을 prefix로 사용합니다.

package Parent.Child is
   procedure Run;
end Parent.Child;

Parent.Child는 source text상 parent specification 안에 물리적으로 nested된 package가 아니라 별도로 compile되는 child library unit입니다. child는 parent declaration에 semantic dependence를 가집니다.

client에서 parent만 with했다고 child가 자동으로 visible해지는 것은 아닙니다.

with Parent;
-- Parent.Child.Run; -- Child를 별도로 문맥에 넣지 않았다면 사용할 수 없음

child를 사용하려면 보통 다음처럼 child를 이름으로 with합니다. child를 언급하는 context는 필요한 ancestor 관계도 함께 반영합니다.

with Parent.Child;

procedure Main is
begin
   Parent.Child.Run;
end Main;

public child와 parent private part

public child의 visible part에서는 parent의 private declaration이 보이지 않습니다. 그렇지 않으면 public child를 통해 parent의 private representation을 외부에 누출할 수 있기 때문입니다. 반면 public child의 private part와 body에서는 parent private part가 visible할 수 있습니다.

package Parent is
   type Object is private;
private
   type Object is record
      Value : Integer;
   end record;
end Parent;

package Parent.Tools is
   procedure Reset (X : in out Parent.Object);
private
   -- 이 영역에서는 parent private view를 사용할 수 있음
end Parent.Tools;

Parent.Tools body도 parent의 private part를 볼 수 있습니다. 그러나 parent package body에서만 선언한 내부 이름까지 child가 자동으로 볼 수 있는 것은 아닙니다.

private child

child library unit 자체를 private로 선언할 수도 있습니다. private child는 parent hierarchy 내부 구현을 나누는 데 사용할 수 있으며, arbitrary 외부 client가 이를 with하는 것은 허용되지 않습니다. private child에서는 parent private declaration의 visibility 범위가 public child보다 넓습니다.

이처럼 child unit은 단순한 파일명 계층이 아니라 semantic dependency와 visibility 경계를 함께 만드는 Ada의 library hierarchy 기능입니다.63

9. 접근 타입과 접근성

Ada의 접근 타입(access type)은 객체나 서브프로그램을 간접적으로 지정하는 값을 표현합니다. C 계열 언어의 포인터와 비슷한 용도로 사용할 수 있지만, Ada는 접근 값이 가리키는 대상이 너무 일찍 사라져 dangling reference가 되지 않도록 접근성(accessibility) 규칙을 언어 차원에서 적용합니다(ARM 3.10, 3.10.2).

이 장에서는 먼저 서브프로그램 접근 타입과 안전한 콜백을 살펴본 뒤, 객체와 서브프로그램에 공통으로 적용되는 접근성 수준과 수명 관계를 설명합니다. 접근성 규칙은 대부분 컴파일 시점의 legality rule로 적용되지만, access parameter나 generic 등 일부 상황에서는 실행 시 접근성 검사도 사용됩니다. 따라서 “Ada는 모든 접근성 문제를 컴파일 시점에만 검사한다”고 일반화해서는 안 됩니다.

9.1 고차 연산과 서브프로그램 접근 타입

서브프로그램 접근 타입(access-to-subprogram type)은 특정 프로파일을 가진 프로시저나 함수를 간접적으로 지정하는 access value의 타입입니다. 구현이 이 값을 실제 기계 주소 하나로 표현한다고 가정할 필요는 없습니다. 언어 수준에서 중요한 것은 해당 값이 지정하는 서브프로그램의 프로파일, 호출 규약, null 여부와 접근성입니다.

9.1.1 서브프로그램에 대한 접근 타입

다음과 같이 프로시저나 함수의 프로파일을 지정해 named access-to-subprogram type을 선언할 수 있습니다(ARM 3.10).

type Procedure_Access is access procedure (X : Integer);
type Function_Access is access function (X : Integer) return Integer;

서브프로그램의 'Access 속성은 기대되는 access-to-subprogram type의 프로파일과 subtype conformant한 서브프로그램을 지정하는 access value를 만듭니다(ARM 3.10.2).

type Unary_Integer_Function is
  access function (X : Integer) return Integer;

function Square (X : Integer) return Integer is (X * X);

F      : Unary_Integer_Function := Square'Access;
Result : Integer := F (5);  -- 25

access-to-subprogram value를 호출할 때는 위처럼 implicit dereference를 사용할 수 있습니다. 명시적으로 F.all (5)라고 적어도 같은 지정 서브프로그램을 호출합니다. access type에는 null 값도 있으며, null access value를 역참조하거나 호출하면 access check가 실패하므로 호출 전에 null 가능성을 설계에서 처리해야 합니다.

서브프로그램 접근 타입은 콜백, 전략 선택, 고차 연산 등에 유용하지만, 어떤 서브프로그램의 'Access든 아무 access type에 저장할 수 있는 것은 아닙니다. P'Access에서 서브프로그램 P의 접근성 수준은 대상 access-to-subprogram type보다 statically deeper해서는 안 됩니다(ARM 3.10.2).

9.1.2 하향 클로저와 익명 서브프로그램 접근 파라미터

Ada 2022의 declare expression은 표현식 안에서 지역 상수와 객체 renaming을 선언하기 위한 기능입니다(ARM 4.5.9). 그 선언부에는 서브프로그램 선언을 둘 수 없고, declare expression 내부에서는 Access나 Unchecked_Access 속성도 사용할 수 없습니다. 따라서 declare expression 안에 로컬 함수를 선언하고 그 'Access를 반환하여 일반적인 escaping closure를 만드는 패턴은 Ada 2022 문법이 아닙니다.

반면 Ada는 중첩 서브프로그램을 더 바깥 서브프로그램에 즉시 전달하되 저장해서 탈출시키지 않는 하향 클로저(downward closure)를 지원합니다. 대표적인 방법은 anonymous access-to-subprogram parameter입니다(ARM 3.10.2, Note 5).

procedure Invoke (Action : not null access procedure) is
begin
   Action.all;
end Invoke;

procedure Outer is
   Count : Integer := 0;

   procedure Increment is
   begin
      Count := Count + 1;  -- Outer의 지역 상태 사용
   end Increment;
begin
   Invoke (Increment'Access);
end Outer;

Increment는 Outer의 지역 상태를 사용하므로 closure와 비슷한 환경 참조를 가집니다. 그러나 Invoke의 anonymous access-to-subprogram parameter는 이러한 더 깊은 중첩 서브프로그램을 호출 범위 안에서 전달하기 위한 특별한 접근성 모델을 사용합니다. 위 예제는 GNAT 15.2.0의 Ada 2022 모드에서 compile/run 검증했습니다.

제네릭 형식 서브프로그램도 비슷한 목적의 정적 대안입니다. 제네릭 인스턴스가 중첩 서브프로그램을 형식 서브프로그램으로 받으면 런타임 access value를 장기 저장하지 않고 호출 관계를 구성할 수 있습니다.

9.2 접근성 규칙(Accessibility Rules)

ARM 3.10.2의 접근성 규칙은 accessibility level을 이용해 dangling reference를 막습니다. 접근성 수준은 단순한 source-code scope 숫자가 아니라 실행 시 master의 중첩 관계를 반영합니다. 서브프로그램 본체의 실행 같은 특정 구성은 master가 되며, 더 안쪽 master에서 생성된 엔티티는 일반적으로 더 깊은 accessibility level을 가집니다.

9.2.1 필요성과 기본 원칙

객체의 'Access를 named access type에 저장하는 경우, 대상 객체의 접근성 수준이 access type보다 statically deeper하면 안 됩니다. 다음 예제는 지역 객체가 프로시저 종료 후 사라질 수 있는데 더 바깥 Ptr이 남을 수 있으므로 불법입니다.

type Integer_Access is access all Integer;
Ptr : Integer_Access;

procedure Example is
   Value : aliased Integer := 42;
begin
   Ptr := Value'Access;  -- illegal: Value가 더 깊음
end Example;

GNAT 15.2.0은 이 예제를 non-local pointer cannot point to local object로 거부했습니다. 반대로 access type과 객체를 같은 Example 내부에 두어 수명 관계를 맞추면 Value'Access를 저장하고 사용할 수 있습니다.

접근성 규칙의 실용적인 직관은 다음과 같습니다.

access value가 사용될 수 있는 기간 동안, 그 값이 지정하는 대상도 유효해야 한다.

다만 이 문장은 규칙을 이해하기 위한 직관입니다. 실제 legality와 dynamic semantics는 ARM의 accessibility level, master, access type 종류, anonymous access type 등의 세부 규칙으로 결정됩니다.

Ada는 안전한 language-defined access 기능을 사용하는 한 이러한 수명 불일치를 제한합니다. 하지만 Unchecked_Access 같은 unchecked 기능은 일부 객체 접근성 제한을 우회할 수 있으므로, “Ada에서는 dangling reference가 절대로 생길 수 없다”고 말할 수는 없습니다. ARM 3.10.2도 accessibility rule의 목적을 unchecked 기능이 없는 경우 dangling reference를 방지하는 것으로 한정합니다.

9.2.2 접근성과 스코프

스코프(scope)는 이름의 가시성과 관련되고, accessibility level은 대상과 access value의 수명 안전성을 결정하는 데 사용됩니다. 둘은 선언 중첩과 관계가 있어 자주 함께 움직이지만 같은 개념은 아닙니다.

procedure Outer is
   Outer_Value : aliased Integer := 10;
begin
   declare
      Inner_Value : aliased Integer := 20;
   begin
      -- Outer_Value는 여기서 보이고 계속 유효하다.
      -- Inner_Value는 이 내부 master에 속한다.
      null;
   end;
end Outer;

더 안쪽에서 바깥 객체를 참조하는 것은 일반적으로 수명 면에서 문제가 없지만, 안쪽 객체에 대한 access value를 더 오래 사는 named access type으로 내보내려 하면 접근성 규칙에 걸릴 수 있습니다.

9.2.3 생명주기 관점

접근성을 단순한 “포인터 문법”으로 이해하면 anonymous access parameter, 함수 결과, generic 안의 runtime accessibility check 같은 규칙을 설명하기 어렵습니다. 더 정확한 관점은 master와 그 master가 만든 엔티티의 유효 기간을 access value가 넘어서지 않도록 제약하는 규칙입니다.

대부분의 정적으로 알 수 있는 관계는 legality rule로 검사합니다. 그러나 ARM 3.10.2는 access parameter와 generic package가 관련된 일부 경우에 runtime accessibility check가 필요하다고 명시합니다. 객체의 X'Access에 대한 동적 접근성 검사가 실패하면 Program_Error가 발생할 수 있습니다.

9.3 서브프로그램의 생명주기

서브프로그램의 기계 코드가 블록 종료 순간 메모리에서 사라진다고 이해해서는 안 됩니다. 접근성에서 중요한 것은 중첩 서브프로그램을 지정하는 호출 문맥이 enclosing master의 activation과 지역 환경에 의존할 수 있다는 점입니다. 특히 중첩 서브프로그램이 바깥 지역 객체를 참조한다면, 그 환경보다 오래 사는 callback으로 탈출시키는 것은 안전하지 않습니다.

9.3.1 중첩 서브프로그램과 수명

procedure Outer is
   Count : Integer := 0;

   procedure Inner is
   begin
      Count := Count + 1;
   end Inner;
begin
   Inner;
end Outer;

Inner와 Count처럼 Outer의 선언부에서 elaboration되는 엔티티의 accessibility level은 그 선언을 포함하는 가장 안쪽 master, 여기서는 Outer 호출의 master와 연결됩니다(ARM 3.10.2). 따라서 이보다 얕은 수준의 named access-to-subprogram type에 Inner를 저장하여 Outer 호출 이후까지 남길 수는 없습니다.

named access-to-subprogram type의 수준과 중첩 서브프로그램의 수준이 맞지 않으면 'Access 자체가 legality rule에 의해 거부됩니다. 반대로 9.1.2의 anonymous access parameter처럼 언어가 의도적으로 downward closure를 허용하는 경로에서는 중첩 서브프로그램을 더 바깥 호출자에게 전달해도 그 참조를 호출 범위 밖으로 탈출시키지 않는 모델을 사용합니다.

9.3.2 참조 저장에서 생기는 문제

다음 구조에서는 Callback 타입이 Outer보다 바깥에 있고 Inner는 Outer 안에 있으므로, Inner'Access를 Saved에 저장하려는 시점에 접근성 수준이 맞지 않습니다.

type Callback is access procedure;
Saved : Callback;

procedure Outer is
   procedure Inner is
   begin
      null;
   end Inner;
begin
   Saved := Inner'Access;  -- illegal
end Outer;

GNAT 15.2.0은 이 예제를 subprogram must not be deeper than access type로 거부했습니다. 중요한 점은 Register 같은 함수가 실제로 저장할지 추측해서 거부하는 것이 아니라, named access type에 넣는 순간부터 그 access value가 type의 접근성 범위만큼 살아남을 수 있다고 보기 때문에 정적 규칙이 적용된다는 것입니다.

객체와 서브프로그램은 세부 규칙이 완전히 같지는 않지만, 접근성의 목적은 공통입니다. access value가 지정 대상의 유효 범위를 넘어 안전하지 않은 참조가 되는 것을 막습니다. 객체에는 Unchecked_Access라는 우회 수단이 있지만, ARM 3.10.2는 서브프로그램에는 Unchecked_Access가 없다고 명시합니다.

9.3.3 생명주기 관점 정리

access-to-subprogram을 설계할 때는 다음 세 질문을 구분하면 좋습니다.

  1. 이 callback을 호출 중에만 사용할 것인가? 그렇다면 anonymous access-to-subprogram parameter나 generic formal subprogram 같은 downward 방식이 적절할 수 있습니다.
  2. callback을 장기간 저장할 것인가? 그렇다면 저장소와 같은 수준 이상으로 오래 유효한 서브프로그램을 지정해야 합니다.
  3. callback이 지역 상태를 필요로 하는가? 그렇다면 서브프로그램 포인터 하나로 환경을 탈출시키려 하지 말고, 상태를 명시적인 객체에 소유시키는 설계를 검토합니다.

9.4 흔히 발생하는 접근성 오류

접근성 오류는 단순한 “포인터를 못 쓰게 하는 제한”이 아니라, access value가 지정 대상보다 오래 살아남을 가능성을 정적으로 또는 동적으로 차단하는 결과입니다.

9.4.1 대표적인 오류

서브프로그램에서 가장 전형적인 정적 오류는 다음과 같습니다.

type Callback is access procedure;
Saved : Callback;

procedure Outer is
   procedure Inner is
   begin
      null;
   end Inner;
begin
   Saved := Inner'Access;
end Outer;

현재 검증 환경의 GNAT 15.2.0은 다음 핵심 진단으로 이 코드를 거부합니다.

error: subprogram must not be deeper than access type

진단 문자열은 GNAT 구현의 표현이며 Ada 표준 그 자체는 아닙니다. 표준 근거는 ARM 3.10.2의 P'Access legality rule입니다.

9.4.2 원인과 위험

Inner가 단지 코드라는 사실만 보면 왜 제한이 필요한지 놓치기 쉽습니다. 중첩 서브프로그램은 enclosing activation의 지역 상태에 접근할 수 있고, 구현은 그러한 환경을 호출에 필요로 할 수 있습니다. Outer가 끝난 뒤에도 Inner를 가리키는 값이 남으면 해당 환경을 안전하게 복원할 수 없을 수 있습니다.

따라서 장기 저장 callback에서는 “코드 주소가 아직 존재하는가”보다 그 서브프로그램을 유효하게 호출하는 데 필요한 환경까지 살아 있는가를 봐야 합니다.

Ada는 정적으로 깊은 관계를 알 수 있을 때 컴파일 시점 legality rule을 사용하고, 정적으로 결정할 수 없는 일부 관계에는 runtime accessibility check를 사용합니다. 그러므로 “접근성 오류는 항상 컴파일 오류”도, “항상 런타임 검사”도 정확하지 않습니다.

9.4.3 오류를 해석하는 관점

접근성 진단을 만나면 먼저 다음을 확인합니다.

  • access type 또는 anonymous access parameter는 어느 master에 속하는가?
  • 지정하려는 객체나 서브프로그램은 어느 master에 속하는가?
  • access value가 더 바깥으로 저장되거나 반환되는가?
  • named access type 대신 호출 중에만 쓰는 anonymous access parameter가 목적에 맞는가?

중첩 서브프로그램 자체가 나쁜 것은 아닙니다. 문제는 그 서브프로그램의 접근 값을 허용된 접근성 범위를 넘어 탈출시키는 것입니다. 지역 알고리즘이나 downward callback처럼 수명 관계가 맞는 사용은 Ada가 정상적으로 지원합니다.

결국 핵심은 “access type을 쓰지 말라”가 아니라 access value가 유효한 대상만 지정하도록 lifetime 관계를 타입과 API 구조에 반영하라는 것입니다.

9.5 안전한 설계 패턴

접근성 규칙을 우회하는 것보다 callback의 실제 보관 기간과 상태 소유권을 API에 드러내는 편이 안전하고 유지보수하기 쉽습니다.

9.5.1 패키지 수준 서브프로그램 사용

library-level 패키지에 선언된 서브프로그램은 library-level named callback type에 저장하는 장기 callback의 가장 단순한 후보입니다.

package Event_Handlers is
   procedure On_Event;
end Event_Handlers;

등록 지점에서는 callback type의 프로파일이 맞는 경우 다음처럼 접근 값을 전달할 수 있습니다.

Register (Event_Handlers.On_Event'Access);

여기서는 callback의 코드와 선언 환경이 지역 activation에 종속되지 않습니다. 다만 실제 API에서는 프로파일과 calling convention도 access-to-subprogram type과 일치해야 합니다.

9.5.2 중첩 서브프로그램 대신 객체 사용

callback이 상태를 장기간 기억해야 한다면 지역 변수를 암묵적으로 capture해서 탈출시키기보다, 상태를 수명이 명확한 객체에 저장하고 그 객체의 연산으로 동작을 표현하는 설계를 고려합니다. tagged type과 dispatching을 이용한 구체적인 객체 기반 다형성은 11장에서 다룹니다.

이 방식의 장점은 callback의 상태가 어느 객체에 소유되고 언제 소멸하는지 소스에 드러난다는 점입니다. 단, 객체를 가리키는 access value를 사용한다면 그 객체에도 동일하게 접근성 규칙이 적용됩니다.

9.5.3 제네릭을 이용한 정적 바인딩

런타임에 callback을 교체할 필요가 없다면 generic formal subprogram을 사용할 수 있습니다.

generic
   with procedure Handler;
procedure Invoke;

procedure Invoke is
begin
   Handler;
end Invoke;

중첩 범위에서 인스턴스화하면 같은 범위의 지역 서브프로그램도 actual로 사용할 수 있습니다. 현재 GNAT 15.2.0 실험에서 지역 상태를 변경하는 중첩 프로시저를 generic formal subprogram으로 전달한 예제가 compile/run에 성공했습니다. 이 설계는 장기간 보관되는 access value 자체가 필요하지 않은 경우에 적합합니다.

9.5.4 지역 처리에는 중첩 서브프로그램 사용

중첩 서브프로그램을 enclosing activation 안에서 직접 호출하는 것은 자연스럽고 안전한 구조입니다. 호출 중에만 callback으로 넘겨야 한다면 9.1.2의 anonymous access-to-subprogram parameter도 검토할 수 있습니다.

procedure Process_File is
   procedure Validate is
   begin
      null;
   end Validate;
begin
   Validate;
end Process_File;

이 경우 Validate에 대한 access value가 바깥 저장소로 탈출하지 않으므로 장기 수명 문제가 생기지 않습니다.

9.5.5 실무 지침과 정리

접근성 오류가 나면 unchecked 기능으로 먼저 우회하기보다 API가 요구하는 수명을 다시 확인합니다.

실무적으로는 다음 기준이 유용합니다.

  1. 장기간 저장되는 callback은 저장소와 같은 수준 이상으로 유효한 서브프로그램을 지정합니다.
  2. 호출 중에만 필요한 nested callback은 anonymous access-to-subprogram parameter 또는 generic formal subprogram을 검토합니다.
  3. callback이 장기 상태를 필요로 하면 상태를 명시적인 객체가 소유하도록 설계합니다.
  4. object Unchecked_Access는 접근성 보장을 우회하므로 FFI나 저수준 경계처럼 수명을 별도로 증명할 수 있는 제한된 상황에서만 사용합니다.
  5. 서브프로그램에는 Unchecked_Access가 없다는 점을 기억합니다.

접근성 규칙은 단순한 소스 scope 규칙도, 모든 상황을 compile-time에 해결하는 단일 검사도 아닙니다. master의 실행 중첩을 반영하는 accessibility level을 통해 객체와 서브프로그램의 안전한 참조 관계를 정의하고, 필요한 경우 runtime check도 사용합니다. access-to-subprogram을 이용할 때는 저장되는 callback과 호출 중에만 전달되는 downward callback을 구분하면 많은 접근성 문제를 더 명확하게 설계할 수 있습니다.


10. 제네릭 프로그래밍 (Generic Programming)

Ada의 generic unit은 객체, 타입, 서브프로그램 또는 package를 형식 매개변수(formal parameter)로 받아 재사용 가능한 package나 subprogram을 정의하는 template입니다. generic unit 자체는 호출 가능한 일반 subprogram이나 일반 package instance가 아니며, generic_instantiation을 통해 nongeneric package/procedure/function instance를 만듭니다.64

generic programming의 핵심은 단순한 소스 코드 복사보다 계약(contract)에 있습니다. generic body는 formal part가 보장하는 성질과 연산만 사용해서 독립적으로 적법해야 하고, instantiation은 actual parameter가 그 formal contract를 만족하는지 검사합니다. 실제 machine code를 instance마다 얼마나 복제하거나 공유하는지는 Ada 언어가 정하는 사항이 아닙니다.

10.1 제네릭의 개념과 재사용성

generic unit은 generic package, generic procedure, generic function 가운데 하나입니다. 예를 들어 타입과 무관한 swap 알고리즘은 다음처럼 formal type을 받을 수 있습니다.

generic
   type Item is private;
procedure Generic_Swap (A, B : in out Item);

이 generic procedure는 직접 Generic_Swap (X, Y)처럼 호출하는 대상이 아닙니다. 대신 instance를 선언합니다.

procedure Swap_Integers is new Generic_Swap (Integer);

그 뒤 Swap_Integers는 일반 procedure이므로 호출할 수 있습니다.

X : Integer := 1;
Y : Integer := 2;

Swap_Integers (X, Y);

library-level generic unit은 다른 compilation unit에서 instantiation하기 위해 with할 수 있습니다. 즉 “generic unit은 with할 수 없다”는 설명은 틀립니다.

with Generic_Swap;

procedure Main is
   procedure Swap is new Generic_Swap (Integer);
begin
   ...
end Main;

반대로 generic instance는 이미 nongeneric unit입니다. library-level instance로 선언했다면 다른 unit이 그 instance를 with할 수도 있습니다. block 안에 선언한 nested instance는 library unit이 아니므로 다른 compilation unit의 context clause에서 with하는 대상이 아닙니다.

Ada RM은 instance를 template의 의미론적 copy로 설명하지만, 이 표현을 “반드시 instance마다 별도의 machine code가 생성된다”는 구현 보장으로 읽어서는 안 됩니다. 현재 GNAT 14.2.0은 generic instantiation을 macro expansion 방식으로 처리한다고 문서화하지만, 다른 Ada 구현은 다른 code-generation 전략을 사용할 수 있습니다.65

10.2 제네릭 단위 선언 (Generic Declaration)

generic_declaration은 generic으로 시작하는 formal part와 generic package/subprogram specification으로 구성됩니다. formal part에는 formal object, formal type, formal subprogram, formal package를 선언할 수 있고 use clause도 둘 수 있습니다.66

generic
   type Item is private;
   Capacity : Positive;
   with function "<" (Left, Right : Item) return Boolean is <>;
package Generic_Store is
   ...
end Generic_Store;

formal parameter는 단순한 “나중에 텍스트 치환할 자리표시자”보다 강한 의미를 갖습니다. formal declaration이 generic body에서 사용할 수 있는 타입 category, operation, value/view를 제한하며 이것이 generic declaration의 contract가 됩니다.

예를 들어 다음 generic body는 actual이 나중에 Integer가 될 예정이어도 적법하지 않습니다.

generic
   type Item is private;
procedure Bad_Add (A, B : Item);

procedure Bad_Add (A, B : Item) is
   C : Item;
begin
   C := A + B;  -- illegal: formal private type은 `+`를 보장하지 않음
end Bad_Add;

type Item is private가 보장하는 연산 집합에 integer addition이 없으므로 generic body 자체가 compile-fail합니다. actual type의 숨은 능력이 generic body를 소급해서 합법으로 만들지는 않습니다. +가 필요하다면 formal subprogram으로 명시하거나, range <> 같은 더 구체적인 formal type category를 선택해야 합니다.

10.3 제네릭 파라미터 (Generic Parameters)

formal parameter의 종류에 따라 generic에 전달되는 계약 정보가 달라집니다.

10.3.1 제네릭 타입 파라미터 (Generic Type Parameters)

formal type의 문법 형태는 actual type이 속해야 할 type category와 generic 내부에서 보장되는 predefined operation을 결정합니다.67

대표적인 형태는 다음과 같습니다.

type Any_Definite        is limited private;
type Any_Indefinite (<>) is limited private;
type Nonlimited          is private;
type Discrete            is (<>);
type Signed_Int          is range <>;
type Modular_Int         is mod <>;
type Floating            is digits <>;
type Ordinary_Fixed      is delta <>;
type Decimal_Fixed       is delta <> digits <>;

여기서 몇 가지 경계를 특히 주의해야 합니다.

  • type T is private;의 category는 nonlimited type입니다. 그러나 discriminant part가 없는 이 formal subtype은 definite이므로 unconstrained String처럼 indefinite subtype을 actual로 받을 수 없습니다.
  • type T is limited private;의 category 자체는 limited 여부를 가리지 않는 모든 type이지만, 역시 discriminant part가 없으면 actual subtype은 definite여야 합니다.
  • indefinite type까지 받으려면 보통 type T (<>) is limited private;처럼 unknown discriminants를 가진 formal을 사용합니다. GNAT 14.2.0에서 String은 type T is limited private actual로는 거부되고 type T (<>) is limited private에는 허용됩니다.
  • type T is (<>);는 enumeration만이 아니라 integer를 포함한 discrete type category와 일치합니다.
  • scalar뿐 아니라 formal array, formal access, formal derived, formal interface type도 정의할 수 있습니다.

formal type은 actual type 자체가 아닙니다. generic body를 분석할 때는 formal declaration이 알려 주는 category만 신뢰합니다. 그래서 type Item is private;에서는 assignment와 해당 formal view에 제공되는 equality 등을 사용할 수 있지만, actual이 Integer라고 해도 +, <, > 같은 integer-specific operation을 임의로 사용할 수 없습니다.

반대로 더 구체적인 formal을 쓰면 해당 category가 보장하는 operation과 attribute를 사용할 수 있습니다.

generic
   type Index is (<>);
package Enum_Table is
   First_Position : constant Integer := Index'Pos (Index'First);
end Enum_Table;

formal derived type은 ancestor의 derivation class를 contract로 삼고, formal array/access/interface에는 각각 별도의 matching rule이 있습니다. 세부 타입 설계에서는 “어떤 actual을 받고 싶은가”뿐 아니라 generic body에 어떤 operation을 보장해야 하는가를 먼저 정하는 편이 안전합니다.

10.3.2 제네릭 객체 파라미터 (Generic Object Parameters)

formal object는 generic에 value 또는 variable view를 전달합니다.68

generic
   Capacity : in Positive := 100;
   Counter  : in out Natural;
package Configured_Store is
   ...
end Configured_Store;

in은 기본 mode이며 generic 안에서 constant처럼 다뤄집니다. actual은 반드시 static expression일 필요가 없습니다. 함수 호출처럼 instantiation elaboration 시 평가되는 expression도 actual이 될 수 있습니다.

function Compute_Capacity return Positive is (128);
Count : Natural := 0;

package Store is new Configured_Store
  (Capacity => Compute_Capacity,
   Counter  => Count);

위에서 Capacity actual은 static expression이 아닌 함수 호출이고, Counter actual은 variable입니다. in out formal object에는 이처럼 variable actual이 필요하며 generic 안에서 Counter를 변경하면 실제 Count object에 반영됩니다.

literal 0처럼 variable이 아닌 actual을 in out formal에 넘길 수 없습니다. 또한 default expression은 in formal object에만 허용됩니다. 다음 선언은 불법입니다.

generic
   Counter : in out Natural := 0;  -- illegal
package Bad is
end Bad;

generic formal object와 subprogram formal parameter는 이름이 비슷하지만 별개의 언어 기능입니다. parameter passing mechanism까지 일반 subprogram 호출과 같다고 가정하지 말고 RM 12.4의 generic formal object 규칙을 따릅니다.

10.3.3 제네릭 서브프로그램 파라미터 (Generic Subprogram Parameters)

formal subprogram은 generic body가 호출할 operation을 contract에 명시합니다.69

generic
   type Item is private;
   with function "<" (Left, Right : Item) return Boolean;
procedure Generic_Sort (A : in out Item_Array);

formal과 actual subprogram의 profile은 language-defined matching rule을 만족해야 합니다. explicit actual을 넘길 수도 있고 default를 지정할 수도 있습니다.

with function "<" (Left, Right : Item) return Boolean is <>;

is <>는 “actual type에 <가 존재하면 어디에 숨어 있든 컴파일러가 찾아낸다”는 뜻이 아닙니다. box default는 instantiation 지점에서 matching entity를 visibility 규칙에 따라 찾습니다. 예를 들어 package에 선언된 private type의 <를 default actual로 쓰려면 그 operator가 instantiation 지점에서 직접 보이도록 use type을 쓰거나 explicit actual로 지정해야 할 수 있습니다.

use type Less_Ops.T;
function Less is new Generic_Less (Less_Ops.T);

같은 코드에서 use type Less_Ops.T;를 제거하면 GNAT 14.2.0은 matching <가 visible하지 않다고 instantiation을 거부합니다.

formal procedure에는 is null 같은 no-op default도 사용할 수 있습니다. 어떤 default 형태를 선택하든 generic body가 필요로 하는 operation을 formal contract에 드러내는 것이 핵심입니다.

10.3.4 제네릭 패키지 파라미터 (Formal Packages)

formal package를 사용하면 어떤 generic package의 instance 자체를 다른 generic의 parameter로 받을 수 있습니다.70

generic
   type Item is private;
package Boxes is
   function Same (L, R : Item) return Boolean is (L = R);
end Boxes;

generic
   with package Box_Instance is new Boxes (<>);
procedure Use_Box;

(<>) 형태에서는 해당 template Boxes의 적절한 instance를 actual로 받을 수 있습니다.

package Integer_Box is new Boxes (Integer);
procedure Use_Integer_Box is new Use_Box (Integer_Box);

formal package는 large API나 container instance를 통째로 generic contract에 전달할 때 유용합니다. 특정 formal actual까지 같아야 하는 더 제한적인 formal package specification도 만들 수 있습니다.

10.4 제네릭 인스턴스화 (Generic Instantiation)

instantiation은 new를 사용해 generic package/procedure/function의 nongeneric instance를 선언합니다.71

procedure Swap_Integers is new Generic_Swap (Item => Integer);
package Integer_Store is new Generic_Store
  (Item     => Integer,
   Capacity => 100);

generic actual association은 positional 또는 named notation을 사용할 수 있으며 positional association은 named association보다 앞에 와야 합니다. formal에 적법한 default가 있으면 actual을 생략할 수 있습니다. 이 표기법이 subprogram call과 비슷하더라도 instantiation은 subprogram call이 아닙니다.

actual 평가와 부작용

instantiation elaboration에서는 generic association이 평가됩니다. 서로 독립적인 actual의 평가 순서는 일반적으로 임의이므로, 다음과 같이 여러 actual expression의 side effect 순서에 의존하는 설계를 피해야 합니다.

package G is new Some_Generic
  (First  => Next_Value,
   Second => Next_Value);

두 함수 호출의 선후를 프로그램 의미로 가정하지 않는 편이 안전합니다. default expression이 다른 actual을 의존하는 경우에는 RM이 별도의 ordering rule을 둡니다.

contract model

generic body의 이름 해석과 legality는 generic 자체를 compile할 때 formal contract를 기준으로 결정됩니다. instantiation은 actual을 대입해 body를 처음부터 새로 overload-resolve하는 과정이 아닙니다. 이 특성이 generic body를 각각의 instance와 독립적으로 검증할 수 있게 합니다.

기존 정렬 예제처럼 <만 formal로 받았다면 body도 <로 표현해야 합니다.

while J >= A'First and then Key < A (J) loop
   A (J + 1) := A (J);
   J := J - 1;
end loop;

formal에 없는 >를 actual이 우연히 제공한다는 이유로 body에서 사용할 수는 없습니다.

instantiation과 freezing

generic_instantiation은 Ada의 freezing point를 만들 수 있습니다.72 representation/operational item은 대상 type이 freeze되기 전에 와야 하는 경우가 있으므로 순서가 중요합니다.

type Small is range 0 .. 10;
package I is new Generic_Freeze (Small);

-- for Small'Size use 8;  -- too late: instantiation이 Small을 freeze할 수 있음

GNAT 14.2.0에서도 위 순서의 representation item은 representation item appears too late로 거부됩니다. generic instantiation을 단순 텍스트 치환으로만 생각하면 놓치기 쉬운 compile-time 효과입니다.

10.5 제네릭 서브프로그램 (Generic Subprograms)

library generic subprogram declaration은 다른 subprogram declaration처럼 completion이 필요합니다(Import 같은 별도 규칙이 적용되는 경우 제외). body에서는 formal contract만 사용해 구현합니다.

generic
   type Item is private;
procedure Generic_Swap (A, B : in out Item);
procedure Generic_Swap (A, B : in out Item) is
   Temp : constant Item := A;
begin
   A := B;
   B := Temp;
end Generic_Swap;

client는 generic unit을 context에 넣고 instance를 만든 뒤 호출합니다.

with Generic_Swap;

procedure Example is
   procedure Swap is new Generic_Swap (Integer);
   X : Integer := 1;
   Y : Integer := 2;
begin
   Swap (X, Y);
end Example;

GNAT 14.2.0에서 이 예제는 정상 compile/run되며 X=2, Y=1이 됩니다.

library-level generic subprogram instance도 library unit으로 선언할 수 있습니다. 예를 들어 Swap_Integers instance를 별도 library unit으로 두면 다른 client가 with Swap_Integers; 한 뒤 일반 procedure처럼 호출할 수 있습니다. GNAT 기본 naming에서 이 procedure instance는 spec 성격의 library item이므로 swap_integers.ads에 둘 수 있습니다.

10.6 제네릭 패키지 (Generic Packages)

generic package는 하나의 알고리즘뿐 아니라 타입·상태·operation 묶음을 parameterize할 때 사용합니다. 하지만 모든 generic package가 별도 body를 필요로 하는 것은 아닙니다. 일반 package와 마찬가지로 specification이 completion을 요구하는 선언을 포함하는지에 따라 body 필요 여부가 결정됩니다.

다음 generic package는 expression function이 specification 안에서 이미 complete되므로 별도 body 없이 instantiate할 수 있습니다.

generic
   type Item is private;
package Generic_No_Body is
   function Same (L, R : Item) return Boolean is (L = R);
end Generic_No_Body;

반면 다음 stack package는 operation declaration의 body가 필요합니다.

generic
   Capacity : Positive;
   type Item is private;
package Generic_Stack is
   Stack_Error : exception;

   procedure Push (V : Item);
   procedure Pop  (V : out Item);
   function Is_Empty return Boolean;
   function Is_Full  return Boolean;
private
   type Storage is array (Positive range <>) of Item;

   Data : Storage (1 .. Capacity);
   Top  : Natural := 0;
end Generic_Stack;
package body Generic_Stack is
   procedure Push (V : Item) is
   begin
      if Is_Full then
         raise Stack_Error;
      end if;

      Top := Top + 1;
      Data (Top) := V;
   end Push;

   procedure Pop (V : out Item) is
   begin
      if Is_Empty then
         raise Stack_Error;
      end if;

      V := Data (Top);
      Top := Top - 1;
   end Pop;

   function Is_Empty return Boolean is (Top = 0);
   function Is_Full  return Boolean is (Top = Capacity);
end Generic_Stack;

instance는 독립된 package declaration을 만듭니다.

package Integer_Stack is new Generic_Stack
  (Capacity => 100,
   Item     => Integer);

이 instance가 다른 compilation unit에서 선언된 library-level instance라면 client가 with Integer_Stack;으로 사용할 수 있습니다. 반대로 procedure 내부에 local instance를 선언한 경우에는 같은 procedure의 scope 안에서 직접 Integer_Stack.Push처럼 사용하지, 별도의 with clause를 쓰지 않습니다.

대규모 프로그램에서 generic instance 수를 늘릴 때는 type safety와 재사용성뿐 아니라 compile time, code size, optimizer 결과도 함께 측정해야 합니다. 특히 GNAT은 instance를 macro expansion으로 처리하므로 많은 대형 instantiation은 compile-time과 binary-size 비용을 키울 수 있습니다. 다만 최종 코드 크기는 optimization, inlining, linker 동작에 따라 달라지므로 source의 instance 개수만으로 단정하지 않습니다.

11. 객체 지향 프로그래밍 (OOP)

이 장은 4장의 타입과 객체, 7장의 서브프로그램, 8장의 패키지와 private 타입을 선수 개념으로 삼아 Ada의 객체 지향 기능을 설명합니다. 패키지 기반 캡슐화의 일반 원리는 8장을 정본으로 두고, 여기서는 태그드 타입(tagged type), 타입 확장(type extension), 프리미티브 서브프로그램(primitive subprogram), 클래스-와이드 타입(class-wide type), 디스패칭(dispatching), 추상 타입과 인터페이스에 집중합니다.73

Ada의 OOP 모델은 “모든 연산을 객체 안에 넣는” 모델이 아닙니다. 타입과 그 연산은 보통 package specification 안에 함께 선언되고, tagged type의 primitive subprogram이 dispatching operation이 됩니다. 이 구조 때문에 Ada에서는 package 모듈화와 tagged-type dispatching을 구분해 이해하는 것이 중요합니다.

11.1 Ada의 객체 지향 패러다임 개요

Ada는 tagged type과 type extension으로 상속과 확장, class-wide type과 dispatching call로 런타임 다형성, package와 private view로 캡슐화와 정보 은닉을 구성합니다.

핵심 흐름은 다음과 같습니다.

  1. tagged type을 선언합니다.
  2. 같은 선언 영역에서 그 타입을 피연산자 또는 결과로 사용하는 primitive subprogram을 선언합니다.
  3. new Parent with ...로 자손 type을 만들면 부모의 primitive subprogram이 상속됩니다.
  4. 자손이 상속된 operation을 override할 수 있습니다.
  5. class-wide actual을 controlling operand로 사용해 primitive operation을 호출하면 runtime tag에 따라 body가 선택될 수 있습니다.

여기서 tag는 구현의 vtable 포인터와 같은 특정 메모리 표현을 뜻하는 언어 용어가 아닙니다. Ada 언어 수준에서는 객체의 specific tagged type을 식별하는 runtime value이며, 실제 표현 방법은 구현체가 정합니다. 객체가 생성된 뒤 그 객체의 tag 자체는 바뀌지 않습니다.74

11.2 캡슐화와 정보 은닉 (Encapsulation and Information Hiding)

Ada의 캡슐화는 OOP 전용 문법에만 의존하지 않습니다. package specification/body, visible/private part, private type의 partial/full view가 일반적인 정보 은닉 경계를 만들고, tagged private type을 사용하면 이 경계 위에 상속과 dispatching을 결합할 수 있습니다.

예를 들어 다음처럼 representation을 숨긴 채 tagged hierarchy의 root를 공개할 수 있습니다.

package Devices is
   type Device is tagged private;
   procedure Reset (D : in out Device);
private
   type Device is tagged record
      Generation : Natural := 0;
   end record;
end Devices;

client는 Generation representation을 직접 보지 못하지만 Device가 tagged라는 사실과 visible primitive operation은 사용할 수 있습니다.

11.2.1 제한된 타입 (Limited Types)

limited type의 assignment·equality·build-in-place·lifetime 규칙은 4.1.6절이 정본입니다. OOP에서는 복사되어서는 안 되는 resource owner를 tagged limited private로 모델링하고, 필요하면 이를 extension하는 식으로 사용할 수 있습니다.

limited는 dispatching을 끄는 키워드가 아닙니다. tagged limited type도 primitive operation과 class-wide type을 가질 수 있습니다. 반대로 limited 여부만으로 tagged type이 되는 것도 아닙니다.

11.3 상속과 타입 확장 (Inheritance and Type Extension)

11.3.1 태그드 타입 (Tagged Types)

record type이나 private type 선언에 tagged가 있으면 tagged type입니다. interface type도 tagged type이고, interface에서 파생된 task/protected type도 tagged type입니다.

type Point is tagged record
   X, Y : Float := 0.0;
end record;

type Shape is tagged private;

specific tagged object에는 자신의 specific type을 식별하는 runtime tag가 연관됩니다. 이 tag가 class-wide dispatching의 기반이 됩니다.

tagged를 붙였다고 자동으로 heap allocation이 일어나거나 garbage collection이 생기는 것은 아닙니다. storage duration과 allocation은 일반 object/access type 규칙을 그대로 따릅니다.

11.3.2 타입 확장 (Extension)과 프리미티브 연산 (Primitive Operations)

태그드 타입에서 파생한 type extension은 새로운 tagged type입니다. record extension에서는 ancestor part에 새 component를 추가할 수 있습니다.75

type Point is tagged record
   X, Y : Float := 0.0;
end record;

type Colored_Point is new Point with record
   Color : Color_Type;
end record;

Colored_Point는 Point의 component와 primitive subprogram을 상속하고, 추가 primitive subprogram을 가질 수 있습니다.

primitive operation의 정확한 경계

primitive operation에는 predefined operation과 user-defined primitive subprogram이 포함됩니다. user-defined subprogram이 특정 type의 primitive가 되는 조건은 단순히 “그 type을 parameter로 쓴다”보다 엄격합니다. 대표적으로 package specification에서 type과 같은 specification에 직접 선언되고 그 type을 operand/result로 사용하는 subprogram이 primitive가 됩니다.76

package Geometry is
   type Point is tagged record
      X, Y : Float := 0.0;
   end record;

   procedure Move (P : in out Point; DX, DY : Float);
   function Origin return Point;
end Geometry;

Move에는 controlling parameter가 있고 Origin에는 controlling result가 있습니다. tagged type의 primitive subprogram은 dispatching operation입니다.

중요한 제한도 있습니다. tagged type의 explicit primitive subprogram declaration은 그 type이 frozen되기 전에 나와야 합니다. 예를 들어 type의 object declaration 등으로 type이 freeze된 뒤 새 primitive를 붙일 수 없습니다. GNAT 14.2.0도 이런 경우 primitive operation is declared too late로 거부합니다.77

11.3.3 연산 재정의 (overriding)

자손 type은 상속한 primitive subprogram을 override할 수 있습니다.

type Point is tagged record
   X, Y : Float := 0.0;
end record;

procedure Display (P : Point);

type Circle is new Point with record
   Radius : Float := 1.0;
end record;

overriding
procedure Display (C : Circle);

overriding indicator는 “이 선언은 반드시 inherited operation을 override해야 한다”는 compile-time check를 요청합니다. 실제 override가 아니면 오류입니다.

반대로 not overriding은 “새 operation이어야 한다”는 의도를 명시합니다. 실제로 inherited operation을 override한다면 오류가 됩니다.

not overriding
function Area (C : Circle) return Float;

이 두 indicator는 선택 사항인 경우가 많지만, hierarchy가 커질수록 이름 오타나 profile 변화로 의도치 않게 새 operation을 만드는 문제를 조기에 잡는 데 유용합니다.

11.4 다형성 (Polymorphism)

Ada의 runtime polymorphism은 dispatching operation을 dynamically determined controlling tag로 호출할 때 발생합니다. tagged type이라는 이유만으로 모든 호출이 자동 dispatching call이 되는 것은 아닙니다.

11.4.1 클래스-와이드 타입 (Class-Wide Types)

specific tagged type T에는 class-wide type T'Class가 연관됩니다. T'Class 값은 T 또는 그 descendant specific type의 tag를 가질 수 있습니다.

procedure Process (Item : Widget'Class);

Process는 Widget, Button, Window 같은 descendant object를 actual로 받을 수 있습니다.

class-wide object는 indefinite이다

class-wide type은 unknown discriminants를 가지므로 class-wide subtype은 unconstrained/indefinite입니다. 따라서 stand-alone class-wide object는 명시적으로 초기화해야 합니다.

B : Button;
Any_Widget : Widget'Class := B;

그리고 object가 만들어진 뒤 그 object의 tag는 바뀌지 않습니다. 이미 Button tag로 만들어진 Widget'Class object에 다른 descendant tag의 값을 대입하려 하면 tag check가 실패할 수 있습니다. GNAT 14.2.0 최소 예제에서도 다른 tag로 assignment할 때 Constraint_Error가 발생했습니다.

이 성질 때문에 서로 크기와 tag가 다른 object들을 array of Widget'Class로 직접 저장하는 방식은 사용할 수 없습니다. array component subtype은 definite해야 하므로 array (...) of Widget'Class는 compile-fail입니다. heterogeneous collection에는 access-to-class-wide, indefinite container 같은 별도 표현이 필요합니다.

class-wide view에서 보이는 component

class-wide view를 통해 공통 ancestor에 선언된 component는 다룰 수 있지만, 특정 descendant에서만 추가된 component를 바로 선택할 수는 없습니다. descendant-specific state가 필요하면 type membership/conversion 또는 dispatching operation으로 경계를 명확히 해야 합니다.

11.4.2 동적 디스패치 (Dynamic Dispatching)

태그드 type T의 primitive subprogram은 dispatching operation입니다. primitive subprogram의 formal이 specific type T 또는 access T 형태로 controlling 역할을 하며, call의 actual이 class-wide view를 통해 dynamically tagged일 때 runtime tag에 따라 body를 선택하는 dispatching call이 됩니다.78

package GUI is
   type Widget is tagged null record;
   procedure Show (W : Widget);

   type Button is new Widget with null record;
   overriding procedure Show (B : Button);
end GUI;

다음 wrapper가 핵심 패턴입니다.

procedure Display_Widget (Item : Widget'Class) is
begin
   Show (Item);
end Display_Widget;

Show의 formal은 Widget이라는 specific type입니다. Item이 Widget'Class이기 때문에 이 call의 controlling tag가 runtime에 결정되고, 실제 tag가 Button이면 Button의 overriding Show가 호출됩니다.

반대로 specific view를 이용한 call은 statically determined tag를 가질 수 있습니다.

Show (Widget (Some_Button));

이런 ancestor view conversion은 부모 implementation을 명시적으로 호출할 때 사용할 수 있습니다.

controlling result도 있다

primitive function의 result가 tagged type T이면 controlling result가 될 수 있습니다. 따라서 dispatching은 “class-wide parameter 하나를 받는 procedure”에만 한정된 개념이 아닙니다. 정확한 controlling tag 결정 규칙은 RM 3.9.2를 따릅니다.

prefixed view 호출은 Ada 2022 신기능이 아니다

태그드 object에는 primitive subprogram을 다음처럼 prefixed view로 호출할 수 있습니다.

Item.Show;

이는 Show (Item)의 다른 표기 형태입니다. 이 기능은 Ada 2005 RM에도 이미 정의되어 있으며 Ada 2022에서 새로 도입된 기능이 아닙니다. GNAT 14.2.0도 -gnat2005에서 같은 최소 예제를 정상 compile했습니다.

또한 일반 untagged record에 같은 문법이 자동으로 허용되는 것은 아닙니다. GNAT 14.2.0에서 untagged T의 X.Inc는 selector가 없다는 오류로 거부되었습니다.79

11.4.3 'tag와 'class 속성

S'Class는 subtype S가 속한 class의 class-wide subtype을 나타냅니다. S'Tag는 specific tagged type의 tag를 얻는 type-oriented attribute입니다. S가 이미 class-wide subtype이라면 S'Tag는 해당 class의 root type tag를 뜻합니다.80

반면 object prefix에 쓰는 X'Tag는 class-wide tagged object에 대해 정의됩니다.

procedure Identify (Item : Widget'Class) is
   T : constant Ada.Tags.Tag := Item'Tag;
begin
   ...
end Identify;

specific object에 바로 X'Tag를 붙이는 것은 허용되지 않습니다. GNAT 14.2.0도 X : Root; X'Tag를 Tag attribute can only be applied to objects of class-wide type으로 거부합니다. specific type의 tag가 필요하면 Root'Tag처럼 type/subtype attribute를 사용합니다.

Ada.Tags.Tag는 private type이므로 equality operator의 visibility도 일반 Ada 이름 해석 규칙을 따릅니다.

with Ada.Tags;

procedure Identify (Item : Widget'Class) is
   use type Ada.Tags.Tag;
begin
   if Item'Tag = Button'Tag then
      ...
   end if;
end Identify;

Root'Tag 같은 type attribute 값을 단순히 “compile-time constant”라고 부르지 않는 편이 정확합니다. nested tagged type 등에서는 tag가 elaboration과 runtime context를 반영할 수 있습니다.

11.4.4 전체 예제 코드

다음 예제는 실제로 서로 다른 descendant object를 하나의 collection으로 처리하기 위해 access-to-class-wide 배열을 사용합니다. array of Widget'Class를 직접 선언하지 않습니다.

gui.ads:

with Ada.Strings.Unbounded;

package GUI is
   use Ada.Strings.Unbounded;

   type Widget is tagged record
      Position_X : Integer := 0;
      Position_Y : Integer := 0;
      Visible    : Boolean := True;
   end record;

   not overriding
   procedure Show (W : Widget);

   type Window is new Widget with record
      Title : Unbounded_String;
   end record;

   overriding
   procedure Show (W : Window);

   type Button is new Widget with record
      Label : Unbounded_String;
   end record;

   overriding
   procedure Show (B : Button);
end GUI;

gui.adb:

with Ada.Strings.Unbounded;
with Ada.Text_IO;

package body GUI is
   use Ada.Strings.Unbounded;
   use Ada.Text_IO;

   procedure Show (W : Widget) is
   begin
      Put ("Widget" & Integer'Image (W.Position_X));
   end Show;

   overriding
   procedure Show (W : Window) is
   begin
      -- specific ancestor view이므로 부모 implementation을 호출한다.
      Show (Widget (W));
      Put_Line (" Window " & To_String (W.Title));
   end Show;

   overriding
   procedure Show (B : Button) is
   begin
      Show (Widget (B));
      Put_Line (" Button " & To_String (B.Label));
   end Show;
end GUI;

main.adb:

with Ada.Strings.Unbounded;
with GUI;

procedure Main is
   use Ada.Strings.Unbounded;
   use GUI;

   type Widget_Access is access all Widget'Class;

   Items : constant array (Positive range 1 .. 2) of Widget_Access :=
     [new Window'(Widget with Title => To_Unbounded_String ("Application")),
      new Button'(Widget with Label => To_Unbounded_String ("Submit"))];
begin
   for Item of Items loop
      Show (Item.all);  -- class-wide actual: dispatching call
      Item.all.Show;    -- 같은 dispatch를 prefixed view로 작성
   end loop;
end Main;

GNAT 14.2.0 -gnat2022에서 이 예제는 compile/run되며 Window와 Button의 overriding body가 각각 선택됩니다. 이 예제의 access objects는 짧은 process lifetime 예제라 명시적인 deallocation을 생략했습니다. 장시간 실행 서비스에서 동적으로 생성·폐기되는 hierarchy를 같은 방식으로 관리한다면 ownership과 storage reclamation 전략을 별도로 설계해야 합니다.

11.5 추상 타입과 인터페이스

11.5.1 추상 타입 (Abstract Types) 및 서브프로그램 (Abstract Subprograms)

abstract type은 자체 object를 만들 수 없는 tagged type이며 descendant의 ancestor로 사용하기 위한 type입니다. class-wide type T'Class 자체는 abstract type이 아니므로 concrete descendant value를 담는 class-wide object/parameter는 사용할 수 있습니다.81

package Shapes is
   type Shape is abstract tagged null record;

   function Area (S : Shape) return Float is abstract;
   procedure Display (S : Shape) is abstract;
end Shapes;

이 specification은 abstract subprogram에 body가 필요하지 않으므로 위 선언만으로 충분합니다. 오히려 completion을 요구하는 다른 declaration이 없는데 package body Shapes is ... end Shapes;를 임의로 추가하면 GNAT 14.2.0은 package가 body를 허용하지 않는다고 거부합니다.

abstract primitive와 descendant

abstract subprogram이 tagged type의 primitive라면 그 tagged type은 abstract여야 합니다. concrete descendant가 inherited abstract primitive를 그대로 남겨 둘 수는 없습니다.

type Circle is new Shape with record
   Radius : Float := 1.0;
end record;

overriding
function Area (S : Circle) return Float;

overriding
procedure Display (S : Circle);

override하지 않으면 concrete descendant declaration이 compile-fail합니다. descendant 자체를 계속 abstract로 선언하는 방법도 있습니다.

abstract subprogram에는 실행할 body가 없으므로 nondispatching call은 허용되지 않습니다. 반면 abstract root의 class-wide operand를 통한 dispatching call은 concrete descendant의 overriding body로 dispatch될 수 있습니다. GNAT 14.2.0 최소 예제에서 Root'Class wrapper를 통해 abstract Run을 호출하면 concrete Child.Run이 실행되었습니다.

11.5.2 인터페이스 타입 (Interface Types)과 다중 상속

interface type은 restricted form of multiple inheritance를 제공하는 specific abstract tagged type입니다. interface 자체에는 component가 없습니다.82

type Loggable is interface;
procedure Write_Log (X : Loggable) is abstract;

interface의 user-defined primitive subprogram은 abstract subprogram 또는 null procedure여야 합니다. 따라서 “interface operation은 모두 abstract여야 한다”라고만 쓰는 것도 정확하지 않습니다.

type Resettable is interface;
procedure Reset (X : Resettable) is null;

하나의 parent + 여러 progenitor interface

record extension은 하나의 parent type에서 state/implementation을 이어받으면서 여러 interface를 progenitor로 둘 수 있습니다.

type Schedulable is interface;
procedure Execute (X : in out Schedulable) is abstract;

type Loggable is interface;
procedure Write_Log (X : Loggable) is abstract;

type Base_Task is tagged record
   ID : Integer := 0;
end record;

type Job is new Base_Task and Schedulable and Loggable with record
   Priority : Integer := 0;
end record;

overriding
procedure Execute (X : in out Job);

overriding
procedure Write_Log (X : Job);

이것은 C++식으로 여러 concrete base의 data representation을 동시에 상속하는 모델이 아닙니다. Ada에서는 interface progenitor를 통해 여러 operation contract를 결합합니다.

interface 자체도 다른 interface들을 상속할 수 있습니다.

type Managed is interface and Schedulable and Loggable;

또한 Ada에는 limited interface, task interface, protected interface, synchronized interface가 있으며, task/protected type도 적합한 interface를 구현할 수 있습니다. 이들 사이에는 limited/synchronized compatibility에 관한 추가 legality rule이 있으므로 “아무 interface나 모든 concrete type에 자유롭게 붙일 수 있다”고 일반화해서는 안 됩니다.

GNAT 14.2.0 최소 예제에서 하나의 tagged record parent와 두 interface를 동시에 상속한 concrete type이 두 abstract operation을 override하여 정상 compile/run되었습니다. 반대로 interface primitive를 일반 concrete subprogram declaration으로만 두면 must be abstract or null 오류가 발생했고, null procedure는 정상 허용됐습니다.

12. 예외, 계약과 검증

Ada의 신뢰성 기능은 하나의 장치가 아니라 여러 층으로 구성됩니다. 타입과 subtype constraint는 표현 가능한 값의 범위를 좁히고, language-defined runtime check는 실행 중 위반을 검출하며, exception은 비정상 제어 이전을 표현합니다. Pre/Post, predicate, invariant와 assertion은 프로그램이 성립해야 한다고 주장하는 조건을 소스에 기록합니다.

이 장에서는 다음 경계를 구분합니다.

  • constraint와 runtime check: 값이나 연산이 언어가 요구하는 조건을 만족하는지 검사합니다.
  • exception: 발생한 exceptional event를 식별하고 handler 또는 바깥 실행 문맥으로 전달합니다.
  • contract와 assertion: 호출 경계나 프로그램 내부의 가정을 명시합니다. 일부 assertion 검사는 policy에 따라 비활성화될 수 있습니다.
  • API의 타입과 이름: subtype constraint, 별도 type, procedure name이 각각 어떤 종류의 의미를 표현하는지 구분합니다.

SPARK의 flow analysis와 proof는 17장 SPARK와 형식 검증에서 별도로 다룹니다. 이 장에서는 일반 Ada 실행 의미와 런타임 검증 경계에 집중합니다.


12.1 예외와 오류 상태

Ada에서 exception은 exception occurrence가 발생했을 때 정상 실행을 중단하고 handler로 제어를 이전하거나 바깥 실행 문맥으로 전파하는 언어 메커니즘입니다. 사용자 정의 exception을 도메인 실패에 사용할 수도 있고, language-defined check가 실패했을 때 런타임이 exception을 발생시키기도 합니다.83

package Standard에는 Constraint_Error, Program_Error, Storage_Error, Tasking_Error 네 predefined exception이 선언되어 있습니다. 예를 들어 많은 range/index/null-access check 실패는 Constraint_Error를 발생시킵니다. 반면 assertion 실패에 사용하는 Assertion_Error는 Standard가 아니라 Ada.Assertions에 선언됩니다. 각 check가 어떤 exception을 발생시키는지는 해당 언어 규칙을 확인해야 하며, 모든 실패를 하나의 exception 종류로 일반화해서는 안 됩니다.84

12.1.1 예외 선언

사용자 정의 exception은 exception declaration으로 만듭니다.

Network_Error  : exception;
Database_Error : exception;

각 single exception declaration은 서로 다른 exception을 선언합니다. 같은 이름의 declaration이 여러 번 elaboration된다고 매번 새 exception이 생기는 것은 아닙니다. 반대로 generic unit 안에 exception declaration이 있으면 서로 다른 generic instance에서 암시적으로 생성된 declaration들은 서로 다른 exception을 나타냅니다.84

따라서 exception의 의미를 단순히 출력되는 이름 문자열로 비교해서는 안 됩니다. exception 자체의 정체성이 필요하면 E'Identity 또는 Ada.Exceptions.Exception_Identity가 제공하는 Exception_Id를 사용합니다.

12.1.2 예외 발생, handler와 propagation

명시적인 raise statement에는 exception 이름과 선택적인 message를 지정할 수 있습니다.85

if Count < 0 then
   raise Constraint_Error with "Count must not be negative";
end if;

expression이 값을 제공해야 하는 위치에서도 raise_expression을 사용할 수 있습니다.

function Parse (OK : Boolean) return Integer is
  (if OK then 42
   else raise Data_Error with "parse failed");

exception은 독립적인 “예외 블록” 문장이 아니라 handled_sequence_of_statements의 handler 부분입니다. 실행 중 exception이 발생하면 그 sequence의 남은 정상 문장은 실행하지 않고 적용 가능한 handler를 찾습니다.86

begin
   Process_Data;
exception
   when X : Constraint_Error =>
      Ada.Text_IO.Put_Line
        (Ada.Exceptions.Exception_Message (X));
   when Storage_Error | Program_Error =>
      Recover_Or_Log;
   when others =>
      Log_Unexpected;
end;

when X : ...의 X는 현재 occurrence를 나타내는 Ada.Exceptions.Exception_Occurrence 상수입니다. others choice는 그 handled sequence의 마지막 handler에서 단독으로 사용해야 합니다. 같은 handled sequence 안에서 동일 exception을 두 handler가 중복해서 덮을 수도 없습니다.

handler가 없거나 어떤 handler choice도 발생한 exception을 덮지 않으면 occurrence는 동적으로 바깥 실행 문맥으로 propagation됩니다. 또한 handler 자체가 새 exception을 발생시키면 같은 handled sequence의 다른 handler가 그것을 다시 잡지 않습니다. 그 새 occurrence는 더 바깥쪽으로 propagation됩니다.

현재 occurrence를 그대로 다시 전달하려면 handler 안에서 bare raise;를 사용합니다.

exception
   when X : Network_Error =>
      Log (Ada.Exceptions.Exception_Message (X));
      raise;

인자 없는 raise;는 re-raise statement이며 exception handler 바로 안에서만 허용됩니다. re-raise는 현재 처리 중인 occurrence를 다시 propagation하므로 raise Network_Error;로 새 occurrence를 만드는 것과 구분해야 합니다.

Ada.Exceptions는 occurrence의 이름과 message를 조회하는 Exception_Name, Exception_Message, 진단용 Exception_Information, identity 조회와 occurrence 저장/re-raise 기능을 제공합니다.87 raise E with Message로 지정한 message는 Exception_Message에서 얻을 수 있습니다. 반면 message를 지정하지 않은 raise나 Exception_Information의 상세 문자열 형식은 구현 정의 요소가 있으므로 로그 parser가 특정 GNAT 문구에 과도하게 의존하지 않는 편이 안전합니다.

12.1.3 제약 검사와 예외

타입, subtype, constraint와 predicate의 기본 설명은 4.1.3절부터 4.1.5절에서 다룹니다. 여기서는 constraint 자체와 constraint check 실패를 전달하는 exception을 구분합니다.

subtype Small is Integer range 1 .. 3;
X : Integer := 4;
Y : Small;

-- 실행 시 range check가 실패하면 Constraint_Error
Y := Small (X);

subtype constraint는 허용되는 값의 범위를 표현하지만 별도의 새 type identity를 만들지는 않습니다. check가 필요한 지점에서 값이 constraint를 만족하지 못하면 해당 language-defined check가 exception을 발생시킵니다. 이것은 12.2~12.3의 assertion policy와도 별개입니다. 예를 들어 range check는 Assertion_Policy (Ignore)로 꺼지는 assertion이 아닙니다.

도메인에서 복구 가능한 입력 오류와 프로그래머의 불변식 위반도 구분하는 편이 좋습니다. 외부 입력이 잘못될 수 있다는 것이 정상적인 API 계약이라면 명시적인 validation과 domain exception/오류 결과를 설계하고, “여기서는 반드시 참이어야 한다”는 내부 가정은 assertion이나 contract로 표현하는 방식이 진단과 유지보수에 유리합니다.


12.2 계약 기반 프로그래밍

계약 기반 프로그래밍(Contract-Based Programming)은 호출 전후에 성립해야 할 조건과 타입이 유지해야 할 성질을 소스에 명시하는 방식입니다. Ada 2012부터 Pre, Post, subtype predicate, type invariant 같은 언어 정의 assertion aspect를 사용할 수 있습니다(ARM 3.2.4, 6.1.1, 7.3.2, 11.4.2).

여기서 계약을 명세하는 것과 실행 중 계약을 검사하는 것은 구분해야 합니다. Pre/Post, predicate, invariant의 실행 검사는 적용되는 Assertion_Policy에 따라 활성화되거나 비활성화될 수 있습니다. 따라서 반드시 실행되어야 하는 외부 입력 검증이나 복구 가능한 오류 처리를 assertion 하나에만 의존해서는 안 됩니다.

태그드 타입의 상속 계약에는 Pre'Class와 Post'Class가 별도로 있으며 상속·디스패치 규칙이 추가됩니다. 이 절에서는 우선 특정 callable entity에 적용하는 Pre와 Post의 기본 의미에 집중합니다.

12.2.1 사전조건(Preconditions)

Pre는 서브프로그램이나 엔트리 호출 전에 만족해야 하는 specific precondition을 지정합니다(ARM 6.1.1). 개념적으로 호출자가 만족시켜야 하는 조건을 인터페이스에 기록하는 데 사용합니다.

procedure Divide
  (Left  : Integer;
   Right : Integer)
with Pre => Right /= 0;

해당 Pre 검사가 활성화되어 있으면 actual parameter를 평가한 뒤 호출 직전에 조건을 검사합니다. 조건이 거짓이면 Ada.Assertions.Assertion_Error가 호출 지점에서 발생하며, 이 실패를 Divide 본체 내부의 exception handler가 처리할 수는 없습니다.

검사가 비활성화되어 있으면 precondition expression 자체가 그 지점에서 평가되지 않습니다. 그러므로 Pre에 프로그램의 정상 동작에 필요한 부작용을 넣어서는 안 됩니다.

12.2.2 후조건(Postconditions)

Post는 서브프로그램이나 엔트리가 정상적으로 반환한 뒤 만족해야 하는 specific postcondition을 지정합니다. 활성화된 후조건이 거짓이면 Ada.Assertions.Assertion_Error가 호출 지점에서 발생합니다.

procedure Increment (Value : in out Integer)
with Post => Value = Value'Old + 1;

X'Old는 활성화된 postcondition을 위해 호출된 본체 실행 전의 X 값을 보존해 참조하는 속성입니다. Old는 postcondition expression 안에서만 사용할 수 있습니다. 함수의 반환 객체는 해당 함수 이름의 'Result 속성으로 참조합니다.

function Twice (Value : Integer) return Integer
with Post => Twice'Result = Value * 2;

by-copy인 out/in out 파라미터가 있다면 postcondition 검사는 정상 반환 뒤 copy-back보다 먼저 수행됩니다(ARM 6.1.1). 따라서 7.3절의 parameter passing 규칙과 후조건의 검사 시점을 같은 것으로 취급하면 안 됩니다.

12.2.3 타입 제약과 계약의 차이

타입 제약(constraint)은 값의 허용 범위를 타입/서브타입 모델의 일부로 제한합니다. 예를 들어 Natural의 범위 제약은 값이 음수가 아닌지를 표현합니다.

subtype Natural is Integer range 0 .. Integer'Last;

반면 Static_Predicate와 Dynamic_Predicate는 서브타입에 의미 조건을 더하는 assertion aspect입니다. 기본 개념과 선언 예는 4.1.5절에서 다룹니다. predicate 검사가 활성화된 경우 subtype conversion 등 ARM이 지정한 지점에서 조건을 검사합니다. 직접 지정된 Predicate_Failure가 별도의 예외를 전파하지 않는 일반적인 경우, 실패는 Ada.Assertions.Assertion_Error로 보고됩니다.

subtype Even_Number is Integer
  with Dynamic_Predicate => Even_Number mod 2 = 0;

predicate policy가 Ignore라고 해서 predicate가 언어에서 완전히 사라지는 것은 아닙니다. membership test와 Valid 같은 일부 문맥에서는 predicate가 여전히 평가됩니다(ARM 3.2.4). 따라서 Assertion_Policy(Ignore)를 단순한 “predicate 제거”로 이해하면 안 됩니다.

Type_Invariant와 Type_Invariant'Class도 assertion aspect입니다. type invariant는 주로 private type의 추상화 경계에서 객체가 외부로 관찰되는 시점에 성립해야 할 성질을 표현하며, ARM 7.3.2가 지정한 초기화·변환·서브프로그램 정상 반환 등의 지점에서 검사됩니다. 객체의 모든 내부 필드 대입 직후에 항상 검사되는 규칙은 아닙니다.

package Counters is
   type Counter is private;
private
   type Counter is record
      Value : Integer := 0;
   end record
     with Type_Invariant => Counter.Value >= 0;
end Counters;

정리하면 constraint는 타입 모델의 값 범위를, predicate는 서브타입의 추가 성질을, pre/postcondition은 호출 경계의 동작 조건을, type invariant는 타입의 추상화 경계에서 유지할 성질을 표현합니다. 이들 assertion aspect의 런타임 검사는 다음 절의 assertion policy에 영향을 받습니다.


12.3 Assertion과 명시적 검증

Ada RM에서 pragma Assert, subtype predicate, pre/postcondition, type invariant 등은 함께 assertion으로 분류됩니다(ARM 11.4.2). 이 assertion들은 조건을 명세하는 방법뿐 아니라 현재 영역에 적용되는 assertion policy에 따라 실행 시 검사할지 여부가 결정된다는 공통점을 가집니다.

12.3.1 pragma Assert

pragma Assert는 선언 또는 문장 순서의 특정 지점에서 Boolean 조건을 assertion으로 둡니다.

pragma Assert (Count >= 0, "Count must not be negative");

Assert 정책이 Check이면 조건을 평가하고, 거짓이면 Ada.Assertions.Assertion_Error를 발생시킵니다. Ignore이면 해당 지점에서 조건과 message expression을 평가하지 않고 런타임 검사도 수행하지 않습니다.

assertion expression에는 정상 동작에 필요한 부작용을 넣지 않는 것이 중요합니다. 정책에 따라 표현식이 평가되지 않을 수 있고, Ada는 assertion 값 판정에 필요하지 않은 일부 함수 호출을 구현이 생략할 수 있도록 허용하기 때문입니다.

12.3.2 Assertion Policy

pragma Assertion_Policy는 assertion 종류별 런타임 검사 정책을 지정합니다. Ada가 정의하는 기본 policy identifier는 Check와 Ignore이며 구현이 추가 policy를 제공할 수도 있습니다. 아무 Assertion_Policy도 적용되지 않을 때의 정책은 구현 정의입니다.

pragma Assertion_Policy (Check);

인자를 하나만 쓰면 해당 정책이 모든 assertion aspect에 적용됩니다. 종류별로 다르게 지정할 수도 있습니다.

pragma Assertion_Policy
  (Pre  => Check,
   Post => Check,
   Assert => Ignore);

안쪽 declarative region의 정책은 같은 assertion 종류에 대해 바깥 정책보다 우선합니다.

GNAT의 -gnata는 Ada 언어 자체의 옵션이 아니라 GNAT 컴파일러 스위치입니다. GNAT 문서에서는 이를 광범위한 pragma Assertion_Policy (Check)와 동등한 방식으로 설명하며 Assert, pre/postcondition, predicate, type invariant 등을 활성화합니다.

현재 검증 환경의 GNAT 14.2.0에서는 별도 policy와 -gnata가 없을 때 false pragma Assert, Pre/Post, Dynamic_Predicate, Type_Invariant의 일반 런타임 검사가 수행되지 않았고, -gnata로 다시 컴파일하면 모두 활성화되었습니다. 이것은 GNAT 동작의 실험 결과이며, Ada 전체 구현의 기본값으로 일반화해서는 안 됩니다.

12.3.3 Ada.Assertions

표준 라이브러리 Ada.Assertions는 Assertion_Error 예외와 두 개의 Assert 프로시저를 제공합니다.

Ada.Assertions.Assert
  (Check   => Count > 0,
   Message => "count must be positive");

이 프로시저 호출은 Assertion_Policy와 독립적으로 동작합니다. Check가 거짓이면 Ada.Assertions.Assertion_Error가 발생합니다. 현재 GNAT 14.2.0 실험에서도 -gnata 없이 false 조건을 호출했을 때 동일한 표준 예외로 처리되는 것을 확인했습니다.

따라서 컴파일 시 assertion policy로 제거되어서는 안 되는 내부 검사를 표현하려면 Ada.Assertions.Assert를 사용할 수 있습니다. 다만 외부 입력 오류처럼 정상적인 복구 경로가 필요한 상황이라면 assertion보다 명시적인 조건 검사와 도메인 예외·오류 반환이 더 적절할 수 있습니다. assertion은 주로 프로그램이 성립해야 한다고 가정하는 불변 조건과 프로그래머 오류를 드러내는 데 사용합니다.


12.4 의미 기반 API 설계

Ada에서 type identity, subtype constraint, API 이름은 서로 다른 설계 도구입니다. 같은 기계 표현을 쓸 수 있다고 해서 같은 도메인 개념일 필요는 없고, 같은 type의 여러 subtype이라고 해서 overload resolution에서 서로 다른 type으로 취급되는 것도 아닙니다.88

예를 들어 offset, count, capacity, index, timeout, identifier는 모두 정수로 표현할 수 있지만 서로 바꿔 써도 되는지는 별도의 도메인 결정입니다.

12.4.1 서브타입만으로 의미 모델링을 끝내지 말 것

Natural과 Positive는 별도 integer type이 아니라 Integer의 subtype입니다. 따라서 다음 세 declaration은 세 overload를 만드는 코드가 아닙니다.

procedure Process (Value : Integer);
procedure Process (Value : Natural);   -- illegal: 같은 parameter type
procedure Process (Value : Positive);  -- illegal: 같은 parameter type

GNAT 14.2.0도 뒤 두 declaration을 첫 declaration과 충돌한다고 진단합니다. overload resolution은 parameter의 type을 사용하며 Natural의 0 .. 또는 Positive의 1 .. constraint를 overload 구별자로 사용하지 않습니다.

도메인을 API 이름으로 명시하는 방법은 단순하고 읽기 쉽습니다.

procedure Set_Offset   (Value : Integer);
procedure Set_Count    (Value : Natural);
procedure Set_Capacity (Value : Positive);

잘못된 도메인 값을 컴파일 단계에서 더 강하게 섞지 못하게 하려면 별도의 type을 선언할 수 있습니다.

type Offset_Value is new Integer;
type Count_Value  is range 0 .. 1_000_000;

procedure Process (Value : Offset_Value);
procedure Process (Value : Count_Value);  -- 별도 type이므로 overload 가능

즉 subtype은 같은 type 안에서 값의 constraint를 표현하는 데 적합하고, 별도 type은 type identity를 분리하며, 서브프로그램 이름은 도메인 행위의 의미를 드러냅니다. 어느 층을 사용할지는 변환 비용, 상호 운용성, 오용 위험과 API 가독성을 함께 보고 결정해야 합니다.

12.4.2 Generic을 이용한 구현 공유

API 이름이나 domain type을 분리해도 내부 알고리즘은 generic으로 공유할 수 있습니다. 10장의 generic contract model과 같은 원리입니다.

generic
   type Value_Type is range <>;
procedure Process_Integer_Like_Value
  (Value : Value_Type);

formal scalar type의 actual은 subtype mark일 수 있으므로 Integer, Natural, Positive를 각각 actual subtype으로 사용할 수 있습니다.

procedure Process_Integer is
  new Process_Integer_Like_Value (Integer);

procedure Process_Natural is
  new Process_Integer_Like_Value (Natural);

procedure Process_Positive is
  new Process_Integer_Like_Value (Positive);

이 세 instance는 서로 다른 wrapper 이름을 가지므로 12.4.1의 illegal subtype-overload 문제와 다릅니다. actual subtype의 constraint도 generic 안에서 의미가 있습니다. GNAT 14.2.0 최소 실험에서 같은 range <> formal을 Natural과 Positive로 instantiate했을 때 각각 T'First = 0, T'First = 1로 확인했습니다.

별도 domain type을 선택한 경우에도 같은 generic을 각 type으로 instantiate하여 구현을 공유할 수 있습니다. generic은 도메인 경계를 없애기 위한 기능이 아니라, 명시한 formal contract를 만족하는 여러 type/subtype에 공통 알고리즘을 재사용하기 위한 기능입니다.

12.4.3 Assertion 유틸리티에서의 적용

테스트·진단 API에서는 type/subtype과 함께 의도를 드러내는 이름과 message가 중요합니다.

Assert_Equal_Count
  (Expected => 3,
   Actual   => Current_Count);

예를 들어 다음과 같은 진단은 단순히 3 /= 2라고 출력하는 것보다 실패한 도메인 의미를 빨리 파악하게 해 줍니다.

expected count : 3
actual count   : 2

여러 integer-like type/subtype에서 비교 알고리즘이 같다면 12.4.2처럼 generic implementation을 공유하고 Assert_Equal_Count, Assert_Equal_Index 같은 public wrapper를 둘 수 있습니다. 반대로 같은 Integer type의 subtype 차이만으로 여러 Assert_Equal overload를 구분하려는 설계는 성립하지 않습니다.

12.3에서 본 것처럼 pragma Assert와 contract assertion은 policy에 따라 runtime check가 생략될 수 있습니다. 테스트 harness처럼 반드시 실행되어야 하는 검사라면 Ada.Assertions.Assert 호출 또는 명시적인 조건 검사/예외 발생을 사용해야 합니다. 외부 입력 오류처럼 복구가 정상 흐름의 일부라면 assertion 하나에 의존하지 말고 API의 오류 결과나 domain exception을 설계합니다.


12.5 정리

Ada의 신뢰성 모델은 서로 다른 역할의 언어 기능을 겹쳐 사용하는 방식으로 이해하는 것이 좋습니다.

  • type/subtype/constraint는 값의 타입 정체성과 허용 범위를 모델링합니다.
  • language-defined check는 필요한 실행 지점에서 위반을 검출하고 정해진 exception을 발생시킵니다.
  • exception handler와 propagation은 실패가 발생한 뒤 제어를 어디에서 처리할지 결정합니다.
  • contract와 assertion은 호출 경계와 내부 불변식을 명시하지만 runtime check 여부는 assertion policy를 확인해야 합니다.
  • 별도 type과 API 이름은 constraint만으로 표현하기 어려운 도메인 의미와 오용 경계를 만듭니다.
  • generic은 이 의미 경계를 지우지 않고도 공통 구현을 공유할 수 있게 합니다.

이 층들을 구분하면 “컴파일 시 타입 오류”, “runtime constraint failure”, “복구 가능한 domain failure”, “프로그램 내부 assertion failure”를 같은 종류의 오류로 뭉뚱그리지 않고 각각 알맞은 인터페이스와 진단 경로로 처리할 수 있습니다. 신뢰성은 exception을 많이 잡거나 assertion을 많이 넣는 것 자체가 아니라, 실패의 종류와 책임 경계를 코드에 정확히 표현하는 것에서 시작합니다.

13. GNAT 프로젝트 관리 및 빌드 시스템

이 장은 Ada 언어 표준이 아니라 GNAT/GPR 도구 계층을 다룹니다. Ada의 컴파일 단위와 주 서브프로그램 규칙은 7~8장을 따르고, 여기서는 GPR 프로젝트 파일, gprbuild, gprclean, scenario variable, 플랫폼별 소스 선택과 크로스 툴체인 구성을 설명합니다.

GPR 도구는 Ada 외의 언어도 함께 다룰 수 있으며 Ada 표준이 특정 빌드 도구를 지정하는 것은 아닙니다. 이 장은 2026-09-01 기준 GPR Reference/User Guide 27.0w와 GNAT 14.2 문서, 그리고 Debian에 설치된 GPRBUILD/GPRCLEAN/GPRCONFIG Pro 18.0w 및 GNAT 14.2.0을 대조하여 검증했습니다. 최신 GPR 도구와 로컬 18.0w 사이에는 engine·option·project-language 세부 차이가 있을 수 있으므로 구현·버전 고유 동작을 Ada 언어 규칙처럼 일반화하지 않습니다.89

13.1 gprbuild 소개

13.1.1 Ada 프로젝트 빌드 도구 개요

gprbuild는 GPR project tree를 빌드하는 다중 언어 도구입니다. 프로젝트 파일을 읽어 필요한 소스를 컴파일하고, Ada 프로그램을 bind하며, 실행 파일과 라이브러리를 생성합니다. C 같은 다른 언어를 프로젝트에 포함할 수도 있습니다.

gprbuild는 의존성과 빌드 설정을 추적해 이미 최신인 산출물의 작업을 반복하지 않습니다. GPR 27.0w에는 legacy engine과 GPRbuild2가 함께 있으며, 현재 문서는 legacy를 기본 engine으로 두면서 GPRbuild2를 권장하고 GPRbuild2의 DAG·persistent build database·checksum signature 동작을 별도로 설명합니다. 따라서 이 내부 구현을 오래된 GPRbuild 전체에 소급해서 일반화하면 안 됩니다. 로컬 GPRBUILD Pro 18.0w에서도 동일 설정의 두 번째 빌드가 up to date로 판정되지만, --gpr=2를 실제 build에 주면 illegal option으로 거부되었습니다.

13.1.2 GNAT, GPRbuild, GNAT Studio와의 관계

  • GNAT은 GCC 기반 Ada 컴파일러와 관련 도구 집합입니다.
  • GPRbuild는 GPR 프로젝트 기술을 읽고 compiler, binder, linker, archiver 등의 실행을 조정합니다.
  • GNAT Studio는 GPR 프로젝트를 사용할 수 있는 IDE입니다. IDE의 동작과 프로젝트 언어 자체의 의미는 구분해서 이해해야 합니다.

즉 Ada 언어 규칙, GNAT 컴파일러 옵션, GPR 프로젝트 규칙은 서로 다른 층위입니다.

13.2 프로젝트 파일(.gpr) 기본 구조

13.2.1 project 선언

기본적인 standard project는 다음과 같이 선언합니다.

project Hello is
   -- project declarations
end Hello;

공식 GPR 문서는 프로젝트 이름과 .gpr 파일 이름이 대소문자를 무시하고 대응하도록 요구하는 형태를 사용합니다. 예를 들어 project Hello는 보통 hello.gpr에 둡니다. 로컬 GPRBUILD Pro 18.0w는 이 이름이 불일치해도 경고 후 계속 처리했지만, 이러한 관대한 동작에 의존하지 않는 것이 좋습니다.

13.2.2 주요 속성: Source_Dirs, Object_Dir, Exec_Dir, Library_Name, Library_Dir

Source_Dirs는 GPR 도구가 프로젝트의 소스를 찾는 디렉터리 목록입니다. 생략하면 project file이 있는 디렉터리가 source directory가 됩니다.90

for Source_Dirs use ("src", "generated");

하위 디렉터리가 자동으로 모두 포함되는 것은 아니지만, GPR은 재귀 패턴도 지원합니다.

for Source_Dirs use ("src/**");

따라서 모든 하위 디렉터리를 반드시 하나씩 나열해야 하는 것은 아닙니다. 로컬 GPRBUILD Pro 18.0w에서도 src/** 아래의 Ada main source를 찾아 빌드하는 것을 확인했습니다.

다중 언어 프로젝트에서는 Languages를 명시합니다. Languages를 생략한 standard project의 기본 언어는 Ada입니다. 따라서 C source를 프로젝트에 넣기만 하고 "C"를 추가하지 않으면 그 C source가 자동으로 compile되는 것은 아닙니다. 로컬 18.0w에서도 일부러 문법 오류가 있는 .c 파일을 같은 source directory에 두고 Languages를 생략한 Ada project를 정상 build했습니다.

for Languages use ("Ada", "C");

Object_Dir은 객체 파일과 ALI 등 컴파일 산출물을 두는 디렉터리이고, Exec_Dir은 링크된 실행 파일을 두는 디렉터리입니다. 둘 다 생략하면 project directory가 기본 위치이며, Object_Dir만 지정하고 Exec_Dir을 생략하면 executable은 Object_Dir에 놓입니다. 로컬 18.0w에서도 두 기본값을 각각 확인했습니다.

for Object_Dir use "obj";
for Exec_Dir   use "bin";

소스와 중간 산출물·실행 파일을 분리하려면 두 속성을 명시하는 편이 좋습니다. GPRbuild는 project 아래의 상대 Object_Dir/Exec_Dir가 없으면 build 과정에서 만들 수 있으므로, CI나 장기 build에서는 profile별 경로를 분리해 stale artifact가 섞이지 않게 하는 것이 안전합니다.

라이브러리는 새 프로젝트에서 종류를 명시적으로 드러내는 것이 명확합니다.

library project Util is
   for Source_Dirs  use ("src");
   for Object_Dir   use "obj";
   for Library_Name use "util";
   for Library_Dir  use "lib";
   for Library_Kind use "static";
end Util;

Library_Name은 라이브러리의 기본 이름이고 Library_Dir은 라이브러리 산출물 위치입니다. 최신 GPR 문서에서는 library project qualifier 사용을 권장하며, qualifier를 생략한 호환 문법에서는 Library_Name과 Library_Dir을 함께 선언한 프로젝트를 library project로 해석합니다. Library_Name 하나만 선언했다고 곧바로 library project가 되는 것은 아닙니다. Library_Dir은 Object_Dir과 달라야 하며, 로컬 18.0w도 같은 디렉터리를 지정한 project를 processing 단계에서 거부했습니다. Library_Kind를 생략한 library project의 기본 kind는 static입니다.91

13.2.3 빌드 명령어 예시

프로젝트 파일을 명시적으로 선택하려면 -P를 사용합니다.

gprbuild -P hello.gpr

현재 GPR 27.0w의 common option 규칙은 -P가 없을 때 현재 디렉터리의 default.gpr을 우선 사용하고, 그것이 없으면 유일한 .gpr 파일을 자동 선택합니다.92 로컬 18.0w에서도 두 경우를 모두 재현했습니다. 여러 project가 있는 저장소·CI에서는 어떤 root project를 쓸지 명백하게 하기 위해 -P로 명시하는 편이 재현성이 좋습니다.

13.3 실행 파일 빌드와 프로젝트 종류

13.3.1 실행 파일 빌드: for Main use ("test_guiyom.adb");

Main 속성은 프로젝트를 main project로 빌드할 때 기본으로 생성할 실행 파일의 main source 목록을 지정합니다.

project Test_Guiyom is
   for Source_Dirs use ("src");
   for Main use ("test_guiyom.adb");
end Test_Guiyom;

그러나 Main은 실행 파일 빌드의 유일한 선택 방법도, 모든 standard project의 필수 속성도 아닙니다. Main이 없는 프로젝트에서도 다음처럼 main source를 명령줄에 지정할 수 있습니다.

gprbuild -P app.gpr main.adb

로컬 GPRBUILD Pro 18.0w에서 Main이 없는 프로젝트를 그냥 빌드하면 Ada 소스는 컴파일되지만 실행 파일은 생성되지 않았고, 같은 프로젝트에 hello.adb를 명령줄 main으로 주면 Exec_Dir에 실행 파일이 생성되었습니다.

GNAT의 기본 Ada source naming convention에서는 최상위 unit 이름과 source file 이름이 대응합니다. 예를 들어 procedure Test_Guiyom은 보통 test_guiyom.adb에 둡니다. 이것은 Ada RM이 정한 파일 이름 규칙이 아니라 toolchain naming convention이며 GPR Naming 설정으로 다른 매핑을 정의할 수도 있습니다. 로컬 GNAT 14.2.0/GPRBUILD Pro 18.0w에서는 이름 불일치가 경고로 보고되면서도 빌드는 계속되었습니다.

13.3.2 package Builder를 이용한 빌드 옵션 제어

Builder 패키지에는 실행 파일 빌드와 관련된 속성을 둘 수 있습니다. 예를 들어 main source별 실행 파일 이름을 바꾸려면 Executable을 사용합니다.

package Builder is
   for Executable ("test_guiyom.adb") use "test-guiyom";
end Builder;

로컬 GPRBUILD Pro 18.0w에서도 이 설정으로 source 이름과 다른 실행 파일 이름이 생성되는 것을 확인했습니다. Builder의 속성과 compiler/linker의 개별 속성은 역할이 다르므로 모든 빌드 옵션을 한 패키지에 모으는 방식으로 이해해서는 안 됩니다.

13.3.3 실행 프로젝트와 라이브러리 프로젝트의 차이 (오류 방지)

standard project는 실행 파일 또는 개별 object를 빌드할 수 있고, library project는 library artifact를 생성합니다. library project에는 Main을 둘 수 없습니다.

로컬 GPRBUILD Pro 18.0w에서도 Library_Name과 Library_Dir로 library project가 된 프로젝트에 Main을 함께 지정하면 project processing 단계에서 거부되었습니다. 라이브러리와 테스트/도구 실행 파일은 별도의 project view로 나누는 것이 명확합니다.

또한 다른 standard project를 with로 import했다고 해서 그 imported project의 Main이 자동으로 실행 파일을 만드는 것은 아닙니다. 실행 entry point는 root build의 Main이나 명령줄 main을 기준으로 생각하는 편이 안전합니다.

13.4 gprclean: 빌드 산출물 정리(Clean)

gprclean은 project tree를 읽어 gprbuild가 만든 object, ALI, executable, library 등 관련 산출물을 정리합니다.93 대규모 build tree나 삭제 범위를 처음 확인할 때는 먼저 -n dry-run으로 삭제 예정 파일을 보는 방법이 안전합니다.

gprclean -n -P app.gpr  # 삭제하지 않고 대상만 출력
gprclean -P app.gpr     # 실제 정리

기본 호출은 main project의 산출물을 정리하고, -r은 project tree의 관련 프로젝트까지 재귀적으로 정리합니다.

gprclean -r -P app.gpr

로컬 실험에서 application이 library project를 with한 상태로 빌드한 뒤 gprclean -P app.gpr을 실행하면 application 산출물만 제거되고 library 산출물은 남았습니다. 이어 gprclean -r -P app.gpr을 실행하면 imported library 산출물도 제거되었습니다.

-q는 출력을 줄이고 -v는 자세한 출력을 요청하는 tool option입니다. 최신 GPRclean은 main source를 명령줄에 지정해 특정 entry point 범위로 clean하는 기능과 추가 옵션도 제공하므로, 단순히 Object_Dir의 파일만 지우는 명령으로 이해하면 안 됩니다.

13.5 프로젝트 간 의존성 관리

13.5.1 with "other.gpr"; 구문

project-level with는 다른 GPR project view를 import합니다.

with "../util/util.gpr";

project App is
   -- ...
end App;

이는 Ada source의 with clause와 같은 구문 체계가 아니며 프로젝트 의존성을 선언합니다. imported standard project의 Ada unit은 root executable의 dependency closure에서 compile/link될 수 있고, imported library project라면 그 library artifact가 dependency로 사용될 수 있습니다. 로컬 18.0w에서도 library가 아닌 standard Util project를 import한 application이 with Util;을 통해 해당 unit을 compile/link해 정상 실행됐습니다.

13.5.2 다중 프로젝트 빌드 사례

gprbuild는 root project의 entry point나 library를 만들기 위해 필요한 imported project의 build action을 의존성 순서에 따라 수행합니다. 이를 “with한 모든 프로젝트의 모든 것을 무조건 빌드한다”고 이해하면 과합니다.

로컬 실험에서는 application project가 static library project를 with했을 때 library source가 먼저 컴파일되고 archive가 생성된 뒤 application이 bind/link되어 정상 실행되었습니다.

13.6 빌드 옵션과 컴파일 스위치

13.6.1 package Compiler와 Default_Switches

Compiler 패키지는 언어별 compiler switch를 지정합니다. Default_Switches는 더 구체적인 Switches 항목이 적용되지 않을 때 사용할 기본 compiler switch 목록입니다. Builder'Default_Switches/Builder'Switches는 이름이 비슷하지만 builder 자체의 switch이므로 -gnat2022, -O2, -gnatwa 같은 compiler option은 Compiler package에 두어야 합니다.

package Compiler is
   for Default_Switches ("Ada") use ("-gnat2022", "-O2", "-gnatwa");
end Compiler;

13.6.2 Ada 2022 모드 설정

GNAT에서 Ada 2022 모드를 선택하는 compiler switch는 -gnat2022입니다.

package Compiler is
   for Default_Switches ("Ada") use ("-gnat2022");
end Compiler;

이는 GNAT compiler option이지 Ada 소스 문법의 일부가 아닙니다.

13.6.3 최적화, 경고 옵션 관리

GCC/GNAT 계열에서는 -O2 같은 optimization option과 -gnatwa 같은 GNAT warning option을 compiler switch 목록에 넣을 수 있습니다. 사용하려는 warning category는 GNAT User’s Guide에서 해당 compiler version을 기준으로 확인해야 합니다.

특히 -gnatW 계열을 warning option으로 혼동하면 안 됩니다. 예를 들어 GNAT 14.2의 -gnatW8은 source의 wide-character encoding을 UTF-8 방식으로 해석하도록 지정하는 옵션입니다. warning control은 주로 소문자 -gnatw... 계열입니다.94

13.7 시나리오 변수 (Scenario Variables)

13.7.1 external 함수를 이용한 시나리오 변수 정의

scenario variable은 하나의 project file로 debug/release나 플랫폼별 설정을 선택할 때 유용합니다. external은 외부 문자열 값을 가져옵니다.95

type Build_Type is ("debug", "release");
Build : Build_Type := external ("BUILD", "debug");

external ("BUILD", "debug")의 값은 우선순위에 따라 다음에서 결정됩니다.

  1. 명령줄 -XBUILD=value
  2. 같은 이름의 환경 변수 BUILD
  3. 두 번째 인수의 기본값 "debug"

로컬 실험에서도 환경 변수보다 -XBUILD=release가 우선했습니다. typed string에 선언되지 않은 -XBUILD=oops는 project loading 단계에서 거부되었습니다.

GPR typed string의 값은 문자열 리터럴로 선언합니다.

type OS_Type is ("linux", "windows", "darwin", "freebsd");
OS_Name : OS_Type := external ("OS", "linux");

13.7.2 실습 예제: 시나리오 변수를 활용한 조건부 플랫폼 구성

scenario variable과 case를 결합하면 사용할 source directory나 compiler/linker setting을 선택할 수 있습니다.

type OS_Type is ("linux", "freebsd");
OS_Name : OS_Type := external ("OS", "linux");

case OS_Name is
   when "linux" =>
      for Source_Dirs use ("src", "src/linux");
   when "freebsd" =>
      for Source_Dirs use ("src", "src/freebsd");
end case;

명령줄에서는 다음처럼 branch를 선택합니다.

gprbuild -P app.gpr -XOS=freebsd

여기서 매우 중요한 점은 -XOS=freebsd가 compiler target을 FreeBSD로 바꾸지 않는다는 것입니다. 이는 단지 project file에서 OS라는 외부 값을 freebsd로 정해 해당 case branch를 선택할 뿐입니다.

로컬 Linux 환경에서 실제로 -XOS=freebsd로 FreeBSD용 subunit directory를 선택해 빌드했지만 결과 실행 파일은 GNU/Linux용 x86-64 ELF였습니다. 진짜 cross compilation은 13.10.1절의 target/toolchain 설정이 별도로 필요합니다.

13.8 테스트 프로젝트 구성 및 명명 규칙

13.8.1 라이브러리와 테스트 실행 파일 분리 (tests.gpr 예시)

library와 test executable을 분리하면 build product의 책임이 명확해집니다. test project는 대상 library project를 import하고 자신의 test main만 지정할 수 있습니다.

with "../lib/lib.gpr";

project Tests is
   for Source_Dirs use ("src");
   for Object_Dir use "obj";
   for Exec_Dir use "bin";
   for Main use ("test_app.adb");
end Tests;

13.8.2 빌드 및 실행 방법

gprbuild -P tests.gpr
./bin/test_app

실제 실행 파일 이름과 위치는 Main, Exec_Dir, Builder'Executable 등 프로젝트 설정에 따라 달라질 수 있습니다.

13.8.3 프로젝트 이름과 파일 이름 규칙 (하이픈 vs 밑줄 경고 처리)

GPR project name은 project language의 identifier로 쓰이므로 Test_Guiyom처럼 밑줄을 사용할 수 있지만 Test-Guiyom처럼 하이픈을 identifier 내부에 쓸 수는 없습니다.

project Test_Guiyom은 관례적으로 test_guiyom.gpr에 둡니다. 로컬 GPRBUILD Pro 18.0w에서 같은 선언을 test-guiyom.gpr에 두면 다음 종류의 경고가 발생하지만 빌드는 계속되었습니다.

warning: file name does not match project name, should be "test_guiyom.gpr"

최신 문서가 이름 대응을 전제로 하므로 경고 허용 동작에 기대기보다 project name과 file name을 맞추는 것이 좋습니다. 현재 GPR 공통 option의 -we는 project-file warning을 error로 취급합니다. 로컬 18.0w에서도 동일한 이름 불일치 project가 기본 호출에서는 warning 뒤 build됐지만 -we를 추가하면 project processing 단계에서 실패했습니다. CI에서 project metadata의 관대한 호환 동작을 놓치고 싶지 않을 때 유용합니다.

13.9 플랫폼 종속성 관리: 서브유닛 교체 (Subunit Swapping)

여기서 “서브유닛 교체”는 별도의 Ada 언어 기능 이름이 아니라, Ada separate subunit과 GPR source selection을 결합하는 프로젝트 구성 패턴을 뜻합니다.

13.9.1 서브유닛 교체 개요

공통 parent body에서 subprogram body를 separate로 두고, 각 플랫폼 디렉터리에 동일한 subunit의 서로 다른 구현을 둘 수 있습니다. GPR project는 scenario variable에 따라 그중 한 디렉터리만 Source_Dirs에 넣습니다.

이 방식은 런타임에 태그를 보고 구현을 고르는 dynamic dispatch를 추가하지 않습니다. 그러나 이를 곧바로 “함수 호출 비용이 0” 또는 “가장 효율적인 Ada 방법”이라고 단정할 수는 없습니다. 실제 call/inlining과 system-call 비용은 compiler, optimization, ABI와 코드 구조에 따라 결정됩니다.

13.9.2 구현 단계 (Clair.IO 사례)

Clair.IO 같은 실제 I/O 패키지에 적용할 수 있는 원리를, 시스템 호출 의존성을 제거한 최소 Platform 예제로 검증해 보겠습니다.

-- platform.adb
package body Platform is
   function Name return String is separate;
end Platform;

Linux용 subunit:

separate (Platform)
function Name return String is
begin
   return "linux";
end Name;

FreeBSD용 디렉터리에는 같은 subunit 이름으로 FreeBSD 구현을 둡니다. GPR project에서는 13.7.2절처럼 Source_Dirs를 하나만 선택합니다.

로컬 GPRBUILD Pro 18.0w에서 이 구조를 실제로 빌드하여 -XOS=linux에서는 Linux subunit, -XOS=freebsd에서는 FreeBSD subunit이 선택되어 각각 다른 결과를 반환하는 것을 확인했습니다. 다시 말하지만 이 source selection은 target toolchain 선택과 별개입니다.

13.10 크로스 컴파일, 빌드 프로파일과 자동화

13.10.1 크로스 컴파일

gprbuild는 --target=target-name을 통해 cross-platform target을 지정할 수 있습니다.92 그러나 target 이름만 바꾼다고 compiler, binder, runtime이 생기는 것은 아닙니다. 해당 target을 지원하는 toolchain과 GPR configuration이 실제로 설치되어 있어야 합니다. GPR 27.0w에서는 --target과 --config/--autoconf가 상호 배타적이므로, command line에서 target을 직접 선택하는 경로와 이미 생성한 configuration project를 지정하는 경로를 섞어 쓰지 않습니다.

gprbuild -P app.gpr --target=x86_64-windows

현재 Debian 환경의 GPRBUILD Pro 18.0w도 --target option을 제공하며, gprconfig --show-targets는 native x86_64-linux 외의 target knowledge를 보여 줍니다. 하지만 실제 --target=x86_64-windows 빌드는 필요한 cross gnatbind를 찾지 못해 실패했습니다. 즉 target metadata가 보인다는 것과 완전한 cross toolchain이 설치되어 있다는 것은 다릅니다.

13.10.2 빌드 프로파일 관리

gprconfig의 주 역할은 debug/release profile을 관리하는 것이 아니라 사용 가능한 compiler/toolchain을 탐색하고 configuration project(.cgpr)를 생성하는 것입니다.96 생성된 .cgpr은 application project가 아니며 project with로 import하는 대신 GPR 도구의 --config=file.cgpr 또는 필요 시 --autoconf=file.cgpr 경로에서 사용합니다. 로컬에서 native Ada compiler를 선택해 생성한 configuration에는 target x86_64-linux와 Ada compiler driver /usr/bin/gcc-14가 기록되었습니다.

반면 debug/release 같은 application build profile은 scenario variable로 표현하는 것이 자연스럽습니다.

type Build_Type is ("debug", "release");
Build : Build_Type := external ("BUILD", "debug");

case Build is
   when "debug" =>
      for Object_Dir use "obj/debug";
   when "release" =>
      for Object_Dir use "obj/release";
end case;

각 profile의 compiler switch와 Exec_Dir도 같은 case에서 구분할 수 있습니다. 서로 다른 profile이 같은 object directory를 공유하지 않게 하면 설정 전환 시 오래된 산출물이 섞이는 위험을 줄일 수 있습니다.

13.10.3 CI/CD 환경에서 gprbuild 활용

gprbuild, gprclean, gprconfig는 명령줄 도구이므로 CI runner에서도 사용할 수 있습니다. 자동화에서는 특히 다음 값을 명시적으로 고정하는 편이 재현성에 유리합니다.

  • 사용할 project file과 scenario 값
  • compiler/GPR tool version
  • native 또는 cross target과 runtime
  • build profile별 artifact directory
  • test executable과 실패 조건

CI 서비스 자체가 GPR 의미를 바꾸는 것은 아닙니다. 로컬과 CI가 같은 source와 project file을 사용하더라도 toolchain, target, runtime, scenario가 다르면 다른 빌드가 될 수 있으므로 이 값을 build log에 남기는 것이 중요합니다.

큰 project tree에서는 parallel build가 유용합니다. GPR 27.0w의 GPRbuild2 문서는 -jN으로 동시 action 수를 제한하고 -j0을 CPU core 수 기준으로 설명합니다.89 로컬 18.0w도 -j0을 받아들였지만 engine별 scheduling 세부는 동일하다고 가정하지 않습니다. 재현 문제를 조사할 때 -f로 build action을 강제 재실행할 수 있으며, 로컬 18.0w에서도 -f가 이미 최신인 Ada main을 다시 compile/bind/link하는 것을 확인했습니다. incremental build가 중요한 대규모 환경에서는 매번 무조건 clean하는 것보다 정확한 dependency와 분리된 artifact directory를 유지하고, clean이 필요할 때만 gprclean -n으로 범위를 먼저 확인하는 편이 안전합니다.

14. 다른 언어와의 인터페이스

Ada의 외부 언어 인터페이스는 Ada 언어가 정의하는 인터페이스 규칙과 실제 compiler·linker·ABI가 제공하는 구현을 함께 이해해야 합니다. Ada 2022 Annex B는 Import, Export, Convention, External_Name, Link_Name 등의 interfacing aspect와 C·Fortran 인터페이스 패키지를 정의합니다.97 실제 심볼 이름, 객체 표현, C++ ABI, 런타임 초기화 방법은 구현과 플랫폼의 영향을 받습니다.

이 장에서는 C 연동을 중심으로 검증된 최소 규칙을 설명하고, GNAT 고유 기능은 표준 Ada 기능과 분리하여 표시합니다. 외부 코드와 맞닿는 declaration이 compile된다는 사실만으로 ABI가 맞는 것은 아닙니다. 가능한 경우 외부 언어 쪽 companion code까지 함께 compile/link/run하고, pointer lifetime·callback lifetime·초기화 순서·exception 경계를 별도로 검증합니다.

14.1 System 패키지와 저수준 표현

System은 언어 정의 패키지이지만 그 안의 많은 값은 target과 Ada 구현에 의존합니다(ARM 13.7). Storage_Unit, Word_Size, Memory_Size, 우선순위 범위와 여러 수치 한계가 대표적입니다.

System.Address는 개별 storage element를 지정할 수 있는 machine address를 표현하는 definite nonlimited type입니다. System.Null_Address는 어떤 객체나 program unit의 주소와도 다른 값입니다. 표준은 Address가 private type인 것을 권고하지만 내부 표현 자체를 고정하지는 않습니다.

with System;
with System.Storage_Elements;

A : System.Address := System.Null_Address;

System.Storage_Elements는 address와 storage offset 사이의 저수준 연산을 제공합니다. 이런 연산은 device register, memory-mapped I/O, 외부 ABI처럼 표현 자체가 중요한 경계에서 사용합니다.

X'Address는 객체·program unit·label의 첫 storage element 주소를 얻는 언어 정의 attribute입니다. 다만 System.Address를 곧바로 표준 C의 void * 타입이라고 생각해서는 안 됩니다. Ada 표준이 두 타입의 보편적인 동일 표현을 보장하지 않기 때문입니다. GNAT처럼 System.Address를 C의 opaque pointer와 직접 연결할 수 있는 구현도 있지만, 그 사용은 compiler와 target ABI를 확인한 뒤 제한된 내부 경계에 두는 편이 좋습니다.

C의 구체적인 T *를 다룰 때는 가능한 한 C-compatible access type이나 Interfaces.C.Pointers를 사용하고, C 문자열은 Interfaces.C.Strings를 사용합니다.

14.2 C 언어와의 연동 (Interfaces.C)

C 경계에서는 다음 네 가지를 별도로 확인해야 합니다.

  1. Ada 타입과 C 타입의 representation이 대응하는가?
  2. parameter와 result가 C ABI가 기대하는 방식으로 전달되는가?
  3. 외부 symbol이 실제 link 입력에 포함되는가?
  4. pointer가 가리키는 객체의 lifetime과 ownership은 누가 책임지는가?

Import나 Convention => C 하나만으로 이 네 조건이 자동으로 해결되는 것은 아닙니다.

14.2.1 C 데이터 타입 연동 (Representation Aspects)

Ada 2022 Interfaces.C는 C와 호환되는 기본 타입을 정의합니다(ARM B.3). int, short, long, unsigned, unsigned_char, C_float, double, long_double, char, C_bool, size_t 등이 여기에 포함됩니다.

with Interfaces.C;

X : Interfaces.C.int;
B : Interfaces.C.C_bool;

Interfaces.C 자체에 표준 char_ptr나 void_ptr가 정의되어 있다고 가정하면 안 됩니다. NUL-terminated char *는 Interfaces.C.Strings.chars_ptr가 담당하고, 일반적인 C-style typed pointer 연산은 generic Interfaces.C.Pointers가 제공합니다.

열거형

C-compatible enumeration은 Convention => C로 선언할 수 있습니다. ARM B.3의 권고되는 C interface correspondence에서는 Ada와 C의 대응 enumeration literal이 같은 internal code를 사용하고 그 code가 C int 범위 안에 있는 형태를 제시합니다.

type Status is (OK, Failed, Pending)
  with Convention => C;

이 규칙을 단순히 Status'Size = sizeof(enum status) * CHAR_BIT이라고 바꾸어 이해하면 안 됩니다. GNAT 14.2.0 실험에서는 위 타입의 Status'Size는 2였지만 Status'Object_Size는 32였고, C의 sizeof(enum status)는 4바이트였습니다. 실제 C 함수에 값을 전달한 결과는 정상적으로 일치했습니다. Ada의 'Size와 외부 ABI의 object representation을 같은 개념으로 사용하지 않는 것이 중요합니다.

레코드와 C_Pass_By_Copy

C 함수가 struct를 값으로 받는 경우에는 C-compatible record type에 C_Pass_By_Copy convention을 사용합니다.

with Interfaces.C;

type Point is record
   X : Interfaces.C.double;
   Y : Interfaces.C.double;
end record
  with Convention => C_Pass_By_Copy;

function Point_Sum (P : Point) return Interfaces.C.double
  with Import        => True,
       Convention    => C,
       External_Name => "point_sum";

ARM B.3의 interface correspondence에서 in parameter인 C_Pass_By_Copy record는 대응 C struct 값으로 전달됩니다. 그 밖의 C-compatible record parameter는 모드에 따라 C struct * 형태로 대응합니다. 로컬 GNAT 14.2.0에서 struct point { double x; double y; }를 값으로 받는 C 함수와 위 Ada 선언을 실제 link/run하여 1.5 + 2.5 = 4.0을 확인했습니다.

배열

C-compatible component를 가진 Ada array는 C array와 연동할 수 있습니다. parameter 경계에서는 대응 element의 pointer로 전달되며 Ada bounds가 별도 ABI 정보로 자동 전달되는 것은 아닙니다.

type Int_Array is array (0 .. 2) of aliased Interfaces.C.int
  with Convention => C;

function Sum3 (A : Int_Array) return Interfaces.C.int
  with Import        => True,
       Convention    => C,
       External_Name => "sum3";

로컬 실험에서 C의 int sum3(const int a[3])과 연결하여 (1, 2, 3)의 결과 6을 확인했습니다.

C의 bool

Ada 2022에서는 Interfaces.C.C_bool이 C bool에 대응하는 언어 정의 타입입니다. GNAT에는 Interfaces.C.Extensions.bool도 있지만 Interfaces.C.Extensions는 GNAT 라이브러리 확장이므로 이식 가능한 수동 binding의 기본 타입으로 가정하지 않습니다. 로컬 GNAT 14.2.0에서는 두 타입 모두 존재했고 C bool 함수를 Interfaces.C.C_bool로 정상 호출했습니다.

14.2.2 C 서브프로그램 연동 (import, export)

Import, Export, Convention, External_Name, Link_Name은 Ada 2022 ARM B.1의 interfacing aspect입니다.

Import => True는 Ada 선언이 외부에서 정의된 entity를 나타낸다는 뜻입니다. imported subprogram declaration에는 Ada body를 작성하지 않습니다. imported object도 외부 저장소를 나타내며 declaration elaboration이 새 객체를 만들거나 default initialization을 수행하지 않습니다.97

with Interfaces.C;

function Add
  (A, B : Interfaces.C.int)
   return Interfaces.C.int
  with Import        => True,
       Convention    => C,
       External_Name => "add_integers";

Counter : Interfaces.C.int
  with Import        => True,
       Convention    => C,
       External_Name => "c_counter";

imported object declaration에는 explicit initialization expression을 둘 수 없습니다. GNAT 14.2.0도 X : Interfaces.C.int := 1 with Import ... 형태를 RM B.1(24) 위반으로 compile-fail 처리했습니다. 로컬 companion C에서 int c_counter = 7;을 정의한 뒤 Ada에서 읽고 9를 대입하자 C 함수에서도 변경된 9가 관찰되어 같은 외부 object를 가리킴을 확인했습니다.

Convention => C는 호출 규약과 C-compatible parameter/result 규칙을 지정하고 External_Name은 외부 언어가 아는 이름을 지정합니다. Link_Name은 외부 compiler의 linker naming convention을 거친 link-level 이름이며 그 의미는 implementation-defined입니다. 둘을 모두 지정하면 Ada RM은 External_Name을 무시하도록 정합니다. 로컬 GNAT 14.2.0에서도 일부러 틀린 External_Name과 올바른 Link_Name => "linked_name_target"을 함께 둔 import가 정상 link/run되어 결과 42를 반환했습니다. 보통 정확한 foreign symbol을 이미 알고 있다면 External_Name 하나가 더 읽기 쉽습니다.

그러나 이 declaration만으로 C object file이나 library가 linker 입력에 자동 추가되지는 않습니다. 예를 들어 로컬 검증에서는 다음처럼 C object를 함께 link했습니다.

gcc -c c_api.c -o c_api.o
gnatmake -gnat2022 ada_import_main.adb -largs c_api.o

타입·calling convention·실제 외부 entity의 계약이 서로 맞지 않으면 Ada 구현이 모든 오류를 검출해 주는 것이 아닙니다. ARM B.1은 interfacing aspect를 사용하면서 Ada semantics를 위반하지 않도록 하는 책임을 프로그래머에게 두며, 그렇지 않은 실행은 erroneous가 될 수 있습니다.

C callback을 넘길 때는 access-to-subprogram type과 실제 Ada subprogram의 convention을 함께 맞춥니다.

type Callback is access function
  (X : Interfaces.C.int) return Interfaces.C.int
  with Convention => C;

function Twice
  (X : Interfaces.C.int) return Interfaces.C.int
  with Convention => C;

로컬에서 library-level Twice'Access를 C의 함수 포인터 parameter로 넘겨 C가 Twice (7)을 호출하게 했을 때 결과 14를 확인했습니다. 반면 중첩 Ada subprogram을 같은 방식으로 C callback으로 넘긴 GNAT 14.2.0/x86-64 Linux 최소 예제는 linker가 executable stack 필요성을 경고했고 ELF GNU_STACK가 RWE가 되었습니다. GNAT이 nested subprogram의 enclosing-environment 정보를 전달하기 위해 trampoline 같은 구현 기법을 사용할 수 있기 때문입니다. C API가 callback을 호출 이후에도 저장한다면 enclosing Ada activation의 lifetime까지 문제가 됩니다. 장시간 실행·보안 민감 코드에서는 callback을 저장하는 foreign API에 library-level 또는 명시적으로 충분히 긴 lifetime의 callback을 우선 사용하고, target/compiler의 trampoline 정책과 executable-stack 요구를 검증합니다. 이것은 Ada 표준이 모든 구현에 동일하게 요구하는 representation이 아닙니다.

Ada 2022는 C variadic function을 위한 C_Variadic_n convention도 정의합니다.98 n은 고정 parameter 수이며 그 뒤 parameter는 C variadic argument로 전달됩니다. 로컬 GNAT 14.2.0에서 Convention => C_Variadic_1인 Ada import로 C의 va_list 함수에 1, 2, 3, 4를 넘겨 합계 10을 확인했습니다. variadic ABI의 default argument promotion과 실제 C prototype이 맞는지까지 확인해야 하므로 printf류 API를 임의의 Ada 타입으로 호출해서는 안 됩니다.

Export => True는 반대로 Ada entity를 외부 코드에서 사용할 수 있도록 합니다.

with Interfaces.C;

package Ada_Math is
   function Subtract
     (A, B : Interfaces.C.int)
      return Interfaces.C.int
     with Export        => True,
          Convention    => C,
          External_Name => "ada_subtract";
end Ada_Math;

GNAT에서 main program이 C에 있고 Ada code를 호출하는 일반적인 binder workflow에서는 Ada runtime elaboration과 finalization도 처리해야 합니다. gnatbind -n은 Ada main을 만들지 않고 외부에서 호출할 수 있는 adainit과 adafinal을 생성합니다. GNAT User’s Guide는 첫 Ada subprogram 호출 전에 adainit, 마지막 Ada 호출 뒤 program 종료 전에 adafinal을 호출하도록 요구합니다.

extern void adainit(void);
extern void adafinal(void);
extern int ada_subtract(int, int);

int main(void) {
    int result;
    adainit();
    result = ada_subtract(100, 40);
    adafinal();
    return result == 60 ? 0 : 1;
}

이번 검증에서는 다음 순서로 실제 C main -> Ada export 호출을 build/run했고 결과 60을 확인했습니다.

gnatmake -c -gnat2022 ada_math.adb
gnatbind -n ada_math.ali
gcc -c c_main.c -o c_main.o
gnatlink -o export_from_c ada_math.ali c_main.o

stand-alone Ada library 같은 다른 GNAT 배포 방식은 initialization을 자동화하거나 별도 API를 제공할 수 있으므로 그 방식의 공식 문서를 따릅니다.

14.2.3 C 포인터와 Opaque Handle

C pointer를 다룰 때 가장 먼저 구분할 것은 typed pointer, 문자열 pointer, opaque pointer입니다.

  • C의 T *처럼 지정 타입이 분명하면 C-compatible access type이나 Interfaces.C.Pointers를 우선 검토합니다.
  • NUL-terminated char *는 Interfaces.C.Strings.chars_ptr를 사용합니다.
  • 구체적인 타입을 노출하지 않는 void *형 opaque handle은 compiler가 보장하는 pointer representation을 확인해야 합니다. GNAT에서는 System.Address를 이런 경계에 사용하는 패턴이 흔하지만 Ada 표준이 모든 구현에서 System.Address = void *를 보장하는 것은 아닙니다.

opaque handle을 public API에서 그대로 System.Address로 노출할 필요는 없습니다. package private type으로 감싸면 서로 다른 handle 종류의 우발적 혼용을 줄일 수 있습니다.

with System;

package Handles is
   type Handle is limited private;
private
   type Handle is new System.Address;
end Handles;

Handle의 full view가 System.Address의 derived type이라면 두 타입 사이에는 일반적인 explicit type conversion을 사용할 수 있습니다. 이 단순한 wrapper를 위해 Ada.Unchecked_Conversion이 필수인 것은 아닙니다. 실제 C ABI 함수의 parameter/result를 System.Address로 선언할 수 있는지 여부는 구현에서 확인해야 하며, 로컬 GNAT 14.2.0/Linux에서는 void * 생성·조회·해제 함수를 이 방식으로 정상 호출했습니다.

pointer 자체보다 더 중요한 것은 ownership과 lifetime입니다. C 함수가 pointer를 호출 중에만 빌리는지, 내부에 저장하는지, 누가 해제해야 하는지를 wrapper 계약에 명시해야 합니다.

Unchecked_Access는 이 lifetime 문제를 없애 주지 않습니다. ARM 13.10에서 X'Unchecked_Access는 일반 'Access의 accessibility rule/check를 우회하지만 prefix X는 여전히 aliased view여야 합니다.

type Int_Access is access all Integer;

X : aliased Integer := 10;
P : Int_Access := X'Unchecked_Access;

반대로 다음과 같이 non-aliased 객체에 적용하는 것은 허용되지 않습니다.

X : Integer := 10;
-- P := X'Unchecked_Access;  -- illegal: X는 aliased view가 아님

로컬 GNAT 14.2.0도 두 번째 형태를 compile-fail로 거부했습니다. 첫 번째 형태라도 P나 C 쪽에 전달한 pointer가 X보다 오래 살아남으면 dangling reference가 될 수 있습니다. C API가 pointer를 저장한다면 Unchecked_Access로 compiler를 피하기보다 대상 객체의 lifetime이 명시적으로 더 길도록 소유 구조를 설계하는 것이 우선입니다.

14.2.4 C 문자열과 NULL 포인터 매핑

Ada String과 C string은 표현이 다릅니다. Interfaces.C.To_C는 Ada String을 char_array로 변환하며 기본 Append_Nul => True일 때 마지막에 Interfaces.C.nul을 붙입니다.

C_Array : Interfaces.C.char_array :=
  Interfaces.C.To_C ("hello");

char_array와 char *도 같은 타입은 아닙니다. Interfaces.C.Strings는 C의 NUL-terminated char *에 대응하는 private type chars_ptr와 allocation/lifetime 연산을 제공합니다.

with Interfaces.C.Strings;

P : Interfaces.C.Strings.chars_ptr :=
  Interfaces.C.Strings.New_String ("hello");

begin
   -- C_Call (P);
   null;
exception
   when others =>
      Interfaces.C.Strings.Free (P);
      raise;
end;
Interfaces.C.Strings.Free (P);

New_String과 New_Char_Array는 새로운 저장공간을 할당하고 Free로 해제할 수 있는 chars_ptr를 반환합니다. 실제 wrapper에서는 정상 경로뿐 아니라 Ada 예외 경로에서도 ownership에 맞게 해제되도록 해야 합니다. 반면 To_Chars_Ptr은 기존 char_array_access에 대한 pointer conversion을 수행하며 새 저장공간을 할당하지 않습니다. 이 경우 backing array가 pointer보다 먼저 사라지지 않도록 호출자가 lifetime을 책임져야 합니다.99

문자열 변환의 실패 조건도 API contract의 일부입니다. To_C ("", Append_Nul => False)는 길이 0의 char_array를 만들 수 없으므로 Constraint_Error를 전파하고, To_Ada (A, Trim_Nul => True)는 A 안에 NUL이 없으면 Terminator_Error를 전파합니다. 로컬 GNAT 14.2.0에서 두 경우를 각각 재현했습니다. 또한 Value (Null_Ptr)는 Dereference_Error를 전파하며, 길이 없는 Value/Strlen/Free에 NUL-terminated가 아닌 임의 pointer를 넘기는 것은 단순한 checked exception이 아니라 RM상 erroneous execution이 될 수 있습니다. 따라서 foreign pointer의 길이·termination·ownership을 알지 못한 상태에서 편의 함수로 탐색하지 않습니다.

또한 Free는 임의의 C library가 반환한 borrowed pointer에 호출하는 일반 free(3) wrapper가 아닙니다. ARM B.3.1은 New_String 또는 New_Char_Array에서 얻지 않은 non-null chars_ptr를 Interfaces.C.Strings.Free에 전달하는 실행을 erroneous로 규정합니다. 외부 library가 반환한 pointer는 그 library가 정한 release API를 사용합니다.

Interfaces.C.Strings.Null_Ptr와 빈 C 문자열을 가리키는 non-null pointer도 서로 다른 값입니다. 따라서 Ada의 빈 문자열 ""을 자동으로 C NULL과 같은 뜻으로 취급해서는 안 됩니다.

예를 들어 XOpenDisplay(NULL)처럼 특정 C API가 NULL에 별도 의미를 부여하고, Ada wrapper가 의도적으로 ""을 그 편의 표현으로 정의할 수는 있습니다.

function Open_Display
  (Display_Name : String := "") return Display;

그러나 이것은 해당 wrapper의 API 설계 정책입니다. C API가 NULL과 ""을 구별한다면 둘을 합치면 의미가 손실됩니다. 필요하다면 별도 overload, option type, 명시적인 Use_Default 인자 등으로 두 상태를 구분합니다.

14.2.5 C 헤더 파일 변환 도구 (GNAT)

GNAT의 GCC front end는 C/C++ header에서 Ada binding 초안을 생성할 수 있습니다. 이 기능은 GNAT 도구 기능이지 Ada 언어 표준 기능은 아닙니다.100 GNAT User’s Guide 자체도 생성기가 반복 작업 대부분을 줄이는 도구이지 100% 정확한 binding을 보장하는 도구가 아니라고 명시합니다.

지정한 C header만 변환하려면 다음과 같이 -fdump-ada-spec-slim을 사용할 수 있습니다.

gcc -c -fdump-ada-spec-slim -C sample.h

로컬 GCC 14.2.0에서는 sample.h에서 sample_h.ads가 생성됐고 생성된 spec도 Ada 2022 모드에서 compile되었습니다. -fdump-ada-spec은 지정 header와 필요한 header의 transitive binding을 생성하는 방식이고, -fdump-ada-spec-slim은 명령줄에 지정한 header 자체에 초점을 둡니다. -C는 가능한 경우 C/C++ comment를 Ada comment로 옮깁니다.

C++ header binding은 GNAT 14.2 User’s Guide가 g++ compiler를 사용하라고 명시하며 C header보다 수동 수정이 더 많이 필요할 수 있습니다. 실제 header의 C/C++ linkage와 맞는 driver를 선택해야 합니다. 이번 단순 header 실험에서 g++ -fdump-ada-spec-slim -C cpp_api.hpp는

function cpp_add (a, b : Interfaces.C.int) return Interfaces.C.int
  with Import => True,
       Convention => CPP,
       External_Name => "_Z7cpp_addii";

형태의 binding을 생성했습니다. 같은 header를 구현한 G++ 14.2 object와 이 spec을 gnatmake ... -largs cpp_api.o --LINK=g++로 link하여 cpp_add (20, 22) = 42를 실제 확인했습니다. C++ runtime·global constructor·exception support 때문에 mixed C++ executable은 일반적으로 C++ linker driver를 사용하는 것이 중요합니다.101 반대로 C++ 측에 extern "C" wrapper를 제공할 수 있다면 복잡한 C++ ABI를 Ada 경계 밖에 두고 14.2의 C interface로 연결하는 방식이 더 단순하고 이식하기 쉽습니다.

생성된 binding은 최종 정본이 아니라 검토할 초안입니다. macro, 복잡한 pointer, compiler extension, ownership, callback lifetime, C++ class/exception ABI는 자동 변환만으로 의미가 완성되지 않습니다. 로컬 GCC 14.2.0의 generator는 C bool에 GNAT 고유 Interfaces.C.Extensions.bool을 사용했는데, 수동 Ada 2022 binding에서는 언어 정의 Interfaces.C.C_bool도 사용할 수 있습니다. 이런 차이 역시 생성 결과를 검토해야 하는 이유입니다.

14.3 Fortran, C++ 연동 개요

Fortran

Ada 2022 ARM B.5는 Interfaces.Fortran과 convention identifier Fortran을 정의합니다. Interfaces.Fortran의 Fortran_Integer, Real, Double_Precision, Logical, Complex, Double_Complex, Fortran_Character 등은 지원되는 Fortran 구현의 기본 intrinsic representation과 대응하도록 정의됩니다.102

다차원 array에 Convention => Fortran을 적용하면 Fortran interface convention에 맞는 representation을 사용하며 RM의 예제는 이를 column-major order로 명시합니다. parameter 전달도 단순히 “항상 address 한 개”로 고정된 Ada 규칙이 아니라 지원되는 Fortran implementation의 parameter passing convention과 INTENT/Ada mode 대응을 따릅니다. 따라서 character length, descriptor, compiler-specific array ABI 같은 세부를 추정하지 말고 실제 Fortran compiler 문서를 확인해야 합니다.

with Interfaces.Fortran;

X : Interfaces.Fortran.Fortran_Integer;

로컬 GNAT 14.2.0에서 Interfaces.Fortran과 2차원 Convention => Fortran array를 compile/run했습니다. Fortran_Integer'Object_Size가 4바이트인 환경에서 M(1,1) 기준 주소 offset은 M(2,1)=4, M(1,2)=8바이트로 나와 첫 번째 index가 연속하는 column-major 배치를 확인했습니다. 다만 현재 환경에는 gfortran이 없어 실제 Ada↔Fortran object link/run은 검증 보류입니다. ARM B.5가 직접 다루지 않는 Fortran 기능은 Fortran의 C interoperability 기능을 통해 C ABI로 좁힌 뒤 Ada B.3 interface를 사용하는 방법도 고려할 수 있습니다.

C++

Ada 2022 B.3은 C와 대응되는 C++ 부분에 Interfaces.C 기반 규칙을 적용합니다. 그러나 C++ class layout, constructor/destructor, exception, name mangling을 포함한 완전한 C++ ABI를 Ada 표준이 하나의 고정된 방식으로 정의하는 것은 아닙니다.

GNAT은 여기에 구현 고유의 C++ 지원을 추가하여 Convention => CPP, C++ constructor/class 관련 pragma와 binding generator를 제공합니다.103 단순 scalar/subprogram 연동에서도 C++ name mangling과 linker driver가 추가 경계가 됩니다. 로컬 GNAT/G++ 14.2.0에서는 위 generator가 만든 mangled External_Name binding을 실제 C++ object와 link/run했습니다.

반대로 Interfaces.CPP라는 library unit을 모든 GNAT이나 모든 Ada 구현에서 사용할 수 있다고 가정하면 안 됩니다. 과거 GNAT 배포에는 그런 implementation-defined package가 있었지만 현재 로컬 GNAT 14.2.0에서는 predefined unit으로 제공되지 않았습니다. 현재 GNAT Reference Manual도 full C++ interface를 CPP convention과 GNAT-specific pragma 중심으로 설명합니다.

C++ symbol을 손으로 mangled name에 고정하면 compiler ABI와 버전에 강하게 결합됩니다. 가능한 경우 실제 C++ header와 사용 compiler를 기준으로 binding generator를 사용하고, 생성 결과의 calling convention·layout·ownership·exception 경계를 검토합니다. GNAT은 C++ exception과 class-level interoperability를 위한 구현 고유 기능도 제공하지만, 이는 Ada 표준이 모든 C++ ABI에 보장하는 경로가 아닙니다. 장기간 유지할 FFI에서는 foreign exception을 언어 경계 밖으로 그대로 통과시키기보다 C/C++ wrapper에서 오류 상태로 변환하는 정책이 toolchain 교체와 장애 격리에 더 유리할 수 있습니다.

15. 동시성 프로그래밍

Ada의 task, entry/rendezvous, select, protected object는 라이브러리 관례가 아니라 언어가 정의하는 동시성·동기화 기능입니다(ARM 9). 다만 실제 스레드 구현, processor 배치, 스케줄링 정책과 운영체제 primitive 매핑은 구현과 실행 환경의 영향을 받습니다. 따라서 Ada task가 존재한다고 해서 여러 CPU에서 동시에 실행된다고 가정해서는 안 됩니다. 언어 의미는 단일 processor의 interleaved execution에서도 성립합니다.

이 장은 기본 task lifecycle, task entry rendezvous, select, protected object의 상호배제와 조건부 대기를 다룹니다. 우선순위·dispatching·시간 제어와 Ravenscar/Jorvik profile은 16장에서 다룹니다.

15.1 태스킹 모델

Ada의 task는 독립적인 sequence of actions를 실행하는 언어 정의 entity입니다. 하나의 task object를 직접 선언할 수도 있고, task type으로 여러 task object를 만들 수도 있습니다. task declaration은 외부에서 보이는 entry 등의 interface를 정의하고 task body는 실제 실행을 정의합니다.

with Ada.Text_IO; use Ada.Text_IO;

procedure Demonstrate_Tasking is
begin
   declare
      task T;

      task body T is
      begin
         delay 0.05;
         Put_Line ("task done");
      end T;
   begin
      Put_Line ("block body");
   end;

   Put_Line ("after block");
end Demonstrate_Tasking;

여기서 task object 선언 직후 task body 전체가 곧바로 실행되는 것으로 이해하면 안 됩니다. ARM 9.2에서 activation은 task body의 declarative part를 elaboration하는 초기 단계입니다. 한 declarative region에서 만들어진 task들의 activation은 그 region의 statement sequence를 시작하기 직전에 시작되고, activator는 그 activation들이 성공 또는 실패로 끝날 때까지 기다립니다. activation이 끝난 뒤에는 task body의 statement sequence와 activator가 동시적으로 진행할 수 있습니다.

activation 중 예외가 밖으로 전파되면 해당 task의 activation은 실패하고 task는 completed 상태가 됩니다. 함께 activation된 task들의 activation이 끝난 뒤 하나라도 activation에 실패했다면 activator에는 Tasking_Error가 발생합니다. 이는 task body의 실행 중 처리되지 않은 예외와 다른 규칙입니다.

task의 lifetime은 master와도 연결됩니다. master가 finalization될 때 그 master에 dependent한 task들의 termination을 먼저 기다립니다(ARM 9.3, 7.6.1). 그래서 위 블록은 T가 terminate하기 전에 완전히 빠져나가지 않습니다. 반대로 activator가 activation 완료 후 task body 자체의 종료까지 매 문장마다 기다리는 것은 아닙니다.

15.1.1 예외와 task 종료

처리되지 않은 예외가 task body까지 도달하면 그 task body는 비정상적으로 완료하지만, 그 exception occurrence가 다른 task의 동적 호출 문맥으로 계속 전파되지는 않습니다(ARM 11.4). 다른 task에서 종료 원인을 관찰해야 한다면 Ada.Task_Termination 같은 언어 정의 기능이나 명시적인 통신 경로를 사용합니다.

또한 task body의 처리되지 않은 예외가 그 task에 dependent한 task들을 자동으로 abort한다고 일반화하면 안 됩니다. master finalization은 dependent task들의 termination을 기다립니다. abort는 별도 언어 기능이며, master 자체가 abort될 때 dependent task들이 abort되는 규칙은 ARM 9.8에서 따로 정의됩니다.

15.1.2 Ada.Task_Termination으로 종료 원인 관찰하기

Ada.Task_Termination은 task가 정상 종료, abort에 의한 abnormal 종료, 처리되지 않은 예외 중 어떤 이유로 terminate했는지 관찰할 수 있는 언어 정의 package입니다(ARM C.7.3). termination handler의 Cause는 Normal, Abnormal, Unhandled_Exception 가운데 하나이며, 처리되지 않은 예외가 원인이면 Exception_Occurrence도 함께 전달됩니다.

with Ada.Task_Termination;

Ada.Task_Termination.Set_Specific_Handler
  (T'Identity, Watch.H'Access);

handler는 protected procedure여야 합니다. handler의 lifetime/accessibility도 맞아야 하므로, 장기적으로 사용할 handler를 nested local protected object에 두고 그대로 'Access하는 방식은 legality에 걸릴 수 있습니다. library-level protected object나 충분히 긴 lifetime을 가진 객체로 설계하는 편이 안전합니다.

GNAT 14.2.0 최소 실험에서는 task body에서 처리되지 않은 Constraint_Error를 발생시켰을 때 termination handler가 UNHANDLED_EXCEPTION을 관찰했습니다. 이는 task body의 예외가 caller에게 직접 propagate되는 것과는 다른 관찰 경로입니다.

15.2 동기화 및 통신: 랑데부

Task entry는 task declaration에 선언되는 potentially queued operation입니다. task body의 accept statement는 자기 task의 해당 entry call을 받아 처리합니다. caller와 accepting task 사이의 이 상호작용을 rendezvous라고 합니다(ARM 9.5.2).

task Server is
   entry Get (Value : out Integer);
end Server;

task body Server is
begin
   accept Get (Value : out Integer) do
      Value := 42;
   end Get;
end Server;

caller가 Server.Get (X);를 실행했는데 서버가 아직 해당 call을 받을 수 없다면 call은 entry queue에서 기다릴 수 있습니다. 서버가 대응하는 accept를 실행하면 caller가 선택되고 rendezvous가 진행됩니다. caller는 rendezvous가 끝날 때까지 기다리며, accept의 handled sequence가 끝난 뒤 두 task는 다시 독립적으로 진행합니다.

accept 내부에서 exception이 전파되면 그 같은 exception이 대응하는 entry call 실행에서도 발생합니다. 반면 이미 completed된 task의 entry를 호출하거나, call을 받아들이기 전에 target task가 completed/abnormal 상태가 되면 Tasking_Error가 발생할 수 있습니다(ARM 9.5.3).

15.2.1 select의 네 형태

Ada의 select_statement에는 서로 목적이 다른 네 형태가 있습니다(ARM 9.7).

  1. selective accept: 서버 task가 여러 accept alternative나 delay/terminate alternative 중 가능한 것을 선택합니다.
  2. timed entry call: caller가 entry call을 일정 시간만 기다리고 timeout alternative를 실행할 수 있습니다.
  3. conditional entry call: entry call을 즉시 선택할 수 없으면 else alternative를 실행합니다.
  4. asynchronous select: triggering event와 abortable part를 결합하여 asynchronous transfer of control을 표현합니다.

따라서 “task가 select 하나로 여러 entry를 기다린다”는 설명은 selective accept에만 해당합니다. timed/conditional entry call은 caller 측 entry call 형태입니다.

select
   Server.Request;
else
   -- 지금 즉시 call을 선택할 수 없으면 실행
   null;
end select;

select
   Server.Request;
or
   delay 0.1;
   -- 제한 시간 안에 rendezvous를 시작하지 못했을 때의 처리
   null;
end select;

실제 프로그램에서는 timeout 숫자 자체보다 “무한 대기가 가능한 call인가, 취소 또는 제한 시간이 필요한가”를 API 설계 단계에서 먼저 결정하는 편이 중요합니다.

15.2.2 terminate alternative와 master 종료

selective accept의 terminate alternative는 단순한 break 문이 아닙니다. ARM 9.3은 task가 completed master에 depend하고, 그 master의 다른 dependent task들도 이미 terminate했거나 같은 조건의 open terminate alternative에서 기다리는 경우에만 terminate alternative가 선택될 수 있도록 정의합니다.

loop
   select
      accept Ping;
   or
      terminate;
   end select;
end loop;

이 패턴은 “새 요청이 더 이상 올 수 없는 master 종료 상황”에서 server task가 자연스럽게 끝나도록 할 때 유용합니다. GNAT 14.2.0에서 Ping을 한 번 처리한 뒤 enclosing master가 끝나자 worker가 terminate alternative를 통해 정상 종료하는 것을 확인했습니다.

15.2.3 asynchronous select와 abort

Asynchronous select는 triggering statement가 완료되면 then abort 뒤의 abortable part를 중단하는 asynchronous transfer of control입니다(ARM 9.7.4).

select
   delay 0.02;
   Put_Line ("triggered");
then abort
   delay 0.20;
   Put_Line ("abortable-finished");
end select;

GNAT 14.2.0에서 위 형태는 triggered만 출력했고 abortable part의 마지막 문장은 실행되지 않았습니다.

abort T;는 더 강한 수단입니다. ARM 9.8은 대상 task를 abnormal로 만들고 task body의 실행을 abort한다고 정의합니다. 다만 abort는 “그 즉시 모든 문장이 끝난다”는 단순 모델이 아닙니다. protected action, controlled object의 Finalize, dependent task termination 대기 등은 abort-deferred operation이고, abort completion point 규칙 때문에 실제 완료 시점에는 제약이 있습니다. controlled local object를 가진 task를 abort한 GNAT 14.2.0 실험에서도 Finalize가 실행되었습니다.

따라서 abort는 일반적인 요청 취소 API의 첫 선택보다는 unconditional termination이 필요한 경계에 한정하는 편이 좋습니다. 가능하면 명시적 stop entry, condition flag + protected object, timed call 같은 협력적 종료 경로를 먼저 설계해야 finalization과 invariants를 더 예측 가능하게 유지할 수 있습니다.

15.3 공유 데이터를 위한 보호 객체

Protected object는 여러 task가 공유하는 상태와 그 상태에 대한 동기화 연산을 하나의 언어 구조로 묶습니다. 보호 연산은 procedure, function, entry 세 종류입니다.

  • protected procedure: protected object의 current instance를 변경할 수 있는 exclusive read-write operation입니다.
  • protected function: current instance가 constant view이므로 protected state를 변경할 수 없습니다. 기본적으로 다른 nonexclusive protected function들과 concurrent read-only access가 허용됩니다.
  • protected entry: barrier가 True일 때 실행할 수 있는 potentially queued operation이며 entry body는 protected state를 변경할 수 있습니다.

Ada 2022의 Exclusive_Functions => True aspect를 protected type/object에 지정하면 protected function도 procedure처럼 exclusive operation으로 취급됩니다. 기본값은 False입니다(ARM 9.5.1).

protected type Shared_Counter is
   entry Wait_Positive (Observed : out Integer);
   procedure Increment;
   function Get return Integer;
private
   Value : Integer := 0;
end Shared_Counter;

protected body Shared_Counter is
   entry Wait_Positive (Observed : out Integer)
     when Value > 0
   is
   begin
      Observed := Value;
   end Wait_Positive;

   procedure Increment is
   begin
      Value := Value + 1;
   end Increment;

   function Get return Integer is
   begin
      return Value;
   end Get;
end Shared_Counter;

Barrier에는 protected object의 component, entry index, Entry'Count 등 허용된 visible entity를 사용할 수 있지만 그 entry의 formal parameter는 사용할 수 없습니다(ARM 9.5.2). 따라서 다음 형태는 illegal입니다.

entry Wait_For (Target : Integer)
  when Value >= Target;  -- illegal: Target은 barrier에서 보이지 않음

호출별 parameter를 보고 대기 조건을 결정해야 한다면 API 구조를 바꾸거나, ARM이 설명하는 것처럼 항상 열린 entry에서 parameter를 검사한 뒤 적절한 private entry로 requeue하는 등의 설계가 필요합니다.

15.3.1 Protected action과 상호배제

외부 protected operation call은 대상 protected object에서 protected action을 시작합니다. 같은 object에서 다른 protected action이 이미 진행 중이면 새 action은 일반적으로 시작되지 않으며, 예외는 두 action 모두 nonexclusive protected function call인 경우입니다. 언어 모델에서는 protected action 시작을 object의 execution resource 획득, 완료를 resource 해제로 설명합니다(ARM 9.5.1).

이 규칙은 protected state에 대한 동기화 경계를 제공하지만 프로그램 전체의 data race나 deadlock을 자동으로 제거한다는 뜻은 아닙니다. 공유 상태를 protected object 밖에서도 우회해 접근하거나 여러 동기화 객체를 잘못 조합하면 문제가 생길 수 있습니다. 또한 protected action 중 potentially blocking operation을 수행하는 것은 bounded error이며, 검출되면 Program_Error가 발생할 수 있고 검출되지 않으면 deadlock 같은 결과가 가능하다고 ARM은 명시합니다.

Protected operation에서 exception이 밖으로 전파되어도 해당 protected action은 완료되면서 execution resource가 해제됩니다. 따라서 raw mutex처럼 모든 exception path에 수동 unlock을 반복해서 작성하는 모델과는 다릅니다. 다만 protected operation 내부에 blocking I/O나 다른 부적절한 synchronization을 넣어도 안전하다는 뜻은 아닙니다.

GNAT 14.2.0은 protected procedure 안의 delay에 potentially blocking operation in protected operation 경고를 냈지만 code generation 자체는 진행했습니다. 이는 ARM이 이 상황을 단순 compile-time illegality가 아니라 bounded error로 정의한다는 점과 일치합니다. 구현이 검출하면 Program_Error가 발생할 수 있고, 검출하지 않으면 deadlock 같은 결과도 가능합니다. 장시간 실행되는 서버에서는 이런 경고를 무시하지 말고 protected action을 짧고 bounded하게 유지해야 합니다.

15.3.2 외부 라이브러리와 함께 사용할 때

Ada protected object는 그 object가 보호하는 Ada-side state와 호출 경로를 동기화합니다. 바인딩된 C/C++ 라이브러리 자체가 thread-safe인지, 한 handle을 여러 thread에서 동시에 사용할 수 있는지, 별도 초기화나 외부 lock이 필요한지는 해당 라이브러리의 계약을 따로 확인해야 합니다.

예를 들어 Xlib의 공식 문서는 여러 thread가 Xlib을 동시에 사용할 수 있도록 초기화하려면 XInitThreads가 첫 Xlib 호출이어야 한다고 규정합니다. 그러나 모든 Xlib 호출을 toolkit이나 client가 별도의 mutual-exclusion mechanism으로 직렬화한다면 XInitThreads가 필요하지 않다고도 명시합니다. 따라서 “Ada task를 두 개 이상 쓰면 항상 XInitThreads를 호출해야 한다”는 규칙은 아닙니다.

외부 C mutex를 직접 binding하는 경우에는 획득·해제와 exception path를 Ada wrapper가 책임져야 합니다. 이런 자원은 controlled type/finalization 등으로 감싸는 방법도 고려할 수 있습니다. 반면 protected object는 protected action의 획득·해제 의미를 언어가 정의합니다. 어느 쪽이 더 빠르거나 어떤 OS primitive로 구현되는지는 target runtime을 측정·확인해야 하며, Ada 표준이 특정 futex·mutex 구현이나 “zero overhead”를 보장하지는 않습니다.

이 장의 범위는 task lifecycle, rendezvous/select, protected object의 기본 동기화 의미까지입니다. priority, scheduling, delay/time semantics와 Ravenscar/Jorvik profile은 다음 16장에서 이어서 검증합니다.

16. 실시간 프로그래밍과 태스킹 프로파일

이 장은 15장의 태스킹과 보호 객체를 바탕으로 Ada의 실시간 시스템 지원을 다룹니다. 핵심은 세 층을 구분하는 것입니다. Ravenscar/Jorvik은 태스킹 기능을 제한하는 usage profile, Task_Dispatching_Policy와 Locking_Policy는 스케줄링·락킹 정책, Ada.Real_Time과 delay는 시간 표현과 실행 지연을 정의합니다.

프로파일을 적용했다고 해서 WCET, deadline 충족, 결정론이나 안전 인증이 자동으로 보장되는 것은 아닙니다. 프로파일은 분석하기 어려운 동적 기능을 줄여 분석과 검증이 가능한 시스템을 구성하기 쉽게 만드는 언어적 기반입니다. 실제 시간 보장에는 대상 하드웨어, 런타임, interrupt, WCET와 schedulability 분석이 함께 필요합니다.

16.1 Ravenscar 프로파일

Ravenscar는 Ada 2005부터 표준에 포함된 language-defined usage profile이며 Ada 2022 ARM D.13에서 Jorvik과 함께 정의됩니다. pragma Profile (Ravenscar);는 단순한 스타일 권고가 아니라 여러 configuration pragma와 restriction의 묶음입니다.

16.1.1 도입 배경 및 목적

Ravenscar는 hard real-time, safety-critical, high-integrity, embedded 시스템에서 사용할 수 있도록 Ada tasking model을 의도적으로 제한합니다. 목표는 전체 Ada tasking의 표현력을 그대로 유지하는 것이 아니라, 작은 런타임과 정적 분석·응답 시간 분석에 적합한 예측 가능한 동시성 모델을 제공하는 것입니다.

여기서 분석 가능성을 높인다는 표현이 중요합니다. Ravenscar 자체가 다음을 증명하지는 않습니다.

  • 어떤 task의 WCET가 얼마인지
  • 모든 deadline이 만족되는지
  • data race나 논리 오류가 전혀 없는지
  • 특정 안전 표준의 인증 요건을 자동으로 충족하는지

이런 성질은 프로그램과 target에 대한 별도 분석·검증의 대상입니다.

16.1.2 Ravenscar 프로파일의 세 가지 핵심 원칙

Ravenscar의 제한은 다음 세 관점으로 이해하면 쉽습니다.

  1. 동적 tasking 구조를 줄입니다. No_Task_Hierarchy, No_Task_Allocators, No_Task_Termination 등으로 task 구조와 수명을 단순화합니다.
  2. 동기화의 형태를 제한합니다. task entry를 없애고 protected object 중심의 동기화를 사용하며, protected entry 수도 제한합니다.
  3. 스케줄링·락킹 정책을 고정합니다. FIFO_Within_Priorities, Ceiling_Locking, Detect_Blocking을 프로파일 일부로 적용합니다.

이 원칙을 “프로그램 실행이 언제나 완전히 결정론적이다”라고 확대해서 해석하면 안 됩니다. interrupt 도착, 외부 입력, 실행 시간 변동과 multiprocessor 배치는 여전히 프로그램의 시간 동작에 영향을 줄 수 있습니다.

16.1.3 태스크 실행 및 동시성 제약

No_Task_Hierarchy는 task가 library-level master 이외의 master에 의존하지 못하게 합니다. 따라서 일반적인 서브프로그램 local task 패턴은 Ravenscar에 맞지 않습니다. No_Task_Allocators는 task object의 allocator를 금지하고, No_Task_Termination은 task가 정상적으로 종료되는 모델도 제외합니다.

주기적 task는 Ravenscar의 대표적인 사용 패턴이지만 모든 Ravenscar task가 반드시 주기적이어야 한다는 언어 규칙은 아닙니다. 주기적 실행에는 Ada.Real_Time과 절대 지연을 흔히 사용합니다. Ravenscar에는 No_Relative_Delay가 있으므로 delay 0.01; 같은 상대 지연은 허용되지 않습니다.

pragma Profile (Ravenscar);

with Ada.Real_Time; use Ada.Real_Time;

package Periodic_Worker is
   task Worker;
end Periodic_Worker;
pragma Profile (Ravenscar);

with Ada.Real_Time; use Ada.Real_Time;

package body Periodic_Worker is
   procedure Perform_Periodic_Job is
   begin
      null;
   end Perform_Periodic_Job;

   task body Worker is
      Period       : constant Time_Span := Milliseconds (10);
      Next_Release : Time := Clock + Period;
   begin
      loop
         delay until Next_Release;
         Perform_Periodic_Job;
         Next_Release := Next_Release + Period;
      end loop;
   end Worker;
end Periodic_Worker;

이 구조는 다음 release 시각을 이전 release 시각에서 누적하므로 delay Period;를 반복하는 것보다 주기 drift를 줄이기 쉽습니다. 그러나 delay until은 “그 시각에 반드시 CPU를 얻는다”는 보장이 아닙니다. expiration time에 task가 ready가 되고, 실제 실행 시작은 dispatching 상황에 따라 더 늦을 수 있습니다.

기능 제한과 trade-off

Ravenscar는 No_Select_Statements, No_Requeue_Statements, Max_Task_Entries => 0, Max_Protected_Entries => 1, Max_Entry_Queue_Length => 1, Simple_Barriers 등으로 task 간 상호작용을 크게 단순화합니다. task rendezvous는 사용할 수 없고 protected entry는 object당 최대 하나이며 그 queue에도 동시에 둘 이상의 caller가 쌓일 수 없습니다.

중요하게도 Ravenscar를 “모든 동적 메모리 할당 금지”라고 설명하는 것은 정확하지 않습니다. 표준 프로파일에는 No_Implicit_Heap_Allocations, No_Task_Allocators, No_Protected_Type_Allocators 등이 포함되지만 일반 access type의 명시적 allocator를 전부 금지하는 No_Standard_Allocators_After_Elaboration 같은 restriction이 자동으로 추가되는 것은 아닙니다. 실제 고신뢰성 프로젝트나 작은 Ravenscar runtime은 별도 restriction 또는 runtime 제약으로 heap 사용을 더 제한할 수 있습니다.

로컬 GNAT 14.2.0에서도 pragma Profile (Ravenscar) 아래의 단순한 new Integer는 컴파일되었고, 상대 delay와 local task는 각각 Ravenscar restriction 위반으로 거부되었습니다. 따라서 profile 자체와 특정 certifiable/light runtime의 추가 제한을 구분해야 합니다.

16.1.4 핵심 기술: 자원 관리 및 동기화

Ravenscar에서는 Max_Task_Entries => 0 때문에 task rendezvous를 사용할 수 없으며 shared state와 조건 대기는 protected object를 중심으로 설계합니다. 보호 객체의 일반 의미는 15.3절을 따르지만 프로파일은 추가 제한을 둡니다.

Simple_Barriers 때문에 Ravenscar protected entry barrier는 복잡한 Boolean expression이 아니라 정적 Boolean expression 또는 protected object의 단순 Boolean component 참조 형태로 작성해야 합니다. 따라서 다음처럼 별도 상태를 두는 패턴이 적합합니다.

pragma Profile (Ravenscar);

package Shared_State is
   protected Counter is
      entry Wait_Ready;
      procedure Increment;
      function Value return Natural;
   private
      Count : Natural := 0;
      Ready : Boolean := False;
   end Counter;
end Shared_State;
package body Shared_State is
   protected body Counter is
      entry Wait_Ready when Ready is
      begin
         null;
      end Wait_Ready;

      procedure Increment is
      begin
         Count := Count + 1;
         Ready := True;
      end Increment;

      function Value return Natural is
      begin
         return Count;
      end Value;
   end Counter;
end Shared_State;

반대로 entry Wait (Target : Natural) when Count >= Target처럼 entry parameter를 barrier에서 사용하는 코드는 15장에서 설명했듯 일반 Ada에서도 불법이고, when Count > 0 같은 복합 barrier는 일반 Ada에서는 가능하지만 Ravenscar의 Simple_Barriers에는 맞지 않습니다.

Ceiling locking

Ravenscar는 pragma Locking_Policy (Ceiling_Locking)을 포함합니다. ARM D.3의 규칙은 다음과 같습니다.

  • protected object에는 ceiling priority가 있습니다.
  • task가 protected operation을 호출할 때 caller의 active priority가 ceiling보다 높으면 Program_Error가 발생합니다.
  • protected action을 수행하는 동안 task는 그 protected object의 ceiling priority를 상속합니다.

이것은 “낮은 우선순위 task가 waiter의 우선순위를 필요할 때마다 상속한다”는 일반적인 priority inheritance protocol과 같은 설명이 아닙니다. Ravenscar의 language-defined locking policy는 priority ceiling locking입니다. protected object의 ceiling은 그 object를 호출할 수 있는 task의 최대 active priority를 고려해 정해야 합니다.

Annex D의 실제 timing/locking 지원 범위와 priority가 운영체제 스케줄러에 매핑되는 방식은 target runtime 문서를 확인해야 합니다.

16.1.5 프로파일의 적용 및 기술 규격

Ravenscar는 configuration pragma로 적용합니다.

pragma Profile (Ravenscar);

프로젝트에서는 configuration pragma file을 사용해 partition 전체에 일관되게 적용하는 방법이 흔합니다. 프로파일은 대략 다음 정책과 restriction 집합을 의미합니다. 아래 표는 대표 항목이며 전체 목록의 정본은 Ada 2022 ARM D.13입니다.

항목 의미
Task_Dispatching_Policy (FIFO_Within_Priorities) 우선순위 기반 선점 dispatching 정책 사용
Locking_Policy (Ceiling_Locking) protected object에 priority ceiling locking 적용
Detect_Blocking protected action의 potentially blocking operation을 검출하도록 요구
No_Task_Hierarchy library-level master 이외에 의존하는 task 금지
No_Task_Allocators task allocator 금지
No_Task_Termination terminating task 모델 제외
Max_Task_Entries => 0 task entry/rendezvous 금지
Max_Protected_Entries => 1 protected unit당 entry 수 제한
Max_Entry_Queue_Length => 1 protected entry의 queued caller 수 제한
No_Select_Statements select statement 금지
No_Requeue_Statements requeue 금지
No_Relative_Delay 상대 delay 금지; 절대 delay until 사용 가능
Simple_Barriers protected entry barrier의 표현 형태 제한
No_Dynamic_Priorities 동적 priority 변경 금지
No_Dynamic_CPU_Assignment 실행 중 CPU assignment 변경 금지
No_Implicit_Heap_Allocations 언어/구현이 암시적으로 요구하는 heap allocation 제한

Ravenscar는 No_Dependence를 통해 Ada.Calendar, asynchronous task control, 일부 execution-time/attribute package 등의 사용도 제한합니다. 실시간 시계에는 Ada.Real_Time을 사용하는 것이 자연스럽습니다.

restriction 중 상당수는 compile-time에 검출할 수 있습니다. 로컬 GNAT 14.2.0에서는 상대 delay, local task, task entry, 두 개의 protected entry, 복잡한 barrier가 각각 해당 restriction 위반으로 거부되었습니다. 그러나 모든 profile 성질을 “컴파일러가 모든 시간 문제를 정적으로 증명한다”는 의미로 확대하면 안 됩니다.

16.1.6 Ravenscar 프로파일의 제한 사항 및 고려사항

Ravenscar를 사용할 때는 다음 경계를 염두에 둡니다.

  • task hierarchy, rendezvous, select, relative delay와 복잡한 protected barrier가 필요한 응용에는 너무 제한적일 수 있습니다.
  • profile legality를 만족하는 것과 target에서 적절한 Ravenscar runtime을 선택하는 것은 별개의 문제입니다.
  • 작은 runtime은 표준 프로파일보다 sequential Ada 기능을 더 제한할 수 있습니다.
  • multiprocessor에서도 Ravenscar 자체가 금지되는 것은 아닙니다. Ada 2022 ARM은 CPU aspect로 task를 processor에 정적으로 배치할 수 있음을 전제로 하며, dynamic CPU assignment는 제한합니다. 여러 CPU를 사용하면 single-CPU schedulability 가정을 그대로 적용할 수 없습니다.
  • 실시간 성능은 compiler 최적화, runtime, kernel/interrupt latency와 hardware에 의존하므로 실제 target 측정과 분석이 필요합니다.

Ravenscar와 execution-time/timing-event 기능

Ravenscar는 실시간 관련 package를 한꺼번에 금지하지 않습니다. 예를 들어 Ada.Execution_Time의 Clock은 특정 task가 실제로 소비한 CPU execution time을 측정하는 언어 정의 기능이며 Ravenscar에서도 사용할 수 있습니다. 반면 profile에는 No_Dependence => Ada.Execution_Time.Timers와 No_Dependence => Ada.Execution_Time.Group_Budgets가 포함됩니다. GNAT 14.2.0에서도 Ravenscar program의 Ada.Execution_Time.Clock 사용은 compile/run되어 TRUE를 출력했지만 Ada.Execution_Time.Timers 의존은 거부되었습니다.

Ada.Real_Time.Timing_Events는 지정한 real-time 시각에 protected handler를 실행하는 기능입니다. Ravenscar는 이 package 자체를 금지하는 대신 No_Local_Timing_Events를 적용하여 Timing_Event object를 library level에 두도록 제한합니다. local Timing_Event 선언은 GNAT 14.2.0에서 해당 restriction 위반으로 compile-fail했습니다. 이 차이는 작은 runtime이나 analyzable architecture를 설계할 때 중요합니다.

16.1.7 실용적 적용 및 표준과의 연관성

Ravenscar는 hard real-time, safety-critical, high-integrity 시스템에서 널리 쓰일 수 있도록 표준화된 Ada tasking subset입니다. 특정 산업 인증 표준이 Ravenscar 사용 자체를 요구하거나, pragma Profile (Ravenscar)만으로 인증을 획득하게 해 주는 것은 아닙니다. 대신 동적 tasking 기능을 제한하고 런타임을 단순화함으로써 별도의 timing analysis, verification, certification evidence를 구성하기 쉬운 기반을 제공합니다.

GNAT Pro의 일부 bare-metal·certifiable target은 Ravenscar/Jorvik을 지원하는 Light-Tasking runtime 같은 별도 runtime을 제공합니다. 이런 runtime은 profile 이외의 sequential-language restriction도 추가할 수 있으므로, 실제 제품에서는 Ada D.13 profile, 선택한 runtime의 지원 subset, 프로젝트가 추가한 restrictions를 별개의 층으로 기록해야 합니다.

16.2 Jorvik 프로파일

Jorvik은 Ada 2022에서 표준화된 language-defined profile입니다. GNAT의 과거 GNAT_Extended_Ravenscar가 표준화된 형태이며, GNAT은 호환성을 위해 그 이전 이름을 Jorvik의 alias로 유지합니다.

16.2.1 도입 배경 및 목적

Jorvik의 목적은 Ravenscar를 “멀티코어용 프로파일”로 바꾸는 것이 아닙니다. Ravenscar 자체도 정적인 CPU assignment를 통해 multiprocessor에서 사용할 수 있습니다. Jorvik은 hard real-time/embedded 응용에서 분석 가능성을 유지하면서 Ravenscar의 일부 제한을 완화해 표현력을 늘리는 프로파일입니다.

따라서 두 프로파일의 선택 기준은 단순히 CPU core 수가 아니라 필요한 동시성 기능과 요구되는 분석·인증 수준입니다.

16.2.2 핵심 특징 및 차이점

Jorvik은 Ravenscar의 큰 틀을 유지하면서 대표적으로 다음 제한을 완화합니다.

  • Max_Protected_Entries => 1을 제거하여 protected object에 여러 entry를 둘 수 있습니다.
  • Max_Entry_Queue_Length => 1을 제거하여 entry에 여러 caller가 queue될 수 있습니다. 필요하면 entry별 Max_Queue_Length로 상한을 다시 둘 수 있습니다.
  • Simple_Barriers 대신 더 표현력이 큰 Pure_Barriers를 사용합니다. side effect·exception·recursion을 일으키지 않는 제한된 Boolean expression을 barrier에 사용할 수 있습니다.
  • No_Relative_Delay를 제거하여 delay 0.01; 같은 상대 delay도 허용합니다.
  • No_Implicit_Heap_Allocations 대신 task/protected object의 implicit allocation에 초점을 둔 제한을 사용합니다.
  • Ada.Calendar와 Ada.Synchronous_Barriers에 대한 Ravenscar의 일부 No_Dependence 제한을 완화합니다.

반면 task entry는 Jorvik에서도 허용되지 않습니다. Max_Task_Entries => 0, No_Task_Hierarchy 등 Ravenscar의 핵심 tasking 제한 상당수는 그대로 남습니다.

로컬 GNAT 14.2.0에서 pragma Profile (Jorvik)은 지원됐으며 상대 delay와 두 개의 protected entry/복합 pure barrier 예제는 컴파일됐고, task entry는 restriction 위반으로 거부되었습니다.


16.3 태스크 스케줄링과 우선순위

프로파일과 별개로 Ada Real-Time Systems Annex는 task priority와 dispatching policy를 정의합니다. “우선순위가 높으면 언제나 즉시 CPU를 선점한다”는 설명은 어떤 dispatching policy가 적용되는지 함께 말해야 정확합니다.

16.3.1 태스크 우선순위 정의

Priority pragma/aspect는 task의 base priority를 지정할 수 있습니다. 값은 target의 System.Any_Priority 범위 안에 있어야 합니다.

task type Sensor_Task is
   pragma Priority (20);
end Sensor_Task;

priority 숫자만으로 실행 순서를 전부 결정할 수는 없습니다. task의 active priority는 protected action의 ceiling inheritance 같은 규칙으로 base priority와 달라질 수 있고, 실제 dispatching은 선택된 task dispatching policy에 따릅니다.

16.3.2 스케줄링 정책

Ada Annex D는 여러 language-defined dispatching policy를 제공합니다. 대표적인 두 가지는 다음과 같습니다.

  • FIFO_Within_Priorities: priority별 ready queue를 사용하며, 더 높은 priority의 ready task가 생기면 현재 낮은-priority task를 선점하는 preemptive policy입니다.
  • Round_Robin_Within_Priorities: 같은 priority의 task들이 time slice를 기준으로 실행 기회를 나누는 policy입니다.

partition 전체 정책은 configuration pragma로 지정할 수 있습니다.

pragma Task_Dispatching_Policy (FIFO_Within_Priorities);

Ravenscar와 Jorvik은 FIFO_Within_Priorities를 프로파일의 일부로 사용합니다. 일반 Ada 프로그램에서 아무 policy도 지정하지 않았을 때의 동작이나 특정 Annex D 기능 지원 범위는 구현 문서를 확인해야 합니다.

Ada가 정의하는 policy는 이 둘뿐이 아닙니다. Non_Preemptive_FIFO_Within_Priorities, Round_Robin_Within_Priorities, EDF_Across_Priorities도 Annex D에 정의되어 있으며, Priority_Specific_Dispatching으로 priority 범위별 policy를 나눌 수도 있습니다. GNAT 14.2.0에서는 partition-wide Round_Robin_Within_Priorities configuration pragma가 정상 compile/run되었습니다. 따라서 “Ada 실시간 스케줄링 = FIFO 하나”로 축약하면 안 됩니다.

16.3.3 우선순위 역전과 해결책

우선순위 역전은 높은 우선순위 task가 낮은 우선순위 task가 사용하는 공유 자원 때문에 진행하지 못하는 상황을 말합니다. Ada의 language-defined Ceiling_Locking은 protected object에 ceiling priority를 부여하고 protected action 동안 caller의 active priority를 그 ceiling으로 올립니다.

pragma Locking_Policy (Ceiling_Locking);

protected object의 ceiling은 그 object를 호출할 수 있는 task의 최고 active priority보다 낮지 않게 설계해야 합니다. caller가 더 높은 active priority로 operation을 호출하면 ARM D.3에 따라 Program_Error가 발생합니다.

이 정책은 blocking을 없애는 것이 아니라 blocking을 분석하기 쉬운 형태로 제한하는 도구입니다. 시스템의 response time을 판정하려면 각 critical section의 최대 실행 시간과 task priority, interference를 함께 분석해야 합니다.

16.3.4 멀티프로세서 배치와 dispatching domain

Ada 2022 Annex D는 multiprocessor target을 위한 CPU aspect와 System.Multiprocessors.Dispatching_Domains를 정의합니다(ARM D.16~D.16.1). task의 CPU aspect는 task를 특정 processor에 정적으로 배치할 수 있게 합니다.

task Worker with CPU => 1;

반면 실행 중 processor assignment를 바꾸는 기능은 System.Multiprocessors.Dispatching_Domains.Set_CPU와 Assign_Task 같은 별도 연산입니다. 로컬 GNAT 14.2.0에서는 CPU => 1과 Set_CPU 모두 compile/run되었습니다. 따라서 “Ada의 CPU 배치는 모두 정적이다”라고 일반화하면 안 됩니다. 다만 Ravenscar/Jorvik은 No_Dynamic_CPU_Assignment를 포함하므로 프로파일 안에서는 동적 CPU assignment가 금지됩니다.

Dispatching domain은 processor 집합과 그 위에서 dispatch되는 task 집합을 묶는 언어 정의 abstraction입니다. 이것이 곧 OS thread affinity API와 동일한 ABI라는 뜻은 아닙니다. 실제 processor numbering, affinity 적용, migration 비용과 scheduler mapping은 target runtime의 Annex D 지원을 확인해야 합니다.

16.3.4 멀티프로세서와 CPU aspect

Annex D.16의 System.Multiprocessors는 available processor 수와 task의 정적 processor assignment를 위한 CPU aspect를 정의합니다. Number_Of_CPUs는 partition에서 사용할 수 있는 processor 수를 반환하고, task의 CPU value가 특정 processor를 가리키면 그 task는 해당 processor에 assign됩니다. 현재 검증 환경에서는 Number_Of_CPUs가 4를 반환했습니다.

Ravenscar의 multiprocessor model은 dynamic migration을 자유롭게 허용하는 모델이 아닙니다. profile은 processor assignment가 정적으로 정의되도록 요구하고 No_Dynamic_CPU_Assignment 계열 제한과 함께 분석 가능한 배치를 지향합니다. CPU => Not_A_Specific_CPU인 task의 실제 processor 선택은 implementation-defined일 수 있으므로 core affinity가 timing proof의 일부라면 명시적 정적 배치와 target runtime 문서를 함께 검증해야 합니다.


16.4 시간 관리 및 지연 제어

Ada에는 wall-clock 계열의 Ada.Calendar.Time, monotonic real-time 계열의 Ada.Real_Time.Time, 초 단위 상대 간격을 나타내는 Duration이 있습니다. 주기적·실시간 scheduling에는 시스템 시각 조정의 영향을 받지 않는 Ada.Real_Time이 일반적으로 적합합니다.

16.4.1 시간 유형

  • Duration: Standard에 선언된 fixed point type이며 초 단위 시간 간격을 나타냅니다. 상대 delay expression의 expected type입니다.
  • Ada.Real_Time.Time: 고해상도 monotonic clock의 time point를 표현하는 private type입니다. epoch는 구현이 정하며 표준 wall-clock과 동기화되어 있다고 가정할 수 없습니다.
  • Ada.Real_Time.Time_Span: Time 값 사이의 real-time 간격을 표현하는 private type입니다. Milliseconds, Seconds, To_Time_Span, To_Duration 등의 연산으로 다룹니다.
with Ada.Real_Time; use Ada.Real_Time;

Period : constant Time_Span := Milliseconds (10);
Next   : Time := Clock + Period;

Time_Span을 단순히 Duration과 같은 fixed point type으로 취급하거나 임의 실수 literal과 직접 혼용하지 않는 편이 좋습니다.

16.4.2 지연 제어 (delay 문)

Ada의 delay statement에는 상대·절대 두 형태가 있습니다.

delay 0.05;

상대 delay expression은 Duration이며 현재 시간에 그 간격을 더한 expiration time까지 task를 block합니다.

delay until Next;

delay until은 expression의 time base에 속한 절대 time point까지 block합니다. Ada.Real_Time.Time이나 Ada.Calendar.Time 같은 time type을 사용할 수 있습니다.

expiration time이 이미 지났다면 task는 block되지 않습니다. expiration에 도달하면 task는 ready가 되며, 실제 statement 다음 문장이 정확히 그 순간 실행된다는 뜻은 아닙니다. scheduling latency와 다른 ready task의 우선순위가 실제 실행 시각에 영향을 줄 수 있습니다.

Ravenscar에서는 No_Relative_Delay 때문에 첫 번째 형태가 금지되고 절대 delay를 사용합니다. Jorvik은 이 제한을 제거하므로 두 형태를 모두 사용할 수 있습니다.

16.4.3 동시성 타임아웃 처리

일반 Ada tasking에서는 timed entry call로 entry call에 시간 제한을 둘 수 있습니다.

select
   Task_Name.Entry_Name;
or
   delay 5.0;
   Put_Line ("태스크 응답 시간 초과.");
end select;

entry call이 선택되어 완료되면 delay alternative는 수행되지 않고, 지정 시간까지 call을 선택할 수 없으면 delay alternative가 수행됩니다. 이 형태는 15.2절의 timed entry call입니다.

단, Ravenscar에서는 No_Select_Statements와 No_Relative_Delay 때문에 이 코드를 사용할 수 없습니다. 제한 프로파일에서는 protected entry, absolute delay, suspension object 등 프로파일이 허용하는 동기화 수단으로 설계를 다시 구성해야 합니다. Jorvik도 task entry 자체는 허용하지 않으므로 일반 task rendezvous 예제를 그대로 옮길 수 없습니다.

16.4.4 CPU 실행 시간

Ada.Execution_Time은 wall-clock 경과 시간이 아니라 task가 소비한 CPU execution time을 측정하기 위한 language-defined package입니다(ARM D.14). Clock은 기본적으로 current task의 CPU_Time을 반환하며 두 CPU_Time의 차이는 Ada.Real_Time.Time_Span으로 얻을 수 있습니다.

with Ada.Execution_Time; use Ada.Execution_Time;
with Ada.Real_Time;      use Ada.Real_Time;

Start : constant CPU_Time := Clock;
-- 측정할 계산
Stop  : constant CPU_Time := Clock;
Used  : constant Time_Span := Stop - Start;

이 값은 Ada.Real_Time.Clock으로 잰 elapsed time과 목적이 다릅니다. task가 preempt되어 기다린 시간, 다른 task가 CPU를 사용한 시간 등을 곧바로 같은 값으로 포함한다고 가정해서는 안 됩니다. 로컬 GNAT 14.2.0에서는 Ada.Execution_Time.Clock과 CPU_Time 차이 계산이 compile/run되었습니다. timer와 group budget 같은 더 높은 수준의 execution-time facility도 Annex D.14.1~D.14.2에 별도로 정의되어 있습니다.

16.4.5 Timing event

Ada.Real_Time.Timing_Events는 별도 polling task나 delay loop를 만들지 않고 지정한 real-time 시각 또는 Time_Span 뒤에 protected procedure handler를 실행하도록 예약하는 기능입니다(ARM D.15).

Ada.Real_Time.Timing_Events.Set_Handler
  (Event   => E,
   In_Time => Milliseconds (10),
   Handler => Watch.Handler'Access);

handler는 access protected procedure (Event : in out Timing_Event) profile을 가져야 하며 lifetime/accessibility도 만족해야 합니다. short-lived local protected object의 handler를 더 긴 lifetime의 timing event에 그대로 저장하려 하면 accessibility legality에 걸릴 수 있으므로, library-level 또는 충분히 긴 lifetime을 가진 handler 객체를 사용하는 편이 안전합니다.

로컬 GNAT 14.2.0에서 library-level protected handler를 In_Time => Milliseconds (10)으로 등록한 최소 예제는 compile/link되었고 200ms 안에 handler가 실행되어 TRUE를 출력했습니다. timing event가 언어 정의 기능이라고 해서 interrupt latency나 handler dispatch latency까지 일정하다고 보장되는 것은 아닙니다. 실제 target의 clock source, runtime, interrupt/scheduler latency를 별도로 측정해야 합니다.

17. SPARK와 형식 검증

SPARK는 Ada를 기반으로 하는 프로그래밍 언어와 검증 도구 집합입니다. 전체 Ada를 그대로 분석하는 것이 아니라 자동 정적 분석과 형식 검증에 적합한 큰 부분집합을 사용하고, Depends, Abstract_State처럼 검증을 위한 계약을 추가합니다. 따라서 SPARK 코드는 Ada 코드이지만, Ada로 컴파일된다는 사실만으로 SPARK 규칙을 만족하거나 증명이 끝난 것은 아닙니다.

이 장에서는 세 단계를 구분합니다.

  1. SPARK 코드 식별과 legality 검사: 분석할 코드를 SPARK_Mode로 표시하고 SPARK 제한을 만족하는지 확인합니다.
  2. flow analysis: 초기화와 데이터 흐름, Global/Depends 같은 계약의 일관성을 확인합니다.
  3. proof: 런타임 오류 부재와 Pre/Post 등 assertion·functional contract를 논리적으로 증명합니다.

현재 검증 기준은 SPARK Reference Manual/User’s Guide 27.0w입니다. 이 문서는 개발 버전이므로 세부 지원 기능은 사용하는 GNATprove 버전의 문서와 함께 확인해야 합니다.

17.1 정적 분석과 SPARK

12장의 계약은 실행 중 검사할 수도 있는 Ada 계약입니다. SPARK는 이 계약과 SPARK 고유의 데이터 흐름 계약을 정적 분석과 proof의 입력으로 사용합니다. 중요한 점은 분석 범위, 계약의 강도, 외부 코드에 대한 가정이 증명 결과의 의미를 결정한다는 것입니다.

17.1.1 SPARK의 목표와 SPARK_Mode

SPARK는 프로그램의 모든 부분에 반드시 적용할 필요가 없습니다. 고신뢰성이 필요한 핵심은 SPARK로 분석하고, GUI·외부 라이브러리·기존 Ada 코드처럼 분석 대상에서 제외할 부분은 일반 Ada로 둘 수 있습니다.

GNATprove에서 분석할 범위는 SPARK_Mode로 표시할 수 있습니다.

package Counter
  with SPARK_Mode => On
is
   procedure Increment (X : in out Integer);
end Counter;

SPARK_Mode는 Ada RM의 language-defined aspect가 아니라 SPARK/GNATprove가 분석 범위를 표시하는 도구 측 marker입니다. SPARK_Mode => On은 해당 section이 SPARK 규칙을 만족해야 하며 GNATprove 분석 대상이라는 뜻입니다. Off는 그 section을 분석하지 않고 SPARK 제한도 요구하지 않습니다. Auto는 도구가 사용하는 implicit/default 상태로, 직접 분석하지 않으면서 다른 SPARK 코드에서 사용할 수 있는지 판정하는 데 쓰입니다. package specification은 SPARK로 유지하고 body 일부를 Off로 두는 식으로 full Ada와 SPARK의 경계를 만들 수도 있습니다.

여기서 valid SPARK code와 proved code를 구분해야 합니다. check/check_all이 SPARK 제한 위반을 찾지 않았다는 것은 분석 가능한 subset에 속한다는 뜻이지, overflow가 없거나 postcondition이 참이라는 뜻은 아닙니다. 그런 성질은 flow 또는 proof 단계에서 별도로 확인합니다.

SPARK와 non-SPARK 경계에서는 가정도 관리해야 합니다. 예를 들어 SPARK 코드가 일반 Ada나 C subprogram을 호출한다면, proof는 그 외부 subprogram이 선언된 contract를 지킨다는 가정에 의존할 수 있습니다. 반대로 일반 Ada 코드가 proved SPARK subprogram을 호출할 때는 precondition, 초기화, aliasing 등의 호출 조건을 지켜야 합니다. 전체 시스템 보장은 이런 경계 가정을 함께 검증할 때 성립합니다.

17.1.2 SPARK의 제한 모델

SPARK는 “pointer와 exception을 모두 금지한 Ada”가 아닙니다. 현재 SPARK는 상당히 큰 Ada 부분집합을 지원하며, 분석 모델을 깨뜨리는 사용 방법을 제한합니다.

대표적인 원칙은 다음과 같습니다.

  • 표현식의 부작용 제한: regular function을 포함한 expression은 숨은 상태 변경이 없도록 제한됩니다. 필요한 상태 변경은 procedure나 명시적인 side-effect 모델로 드러내는 편이 분석에 적합합니다.
  • aliasing과 interference 제한: 서로 다른 이름이 같은 mutable object를 가리켜 분석 모델을 깨뜨리지 않도록 parameter/global aliasing에 추가 규칙이 적용됩니다.
  • access type의 ownership: access type 자체는 지원됩니다. 다만 Rust의 ownership과 유사한 memory ownership policy를 통해 pointer 복사·이동·aliasing을 제한합니다.
  • 동시성 제한: task와 protected object도 지원되지만 현재 SPARK 분석에서는 Ravenscar 또는 Jorvik profile에 맞는 tasking을 사용합니다.
  • 일부 Ada 기능 제외: controlled type처럼 현재 분석 모델에서 지원하지 않는 기능도 있습니다.

예외 역시 “SPARK에서는 전부 금지”라고 일반화하면 안 됩니다. 현재 상세 SPARK 문서는 explicit raise와 Exceptional_Cases 계약을 지원하고 expected exception을 모델링합니다. 반면 예상하지 않은 exception이나 일반 런타임 check failure는 proof에서 도달하지 않음을 증명해야 하는 대상으로 취급됩니다. 따라서 exception 지원과 absence of run-time errors(AoRTE) 목표는 서로 모순되지 않습니다.

SPARK subset은 도구 버전에 따라 넓어져 왔습니다. 오래된 SPARK 2005나 초기 SPARK 2014 설명을 현재 버전의 제한 목록으로 그대로 사용하지 않는 것이 중요합니다.

17.1.3 계약, flow analysis와 proof

SPARK에서는 계약의 종류에 따라 검사 방법이 다릅니다. 다음 예제는 global state, flow dependency와 functional contract를 함께 표현합니다.

package Totals
  with SPARK_Mode => On
is
   Total : Integer := 0;

   procedure Add (X : Positive)
     with Global  => (In_Out => Total),
          Depends => (Total =>+ X),
          Pre     => Total <= Integer'Last - X,
          Post    => Total = Total'Old + X;
end Totals;

Global은 이 subprogram이 어떤 global state를 읽고 수정하는지 기술합니다. 7장에서 설명했듯 Ada 2022에도 language-defined Global aspect가 있으며, SPARK는 이를 flow analysis와 state abstraction/refinement에 사용합니다.

Depends는 SPARK의 flow dependency contract입니다. 위의 Total =>+ X는 호출 뒤의 Total이 호출 전 Total과 입력 X에 의존한다는 뜻입니다. Depends는 실행 시 평가하는 Boolean postcondition이 아니라 정적 정보 흐름 관계입니다.

GNATprove의 주요 분석 단계를 구분하면 다음과 같습니다.

  • check / check_all: SPARK legality와 제한 위반을 확인합니다. check_all은 regular function의 side effect처럼 더 완전한 SPARK 검사를 포함합니다.
  • flow: 읽기 전에 초기화되지 않은 data가 없는지 확인하고, Global, Depends, Abstract_State, Initializes 및 refinement 계약이 implementation과 맞는지 검사합니다.
  • prove: overflow, division by zero, range/index check 같은 런타임 오류가 발생하지 않는지와 Pre, Post, assertion 등 functional contract가 성립하는지 deductive proof를 수행합니다.
  • all: flow analysis와 proof를 함께 수행하는 기본 모드입니다. 현재 27.0w 문서에서는 stone은 check_all, bronze는 flow, silver와 gold는 all의 synonym으로 정의됩니다.

--level은 단순한 “증명 강도 등급”이라기보다 prover 집합, timeout, memory limit, counterexample 설정의 preset입니다. 예를 들어 27.0w의 level 2~4는 cvc5, z3, altergo를 함께 사용하면서 timeout/memory budget을 늘립니다. 같은 level이라도 machine과 prover 상태에 따라 결과가 달라질 수 있으므로 CI나 장기 회귀 검증에서는 --steps, proof session/replay 같은 재현 가능한 기준을 함께 기록하는 편이 좋습니다.

proof 결과도 계약과 가정의 범위 안에서 해석해야 합니다. 약한 postcondition을 완전히 증명했다고 해서 요구사항 전체가 자동으로 증명되는 것은 아니며, non-SPARK 코드나 imported code의 contract가 맞다는 가정이 남을 수 있습니다. 또한 현재 GNATprove의 AoRTE 분석은 Storage_Error를 포함하지 않으므로 stack/heap exhaustion 같은 자원 고갈은 별도로 평가해야 합니다.

자동 prover가 어떤 check를 증명하지 못했다는 사실 역시 곧바로 실제 bug를 뜻하지는 않습니다. 코드나 contract가 틀렸을 수도 있지만, 필요한 invariant가 없거나 모델·prover·timeout 한계 때문일 수도 있습니다. counterexample이 생성되더라도 먼저 모델과 path가 실제 실행 가능한지 검토해야 합니다. 반대로 proof가 성공해도 분석하지 않은 subprogram, SPARK_Mode => Off 코드, imported code, pragma Assume 같은 가정이 남아 있으면 시스템 전체 보장은 그 가정의 정당성에 의존합니다. gnatprove --assumptions 같은 보고 기능으로 남은 가정을 검토하는 이유입니다. 따라서 “proved”, “unproved”, “assumed/justified”를 구분하여 기록해야 합니다.

17.1.4 proof 결과를 운영 증거로 사용할 때

대규모 프로젝트에서는 단순히 “GNATprove 성공”만 저장하지 말고 분석 대상 unit, mode, prover/level 또는 steps, timeout, tool version, 남은 unproved check와 assumptions를 함께 기록해야 합니다. proof level preset은 machine-dependent한 timeout 결과를 만들 수 있으므로 장기 회귀 기준에는 재현 가능한 proof session이나 steps 기반 설정이 더 적합할 수 있습니다. 또한 GNATprove가 대상으로 하는 AoRTE에는 Storage_Error가 포함되지 않으므로 stack/heap exhaustion, 외부 runtime failure, hardware fault 같은 자원·플랫폼 실패는 별도의 검증 항목으로 남겨야 합니다.

현재 27.0w에서 stone, bronze, silver, gold는 서로 다른 prover 강도를 뜻하는 네 단계 스위치가 아닙니다. stone = check_all, bronze = flow, silver = all, gold = all인 mode alias입니다. proof 탐색 자원은 --level, --prover, --timeout, --steps 등으로 별도로 정합니다. 특히 --level은 timeout을 포함한 preset이므로 다른 machine에서 같은 결과를 보장하지 않으며, 공식 문서도 장기 회귀에는 --steps 또는 --replay 같은 더 재현 가능한 기준을 권합니다.

GNATprove는 기본 설정만으로 dead code나 모든 contract/assumption의 논리적 일관성을 완전하게 검사하지 않습니다. 모순된 Assume나 contract가 있으면 이후 proof가 부당하게 쉬워질 수 있으므로 --proof-warnings=on과 --assumptions 보고를 함께 검토해야 합니다. 다만 proof warning이 없다는 사실 자체도 모든 contract가 일관적이라는 완전한 증명은 아닙니다.

counterexample도 “실제 실행 trace가 확정되었다”고 해석하지 않습니다. 현재 GNATprove는 기본적으로 counterexample을 내부 검증하여 명백히 부적절한 후보를 걸러내지만, missing contract/invariant나 abstraction 때문에 실제 실행 가능성과 어긋날 수 있습니다. 특히 --check-counterexamples=off를 사용했다면 이 가능성을 더 명시적으로 고려해야 합니다.

이 작업 환경에는 현재 gnatprove가 설치되어 있지 않습니다. GNAT 14.2.0의 일반 컴파일에서는 SPARK_Mode, Global, Depends가 포함된 예제뿐 아니라 SPARK에서는 허용되지 않는 side-effecting regular function까지 컴파일되었습니다. 따라서 일반 GNAT 컴파일 성공을 SPARK legality나 proof의 증거로 사용하지 않습니다. 이 장의 flow/proof 동작은 SPARK 27.0w 공식 문서로 검증했으며, 실제 GNATprove 실행 결과는 도구가 제공되는 환경에서 별도 확인해야 합니다.

부록: Clair 코딩 스타일 가이드

Clair 코딩 스타일 가이드는 주요 프로그래밍 언어의 보편적인 스타일을 Ada의 특성에 맞게 적용한 기준입니다. 변수와 서브프로그램에는 snake_case를, 타입 및 패키지 등에는 각 단어의 첫 글자를 대문자로 하고 밑줄로 구분하는 Mixed_Case를 사용합니다. 이를 통해 코드의 일관성과 명확성을 유지합니다.


들여쓰기: 탭 대신 2칸 공백을 사용합니다.

  • 근거: Ada의 구문 특성상 깊게 중첩된 코드 블록이 발생할 수 있습니다. 2칸 들여쓰기는 가로 공간을 최소화하며 계층 구조를 명확히 나타냅니다.

가로 길이 제한 및 줄 바꿈: 코드 한 줄의 길이는 최대 80컬럼으로 제한합니다.

  • 근거: 다중 분할 창, 터미널, 코드 리뷰 도구 등에서 가로 스크롤 없이 코드를 확인할 수 있도록 하기 위함입니다.
  • 변수 선언부 줄 바꿈: 변수의 타입 이름이나 초기화 수식이 길어져 80컬럼을 초과할 경우, 대입 연산자(:=) 앞에서 줄바꿈을 합니다. 이때 타입 선언 기호(:)와 대입 연산자(:=)가 수직으로 일직선상에 위치하도록 정렬합니다.
    • 예시:
      res : constant Guiyom.Display.Window_Resource_Access
          := self.resolve_handle (handle);
      

공백:

  • 서브프로그램 호출 및 선언: 서브프로그램 이름과 여는 괄호 ( 사이에 한 칸 공백을 사용합니다.
    • 근거: 서브프로그램 이름과 파라미터 목록을 시각적으로 구분합니다. 또한 도구에서 ` subprogram_name `(이름 뒤에 공백)을 검색하여, 동일한 접두사를 공유하는 타 식별자와의 오탐지를 방지하고 호출 위치를 정확히 검색할 수 있도록 합니다.
    • 예시 (호출): Clair.Error.get_error_message (errno_code);
    • 예시 (선언): procedure exit_process (status : Integer := EXIT_SUCCESS);
  • 속성 호출: 속성 이름('image, 'access 등)과 여는 괄호 ( 사이에 한 칸 공백을 사용합니다.
    • 근거: 속성 호출은 서브프로그램 호출과 문법적 형태가 유사하므로, 동일한 공백 규칙을 적용하여 일관성을 유지합니다.
    • 예시: Interfaces.C.int'image (fd);
  • 타입 변환: 타입 이름과 여는 괄호 ( 사이에 공백을 사용하지 않습니다.
    • 근거: 공백 없이 작성된 Float(x)를 하나의 식별 단위로 묶어 코드 해석의 모호성을 방지합니다.
    • 예시: z := Float(x) + y;
  • 배열 인덱싱: 배열 이름과 여는 괄호 ( 사이에 공백을 사용하지 않습니다.
    • 근거: 공백이 필요한 서브프로그램 호출과 시각적으로 구별하기 위함입니다.
    • 예시: all_bids(n), my_matrix(row, col)
  • 범위 연산자 (..): 범위 연산자 양쪽에 한 칸 공백을 사용합니다.
    • 근거: 연산자를 범위의 경계와 시각적으로 분리하여, 부동소수점 또는 고정소수점 리터럴과 함께 사용될 때 혼동을 방지합니다.
    • 예시 (타입 선언): range 0.0 .. 100.0
    • 예시 (루프): for i in 1 .. 10 loop

주석 (Comments): 주석 시작 기호(--)와 주석 내용 사이에는 기본 한 칸의 공백을 사용합니다.

  • 근거: 타 프로그래밍 언어와의 일관성을 유지하고 여백을 최소화하여 가독성을 높이기 위함입니다.
  • 예외: 주석의 수직 정렬이나 특정 서식을 맞추기 위한 다중 공백 사용은 허용됩니다.
  • 예시 (권장):
    -- 시스템 초기화
    -- 변수 정렬을 위한 공백:
    --   indent_logic
    
  • 예시 (비권장): --시스템초기화 (공백 없음), -- 시스템초기화 (단순 다중 공백)

명명 규칙 (Naming Conventions): 식별자의 종류에 따라 일관된 대소문자 및 밑줄 표기법을 적용합니다.

  • snake_case (전체 소문자 및 밑줄):
    • 적용 대상: 예약어, 애스펙트, 프라그마, 변수, 서브프로그램, 엔트리, 속성, 런타임 상수, Interfaces.C 하위의 타입 및 서브프로그램.
    • 근거: C, C++, Python 등 주요 언어의 표준 관례와 일치시켜 가독성을 확보합니다. 런타임 상수는 ‘읽기 전용 변수’로 취급합니다.
    • 반환 값 변수: 상태 코드를 담는 변수는 result와의 충돌을 피하기 위해 retval을 사용하며, 특정 데이터인 경우 의미를 명확히 하는 이름(예: bytes_written)을 사용합니다.
    • 예시: pragma import (c, my_func), my_variable, get_item, errmsg'length
  • Mixed_Case (첫 글자 대문자 및 밑줄):
    • 적용 대상: 타입, 서브타입, 예외, 보호 객체, 패키지, 루프 이름, goto 레이블.
    • 근거: snake_case가 적용된 실행 요소 및 변수와 정적인 구조(타입, 패키지)를 시각적으로 구분합니다.
    • 중복 방지: 패키지 내 선언된 타입 접두사에 패키지 이름을 반복하지 않습니다. (예: File.File_Descriptor 대신 File.Descriptor 사용)
    • 예외: 약어로 인한 식별 혼동을 방지하기 위해 ‘Dl’과 같은 특정 문자는 대문자(DL)로 표기합니다. (예: Clair.DL)
    • 예시: Library_Load_Error, Main_Process_Loop, Clair.Process
  • UPPER_CASE_WITH_UNDERSCORES (전체 대문자 및 밑줄):
    • 적용 대상: 컴파일 타임 상수, 표준 라이브러리 상수.
    • 근거: 정적 상수를 동적 변수 및 기타 식별자와 명확하게 구별하기 위함입니다.
    • 예시: EXIT_SUCCESS, System.NULL_ADDRESS

API 설계 (API Design):

  • 추상화 수준에 따른 타입 명명: 라이브러리의 추상화 수준과 역할에 따라 주 타입(Primary Type)의 명칭을 구분합니다.
    • 고수준(high-level): 객체 지향적이고 능동적인 개체에는 Object를 사용합니다. 이는 상속과 다형성을 활용하는 GUI 위젯 등에 적합합니다.
      • 예시: Guiyom.Window.Object, Guiyom.Button.Object
    • 저수준(low-level): 리소스 관리나 실행 환경에는 Context 또는 Handle을 사용합니다. 이는 내부 구현이 숨겨진 불투명한 타입이나 시스템 자원을 관리하는 객체에 적합합니다.
      • 예시: Clair.Event_Loop.Context, Clair.DL.Handle
  • 점 표기법(dot notation) 지원: Ada 2005부터 tagged 타입의 primitive operation에는 obj.method 형태의 prefixed view를 사용할 수 있습니다. API 설계 시 이를 고려하여 프리미티브 연산의 첫 번째 인자를 해당 객체(주로 self)로 지정하여, 사용자가 객체 지향적인 점 표기법을 자연스럽게 사용할 수 있도록 합니다.

제어 흐름 및 중첩 방지 (Control Flow & Nesting):

  • 보호 구문(Guard Clause) 활용: if 문의 깊은 중첩을 방지하기 위해, 예외 조건이나 실패 조건을 먼저 검사하여 조기 종료(return 또는 goto Next_Iter;)하는 방식을 사용합니다.
    • 근거: Ada에는 continue 예약어가 없으므로, 조건 불일치 시 조기 탈출을 유도하여 코드의 들여쓰기 중첩을 방지합니다.
    • 예시:
      if not is_valid then
        goto Next_Item;
      end if;
      -- 평평해진 메인 로직 실행
      

  1. Whitaker, William A. “Ada - The Project, The DoD High Order Language Working Group.” ACM SIGPLAN Notices, vol. 28, no. 3, 1993, http://archive.adaic.com/pol-hist/history/holwg-93/holwg-93.htm ↩

  2. Ada Resource Association, “Ada Standards,” https://www.adaic.org/ada-resources/standards/; “Ada 2022 Documents,” https://www.adaic.org/ada-resources/standards/ada22/ ↩

  3. Ada 2022 Reference Manual, Introduction, Design Goals, https://www.adaic.org/resources/add_content/standards/22rm/html/RM-0-2.html ↩

  4. Booch, Grady, et al. Object-Oriented Analysis and Design with Applications. 3rd ed., Addison-Wesley, 2007. ↩

  5. Johnson, Ralph (August 26, 1991). “Designing Reusable Classes” (PDF). www.cse.msu.edu. ↩

  6. Stroustrup, Bjarne (February 19, 2007). “Bjarne Stroustrup’s C++ Glossary”. “polymorphism – providing a single interface to entities of different types.” ↩

  7. GNAT User’s Guide for Native Platforms, GNAT, The GNU Ada Development Environment 및 Building Executable Programs with GNAT, https://gcc.gnu.org/onlinedocs/gcc-16.2.0/gnat_ugn/, https://docs.adacore.com/gnat_ugn-docs/html/gnat_ugn/gnat_ugn/building_executable_programs_with_gnat.html ↩ ↩2

  8. AdaCore, A New Era For Ada/SPARK Open Source Community 및 Community. GNAT Community 2021이 마지막 릴리스이며 현재 community tooling의 시작점으로 Alire를 안내합니다. https://www.adacore.com/blog/a-new-era-for-ada-spark-open-source-community, https://www.adacore.com/community ↩

  9. Alire, Getting Started, https://alire.ada.dev/docs/getting-started ↩

  10. FreeBSD Ports Collection, lang/gnat12~lang/gnat15, devel/gprbuild, devel/alire, 2026-09-01 확인. https://cgit.freebsd.org/ports/tree/lang, https://cgit.freebsd.org/ports/tree/devel/gprbuild/, https://cgit.freebsd.org/ports/tree/devel/alire/ ↩

  11. GNAT Studio User’s Guide 2026.x, Welcome to GNAT Studio, The Menu Bar, Project Handling / Alire Support. https://docs.adacore.com/gps-docs/users_guide/_build/html/intro.html, https://docs.adacore.com/live/wave/gps/html/gps_ug/menus.html, https://docs.adacore.com/live/wave/gps/html/gps_ug/projects.html ↩ ↩2

  12. AdaCore, Ada & SPARK for Visual Studio Code 및 Using Ada in VS Code. https://marketplace.visualstudio.com/items?itemName=AdaCore.ada, https://docs.adacore.com/live/wave/als/html/als-doc/Getting-Started.html ↩

  13. Ada 2022 Reference Manual 2.1~2.9, 특히 2.2 Lexical Elements, Separators, and Delimiters, https://www.adaic.org/resources/add_content/standards/22rm/html/RM-2-2.html ↩

  14. GNAT User’s Guide의 source character set / wide-character encoding 옵션. GNAT의 source encoding은 Ada 언어의 character category 규칙과 별도의 구현 입력 문제입니다. ↩

  15. Ada 2022 Reference Manual 2.3 Identifiers, https://www.adaic.org/resources/add_content/standards/22rm/html/RM-2-3.html ↩

  16. Ada 2022 Reference Manual 2.9 Reserved Words, https://www.adaic.org/resources/add_content/standards/22rm/html/RM-2-9.html ↩

  17. Ada 2022 Reference Manual 2.4~2.4.2 Numeric Literals, https://www.adaic.org/resources/add_content/standards/22rm/html/RM-2-4.html ↩

  18. Ada 2022 Reference Manual 2.5 Character Literals, https://www.adaic.org/resources/add_content/standards/22rm/html/RM-2-5.html ↩

  19. Ada 2022 Reference Manual 2.6 String Literals, https://www.adaic.org/resources/add_content/standards/22rm/html/RM-2-6.html ↩

  20. Ada 2022 Reference Manual 2.2 Lexical Elements, Separators, and Delimiters, https://www.adaic.org/resources/add_content/standards/22rm/html/RM-2-2.html ↩

  21. Ada 2022 Reference Manual 2.7 Comments, https://www.adaic.org/resources/add_content/standards/22rm/html/RM-2-7.html ↩

  22. Ada 2022 Reference Manual 2.8 Pragmas, https://www.adaic.org/resources/add_content/standards/22rm/html/RM-2-8.html ↩

  23. GNAT Reference Manual, Implementation Defined Pragmas, https://gcc.gnu.org/onlinedocs/gnat_rm/Implementation-Defined-Pragmas.html ↩

  24. GNAT Reference Manual, Pragma Ada_2022 및 Pragma Ada_2012, https://gcc.gnu.org/onlinedocs/gnat_rm/Pragma-Ada_005f2022.html ↩

  25. GNAT Reference Manual, Pragma Warnings, https://gcc.gnu.org/onlinedocs/gnat_rm/Pragma-Warnings.html ↩

  26. Ada 2022 Reference Manual 3.2 Types and Subtypes, 3.2.1 Type Declarations, 3.2.2 Subtype Declarations, 3.3 Objects and Named Numbers, 3.3.1 Object Declarations. https://www.adaic.org/resources/add_content/standards/22rm/html/RM-3-2.html, https://www.adaic.org/resources/add_content/standards/22rm/html/RM-3-3.html ↩

  27. Ada 2022 Reference Manual 3.3 Objects and Named Numbers. https://www.adaic.org/resources/add_content/standards/22rm/html/RM-3-3.html ↩

  28. Ada 2022 Reference Manual 3.2 Types and Subtypes, 3.2.2 Subtype Declarations. https://www.adaic.org/resources/add_content/standards/22rm/html/RM-3-2.html, https://www.adaic.org/resources/add_content/standards/22rm/html/RM-3-2-2.html ↩

  29. Ada 2022 Reference Manual 3.2.4 Subtype Predicates, 11.4.2 Pragmas Assert and Assertion_Policy. https://www.adaic.org/resources/add_content/standards/22rm/html/RM-3-2-4.html ↩

  30. Ada 2022 Reference Manual 7.5 Limited Types, 7.6 User-Defined Assignment and Finalization. https://www.adaic.org/resources/add_content/standards/22rm/html/RM-7-5.html ↩

  31. Ada 2022 Reference Manual 3.3 Objects and Named Numbers, 특히 constant/variable view 규칙. https://www.adaic.org/resources/add_content/standards/22rm/html/RM-3-3.html ↩

  32. Ada 2022 Reference Manual 8.2 Scope of Declarations, 8.3 Visibility. https://www.adaic.org/resources/add_content/standards/22rm/html/RM-8-2.html ↩

  33. Ada 2022 Reference Manual 7.6.1 Completion and Finalization. https://www.adaic.org/resources/add_content/standards/22rm/html/RM-7-6-1.html ↩

  34. Ada 2022 Reference Manual 4.4 Expressions와 4.5 Operators and Expression Evaluation. https://www.adaic.org/resources/add_content/standards/22rm/html/RM-4-4.html, https://www.adaic.org/resources/add_content/standards/22rm/html/RM-4-5.html ↩

  35. Ada 2022 Reference Manual 4.5 Operators and Expression Evaluation. 연산자 operand와 function-call parameter의 평가 순서는 별도 규칙이 없는 한 임의일 수 있다. ↩

  36. Ada 2022 Reference Manual 4.5.5 Multiplying Operators와 4.5.6 Highest Precedence Operators. https://www.adaic.org/resources/add_content/standards/22rm/html/RM-4-5-5.html ↩

  37. Ada 2022 Reference Manual 4.5.1 Logical Operators and Short-circuit Control Forms. https://www.adaic.org/resources/add_content/standards/22rm/html/RM-4-5-1.html ↩ ↩2

  38. Ada 2022 Reference Manual 4.5.2 Relational Operators and Membership Tests. https://www.adaic.org/resources/add_content/standards/22rm/html/RM-4-5-2.html ↩

  39. Ada 2022 Reference Manual 4.5의 operator precedence 표와 각 operator class 절. ↩

  40. Ada 2022 Reference Manual 4.7 Qualified Expressions. https://www.adaic.org/resources/add_content/standards/22rm/html/RM-4-7.html ↩

  41. Ada 2022 Reference Manual 4.5.7 Conditional Expressions. Conditional/quantified expression은 Ada 2012에서 추가되었고 Ada 2022 RM에 통합되어 있다. https://www.adaic.org/resources/add_content/standards/22rm/html/RM-4-5-7.html ↩

  42. Ada 2022 Reference Manual 4.5.10 Reduction Expressions. Reduce(Reducer, Initial_Value)와 Parallel_Reduce(Reducer, Initial_Value)가 language-defined reduction attribute로 정의된다. https://www.adaic.org/resources/add_content/standards/22rm/html/RM-4-5-10.html ↩

  43. Ada 2022 Reference Manual 4.5.8 Quantified Expressions. Ada 2012 Rationale도 conditional expression과 quantified expression을 Ada 2012의 새 expression form으로 설명한다. https://www.adaic.org/resources/add_content/standards/22rm/html/RM-4-5-8.html ↩

  44. Ada 2022 Reference Manual 5.1 Simple and Compound Statements - Sequences of Statements. https://www.adaic.org/resources/add_content/standards/22rm/html/RM-5-1.html ↩

  45. Ada 2022 Reference Manual 5.6 Block Statements. https://www.adaic.org/resources/add_content/standards/22rm/html/RM-5-6.html ↩

  46. Ada 2022 Reference Manual 5.6.1 Parallel Block Statements. https://docs.adacore.com/live/wave/arm05/html/arm05/arm22-5-6-1.html ↩

  47. Ada 2022 Reference Manual 5.3 If Statements. https://www.adaic.org/resources/add_content/standards/22rm/html/RM-5-3.html ↩

  48. Ada 2022 Reference Manual 5.4 Case Statements. https://www.adaic.org/resources/add_content/standards/22rm/html/RM-5-4.html ↩

  49. Ada 2022 Reference Manual 5.5 Loop Statements. https://www.adaic.org/resources/add_content/standards/22rm/html/RM-5-5.html ↩

  50. Ada 2022 Reference Manual 5.5.1 User-Defined Iterator Types, 5.5.2 Generalized Loop Iteration. https://www.adaic.org/resources/add_content/standards/22rm/html/RM-5-5-1.html, https://www.adaic.org/resources/add_content/standards/22rm/html/RM-5-5-2.html ↩

  51. Ada 2022 Reference Manual 5.5 Loop Statements, parallel iteration scheme. https://www.adaic.org/resources/add_content/standards/22rm/html/RM-5-5.html ↩

  52. Ada 2022 Reference Manual 5.5, iterator_filter. https://www.adaic.org/resources/add_content/standards/22rm/html/RM-5-5.html ↩

  53. Ada 2022 Reference Manual 5.5.3 Procedural Iterators. https://www.adaic.org/resources/add_content/standards/22rm/html/RM-5-5-3.html ↩

  54. Ada 2022 Reference Manual 5.1 및 5.8 Goto Statements. https://docs.adacore.com/live/wave/arm22/html/arm22/arm22-5-8.html ↩

  55. Ada 2022 Reference Manual 7.1 Package Specifications and Declarations, 7.2 Package Bodies. https://www.adaic.org/resources/add_content/standards/22rm/html/RM-7-1.html, https://www.adaic.org/resources/add_content/standards/22rm/html/RM-7-2.html ↩

  56. GCC 14.2 GNAT User’s Guide, File Naming Rules. 기본 규칙은 full expanded unit name의 점을 하이픈으로 바꾸고 소문자로 만들며 .ads/.adb를 사용한다. subunit 예로 main-sub.adb도 제시한다. https://gcc.gnu.org/onlinedocs/gcc-14.2.0/gnat_ugn/File-Naming-Rules.html ↩

  57. Ada 2022 Reference Manual 8.4 Use Clauses, 10.1.2 Context Clauses - With Clauses. https://www.adaic.org/resources/add_content/standards/22rm/html/RM-8-4.html, https://www.adaic.org/resources/add_content/standards/22rm/html/RM-10-1-2.html ↩

  58. Ada 2022 Reference Manual 8.5 Renaming Declarations. https://www.adaic.org/resources/add_content/standards/22rm/html/RM-8-5.html ↩

  59. Ada 2022 Reference Manual 7.3 Private Types and Private Extensions, 7.3.1 Private Operations. https://www.adaic.org/resources/add_content/standards/22rm/html/RM-7-3.html ↩

  60. Ada 2022 Reference Manual 10.1.1 Compilation Units - Library Units. https://www.adaic.org/resources/add_content/standards/22rm/html/RM-10-1-1.html ↩

  61. Ada 2022 Reference Manual 10.1.3 Subunits of Compilation Units. https://www.adaic.org/resources/add_content/standards/22rm/html/RM-10-1-3.html ↩

  62. Ada 2022 Reference Manual 10.2 Program Execution, 10.2.1 Elaboration Control. https://www.adaic.org/resources/add_content/standards/22rm/html/RM-10-2.html, https://www.adaic.org/resources/add_content/standards/22rm/html/RM-10-2-1.html ↩

  63. Ada 2022 Reference Manual 10.1.1 Compilation Units - Library Units, 8.2 Scope of Declarations. public child의 visible part는 parent private part를 노출하지 않으며, public child의 private part/body와 private descendant에서는 더 넓은 parent-private visibility가 허용된다. https://www.adaic.org/resources/add_content/standards/22rm/html/RM-10-1-1.html, https://www.adaic.org/resources/add_content/standards/22rm/html/RM-8-2.html ↩

  64. Ada 2022 Reference Manual 12 Generic Units, 12.1 Generic Declarations, 12.3 Generic Instantiation. https://www.adaic.org/resources/add_content/standards/22rm/html/RM-12.html, https://www.adaic.org/resources/add_content/standards/22rm/html/RM-12-1.html, https://www.adaic.org/resources/add_content/standards/22rm/html/RM-12-3.html ↩

  65. GCC 14.2 GNAT User’s Guide, Generating Object Files와 source dependency 설명. GNAT은 generic instantiation을 macro expansion으로 처리하며 generic unit 자체의 object는 dependency/elaboration용 작은 dummy object가 될 수 있다고 설명한다. https://gcc.gnu.org/onlinedocs/gcc-14.2.0/gnat_ugn/Generating-Object-Files.html ↩

  66. Ada 2022 Reference Manual 12.1 Generic Declarations, 12.2 Generic Bodies. https://www.adaic.org/resources/add_content/standards/22rm/html/RM-12-1.html, https://www.adaic.org/resources/add_content/standards/22rm/html/RM-12-2.html ↩

  67. Ada 2022 Reference Manual 12.5 Formal Types와 12.5.1~12.5.5. https://www.adaic.org/resources/add_content/standards/22rm/html/RM-12-5.html ↩

  68. Ada 2022 Reference Manual 12.4 Formal Objects. https://www.adaic.org/resources/add_content/standards/22rm/html/RM-12-4.html ↩

  69. Ada 2022 Reference Manual 12.6 Formal Subprograms. https://www.adaic.org/resources/add_content/standards/22rm/html/RM-12-6.html ↩

  70. Ada 2022 Reference Manual 12.7 Formal Packages. https://www.adaic.org/resources/add_content/standards/22rm/html/RM-12-7.html ↩

  71. Ada 2022 Reference Manual 12.3 Generic Instantiation. https://www.adaic.org/resources/add_content/standards/22rm/html/RM-12-3.html ↩

  72. Ada 2022 Reference Manual 13.14 Freezing Rules. generic instantiation의 occurrence가 freezing을 유발하는 규칙을 정의한다. https://www.adaic.org/resources/add_content/standards/22rm/html/RM-13-14.html ↩

  73. Ada 2022 Reference Manual 3.9 Tagged Types and Type Extensions, 3.9.1~3.9.4. https://www.adaic.org/resources/add_content/standards/22rm/html/RM-3-9.html ↩

  74. Ada 2022 RM 3.9(3), 3.9 Notes. tagged object의 run-time tag와 object 생성 후 tag 불변성을 정의한다. ↩

  75. Ada 2022 RM 3.9.1 Type Extensions. ↩

  76. Ada 2022 RM 3.2.3 Classification of Operations. primitive operation과 user-defined primitive subprogram의 범위를 정의한다. https://www.adaic.org/resources/add_content/standards/22rm/html/RM-3-2-3.html ↩

  77. Ada 2022 RM 3.9.2 Dispatching Operations of Tagged Types. explicit primitive subprogram declaration의 freezing 제한과 controlling operand/result 규칙을 정의한다. https://www.adaic.org/resources/add_content/standards/22rm/html/RM-3-9-2.html ↩

  78. Ada 2022 RM 3.9.2. primitive subprogram, controlling operand/result, statically/dynamically determined controlling tag를 정의한다. ↩

  79. Ada 2022 RM 4.1.3 Selected Components의 prefixed view 규칙. 같은 tagged prefixed-view 규칙은 Ada 2005 RM에도 존재한다. https://www.adaic.org/resources/add_content/standards/22rm/html/RM-4-1-3.html ↩

  80. Ada 2022 RM 3.9의 S'Class, S'Tag, X'Tag attribute 정의. https://www.adaic.org/resources/add_content/standards/22rm/html/RM-3-9.html ↩

  81. Ada 2022 RM 3.9.3 Abstract Types and Subprograms. https://www.adaic.org/resources/add_content/standards/22rm/html/RM-3-9-3.html ↩

  82. Ada 2022 RM 3.9.4 Interface Types. interface의 abstract/tagged 성격, component 금지, primitive subprogram의 abstract/null 제한과 task/protected/synchronized interface 규칙을 정의한다. https://www.adaic.org/resources/add_content/standards/22rm/html/RM-3-9-4.html ↩

  83. Ada 2022 Reference Manual 11.4 Exception Handling. exception occurrence가 발생하면 정상 실행이 중단되고 적용 가능한 handler를 찾거나 바깥 실행으로 propagation한다. https://www.adaic.org/resources/add_content/standards/22rm/html/RM-11-4.html ↩

  84. Ada 2022 Reference Manual 11.1 Exception Declarations. Standard의 predefined exceptions와 generic instance별 exception identity를 정의한다. https://www.adaic.org/resources/add_content/standards/22rm/html/RM-11-1.html ↩ ↩2

  85. Ada 2022 Reference Manual 11.3 Raise Statements and Raise Expressions. https://www.adaic.org/resources/add_content/standards/22rm/html/RM-11-3.html ↩

  86. Ada 2022 Reference Manual 11.2 Exception Handlers. handler choice, choice parameter와 others의 위치 규칙을 정의한다. https://www.adaic.org/resources/add_content/standards/22rm/html/RM-11-2.html ↩

  87. Ada 2022 Reference Manual 11.4.1 The Package Exceptions. https://www.adaic.org/resources/add_content/standards/22rm/html/RM-11-4-1.html ↩

  88. Ada 2022 Reference Manual 8.6 The Context of Overload Resolution. subtype은 overload resolution에 사용되지 않으며 constraint 위반은 실행 중 exception을 발생시킬 수 있다고 명시한다. https://www.adaic.org/resources/add_content/standards/22rm/html/RM-8-6.html ↩

  89. GPR Reference Manual 27.0w, GPRbuild Reference. 현재 legacy/GPRbuild2 engine 선택, main source와 build execution을 설명한다. https://docs.adacore.com/live/wave/gprbuild/html/gpr_reference/gprbuild.html ↩ ↩2

  90. GPR User Guide 27.0w, Your First Project. Source_Dirs, Languages, Object_Dir, Exec_Dir, Main의 기본 동작을 설명한다. https://docs.adacore.com/live/wave/gprbuild/html/gpr_user_guide/first_project.html ↩

  91. GPR Reference Manual 27.0w, Project Kinds. library qualifier, Library_Name/Library_Dir, default static kind와 directory 제약을 설명한다. https://docs.adacore.com/live/wave/gprbuild/html/gpr_reference/project_kinds.html ↩

  92. GPR Reference Manual 27.0w, Common Command-Line Options. project 자동 검색, -P, -X, --config, --autoconf, --target을 설명한다. https://docs.adacore.com/live/wave/gprbuild/html/gpr_reference/common_options.html ↩ ↩2

  93. GPR Reference Manual 27.0w, GPRclean Reference. -r, -n 등 project-aware clean 범위를 설명한다. https://docs.adacore.com/live/wave/gprbuild/html/gpr_reference/gprclean.html ↩

  94. GCC 14.2.0 GNAT User’s Guide, GNAT Compiler Switches. -gnatw... warning control과 -gnatW8 wide-character encoding을 구분한다. https://gcc.gnu.org/onlinedocs/gcc-14.2.0/gnat_ugn.pdf ↩

  95. GPR User Guide 27.0w, Scenarios. external, -X와 typed scenario variable을 설명한다. https://docs.adacore.com/live/wave/gprbuild/html/gpr_user_guide/scenarios.html ↩

  96. GPR Reference Manual 27.0w, GPRconfig Reference. compiler 탐색과 .cgpr configuration project 생성을 설명한다. https://docs.adacore.com/live/wave/gprbuild/html/gpr_reference/gprconfig.html ↩

  97. Ada 2022 Reference Manual B.1 Interfacing Aspects. Import, Export, Convention, External_Name, Link_Name, imported object initialization, callback convention과 erroneous-execution 책임을 정의한다. https://www.adaic.org/resources/add_content/standards/22rm/html/RM-B-1.html ↩ ↩2

  98. Ada 2022 Reference Manual B.3 Interfacing with C and C++. Interfaces.C, C_Pass_By_Copy, C_Variadic_n과 C interface correspondence를 정의한다. https://www.adaic.org/resources/add_content/standards/22rm/html/RM-B-3.html ↩

  99. Ada 2022 Reference Manual B.3.1 The Package Interfaces.C.Strings. allocation·NUL termination·Free ownership과 erroneous-execution 조건을 정의한다. https://docs.adacore.com/live/wave/arm05/html/arm05/arm22-B-3-1.html ↩

  100. GCC 14.2.0 GNAT User’s Guide, Generating Ada Bindings for C and C++ headers. generator의 한계, -fdump-ada-spec[-slim], C++ header의 g++ 사용을 설명한다. https://gcc.gnu.org/onlinedocs/gcc-14.2.0/gnat_ugn/Generating-Ada-Bindings-for-C-and-C_002b_002b-headers.html ↩

  101. Ada 2022 Reference Manual B.5 Interfacing with Fortran. Interfaces.Fortran, Fortran convention, parameter correspondence와 column-major matrix 예제를 정의한다. https://www.adaic.org/resources/add_content/standards/22rm/html/RM-B-5.html ↩

  102. GNAT Reference Manual, Interfacing to C++. CPP convention과 GNAT-specific C++ class/constructor interface pragmas를 설명한다. https://gcc.gnu.org/onlinedocs/gnat_rm/Interfacing-to-C_002b_002b.html ↩