ahartikainen: We need to merge #423 before this, so typing will pass.
codecov-commenter: # [Codecov](https://codecov.io/gh/stan-dev/httpstan/pull/424?src=pr&el=h1) Report
> Merging [#424](https://codecov.io/gh/stan-dev/httpstan/pull/424?src=pr&el=desc) into [main](https://codecov.io/gh/stan-dev/httpstan/commit/1fd4ebee640d972d77723f2fcee02a391ef0f1ce&el=desc) will **decrease** coverage by `0.71%`.
> The diff coverage is `100.00%`.

[![Impacted file tree graph](https://codecov.io/gh/stan-dev/httpstan/pull/424/graphs/tree.svg?width=650&height=150&src=pr&token=4hDER1kyIE)](https://codecov.io/gh/stan-dev/httpstan/pull/424?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##             main     #424      +/-   ##
==========================================
- Coverage   94.76%   94.05%   -0.72%     
==========================================
  Files          15       15              
  Lines         611      622      +11     
==========================================
+ Hits          579      585       +6     
- Misses         32       37       +5     
```


| [Impacted Files](https://codecov.io/gh/stan-dev/httpstan/pull/424?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [httpstan/compile.py](https://codecov.io/gh/stan-dev/httpstan/pull/424/diff?src=pr&el=tree#diff-aHR0cHN0YW4vY29tcGlsZS5weQ==) | `100.00% <100.00%> (ø)` | |
| [httpstan/models.py](https://codecov.io/gh/stan-dev/httpstan/pull/424/diff?src=pr&el=tree#diff-aHR0cHN0YW4vbW9kZWxzLnB5) | `98.78% <100.00%> (-1.22%)` | :arrow_down: |
| [httpstan/schemas.py](https://codecov.io/gh/stan-dev/httpstan/pull/424/diff?src=pr&el=tree#diff-aHR0cHN0YW4vc2NoZW1hcy5weQ==) | `97.26% <100.00%> (+0.03%)` | :arrow_up: |
| [httpstan/views.py](https://codecov.io/gh/stan-dev/httpstan/pull/424/diff?src=pr&el=tree#diff-aHR0cHN0YW4vdmlld3MucHk=) | `87.76% <100.00%> (-2.46%)` | :arrow_down: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/stan-dev/httpstan/pull/424?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/stan-dev/httpstan/pull/424?src=pr&el=footer). Last update [1fd4ebe...ccd8538](https://codecov.io/gh/stan-dev/httpstan/pull/424?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

diff --git a/httpstan/compile.py b/httpstan/compile.py
index ca915f5a..2b05c46c 100644
--- a/httpstan/compile.py
+++ b/httpstan/compile.py
@@ -3,10 +3,10 @@ import importlib.resources
 import os
 import subprocess
 import tempfile
-from typing import List, Union
+from typing import List, Tuple, Union
 
 
-def compile(program_code: str, stan_model_name: str) -> str:
+def compile(program_code: str, stan_model_name: str) -> Tuple[str, str]:
     """Return C++ code for Stan model specified by `program_code`.
 
     Arguments:
@@ -14,7 +14,7 @@ def compile(program_code: str, stan_model_name: str) -> str:
         stan_model_name
 
     Returns:
-        str: C++ code
+        (str, str): C++ code, warnings
 
     Raises:
         ValueError: Syntax or semantic error in program code.
@@ -32,9 +32,7 @@ def compile(program_code: str, stan_model_name: str) -> str:
                 fh.name,
             ]
             completed_process = subprocess.run(run_args, capture_output=True, timeout=1)
-    stderr = completed_process.stderr.decode()
-    if stderr:
-        # `strip` unnecessary newlines in stanc error message
-        raise ValueError(stderr.strip())
-
-    return completed_process.stdout.decode().strip()
+    stderr = completed_process.stderr.decode().strip()
+    if completed_process.returncode != 0:
+        raise ValueError(stderr)
+    return completed_process.stdout.decode().strip(), stderr
diff --git a/httpstan/models.py b/httpstan/models.py
index 48bb7d7d..13e3a0f8 100644
--- a/httpstan/models.py
+++ b/httpstan/models.py
@@ -95,6 +95,20 @@ def import_services_extension_module(model_name: str) -> ModuleType:
     return module
 
 
+async def stanc_warnings(program_code: str) -> str:
+    """Call external `stanc` program, returning warnings.
+
+    Raises an exception if `program_code` does not compile.
+
+    """
+    # C++ model name must be a valid C++ identifier. Cannot start with number.
+    stan_model_name = f"model_{calculate_model_name(program_code).split('/')[1]}"
+    _, warnings = await asyncio.get_event_loop().run_in_executor(
+        None, httpstan.compile.compile, program_code, stan_model_name
+    )
+    return warnings
+
+
 async def generate_model_cpp_code(program_code: str) -> str:
     """Call external `stanc` program to generate C++ code from `program_code`.
 
@@ -112,7 +126,7 @@ async def generate_model_cpp_code(program_code: str) -> str:
     # C++ model name must be a valid C++ identifier. Cannot start with number.
     stan_model_name = f"model_{model_name.split('/')[1]}"
     logger.info(f"generating cpp for `{model_name}`.")
-    cpp_code = await asyncio.get_event_loop().run_in_executor(
+    cpp_code, _ = await asyncio.get_event_loop().run_in_executor(
         None, httpstan.compile.compile, program_code, stan_model_name
     )
     return cpp_code
diff --git a/httpstan/schemas.py b/httpstan/schemas.py
index 85627899..c32c4019 100644
--- a/httpstan/schemas.py
+++ b/httpstan/schemas.py
@@ -52,6 +52,7 @@ class CreateModelRequest(marshmallow.Schema):
 class Model(marshmallow.Schema):
     name = fields.String(required=True)
     compiler_output = fields.String(required=True)
+    stanc_warnings = fields.String(required=True)
 
 
 class Data(marshmallow.Schema):
diff --git a/httpstan/views.py b/httpstan/views.py
index 1988aa6f..2a70cfb6 100644
--- a/httpstan/views.py
+++ b/httpstan/views.py
@@ -85,6 +85,15 @@ async def handle_models(request: aiohttp.web.Request) -> aiohttp.web.Response:
 
     program_code = args["program_code"]
     model_name = httpstan.models.calculate_model_name(program_code)
+
+    # compile `program_code` twice, first time only to check for errors and get warnings
+    try:
+        stanc_warnings = await httpstan.models.stanc_warnings(program_code)
+    except ValueError as exc:
+        message, status = f"Exception while compiling `program_code`: `{repr(exc)}`", 400
+        logger.critical(message)
+        return aiohttp.web.json_response(_make_error(message, status=status), status=status)
+
     try:
         httpstan.models.import_services_extension_module(model_name)
     except KeyError:
@@ -101,7 +110,9 @@ async def handle_models(request: aiohttp.web.Request) -> aiohttp.web.Response:
     else:
         logger.info(f"Found Stan model in cache (`{model_name}`).")
     compiler_output = httpstan.cache.services_extension_module_compiler_output(model_name)
-    response_dict = schemas.Model().load({"name": model_name, "compiler_output": compiler_output})
+    response_dict = schemas.Model().load(
+        {"name": model_name, "compiler_output": compiler_output, "stanc_warnings": stanc_warnings}
+    )
     return aiohttp.web.json_response(response_dict, status=201)
 
 

Pedantic mode warnings not returned to user
(This assumes stanc3 is being used.)

The pedantic mode warnings are useful. They need to be accessible.
diff --git a/tests/test_compile.py b/tests/test_compile.py
index 6dc58db3..a0bc60ba 100644
--- a/tests/test_compile.py
+++ b/tests/test_compile.py
@@ -8,8 +8,8 @@ import httpstan.compile
 
 def test_compile() -> None:
     program_code = "parameters {real y;} model {y ~ normal(0,1);}"
-    cpp_code = httpstan.compile.compile(program_code, "test_model")
-    assert "Code generated by stanc" in cpp_code
+    cpp_code, warnings = httpstan.compile.compile(program_code, "test_model")
+    assert "Code generated by stanc" in cpp_code and warnings == ""
 
 
 def test_compile_syntax_error() -> None:
@@ -25,8 +25,14 @@ def test_compile_semantic_error() -> None:
         httpstan.compile.compile(program_code, "test_model")
 
 
+def test_compile_warning() -> None:
+    program_code = """model { real x; x <- 5; }"""
+    cpp_code, warnings = httpstan.compile.compile(program_code, "test_model")
+    assert cpp_code and "assignment operator <- is deprecated in the Stan language" in warnings
+
+
 @pytest.mark.asyncio
-async def test_compile_invalid_distribution(api_url: str) -> None:
+async def test_build_invalid_distribution(api_url: str) -> None:
     """Check that compiler error is returned to client."""
 
     program_code = "parameters {real z;} model {z ~ no_such_distribution();}"
@@ -41,7 +47,7 @@ async def test_compile_invalid_distribution(api_url: str) -> None:
 
 
 @pytest.mark.asyncio
-async def test_compile_unknown_arg(api_url: str) -> None:
+async def test_build_unknown_arg(api_url: str) -> None:
     """Check that compiler error is returned to client.
 
     This error can be detected by schema validation.
@@ -56,3 +62,33 @@ async def test_compile_unknown_arg(api_url: str) -> None:
             assert resp.status == 422
             response_payload = await resp.json()
     assert "unknown_arg" in response_payload
+
+
+@pytest.mark.asyncio
+async def test_build_warning(api_url: str) -> None:
+    """Test building which succeeds but which generates a warning."""
+
+    # prints warning:
+    # assignment operator <- is deprecated in the Stan language; use = instead.
+    program_code = """
+    parameters {
+    real y;
+    }
+    model {
+    real x;
+    x <- 5;
+    }
+    """
+
+    payload = {"program_code": program_code}
+    models_url = f"{api_url}/models"
+    async with aiohttp.ClientSession() as session:
+        async with session.post(models_url, json=payload) as resp:
+            assert resp.status == 201
+            response_payload = await resp.json()
+    assert "compiler_output" in response_payload
+    assert "stanc_warnings" in response_payload
+    assert (
+        "assignment operator <- is deprecated in the Stan language; use = instead."
+        in response_payload["stanc_warnings"]
+    )
diff --git a/tests/test_schemas.py b/tests/test_schemas.py
index 882a9220..07f00ee4 100644
--- a/tests/test_schemas.py
+++ b/tests/test_schemas.py
@@ -6,7 +6,7 @@ import httpstan.schemas as schemas
 
 
 def test_model_schema() -> None:
-    result = schemas.Model().load({"name": "12345", "compiler_output": ""})
+    result = schemas.Model().load({"name": "12345", "compiler_output": "", "stanc_warnings": ""})
     assert result
 
 

pip install -e .[dev]
parse_log_pytest
requirements.txt
pytest pytest-cov pytest-asyncio pytest-mock mypy-protobuf
apt-get update apt-get install -y gcc
3.9
docs-requirements.txt
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
aiohappyeyeballs==2.6.1
aiohttp==3.11.14
aiosignal==1.3.2
alabaster==0.7.16
apispec==3.3.2
appdirs==1.4.4
astroid==3.3.9
async-timeout==5.0.1
attrs==25.3.0
babel==2.17.0
certifi==2025.1.31
chardet==5.2.0
charset-normalizer==3.4.1
coverage==7.8.0
Cython==0.29.37
deepmerge==2.0
docutils==0.16
exceptiongroup==1.2.2
frozenlist==1.5.0
-e git+https://github.com/stan-dev/httpstan.git@1fd4ebee640d972d77723f2fcee02a391ef0f1ce#egg=httpstan
idna==3.10
imagesize==1.4.1
iniconfig==2.1.0
Jinja2==3.1.6
jsonschema==4.17.3
jsonschema-spec==0.1.6
lazy-object-proxy==1.10.0
MarkupSafe==3.0.2
marshmallow==3.26.1
mistune==3.1.3
multidict==6.2.0
mypy-protobuf==3.6.0
numpy==1.26.4
openapi-schema-validator==0.4.4
openapi-spec-validator==0.5.7
packaging==24.2
pathable==0.4.4
picobox==4.0.0
pluggy==1.5.0
prance==23.6.21.0
propcache==0.3.1
protobuf==3.20.3
Pygments==2.19.1
pyrsistent==0.20.0
pytest==8.3.5
pytest-asyncio==0.26.0
pytest-cov==6.0.0
pytest-mock==3.14.0
PyYAML==6.0.2
requests==2.32.3
rfc3339-validator==0.1.4
ruamel.yaml==0.18.10
ruamel.yaml.clib==0.2.12
six==1.17.0
snowballstemmer==2.2.0
Sphinx==3.5.4
sphinx-autoapi==1.9.0
sphinx-rtd-theme==0.5.2
sphinx_mdinclude==0.6.0
sphinxcontrib-applehelp==2.0.0
sphinxcontrib-devhelp==2.0.0
sphinxcontrib-htmlhelp==2.1.0
sphinxcontrib-httpdomain==1.8.1
sphinxcontrib-jsmath==1.0.1
sphinxcontrib-openapi==0.8.4
sphinxcontrib-qthelp==2.0.0
sphinxcontrib-redoc==1.6.0
sphinxcontrib-serializinghtml==2.0.0
tomli==2.2.1
types-protobuf==5.29.1.20250315
typing_extensions==4.13.0
Unidecode==1.3.8
urllib3==2.3.0
webargs==5.5.3
yarl==1.18.3

name: httpstan
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - ca-certificates=2025.2.25=h06a4308_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.4.4=h6a678d5_1
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=3.0.16=h5eee18b_0
  - pip=25.0=py39h06a4308_0
  - python=3.9.21=he870216_1
  - readline=8.2=h5eee18b_0
  - setuptools=75.8.0=py39h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - tzdata=2025a=h04d1e81_0
  - wheel=0.45.1=py39h06a4308_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - aiohappyeyeballs==2.6.1
      - aiohttp==3.11.14
      - aiosignal==1.3.2
      - alabaster==0.7.16
      - apispec==3.3.2
      - appdirs==1.4.4
      - astroid==3.3.9
      - async-timeout==5.0.1
      - attrs==25.3.0
      - babel==2.17.0
      - certifi==2025.1.31
      - chardet==5.2.0
      - charset-normalizer==3.4.1
      - coverage==7.8.0
      - cython==0.29.37
      - deepmerge==2.0
      - docutils==0.16
      - exceptiongroup==1.2.2
      - frozenlist==1.5.0
      - httpstan==2.1.0
      - idna==3.10
      - imagesize==1.4.1
      - iniconfig==2.1.0
      - jinja2==3.1.6
      - jsonschema==4.17.3
      - jsonschema-spec==0.1.6
      - lazy-object-proxy==1.10.0
      - markupsafe==3.0.2
      - marshmallow==3.26.1
      - mistune==3.1.3
      - multidict==6.2.0
      - mypy-protobuf==3.6.0
      - numpy==1.26.4
      - openapi-schema-validator==0.4.4
      - openapi-spec-validator==0.5.7
      - packaging==24.2
      - pathable==0.4.4
      - picobox==4.0.0
      - pluggy==1.5.0
      - prance==23.6.21.0
      - propcache==0.3.1
      - protobuf==3.20.3
      - pygments==2.19.1
      - pyrsistent==0.20.0
      - pytest==8.3.5
      - pytest-asyncio==0.26.0
      - pytest-cov==6.0.0
      - pytest-mock==3.14.0
      - pyyaml==6.0.2
      - requests==2.32.3
      - rfc3339-validator==0.1.4
      - ruamel-yaml==0.18.10
      - ruamel-yaml-clib==0.2.12
      - six==1.17.0
      - snowballstemmer==2.2.0
      - sphinx==3.5.4
      - sphinx-autoapi==1.9.0
      - sphinx-mdinclude==0.6.0
      - sphinx-rtd-theme==0.5.2
      - sphinxcontrib-applehelp==2.0.0
      - sphinxcontrib-devhelp==2.0.0
      - sphinxcontrib-htmlhelp==2.1.0
      - sphinxcontrib-httpdomain==1.8.1
      - sphinxcontrib-jsmath==1.0.1
      - sphinxcontrib-openapi==0.8.4
      - sphinxcontrib-qthelp==2.0.0
      - sphinxcontrib-redoc==1.6.0
      - sphinxcontrib-serializinghtml==2.0.0
      - tomli==2.2.1
      - types-protobuf==5.29.1.20250315
      - typing-extensions==4.13.0
      - unidecode==1.3.8
      - urllib3==2.3.0
      - webargs==5.5.3
      - yarl==1.18.3
prefix: /opt/conda/envs/httpstan

tests/test_schemas.py::test_model_schema
tests/test_compile.py::test_compile tests/test_compile.py::test_compile_syntax_error tests/test_compile.py::test_compile_semantic_error tests/test_compile.py::test_compile_warning tests/test_compile.py::test_build_invalid_distribution tests/test_compile.py::test_build_unknown_arg tests/test_compile.py::test_build_warning
tests/test_schemas.py::test_data_schema tests/test_schemas.py::test_data_schema_invalid
httpstan/views.py httpstan/compile.py httpstan/schemas.py httpstan/models.py
httpstan/views.py httpstan/compile.py httpstan/schemas.py httpstan/models.py